sovrium 0.1.0 → 0.2.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,444 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { type OpenAPIHono, createRoute } from '@hono/zod-openapi'
9
+ import {
10
+ createCommentResponseSchema,
11
+ getCommentResponseSchema,
12
+ getRecordHistoryResponseSchema,
13
+ listCommentsResponseSchema,
14
+ updateCommentResponseSchema,
15
+ } from '@/domain/models/api/comments'
16
+ import { errorResponseSchema } from '@/domain/models/api/error'
17
+ import {
18
+ commentBodySchema,
19
+ commentIdParamSchema,
20
+ listRecordsQuerySchema,
21
+ recordIdParamSchema,
22
+ tableIdParamSchema,
23
+ } from '@/domain/models/api/params'
24
+ import { createRecordRequestSchema, updateRecordRequestSchema } from '@/domain/models/api/request'
25
+ import {
26
+ createRecordResponseSchema,
27
+ deleteRecordResponseSchema,
28
+ getRecordResponseSchema,
29
+ listRecordsResponseSchema,
30
+ restoreRecordResponseSchema,
31
+ updateRecordResponseSchema,
32
+ } from '@/domain/models/api/tables'
33
+
34
+ /**
35
+ * Register record CRUD routes for OpenAPI schema generation
36
+ */
37
+ export function registerRecordRoutes(app: OpenAPIHono): void {
38
+ registerCrudRoutes(app)
39
+ registerCommentRoutes(app)
40
+ }
41
+
42
+ function registerCrudRoutes(app: OpenAPIHono): void {
43
+ // GET /api/tables/{tableId}/records
44
+ app.openapi(
45
+ createRoute({
46
+ method: 'get',
47
+ path: '/api/tables/{tableId}/records',
48
+ summary: 'List records',
49
+ description: 'Returns paginated records for a table with optional sorting and filtering.',
50
+ operationId: 'listRecords',
51
+ tags: ['records'],
52
+ request: { params: tableIdParamSchema, query: listRecordsQuerySchema },
53
+ responses: {
54
+ 200: {
55
+ content: { 'application/json': { schema: listRecordsResponseSchema } },
56
+ description: 'List of records',
57
+ },
58
+ 401: {
59
+ content: { 'application/json': { schema: errorResponseSchema } },
60
+ description: 'Unauthorized',
61
+ },
62
+ 404: {
63
+ content: { 'application/json': { schema: errorResponseSchema } },
64
+ description: 'Table not found',
65
+ },
66
+ },
67
+ }),
68
+ (c) => c.json({} as never)
69
+ )
70
+
71
+ // POST /api/tables/{tableId}/records
72
+ app.openapi(
73
+ createRoute({
74
+ method: 'post',
75
+ path: '/api/tables/{tableId}/records',
76
+ summary: 'Create a record',
77
+ description: 'Creates a new record in the table with the given field values.',
78
+ operationId: 'createRecord',
79
+ tags: ['records'],
80
+ request: {
81
+ params: tableIdParamSchema,
82
+ body: { content: { 'application/json': { schema: createRecordRequestSchema } } },
83
+ },
84
+ responses: {
85
+ 201: {
86
+ content: { 'application/json': { schema: createRecordResponseSchema } },
87
+ description: 'Record created',
88
+ },
89
+ 400: {
90
+ content: { 'application/json': { schema: errorResponseSchema } },
91
+ description: 'Validation error',
92
+ },
93
+ 401: {
94
+ content: { 'application/json': { schema: errorResponseSchema } },
95
+ description: 'Unauthorized',
96
+ },
97
+ 404: {
98
+ content: { 'application/json': { schema: errorResponseSchema } },
99
+ description: 'Table not found',
100
+ },
101
+ },
102
+ }),
103
+ (c) => c.json({} as never, 201)
104
+ )
105
+
106
+ // GET /api/tables/{tableId}/records/{recordId}
107
+ app.openapi(
108
+ createRoute({
109
+ method: 'get',
110
+ path: '/api/tables/{tableId}/records/{recordId}',
111
+ summary: 'Get a record',
112
+ description: 'Returns a single record by ID with all field values.',
113
+ operationId: 'getRecord',
114
+ tags: ['records'],
115
+ request: { params: recordIdParamSchema },
116
+ responses: {
117
+ 200: {
118
+ content: { 'application/json': { schema: getRecordResponseSchema } },
119
+ description: 'Record details',
120
+ },
121
+ 401: {
122
+ content: { 'application/json': { schema: errorResponseSchema } },
123
+ description: 'Unauthorized',
124
+ },
125
+ 404: {
126
+ content: { 'application/json': { schema: errorResponseSchema } },
127
+ description: 'Record not found',
128
+ },
129
+ },
130
+ }),
131
+ (c) => c.json({} as never)
132
+ )
133
+
134
+ // PATCH /api/tables/{tableId}/records/{recordId}
135
+ app.openapi(
136
+ createRoute({
137
+ method: 'patch',
138
+ path: '/api/tables/{tableId}/records/{recordId}',
139
+ summary: 'Update a record',
140
+ description: 'Updates field values of an existing record.',
141
+ operationId: 'updateRecord',
142
+ tags: ['records'],
143
+ request: {
144
+ params: recordIdParamSchema,
145
+ body: { content: { 'application/json': { schema: updateRecordRequestSchema } } },
146
+ },
147
+ responses: {
148
+ 200: {
149
+ content: { 'application/json': { schema: updateRecordResponseSchema } },
150
+ description: 'Record updated',
151
+ },
152
+ 400: {
153
+ content: { 'application/json': { schema: errorResponseSchema } },
154
+ description: 'Validation error',
155
+ },
156
+ 401: {
157
+ content: { 'application/json': { schema: errorResponseSchema } },
158
+ description: 'Unauthorized',
159
+ },
160
+ 404: {
161
+ content: { 'application/json': { schema: errorResponseSchema } },
162
+ description: 'Record not found',
163
+ },
164
+ },
165
+ }),
166
+ (c) => c.json({} as never)
167
+ )
168
+
169
+ // DELETE /api/tables/{tableId}/records/{recordId}
170
+ app.openapi(
171
+ createRoute({
172
+ method: 'delete',
173
+ path: '/api/tables/{tableId}/records/{recordId}',
174
+ summary: 'Delete a record',
175
+ description: 'Soft-deletes a record. Use the restore endpoint to undo.',
176
+ operationId: 'deleteRecord',
177
+ tags: ['records'],
178
+ request: { params: recordIdParamSchema },
179
+ responses: {
180
+ 200: {
181
+ content: { 'application/json': { schema: deleteRecordResponseSchema } },
182
+ description: 'Record deleted',
183
+ },
184
+ 401: {
185
+ content: { 'application/json': { schema: errorResponseSchema } },
186
+ description: 'Unauthorized',
187
+ },
188
+ 404: {
189
+ content: { 'application/json': { schema: errorResponseSchema } },
190
+ description: 'Record not found',
191
+ },
192
+ },
193
+ }),
194
+ (c) => c.json({} as never)
195
+ )
196
+
197
+ // POST /api/tables/{tableId}/records/{recordId}/restore
198
+ app.openapi(
199
+ createRoute({
200
+ method: 'post',
201
+ path: '/api/tables/{tableId}/records/{recordId}/restore',
202
+ summary: 'Restore a deleted record',
203
+ description: 'Restores a soft-deleted record back to active state.',
204
+ operationId: 'restoreRecord',
205
+ tags: ['records'],
206
+ request: { params: recordIdParamSchema },
207
+ responses: {
208
+ 200: {
209
+ content: { 'application/json': { schema: restoreRecordResponseSchema } },
210
+ description: 'Record restored',
211
+ },
212
+ 401: {
213
+ content: { 'application/json': { schema: errorResponseSchema } },
214
+ description: 'Unauthorized',
215
+ },
216
+ 404: {
217
+ content: { 'application/json': { schema: errorResponseSchema } },
218
+ description: 'Record not found',
219
+ },
220
+ },
221
+ }),
222
+ (c) => c.json({} as never)
223
+ )
224
+
225
+ // GET /api/tables/{tableId}/trash
226
+ app.openapi(
227
+ createRoute({
228
+ method: 'get',
229
+ path: '/api/tables/{tableId}/trash',
230
+ summary: 'List deleted records',
231
+ description: 'Returns paginated list of soft-deleted records in the trash.',
232
+ operationId: 'listTrashRecords',
233
+ tags: ['records'],
234
+ request: { params: tableIdParamSchema, query: listRecordsQuerySchema },
235
+ responses: {
236
+ 200: {
237
+ content: { 'application/json': { schema: listRecordsResponseSchema } },
238
+ description: 'List of deleted records',
239
+ },
240
+ 401: {
241
+ content: { 'application/json': { schema: errorResponseSchema } },
242
+ description: 'Unauthorized',
243
+ },
244
+ 404: {
245
+ content: { 'application/json': { schema: errorResponseSchema } },
246
+ description: 'Table not found',
247
+ },
248
+ },
249
+ }),
250
+ (c) => c.json({} as never)
251
+ )
252
+
253
+ // GET /api/tables/{tableId}/records/{recordId}/history
254
+ app.openapi(
255
+ createRoute({
256
+ method: 'get',
257
+ path: '/api/tables/{tableId}/records/{recordId}/history',
258
+ summary: 'Get record history',
259
+ description: 'Returns the audit trail of changes for a specific record.',
260
+ operationId: 'getRecordHistory',
261
+ tags: ['records'],
262
+ request: { params: recordIdParamSchema },
263
+ responses: {
264
+ 200: {
265
+ content: { 'application/json': { schema: getRecordHistoryResponseSchema } },
266
+ description: 'Record history',
267
+ },
268
+ 401: {
269
+ content: { 'application/json': { schema: errorResponseSchema } },
270
+ description: 'Unauthorized',
271
+ },
272
+ 404: {
273
+ content: { 'application/json': { schema: errorResponseSchema } },
274
+ description: 'Record not found',
275
+ },
276
+ },
277
+ }),
278
+ (c) => c.json({} as never)
279
+ )
280
+ }
281
+
282
+ function registerCommentRoutes(app: OpenAPIHono): void {
283
+ // GET /api/tables/{tableId}/records/{recordId}/comments
284
+ app.openapi(
285
+ createRoute({
286
+ method: 'get',
287
+ path: '/api/tables/{tableId}/records/{recordId}/comments',
288
+ summary: 'List comments on a record',
289
+ description: 'Returns paginated comments for a specific record.',
290
+ operationId: 'listComments',
291
+ tags: ['records'],
292
+ request: { params: recordIdParamSchema },
293
+ responses: {
294
+ 200: {
295
+ content: { 'application/json': { schema: listCommentsResponseSchema } },
296
+ description: 'List of comments',
297
+ },
298
+ 401: {
299
+ content: { 'application/json': { schema: errorResponseSchema } },
300
+ description: 'Unauthorized',
301
+ },
302
+ 404: {
303
+ content: { 'application/json': { schema: errorResponseSchema } },
304
+ description: 'Record not found',
305
+ },
306
+ },
307
+ }),
308
+ (c) => c.json({} as never)
309
+ )
310
+
311
+ // POST /api/tables/{tableId}/records/{recordId}/comments
312
+ app.openapi(
313
+ createRoute({
314
+ method: 'post',
315
+ path: '/api/tables/{tableId}/records/{recordId}/comments',
316
+ summary: 'Create a comment',
317
+ description: 'Adds a comment to a record.',
318
+ operationId: 'createComment',
319
+ tags: ['records'],
320
+ request: {
321
+ params: recordIdParamSchema,
322
+ body: { content: { 'application/json': { schema: commentBodySchema } } },
323
+ },
324
+ responses: {
325
+ 201: {
326
+ content: { 'application/json': { schema: createCommentResponseSchema } },
327
+ description: 'Comment created',
328
+ },
329
+ 400: {
330
+ content: { 'application/json': { schema: errorResponseSchema } },
331
+ description: 'Validation error',
332
+ },
333
+ 401: {
334
+ content: { 'application/json': { schema: errorResponseSchema } },
335
+ description: 'Unauthorized',
336
+ },
337
+ 404: {
338
+ content: { 'application/json': { schema: errorResponseSchema } },
339
+ description: 'Record not found',
340
+ },
341
+ },
342
+ }),
343
+ (c) => c.json({} as never, 201)
344
+ )
345
+
346
+ // GET /api/tables/{tableId}/records/{recordId}/comments/{commentId}
347
+ app.openapi(
348
+ createRoute({
349
+ method: 'get',
350
+ path: '/api/tables/{tableId}/records/{recordId}/comments/{commentId}',
351
+ summary: 'Get a comment',
352
+ description: 'Returns a single comment by ID.',
353
+ operationId: 'getComment',
354
+ tags: ['records'],
355
+ request: { params: commentIdParamSchema },
356
+ responses: {
357
+ 200: {
358
+ content: { 'application/json': { schema: getCommentResponseSchema } },
359
+ description: 'Comment details',
360
+ },
361
+ 401: {
362
+ content: { 'application/json': { schema: errorResponseSchema } },
363
+ description: 'Unauthorized',
364
+ },
365
+ 404: {
366
+ content: { 'application/json': { schema: errorResponseSchema } },
367
+ description: 'Comment not found',
368
+ },
369
+ },
370
+ }),
371
+ (c) => c.json({} as never)
372
+ )
373
+
374
+ // PATCH /api/tables/{tableId}/records/{recordId}/comments/{commentId}
375
+ app.openapi(
376
+ createRoute({
377
+ method: 'patch',
378
+ path: '/api/tables/{tableId}/records/{recordId}/comments/{commentId}',
379
+ summary: 'Update a comment',
380
+ description: 'Updates the content of a comment. Only the author can update.',
381
+ operationId: 'updateComment',
382
+ tags: ['records'],
383
+ request: {
384
+ params: commentIdParamSchema,
385
+ body: { content: { 'application/json': { schema: commentBodySchema } } },
386
+ },
387
+ responses: {
388
+ 200: {
389
+ content: { 'application/json': { schema: updateCommentResponseSchema } },
390
+ description: 'Comment updated',
391
+ },
392
+ 400: {
393
+ content: { 'application/json': { schema: errorResponseSchema } },
394
+ description: 'Validation error',
395
+ },
396
+ 401: {
397
+ content: { 'application/json': { schema: errorResponseSchema } },
398
+ description: 'Unauthorized',
399
+ },
400
+ 403: {
401
+ content: { 'application/json': { schema: errorResponseSchema } },
402
+ description: 'Forbidden',
403
+ },
404
+ 404: {
405
+ content: { 'application/json': { schema: errorResponseSchema } },
406
+ description: 'Comment not found',
407
+ },
408
+ },
409
+ }),
410
+ (c) => c.json({} as never)
411
+ )
412
+
413
+ // DELETE /api/tables/{tableId}/records/{recordId}/comments/{commentId}
414
+ app.openapi(
415
+ createRoute({
416
+ method: 'delete',
417
+ path: '/api/tables/{tableId}/records/{recordId}/comments/{commentId}',
418
+ summary: 'Delete a comment',
419
+ description: 'Soft-deletes a comment. Only the author or admin can delete.',
420
+ operationId: 'deleteComment',
421
+ tags: ['records'],
422
+ request: { params: commentIdParamSchema },
423
+ responses: {
424
+ 204: {
425
+ description: 'Comment deleted (no content)',
426
+ },
427
+ 401: {
428
+ content: { 'application/json': { schema: errorResponseSchema } },
429
+ description: 'Unauthorized',
430
+ },
431
+ 403: {
432
+ content: { 'application/json': { schema: errorResponseSchema } },
433
+ description: 'Forbidden',
434
+ },
435
+ 404: {
436
+ content: { 'application/json': { schema: errorResponseSchema } },
437
+ description: 'Comment not found',
438
+ },
439
+ },
440
+ }),
441
+ // eslint-disable-next-line unicorn/no-null -- Hono c.body() requires null for 204
442
+ (c) => c.body(null, 204)
443
+ )
444
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { type OpenAPIHono, createRoute } from '@hono/zod-openapi'
9
+ import { errorResponseSchema } from '@/domain/models/api/error'
10
+ import { tableIdParamSchema } from '@/domain/models/api/params'
11
+ import {
12
+ getTablePermissionsResponseSchema,
13
+ getTableResponseSchema,
14
+ listTablesResponseSchema,
15
+ } from '@/domain/models/api/tables'
16
+
17
+ /**
18
+ * Register table management routes for OpenAPI schema generation
19
+ */
20
+ export function registerTableRoutes(app: OpenAPIHono): void {
21
+ // GET /api/tables
22
+ app.openapi(
23
+ createRoute({
24
+ method: 'get',
25
+ path: '/api/tables',
26
+ summary: 'List all tables',
27
+ description: 'Returns a list of all tables with summary information.',
28
+ operationId: 'listTables',
29
+ tags: ['tables'],
30
+ responses: {
31
+ 200: {
32
+ content: { 'application/json': { schema: listTablesResponseSchema } },
33
+ description: 'List of tables',
34
+ },
35
+ 401: {
36
+ content: { 'application/json': { schema: errorResponseSchema } },
37
+ description: 'Unauthorized',
38
+ },
39
+ },
40
+ }),
41
+ (c) => c.json({} as never)
42
+ )
43
+
44
+ // GET /api/tables/{tableId}
45
+ app.openapi(
46
+ createRoute({
47
+ method: 'get',
48
+ path: '/api/tables/{tableId}',
49
+ summary: 'Get table details',
50
+ description: 'Returns full table definition including fields, views, and permissions.',
51
+ operationId: 'getTable',
52
+ tags: ['tables'],
53
+ request: { params: tableIdParamSchema },
54
+ responses: {
55
+ 200: {
56
+ content: { 'application/json': { schema: getTableResponseSchema } },
57
+ description: 'Table details',
58
+ },
59
+ 401: {
60
+ content: { 'application/json': { schema: errorResponseSchema } },
61
+ description: 'Unauthorized',
62
+ },
63
+ 404: {
64
+ content: { 'application/json': { schema: errorResponseSchema } },
65
+ description: 'Table not found',
66
+ },
67
+ },
68
+ }),
69
+ (c) => c.json({} as never)
70
+ )
71
+
72
+ // GET /api/tables/{tableId}/permissions
73
+ app.openapi(
74
+ createRoute({
75
+ method: 'get',
76
+ path: '/api/tables/{tableId}/permissions',
77
+ summary: 'Get table permissions',
78
+ description:
79
+ "Returns the current user's permissions for the table, including field-level access.",
80
+ operationId: 'getTablePermissions',
81
+ tags: ['tables'],
82
+ request: { params: tableIdParamSchema },
83
+ responses: {
84
+ 200: {
85
+ content: { 'application/json': { schema: getTablePermissionsResponseSchema } },
86
+ description: 'Table permissions',
87
+ },
88
+ 401: {
89
+ content: { 'application/json': { schema: errorResponseSchema } },
90
+ description: 'Unauthorized',
91
+ },
92
+ 404: {
93
+ content: { 'application/json': { schema: errorResponseSchema } },
94
+ description: 'Table not found',
95
+ },
96
+ },
97
+ }),
98
+ (c) => c.json({} as never)
99
+ )
100
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { type OpenAPIHono, createRoute } from '@hono/zod-openapi'
9
+ import { errorResponseSchema } from '@/domain/models/api/error'
10
+ import { tableIdParamSchema, viewIdParamSchema } from '@/domain/models/api/params'
11
+ import {
12
+ getViewRecordsResponseSchema,
13
+ getViewResponseSchema,
14
+ listViewsResponseSchema,
15
+ } from '@/domain/models/api/tables'
16
+
17
+ /**
18
+ * Register view routes for OpenAPI schema generation
19
+ */
20
+ export function registerViewRoutes(app: OpenAPIHono): void {
21
+ // GET /api/tables/{tableId}/views
22
+ app.openapi(
23
+ createRoute({
24
+ method: 'get',
25
+ path: '/api/tables/{tableId}/views',
26
+ summary: 'List views',
27
+ description: 'Returns all views defined for a table.',
28
+ operationId: 'listViews',
29
+ tags: ['views'],
30
+ request: { params: tableIdParamSchema },
31
+ responses: {
32
+ 200: {
33
+ content: { 'application/json': { schema: listViewsResponseSchema } },
34
+ description: 'List of views',
35
+ },
36
+ 401: {
37
+ content: { 'application/json': { schema: errorResponseSchema } },
38
+ description: 'Unauthorized',
39
+ },
40
+ 404: {
41
+ content: { 'application/json': { schema: errorResponseSchema } },
42
+ description: 'Table not found',
43
+ },
44
+ },
45
+ }),
46
+ (c) => c.json({} as never)
47
+ )
48
+
49
+ // GET /api/tables/{tableId}/views/{viewId}
50
+ app.openapi(
51
+ createRoute({
52
+ method: 'get',
53
+ path: '/api/tables/{tableId}/views/{viewId}',
54
+ summary: 'Get view details',
55
+ description: 'Returns view configuration including filters, sorts, and visible fields.',
56
+ operationId: 'getView',
57
+ tags: ['views'],
58
+ request: { params: viewIdParamSchema },
59
+ responses: {
60
+ 200: {
61
+ content: { 'application/json': { schema: getViewResponseSchema } },
62
+ description: 'View details',
63
+ },
64
+ 401: {
65
+ content: { 'application/json': { schema: errorResponseSchema } },
66
+ description: 'Unauthorized',
67
+ },
68
+ 404: {
69
+ content: { 'application/json': { schema: errorResponseSchema } },
70
+ description: 'View not found',
71
+ },
72
+ },
73
+ }),
74
+ (c) => c.json({} as never)
75
+ )
76
+
77
+ // GET /api/tables/{tableId}/views/{viewId}/records
78
+ app.openapi(
79
+ createRoute({
80
+ method: 'get',
81
+ path: '/api/tables/{tableId}/views/{viewId}/records',
82
+ summary: 'Get records through a view',
83
+ description: 'Returns records filtered and sorted according to the view configuration.',
84
+ operationId: 'getViewRecords',
85
+ tags: ['views'],
86
+ request: { params: viewIdParamSchema },
87
+ responses: {
88
+ 200: {
89
+ content: { 'application/json': { schema: getViewRecordsResponseSchema } },
90
+ description: 'Filtered records',
91
+ },
92
+ 401: {
93
+ content: { 'application/json': { schema: errorResponseSchema } },
94
+ description: 'Unauthorized',
95
+ },
96
+ 404: {
97
+ content: { 'application/json': { schema: errorResponseSchema } },
98
+ description: 'View not found',
99
+ },
100
+ },
101
+ }),
102
+ (c) => c.json({} as never)
103
+ )
104
+ }