@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,226 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { resolveModelIncludes } from '../include-resolver.js';
|
|
3
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
4
|
-
import type { ModelRelationship } from '../../schema/types.js';
|
|
5
|
-
|
|
6
|
-
function makeRegistry(relationships: ModelRelationship[]): SchemaRegistry {
|
|
7
|
-
return {
|
|
8
|
-
getRelationshipsForModel: (model: string) => relationships.filter((r) => r.from === model),
|
|
9
|
-
getModel: (qn: string) => ({ qualifiedName: qn }),
|
|
10
|
-
getAllModels: () => [],
|
|
11
|
-
getFieldsForModel: () => [],
|
|
12
|
-
} as unknown as SchemaRegistry;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function makeMockDb(tables: Record<string, any[]>) {
|
|
16
|
-
const db: any = {
|
|
17
|
-
selectFrom: vi.fn((table: string) => {
|
|
18
|
-
const q: any = {
|
|
19
|
-
_table: table,
|
|
20
|
-
_wheres: [] as any[],
|
|
21
|
-
};
|
|
22
|
-
q.selectAll = vi.fn((..._args: any[]) => q);
|
|
23
|
-
q.select = vi.fn(() => q);
|
|
24
|
-
q.innerJoin = vi.fn(() => q);
|
|
25
|
-
q.where = vi.fn((field: string, op: string, value: any) => {
|
|
26
|
-
q._wheres.push({ field, op, value });
|
|
27
|
-
return q;
|
|
28
|
-
});
|
|
29
|
-
q.execute = vi.fn(async () => {
|
|
30
|
-
const tableData = tables[table] ?? [];
|
|
31
|
-
const inFilter = q._wheres.find((w: any) => w.op === 'in');
|
|
32
|
-
if (inFilter) {
|
|
33
|
-
return tableData.filter((row: any) => {
|
|
34
|
-
const fieldName = inFilter.field.includes('.')
|
|
35
|
-
? inFilter.field.split('.').pop()
|
|
36
|
-
: inFilter.field;
|
|
37
|
-
return inFilter.value.includes(row[fieldName]);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return tableData;
|
|
41
|
-
});
|
|
42
|
-
return q;
|
|
43
|
-
}),
|
|
44
|
-
};
|
|
45
|
-
return db;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('resolveModelIncludes', () => {
|
|
49
|
-
describe('link relationship', () => {
|
|
50
|
-
it('resolves link by collecting IDs and batch-fetching', async () => {
|
|
51
|
-
const relationships: ModelRelationship[] = [
|
|
52
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
53
|
-
];
|
|
54
|
-
const registry = makeRegistry(relationships);
|
|
55
|
-
const db = makeMockDb({
|
|
56
|
-
billing__Customer: [
|
|
57
|
-
{ id: 'c1', name: 'Acme' },
|
|
58
|
-
{ id: 'c2', name: 'Beta' },
|
|
59
|
-
],
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const records = [
|
|
63
|
-
{ id: 'o1', customer: 'c1' },
|
|
64
|
-
{ id: 'o2', customer: 'c2' },
|
|
65
|
-
{ id: 'o3', customer: 'c1' },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
await resolveModelIncludes(records, ['customer'], registry, db, 'sales.Order');
|
|
69
|
-
|
|
70
|
-
expect(records[0].customer).toEqual({ id: 'c1', name: 'Acme' });
|
|
71
|
-
expect(records[1].customer).toEqual({ id: 'c2', name: 'Beta' });
|
|
72
|
-
expect(records[2].customer).toEqual({ id: 'c1', name: 'Acme' });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('sets null when link ID is null', async () => {
|
|
76
|
-
const relationships: ModelRelationship[] = [
|
|
77
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
78
|
-
];
|
|
79
|
-
const registry = makeRegistry(relationships);
|
|
80
|
-
const db = makeMockDb({ billing__Customer: [] });
|
|
81
|
-
|
|
82
|
-
const records = [{ id: 'o1', customer: null }];
|
|
83
|
-
await resolveModelIncludes(records, ['customer'], registry, db, 'sales.Order');
|
|
84
|
-
|
|
85
|
-
expect(records[0].customer).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('sets null when referenced record does not exist', async () => {
|
|
89
|
-
const relationships: ModelRelationship[] = [
|
|
90
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
91
|
-
];
|
|
92
|
-
const registry = makeRegistry(relationships);
|
|
93
|
-
const db = makeMockDb({ billing__Customer: [] });
|
|
94
|
-
|
|
95
|
-
const records = [{ id: 'o1', customer: 'non-existent' }];
|
|
96
|
-
await resolveModelIncludes(records, ['customer'], registry, db, 'sales.Order');
|
|
97
|
-
|
|
98
|
-
expect(records[0].customer).toBeNull();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('deduplicates IDs for batch fetch', async () => {
|
|
102
|
-
const relationships: ModelRelationship[] = [
|
|
103
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
104
|
-
];
|
|
105
|
-
const registry = makeRegistry(relationships);
|
|
106
|
-
const db = makeMockDb({
|
|
107
|
-
billing__Customer: [{ id: 'c1', name: 'Acme' }],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const records = [
|
|
111
|
-
{ id: 'o1', customer: 'c1' },
|
|
112
|
-
{ id: 'o2', customer: 'c1' },
|
|
113
|
-
{ id: 'o3', customer: 'c1' },
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
await resolveModelIncludes(records, ['customer'], registry, db, 'sales.Order');
|
|
117
|
-
|
|
118
|
-
expect(db.selectFrom).toHaveBeenCalledTimes(1);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe('hasMany relationship', () => {
|
|
123
|
-
it('resolves hasMany by fetching children by foreign key', async () => {
|
|
124
|
-
const relationships: ModelRelationship[] = [
|
|
125
|
-
{
|
|
126
|
-
type: 'hasMany',
|
|
127
|
-
from: 'sales.Order',
|
|
128
|
-
field: 'lineItems',
|
|
129
|
-
to: 'sales.LineItem',
|
|
130
|
-
foreignKey: 'order_id',
|
|
131
|
-
},
|
|
132
|
-
];
|
|
133
|
-
const registry = makeRegistry(relationships);
|
|
134
|
-
const db = makeMockDb({
|
|
135
|
-
sales__LineItem: [
|
|
136
|
-
{ id: 'li1', order_id: 'o1', product: 'Widget' },
|
|
137
|
-
{ id: 'li2', order_id: 'o1', product: 'Gadget' },
|
|
138
|
-
{ id: 'li3', order_id: 'o2', product: 'Thingy' },
|
|
139
|
-
],
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const records: any[] = [
|
|
143
|
-
{ id: 'o1', status: 'confirmed' },
|
|
144
|
-
{ id: 'o2', status: 'draft' },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
await resolveModelIncludes(records, ['lineItems'], registry, db, 'sales.Order');
|
|
148
|
-
|
|
149
|
-
expect(records[0].lineItems).toHaveLength(2);
|
|
150
|
-
expect(records[1].lineItems).toHaveLength(1);
|
|
151
|
-
expect(records[0].lineItems[0].product).toBe('Widget');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('returns empty array when no children exist', async () => {
|
|
155
|
-
const relationships: ModelRelationship[] = [
|
|
156
|
-
{
|
|
157
|
-
type: 'hasMany',
|
|
158
|
-
from: 'sales.Order',
|
|
159
|
-
field: 'lineItems',
|
|
160
|
-
to: 'sales.LineItem',
|
|
161
|
-
foreignKey: 'order_id',
|
|
162
|
-
},
|
|
163
|
-
];
|
|
164
|
-
const registry = makeRegistry(relationships);
|
|
165
|
-
const db = makeMockDb({ sales__LineItem: [] });
|
|
166
|
-
|
|
167
|
-
const records: any[] = [{ id: 'o1', status: 'confirmed' }];
|
|
168
|
-
await resolveModelIncludes(records, ['lineItems'], registry, db, 'sales.Order');
|
|
169
|
-
|
|
170
|
-
expect(records[0].lineItems).toEqual([]);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('unknown relation', () => {
|
|
175
|
-
it('silently skips unknown relation names', async () => {
|
|
176
|
-
const registry = makeRegistry([]);
|
|
177
|
-
const db = makeMockDb({});
|
|
178
|
-
|
|
179
|
-
const records = [{ id: 'o1', name: 'Test' }];
|
|
180
|
-
await resolveModelIncludes(records, ['nonExistent'], registry, db, 'sales.Order');
|
|
181
|
-
|
|
182
|
-
expect(records[0]).toEqual({ id: 'o1', name: 'Test' });
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe('empty records', () => {
|
|
187
|
-
it('does nothing when records array is empty', async () => {
|
|
188
|
-
const relationships: ModelRelationship[] = [
|
|
189
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
190
|
-
];
|
|
191
|
-
const registry = makeRegistry(relationships);
|
|
192
|
-
const db = makeMockDb({});
|
|
193
|
-
|
|
194
|
-
const records: any[] = [];
|
|
195
|
-
await resolveModelIncludes(records, ['customer'], registry, db, 'sales.Order');
|
|
196
|
-
|
|
197
|
-
expect(db.selectFrom).not.toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('multiple includes', () => {
|
|
202
|
-
it('resolves multiple relations on same records', async () => {
|
|
203
|
-
const relationships: ModelRelationship[] = [
|
|
204
|
-
{ type: 'link', from: 'sales.Order', field: 'customer', to: 'billing.Customer' },
|
|
205
|
-
{
|
|
206
|
-
type: 'hasMany',
|
|
207
|
-
from: 'sales.Order',
|
|
208
|
-
field: 'lineItems',
|
|
209
|
-
to: 'sales.LineItem',
|
|
210
|
-
foreignKey: 'order_id',
|
|
211
|
-
},
|
|
212
|
-
];
|
|
213
|
-
const registry = makeRegistry(relationships);
|
|
214
|
-
const db = makeMockDb({
|
|
215
|
-
billing__Customer: [{ id: 'c1', name: 'Acme' }],
|
|
216
|
-
sales__LineItem: [{ id: 'li1', order_id: 'o1', product: 'Widget' }],
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const records: any[] = [{ id: 'o1', customer: 'c1' }];
|
|
220
|
-
await resolveModelIncludes(records, ['customer', 'lineItems'], registry, db, 'sales.Order');
|
|
221
|
-
|
|
222
|
-
expect(records[0].customer).toEqual({ id: 'c1', name: 'Acme' });
|
|
223
|
-
expect(records[0].lineItems).toHaveLength(1);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { createModelAccess } from '../index.js';
|
|
3
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
4
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
5
|
-
import type { RequestContext } from '../../auth/types.js';
|
|
6
|
-
|
|
7
|
-
function makeModel(qualifiedName: string, traits: string[] = []): ResolvedModel {
|
|
8
|
-
return {
|
|
9
|
-
qualifiedName,
|
|
10
|
-
app: 'test',
|
|
11
|
-
module: qualifiedName.split('.')[0],
|
|
12
|
-
name: qualifiedName.split('.')[1],
|
|
13
|
-
auditLog: false,
|
|
14
|
-
traits,
|
|
15
|
-
fields: [
|
|
16
|
-
{ name: 'id', config: { type: 'uuid' }, provenance: { source: 'base' } },
|
|
17
|
-
{ name: 'name', config: { type: 'string', required: true }, provenance: { source: 'base' } },
|
|
18
|
-
{ name: 'status', config: { type: 'string' }, provenance: { source: 'base' } },
|
|
19
|
-
],
|
|
20
|
-
indexes: [],
|
|
21
|
-
} as ResolvedModel;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeRegistry(models: ResolvedModel[]): SchemaRegistry {
|
|
25
|
-
return {
|
|
26
|
-
getModel: (qn: string) => models.find((m) => m.qualifiedName === qn),
|
|
27
|
-
getAllModels: () => models,
|
|
28
|
-
getRelationshipsForModel: () => [],
|
|
29
|
-
getFieldsForModel: (qn: string) => models.find((m) => m.qualifiedName === qn)?.fields ?? [],
|
|
30
|
-
} as unknown as SchemaRegistry;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function makeMockDb() {
|
|
34
|
-
const rows = [
|
|
35
|
-
{ id: 'r1', name: 'Record 1', status: 'active' },
|
|
36
|
-
{ id: 'r2', name: 'Record 2', status: 'draft' },
|
|
37
|
-
{ id: 'r3', name: 'Record 3', status: 'active' },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const db: any = {
|
|
41
|
-
selectFrom: vi.fn(() => {
|
|
42
|
-
const q: any = { _wheres: [] as any[], _limit: undefined, _offset: undefined };
|
|
43
|
-
q.selectAll = vi.fn(() => q);
|
|
44
|
-
q.select = vi.fn(() => q);
|
|
45
|
-
q.where = vi.fn((field: string, op: string, value: any) => {
|
|
46
|
-
q._wheres.push({ field, op, value });
|
|
47
|
-
return q;
|
|
48
|
-
});
|
|
49
|
-
q.orderBy = vi.fn(() => q);
|
|
50
|
-
q.limit = vi.fn((n: number) => {
|
|
51
|
-
q._limit = n;
|
|
52
|
-
return q;
|
|
53
|
-
});
|
|
54
|
-
q.offset = vi.fn((n: number) => {
|
|
55
|
-
q._offset = n;
|
|
56
|
-
return q;
|
|
57
|
-
});
|
|
58
|
-
q.execute = vi.fn(async () => {
|
|
59
|
-
let result = [...rows];
|
|
60
|
-
for (const w of q._wheres) {
|
|
61
|
-
if (w.op === '=') result = result.filter((r: any) => r[w.field] === w.value);
|
|
62
|
-
if (w.op === 'in') result = result.filter((r: any) => w.value.includes(r[w.field]));
|
|
63
|
-
}
|
|
64
|
-
if (q._limit !== undefined) result = result.slice(0, q._limit);
|
|
65
|
-
return result;
|
|
66
|
-
});
|
|
67
|
-
q.executeTakeFirst = vi.fn(async () => {
|
|
68
|
-
const result = await q.execute();
|
|
69
|
-
return result[0] ?? null;
|
|
70
|
-
});
|
|
71
|
-
q.compile = vi.fn(() => ({ sql: '', parameters: [] }));
|
|
72
|
-
return q;
|
|
73
|
-
}),
|
|
74
|
-
insertInto: vi.fn(() => {
|
|
75
|
-
const q: any = {};
|
|
76
|
-
q.values = vi.fn((_data: any) => q);
|
|
77
|
-
q.returningAll = vi.fn(() => q);
|
|
78
|
-
q.executeTakeFirstOrThrow = vi.fn(async () => ({ ...rows[0], id: 'new-id' }));
|
|
79
|
-
return q;
|
|
80
|
-
}),
|
|
81
|
-
updateTable: vi.fn(() => {
|
|
82
|
-
const q: any = {};
|
|
83
|
-
q.set = vi.fn(() => q);
|
|
84
|
-
q.where = vi.fn(() => q);
|
|
85
|
-
q.returningAll = vi.fn(() => q);
|
|
86
|
-
q.executeTakeFirstOrThrow = vi.fn(async () => ({
|
|
87
|
-
id: 'r1',
|
|
88
|
-
name: 'Updated',
|
|
89
|
-
status: 'active',
|
|
90
|
-
}));
|
|
91
|
-
q.execute = vi.fn(async () => {});
|
|
92
|
-
return q;
|
|
93
|
-
}),
|
|
94
|
-
deleteFrom: vi.fn(() => {
|
|
95
|
-
const q: any = {};
|
|
96
|
-
q.where = vi.fn(() => q);
|
|
97
|
-
q.execute = vi.fn(async () => {});
|
|
98
|
-
return q;
|
|
99
|
-
}),
|
|
100
|
-
};
|
|
101
|
-
return db;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
describe('createModelAccess', () => {
|
|
105
|
-
const model = makeModel('sales.Order');
|
|
106
|
-
const registry = makeRegistry([model]);
|
|
107
|
-
|
|
108
|
-
it('returns an object with get, query, create, update, delete', () => {
|
|
109
|
-
const db = makeMockDb();
|
|
110
|
-
const access = createModelAccess({ db, registry });
|
|
111
|
-
|
|
112
|
-
expect(access.get).toBeTypeOf('function');
|
|
113
|
-
expect(access.query).toBeTypeOf('function');
|
|
114
|
-
expect(access.create).toBeTypeOf('function');
|
|
115
|
-
expect(access.update).toBeTypeOf('function');
|
|
116
|
-
expect(access.delete).toBeTypeOf('function');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('get', () => {
|
|
120
|
-
it('returns a single record by id', async () => {
|
|
121
|
-
const db = makeMockDb();
|
|
122
|
-
const access = createModelAccess({ db, registry });
|
|
123
|
-
|
|
124
|
-
const result = await access.get('sales.Order', 'r1');
|
|
125
|
-
expect(result).toEqual({ id: 'r1', name: 'Record 1', status: 'active' });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('returns null when record not found', async () => {
|
|
129
|
-
const db = makeMockDb();
|
|
130
|
-
db.selectFrom = vi.fn(() => {
|
|
131
|
-
const q: any = {};
|
|
132
|
-
q.selectAll = vi.fn(() => q);
|
|
133
|
-
q.where = vi.fn(() => q);
|
|
134
|
-
q.executeTakeFirst = vi.fn(async () => null);
|
|
135
|
-
return q;
|
|
136
|
-
});
|
|
137
|
-
const access = createModelAccess({ db, registry });
|
|
138
|
-
|
|
139
|
-
const result = await access.get('sales.Order', 'non-existent');
|
|
140
|
-
expect(result).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('throws when model is not found in registry', async () => {
|
|
144
|
-
const db = makeMockDb();
|
|
145
|
-
const access = createModelAccess({ db, registry });
|
|
146
|
-
|
|
147
|
-
await expect(access.get('unknown.Model', 'r1')).rejects.toThrow(/model not found/i);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('query', () => {
|
|
152
|
-
it('returns a chainable query builder', () => {
|
|
153
|
-
const db = makeMockDb();
|
|
154
|
-
const access = createModelAccess({ db, registry });
|
|
155
|
-
|
|
156
|
-
const q = access.query('sales.Order');
|
|
157
|
-
expect(q.filter).toBeTypeOf('function');
|
|
158
|
-
expect(q.sort).toBeTypeOf('function');
|
|
159
|
-
expect(q.limit).toBeTypeOf('function');
|
|
160
|
-
expect(q.offset).toBeTypeOf('function');
|
|
161
|
-
expect(q.include).toBeTypeOf('function');
|
|
162
|
-
expect(q.fields).toBeTypeOf('function');
|
|
163
|
-
expect(q.exec).toBeTypeOf('function');
|
|
164
|
-
expect(q.first).toBeTypeOf('function');
|
|
165
|
-
expect(q.count).toBeTypeOf('function');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('throws when model is not found in registry', () => {
|
|
169
|
-
const db = makeMockDb();
|
|
170
|
-
const access = createModelAccess({ db, registry });
|
|
171
|
-
|
|
172
|
-
expect(() => access.query('unknown.Model')).toThrow(/model not found/i);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('create', () => {
|
|
177
|
-
it('creates a record and returns it', async () => {
|
|
178
|
-
const db = makeMockDb();
|
|
179
|
-
const access = createModelAccess({ db, registry });
|
|
180
|
-
|
|
181
|
-
const result = await access.create('sales.Order', { name: 'New Order' });
|
|
182
|
-
expect(result).toBeDefined();
|
|
183
|
-
expect(result.id).toBe('new-id');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('throws when model is not found', async () => {
|
|
187
|
-
const db = makeMockDb();
|
|
188
|
-
const access = createModelAccess({ db, registry });
|
|
189
|
-
|
|
190
|
-
await expect(access.create('unknown.Model', { name: 'X' })).rejects.toThrow(
|
|
191
|
-
/model not found/i,
|
|
192
|
-
);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('throws when required fields are missing at DB level', async () => {
|
|
196
|
-
const db = makeMockDb();
|
|
197
|
-
const access = createModelAccess({ db, registry });
|
|
198
|
-
|
|
199
|
-
// Required field validation is now the caller's responsibility (handlers/middleware).
|
|
200
|
-
// ModelOps no longer validates — it trusts pre-validated data.
|
|
201
|
-
const result = await access.create('sales.Order', { status: 'draft' });
|
|
202
|
-
expect(result).toBeDefined();
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('update', () => {
|
|
207
|
-
it('updates a record and returns it', async () => {
|
|
208
|
-
const db = makeMockDb();
|
|
209
|
-
const access = createModelAccess({ db, registry });
|
|
210
|
-
|
|
211
|
-
const result = await access.update('sales.Order', 'r1', { name: 'Updated' });
|
|
212
|
-
expect(result).toBeDefined();
|
|
213
|
-
expect(result.id).toBe('r1');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('throws when model is not found', async () => {
|
|
217
|
-
const db = makeMockDb();
|
|
218
|
-
const access = createModelAccess({ db, registry });
|
|
219
|
-
|
|
220
|
-
await expect(access.update('unknown.Model', 'r1', { name: 'X' })).rejects.toThrow(
|
|
221
|
-
/model not found/i,
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('delete', () => {
|
|
227
|
-
it('deletes a record and returns it', async () => {
|
|
228
|
-
const db = makeMockDb();
|
|
229
|
-
const access = createModelAccess({ db, registry });
|
|
230
|
-
|
|
231
|
-
const result = await access.delete('sales.Order', 'r1');
|
|
232
|
-
expect(result).toBeDefined();
|
|
233
|
-
expect(result.id).toBe('r1');
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('throws when model is not found', async () => {
|
|
237
|
-
const db = makeMockDb();
|
|
238
|
-
const access = createModelAccess({ db, registry });
|
|
239
|
-
|
|
240
|
-
await expect(access.delete('unknown.Model', 'r1')).rejects.toThrow(/model not found/i);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('auth context passthrough', () => {
|
|
245
|
-
it('passes auth to ModelOps (stamping is caller responsibility)', async () => {
|
|
246
|
-
const timestampedModel = makeModel('sales.Order');
|
|
247
|
-
timestampedModel.traits = ['timestamped'];
|
|
248
|
-
timestampedModel.fields.push(
|
|
249
|
-
{
|
|
250
|
-
name: 'created_at',
|
|
251
|
-
config: { type: 'datetime' },
|
|
252
|
-
provenance: { source: 'trait' as const, trait: 'timestamped' },
|
|
253
|
-
} as any,
|
|
254
|
-
{
|
|
255
|
-
name: 'updated_at',
|
|
256
|
-
config: { type: 'datetime' },
|
|
257
|
-
provenance: { source: 'trait' as const, trait: 'timestamped' },
|
|
258
|
-
} as any,
|
|
259
|
-
{
|
|
260
|
-
name: 'created_by',
|
|
261
|
-
config: { type: 'uuid' },
|
|
262
|
-
provenance: { source: 'trait' as const, trait: 'timestamped' },
|
|
263
|
-
} as any,
|
|
264
|
-
{
|
|
265
|
-
name: 'updated_by',
|
|
266
|
-
config: { type: 'uuid' },
|
|
267
|
-
provenance: { source: 'trait' as const, trait: 'timestamped' },
|
|
268
|
-
} as any,
|
|
269
|
-
);
|
|
270
|
-
const tsRegistry = makeRegistry([timestampedModel]);
|
|
271
|
-
|
|
272
|
-
const db = makeMockDb();
|
|
273
|
-
const auth: RequestContext = {
|
|
274
|
-
user: { id: 'u1', email: 'a@b.c', full_name: 'A', enabled: true, password_hash: '' },
|
|
275
|
-
};
|
|
276
|
-
const access = createModelAccess({ db, registry: tsRegistry, auth });
|
|
277
|
-
|
|
278
|
-
// Stamping is now the caller's job — ModelOps receives pre-stamped data.
|
|
279
|
-
// This test verifies ModelOps still accepts and passes through auth without error.
|
|
280
|
-
const result = await access.create('sales.Order', { name: 'Test' });
|
|
281
|
-
expect(result).toBeDefined();
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|