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