first commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type {
|
||||
AgentAvailability,
|
||||
Bootstrap,
|
||||
Channel,
|
||||
InboxItem,
|
||||
InboxState,
|
||||
Message,
|
||||
Notification,
|
||||
SearchResult,
|
||||
Ticket,
|
||||
TicketState,
|
||||
User,
|
||||
} from "@lynkd/messaging-inbox-sdk";
|
||||
|
||||
/**
|
||||
* The contract every BFF handler talks to. This is the ONE seam between the UI
|
||||
* and a real backend. Two implementations:
|
||||
* - MockBackend (default) — the in-memory store
|
||||
* - IiosBackend — proxies the real IIOS service
|
||||
* Selected by BFF_BACKEND ("mock" | "iios"). All methods are async so a network
|
||||
* backend drops in without changing the routes.
|
||||
*/
|
||||
export interface Backend {
|
||||
bootstrap(): Promise<Bootstrap>;
|
||||
listChannels(): Promise<Channel[]>;
|
||||
createChannel(input: { name: string; kind?: string; topic?: string; memberIds?: string[] }): Promise<Channel>;
|
||||
addMember(channelId: string, userId: string): Promise<Channel | null>;
|
||||
removeMember(channelId: string, userId: string): Promise<Channel | null>;
|
||||
markRead(channelId: string): Promise<Channel | null>;
|
||||
setStatus(text: string | undefined, emoji: string | undefined, clearAt?: number): Promise<User>;
|
||||
updateMe(patch: { name?: string; title?: string; avatarColor?: string; avatarUrl?: string | null; presence?: string }): Promise<User>;
|
||||
channelMessages(channelId: string): Promise<Message[]>;
|
||||
thread(channelId: string, parentId: string): Promise<{ parent: Message | null; replies: Message[] }>;
|
||||
send(channelId: string, body: string, opts?: { parentId?: string; scheduledFor?: number; attachments?: import("@lynkd/messaging-inbox-sdk").Attachment[]; threadRootId?: string | null }): Promise<Message>;
|
||||
editMessage(channelId: string, messageId: string, body: string): Promise<Message | null>;
|
||||
deleteMessage(channelId: string, messageId: string): Promise<{ ok: boolean }>;
|
||||
react(channelId: string, messageId: string, emoji: string): Promise<Message | null>;
|
||||
pin(channelId: string, messageId: string): Promise<Message | null>;
|
||||
save(channelId: string, messageId: string): Promise<Message | null>;
|
||||
savedMessages(): Promise<Message[]>;
|
||||
threadParents(): Promise<Message[]>;
|
||||
listInbox(state?: InboxState): Promise<InboxItem[]>;
|
||||
patchInbox(id: string, state: InboxState): Promise<InboxItem | null>;
|
||||
listTickets(scope?: string): Promise<Ticket[]>;
|
||||
getTicket(id: string): Promise<Ticket | null>;
|
||||
patchTicket(id: string, state: TicketState): Promise<Ticket | null>;
|
||||
replyTicket(id: string, body: string): Promise<Ticket | null>;
|
||||
availability(): Promise<AgentAvailability[]>;
|
||||
search(q: string): Promise<SearchResult[]>;
|
||||
notifications(): Promise<Notification[]>;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Backend } from "./Backend";
|
||||
import { mockBackend } from "./mock";
|
||||
import { iios } from "@/lib/iios/client";
|
||||
import { toInboxItem, toMessage, toTicket } from "@/lib/iios/map";
|
||||
import manifest from "@/lib/iios/seed-manifest.json";
|
||||
import type { Bootstrap, InboxState, Message, TicketState } from "@lynkd/messaging-inbox-sdk";
|
||||
|
||||
// If the IIOS DB has been seeded (scripts/seed-iios.mjs), a manifest maps the real
|
||||
// IIOS thread ids to a channel directory so the sidebar shows live data. Otherwise
|
||||
// bootstrap/channels fall back to the mock store (IIOS has no channel-list endpoint).
|
||||
const seeded = (manifest as any)?.seeded === true;
|
||||
const seededBootstrap = () => manifest as unknown as Bootstrap;
|
||||
|
||||
/**
|
||||
* IIOS-backed implementation. Endpoints that map cleanly to the IIOS API are wired
|
||||
* for real (messages, threads, inbox, tickets). Concepts IIOS doesn't expose 1:1 yet
|
||||
* (bootstrap/users/channels, reactions, search, notifications, availability) delegate
|
||||
* to the mock backend so the UI keeps working — replace these as IIOS grows portal/
|
||||
* directory endpoints. See docs/IIOS_INTEGRATION.md.
|
||||
*/
|
||||
export const iiosBackend: Backend = {
|
||||
// --- bootstrap/channels: from the seed manifest if present, else mock ---
|
||||
bootstrap: () => (seeded ? Promise.resolve(seededBootstrap()) : mockBackend.bootstrap()),
|
||||
listChannels: () => (seeded ? Promise.resolve(seededBootstrap().channels) : mockBackend.listChannels()),
|
||||
createChannel: (input) => mockBackend.createChannel(input),
|
||||
addMember: (channelId, userId) => mockBackend.addMember(channelId, userId),
|
||||
removeMember: (channelId, userId) => mockBackend.removeMember(channelId, userId),
|
||||
markRead: (channelId) => mockBackend.markRead(channelId),
|
||||
setStatus: (text, emoji, clearAt) => mockBackend.setStatus(text, emoji, clearAt),
|
||||
updateMe: (patch) => mockBackend.updateMe(patch),
|
||||
availability: () => mockBackend.availability(),
|
||||
notifications: () => mockBackend.notifications(),
|
||||
|
||||
// IIOS has no reaction/pin/save/edit/delete endpoints — keep local behavior
|
||||
react: (channelId, messageId, emoji) => mockBackend.react(channelId, messageId, emoji),
|
||||
pin: (channelId, messageId) => mockBackend.pin(channelId, messageId),
|
||||
save: (channelId, messageId) => mockBackend.save(channelId, messageId),
|
||||
editMessage: (channelId, messageId, body) => mockBackend.editMessage(channelId, messageId, body),
|
||||
deleteMessage: (channelId, messageId) => mockBackend.deleteMessage(channelId, messageId),
|
||||
savedMessages: () => mockBackend.savedMessages(),
|
||||
threadParents: () => mockBackend.threadParents(),
|
||||
|
||||
// IIOS has no cross-entity search endpoint documented — use local search
|
||||
search: (q) => mockBackend.search(q),
|
||||
|
||||
// --- wired to the real IIOS service ---
|
||||
async channelMessages(channelId) {
|
||||
const r = await iios.getThreadMessages(channelId);
|
||||
const list = Array.isArray(r) ? r : (r.messages ?? []);
|
||||
return list.filter((m: any) => !(m.parentInteractionId ?? m.parentId)).map((m: any) => toMessage(m, channelId));
|
||||
},
|
||||
|
||||
async thread(channelId, parentId) {
|
||||
const r = await iios.getThreadMessages(channelId);
|
||||
const list: Message[] = (Array.isArray(r) ? r : r.messages ?? []).map((m: any) => toMessage(m, channelId));
|
||||
return {
|
||||
parent: list.find((m) => m.id === parentId) ?? null,
|
||||
replies: list.filter((m) => m.parentId === parentId),
|
||||
};
|
||||
},
|
||||
|
||||
async send(channelId, body, opts) {
|
||||
const idem = `web-${channelId}-${body.length}-${Math.round((opts?.scheduledFor ?? 0) / 1000)}`;
|
||||
const r = await iios.sendMessage(channelId, body, idem);
|
||||
return toMessage(r.message ?? r, channelId);
|
||||
},
|
||||
|
||||
async listInbox(state) {
|
||||
const r = await iios.listInbox(state);
|
||||
const list = Array.isArray(r) ? r : r.items ?? [];
|
||||
return list.map(toInboxItem);
|
||||
},
|
||||
|
||||
async patchInbox(id, state: InboxState) {
|
||||
const r = await iios.patchInbox(id, state);
|
||||
return toInboxItem(r.item ?? r);
|
||||
},
|
||||
|
||||
async listTickets(scope) {
|
||||
const r = await iios.listTickets(scope);
|
||||
const list = Array.isArray(r) ? r : r.tickets ?? [];
|
||||
return list.map(toTicket);
|
||||
},
|
||||
|
||||
async getTicket(id) {
|
||||
// IIOS lists tickets; fetch and find (swap for a GET /tickets/:id when available)
|
||||
const r = await iios.listTickets("all");
|
||||
const list = Array.isArray(r) ? r : r.tickets ?? [];
|
||||
const found = list.find((t: any) => String(t.id) === id || String(t.ref) === id);
|
||||
return found ? toTicket(found) : null;
|
||||
},
|
||||
|
||||
async patchTicket(id, state: TicketState) {
|
||||
const r = await iios.patchTicket(id, state);
|
||||
return toTicket(r.ticket ?? r);
|
||||
},
|
||||
|
||||
async replyTicket(id, body) {
|
||||
// A ticket reply is a message on the ticket's linked thread.
|
||||
const t = await this.getTicket(id);
|
||||
if (t?.channelId) await iios.sendMessage(t.channelId, body);
|
||||
return this.getTicket(id);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Backend } from "./Backend";
|
||||
import { mockBackend } from "./mock";
|
||||
import { iiosBackend } from "./iios";
|
||||
|
||||
/**
|
||||
* Choose the BFF backend from env. Default "mock" (in-memory store). Set
|
||||
* BFF_BACKEND=iios (+ IIOS_URL etc.) to proxy the real IIOS service.
|
||||
* Importing the IIOS backend runs no network calls until a method is invoked
|
||||
* (the token is minted lazily), so the mock demo never needs IIOS present.
|
||||
*/
|
||||
export function getBackend(): Backend {
|
||||
return (process.env.BFF_BACKEND ?? "mock").toLowerCase() === "iios" ? iiosBackend : mockBackend;
|
||||
}
|
||||
|
||||
export type { Backend };
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Backend } from "./Backend";
|
||||
import { store } from "@/lib/mock/store";
|
||||
|
||||
/** The default backend: the in-memory mock store, wrapped as async. */
|
||||
export const mockBackend: Backend = {
|
||||
async bootstrap() {
|
||||
return store.bootstrap();
|
||||
},
|
||||
async listChannels() {
|
||||
return store.channels;
|
||||
},
|
||||
async createChannel(input) {
|
||||
return store.createChannel(input);
|
||||
},
|
||||
async addMember(channelId, userId) {
|
||||
return store.addMember(channelId, userId);
|
||||
},
|
||||
async removeMember(channelId, userId) {
|
||||
return store.removeMember(channelId, userId);
|
||||
},
|
||||
async markRead(channelId) {
|
||||
return store.markRead(channelId);
|
||||
},
|
||||
async setStatus(text, emoji, clearAt) {
|
||||
return store.setStatus(text, emoji, clearAt);
|
||||
},
|
||||
async updateMe(patch) {
|
||||
return store.updateMe(patch);
|
||||
},
|
||||
async channelMessages(channelId) {
|
||||
return store.channelMessages(channelId);
|
||||
},
|
||||
async thread(channelId, parentId) {
|
||||
return store.thread(channelId, parentId);
|
||||
},
|
||||
async send(channelId, body, opts) {
|
||||
return store.send(channelId, body, opts);
|
||||
},
|
||||
async editMessage(_channelId, messageId, body) {
|
||||
return store.editMessage(messageId, body);
|
||||
},
|
||||
async deleteMessage(_channelId, messageId) {
|
||||
return { ok: store.deleteMessage(messageId) };
|
||||
},
|
||||
async savedMessages() {
|
||||
return store.savedMessages();
|
||||
},
|
||||
async threadParents() {
|
||||
return store.threadParents();
|
||||
},
|
||||
async react(_channelId, messageId, emoji) {
|
||||
return store.react(messageId, emoji);
|
||||
},
|
||||
async pin(_channelId, messageId) {
|
||||
return store.pin(messageId);
|
||||
},
|
||||
async save(_channelId, messageId) {
|
||||
return store.save(messageId);
|
||||
},
|
||||
async listInbox(state) {
|
||||
return store.listInbox(state);
|
||||
},
|
||||
async patchInbox(id, state) {
|
||||
return store.patchInbox(id, state);
|
||||
},
|
||||
async listTickets(scope) {
|
||||
return store.listTickets(scope);
|
||||
},
|
||||
async getTicket(id) {
|
||||
return store.getTicket(id);
|
||||
},
|
||||
async patchTicket(id, state) {
|
||||
return store.patchTicket(id, state);
|
||||
},
|
||||
async replyTicket(id, body) {
|
||||
return store.replyTicket(id, body);
|
||||
},
|
||||
async availability() {
|
||||
return store.availability;
|
||||
},
|
||||
async search(q) {
|
||||
return store.search(q);
|
||||
},
|
||||
async notifications() {
|
||||
return store.notifications;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user