@tangle-network/agent-app 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ import { I as IntakeAnswerValue, a as IntakeQuestion, b as IntakeGraph } from '../model-Cuj7d-xT.js';
2
+ import { IntakeStore } from './drizzle.js';
3
+ import 'drizzle-orm/sqlite-core';
4
+ import './index.js';
5
+
6
+ /**
7
+ * Framework-neutral intake API: the get-current / save-answer / complete logic,
8
+ * lifted out of any one app's route file. Each app mounts these in its own
9
+ * route with its own auth — the handlers take an already-resolved store (the
10
+ * caller built it from `createUserIntakeStore` / `createProjectIntakeStore`
11
+ * after running RBAC), the intake graph, and the parsed inputs. They return
12
+ * web-standard `Response`s (available in Workers, Node 18+, Deno, browsers), so
13
+ * "framework-neutral" is literal: no Remix/React-Router/Express import anywhere.
14
+ *
15
+ * The handlers own the graph→HTTP mapping the UI needs: `getCurrentIntake`
16
+ * returns the loaded state PLUS the next question to ask (the question-graph
17
+ * traversal from the `./intakes` leaf) and the progress counter, so the client
18
+ * renders one question at a time without re-deriving the graph. Store
19
+ * `IntakeError`s map to typed 4xx codes, never a generic 500 — an invalid
20
+ * answer is a 400, an incomplete-complete is a 409, never a silent success.
21
+ *
22
+ * Imports `drizzle-orm` transitively (through the store types), so this is a
23
+ * subpath, never re-exported from root.
24
+ */
25
+
26
+ /** What the client renders: the saved state, the next prompt, and progress. */
27
+ interface CurrentIntakeView {
28
+ graphId: string;
29
+ title: string;
30
+ description?: string;
31
+ answers: Record<string, IntakeAnswerValue>;
32
+ /** The next question to ask, or null when the interview is done. */
33
+ nextQuestion: IntakeQuestion | null;
34
+ completed: boolean;
35
+ completedAt: string | null;
36
+ progress: {
37
+ answered: number;
38
+ total: number;
39
+ };
40
+ }
41
+ interface IntakeApiOptions {
42
+ store: IntakeStore;
43
+ graph: IntakeGraph;
44
+ }
45
+ /**
46
+ * Build the intake API bound to one store + graph. Returns three handlers; an
47
+ * app maps its route methods onto them (GET→getCurrentIntake, POST→saveAnswer,
48
+ * a complete action→completeIntake).
49
+ */
50
+ declare function createIntakeApi(opts: IntakeApiOptions): {
51
+ getCurrentIntake: () => Promise<Response>;
52
+ saveAnswer: (input: {
53
+ questionId?: string;
54
+ value?: IntakeAnswerValue;
55
+ }) => Promise<Response>;
56
+ completeIntake: () => Promise<Response>;
57
+ };
58
+
59
+ export { type CurrentIntakeView, type IntakeApiOptions, createIntakeApi };
@@ -0,0 +1,63 @@
1
+ import {
2
+ IntakeError
3
+ } from "../chunk-4K7BZYO7.js";
4
+ import "../chunk-USYXHDKE.js";
5
+ import {
6
+ intakeProgress,
7
+ nextQuestion
8
+ } from "../chunk-ND3XEGBE.js";
9
+
10
+ // src/intakes/api.ts
11
+ function createIntakeApi(opts) {
12
+ const { store, graph } = opts;
13
+ function view(state) {
14
+ return {
15
+ graphId: graph.id,
16
+ title: graph.title,
17
+ ...graph.description ? { description: graph.description } : {},
18
+ answers: state.payload.answers,
19
+ nextQuestion: nextQuestion(graph, state.payload.answers),
20
+ completed: state.completed,
21
+ completedAt: state.completedAt ? state.completedAt.toISOString() : null,
22
+ progress: intakeProgress(graph, state.payload.answers)
23
+ };
24
+ }
25
+ async function getCurrentIntake() {
26
+ const state = await store.get();
27
+ return Response.json(view(state));
28
+ }
29
+ async function saveAnswer(input) {
30
+ if (!input.questionId) return Response.json({ error: "Missing questionId" }, { status: 400 });
31
+ try {
32
+ const state = await store.save(input.questionId, input.value ?? null);
33
+ return Response.json(view(state));
34
+ } catch (err) {
35
+ return intakeErrorResponse(err);
36
+ }
37
+ }
38
+ async function completeIntake() {
39
+ try {
40
+ const state = await store.complete();
41
+ return Response.json(view(state));
42
+ } catch (err) {
43
+ return intakeErrorResponse(err);
44
+ }
45
+ }
46
+ return { getCurrentIntake, saveAnswer, completeIntake };
47
+ }
48
+ var ERROR_STATUS = {
49
+ "invalid-answer": 400,
50
+ "unknown-question": 404,
51
+ incomplete: 409,
52
+ "stale-graph": 409
53
+ };
54
+ function intakeErrorResponse(err) {
55
+ if (err instanceof IntakeError) {
56
+ return Response.json({ error: err.message, code: err.code }, { status: ERROR_STATUS[err.code] ?? 400 });
57
+ }
58
+ throw err;
59
+ }
60
+ export {
61
+ createIntakeApi
62
+ };
63
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/intakes/api.ts"],"sourcesContent":["/**\n * Framework-neutral intake API: the get-current / save-answer / complete logic,\n * lifted out of any one app's route file. Each app mounts these in its own\n * route with its own auth — the handlers take an already-resolved store (the\n * caller built it from `createUserIntakeStore` / `createProjectIntakeStore`\n * after running RBAC), the intake graph, and the parsed inputs. They return\n * web-standard `Response`s (available in Workers, Node 18+, Deno, browsers), so\n * \"framework-neutral\" is literal: no Remix/React-Router/Express import anywhere.\n *\n * The handlers own the graph→HTTP mapping the UI needs: `getCurrentIntake`\n * returns the loaded state PLUS the next question to ask (the question-graph\n * traversal from the `./intakes` leaf) and the progress counter, so the client\n * renders one question at a time without re-deriving the graph. Store\n * `IntakeError`s map to typed 4xx codes, never a generic 500 — an invalid\n * answer is a 400, an incomplete-complete is a 409, never a silent success.\n *\n * Imports `drizzle-orm` transitively (through the store types), so this is a\n * subpath, never re-exported from root.\n */\n\nimport {\n type IntakeAnswerValue,\n type IntakeGraph,\n type IntakeQuestion,\n intakeProgress,\n nextQuestion,\n} from './model'\nimport { type IntakeStore, IntakeError } from './drizzle/store'\n\n/** What the client renders: the saved state, the next prompt, and progress. */\nexport interface CurrentIntakeView {\n graphId: string\n title: string\n description?: string\n answers: Record<string, IntakeAnswerValue>\n /** The next question to ask, or null when the interview is done. */\n nextQuestion: IntakeQuestion | null\n completed: boolean\n completedAt: string | null\n progress: { answered: number; total: number }\n}\n\nexport interface IntakeApiOptions {\n store: IntakeStore\n graph: IntakeGraph\n}\n\n/**\n * Build the intake API bound to one store + graph. Returns three handlers; an\n * app maps its route methods onto them (GET→getCurrentIntake, POST→saveAnswer,\n * a complete action→completeIntake).\n */\nexport function createIntakeApi(opts: IntakeApiOptions) {\n const { store, graph } = opts\n\n function view(state: Awaited<ReturnType<IntakeStore['get']>>): CurrentIntakeView {\n return {\n graphId: graph.id,\n title: graph.title,\n ...(graph.description ? { description: graph.description } : {}),\n answers: state.payload.answers,\n nextQuestion: nextQuestion(graph, state.payload.answers),\n completed: state.completed,\n completedAt: state.completedAt ? state.completedAt.toISOString() : null,\n progress: intakeProgress(graph, state.payload.answers),\n }\n }\n\n async function getCurrentIntake(): Promise<Response> {\n const state = await store.get()\n return Response.json(view(state))\n }\n\n async function saveAnswer(input: { questionId?: string; value?: IntakeAnswerValue }): Promise<Response> {\n if (!input.questionId) return Response.json({ error: 'Missing questionId' }, { status: 400 })\n try {\n const state = await store.save(input.questionId, input.value ?? null)\n return Response.json(view(state))\n } catch (err) {\n return intakeErrorResponse(err)\n }\n }\n\n async function completeIntake(): Promise<Response> {\n try {\n const state = await store.complete()\n return Response.json(view(state))\n } catch (err) {\n return intakeErrorResponse(err)\n }\n }\n\n return { getCurrentIntake, saveAnswer, completeIntake }\n}\n\nconst ERROR_STATUS: Record<string, number> = {\n 'invalid-answer': 400,\n 'unknown-question': 404,\n incomplete: 409,\n 'stale-graph': 409,\n}\n\nfunction intakeErrorResponse(err: unknown): Response {\n if (err instanceof IntakeError) {\n return Response.json({ error: err.message, code: err.code }, { status: ERROR_STATUS[err.code] ?? 400 })\n }\n throw err\n}\n"],"mappings":";;;;;;;;;;AAoDO,SAAS,gBAAgB,MAAwB;AACtD,QAAM,EAAE,OAAO,MAAM,IAAI;AAEzB,WAAS,KAAK,OAAmE;AAC/E,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,GAAI,MAAM,cAAc,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,MAC9D,SAAS,MAAM,QAAQ;AAAA,MACvB,cAAc,aAAa,OAAO,MAAM,QAAQ,OAAO;AAAA,MACvD,WAAW,MAAM;AAAA,MACjB,aAAa,MAAM,cAAc,MAAM,YAAY,YAAY,IAAI;AAAA,MACnE,UAAU,eAAe,OAAO,MAAM,QAAQ,OAAO;AAAA,IACvD;AAAA,EACF;AAEA,iBAAe,mBAAsC;AACnD,UAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,WAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,EAClC;AAEA,iBAAe,WAAW,OAA8E;AACtG,QAAI,CAAC,MAAM,WAAY,QAAO,SAAS,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC5F,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,YAAY,MAAM,SAAS,IAAI;AACpE,aAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAClC,SAAS,KAAK;AACZ,aAAO,oBAAoB,GAAG;AAAA,IAChC;AAAA,EACF;AAEA,iBAAe,iBAAoC;AACjD,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,SAAS;AACnC,aAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAClC,SAAS,KAAK;AACZ,aAAO,oBAAoB,GAAG;AAAA,IAChC;AAAA,EACF;AAEA,SAAO,EAAE,kBAAkB,YAAY,eAAe;AACxD;AAEA,IAAM,eAAuC;AAAA,EAC3C,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,eAAe;AACjB;AAEA,SAAS,oBAAoB,KAAwB;AACnD,MAAI,eAAe,aAAa;AAC9B,WAAO,SAAS,KAAK,EAAE,OAAO,IAAI,SAAS,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,aAAa,IAAI,IAAI,KAAK,IAAI,CAAC;AAAA,EACxG;AACA,QAAM;AACR;","names":[]}
@@ -0,0 +1,385 @@
1
+ import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
2
+ import { AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
3
+ import { I as IntakeAnswerValue, b as IntakeGraph } from '../model-Cuj7d-xT.js';
4
+ import { IntakePayload } from './index.js';
5
+
6
+ /** A product table referenced by FK — only the `id` column is touched. */
7
+ type IntakeParentTable = AnySQLiteTable & {
8
+ id: AnySQLiteColumn;
9
+ };
10
+ interface CreateIntakeTablesOptions {
11
+ /** The product's user table — user-intake rows reference `userTable.id`. */
12
+ userTable: IntakeParentTable;
13
+ /**
14
+ * The product's workspace table — project-intake rows reference
15
+ * `workspaceTable.id`. OPTIONAL: omit it for a single-user / non-workspace
16
+ * app that wants per-user onboarding only. When omitted, no `projectIntake`
17
+ * table is created.
18
+ */
19
+ workspaceTable?: IntakeParentTable;
20
+ }
21
+ declare function createUserIntakeTable(userTable: IntakeParentTable): drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
22
+ name: "user_intake";
23
+ schema: undefined;
24
+ columns: {
25
+ id: drizzle_orm_sqlite_core.SQLiteColumn<{
26
+ name: "id";
27
+ tableName: "user_intake";
28
+ dataType: "string";
29
+ columnType: "SQLiteText";
30
+ data: string;
31
+ driverParam: string;
32
+ notNull: true;
33
+ hasDefault: true;
34
+ isPrimaryKey: true;
35
+ isAutoincrement: false;
36
+ hasRuntimeDefault: false;
37
+ enumValues: [string, ...string[]];
38
+ baseColumn: never;
39
+ identity: undefined;
40
+ generated: undefined;
41
+ }, {}, {
42
+ length: number | undefined;
43
+ }>;
44
+ userId: drizzle_orm_sqlite_core.SQLiteColumn<{
45
+ name: "user_id";
46
+ tableName: "user_intake";
47
+ dataType: "string";
48
+ columnType: "SQLiteText";
49
+ data: string;
50
+ driverParam: string;
51
+ notNull: true;
52
+ hasDefault: false;
53
+ isPrimaryKey: false;
54
+ isAutoincrement: false;
55
+ hasRuntimeDefault: false;
56
+ enumValues: [string, ...string[]];
57
+ baseColumn: never;
58
+ identity: undefined;
59
+ generated: undefined;
60
+ }, {}, {
61
+ length: number | undefined;
62
+ }>;
63
+ graphId: drizzle_orm_sqlite_core.SQLiteColumn<{
64
+ name: "graph_id";
65
+ tableName: "user_intake";
66
+ dataType: "string";
67
+ columnType: "SQLiteText";
68
+ data: string;
69
+ driverParam: string;
70
+ notNull: true;
71
+ hasDefault: false;
72
+ isPrimaryKey: false;
73
+ isAutoincrement: false;
74
+ hasRuntimeDefault: false;
75
+ enumValues: [string, ...string[]];
76
+ baseColumn: never;
77
+ identity: undefined;
78
+ generated: undefined;
79
+ }, {}, {
80
+ length: number | undefined;
81
+ }>;
82
+ payload: drizzle_orm_sqlite_core.SQLiteColumn<{
83
+ name: "payload";
84
+ tableName: "user_intake";
85
+ dataType: "json";
86
+ columnType: "SQLiteTextJson";
87
+ data: unknown;
88
+ driverParam: string;
89
+ notNull: true;
90
+ hasDefault: false;
91
+ isPrimaryKey: false;
92
+ isAutoincrement: false;
93
+ hasRuntimeDefault: false;
94
+ enumValues: undefined;
95
+ baseColumn: never;
96
+ identity: undefined;
97
+ generated: undefined;
98
+ }, {}, {}>;
99
+ completedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
100
+ name: "completed_at";
101
+ tableName: "user_intake";
102
+ dataType: "date";
103
+ columnType: "SQLiteTimestamp";
104
+ data: Date;
105
+ driverParam: number;
106
+ notNull: false;
107
+ hasDefault: false;
108
+ isPrimaryKey: false;
109
+ isAutoincrement: false;
110
+ hasRuntimeDefault: false;
111
+ enumValues: undefined;
112
+ baseColumn: never;
113
+ identity: undefined;
114
+ generated: undefined;
115
+ }, {}, {}>;
116
+ createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
117
+ name: "created_at";
118
+ tableName: "user_intake";
119
+ dataType: "date";
120
+ columnType: "SQLiteTimestamp";
121
+ data: Date;
122
+ driverParam: number;
123
+ notNull: true;
124
+ hasDefault: true;
125
+ isPrimaryKey: false;
126
+ isAutoincrement: false;
127
+ hasRuntimeDefault: false;
128
+ enumValues: undefined;
129
+ baseColumn: never;
130
+ identity: undefined;
131
+ generated: undefined;
132
+ }, {}, {}>;
133
+ updatedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
134
+ name: "updated_at";
135
+ tableName: "user_intake";
136
+ dataType: "date";
137
+ columnType: "SQLiteTimestamp";
138
+ data: Date;
139
+ driverParam: number;
140
+ notNull: true;
141
+ hasDefault: true;
142
+ isPrimaryKey: false;
143
+ isAutoincrement: false;
144
+ hasRuntimeDefault: false;
145
+ enumValues: undefined;
146
+ baseColumn: never;
147
+ identity: undefined;
148
+ generated: undefined;
149
+ }, {}, {}>;
150
+ };
151
+ dialect: "sqlite";
152
+ }>;
153
+ declare function createProjectIntakeTable(workspaceTable: IntakeParentTable): drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
154
+ name: "project_intake";
155
+ schema: undefined;
156
+ columns: {
157
+ id: drizzle_orm_sqlite_core.SQLiteColumn<{
158
+ name: "id";
159
+ tableName: "project_intake";
160
+ dataType: "string";
161
+ columnType: "SQLiteText";
162
+ data: string;
163
+ driverParam: string;
164
+ notNull: true;
165
+ hasDefault: true;
166
+ isPrimaryKey: true;
167
+ isAutoincrement: false;
168
+ hasRuntimeDefault: false;
169
+ enumValues: [string, ...string[]];
170
+ baseColumn: never;
171
+ identity: undefined;
172
+ generated: undefined;
173
+ }, {}, {
174
+ length: number | undefined;
175
+ }>;
176
+ workspaceId: drizzle_orm_sqlite_core.SQLiteColumn<{
177
+ name: "workspace_id";
178
+ tableName: "project_intake";
179
+ dataType: "string";
180
+ columnType: "SQLiteText";
181
+ data: string;
182
+ driverParam: string;
183
+ notNull: true;
184
+ hasDefault: false;
185
+ isPrimaryKey: false;
186
+ isAutoincrement: false;
187
+ hasRuntimeDefault: false;
188
+ enumValues: [string, ...string[]];
189
+ baseColumn: never;
190
+ identity: undefined;
191
+ generated: undefined;
192
+ }, {}, {
193
+ length: number | undefined;
194
+ }>;
195
+ graphId: drizzle_orm_sqlite_core.SQLiteColumn<{
196
+ name: "graph_id";
197
+ tableName: "project_intake";
198
+ dataType: "string";
199
+ columnType: "SQLiteText";
200
+ data: string;
201
+ driverParam: string;
202
+ notNull: true;
203
+ hasDefault: false;
204
+ isPrimaryKey: false;
205
+ isAutoincrement: false;
206
+ hasRuntimeDefault: false;
207
+ enumValues: [string, ...string[]];
208
+ baseColumn: never;
209
+ identity: undefined;
210
+ generated: undefined;
211
+ }, {}, {
212
+ length: number | undefined;
213
+ }>;
214
+ payload: drizzle_orm_sqlite_core.SQLiteColumn<{
215
+ name: "payload";
216
+ tableName: "project_intake";
217
+ dataType: "json";
218
+ columnType: "SQLiteTextJson";
219
+ data: unknown;
220
+ driverParam: string;
221
+ notNull: true;
222
+ hasDefault: false;
223
+ isPrimaryKey: false;
224
+ isAutoincrement: false;
225
+ hasRuntimeDefault: false;
226
+ enumValues: undefined;
227
+ baseColumn: never;
228
+ identity: undefined;
229
+ generated: undefined;
230
+ }, {}, {}>;
231
+ completedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
232
+ name: "completed_at";
233
+ tableName: "project_intake";
234
+ dataType: "date";
235
+ columnType: "SQLiteTimestamp";
236
+ data: Date;
237
+ driverParam: number;
238
+ notNull: false;
239
+ hasDefault: false;
240
+ isPrimaryKey: false;
241
+ isAutoincrement: false;
242
+ hasRuntimeDefault: false;
243
+ enumValues: undefined;
244
+ baseColumn: never;
245
+ identity: undefined;
246
+ generated: undefined;
247
+ }, {}, {}>;
248
+ createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
249
+ name: "created_at";
250
+ tableName: "project_intake";
251
+ dataType: "date";
252
+ columnType: "SQLiteTimestamp";
253
+ data: Date;
254
+ driverParam: number;
255
+ notNull: true;
256
+ hasDefault: true;
257
+ isPrimaryKey: false;
258
+ isAutoincrement: false;
259
+ hasRuntimeDefault: false;
260
+ enumValues: undefined;
261
+ baseColumn: never;
262
+ identity: undefined;
263
+ generated: undefined;
264
+ }, {}, {}>;
265
+ updatedAt: drizzle_orm_sqlite_core.SQLiteColumn<{
266
+ name: "updated_at";
267
+ tableName: "project_intake";
268
+ dataType: "date";
269
+ columnType: "SQLiteTimestamp";
270
+ data: Date;
271
+ driverParam: number;
272
+ notNull: true;
273
+ hasDefault: true;
274
+ isPrimaryKey: false;
275
+ isAutoincrement: false;
276
+ hasRuntimeDefault: false;
277
+ enumValues: undefined;
278
+ baseColumn: never;
279
+ identity: undefined;
280
+ generated: undefined;
281
+ }, {}, {}>;
282
+ };
283
+ dialect: "sqlite";
284
+ }>;
285
+ /**
286
+ * Build the intake tables wired to the product's tables. Always returns
287
+ * `userIntake`; returns `projectIntake` only when `workspaceTable` is passed.
288
+ * The return type carries `projectIntake?` so a consumer that passes no
289
+ * workspace table cannot reference a table that does not exist.
290
+ */
291
+ declare function createIntakeTables<O extends CreateIntakeTablesOptions>(opts: O): IntakeTables<O>;
292
+ type UserIntakeTable = ReturnType<typeof createUserIntakeTable>;
293
+ type ProjectIntakeTable = ReturnType<typeof createProjectIntakeTable>;
294
+ /**
295
+ * The tables returned for a given options shape: `projectIntake` is present in
296
+ * the type exactly when `workspaceTable` was provided.
297
+ */
298
+ type IntakeTables<O extends CreateIntakeTablesOptions> = O extends {
299
+ workspaceTable: IntakeParentTable;
300
+ } ? {
301
+ userIntake: UserIntakeTable;
302
+ projectIntake: ProjectIntakeTable;
303
+ } : {
304
+ userIntake: UserIntakeTable;
305
+ projectIntake?: ProjectIntakeTable;
306
+ };
307
+ /** The union table shape, for code that handles either scope generically. */
308
+ type AnyIntakeTables = {
309
+ userIntake: UserIntakeTable;
310
+ projectIntake?: ProjectIntakeTable;
311
+ };
312
+ type UserIntakeRow = UserIntakeTable['$inferSelect'];
313
+ type ProjectIntakeRow = ProjectIntakeTable['$inferSelect'];
314
+
315
+ /**
316
+ * Drizzle-backed store over the tables from `createIntakeTables`. One store
317
+ * builder per scope: `createUserIntakeStore` (keyed on userId) and
318
+ * `createProjectIntakeStore` (keyed on workspaceId). Each returns the same
319
+ * three-method surface — `get` / `save` / `complete` — so the api handlers and
320
+ * the UI talk to one shape regardless of scope.
321
+ *
322
+ * Works against any SQLite drizzle driver (better-sqlite3, D1, libsql): the
323
+ * builders are awaited, never `.run()`/`.all()`, so sync and async drivers
324
+ * behave identically.
325
+ *
326
+ * Validation is fail-loud and pure (from the `./intakes` leaf). `save` runs
327
+ * `validateAnswer` before writing, so an invalid answer can never enter the
328
+ * payload; `complete` runs `payloadComplete` and refuses to stamp an
329
+ * incomplete (or stale-graph) intake — it throws a typed `IntakeError` rather
330
+ * than silently writing a half-finished onboarding state.
331
+ *
332
+ * `getProjectIntakeStore` pins `workspaceId` in every WHERE clause; RBAC runs
333
+ * in the route before the store is built, but the scope key is enforced here
334
+ * too, so a leaked id can never read or write across the boundary.
335
+ *
336
+ * Imports `drizzle-orm`, so this is a subpath, never re-exported from root.
337
+ */
338
+
339
+ /** Any SQLite drizzle database — `any` erases driver-specific generics so
340
+ * better-sqlite3, D1, and libsql handles all fit. */
341
+ type IntakeDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any>;
342
+ /** A loaded intake: the payload plus whether it is complete against the graph. */
343
+ interface IntakeState {
344
+ payload: IntakePayload;
345
+ completed: boolean;
346
+ completedAt: Date | null;
347
+ }
348
+ type IntakeErrorCode = 'invalid-answer' | 'unknown-question' | 'incomplete' | 'stale-graph';
349
+ /** Thrown by store mutations on a refused write — callers map it to a 4xx. */
350
+ declare class IntakeError extends Error {
351
+ readonly code: IntakeErrorCode;
352
+ constructor(code: IntakeErrorCode, message: string);
353
+ }
354
+ /** The three-method store surface, identical for both scopes. */
355
+ interface IntakeStore {
356
+ /** Load the current intake, or seed an empty payload when none exists yet. */
357
+ get(): Promise<IntakeState>;
358
+ /** Validate and persist one answer; returns the updated state. */
359
+ save(questionId: string, value: IntakeAnswerValue): Promise<IntakeState>;
360
+ /** Stamp the intake complete; throws IntakeError when not yet completable. */
361
+ complete(): Promise<IntakeState>;
362
+ }
363
+ interface CreateUserIntakeStoreOptions {
364
+ db: IntakeDatabase;
365
+ /** The user-intake table from createIntakeTables. */
366
+ table: UserIntakeTable;
367
+ /** The intake definition this store collects answers against. */
368
+ graph: IntakeGraph;
369
+ /** The user whose onboarding this is. */
370
+ userId: string;
371
+ }
372
+ /** Build the per-user onboarding store, scoped to one userId. */
373
+ declare function createUserIntakeStore(opts: CreateUserIntakeStoreOptions): IntakeStore;
374
+ interface CreateProjectIntakeStoreOptions {
375
+ db: IntakeDatabase;
376
+ /** The project-intake table from createIntakeTables (workspace scope). */
377
+ table: ProjectIntakeTable;
378
+ graph: IntakeGraph;
379
+ /** The workspace this intake is attached to. */
380
+ workspaceId: string;
381
+ }
382
+ /** Build the per-project store, scoped to one workspaceId. */
383
+ declare function createProjectIntakeStore(opts: CreateProjectIntakeStoreOptions): IntakeStore;
384
+
385
+ export { type AnyIntakeTables, type CreateIntakeTablesOptions, type CreateProjectIntakeStoreOptions, type CreateUserIntakeStoreOptions, type IntakeDatabase, IntakeError, type IntakeErrorCode, type IntakeParentTable, type IntakeState, type IntakeStore, type IntakeTables, type ProjectIntakeRow, type ProjectIntakeTable, type UserIntakeRow, type UserIntakeTable, createIntakeTables, createProjectIntakeStore, createUserIntakeStore };
@@ -0,0 +1,61 @@
1
+ import {
2
+ IntakeError,
3
+ createProjectIntakeStore,
4
+ createUserIntakeStore
5
+ } from "../chunk-4K7BZYO7.js";
6
+ import "../chunk-USYXHDKE.js";
7
+ import "../chunk-ND3XEGBE.js";
8
+
9
+ // src/intakes/drizzle/schema.ts
10
+ import { sql } from "drizzle-orm";
11
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
12
+ var hexId = () => text("id").primaryKey().default(sql`(lower(hex(randomblob(16))))`);
13
+ var createdAt = () => integer("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`);
14
+ var updatedAt = () => integer("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`);
15
+ function createUserIntakeTable(userTable) {
16
+ return sqliteTable("user_intake", {
17
+ id: hexId(),
18
+ userId: text("user_id").notNull().references(() => userTable.id, { onDelete: "cascade" }),
19
+ /** The intake definition id the payload was collected against. */
20
+ graphId: text("graph_id").notNull(),
21
+ /** The IntakePayload JSON blob. */
22
+ payload: text("payload", { mode: "json" }).notNull(),
23
+ /** Set when the user finishes the onboarding interview; null while open. */
24
+ completedAt: integer("completed_at", { mode: "timestamp" }),
25
+ createdAt: createdAt(),
26
+ updatedAt: updatedAt()
27
+ }, (table) => [
28
+ uniqueIndex("uniq_user_intake_user").on(table.userId)
29
+ ]);
30
+ }
31
+ function createProjectIntakeTable(workspaceTable) {
32
+ return sqliteTable("project_intake", {
33
+ id: hexId(),
34
+ workspaceId: text("workspace_id").notNull().references(() => workspaceTable.id, { onDelete: "cascade" }),
35
+ graphId: text("graph_id").notNull(),
36
+ payload: text("payload", { mode: "json" }).notNull(),
37
+ completedAt: integer("completed_at", { mode: "timestamp" }),
38
+ createdAt: createdAt(),
39
+ updatedAt: updatedAt()
40
+ }, (table) => [
41
+ uniqueIndex("uniq_project_intake_workspace").on(table.workspaceId),
42
+ index("idx_project_intake_graph").on(table.graphId)
43
+ ]);
44
+ }
45
+ function createIntakeTables(opts) {
46
+ const userIntake = createUserIntakeTable(opts.userTable);
47
+ const result = {
48
+ userIntake
49
+ };
50
+ if (opts.workspaceTable) {
51
+ result.projectIntake = createProjectIntakeTable(opts.workspaceTable);
52
+ }
53
+ return result;
54
+ }
55
+ export {
56
+ IntakeError,
57
+ createIntakeTables,
58
+ createProjectIntakeStore,
59
+ createUserIntakeStore
60
+ };
61
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/intakes/drizzle/schema.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the intake tables. The product owns the user (and\n * optionally workspace) tables; this factory creates the intake tables and\n * wires their foreign keys into the passed-in tables so the whole graph lives\n * in one drizzle schema with real cascade semantics.\n *\n * Two tables, two scopes:\n * - `user_intake` — the one-time onboarding interview, keyed on `user.id`\n * (UNIQUE per user: one onboarding payload per user). ALWAYS created.\n * - `project_intake` — the structured intake attached to a workspace, keyed\n * on `workspace.id` (UNIQUE per workspace). Created ONLY when a\n * `workspaceTable` is passed.\n *\n * `workspaceTable` is OPTIONAL by design: a non-workspace app (a single-user\n * tool, tax-without-workspaces) adopts per-user onboarding alone with zero\n * teams and zero workspace concept. When omitted, `createIntakeTables` returns\n * `{ userIntake }` and no `projectIntake` — the FK to a workspace table that\n * does not exist is never created.\n *\n * Imports `drizzle-orm` at module top — that is WHY this lives behind the\n * `/intakes/drizzle` sub-subpath. The pure `./intakes` leaf imports none of\n * this, so a consumer that never touches the DB never pulls the optional peer.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable } from 'drizzle-orm/sqlite-core'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type IntakeParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\nexport interface CreateIntakeTablesOptions {\n /** The product's user table — user-intake rows reference `userTable.id`. */\n userTable: IntakeParentTable\n /**\n * The product's workspace table — project-intake rows reference\n * `workspaceTable.id`. OPTIONAL: omit it for a single-user / non-workspace\n * app that wants per-user onboarding only. When omitted, no `projectIntake`\n * table is created.\n */\n workspaceTable?: IntakeParentTable\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nfunction createUserIntakeTable(userTable: IntakeParentTable) {\n return sqliteTable('user_intake', {\n id: hexId(),\n userId: text('user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n /** The intake definition id the payload was collected against. */\n graphId: text('graph_id').notNull(),\n /** The IntakePayload JSON blob. */\n payload: text('payload', { mode: 'json' }).notNull(),\n /** Set when the user finishes the onboarding interview; null while open. */\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_user_intake_user').on(table.userId),\n ])\n}\n\nfunction createProjectIntakeTable(workspaceTable: IntakeParentTable) {\n return sqliteTable('project_intake', {\n id: hexId(),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n graphId: text('graph_id').notNull(),\n payload: text('payload', { mode: 'json' }).notNull(),\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_project_intake_workspace').on(table.workspaceId),\n index('idx_project_intake_graph').on(table.graphId),\n ])\n}\n\n/**\n * Build the intake tables wired to the product's tables. Always returns\n * `userIntake`; returns `projectIntake` only when `workspaceTable` is passed.\n * The return type carries `projectIntake?` so a consumer that passes no\n * workspace table cannot reference a table that does not exist.\n */\nexport function createIntakeTables<O extends CreateIntakeTablesOptions>(\n opts: O,\n): IntakeTables<O> {\n const userIntake = createUserIntakeTable(opts.userTable)\n const result: { userIntake: ReturnType<typeof createUserIntakeTable>; projectIntake?: ReturnType<typeof createProjectIntakeTable> } = {\n userIntake,\n }\n if (opts.workspaceTable) {\n result.projectIntake = createProjectIntakeTable(opts.workspaceTable)\n }\n return result as IntakeTables<O>\n}\n\nexport type UserIntakeTable = ReturnType<typeof createUserIntakeTable>\nexport type ProjectIntakeTable = ReturnType<typeof createProjectIntakeTable>\n\n/**\n * The tables returned for a given options shape: `projectIntake` is present in\n * the type exactly when `workspaceTable` was provided.\n */\nexport type IntakeTables<O extends CreateIntakeTablesOptions> =\n O extends { workspaceTable: IntakeParentTable }\n ? { userIntake: UserIntakeTable; projectIntake: ProjectIntakeTable }\n : { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\n/** The union table shape, for code that handles either scope generically. */\nexport type AnyIntakeTables = { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\nexport type UserIntakeRow = UserIntakeTable['$inferSelect']\nexport type ProjectIntakeRow = ProjectIntakeTable['$inferSelect']\n"],"mappings":";;;;;;;;;AAwBA,SAAS,WAAW;AACpB,SAAS,OAAO,SAAS,aAAa,MAAM,mBAAmB;AAkB/D,IAAM,QAAQ,MAAM,KAAK,IAAI,EAAE,WAAW,EAAE,QAAQ,iCAAiC;AAErF,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAEzG,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAEzG,SAAS,sBAAsB,WAA8B;AAC3D,SAAO,YAAY,eAAe;AAAA,IAChC,IAAI,MAAM;AAAA,IACV,QAAQ,KAAK,SAAS,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA;AAAA,IAExF,SAAS,KAAK,UAAU,EAAE,QAAQ;AAAA;AAAA,IAElC,SAAS,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,EAAE,QAAQ;AAAA;AAAA,IAEnD,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAAA,IAC1D,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,uBAAuB,EAAE,GAAG,MAAM,MAAM;AAAA,EACtD,CAAC;AACH;AAEA,SAAS,yBAAyB,gBAAmC;AACnE,SAAO,YAAY,kBAAkB;AAAA,IACnC,IAAI,MAAM;AAAA,IACV,aAAa,KAAK,cAAc,EAAE,QAAQ,EAAE,WAAW,MAAM,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACvG,SAAS,KAAK,UAAU,EAAE,QAAQ;AAAA,IAClC,SAAS,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,EAAE,QAAQ;AAAA,IACnD,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAAA,IAC1D,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,+BAA+B,EAAE,GAAG,MAAM,WAAW;AAAA,IACjE,MAAM,0BAA0B,EAAE,GAAG,MAAM,OAAO;AAAA,EACpD,CAAC;AACH;AAQO,SAAS,mBACd,MACiB;AACjB,QAAM,aAAa,sBAAsB,KAAK,SAAS;AACvD,QAAM,SAAgI;AAAA,IACpI;AAAA,EACF;AACA,MAAI,KAAK,gBAAgB;AACvB,WAAO,gBAAgB,yBAAyB,KAAK,cAAc;AAAA,EACrE;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,42 @@
1
+ import { c as IntakeAnswers, b as IntakeGraph, I as IntakeAnswerValue } from '../model-Cuj7d-xT.js';
2
+ export { A as AnswerRejectionReason, d as AnswerValidationResult, e as IntakeAnswerType, f as IntakeOption, a as IntakeQuestion, g as getQuestion, h as hasAnswer, i as intakeProgress, j as isComplete, n as nextQuestion, r as reachableQuestions, v as validateAnswer } from '../model-Cuj7d-xT.js';
3
+
4
+ /**
5
+ * Pure completion-state helpers for an intake payload — the small algebra over
6
+ * the JSON blob the store persists. No I/O: the store reads/writes the row;
7
+ * these functions only build and judge the payload shape, so the same logic
8
+ * runs in the UI, the handlers, and the DB layer without divergence.
9
+ *
10
+ * A payload is `{ graphId, answers, completedAt? }`. It carries the answers
11
+ * and the id of the graph they were collected against, so a later graph
12
+ * revision can detect a stale payload (`graphId` mismatch) rather than
13
+ * silently mixing answers from two question sets.
14
+ */
15
+
16
+ /** The persisted JSON for one intake (the `payload` column). */
17
+ interface IntakePayload {
18
+ /** The graph id the answers were collected against — guards stale schemas. */
19
+ graphId: string;
20
+ answers: IntakeAnswers;
21
+ /** ISO-8601 instant the intake was completed, or undefined while in progress. */
22
+ completedAt?: string;
23
+ }
24
+ /** An empty payload for a fresh intake against `graph`. */
25
+ declare function emptyPayload(graph: IntakeGraph): IntakePayload;
26
+ /** A copy of `payload` with one answer set (does not mutate the input). */
27
+ declare function withAnswer(payload: IntakePayload, questionId: string, value: IntakeAnswerValue): IntakePayload;
28
+ /**
29
+ * True when the payload's answers complete the graph AND the payload was
30
+ * collected against THIS graph. A `graphId` mismatch is never "complete" —
31
+ * the answers belong to a different question set and must be re-collected.
32
+ */
33
+ declare function payloadComplete(graph: IntakeGraph, payload: IntakePayload): boolean;
34
+ /** True when the payload was collected against a DIFFERENT graph revision. */
35
+ declare function payloadIsStale(graph: IntakeGraph, payload: IntakePayload): boolean;
36
+ /**
37
+ * Stamp the payload complete at `at` (default now). Returns a copy; pure. The
38
+ * caller has already checked `payloadComplete` — this only records the instant.
39
+ */
40
+ declare function markComplete(payload: IntakePayload, at?: Date): IntakePayload;
41
+
42
+ export { IntakeAnswerValue, IntakeAnswers, IntakeGraph, type IntakePayload, emptyPayload, markComplete, payloadComplete, payloadIsStale, withAnswer };
@@ -0,0 +1,31 @@
1
+ import {
2
+ emptyPayload,
3
+ markComplete,
4
+ payloadComplete,
5
+ payloadIsStale,
6
+ withAnswer
7
+ } from "../chunk-USYXHDKE.js";
8
+ import {
9
+ getQuestion,
10
+ hasAnswer,
11
+ intakeProgress,
12
+ isComplete,
13
+ nextQuestion,
14
+ reachableQuestions,
15
+ validateAnswer
16
+ } from "../chunk-ND3XEGBE.js";
17
+ export {
18
+ emptyPayload,
19
+ getQuestion,
20
+ hasAnswer,
21
+ intakeProgress,
22
+ isComplete,
23
+ markComplete,
24
+ nextQuestion,
25
+ payloadComplete,
26
+ payloadIsStale,
27
+ reachableQuestions,
28
+ validateAnswer,
29
+ withAnswer
30
+ };
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}