@ganju/utils 0.0.15 → 0.0.16

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.
Files changed (57) hide show
  1. package/dist/constants.d.ts +23 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +95 -8
  4. package/dist/data.d.ts +9 -0
  5. package/dist/data.d.ts.map +1 -0
  6. package/dist/data.js +78 -0
  7. package/dist/dataApi.d.ts +511 -0
  8. package/dist/dataApi.d.ts.map +1 -0
  9. package/dist/dataApi.js +355 -0
  10. package/dist/dataCompiler.d.ts +68 -0
  11. package/dist/dataCompiler.d.ts.map +1 -0
  12. package/dist/dataCompiler.js +726 -0
  13. package/dist/dataDdl.d.ts +29 -0
  14. package/dist/dataDdl.d.ts.map +1 -0
  15. package/dist/dataDdl.js +354 -0
  16. package/dist/dataEngine.d.ts +143 -0
  17. package/dist/dataEngine.d.ts.map +1 -0
  18. package/dist/dataEngine.js +2022 -0
  19. package/dist/dataIdentifiers.d.ts +8 -0
  20. package/dist/dataIdentifiers.d.ts.map +1 -0
  21. package/dist/dataIdentifiers.js +55 -0
  22. package/dist/dataImport.d.ts +77 -0
  23. package/dist/dataImport.d.ts.map +1 -0
  24. package/dist/dataImport.js +450 -0
  25. package/dist/dataRpc.d.ts +131 -0
  26. package/dist/dataRpc.d.ts.map +1 -0
  27. package/dist/dataRpc.js +2 -0
  28. package/dist/dataTypes.d.ts +157 -0
  29. package/dist/dataTypes.d.ts.map +1 -0
  30. package/dist/dataTypes.js +160 -0
  31. package/dist/index.d.ts +308 -6
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/oauth.d.ts +14 -0
  35. package/dist/oauth.d.ts.map +1 -1
  36. package/dist/oauth.js +42 -2
  37. package/dist/resourceRerank.d.ts +3 -2
  38. package/dist/resourceRerank.d.ts.map +1 -1
  39. package/dist/resourceRerank.js +5 -2
  40. package/dist/schema.d.ts +285 -4
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +203 -0
  43. package/package.json +17 -2
  44. package/src/constants.ts +108 -8
  45. package/src/data.ts +74 -0
  46. package/src/dataApi.ts +428 -0
  47. package/src/dataCompiler.ts +948 -0
  48. package/src/dataDdl.ts +419 -0
  49. package/src/dataEngine.ts +2753 -0
  50. package/src/dataIdentifiers.ts +63 -0
  51. package/src/dataImport.ts +554 -0
  52. package/src/dataRpc.ts +121 -0
  53. package/src/dataTypes.ts +312 -0
  54. package/src/index.ts +4 -0
  55. package/src/oauth.ts +42 -1
  56. package/src/resourceRerank.ts +5 -2
  57. package/src/schema.ts +246 -0
