The Coline App SDK: apps, tools, file types and surfaces that run inside Coline workspaces, and public websites with Coline Sites. TypeScript, with an in-memory test workspace. MIT.
| 6 | } from "../capabilities/workspace"; |
| 7 | import type { TestWorkspaceState } from "./fake-capabilities"; |
| 8 |
| 9 | export function createFakeWorkspaceCapabilities( |
| 10 | state: TestWorkspaceState, |
| 11 | userId: string | null, |
| 12 | ): ColineWorkspaceCapabilities { |
| 13 | const missing = (): never => { |
| 14 | throw Object.assign(new Error("Workspace object not found."), { code: "not_found" }); |
| 15 | }; |
| 16 | return { |
| 17 | tasks: { |
| 18 | list: async (input) => { |
| 19 | const all = [...state.tasks.values()].filter( |
| 20 | (t) => |
| 21 | t.taskboardId === input.taskboardId && |
| 22 | (!input.query || t.title.toLowerCase().includes(input.query.toLowerCase())), |
| 23 | ); |
| 24 | const start = Number(input.cursor ?? 0), |
| 25 | limit = input.limit ?? 50; |
| 26 | return { |
| 27 | tasks: structuredClone(all.slice(start, start + limit)), |
| 28 | nextCursor: all.length > start + limit ? String(start + limit) : null, |
| 29 | }; |
| 30 | }, |
| 31 | board: async (id) => ({ id, statuses: [{ id: "status_open", name: "Open" }] }), |
| 32 | get: async (board, id) => { |
| 33 | const task = state.tasks.get(id); |
| 34 | if (!task || task.taskboardId !== board) return missing(); |
| 35 | return structuredClone(task); |
| 36 | }, |
| 37 | create: async (board, input) => { |
| 38 | const task: ColineTask = { |
| 39 | id: `task_${state.tasks.size + 1}`, |
| 40 | taskboardId: board, |
| 41 | title: input.title, |
| 42 | description: input.description ?? "", |
| 43 | statusId: input.statusId ?? "status_open", |
| 44 | priority: input.priority ?? "none", |
| 45 | dueDate: input.dueDate ?? null, |
| 46 | assigneeUserIds: input.assigneeUserIds ?? [], |
| 47 | }; |
| 48 | state.tasks.set(task.id, structuredClone(task)); |
| 49 | return task; |
| 50 | }, |
| 51 | update: async (board, id, input) => { |
| 52 | const old = state.tasks.get(id); |
| 53 | if (!old || old.taskboardId !== board) return missing(); |
| 54 | const task = { ...old, ...input }; |
| 55 | state.tasks.set(id, structuredClone(task)); |
| 56 | return task; |
| 57 | }, |
| 58 | delete: async (board, id) => { |
| 59 | if (state.tasks.get(id)?.taskboardId !== board) return missing(); |
| 60 | state.tasks.delete(id); |
| 61 | }, |
| 62 | }, |
| 63 | calendar: { |
| 64 | list: async (input) => ({ |
| 65 | events: structuredClone( |
| 66 | [...state.calendar.values()].filter( |
| 67 | (e) => e.startsAt < input.end && e.endsAt > input.start, |
| 68 | ), |
| 69 | ), |
| 70 | }), |
| 71 | get: async (id) => structuredClone(state.calendar.get(id) ?? missing()), |
| 72 | create: async (input) => { |
| 73 | if (Date.parse(input.endsAt) <= Date.parse(input.startsAt)) |
| 74 | throw new Error("Event end must follow start."); |
| 75 | const event: ColineCalendarEvent = { |
| 76 | ...input, |
| 77 | id: `event_${state.calendar.size + 1}`, |
| 78 | description: input.description ?? null, |
| 79 | location: input.location ?? null, |
| 80 | isAllDay: input.isAllDay ?? false, |
| 81 | }; |
| 82 | state.calendar.set(event.id, structuredClone(event)); |
| 83 | return event; |
| 84 | }, |
| 85 | update: async (id, input) => { |
| 86 | const old = state.calendar.get(id) ?? missing(); |
| 87 | const event = { ...old, ...input }; |
| 88 | state.calendar.set(id, structuredClone(event)); |
| 89 | return event; |
| 90 | }, |
| 91 | delete: async (id) => { |
| 92 | if (!state.calendar.delete(id)) missing(); |
| 93 | }, |
| 94 | }, |
| 95 | messages: { |
| 96 | list: async (input) => ({ |
| 97 | messages: structuredClone( |
| 98 | (state.messages.get(`${input.conversation.type}:${input.conversation.id}`) ?? []).slice( |
| 99 | 0, |
| 100 | input.limit ?? 50, |
| 101 | ), |
| 102 | ), |
| 103 | nextCursor: null, |
| 104 | }), |
| 105 | send: async (input) => { |
| 106 | const key = `${input.conversation.type}:${input.conversation.id}`; |
| 107 | const list = state.messages.get(key) ?? []; |
| 108 | const message: ColineMessage = { |
| 109 | id: `message_${list.length + 1}`, |
| 110 | text: input.text, |
| 111 | createdAt: new Date().toISOString(), |
| 112 | authorUserId: userId, |
| 113 | }; |
| 114 | list.push(message); |
| 115 | state.messages.set(key, list); |
| 116 | return structuredClone(message); |
| 117 | }, |
| 118 | }, |
| 119 | references: { |
| 120 | resolve: async (reference) => { |
| 121 | let title: string | undefined; |
| 122 | if (reference.kind === "file") title = state.files.get(reference.id)?.name; |
| 123 | if (reference.kind === "task") title = state.tasks.get(reference.id)?.title; |
| 124 | if (reference.kind === "event") title = state.calendar.get(reference.id)?.title; |
| 125 | if (reference.kind === "member") title = reference.id; |
| 126 | if (reference.kind === "app_record") { |
| 127 | for (const records of state.records.values()) { |
| 128 | const data = records.get(reference.id)?.data; |
| 129 | if (data) title = String(data.title ?? data.name ?? "Record"); |
| 130 | } |
| 131 | } |
| 132 | if (!title) return missing(); |
| 133 | return { reference, title, href: null }; |
| 134 | }, |
| 135 | }, |
| 136 | }; |
| 137 | } |
| 138 |