@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.
- package/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -18
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- 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
|
-
}
|