package/src/dataApi.ts ADDED
@@ -0,0 +1,428 @@
1
+ // The Data API's routes, as one list: apps/api registers its data routes from
2
+ // it, and the OpenAPI file is generated from it, so the documented API and the
3
+ // served one can't drift apart. Entry point `@ganju/utils/dataApi`.
4
+ import { z } from 'zod';
5
+
6
+ import { Schema } from './schema';
7
+
8
+ export const DATA_API_BASE =
9
+ '/organization/:organizationId/project/:projectId/artifact/data';
10
+
11
+ export type DataApiMethod = 'get' | 'post' | 'patch' | 'delete';
12
+
13
+ export interface DataApiRoute {
14
+ method: DataApiMethod;
15
+ // Relative to DATA_API_BASE, in Hono's `:param` form.
16
+ path: string;
17
+ summary: string;
18
+ description?: string;
19
+ tag: 'Database' | 'Tables' | 'Rows' | 'History and export';
20
+ body?: z.ZodType;
21
+ query?: z.ZodObject;
22
+ // What a 200/201 answers with, in a sentence.
23
+ returns: string;
24
+ status?: 200 | 201;
25
+ // Answers with a file instead of JSON.
26
+ file?: boolean;
27
+ }
28
+
29
+ const {
30
+ DATA_DATABASE_CREATE,
31
+ DATA_DATABASE_DELETE,
32
+ DATA_TABLE_INPUT,
33
+ DATA_TABLE_PATCH,
34
+ DATA_COLUMN_INPUT,
35
+ DATA_COLUMN_PATCH,
36
+ DATA_TABLE_DROP_BODY,
37
+ DATA_ROW_LIST_QUERY,
38
+ DATA_ROW_GET_QUERY,
39
+ DATA_ROW_CREATE_BODY,
40
+ DATA_ROW_UPDATE_BODY,
41
+ DATA_ROW_DELETE_BODY,
42
+ DATA_DELETE_PREVIEW_BODY,
43
+ DATA_WRITE_BODY,
44
+ DATA_ROW_HISTORY_QUERY,
45
+ DATA_RESTORE_POINT_BODY,
46
+ DATA_EXPORT_BODY,
47
+ DATA_EXPORT_QUERY,
48
+ DATA_EXPORT_FILE_QUERY,
49
+ DATA_CHANGE_LIST_QUERY
50
+ } = Schema;
51
+
52
+ export const DATA_API_ROUTES = {
53
+ getDatabase: {
54
+ method: 'get',
55
+ path: '',
56
+ tag: 'Database',
57
+ summary: "The project's database",
58
+ description:
59
+ 'Its status and size, and its tables when it is ready. `database` is null when the project has none.',
60
+ returns: '{ database }'
61
+ },
62
+ create: {
63
+ method: 'post',
64
+ path: '',
65
+ tag: 'Database',
66
+ summary: 'Create the database',
67
+ description:
68
+ "Pro and Enterprise. Safe to repeat: a second call returns the same database. `name` defaults to the project's slug. While a deleted database is in its grace period this is refused unless `replaceDeleted` is true: the deleted one can then no longer be restored or exported, and is emptied when its grace period ends.",
69
+ body: DATA_DATABASE_CREATE.pick({ name: true, replaceDeleted: true }),
70
+ returns: '{ database }',
71
+ status: 201
72
+ },
73
+ remove: {
74
+ method: 'delete',
75
+ path: '',
76
+ tag: 'Database',
77
+ summary: 'Delete the database',
78
+ description:
79
+ 'Needs `confirmName`, the database name typed out. It can be restored for 7 days.',
80
+ body: DATA_DATABASE_DELETE.pick({ confirmName: true }),
81
+ returns: '{ database }'
82
+ },
83
+ restore: {
84
+ method: 'post',
85
+ path: '/restore',
86
+ tag: 'Database',
87
+ summary: 'Undo a delete',
88
+ description: 'Within the 7-day grace period.',
89
+ returns: '{ database }'
90
+ },
91
+ restorePoint: {
92
+ method: 'post',
93
+ path: '/restore-point',
94
+ tag: 'Database',
95
+ summary: 'Restore to a point in time',
96
+ description:
97
+ 'Replaces everything with the database as it was at `timestamp` (within 30 days), or at the `bookmark` a checkpoint, a dropped table or column, a type change or an earlier restore answered with. The answer carries `undoBookmark`, which undoes the restore. Calls in flight while it restarts fail.',
98
+ body: DATA_RESTORE_POINT_BODY,
99
+ returns: '{ bookmark, undoBookmark }'
100
+ },
101
+ checkpoint: {
102
+ method: 'post',
103
+ path: '/checkpoint',
104
+ tag: 'Database',
105
+ summary: 'Take a restore point',
106
+ description:
107
+ "Answers with the `bookmark` of the database as it is right now, which `POST /restore-point` accepts. Take one before a large change, such as replacing a table's rows, to undo exactly that change: a time is not that precise.",
108
+ returns: '{ bookmark }'
109
+ },
110
+ dismissCopy: {
111
+ method: 'delete',
112
+ path: '/copy',
113
+ tag: 'Tables',
114
+ summary: 'Dismiss a failed table copy',
115
+ description:
116
+ 'A column change on a table over 100,000 rows copies it in the background; table changes, and writes that would change its rows, wait until it finishes (`copy` on `GET` shows how far it got). A copy that failed left the table as it was and stays in `copy` with its `error` until dismissed, or until the next column change.',
117
+ returns: '{ copy: null }'
118
+ },
119
+ listTables: {
120
+ method: 'get',
121
+ path: '/table',
122
+ tag: 'Tables',
123
+ summary: 'Tables and their columns',
124
+ returns: '{ tables }'
125
+ },
126
+ createTable: {
127
+ method: 'post',
128
+ path: '/table',
129
+ tag: 'Tables',
130
+ summary: 'Create a table',
131
+ body: DATA_TABLE_INPUT,
132
+ returns: '{ table }',
133
+ status: 201
134
+ },
135
+ updateTable: {
136
+ method: 'patch',
137
+ path: '/table/:table',
138
+ tag: 'Tables',
139
+ summary: 'Rename a table or change its settings',
140
+ body: DATA_TABLE_PATCH,
141
+ returns: '{ table }'
142
+ },
143
+ dropTable: {
144
+ method: 'delete',
145
+ path: '/table/:table',
146
+ tag: 'Tables',
147
+ summary: 'Drop a table',
148
+ description:
149
+ 'Needs `confirmName`, the table name typed out. The answer carries a `bookmark`: restoring to it brings the table back.',
150
+ body: DATA_TABLE_DROP_BODY,
151
+ returns: '{ dropped, bookmark }'
152
+ },
153
+ addColumn: {
154
+ method: 'post',
155
+ path: '/table/:table/column',
156
+ tag: 'Tables',
157
+ summary: 'Add a column',
158
+ body: DATA_COLUMN_INPUT,
159
+ returns: '{ table }',
160
+ status: 201
161
+ },
162
+ updateColumn: {
163
+ method: 'patch',
164
+ path: '/table/:table/column/:column',
165
+ tag: 'Tables',
166
+ summary: 'Rename a column or change it',
167
+ description:
168
+ 'A type change copies the table and lists the first rows that would not fit. When it copies, the answer carries a `bookmark` from just before. Over 100,000 rows the copy runs in the background: the answer carries `copy`, the table keeps its old columns until it finishes, and meanwhile table changes, and writes that would change its rows, are refused.',
169
+ body: DATA_COLUMN_PATCH,
170
+ returns: '{ table, bookmark, copy }'
171
+ },
172
+ dropColumn: {
173
+ method: 'delete',
174
+ path: '/table/:table/column/:column',
175
+ tag: 'Tables',
176
+ summary: 'Drop a column',
177
+ description:
178
+ 'The answer carries a `bookmark`: restoring to it brings the column back. Dropping a relation column copies the table, in the background over 100,000 rows, as for a type change.',
179
+ returns: '{ table, bookmark, copy }'
180
+ },
181
+ listRows: {
182
+ method: 'get',
183
+ path: '/table/:table/row',
184
+ tag: 'Rows',
185
+ summary: 'List, filter and search rows',
186
+ description:
187
+ 'Up to 500 rows or 1 MB a page; `nextCursor` reads the next. `where` is JSON, e.g. `{"status":"open","price":{"lt":30000}}`. `sort` is a list of columns, `-` for descending. `q` searches the searchable columns.',
188
+ query: DATA_ROW_LIST_QUERY,
189
+ returns: '{ table, rows, total?, nextCursor? }'
190
+ },
191
+ createRows: {
192
+ method: 'post',
193
+ path: '/table/:table/row',
194
+ tag: 'Rows',
195
+ summary: 'Insert one row, or up to 500',
196
+ body: DATA_ROW_CREATE_BODY,
197
+ returns: '{ ids }',
198
+ status: 201
199
+ },
200
+ getRow: {
201
+ method: 'get',
202
+ path: '/table/:table/row/:id',
203
+ tag: 'Rows',
204
+ summary: 'One row',
205
+ query: DATA_ROW_GET_QUERY,
206
+ returns: '{ row }'
207
+ },
208
+ updateRow: {
209
+ method: 'patch',
210
+ path: '/table/:table/row/:id',
211
+ tag: 'Rows',
212
+ summary: 'Change a row',
213
+ description:
214
+ 'Send the `version` you read: if the row changed since, the answer is 409 with the current row and who changed it.',
215
+ body: DATA_ROW_UPDATE_BODY,
216
+ returns: '{ row }'
217
+ },
218
+ deleteRow: {
219
+ method: 'delete',
220
+ path: '/table/:table/row/:id',
221
+ tag: 'Rows',
222
+ summary: 'Delete a row',
223
+ description: 'Send the `version` you read, as for a change.',
224
+ body: DATA_ROW_DELETE_BODY,
225
+ returns: '{ deleted }'
226
+ },
227
+ deletePreview: {
228
+ method: 'post',
229
+ path: '/table/:table/delete-preview',
230
+ tag: 'Rows',
231
+ summary: 'What deleting rows would do',
232
+ description:
233
+ 'For `ids`, or every row without them: each relation column pointing at a row the delete removes, with how many rows it would delete (cascade), clear (set null) or that refuse the delete (restrict). Cascades are followed all the way down. `blocked` says a restrict would refuse it. Past 200,000 affected rows `truncated` is true and the counts are lower bounds. Nothing is deleted.',
234
+ body: DATA_DELETE_PREVIEW_BODY,
235
+ returns: '{ table, rows, effects, blocked, truncated }'
236
+ },
237
+ rowHistory: {
238
+ method: 'get',
239
+ path: '/table/:table/row/:id/history',
240
+ tag: 'History and export',
241
+ summary: "A row's history",
242
+ description: 'Newest first, kept 90 days, with who made each change.',
243
+ query: DATA_ROW_HISTORY_QUERY,
244
+ returns: '{ entries, nextCursor? }'
245
+ },
246
+ write: {
247
+ method: 'post',
248
+ path: '/write',
249
+ tag: 'Rows',
250
+ summary: 'Several writes, all or nothing',
251
+ description:
252
+ 'Inserts, updates and deletes across tables in one transaction. A guard (`where` plus `expect`) that fails rolls everything back. A later step uses an earlier single-row insert\'s id as `{ "$ref": <step> }`, in its values or as the `id` it updates or deletes.',
253
+ body: DATA_WRITE_BODY,
254
+ returns: '{ results }'
255
+ },
256
+ startExport: {
257
+ method: 'post',
258
+ path: '/export',
259
+ tag: 'History and export',
260
+ summary: 'Start an export',
261
+ description:
262
+ 'Writes a copy in the background: schema.json and one JSONL or CSV file per table. Poll `GET /export` until `status` is `done`. Works during the 7-day grace period after a delete too.',
263
+ body: DATA_EXPORT_BODY,
264
+ returns: '{ exportId, status }',
265
+ status: 201
266
+ },
267
+ exportStatus: {
268
+ method: 'get',
269
+ path: '/export',
270
+ tag: 'History and export',
271
+ summary: 'The latest export',
272
+ query: DATA_EXPORT_QUERY,
273
+ returns: '{ export }'
274
+ },
275
+ exportFile: {
276
+ method: 'get',
277
+ path: '/export/file',
278
+ tag: 'History and export',
279
+ summary: "Download one of an export's files",
280
+ description: "A table's file, or schema.json without `table`.",
281
+ query: DATA_EXPORT_FILE_QUERY,
282
+ returns: 'The file',
283
+ file: true
284
+ },
285
+ listChanges: {
286
+ method: 'get',
287
+ path: '/change',
288
+ tag: 'History and export',
289
+ summary: 'The schema change log',
290
+ description:
291
+ 'Newest first: tables and columns changed, restores, exports, deletes.',
292
+ query: DATA_CHANGE_LIST_QUERY,
293
+ returns: '{ changes, nextCursor? }'
294
+ }
295
+ } as const satisfies Record<string, DataApiRoute>;
296
+
297
+ export type DataApiRouteId = keyof typeof DATA_API_ROUTES;
298
+
299
+ // `/table/:table` → `/table/{table}`
300
+ const openApiPath = (path: string) => path.replace(/:([A-Za-z]+)/g, '{$1}');
301
+
302
+ const jsonSchema = (schema: z.ZodType) =>
303
+ z.toJSONSchema(schema, {
304
+ io: 'input',
305
+ unrepresentable: 'any',
306
+ target: 'draft-2020-12'
307
+ });
308
+
309
+ const errorResponse = (description: string) => ({
310
+ description,
311
+ content: {
312
+ 'application/json': { schema: { $ref: '#/components/schemas/Error' } }
313
+ }
314
+ });
315
+
316
+ /**
317
+ * The OpenAPI 3.1 document for the Data API, served by apps/api at
318
+ * /openapi/data.json.
319
+ */
320
+ export const dataOpenApi = (serverUrl: string) => {
321
+ const paths: Record<string, Record<string, unknown>> = {};
322
+ for (const [id, route] of Object.entries(DATA_API_ROUTES) as [
323
+ DataApiRouteId,
324
+ DataApiRoute
325
+ ][]) {
326
+ const fullPath = openApiPath(`${DATA_API_BASE}${route.path}`);
327
+ const pathParams = [...fullPath.matchAll(/\{([A-Za-z]+)\}/g)].map(m => ({
328
+ name: m[1],
329
+ in: 'path',
330
+ required: true,
331
+ schema:
332
+ m[1] === 'id' ? { type: 'integer', minimum: 1 } : { type: 'string' }
333
+ }));
334
+ const queryParams = route.query
335
+ ? Object.entries(route.query.shape).map(([name, field]) => ({
336
+ name,
337
+ in: 'query',
338
+ required: !(field as z.ZodType).safeParse(undefined).success,
339
+ schema: jsonSchema(field as z.ZodType)
340
+ }))
341
+ : [];
342
+ const status = String(route.status ?? 200);
343
+ paths[fullPath] ??= {};
344
+ paths[fullPath][route.method] = {
345
+ operationId: id,
346
+ tags: [route.tag],
347
+ summary: route.summary,
348
+ ...(route.description ? { description: route.description } : {}),
349
+ parameters: [...pathParams, ...queryParams],
350
+ ...(route.body
351
+ ? {
352
+ requestBody: {
353
+ required: true,
354
+ content: {
355
+ 'application/json': { schema: jsonSchema(route.body) }
356
+ }
357
+ }
358
+ }
359
+ : {}),
360
+ responses: {
361
+ [status]: route.file
362
+ ? {
363
+ description: route.returns,
364
+ content: { 'application/octet-stream': {} }
365
+ }
366
+ : {
367
+ description: route.returns,
368
+ content: { 'application/json': { schema: { type: 'object' } } }
369
+ },
370
+ '400': errorResponse(
371
+ 'The request is not valid; `errors` lists each field.'
372
+ ),
373
+ '401': errorResponse('No session or token.'),
374
+ '402': errorResponse('The plan does not include Data.'),
375
+ '403': errorResponse(
376
+ 'Not an admin of the project, or the token is for another project.'
377
+ ),
378
+ '404': errorResponse('No database, table or row by that name.'),
379
+ '409': errorResponse(
380
+ 'A conflict: a stale `version` (with `details.current`), a guard that failed, a constraint, or a full database.'
381
+ ),
382
+ '429': errorResponse('Too many requests for this token; wait a minute.')
383
+ }
384
+ };
385
+ }
386
+ return {
387
+ openapi: '3.1.0',
388
+ info: {
389
+ title: 'Ganju Data API',
390
+ version: '1',
391
+ description:
392
+ "Tables, rows and search in a project's database. Authenticate with a personal access token (`ganju token create`) as a bearer token; a token reaches only its own project."
393
+ },
394
+ servers: [{ url: serverUrl }],
395
+ security: [{ bearer: [] }],
396
+ tags: [
397
+ { name: 'Database' },
398
+ { name: 'Tables' },
399
+ { name: 'Rows' },
400
+ { name: 'History and export' }
401
+ ],
402
+ paths,
403
+ components: {
404
+ securitySchemes: { bearer: { type: 'http', scheme: 'bearer' } },
405
+ schemas: {
406
+ Error: {
407
+ type: 'object',
408
+ properties: {
409
+ error: { type: 'string' },
410
+ code: { type: 'string' },
411
+ details: { type: 'object' },
412
+ // A request that didn't parse: one entry per field.
413
+ errors: {
414
+ type: 'array',
415
+ items: {
416
+ type: 'object',
417
+ properties: {
418
+ path: { type: 'string' },
419
+ message: { type: 'string' }
420
+ }
421
+ }
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ };
428
+ };