@rangka/core 0.1.0 → 0.1.2

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 (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -18
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,448 +0,0 @@
1
- import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
- import type { SchemaRegistry } from '../schema/registry.js';
3
- import type { DatabaseClient } from '../db/client.js';
4
- import type { PermissionRegistry } from '../auth/permission-registry.js';
5
- import type { HookRegistry } from '../hooks/registry.js';
6
- import type { ServiceRegistry } from '../services/registry.js';
7
- import type { EventBus } from '../events/bus.js';
8
- import type { ResolvedModel } from '../schema/types.js';
9
- import { KyselyModelOps } from '../db/model-ops.js';
10
- import type { ModelOps } from '../model-api/types.js';
11
- import type { ModelAccessOptions } from '../model-api/types.js';
12
- import {
13
- listHandler,
14
- getHandler,
15
- createHandler,
16
- updateHandler,
17
- deleteHandler,
18
- } from './handlers.js';
19
- import { createAuthHook, createSessionHandler, deleteSessionHandler } from '../auth/session.js';
20
- import { createModelPermissionGuard } from '../auth/model-permissions.js';
21
- import { createScopeHook, createScopeWriteGuard } from '../auth/scopes.js';
22
- import type { ScopeRegistry } from '../auth/scope-registry.js';
23
- import { createFieldWriteGuard, createFieldStripHook } from '../auth/field-permissions.js';
24
- import { withHooksCreate, withHooksUpdate, withHooksDelete } from '../hooks/middleware.js';
25
- import {
26
- modelToSchemaComponent,
27
- modelToCreateSchema,
28
- modelToUpdateSchema,
29
- } from './openapi-schema.js';
30
- import { createMetaBootHandler } from './meta-handler.js';
31
- import type { MetaBootContext } from './meta-handler.js';
32
- import type { ModuleConfig, PageDefinition, WidgetDefinitionMeta } from '@rangka/shared';
33
-
34
- // ─── Types ───────────────────────────────────────────────────────────────────
35
-
36
- export interface RouteGeneratorOptions {
37
- permissionRegistry?: PermissionRegistry;
38
- hookRegistry?: HookRegistry;
39
- serviceRegistry?: ServiceRegistry;
40
- eventBus?: EventBus;
41
- scopeRegistry?: ScopeRegistry;
42
- config?: Record<string, unknown>;
43
- pages?: Array<{ module: string; page: PageDefinition }>;
44
- modules?: ModuleConfig[];
45
- widgets?: WidgetDefinitionMeta[];
46
- adapterRegistry?: import('../plugins/adapter-registry.js').AdapterRegistry;
47
- adapterCapabilities?: Record<string, import('../plugins/types.js').AdapterCapability[]>;
48
- }
49
-
50
- /** Context passed to lifecycle hook middleware (create/update/delete with hooks). */
51
- interface HookMiddlewareContext {
52
- model: ResolvedModel;
53
- registry: SchemaRegistry;
54
- db: import('kysely').Kysely<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
55
- ops: ModelOps;
56
- hookRegistry: HookRegistry;
57
- serviceRegistry?: ServiceRegistry;
58
- eventBus?: EventBus;
59
- config: Record<string, unknown>;
60
- }
61
-
62
- /** Context for registering all routes belonging to a single model. */
63
- interface ModelRouteContext {
64
- registry: SchemaRegistry;
65
- db: DatabaseClient;
66
- permissionRegistry?: PermissionRegistry;
67
- hookRegistry?: HookRegistry;
68
- serviceRegistry?: ServiceRegistry;
69
- eventBus?: EventBus;
70
- scopeRegistry?: ScopeRegistry;
71
- config: Record<string, unknown>;
72
- }
73
-
74
- /** Extended context for external model routes. */
75
- interface ExternalModelRouteContext extends ModelRouteContext {
76
- adapterRegistry?: import('../plugins/adapter-registry.js').AdapterRegistry;
77
- adapterCapabilities?: Record<string, import('../plugins/types.js').AdapterCapability[]>;
78
- }
79
-
80
- // ─── Main entry point ────────────────────────────────────────────────────────
81
-
82
- /**
83
- * Registers all REST routes for every model in the schema registry,
84
- * plus session and meta routes.
85
- */
86
- export function generateRoutes(
87
- server: FastifyInstance,
88
- registry: SchemaRegistry,
89
- db: DatabaseClient,
90
- options?: RouteGeneratorOptions,
91
- ): void {
92
- const permissionRegistry = options?.permissionRegistry;
93
- const hookRegistry = options?.hookRegistry;
94
- const serviceRegistry = options?.serviceRegistry;
95
- const eventBus = options?.eventBus;
96
- const scopeRegistry = options?.scopeRegistry;
97
- const config = options?.config ?? {};
98
-
99
- registerSessionRoutes(server, db, permissionRegistry);
100
- registerMetaRoute(server, db, registry, permissionRegistry, options);
101
-
102
- for (const [module, models] of registry.getModelsByModule()) {
103
- for (const model of models) {
104
- // Session model has dedicated routes above; skip it here.
105
- if (model.qualifiedName === 'core.session') continue;
106
-
107
- if (model.source) {
108
- registerExternalModelRoutes(server, model, module, {
109
- registry,
110
- db,
111
- permissionRegistry,
112
- hookRegistry,
113
- serviceRegistry,
114
- eventBus,
115
- scopeRegistry,
116
- config,
117
- adapterRegistry: options?.adapterRegistry,
118
- adapterCapabilities: options?.adapterCapabilities,
119
- });
120
- } else {
121
- registerModelRoutes(server, model, module, {
122
- registry,
123
- db,
124
- permissionRegistry,
125
- hookRegistry,
126
- serviceRegistry,
127
- eventBus,
128
- scopeRegistry,
129
- config,
130
- });
131
- }
132
- }
133
- }
134
- }
135
-
136
- // ─── Session routes (login/logout) ───────────────────────────────────────────
137
-
138
- function registerSessionRoutes(
139
- server: FastifyInstance,
140
- db: DatabaseClient,
141
- permissionRegistry?: PermissionRegistry,
142
- ) {
143
- server.post('/api/core/session', createSessionHandler(db));
144
-
145
- if (permissionRegistry) {
146
- server.delete('/api/core/session', {
147
- onRequest: createAuthHook(db, permissionRegistry),
148
- handler: deleteSessionHandler(db),
149
- });
150
- }
151
- }
152
-
153
- // ─── Meta boot route (provides UI shell config) ─────────────────────────────
154
-
155
- function registerMetaRoute(
156
- server: FastifyInstance,
157
- db: DatabaseClient,
158
- registry: SchemaRegistry,
159
- permissionRegistry?: PermissionRegistry,
160
- options?: RouteGeneratorOptions,
161
- ) {
162
- if (!permissionRegistry || !options?.pages || !options?.modules) return;
163
-
164
- const metaCtx: MetaBootContext = {
165
- schemaRegistry: registry,
166
- pages: options.pages,
167
- modules: options.modules,
168
- widgets: options.widgets,
169
- };
170
-
171
- server.get('/api/meta/boot', {
172
- onRequest: createAuthHook(db, permissionRegistry),
173
- schema: { tags: ['meta'] },
174
- handler: createMetaBootHandler(metaCtx),
175
- });
176
- }
177
-
178
- // ─── Per-model CRUD routes ───────────────────────────────────────────────────
179
-
180
- /** Registers list, get, create, update, delete routes for a model. */
181
- function registerModelRoutes(
182
- server: FastifyInstance,
183
- model: ResolvedModel,
184
- module: string,
185
- ctx: ModelRouteContext,
186
- ) {
187
- const basePath = `/api/${module}/${model.name}`;
188
- const authHooks = buildAuthHooks(model, ctx);
189
- const schemas = buildRouteSchemas(model, module);
190
- const modelAccessOpts: Omit<ModelAccessOptions, 'auth'> = { db: ctx.db, registry: ctx.registry };
191
- const handlerCtx = { model, registry: ctx.registry, db: ctx.db.kysely, modelAccessOpts };
192
-
193
- const modelHasHooks = ctx.hookRegistry?.hasHooks(model.qualifiedName) ?? false;
194
- const hookMiddlewareCtx: HookMiddlewareContext = {
195
- model,
196
- registry: ctx.registry,
197
- db: ctx.db.kysely,
198
- ops: new KyselyModelOps({ db: ctx.db, model, registry: ctx.registry }),
199
- hookRegistry: ctx.hookRegistry!,
200
- serviceRegistry: ctx.serviceRegistry,
201
- eventBus: ctx.eventBus,
202
- config: ctx.config,
203
- };
204
-
205
- // Read endpoints
206
- server.get(basePath, { ...authHooks, schema: schemas.list, handler: listHandler(handlerCtx) });
207
- server.get(`${basePath}/:id`, {
208
- ...authHooks,
209
- schema: schemas.get,
210
- handler: getHandler(handlerCtx),
211
- });
212
-
213
- // Write endpoints (use hook middleware when lifecycle hooks are registered)
214
- server.post(basePath, {
215
- ...authHooks,
216
- schema: schemas.create,
217
- handler: modelHasHooks ? withHooksCreate(hookMiddlewareCtx) : createHandler(handlerCtx),
218
- });
219
-
220
- server.put(`${basePath}/:id`, {
221
- ...authHooks,
222
- schema: schemas.update,
223
- handler: modelHasHooks ? withHooksUpdate(hookMiddlewareCtx) : updateHandler(handlerCtx),
224
- });
225
-
226
- server.delete(`${basePath}/:id`, {
227
- ...authHooks,
228
- schema: schemas.delete,
229
- handler: modelHasHooks ? withHooksDelete(hookMiddlewareCtx) : deleteHandler(handlerCtx),
230
- });
231
- }
232
-
233
- // ─── External model routes (capability-gated, no hooks) ────────────────────
234
-
235
- /** Registers routes for an external model, limited by adapter capabilities. */
236
- function registerExternalModelRoutes(
237
- server: FastifyInstance,
238
- model: ResolvedModel,
239
- module: string,
240
- ctx: ExternalModelRouteContext,
241
- ) {
242
- const basePath = `/api/${module}/${model.name}`;
243
- const authHooks = buildAuthHooks(model, ctx);
244
- const schemas = buildRouteSchemas(model, module);
245
- const modelAccessOpts: Omit<ModelAccessOptions, 'auth'> = {
246
- db: ctx.db,
247
- registry: ctx.registry,
248
- adapterRegistry: ctx.adapterRegistry,
249
- adapterCapabilities: ctx.adapterCapabilities,
250
- };
251
- const handlerCtx = { model, registry: ctx.registry, db: ctx.db.kysely, modelAccessOpts };
252
-
253
- const capabilities = new Set(ctx.adapterCapabilities?.[model.source!] ?? ['read']);
254
-
255
- // GET single is always available (read is required for all adapters)
256
- server.get(`${basePath}/:id`, {
257
- ...authHooks,
258
- schema: schemas.get,
259
- handler: getHandler(handlerCtx),
260
- });
261
-
262
- // GET list requires 'list' capability
263
- if (capabilities.has('list') || capabilities.has('read')) {
264
- server.get(basePath, { ...authHooks, schema: schemas.list, handler: listHandler(handlerCtx) });
265
- }
266
-
267
- // POST create requires 'create' capability
268
- if (capabilities.has('create')) {
269
- server.post(basePath, {
270
- ...authHooks,
271
- schema: schemas.create,
272
- handler: createHandler(handlerCtx),
273
- });
274
- }
275
-
276
- // PUT update requires 'update' capability
277
- if (capabilities.has('update')) {
278
- server.put(`${basePath}/:id`, {
279
- ...authHooks,
280
- schema: schemas.update,
281
- handler: updateHandler(handlerCtx),
282
- });
283
- }
284
-
285
- // DELETE requires 'delete' capability
286
- if (capabilities.has('delete')) {
287
- server.delete(`${basePath}/:id`, {
288
- ...authHooks,
289
- schema: schemas.delete,
290
- handler: deleteHandler(handlerCtx),
291
- });
292
- }
293
- }
294
-
295
- // ─── Auth hook assembly ──────────────────────────────────────────────────────
296
-
297
- /** Builds the full set of Fastify auth hooks (onRequest, preHandler, onSend) for a model. */
298
- function buildAuthHooks(model: ResolvedModel, ctx: ModelRouteContext) {
299
- if (!ctx.permissionRegistry) return {};
300
-
301
- const { db, permissionRegistry, scopeRegistry } = ctx;
302
-
303
- const preHandler: Array<(req: FastifyRequest, rep: FastifyReply) => Promise<void>> = [
304
- createModelPermissionGuard(model, permissionRegistry),
305
- ];
306
-
307
- if (scopeRegistry) {
308
- const scopeCtx = { model, scopeRegistry, db };
309
- preHandler.push(createScopeHook(scopeCtx));
310
- preHandler.push(createScopeWriteGuard(scopeCtx));
311
- }
312
-
313
- preHandler.push(createFieldWriteGuard(model));
314
-
315
- return {
316
- onRequest: createAuthHook(db, permissionRegistry),
317
- preHandler,
318
- onSend: createFieldStripHook(model),
319
- };
320
- }
321
-
322
- // ─── Route schema definitions ────────────────────────────────────────────────
323
-
324
- /** Builds Fastify JSON Schema objects for each CRUD operation on a model. */
325
- function buildRouteSchemas(model: ResolvedModel, module: string) {
326
- const tag = module;
327
- const createBody = modelToCreateSchema(model);
328
- const updateBody = modelToUpdateSchema(model);
329
-
330
- const idParams = {
331
- type: 'object' as const,
332
- properties: { id: { type: 'string' as const, description: 'Record ID' } },
333
- required: ['id'] as const,
334
- };
335
-
336
- const listQuerystring = {
337
- type: 'object' as const,
338
- properties: {
339
- page: { type: 'integer' as const, default: 1, description: 'Page number' },
340
- limit: {
341
- type: 'integer' as const,
342
- default: 25,
343
- maximum: 100,
344
- description: 'Records per page',
345
- },
346
- sort: {
347
- type: 'string' as const,
348
- description: 'Sort fields (prefix with - for descending, comma-separated)',
349
- },
350
- fields: {
351
- type: 'string' as const,
352
- description: 'Sparse fieldset (comma-separated field names)',
353
- },
354
- include: {
355
- type: 'string' as const,
356
- description: 'Eager-load relations (comma-separated, dot notation for nested)',
357
- },
358
- includeArchived: {
359
- type: 'string' as const,
360
- description: 'Include soft-deleted (archived) records (true/false)',
361
- },
362
- search: {
363
- type: 'string' as const,
364
- description: 'Search keyword applied across searchable fields',
365
- },
366
- },
367
- additionalProperties: true,
368
- };
369
-
370
- return {
371
- list: { tags: [tag], querystring: listQuerystring },
372
- get: { tags: [tag], params: idParams },
373
- create: { tags: [tag], body: createBody },
374
- update: { tags: [tag], params: idParams, body: updateBody },
375
- delete: { tags: [tag], params: idParams },
376
- };
377
- }
378
-
379
- // ─── OpenAPI response schemas ────────────────────────────────────────────────
380
-
381
- /** Builds OpenAPI response schemas (200, 201, 400, 404, etc.) for a model's endpoints. */
382
- export function buildOpenApiResponseSchemas(model: ResolvedModel) {
383
- const responseSchema = modelToSchemaComponent(model);
384
-
385
- const errorResponse = {
386
- type: 'object' as const,
387
- properties: {
388
- error: {
389
- type: 'object' as const,
390
- properties: {
391
- code: { type: 'string' as const },
392
- message: { type: 'string' as const },
393
- },
394
- },
395
- },
396
- };
397
-
398
- const paginationMeta = {
399
- type: 'object' as const,
400
- properties: {
401
- total: { type: 'integer' as const },
402
- page: { type: 'integer' as const },
403
- limit: { type: 'integer' as const },
404
- totalPages: { type: 'integer' as const },
405
- },
406
- };
407
-
408
- return {
409
- list: {
410
- 200: {
411
- description: 'Paginated list',
412
- type: 'object' as const,
413
- properties: {
414
- data: { type: 'array' as const, items: responseSchema },
415
- meta: paginationMeta,
416
- },
417
- },
418
- },
419
- get: {
420
- 200: {
421
- description: 'Single record',
422
- type: 'object' as const,
423
- properties: { data: responseSchema },
424
- },
425
- 404: errorResponse,
426
- },
427
- create: {
428
- 201: {
429
- description: 'Created record',
430
- type: 'object' as const,
431
- properties: { data: responseSchema },
432
- },
433
- 400: errorResponse,
434
- },
435
- update: {
436
- 200: {
437
- description: 'Updated record',
438
- type: 'object' as const,
439
- properties: { data: responseSchema },
440
- },
441
- 404: errorResponse,
442
- },
443
- delete: {
444
- 204: { description: 'No content', type: 'null' as const },
445
- 404: errorResponse,
446
- },
447
- };
448
- }
package/src/api/server.ts DELETED
@@ -1,147 +0,0 @@
1
- import Fastify from 'fastify';
2
- import type { FastifyInstance, FastifyError } from 'fastify';
3
- import fastifySwagger from '@fastify/swagger';
4
- import fastifySwaggerUi from '@fastify/swagger-ui';
5
- import qs from 'qs';
6
- import type { ServerConfig } from './types.js';
7
- import { AppError } from '../errors.js';
8
-
9
- /**
10
- * Create and configure the Fastify server instance with JSON parsing,
11
- * error handling, and optional OpenAPI docs.
12
- */
13
- export async function createServer(options?: ServerConfig): Promise<FastifyInstance> {
14
- const server = Fastify({
15
- logger: options?.logger ?? { level: 'error' },
16
- genReqId: () => crypto.randomUUID(),
17
- requestIdHeader: options?.requestIdHeader ?? 'x-request-id',
18
- routerOptions: {
19
- querystringParser: (str) => qs.parse(str),
20
- },
21
- });
22
-
23
- const docsEnabled = options?.docs !== false;
24
- if (docsEnabled) {
25
- await registerOpenApiDocs(server, options);
26
- }
27
-
28
- registerJsonParser(server);
29
- registerErrorHandler(server);
30
-
31
- return server;
32
- }
33
-
34
- /** Register @fastify/swagger and swagger-ui for API documentation. */
35
- async function registerOpenApiDocs(server: FastifyInstance, options?: ServerConfig): Promise<void> {
36
- await server.register(fastifySwagger, {
37
- openapi: {
38
- openapi: '3.1.0',
39
- info: {
40
- title: 'Rangka API',
41
- version: '1.0.0',
42
- },
43
- tags: options?.tags ?? [],
44
- },
45
- });
46
-
47
- await server.register(fastifySwaggerUi, {
48
- routePrefix: '/api/docs',
49
- });
50
- }
51
-
52
- /** Override the default JSON content-type parser with explicit error handling. */
53
- function registerJsonParser(server: FastifyInstance): void {
54
- server.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
55
- try {
56
- const str = (body as string).trim();
57
- if (!str) {
58
- done(null, undefined);
59
- return;
60
- }
61
- const parsed = JSON.parse(str);
62
- done(null, parsed);
63
- } catch (err: unknown) {
64
- done(err as Error, undefined);
65
- }
66
- });
67
- }
68
-
69
- /** Normalize all errors into a consistent { error: { code, message, details? } } shape. */
70
- function registerErrorHandler(server: FastifyInstance): void {
71
- server.setErrorHandler((error: FastifyError | AppError, request, reply) => {
72
- if (error instanceof AppError) {
73
- if (error.statusCode >= 500) {
74
- request.log.error(
75
- { err: error, url: request.url },
76
- 'AppError %s: %s',
77
- error.code,
78
- error.message,
79
- );
80
- }
81
- const response: Record<string, unknown> = {
82
- error: {
83
- code: error.code,
84
- message: error.message,
85
- ...(error.details !== undefined && { details: error.details }),
86
- },
87
- };
88
- return reply.status(error.statusCode).send(response);
89
- }
90
-
91
- // Handle Postgres/database errors with meaningful messages
92
-
93
- const pgCode = (error as unknown as Record<string, unknown>).code;
94
- if (typeof pgCode === 'string' && pgCode.match(/^[0-9A-Z]{5}$/)) {
95
- const detail = mapPgError(pgCode, error.message);
96
- request.log.error({ err: error, url: request.url, pgCode }, 'Database error: %s', detail);
97
- return reply.status(500).send({
98
- error: { code: 'INTERNAL_ERROR', message: 'An internal error occurred' },
99
- });
100
- }
101
-
102
- const statusCode = error.statusCode ?? 500;
103
- if (statusCode >= 500) {
104
- request.log.error({ err: error, url: request.url }, 'Unhandled error: %s', error.message);
105
- }
106
- const response = {
107
- error: {
108
- code: error.code ?? 'INTERNAL_ERROR',
109
- message: statusCode >= 500 ? 'An internal error occurred' : error.message,
110
- ...(error.validation && { details: error.validation }),
111
- },
112
- };
113
- reply.status(statusCode).send(response);
114
- });
115
- }
116
-
117
- function mapPgError(code: string, raw: string): string {
118
- switch (code) {
119
- case '42P01':
120
- return `Table does not exist: ${extractRelation(raw)}`;
121
- case '42703':
122
- return `Column does not exist: ${extractDetail(raw)}`;
123
- case '23505':
124
- return `Duplicate value violates unique constraint`;
125
- case '23503':
126
- return `Referenced record does not exist (foreign key violation)`;
127
- case '23502':
128
- return `Missing required field (not-null violation)`;
129
- case '42P02':
130
- return `Invalid query parameter`;
131
- case '08001':
132
- case '08006':
133
- return `Database connection failed`;
134
- default:
135
- return `Database error (${code})`;
136
- }
137
- }
138
-
139
- function extractRelation(msg: string): string {
140
- const match = msg.match(/relation "([^"]+)"/);
141
- return match ? match[1] : 'unknown';
142
- }
143
-
144
- function extractDetail(msg: string): string {
145
- const match = msg.match(/column "([^"]+)"/);
146
- return match ? match[1] : 'unknown';
147
- }
package/src/api/types.ts DELETED
@@ -1,16 +0,0 @@
1
- export interface ServerConfig {
2
- port?: number;
3
- host?: string;
4
- logger?: boolean | { level: string };
5
- requestIdHeader?: string;
6
- docs?: boolean;
7
- tags?: Array<{ name: string; description?: string }>;
8
- }
9
-
10
- export interface ApiDefinition {
11
- method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
12
- path: string;
13
- roles?: string[];
14
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- handler: (request: any, reply: any) => Promise<unknown>;
16
- }