@rangka/core 0.1.1 → 0.1.3
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 -25
- 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,433 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ValidationError } from '../errors.js';
|
|
3
|
-
import { executeHookPipeline } from '../executor.js';
|
|
4
|
-
import { HookRegistry } from '../registry.js';
|
|
5
|
-
import type { HookDocument } from '../types.js';
|
|
6
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
7
|
-
import type { RequestContext } from '../../auth/types.js';
|
|
8
|
-
|
|
9
|
-
function mockSchema(): SchemaRegistry {
|
|
10
|
-
return {} as unknown as SchemaRegistry;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function mockAuth(): RequestContext {
|
|
14
|
-
return {
|
|
15
|
-
user: { id: '1', email: 'test@test.com' },
|
|
16
|
-
roles: ['Admin'],
|
|
17
|
-
scopeFilters: [],
|
|
18
|
-
} as unknown as RequestContext;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mockKysely() {
|
|
22
|
-
const trxQueries: any[] = [];
|
|
23
|
-
const trx: any = {
|
|
24
|
-
insertInto: (table: string) => {
|
|
25
|
-
const q: any = {};
|
|
26
|
-
q.values = (v: any) => {
|
|
27
|
-
trxQueries.push({ op: 'insert', table, values: v });
|
|
28
|
-
return q;
|
|
29
|
-
};
|
|
30
|
-
q.returningAll = () => q;
|
|
31
|
-
q.executeTakeFirstOrThrow = async () => ({
|
|
32
|
-
id: '1',
|
|
33
|
-
...trxQueries[trxQueries.length - 1]?.values,
|
|
34
|
-
});
|
|
35
|
-
return q;
|
|
36
|
-
},
|
|
37
|
-
updateTable: (table: string) => {
|
|
38
|
-
const q: any = {};
|
|
39
|
-
q.set = (v: any) => {
|
|
40
|
-
trxQueries.push({ op: 'update', table, values: v });
|
|
41
|
-
return q;
|
|
42
|
-
};
|
|
43
|
-
q.where = () => q;
|
|
44
|
-
q.returningAll = () => q;
|
|
45
|
-
q.executeTakeFirstOrThrow = async () => ({
|
|
46
|
-
id: '1',
|
|
47
|
-
...trxQueries[trxQueries.length - 1]?.values,
|
|
48
|
-
});
|
|
49
|
-
return q;
|
|
50
|
-
},
|
|
51
|
-
deleteFrom: (table: string) => {
|
|
52
|
-
const q: any = {};
|
|
53
|
-
q.where = () => q;
|
|
54
|
-
q.execute = async () => {
|
|
55
|
-
trxQueries.push({ op: 'delete', table });
|
|
56
|
-
};
|
|
57
|
-
return q;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const db: any = {
|
|
62
|
-
transaction: () => ({
|
|
63
|
-
execute: async (cb: (trx: any) => Promise<any>) => cb(trx),
|
|
64
|
-
}),
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return { db, trx, trxQueries };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe('executeHookPipeline', () => {
|
|
71
|
-
it('runs full create pipeline: validate → beforeCreate → execute → afterCreate', async () => {
|
|
72
|
-
const order: string[] = [];
|
|
73
|
-
const registry = new HookRegistry();
|
|
74
|
-
registry.register(
|
|
75
|
-
'sales.invoice',
|
|
76
|
-
{
|
|
77
|
-
validate: () => {
|
|
78
|
-
order.push('validate');
|
|
79
|
-
},
|
|
80
|
-
beforeCreate: async () => {
|
|
81
|
-
order.push('beforeCreate');
|
|
82
|
-
},
|
|
83
|
-
afterCreate: async () => {
|
|
84
|
-
order.push('afterCreate');
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
'sales',
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const { db } = mockKysely();
|
|
91
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
92
|
-
|
|
93
|
-
await executeHookPipeline({
|
|
94
|
-
model: 'sales.invoice',
|
|
95
|
-
operation: 'create',
|
|
96
|
-
chain,
|
|
97
|
-
doc: { name: 'INV-001' },
|
|
98
|
-
db,
|
|
99
|
-
schema: mockSchema(),
|
|
100
|
-
auth: mockAuth(),
|
|
101
|
-
execute: async (doc) => {
|
|
102
|
-
order.push('execute');
|
|
103
|
-
return { id: '1', ...doc };
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(order).toEqual(['validate', 'beforeCreate', 'execute', 'afterCreate']);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('runs beforeSave/afterSave for create operations', async () => {
|
|
111
|
-
const order: string[] = [];
|
|
112
|
-
const registry = new HookRegistry();
|
|
113
|
-
registry.register(
|
|
114
|
-
'sales.invoice',
|
|
115
|
-
{
|
|
116
|
-
beforeSave: async () => {
|
|
117
|
-
order.push('beforeSave');
|
|
118
|
-
},
|
|
119
|
-
afterSave: async () => {
|
|
120
|
-
order.push('afterSave');
|
|
121
|
-
},
|
|
122
|
-
beforeCreate: async () => {
|
|
123
|
-
order.push('beforeCreate');
|
|
124
|
-
},
|
|
125
|
-
afterCreate: async () => {
|
|
126
|
-
order.push('afterCreate');
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
'sales',
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const { db } = mockKysely();
|
|
133
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
134
|
-
|
|
135
|
-
await executeHookPipeline({
|
|
136
|
-
model: 'sales.invoice',
|
|
137
|
-
operation: 'create',
|
|
138
|
-
chain,
|
|
139
|
-
doc: { name: 'INV-001' },
|
|
140
|
-
db,
|
|
141
|
-
schema: mockSchema(),
|
|
142
|
-
auth: mockAuth(),
|
|
143
|
-
execute: async (doc) => ({ id: '1', ...doc }),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(order).toEqual(['beforeSave', 'beforeCreate', 'afterCreate', 'afterSave']);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('validate hook rejection aborts pipeline', async () => {
|
|
150
|
-
const registry = new HookRegistry();
|
|
151
|
-
registry.register(
|
|
152
|
-
'sales.invoice',
|
|
153
|
-
{
|
|
154
|
-
validate: (doc) => {
|
|
155
|
-
if (!doc.customer) {
|
|
156
|
-
throw new ValidationError('customer', 'Customer is required');
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
beforeCreate: async () => {
|
|
160
|
-
throw new Error('Should not reach beforeCreate');
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
'sales',
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const { db } = mockKysely();
|
|
167
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
168
|
-
|
|
169
|
-
await expect(
|
|
170
|
-
executeHookPipeline({
|
|
171
|
-
model: 'sales.invoice',
|
|
172
|
-
operation: 'create',
|
|
173
|
-
chain,
|
|
174
|
-
doc: { name: 'INV-001' },
|
|
175
|
-
db,
|
|
176
|
-
schema: mockSchema(),
|
|
177
|
-
auth: mockAuth(),
|
|
178
|
-
execute: async (doc) => doc,
|
|
179
|
-
}),
|
|
180
|
-
).rejects.toThrow(ValidationError);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('beforeCreate can modify the document', async () => {
|
|
184
|
-
const registry = new HookRegistry();
|
|
185
|
-
registry.register(
|
|
186
|
-
'sales.invoice',
|
|
187
|
-
{
|
|
188
|
-
beforeCreate: async (doc) => {
|
|
189
|
-
doc.computed_total = 100;
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
'sales',
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const { db } = mockKysely();
|
|
196
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
197
|
-
let executedDoc: HookDocument | undefined;
|
|
198
|
-
|
|
199
|
-
await executeHookPipeline({
|
|
200
|
-
model: 'sales.invoice',
|
|
201
|
-
operation: 'create',
|
|
202
|
-
chain,
|
|
203
|
-
doc: { name: 'INV-001' },
|
|
204
|
-
db,
|
|
205
|
-
schema: mockSchema(),
|
|
206
|
-
auth: mockAuth(),
|
|
207
|
-
execute: async (doc) => {
|
|
208
|
-
executedDoc = doc;
|
|
209
|
-
return { id: '1', ...doc };
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
expect(executedDoc!.computed_total).toBe(100);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('afterCreate receives the result from execute', async () => {
|
|
217
|
-
const registry = new HookRegistry();
|
|
218
|
-
let afterDoc: HookDocument | undefined;
|
|
219
|
-
registry.register(
|
|
220
|
-
'sales.invoice',
|
|
221
|
-
{
|
|
222
|
-
afterCreate: async (doc) => {
|
|
223
|
-
afterDoc = doc;
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
'sales',
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const { db } = mockKysely();
|
|
230
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
231
|
-
|
|
232
|
-
await executeHookPipeline({
|
|
233
|
-
model: 'sales.invoice',
|
|
234
|
-
operation: 'create',
|
|
235
|
-
chain,
|
|
236
|
-
doc: { name: 'INV-001' },
|
|
237
|
-
db,
|
|
238
|
-
schema: mockSchema(),
|
|
239
|
-
auth: mockAuth(),
|
|
240
|
-
execute: async (doc) => ({ id: '1', ...doc, server_generated: true }),
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
expect(afterDoc!.server_generated).toBe(true);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('rolls back transaction on beforeCreate throw', async () => {
|
|
247
|
-
const registry = new HookRegistry();
|
|
248
|
-
registry.register(
|
|
249
|
-
'sales.invoice',
|
|
250
|
-
{
|
|
251
|
-
beforeCreate: async () => {
|
|
252
|
-
throw new Error('Business rule violated');
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
'sales',
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
let transactionRolledBack = false;
|
|
259
|
-
const db: any = {
|
|
260
|
-
transaction: () => ({
|
|
261
|
-
execute: async (cb: (trx: any) => Promise<any>) => {
|
|
262
|
-
try {
|
|
263
|
-
return await cb({});
|
|
264
|
-
} catch (err) {
|
|
265
|
-
transactionRolledBack = true;
|
|
266
|
-
throw err;
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
}),
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
273
|
-
|
|
274
|
-
await expect(
|
|
275
|
-
executeHookPipeline({
|
|
276
|
-
model: 'sales.invoice',
|
|
277
|
-
operation: 'create',
|
|
278
|
-
chain,
|
|
279
|
-
doc: { name: 'INV-001' },
|
|
280
|
-
db,
|
|
281
|
-
schema: mockSchema(),
|
|
282
|
-
auth: mockAuth(),
|
|
283
|
-
execute: async (doc) => doc,
|
|
284
|
-
}),
|
|
285
|
-
).rejects.toThrow('Business rule violated');
|
|
286
|
-
|
|
287
|
-
expect(transactionRolledBack).toBe(true);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('extension hooks run after base hooks', async () => {
|
|
291
|
-
const order: string[] = [];
|
|
292
|
-
const registry = new HookRegistry();
|
|
293
|
-
registry.register(
|
|
294
|
-
'sales.invoice',
|
|
295
|
-
{
|
|
296
|
-
beforeCreate: async () => {
|
|
297
|
-
order.push('base');
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
'sales',
|
|
301
|
-
);
|
|
302
|
-
registry.register(
|
|
303
|
-
'sales.invoice',
|
|
304
|
-
{
|
|
305
|
-
beforeCreate: async () => {
|
|
306
|
-
order.push('extension');
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
'custom',
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
const { db } = mockKysely();
|
|
313
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
314
|
-
|
|
315
|
-
await executeHookPipeline({
|
|
316
|
-
model: 'sales.invoice',
|
|
317
|
-
operation: 'create',
|
|
318
|
-
chain,
|
|
319
|
-
doc: {},
|
|
320
|
-
db,
|
|
321
|
-
schema: mockSchema(),
|
|
322
|
-
auth: mockAuth(),
|
|
323
|
-
execute: async (doc) => doc,
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
expect(order).toEqual(['base', 'extension']);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('handles update operation with beforeUpdate/afterUpdate', async () => {
|
|
330
|
-
const order: string[] = [];
|
|
331
|
-
const registry = new HookRegistry();
|
|
332
|
-
registry.register(
|
|
333
|
-
'sales.invoice',
|
|
334
|
-
{
|
|
335
|
-
beforeUpdate: async () => {
|
|
336
|
-
order.push('beforeUpdate');
|
|
337
|
-
},
|
|
338
|
-
afterUpdate: async () => {
|
|
339
|
-
order.push('afterUpdate');
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
'sales',
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
const { db } = mockKysely();
|
|
346
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
347
|
-
|
|
348
|
-
await executeHookPipeline({
|
|
349
|
-
model: 'sales.invoice',
|
|
350
|
-
operation: 'update',
|
|
351
|
-
chain,
|
|
352
|
-
doc: { id: '1', name: 'INV-002' },
|
|
353
|
-
db,
|
|
354
|
-
schema: mockSchema(),
|
|
355
|
-
auth: mockAuth(),
|
|
356
|
-
execute: async (doc) => doc,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
expect(order).toEqual(['beforeUpdate', 'afterUpdate']);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('handles delete operation with beforeDelete/afterDelete', async () => {
|
|
363
|
-
const order: string[] = [];
|
|
364
|
-
const registry = new HookRegistry();
|
|
365
|
-
registry.register(
|
|
366
|
-
'sales.invoice',
|
|
367
|
-
{
|
|
368
|
-
beforeDelete: async () => {
|
|
369
|
-
order.push('beforeDelete');
|
|
370
|
-
},
|
|
371
|
-
afterDelete: async () => {
|
|
372
|
-
order.push('afterDelete');
|
|
373
|
-
},
|
|
374
|
-
},
|
|
375
|
-
'sales',
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
const { db } = mockKysely();
|
|
379
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
380
|
-
|
|
381
|
-
await executeHookPipeline({
|
|
382
|
-
model: 'sales.invoice',
|
|
383
|
-
operation: 'delete',
|
|
384
|
-
chain,
|
|
385
|
-
doc: { id: '1' },
|
|
386
|
-
db,
|
|
387
|
-
schema: mockSchema(),
|
|
388
|
-
auth: mockAuth(),
|
|
389
|
-
execute: async (doc) => doc,
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
expect(order).toEqual(['beforeDelete', 'afterDelete']);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('does not run beforeSave/afterSave for delete operations', async () => {
|
|
396
|
-
const order: string[] = [];
|
|
397
|
-
const registry = new HookRegistry();
|
|
398
|
-
registry.register(
|
|
399
|
-
'sales.invoice',
|
|
400
|
-
{
|
|
401
|
-
beforeSave: async () => {
|
|
402
|
-
order.push('beforeSave');
|
|
403
|
-
},
|
|
404
|
-
afterSave: async () => {
|
|
405
|
-
order.push('afterSave');
|
|
406
|
-
},
|
|
407
|
-
beforeDelete: async () => {
|
|
408
|
-
order.push('beforeDelete');
|
|
409
|
-
},
|
|
410
|
-
afterDelete: async () => {
|
|
411
|
-
order.push('afterDelete');
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
'sales',
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const { db } = mockKysely();
|
|
418
|
-
const chain = registry.getChain('sales.invoice')!;
|
|
419
|
-
|
|
420
|
-
await executeHookPipeline({
|
|
421
|
-
model: 'sales.invoice',
|
|
422
|
-
operation: 'delete',
|
|
423
|
-
chain,
|
|
424
|
-
doc: { id: '1' },
|
|
425
|
-
db,
|
|
426
|
-
schema: mockSchema(),
|
|
427
|
-
auth: mockAuth(),
|
|
428
|
-
execute: async (doc) => doc,
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
expect(order).toEqual(['beforeDelete', 'afterDelete']);
|
|
432
|
-
});
|
|
433
|
-
});
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createServer } from '../../api/server.js';
|
|
3
|
-
import { generateRoutes } from '../../api/route-generator.js';
|
|
4
|
-
import { HookRegistry } from '../registry.js';
|
|
5
|
-
import { ValidationError } from '../errors.js';
|
|
6
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
7
|
-
import type { DatabaseClient } from '../../db/client.js';
|
|
8
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
9
|
-
|
|
10
|
-
function makeModel(module: string, name: string, traits: string[] = []): ResolvedModel {
|
|
11
|
-
return {
|
|
12
|
-
qualifiedName: `${module}.${name}`,
|
|
13
|
-
app: 'test',
|
|
14
|
-
module,
|
|
15
|
-
name,
|
|
16
|
-
auditLog: false,
|
|
17
|
-
traits,
|
|
18
|
-
fields: [
|
|
19
|
-
{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } },
|
|
20
|
-
{ name: 'name', config: { type: 'string', required: true }, provenance: { source: 'base' } },
|
|
21
|
-
],
|
|
22
|
-
indexes: [],
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeRegistry(models: ResolvedModel[]): SchemaRegistry {
|
|
27
|
-
return {
|
|
28
|
-
getModelsByModule: () => {
|
|
29
|
-
const map = new Map<string, ResolvedModel[]>();
|
|
30
|
-
for (const m of models) {
|
|
31
|
-
const list = map.get(m.module) ?? [];
|
|
32
|
-
list.push(m);
|
|
33
|
-
map.set(m.module, list);
|
|
34
|
-
}
|
|
35
|
-
return map;
|
|
36
|
-
},
|
|
37
|
-
getRelationshipsForModel: () => [],
|
|
38
|
-
getModel: (name: string) => models.find((m) => m.qualifiedName === name) ?? null,
|
|
39
|
-
} as unknown as SchemaRegistry;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeMockDb(store: Record<string, any> = {}): DatabaseClient {
|
|
43
|
-
const buildQuery = (op?: string) => {
|
|
44
|
-
const q: any = { _where: null, _values: null, _set: null };
|
|
45
|
-
q.select = () => q;
|
|
46
|
-
q.selectAll = () => q;
|
|
47
|
-
q.where = (_field: string, _op: string, val: any) => {
|
|
48
|
-
q._where = val;
|
|
49
|
-
return q;
|
|
50
|
-
};
|
|
51
|
-
q.orderBy = () => q;
|
|
52
|
-
q.offset = () => q;
|
|
53
|
-
q.limit = () => q;
|
|
54
|
-
q.execute = async () => {
|
|
55
|
-
return [];
|
|
56
|
-
};
|
|
57
|
-
q.executeTakeFirst = async () => {
|
|
58
|
-
if (q._where && store[q._where]) return store[q._where];
|
|
59
|
-
return { count: '0' };
|
|
60
|
-
};
|
|
61
|
-
q.executeTakeFirstOrThrow = async () => {
|
|
62
|
-
if (op === 'insert') return { id: 'new-1', ...q._values };
|
|
63
|
-
if (op === 'update') return { ...store[q._where], ...q._set };
|
|
64
|
-
return {};
|
|
65
|
-
};
|
|
66
|
-
q.values = (v: any) => {
|
|
67
|
-
q._values = v;
|
|
68
|
-
return q;
|
|
69
|
-
};
|
|
70
|
-
q.set = (v: any) => {
|
|
71
|
-
q._set = v;
|
|
72
|
-
return q;
|
|
73
|
-
};
|
|
74
|
-
q.returningAll = () => q;
|
|
75
|
-
return q;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const trxProxy: any = {
|
|
79
|
-
selectFrom: () => buildQuery(),
|
|
80
|
-
insertInto: () => buildQuery('insert'),
|
|
81
|
-
updateTable: () => buildQuery('update'),
|
|
82
|
-
deleteFrom: () => buildQuery('delete'),
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
selectFrom: () => buildQuery(),
|
|
87
|
-
insertInto: () => buildQuery('insert'),
|
|
88
|
-
updateTable: () => buildQuery('update'),
|
|
89
|
-
deleteFrom: () => buildQuery('delete'),
|
|
90
|
-
kysely: {
|
|
91
|
-
fn: { countAll: () => ({ as: () => 'count' }) },
|
|
92
|
-
transaction: () => ({
|
|
93
|
-
execute: async (cb: (trx: any) => Promise<any>) => cb(trxProxy),
|
|
94
|
-
}),
|
|
95
|
-
},
|
|
96
|
-
} as unknown as DatabaseClient;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
describe('hooks middleware integration', () => {
|
|
100
|
-
it('create with validation hook rejects invalid doc', async () => {
|
|
101
|
-
const hookRegistry = new HookRegistry();
|
|
102
|
-
hookRegistry.register(
|
|
103
|
-
'sales.invoice',
|
|
104
|
-
{
|
|
105
|
-
validate: (doc) => {
|
|
106
|
-
if (!doc.name) throw new ValidationError('name', 'Name is required');
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
'sales',
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const models = [makeModel('sales', 'invoice')];
|
|
113
|
-
const server = await createServer();
|
|
114
|
-
generateRoutes(server, makeRegistry(models), makeMockDb(), { hookRegistry });
|
|
115
|
-
|
|
116
|
-
const res = await server.inject({
|
|
117
|
-
method: 'POST',
|
|
118
|
-
url: '/api/sales/invoice',
|
|
119
|
-
headers: { 'content-type': 'application/json' },
|
|
120
|
-
payload: JSON.stringify({ name: '' }),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(res.statusCode).toBe(400);
|
|
124
|
-
const body = JSON.parse(res.body);
|
|
125
|
-
expect(body.error.code).toBe('VALIDATION_ERROR');
|
|
126
|
-
expect(body.error.details.field).toBe('name');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('create succeeds when validation passes', async () => {
|
|
130
|
-
const hookRegistry = new HookRegistry();
|
|
131
|
-
hookRegistry.register(
|
|
132
|
-
'sales.invoice',
|
|
133
|
-
{
|
|
134
|
-
validate: (doc) => {
|
|
135
|
-
if (!doc.name) throw new ValidationError('name', 'Name is required');
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
'sales',
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const models = [makeModel('sales', 'invoice')];
|
|
142
|
-
const server = await createServer();
|
|
143
|
-
generateRoutes(server, makeRegistry(models), makeMockDb(), { hookRegistry });
|
|
144
|
-
|
|
145
|
-
const res = await server.inject({
|
|
146
|
-
method: 'POST',
|
|
147
|
-
url: '/api/sales/invoice',
|
|
148
|
-
headers: { 'content-type': 'application/json' },
|
|
149
|
-
payload: JSON.stringify({ name: 'INV-001' }),
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(res.statusCode).toBe(201);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('update with beforeUpdate modifies data', async () => {
|
|
156
|
-
const hookRegistry = new HookRegistry();
|
|
157
|
-
hookRegistry.register(
|
|
158
|
-
'sales.invoice',
|
|
159
|
-
{
|
|
160
|
-
beforeUpdate: async (doc) => {
|
|
161
|
-
doc.updated_flag = true;
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
'sales',
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const store: Record<string, any> = { '1': { id: '1', name: 'INV-001' } };
|
|
168
|
-
const models = [makeModel('sales', 'invoice')];
|
|
169
|
-
const server = await createServer();
|
|
170
|
-
generateRoutes(server, makeRegistry(models), makeMockDb(store), { hookRegistry });
|
|
171
|
-
|
|
172
|
-
const res = await server.inject({
|
|
173
|
-
method: 'PUT',
|
|
174
|
-
url: '/api/sales/invoice/1',
|
|
175
|
-
headers: { 'content-type': 'application/json' },
|
|
176
|
-
payload: JSON.stringify({ name: 'INV-002' }),
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(res.statusCode).toBe(200);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('delete with afterDelete runs side effect', async () => {
|
|
183
|
-
let sideEffectRan = false;
|
|
184
|
-
const hookRegistry = new HookRegistry();
|
|
185
|
-
hookRegistry.register(
|
|
186
|
-
'sales.invoice',
|
|
187
|
-
{
|
|
188
|
-
afterDelete: async () => {
|
|
189
|
-
sideEffectRan = true;
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
'sales',
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const store: Record<string, any> = { '1': { id: '1', name: 'INV-001' } };
|
|
196
|
-
const models = [makeModel('sales', 'invoice')];
|
|
197
|
-
const server = await createServer();
|
|
198
|
-
generateRoutes(server, makeRegistry(models), makeMockDb(store), { hookRegistry });
|
|
199
|
-
|
|
200
|
-
const res = await server.inject({
|
|
201
|
-
method: 'DELETE',
|
|
202
|
-
url: '/api/sales/invoice/1',
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(res.statusCode).toBe(204);
|
|
206
|
-
expect(sideEffectRan).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('models without hooks use standard handlers', async () => {
|
|
210
|
-
const hookRegistry = new HookRegistry();
|
|
211
|
-
const models = [makeModel('sales', 'invoice')];
|
|
212
|
-
const server = await createServer();
|
|
213
|
-
generateRoutes(server, makeRegistry(models), makeMockDb(), { hookRegistry });
|
|
214
|
-
|
|
215
|
-
const res = await server.inject({
|
|
216
|
-
method: 'POST',
|
|
217
|
-
url: '/api/sales/invoice',
|
|
218
|
-
headers: { 'content-type': 'application/json' },
|
|
219
|
-
payload: JSON.stringify({ name: 'INV-001' }),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
expect(res.statusCode).toBe(201);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { HookRegistry } from '../registry.js';
|
|
3
|
-
|
|
4
|
-
describe('HookRegistry', () => {
|
|
5
|
-
it('registers base hooks and retrieves chain', () => {
|
|
6
|
-
const registry = new HookRegistry();
|
|
7
|
-
const hooks = { validate: () => {} };
|
|
8
|
-
registry.register('sales.invoice', hooks, 'sales');
|
|
9
|
-
|
|
10
|
-
const chain = registry.getChain('sales.invoice');
|
|
11
|
-
expect(chain).toBeDefined();
|
|
12
|
-
expect(chain!.entries).toHaveLength(1);
|
|
13
|
-
expect(chain!.entries[0].source).toBe('sales');
|
|
14
|
-
expect(chain!.entries[0].hooks).toBe(hooks);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns undefined for unknown model', () => {
|
|
18
|
-
const registry = new HookRegistry();
|
|
19
|
-
expect(registry.getChain('unknown.model')).toBeUndefined();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('hasHooks returns false for unknown model', () => {
|
|
23
|
-
const registry = new HookRegistry();
|
|
24
|
-
expect(registry.hasHooks('unknown.model')).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('merges extension hooks after base in registration order', () => {
|
|
28
|
-
const registry = new HookRegistry();
|
|
29
|
-
const baseHooks = { beforeCreate: async () => {} };
|
|
30
|
-
const extHooks = { beforeCreate: async () => {} };
|
|
31
|
-
|
|
32
|
-
registry.register('sales.invoice', baseHooks, 'sales');
|
|
33
|
-
registry.register('sales.invoice', extHooks, 'custom');
|
|
34
|
-
|
|
35
|
-
const chain = registry.getChain('sales.invoice');
|
|
36
|
-
expect(chain!.entries).toHaveLength(2);
|
|
37
|
-
expect(chain!.entries[0].source).toBe('sales');
|
|
38
|
-
expect(chain!.entries[1].source).toBe('custom');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('maintains app dependency order across multiple extensions', () => {
|
|
42
|
-
const registry = new HookRegistry();
|
|
43
|
-
registry.register('sales.invoice', { validate: () => {} }, 'sales');
|
|
44
|
-
registry.register('sales.invoice', { validate: () => {} }, 'addon_a');
|
|
45
|
-
registry.register('sales.invoice', { validate: () => {} }, 'addon_b');
|
|
46
|
-
|
|
47
|
-
const chain = registry.getChain('sales.invoice');
|
|
48
|
-
expect(chain!.entries.map((e) => e.source)).toEqual(['sales', 'addon_a', 'addon_b']);
|
|
49
|
-
});
|
|
50
|
-
});
|