@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,393 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { resolveIncludes } from '../include-resolver.js';
|
|
3
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
4
|
-
import type { ModelRelationship } from '../../schema/types.js';
|
|
5
|
-
import type { ParsedInclude } from '../query-parser.js';
|
|
6
|
-
|
|
7
|
-
// --- Test helpers ---
|
|
8
|
-
|
|
9
|
-
function makeRegistry(relationships: ModelRelationship[]): SchemaRegistry {
|
|
10
|
-
return {
|
|
11
|
-
getRelationshipsForModel: (model: string) => relationships.filter((r) => r.from === model),
|
|
12
|
-
getModel: (qn: string) => ({ qualifiedName: qn }),
|
|
13
|
-
} as unknown as SchemaRegistry;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Creates a mock Kysely-like DB that stores tables in memory.
|
|
18
|
-
* Supports selectFrom with where('id', 'in', [...]) and where(field, 'in', [...]).
|
|
19
|
-
* Also supports innerJoin for manyToMany queries.
|
|
20
|
-
*/
|
|
21
|
-
function makeMockDb(tables: Record<string, any[]>) {
|
|
22
|
-
const db: any = {
|
|
23
|
-
selectFrom: vi.fn((table: string) => {
|
|
24
|
-
const q: any = {
|
|
25
|
-
_table: table,
|
|
26
|
-
_wheres: [] as any[],
|
|
27
|
-
_joins: [] as any[],
|
|
28
|
-
_selectAll: false,
|
|
29
|
-
_selects: [] as string[],
|
|
30
|
-
};
|
|
31
|
-
q.selectAll = vi.fn(() => {
|
|
32
|
-
q._selectAll = true;
|
|
33
|
-
return q;
|
|
34
|
-
});
|
|
35
|
-
q.select = vi.fn((fields: string[]) => {
|
|
36
|
-
q._selects.push(...fields);
|
|
37
|
-
return q;
|
|
38
|
-
});
|
|
39
|
-
q.where = vi.fn((field: string, op: string, value: any) => {
|
|
40
|
-
q._wheres.push({ field, op, value });
|
|
41
|
-
return q;
|
|
42
|
-
});
|
|
43
|
-
q.innerJoin = vi.fn((junctionTable: string, joinCol1: string, joinCol2: string) => {
|
|
44
|
-
q._joins.push({ junctionTable, joinCol1, joinCol2 });
|
|
45
|
-
return q;
|
|
46
|
-
});
|
|
47
|
-
q.execute = vi.fn(async () => {
|
|
48
|
-
let data = tables[q._table] ?? [];
|
|
49
|
-
|
|
50
|
-
// Handle manyToMany join
|
|
51
|
-
if (q._joins.length > 0) {
|
|
52
|
-
const join = q._joins[0];
|
|
53
|
-
const junctionData = tables[join.junctionTable] ?? [];
|
|
54
|
-
// join.joinCol1 = "junction.target_fk", join.joinCol2 = "target.id"
|
|
55
|
-
const targetFk = join.joinCol1.split('.')[1];
|
|
56
|
-
const sourceFk = q._wheres.find((w: any) => w.op === 'in')?.field?.split('.')[1];
|
|
57
|
-
const sourceIds = q._wheres.find((w: any) => w.op === 'in')?.value ?? [];
|
|
58
|
-
|
|
59
|
-
const results: any[] = [];
|
|
60
|
-
for (const jRow of junctionData) {
|
|
61
|
-
if (sourceIds.includes(jRow[sourceFk!])) {
|
|
62
|
-
const target = data.find((t: any) => t.id === jRow[targetFk]);
|
|
63
|
-
if (target) {
|
|
64
|
-
results.push({ ...target, _source_fk: jRow[sourceFk!] });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return results;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Handle IN queries
|
|
72
|
-
for (const w of q._wheres) {
|
|
73
|
-
if (w.op === 'in') {
|
|
74
|
-
const field = w.field;
|
|
75
|
-
data = data.filter((r: any) => w.value.includes(r[field]));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return data;
|
|
79
|
-
});
|
|
80
|
-
return q;
|
|
81
|
-
}),
|
|
82
|
-
};
|
|
83
|
-
return db;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function makeRequest(scopeFilters: any[] = []): any {
|
|
87
|
-
return {
|
|
88
|
-
_scopeFilters: scopeFilters,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// --- Tests ---
|
|
93
|
-
|
|
94
|
-
describe('resolveIncludes', () => {
|
|
95
|
-
describe('link (belongs-to)', () => {
|
|
96
|
-
const relationships: ModelRelationship[] = [
|
|
97
|
-
{ type: 'link', from: 'sales.invoice', field: 'customer', to: 'sales.customer' },
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
it('resolves single FK to full object', async () => {
|
|
101
|
-
const registry = makeRegistry(relationships);
|
|
102
|
-
const db = makeMockDb({
|
|
103
|
-
sales__customer: [
|
|
104
|
-
{ id: 'C-001', name: 'Acme Corp' },
|
|
105
|
-
{ id: 'C-002', name: 'Globex' },
|
|
106
|
-
],
|
|
107
|
-
});
|
|
108
|
-
const records = [
|
|
109
|
-
{ id: 'inv-001', customer: 'C-001', total: 100 },
|
|
110
|
-
{ id: 'inv-002', customer: 'C-002', total: 200 },
|
|
111
|
-
];
|
|
112
|
-
const includes: ParsedInclude[] = [{ relation: 'customer' }];
|
|
113
|
-
|
|
114
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
115
|
-
|
|
116
|
-
expect(records[0].customer).toEqual({ id: 'C-001', name: 'Acme Corp' });
|
|
117
|
-
expect(records[1].customer).toEqual({ id: 'C-002', name: 'Globex' });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('returns null for orphaned FK', async () => {
|
|
121
|
-
const registry = makeRegistry(relationships);
|
|
122
|
-
const db = makeMockDb({
|
|
123
|
-
sales__customer: [{ id: 'C-001', name: 'Acme Corp' }],
|
|
124
|
-
});
|
|
125
|
-
const records = [{ id: 'inv-001', customer: 'C-999', total: 100 }];
|
|
126
|
-
const includes: ParsedInclude[] = [{ relation: 'customer' }];
|
|
127
|
-
|
|
128
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
129
|
-
|
|
130
|
-
expect(records[0].customer).toBeNull();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('deduplicates IDs in batch query', async () => {
|
|
134
|
-
const registry = makeRegistry(relationships);
|
|
135
|
-
const db = makeMockDb({
|
|
136
|
-
sales__customer: [{ id: 'C-001', name: 'Acme Corp' }],
|
|
137
|
-
});
|
|
138
|
-
const records = [
|
|
139
|
-
{ id: 'inv-001', customer: 'C-001', total: 100 },
|
|
140
|
-
{ id: 'inv-002', customer: 'C-001', total: 200 },
|
|
141
|
-
];
|
|
142
|
-
const includes: ParsedInclude[] = [{ relation: 'customer' }];
|
|
143
|
-
|
|
144
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
145
|
-
|
|
146
|
-
expect(records[0].customer).toEqual({ id: 'C-001', name: 'Acme Corp' });
|
|
147
|
-
expect(records[1].customer).toEqual({ id: 'C-001', name: 'Acme Corp' });
|
|
148
|
-
// Only one selectFrom call (batched)
|
|
149
|
-
expect(db.selectFrom).toHaveBeenCalledTimes(1);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('hasMany (one-to-many)', () => {
|
|
154
|
-
const relationships: ModelRelationship[] = [
|
|
155
|
-
{
|
|
156
|
-
type: 'hasMany',
|
|
157
|
-
from: 'sales.invoice',
|
|
158
|
-
field: 'items',
|
|
159
|
-
to: 'sales.invoice_item',
|
|
160
|
-
foreignKey: 'invoice_id',
|
|
161
|
-
},
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
it('returns array grouped by parent', async () => {
|
|
165
|
-
const registry = makeRegistry(relationships);
|
|
166
|
-
const db = makeMockDb({
|
|
167
|
-
sales__invoice_item: [
|
|
168
|
-
{ id: 'item-001', invoice_id: 'inv-001', qty: 10 },
|
|
169
|
-
{ id: 'item-002', invoice_id: 'inv-001', qty: 5 },
|
|
170
|
-
{ id: 'item-003', invoice_id: 'inv-002', qty: 3 },
|
|
171
|
-
],
|
|
172
|
-
});
|
|
173
|
-
const records: any[] = [
|
|
174
|
-
{ id: 'inv-001', total: 100 },
|
|
175
|
-
{ id: 'inv-002', total: 200 },
|
|
176
|
-
];
|
|
177
|
-
const includes: ParsedInclude[] = [{ relation: 'items' }];
|
|
178
|
-
|
|
179
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
180
|
-
|
|
181
|
-
expect(records[0].items).toHaveLength(2);
|
|
182
|
-
expect(records[0].items[0].id).toBe('item-001');
|
|
183
|
-
expect(records[1].items).toHaveLength(1);
|
|
184
|
-
expect(records[1].items[0].id).toBe('item-003');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('returns empty array when no children exist', async () => {
|
|
188
|
-
const registry = makeRegistry(relationships);
|
|
189
|
-
const db = makeMockDb({ sales__invoice_item: [] });
|
|
190
|
-
const records: any[] = [{ id: 'inv-001', total: 100 }];
|
|
191
|
-
const includes: ParsedInclude[] = [{ relation: 'items' }];
|
|
192
|
-
|
|
193
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
194
|
-
|
|
195
|
-
expect(records[0].items).toEqual([]);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe('children (parent-child)', () => {
|
|
200
|
-
const relationships: ModelRelationship[] = [
|
|
201
|
-
{
|
|
202
|
-
type: 'children',
|
|
203
|
-
from: 'sales.order',
|
|
204
|
-
field: 'items',
|
|
205
|
-
to: 'sales.order_item',
|
|
206
|
-
foreignKey: 'order_id',
|
|
207
|
-
},
|
|
208
|
-
];
|
|
209
|
-
|
|
210
|
-
it('behaves same as hasMany', async () => {
|
|
211
|
-
const registry = makeRegistry(relationships);
|
|
212
|
-
const db = makeMockDb({
|
|
213
|
-
sales__order_item: [
|
|
214
|
-
{ id: 'oi-001', order_id: 'ord-001', qty: 2 },
|
|
215
|
-
{ id: 'oi-002', order_id: 'ord-001', qty: 7 },
|
|
216
|
-
],
|
|
217
|
-
});
|
|
218
|
-
const records: any[] = [{ id: 'ord-001', total: 500 }];
|
|
219
|
-
const includes: ParsedInclude[] = [{ relation: 'items' }];
|
|
220
|
-
|
|
221
|
-
await resolveIncludes(records, includes, registry, db, 'sales.order', makeRequest());
|
|
222
|
-
|
|
223
|
-
expect(records[0].items).toHaveLength(2);
|
|
224
|
-
expect(records[0].items[0].id).toBe('oi-001');
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('manyToMany', () => {
|
|
229
|
-
const relationships: ModelRelationship[] = [
|
|
230
|
-
{
|
|
231
|
-
type: 'manyToMany',
|
|
232
|
-
from: 'sales.order',
|
|
233
|
-
field: 'tags',
|
|
234
|
-
to: 'core.tag',
|
|
235
|
-
through: 'sales.order_tag',
|
|
236
|
-
},
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
it('resolves through junction table', async () => {
|
|
240
|
-
const registry = makeRegistry(relationships);
|
|
241
|
-
const db = makeMockDb({
|
|
242
|
-
core__tag: [
|
|
243
|
-
{ id: 'tag-001', name: 'urgent' },
|
|
244
|
-
{ id: 'tag-002', name: 'vip' },
|
|
245
|
-
],
|
|
246
|
-
sales__order_tag: [
|
|
247
|
-
{ order_id: 'ord-001', tag_id: 'tag-001' },
|
|
248
|
-
{ order_id: 'ord-001', tag_id: 'tag-002' },
|
|
249
|
-
{ order_id: 'ord-002', tag_id: 'tag-001' },
|
|
250
|
-
],
|
|
251
|
-
});
|
|
252
|
-
const records: any[] = [
|
|
253
|
-
{ id: 'ord-001', total: 100 },
|
|
254
|
-
{ id: 'ord-002', total: 200 },
|
|
255
|
-
];
|
|
256
|
-
const includes: ParsedInclude[] = [{ relation: 'tags' }];
|
|
257
|
-
|
|
258
|
-
await resolveIncludes(records, includes, registry, db, 'sales.order', makeRequest());
|
|
259
|
-
|
|
260
|
-
expect(records[0].tags).toHaveLength(2);
|
|
261
|
-
expect(records[0].tags.map((t: any) => t.name)).toContain('urgent');
|
|
262
|
-
expect(records[0].tags.map((t: any) => t.name)).toContain('vip');
|
|
263
|
-
expect(records[1].tags).toHaveLength(1);
|
|
264
|
-
expect(records[1].tags[0].name).toBe('urgent');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('returns empty array when no associations', async () => {
|
|
268
|
-
const registry = makeRegistry(relationships);
|
|
269
|
-
const db = makeMockDb({
|
|
270
|
-
core__tag: [{ id: 'tag-001', name: 'urgent' }],
|
|
271
|
-
sales__order_tag: [],
|
|
272
|
-
});
|
|
273
|
-
const records: any[] = [{ id: 'ord-001', total: 100 }];
|
|
274
|
-
const includes: ParsedInclude[] = [{ relation: 'tags' }];
|
|
275
|
-
|
|
276
|
-
await resolveIncludes(records, includes, registry, db, 'sales.order', makeRequest());
|
|
277
|
-
|
|
278
|
-
expect(records[0].tags).toEqual([]);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe('dynamicLink (polymorphic)', () => {
|
|
283
|
-
const relationships: ModelRelationship[] = [
|
|
284
|
-
{
|
|
285
|
-
type: 'dynamicLink',
|
|
286
|
-
from: 'core.comment',
|
|
287
|
-
field: 'reference',
|
|
288
|
-
to: '*',
|
|
289
|
-
modelField: 'reference_type',
|
|
290
|
-
},
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
it('resolves grouped by discriminator', async () => {
|
|
294
|
-
const registry = makeRegistry(relationships);
|
|
295
|
-
const db = makeMockDb({
|
|
296
|
-
sales__order: [{ id: 'ord-001', total: 500 }],
|
|
297
|
-
sales__invoice: [{ id: 'inv-001', total: 1000 }],
|
|
298
|
-
});
|
|
299
|
-
const records: any[] = [
|
|
300
|
-
{ id: 'cmt-001', reference_type: 'sales.order', reference: 'ord-001' },
|
|
301
|
-
{ id: 'cmt-002', reference_type: 'sales.invoice', reference: 'inv-001' },
|
|
302
|
-
];
|
|
303
|
-
const includes: ParsedInclude[] = [{ relation: 'reference' }];
|
|
304
|
-
|
|
305
|
-
await resolveIncludes(records, includes, registry, db, 'core.comment', makeRequest());
|
|
306
|
-
|
|
307
|
-
expect(records[0].reference).toEqual({ id: 'ord-001', total: 500 });
|
|
308
|
-
expect(records[1].reference).toEqual({ id: 'inv-001', total: 1000 });
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('returns null when target not found', async () => {
|
|
312
|
-
const registry = makeRegistry(relationships);
|
|
313
|
-
const db = makeMockDb({ sales__order: [] });
|
|
314
|
-
const records: any[] = [
|
|
315
|
-
{ id: 'cmt-001', reference_type: 'sales.order', reference: 'ord-999' },
|
|
316
|
-
];
|
|
317
|
-
const includes: ParsedInclude[] = [{ relation: 'reference' }];
|
|
318
|
-
|
|
319
|
-
await resolveIncludes(records, includes, registry, db, 'core.comment', makeRequest());
|
|
320
|
-
|
|
321
|
-
expect(records[0].reference).toBeNull();
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
describe('nested includes (depth 2)', () => {
|
|
326
|
-
const relationships: ModelRelationship[] = [
|
|
327
|
-
{
|
|
328
|
-
type: 'children',
|
|
329
|
-
from: 'sales.invoice',
|
|
330
|
-
field: 'items',
|
|
331
|
-
to: 'sales.invoice_item',
|
|
332
|
-
foreignKey: 'invoice_id',
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
type: 'link',
|
|
336
|
-
from: 'sales.invoice_item',
|
|
337
|
-
field: 'item_group',
|
|
338
|
-
to: 'core.item_group',
|
|
339
|
-
},
|
|
340
|
-
];
|
|
341
|
-
|
|
342
|
-
it('resolves depth-2 nested includes', async () => {
|
|
343
|
-
const registry = makeRegistry(relationships);
|
|
344
|
-
const db = makeMockDb({
|
|
345
|
-
sales__invoice_item: [
|
|
346
|
-
{ id: 'item-001', invoice_id: 'inv-001', item_group: 'grp-001', qty: 10 },
|
|
347
|
-
{ id: 'item-002', invoice_id: 'inv-001', item_group: 'grp-002', qty: 5 },
|
|
348
|
-
],
|
|
349
|
-
core__item_group: [
|
|
350
|
-
{ id: 'grp-001', name: 'Finished Goods' },
|
|
351
|
-
{ id: 'grp-002', name: 'Raw Materials' },
|
|
352
|
-
],
|
|
353
|
-
});
|
|
354
|
-
const records: any[] = [{ id: 'inv-001', total: 1000 }];
|
|
355
|
-
const includes: ParsedInclude[] = [
|
|
356
|
-
{ relation: 'items', nested: [{ relation: 'item_group' }] },
|
|
357
|
-
];
|
|
358
|
-
|
|
359
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
360
|
-
|
|
361
|
-
expect(records[0].items[0].item_group).toEqual({ id: 'grp-001', name: 'Finished Goods' });
|
|
362
|
-
expect(records[0].items[1].item_group).toEqual({ id: 'grp-002', name: 'Raw Materials' });
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe('edge cases', () => {
|
|
367
|
-
it('handles empty records array', async () => {
|
|
368
|
-
const registry = makeRegistry([
|
|
369
|
-
{ type: 'link', from: 'sales.invoice', field: 'customer', to: 'sales.customer' },
|
|
370
|
-
]);
|
|
371
|
-
const db = makeMockDb({});
|
|
372
|
-
const records: any[] = [];
|
|
373
|
-
const includes: ParsedInclude[] = [{ relation: 'customer' }];
|
|
374
|
-
|
|
375
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
376
|
-
|
|
377
|
-
expect(records).toEqual([]);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('handles null FK values in link fields', async () => {
|
|
381
|
-
const registry = makeRegistry([
|
|
382
|
-
{ type: 'link', from: 'sales.invoice', field: 'customer', to: 'sales.customer' },
|
|
383
|
-
]);
|
|
384
|
-
const db = makeMockDb({ sales__customer: [{ id: 'C-001', name: 'Acme' }] });
|
|
385
|
-
const records: any[] = [{ id: 'inv-001', customer: null, total: 100 }];
|
|
386
|
-
const includes: ParsedInclude[] = [{ relation: 'customer' }];
|
|
387
|
-
|
|
388
|
-
await resolveIncludes(records, includes, registry, db, 'sales.invoice', makeRequest());
|
|
389
|
-
|
|
390
|
-
expect(records[0].customer).toBeNull();
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createServer } from '../server.js';
|
|
3
|
-
import { generateRoutes } from '../route-generator.js';
|
|
4
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
5
|
-
import type { DatabaseClient } from '../../db/client.js';
|
|
6
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
7
|
-
|
|
8
|
-
function makeModel(): ResolvedModel {
|
|
9
|
-
return {
|
|
10
|
-
qualifiedName: 'test.item',
|
|
11
|
-
app: 'test',
|
|
12
|
-
module: 'test',
|
|
13
|
-
name: 'item',
|
|
14
|
-
auditLog: false,
|
|
15
|
-
traits: [],
|
|
16
|
-
fields: [{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } }],
|
|
17
|
-
indexes: [],
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function makeRegistry(): SchemaRegistry {
|
|
22
|
-
const model = makeModel();
|
|
23
|
-
return {
|
|
24
|
-
getModelsByModule: () => new Map([['test', [model]]]),
|
|
25
|
-
getRelationshipsForModel: () => [],
|
|
26
|
-
getModel: (name: string) => (name === model.qualifiedName ? model : null),
|
|
27
|
-
} as unknown as SchemaRegistry;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeMockDb(): DatabaseClient {
|
|
31
|
-
const buildQuery = () => {
|
|
32
|
-
const q: any = {};
|
|
33
|
-
q.select = () => q;
|
|
34
|
-
q.selectAll = () => q;
|
|
35
|
-
q.where = () => q;
|
|
36
|
-
q.orderBy = () => q;
|
|
37
|
-
q.offset = () => q;
|
|
38
|
-
q.limit = () => q;
|
|
39
|
-
q.execute = async () => [];
|
|
40
|
-
q.executeTakeFirst = async () => ({ count: '0' });
|
|
41
|
-
q.executeTakeFirstOrThrow = async () => ({});
|
|
42
|
-
q.values = () => q;
|
|
43
|
-
q.set = () => q;
|
|
44
|
-
q.returningAll = () => q;
|
|
45
|
-
return q;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
selectFrom: () => buildQuery(),
|
|
50
|
-
insertInto: () => buildQuery(),
|
|
51
|
-
updateTable: () => buildQuery(),
|
|
52
|
-
deleteFrom: () => buildQuery(),
|
|
53
|
-
kysely: { fn: { countAll: () => ({ as: () => 'count' }) } },
|
|
54
|
-
} as unknown as DatabaseClient;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('Middleware hooks', () => {
|
|
58
|
-
it('registers onRequest, preHandler, and onSend hooks', async () => {
|
|
59
|
-
const server = await createServer();
|
|
60
|
-
generateRoutes(server, makeRegistry(), makeMockDb());
|
|
61
|
-
|
|
62
|
-
const res = await server.inject({ method: 'GET', url: '/api/test/item' });
|
|
63
|
-
expect(res.statusCode).toBe(200);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('onSend hook passes response through (field stripping placeholder)', async () => {
|
|
67
|
-
const server = await createServer();
|
|
68
|
-
generateRoutes(server, makeRegistry(), makeMockDb());
|
|
69
|
-
|
|
70
|
-
const res = await server.inject({ method: 'GET', url: '/api/test/item' });
|
|
71
|
-
const body = JSON.parse(res.body);
|
|
72
|
-
expect(body.data).toBeDefined();
|
|
73
|
-
expect(body.meta).toBeDefined();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('hooks execute in order: onRequest → preHandler → handler → onSend', async () => {
|
|
77
|
-
const order: string[] = [];
|
|
78
|
-
|
|
79
|
-
const server = await createServer();
|
|
80
|
-
|
|
81
|
-
server.addHook('onRequest', async () => {
|
|
82
|
-
order.push('onRequest');
|
|
83
|
-
});
|
|
84
|
-
server.addHook('preHandler', async () => {
|
|
85
|
-
order.push('preHandler');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
generateRoutes(server, makeRegistry(), makeMockDb());
|
|
89
|
-
|
|
90
|
-
server.addHook('onSend', async (_req, _rep, payload) => {
|
|
91
|
-
order.push('onSend');
|
|
92
|
-
return payload;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
await server.inject({ method: 'GET', url: '/api/test/item' });
|
|
96
|
-
|
|
97
|
-
expect(order.indexOf('onRequest')).toBeLessThan(order.indexOf('preHandler'));
|
|
98
|
-
expect(order.indexOf('preHandler')).toBeLessThan(order.indexOf('onSend'));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { fieldToJsonSchema, modelToSchemaComponent } from '../openapi-schema.js';
|
|
3
|
-
import type { FieldConfig } from '@rangka/shared';
|
|
4
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
5
|
-
|
|
6
|
-
describe('fieldToJsonSchema', () => {
|
|
7
|
-
it('maps string field', () => {
|
|
8
|
-
const result = fieldToJsonSchema({ type: 'string' } as FieldConfig);
|
|
9
|
-
expect(result).toEqual({ type: 'string' });
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('maps text field', () => {
|
|
13
|
-
const result = fieldToJsonSchema({ type: 'text' } as FieldConfig);
|
|
14
|
-
expect(result).toEqual({ type: 'string' });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('maps int field', () => {
|
|
18
|
-
const result = fieldToJsonSchema({ type: 'int' } as FieldConfig);
|
|
19
|
-
expect(result).toEqual({ type: 'integer' });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('maps decimal field', () => {
|
|
23
|
-
const result = fieldToJsonSchema({ type: 'decimal' } as FieldConfig);
|
|
24
|
-
expect(result).toEqual({ type: 'number' });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('maps boolean field', () => {
|
|
28
|
-
const result = fieldToJsonSchema({ type: 'boolean' } as FieldConfig);
|
|
29
|
-
expect(result).toEqual({ type: 'boolean' });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('maps date field with format', () => {
|
|
33
|
-
const result = fieldToJsonSchema({ type: 'date' } as FieldConfig);
|
|
34
|
-
expect(result).toEqual({ type: 'string', format: 'date' });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('maps datetime field with format', () => {
|
|
38
|
-
const result = fieldToJsonSchema({ type: 'datetime' } as FieldConfig);
|
|
39
|
-
expect(result).toEqual({ type: 'string', format: 'date-time' });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('maps enum field with values', () => {
|
|
43
|
-
const result = fieldToJsonSchema({
|
|
44
|
-
type: 'enum',
|
|
45
|
-
options: ['Draft', 'Submitted', 'Paid'],
|
|
46
|
-
} as FieldConfig);
|
|
47
|
-
expect(result).toEqual({ type: 'string', enum: ['Draft', 'Submitted', 'Paid'] });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('maps json field', () => {
|
|
51
|
-
const result = fieldToJsonSchema({ type: 'json' } as FieldConfig);
|
|
52
|
-
expect(result).toEqual({ type: 'object' });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('maps link field as string reference', () => {
|
|
56
|
-
const result = fieldToJsonSchema({ type: 'link', model: 'customer' } as FieldConfig);
|
|
57
|
-
expect(result).toEqual({ type: 'string', description: 'Reference to customer' });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('maps money field as number', () => {
|
|
61
|
-
const result = fieldToJsonSchema({ type: 'money' } as FieldConfig);
|
|
62
|
-
expect(result).toEqual({ type: 'number' });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('maps sequence field as string', () => {
|
|
66
|
-
const result = fieldToJsonSchema({ type: 'sequence' } as FieldConfig);
|
|
67
|
-
expect(result).toEqual({ type: 'string' });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('returns null for hasMany', () => {
|
|
71
|
-
const result = fieldToJsonSchema({
|
|
72
|
-
type: 'hasMany',
|
|
73
|
-
model: 'item',
|
|
74
|
-
foreignKey: 'invoice_id',
|
|
75
|
-
} as FieldConfig);
|
|
76
|
-
expect(result).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns null for children', () => {
|
|
80
|
-
const result = fieldToJsonSchema({
|
|
81
|
-
type: 'children',
|
|
82
|
-
model: 'item',
|
|
83
|
-
foreignKey: 'invoice_id',
|
|
84
|
-
} as FieldConfig);
|
|
85
|
-
expect(result).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('returns null for manyToMany', () => {
|
|
89
|
-
const result = fieldToJsonSchema({
|
|
90
|
-
type: 'manyToMany',
|
|
91
|
-
model: 'tag',
|
|
92
|
-
through: 'invoice_tag',
|
|
93
|
-
} as FieldConfig);
|
|
94
|
-
expect(result).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('returns null for tree', () => {
|
|
98
|
-
const result = fieldToJsonSchema({
|
|
99
|
-
type: 'tree',
|
|
100
|
-
strategy: 'materialized_path',
|
|
101
|
-
} as FieldConfig);
|
|
102
|
-
expect(result).toBeNull();
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('modelToSchemaComponent', () => {
|
|
107
|
-
const baseModel: ResolvedModel = {
|
|
108
|
-
qualifiedName: 'sales.customer',
|
|
109
|
-
app: 'basic',
|
|
110
|
-
module: 'sales',
|
|
111
|
-
name: 'customer',
|
|
112
|
-
label: 'Customer',
|
|
113
|
-
auditLog: false,
|
|
114
|
-
traits: [],
|
|
115
|
-
fields: [
|
|
116
|
-
{
|
|
117
|
-
name: 'name',
|
|
118
|
-
config: { type: 'string', required: true, label: 'Customer Name' },
|
|
119
|
-
provenance: { source: 'base' },
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: 'email',
|
|
123
|
-
config: { type: 'string', required: true, label: 'Email' },
|
|
124
|
-
provenance: { source: 'base' },
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'is_active',
|
|
128
|
-
config: { type: 'boolean', label: 'Active' },
|
|
129
|
-
provenance: { source: 'base' },
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
indexes: [],
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
it('includes id property', () => {
|
|
136
|
-
const schema = modelToSchemaComponent(baseModel);
|
|
137
|
-
expect(schema.properties.id).toEqual({ type: 'string', description: 'Primary key' });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('maps fields to properties', () => {
|
|
141
|
-
const schema = modelToSchemaComponent(baseModel);
|
|
142
|
-
expect(schema.properties.name).toEqual({ type: 'string', description: 'Customer Name' });
|
|
143
|
-
expect(schema.properties.email).toEqual({ type: 'string', description: 'Email' });
|
|
144
|
-
expect(schema.properties.is_active).toEqual({ type: 'boolean', description: 'Active' });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('collects required fields', () => {
|
|
148
|
-
const schema = modelToSchemaComponent(baseModel);
|
|
149
|
-
expect(schema.required).toEqual(['name', 'email']);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('omits required array when no fields are required', () => {
|
|
153
|
-
const model: ResolvedModel = {
|
|
154
|
-
...baseModel,
|
|
155
|
-
fields: [{ name: 'notes', config: { type: 'text' }, provenance: { source: 'base' } }],
|
|
156
|
-
};
|
|
157
|
-
const schema = modelToSchemaComponent(model);
|
|
158
|
-
expect(schema.required).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('uses label as description', () => {
|
|
162
|
-
const schema = modelToSchemaComponent(baseModel);
|
|
163
|
-
expect(schema.properties.name.description).toBe('Customer Name');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('omits description when no label', () => {
|
|
167
|
-
const model: ResolvedModel = {
|
|
168
|
-
...baseModel,
|
|
169
|
-
fields: [{ name: 'code', config: { type: 'string' }, provenance: { source: 'base' } }],
|
|
170
|
-
};
|
|
171
|
-
const schema = modelToSchemaComponent(model);
|
|
172
|
-
expect(schema.properties.code.description).toBeUndefined();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('includes enum values', () => {
|
|
176
|
-
const model: ResolvedModel = {
|
|
177
|
-
...baseModel,
|
|
178
|
-
fields: [
|
|
179
|
-
{
|
|
180
|
-
name: 'status',
|
|
181
|
-
config: { type: 'enum', options: ['Draft', 'Submitted', 'Paid'] },
|
|
182
|
-
provenance: { source: 'base' },
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
};
|
|
186
|
-
const schema = modelToSchemaComponent(model);
|
|
187
|
-
expect(schema.properties.status.enum).toEqual(['Draft', 'Submitted', 'Paid']);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('skips relationship fields that return null', () => {
|
|
191
|
-
const model: ResolvedModel = {
|
|
192
|
-
...baseModel,
|
|
193
|
-
fields: [
|
|
194
|
-
{
|
|
195
|
-
name: 'name',
|
|
196
|
-
config: { type: 'string', required: true },
|
|
197
|
-
provenance: { source: 'base' },
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: 'items',
|
|
201
|
-
config: { type: 'hasMany', model: 'item', foreignKey: 'customer_id' },
|
|
202
|
-
provenance: { source: 'base' },
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
};
|
|
206
|
-
const schema = modelToSchemaComponent(model);
|
|
207
|
-
expect(schema.properties.items).toBeUndefined();
|
|
208
|
-
expect(schema.properties.name).toBeDefined();
|
|
209
|
-
});
|
|
210
|
-
});
|