feat(p6): kernel-client route methods + @insignia/iios-community-web SDK
Task 6.5: add createBinding/listBindings/simulateRoute/listRouteDecisions/ approveDecision/denyDecision to RestClient + RouteBinding/RouteDecision types. New @insignia/iios-community-web package (CommunityProvider, useBindings, useRoutePreview, useRouteDecisions). Boundary allowlist: community-web -> contracts + kernel-client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@insignia/iios-community-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@insignia/iios-kernel-client": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { CommunityProvider, useBindings, useRoutePreview, useRouteDecisions } from './react';
|
||||||
|
export type { RouteBinding, RouteDecision } from '@insignia/iios-kernel-client';
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { RestClient, type RouteBinding, type RouteDecision } from '@insignia/iios-kernel-client';
|
||||||
|
|
||||||
|
const CommunityContext = createContext<RestClient | null>(null);
|
||||||
|
|
||||||
|
export function CommunityProvider({
|
||||||
|
serviceUrl,
|
||||||
|
token,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
token: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]);
|
||||||
|
return <CommunityContext.Provider value={client}>{children}</CommunityContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useClient(): RestClient {
|
||||||
|
const c = useContext(CommunityContext);
|
||||||
|
if (!c) throw new Error('community hooks must be used within <CommunityProvider>');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Manage route bindings (origin→destination rules). */
|
||||||
|
export function useBindings(): {
|
||||||
|
bindings: RouteBinding[];
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
createBinding: (input: Partial<RouteBinding>) => Promise<RouteBinding>;
|
||||||
|
} {
|
||||||
|
const client = useClient();
|
||||||
|
const [bindings, setBindings] = useState<RouteBinding[]>([]);
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setBindings(await client.listBindings());
|
||||||
|
} catch {
|
||||||
|
/* transient */
|
||||||
|
}
|
||||||
|
}, [client]);
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
const createBinding = async (input: Partial<RouteBinding>) => {
|
||||||
|
const b = await client.createBinding(input);
|
||||||
|
await refresh();
|
||||||
|
return b;
|
||||||
|
};
|
||||||
|
return { bindings, refresh, createBinding };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview-first: simulate an interaction against the bindings (no send). */
|
||||||
|
export function useRoutePreview(): (interactionId: string, originChannelType: string, originRef?: string) => Promise<RouteDecision[]> {
|
||||||
|
const client = useClient();
|
||||||
|
return async (interactionId, originChannelType, originRef) =>
|
||||||
|
(await client.simulateRoute({ interactionId, originChannelType, originRef })).decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pending decisions + admin approve/deny. */
|
||||||
|
export function useRouteDecisions(opts?: { state?: string; pollMs?: number }): {
|
||||||
|
decisions: RouteDecision[];
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
approve: (id: string) => Promise<void>;
|
||||||
|
deny: (id: string) => Promise<void>;
|
||||||
|
} {
|
||||||
|
const client = useClient();
|
||||||
|
const [decisions, setDecisions] = useState<RouteDecision[]>([]);
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setDecisions(await client.listRouteDecisions(opts?.state));
|
||||||
|
} catch {
|
||||||
|
/* transient */
|
||||||
|
}
|
||||||
|
}, [client, opts?.state]);
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
const t = setInterval(() => void refresh(), opts?.pollMs ?? 3000);
|
||||||
|
return () => clearInterval(t);
|
||||||
|
}, [refresh, opts?.pollMs]);
|
||||||
|
const approve = async (id: string) => {
|
||||||
|
await client.approveDecision(id);
|
||||||
|
await refresh();
|
||||||
|
};
|
||||||
|
const deny = async (id: string) => {
|
||||||
|
await client.denyDecision(id);
|
||||||
|
await refresh();
|
||||||
|
};
|
||||||
|
return { decisions, refresh, approve, deny };
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["react"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
external: ['react', 'react-dom', '@insignia/iios-kernel-client'],
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { IngestInteractionRequest } from '@insignia/iios-contracts';
|
import type { IngestInteractionRequest } from '@insignia/iios-contracts';
|
||||||
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest } from './types';
|
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest, RouteBinding, RouteDecision } from './types';
|
||||||
|
|
||||||
export interface RestConfig {
|
export interface RestConfig {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
@@ -100,6 +100,30 @@ export class RestClient {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── routing (P6) ─────────────────────────────────────────────
|
||||||
|
async createBinding(input: Partial<RouteBinding>): Promise<RouteBinding> {
|
||||||
|
return this.post<RouteBinding>('/v1/routes/bindings', input);
|
||||||
|
}
|
||||||
|
async listBindings(): Promise<RouteBinding[]> {
|
||||||
|
const r = await fetch(this.url('/v1/routes/bindings'), { headers: this.headers() });
|
||||||
|
if (!r.ok) throw new Error(`listBindings ${r.status}`);
|
||||||
|
return (await r.json()) as RouteBinding[];
|
||||||
|
}
|
||||||
|
async simulateRoute(input: { interactionId: string; originChannelType: string; originRef?: string }): Promise<{ simulationId: string; decisions: RouteDecision[] }> {
|
||||||
|
return this.post('/v1/routes/simulate', input);
|
||||||
|
}
|
||||||
|
async listRouteDecisions(state?: string): Promise<RouteDecision[]> {
|
||||||
|
const r = await fetch(this.url(`/v1/routes/decisions${state ? `?state=${state}` : ''}`), { headers: this.headers() });
|
||||||
|
if (!r.ok) throw new Error(`listRouteDecisions ${r.status}`);
|
||||||
|
return (await r.json()) as RouteDecision[];
|
||||||
|
}
|
||||||
|
async approveDecision(id: string): Promise<RouteDecision> {
|
||||||
|
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/approve`, {});
|
||||||
|
}
|
||||||
|
async denyDecision(id: string): Promise<RouteDecision> {
|
||||||
|
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/deny`, {});
|
||||||
|
}
|
||||||
|
|
||||||
private async post<T>(path: string, body: unknown): Promise<T> {
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
||||||
const r = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
|
const r = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
|
||||||
if (!r.ok) throw new Error(`POST ${path} ${r.status}`);
|
if (!r.ok) throw new Error(`POST ${path} ${r.status}`);
|
||||||
|
|||||||
@@ -77,6 +77,31 @@ export interface CallbackRequest {
|
|||||||
preferChannel: string;
|
preferChannel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteBinding {
|
||||||
|
id: string;
|
||||||
|
originChannelType: string;
|
||||||
|
originRef?: string | null;
|
||||||
|
destinationChannelType: string;
|
||||||
|
destinationRef?: string | null;
|
||||||
|
restrictionProfile?: string | null;
|
||||||
|
mode: string;
|
||||||
|
outputFormat: string;
|
||||||
|
requiresReview: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteDecision {
|
||||||
|
id: string;
|
||||||
|
interactionId: string;
|
||||||
|
routeBindingId: string;
|
||||||
|
decisionState: string;
|
||||||
|
reasonCodes: string[];
|
||||||
|
previewPayload: { text?: string; destinationRef?: string | null; format?: string } | unknown;
|
||||||
|
outputFormat: string;
|
||||||
|
executed: boolean;
|
||||||
|
routeBinding?: RouteBinding;
|
||||||
|
}
|
||||||
|
|
||||||
/** Minimal socket surface so the facade can be unit-tested with a fake. */
|
/** Minimal socket surface so the facade can be unit-tested with a fake. */
|
||||||
export interface SocketLike {
|
export interface SocketLike {
|
||||||
on(event: string, handler: (...args: unknown[]) => void): unknown;
|
on(event: string, handler: (...args: unknown[]) => void): unknown;
|
||||||
|
|||||||
Generated
+19
@@ -120,6 +120,25 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../iios-contracts
|
version: link:../iios-contracts
|
||||||
|
|
||||||
|
packages/iios-community-web:
|
||||||
|
dependencies:
|
||||||
|
'@insignia/iios-kernel-client':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../iios-kernel-client
|
||||||
|
devDependencies:
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.17
|
||||||
|
react:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.7
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.3.5
|
||||||
|
version: 8.5.1(jiti@2.7.0)(postcss@8.5.16)(typescript@5.9.3)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages/iios-contracts: {}
|
packages/iios-contracts: {}
|
||||||
|
|
||||||
packages/iios-inbox-web:
|
packages/iios-inbox-web:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const ALLOWED = {
|
|||||||
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit', '@insignia/iios-adapter-sdk'],
|
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit', '@insignia/iios-adapter-sdk'],
|
||||||
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||||
'@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
'@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||||
|
'@insignia/iios-community-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||||
'@insignia/iios-support-web': [
|
'@insignia/iios-support-web': [
|
||||||
'@insignia/iios-contracts',
|
'@insignia/iios-contracts',
|
||||||
'@insignia/iios-kernel-client',
|
'@insignia/iios-kernel-client',
|
||||||
|
|||||||
Reference in New Issue
Block a user