@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,355 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { mapFieldsToColumns, modelToTableName } from '../field-mapper.js';
|
|
3
|
-
import type { ResolvedModel, ResolvedField } from '../../schema/types.js';
|
|
4
|
-
|
|
5
|
-
function makeModel(overrides: Partial<ResolvedModel> = {}): ResolvedModel {
|
|
6
|
-
return {
|
|
7
|
-
qualifiedName: 'sales.invoice',
|
|
8
|
-
app: 'sales',
|
|
9
|
-
module: 'sales',
|
|
10
|
-
name: 'invoice',
|
|
11
|
-
auditLog: true,
|
|
12
|
-
traits: [],
|
|
13
|
-
fields: [],
|
|
14
|
-
indexes: [],
|
|
15
|
-
...overrides,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function makeField(name: string, config: any): ResolvedField {
|
|
20
|
-
return { name, config, provenance: { source: 'base' } };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('modelToTableName', () => {
|
|
24
|
-
it('converts qualified name to table name', () => {
|
|
25
|
-
expect(modelToTableName('sales.invoice')).toBe('sales__invoice');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('handles single-segment names', () => {
|
|
29
|
-
expect(modelToTableName('core.user')).toBe('core__user');
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('mapFieldsToColumns', () => {
|
|
34
|
-
it('always includes id UUID primary key', () => {
|
|
35
|
-
const model = makeModel();
|
|
36
|
-
const result = mapFieldsToColumns(model);
|
|
37
|
-
const idCol = result.columns.find((c) => c.name === 'id');
|
|
38
|
-
expect(idCol).toBeDefined();
|
|
39
|
-
expect(idCol!.type).toBe('UUID');
|
|
40
|
-
expect(idCol!.primaryKey).toBe(true);
|
|
41
|
-
expect(idCol!.defaultValue).toBe('gen_random_uuid()');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('primitive fields', () => {
|
|
45
|
-
it('maps string field to VARCHAR', () => {
|
|
46
|
-
const model = makeModel({
|
|
47
|
-
fields: [makeField('title', { type: 'string', maxLength: 200 })],
|
|
48
|
-
});
|
|
49
|
-
const result = mapFieldsToColumns(model);
|
|
50
|
-
const col = result.columns.find((c) => c.name === 'title');
|
|
51
|
-
expect(col).toBeDefined();
|
|
52
|
-
expect(col!.type).toBe('VARCHAR(200)');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('maps string field with default maxLength', () => {
|
|
56
|
-
const model = makeModel({
|
|
57
|
-
fields: [makeField('title', { type: 'string' })],
|
|
58
|
-
});
|
|
59
|
-
const result = mapFieldsToColumns(model);
|
|
60
|
-
const col = result.columns.find((c) => c.name === 'title');
|
|
61
|
-
expect(col!.type).toBe('VARCHAR(255)');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('maps text field to TEXT', () => {
|
|
65
|
-
const model = makeModel({
|
|
66
|
-
fields: [makeField('description', { type: 'text' })],
|
|
67
|
-
});
|
|
68
|
-
const result = mapFieldsToColumns(model);
|
|
69
|
-
const col = result.columns.find((c) => c.name === 'description');
|
|
70
|
-
expect(col!.type).toBe('TEXT');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('maps int field to INTEGER', () => {
|
|
74
|
-
const model = makeModel({
|
|
75
|
-
fields: [makeField('quantity', { type: 'int' })],
|
|
76
|
-
});
|
|
77
|
-
const result = mapFieldsToColumns(model);
|
|
78
|
-
const col = result.columns.find((c) => c.name === 'quantity');
|
|
79
|
-
expect(col!.type).toBe('INTEGER');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('maps decimal field to DECIMAL with precision/scale', () => {
|
|
83
|
-
const model = makeModel({
|
|
84
|
-
fields: [makeField('rate', { type: 'decimal', precision: 10, scale: 2 })],
|
|
85
|
-
});
|
|
86
|
-
const result = mapFieldsToColumns(model);
|
|
87
|
-
const col = result.columns.find((c) => c.name === 'rate');
|
|
88
|
-
expect(col!.type).toBe('DECIMAL(10,2)');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('maps decimal field with default precision/scale', () => {
|
|
92
|
-
const model = makeModel({
|
|
93
|
-
fields: [makeField('amount', { type: 'decimal' })],
|
|
94
|
-
});
|
|
95
|
-
const result = mapFieldsToColumns(model);
|
|
96
|
-
const col = result.columns.find((c) => c.name === 'amount');
|
|
97
|
-
expect(col!.type).toBe('DECIMAL(18,6)');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('maps boolean field to BOOLEAN', () => {
|
|
101
|
-
const model = makeModel({
|
|
102
|
-
fields: [makeField('is_active', { type: 'boolean' })],
|
|
103
|
-
});
|
|
104
|
-
const result = mapFieldsToColumns(model);
|
|
105
|
-
const col = result.columns.find((c) => c.name === 'is_active');
|
|
106
|
-
expect(col!.type).toBe('BOOLEAN');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('maps date field to DATE', () => {
|
|
110
|
-
const model = makeModel({
|
|
111
|
-
fields: [makeField('posting_date', { type: 'date' })],
|
|
112
|
-
});
|
|
113
|
-
const result = mapFieldsToColumns(model);
|
|
114
|
-
const col = result.columns.find((c) => c.name === 'posting_date');
|
|
115
|
-
expect(col!.type).toBe('DATE');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('maps datetime field to TIMESTAMPTZ', () => {
|
|
119
|
-
const model = makeModel({
|
|
120
|
-
fields: [makeField('created', { type: 'datetime' })],
|
|
121
|
-
});
|
|
122
|
-
const result = mapFieldsToColumns(model);
|
|
123
|
-
const col = result.columns.find((c) => c.name === 'created');
|
|
124
|
-
expect(col!.type).toBe('TIMESTAMPTZ');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('maps enum field to VARCHAR with CHECK constraint', () => {
|
|
128
|
-
const model = makeModel({
|
|
129
|
-
fields: [makeField('status', { type: 'enum', options: ['Draft', 'Submitted', 'Paid'] })],
|
|
130
|
-
});
|
|
131
|
-
const result = mapFieldsToColumns(model);
|
|
132
|
-
const col = result.columns.find((c) => c.name === 'status');
|
|
133
|
-
expect(col!.type).toBe('VARCHAR(255)');
|
|
134
|
-
expect(result.checkConstraints).toHaveLength(1);
|
|
135
|
-
expect(result.checkConstraints[0].expression).toContain("'Draft'");
|
|
136
|
-
expect(result.checkConstraints[0].expression).toContain("'Submitted'");
|
|
137
|
-
expect(result.checkConstraints[0].expression).toContain("'Paid'");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('maps json field to JSONB', () => {
|
|
141
|
-
const model = makeModel({
|
|
142
|
-
fields: [makeField('metadata', { type: 'json' })],
|
|
143
|
-
});
|
|
144
|
-
const result = mapFieldsToColumns(model);
|
|
145
|
-
const col = result.columns.find((c) => c.name === 'metadata');
|
|
146
|
-
expect(col!.type).toBe('JSONB');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('maps code field to TEXT', () => {
|
|
150
|
-
const model = makeModel({
|
|
151
|
-
fields: [makeField('formula', { type: 'code', language: 'expression' })],
|
|
152
|
-
});
|
|
153
|
-
const result = mapFieldsToColumns(model);
|
|
154
|
-
const col = result.columns.find((c) => c.name === 'formula');
|
|
155
|
-
expect(col!.type).toBe('TEXT');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('link fields', () => {
|
|
160
|
-
it('maps link field to UUID with FK', () => {
|
|
161
|
-
const model = makeModel({
|
|
162
|
-
fields: [makeField('customer', { type: 'link', model: 'sales.customer' })],
|
|
163
|
-
});
|
|
164
|
-
const result = mapFieldsToColumns(model);
|
|
165
|
-
const col = result.columns.find((c) => c.name === 'customer');
|
|
166
|
-
expect(col!.type).toBe('UUID');
|
|
167
|
-
expect(result.foreignKeys).toHaveLength(1);
|
|
168
|
-
expect(result.foreignKeys[0].referencedTable).toBe('sales__customer');
|
|
169
|
-
expect(result.foreignKeys[0].referencedColumn).toBe('id');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('maps required link field to NOT NULL', () => {
|
|
173
|
-
const model = makeModel({
|
|
174
|
-
fields: [
|
|
175
|
-
makeField('customer', {
|
|
176
|
-
type: 'link',
|
|
177
|
-
model: 'sales.customer',
|
|
178
|
-
required: true,
|
|
179
|
-
nullable: false,
|
|
180
|
-
}),
|
|
181
|
-
],
|
|
182
|
-
});
|
|
183
|
-
const result = mapFieldsToColumns(model);
|
|
184
|
-
const col = result.columns.find((c) => c.name === 'customer');
|
|
185
|
-
expect(col!.nullable).toBe(false);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('dynamic link fields', () => {
|
|
190
|
-
it('maps dynamicLink to discriminator + UUID columns', () => {
|
|
191
|
-
const model = makeModel({
|
|
192
|
-
fields: [makeField('source_document', { type: 'dynamicLink', modelField: 'source_type' })],
|
|
193
|
-
});
|
|
194
|
-
const result = mapFieldsToColumns(model);
|
|
195
|
-
const typeCol = result.columns.find((c) => c.name === 'source_type');
|
|
196
|
-
const idCol = result.columns.find((c) => c.name === 'source_document');
|
|
197
|
-
expect(typeCol!.type).toBe('VARCHAR(255)');
|
|
198
|
-
expect(idCol!.type).toBe('UUID');
|
|
199
|
-
expect(result.foreignKeys).toHaveLength(0);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe('money fields', () => {
|
|
204
|
-
it('maps money field to two DECIMAL columns', () => {
|
|
205
|
-
const model = makeModel({
|
|
206
|
-
fields: [makeField('total_amount', { type: 'money' })],
|
|
207
|
-
});
|
|
208
|
-
const result = mapFieldsToColumns(model);
|
|
209
|
-
const valueCol = result.columns.find((c) => c.name === 'total_amount');
|
|
210
|
-
const baseCol = result.columns.find((c) => c.name === 'total_amount_base');
|
|
211
|
-
expect(valueCol!.type).toBe('DECIMAL(18,6)');
|
|
212
|
-
expect(baseCol!.type).toBe('DECIMAL(18,6)');
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('tree fields', () => {
|
|
217
|
-
it('maps materialized_path tree to parent + path + depth', () => {
|
|
218
|
-
const model = makeModel({
|
|
219
|
-
fields: [makeField('parent', { type: 'tree', strategy: 'materialized_path' })],
|
|
220
|
-
});
|
|
221
|
-
const result = mapFieldsToColumns(model);
|
|
222
|
-
expect(result.columns.find((c) => c.name === 'parent')).toBeDefined();
|
|
223
|
-
expect(result.columns.find((c) => c.name === 'path')).toBeDefined();
|
|
224
|
-
expect(result.columns.find((c) => c.name === 'depth')).toBeDefined();
|
|
225
|
-
expect(result.foreignKeys.find((fk) => fk.column === 'parent')!.referencedTable).toBe(
|
|
226
|
-
'sales__invoice',
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('maps nested_set tree to parent + lft + rgt', () => {
|
|
231
|
-
const model = makeModel({
|
|
232
|
-
fields: [makeField('parent', { type: 'tree', strategy: 'nested_set' })],
|
|
233
|
-
});
|
|
234
|
-
const result = mapFieldsToColumns(model);
|
|
235
|
-
expect(result.columns.find((c) => c.name === 'parent')).toBeDefined();
|
|
236
|
-
expect(result.columns.find((c) => c.name === 'lft')).toBeDefined();
|
|
237
|
-
expect(result.columns.find((c) => c.name === 'rgt')).toBeDefined();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('maps closure_table tree to parent + extra closure table', () => {
|
|
241
|
-
const model = makeModel({
|
|
242
|
-
fields: [makeField('parent', { type: 'tree', strategy: 'closure_table' })],
|
|
243
|
-
});
|
|
244
|
-
const result = mapFieldsToColumns(model);
|
|
245
|
-
expect(result.columns.find((c) => c.name === 'parent')).toBeDefined();
|
|
246
|
-
expect(result.extraTables).toHaveLength(1);
|
|
247
|
-
expect(result.extraTables[0].name).toBe('sales__invoice_closure');
|
|
248
|
-
expect(result.extraTables[0].columns.map((c) => c.name)).toEqual([
|
|
249
|
-
'ancestor_id',
|
|
250
|
-
'descendant_id',
|
|
251
|
-
'depth',
|
|
252
|
-
]);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('sequence fields', () => {
|
|
257
|
-
it('maps sequence field to VARCHAR', () => {
|
|
258
|
-
const model = makeModel({
|
|
259
|
-
fields: [makeField('invoice_number', { type: 'sequence', prefix: 'INV-', digits: 5 })],
|
|
260
|
-
});
|
|
261
|
-
const result = mapFieldsToColumns(model);
|
|
262
|
-
const col = result.columns.find((c) => c.name === 'invoice_number');
|
|
263
|
-
expect(col!.type).toBe('VARCHAR(255)');
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('relationship fields (no columns)', () => {
|
|
268
|
-
it('hasMany produces no columns', () => {
|
|
269
|
-
const model = makeModel({
|
|
270
|
-
fields: [
|
|
271
|
-
makeField('items', {
|
|
272
|
-
type: 'hasMany',
|
|
273
|
-
model: 'sales.invoice_item',
|
|
274
|
-
foreignKey: 'invoice_id',
|
|
275
|
-
}),
|
|
276
|
-
],
|
|
277
|
-
});
|
|
278
|
-
const result = mapFieldsToColumns(model);
|
|
279
|
-
expect(result.columns.find((c) => c.name === 'items')).toBeUndefined();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('children produces no columns', () => {
|
|
283
|
-
const model = makeModel({
|
|
284
|
-
fields: [
|
|
285
|
-
makeField('items', {
|
|
286
|
-
type: 'children',
|
|
287
|
-
model: 'sales.invoice_item',
|
|
288
|
-
foreignKey: 'parent',
|
|
289
|
-
}),
|
|
290
|
-
],
|
|
291
|
-
});
|
|
292
|
-
const result = mapFieldsToColumns(model);
|
|
293
|
-
expect(result.columns.find((c) => c.name === 'items')).toBeUndefined();
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('manyToMany produces no columns', () => {
|
|
297
|
-
const model = makeModel({
|
|
298
|
-
fields: [
|
|
299
|
-
makeField('tags', { type: 'manyToMany', model: 'core.tag', through: 'invoice_tag' }),
|
|
300
|
-
],
|
|
301
|
-
});
|
|
302
|
-
const result = mapFieldsToColumns(model);
|
|
303
|
-
expect(result.columns.find((c) => c.name === 'tags')).toBeUndefined();
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('trait column injection', () => {
|
|
308
|
-
it('timestamped adds created_at/updated_at/created_by/updated_by', () => {
|
|
309
|
-
const model = makeModel({ traits: ['timestamped'] });
|
|
310
|
-
const result = mapFieldsToColumns(model);
|
|
311
|
-
expect(result.columns.find((c) => c.name === 'created_at')!.type).toBe('TIMESTAMPTZ');
|
|
312
|
-
expect(result.columns.find((c) => c.name === 'updated_at')!.type).toBe('TIMESTAMPTZ');
|
|
313
|
-
expect(result.columns.find((c) => c.name === 'created_by')!.type).toBe('UUID');
|
|
314
|
-
expect(result.columns.find((c) => c.name === 'updated_by')!.type).toBe('UUID');
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('default values', () => {
|
|
319
|
-
it('maps boolean default', () => {
|
|
320
|
-
const model = makeModel({
|
|
321
|
-
fields: [makeField('is_active', { type: 'boolean', default: false })],
|
|
322
|
-
});
|
|
323
|
-
const result = mapFieldsToColumns(model);
|
|
324
|
-
const col = result.columns.find((c) => c.name === 'is_active');
|
|
325
|
-
expect(col!.defaultValue).toBe('FALSE');
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('maps string default', () => {
|
|
329
|
-
const model = makeModel({
|
|
330
|
-
fields: [makeField('status', { type: 'string', default: 'Draft' })],
|
|
331
|
-
});
|
|
332
|
-
const result = mapFieldsToColumns(model);
|
|
333
|
-
const col = result.columns.find((c) => c.name === 'status');
|
|
334
|
-
expect(col!.defaultValue).toBe("'Draft'");
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('maps numeric default', () => {
|
|
338
|
-
const model = makeModel({
|
|
339
|
-
fields: [makeField('quantity', { type: 'int', default: 1 })],
|
|
340
|
-
});
|
|
341
|
-
const result = mapFieldsToColumns(model);
|
|
342
|
-
const col = result.columns.find((c) => c.name === 'quantity');
|
|
343
|
-
expect(col!.defaultValue).toBe('1');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('no default when not specified', () => {
|
|
347
|
-
const model = makeModel({
|
|
348
|
-
fields: [makeField('title', { type: 'string' })],
|
|
349
|
-
});
|
|
350
|
-
const result = mapFieldsToColumns(model);
|
|
351
|
-
const col = result.columns.find((c) => c.name === 'title');
|
|
352
|
-
expect(col!.defaultValue).toBeUndefined();
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { introspect } from '../introspect.js';
|
|
3
|
-
|
|
4
|
-
function createMockDb(responses: any[][]) {
|
|
5
|
-
let callCount = 0;
|
|
6
|
-
const mockExecutor = {
|
|
7
|
-
transformQuery: vi.fn().mockImplementation((node: any) => node),
|
|
8
|
-
compileQuery: vi.fn().mockImplementation(() => ({ sql: '', parameters: [] })),
|
|
9
|
-
executeQuery: vi.fn().mockImplementation(() => {
|
|
10
|
-
const rows = responses[callCount] ?? [];
|
|
11
|
-
callCount++;
|
|
12
|
-
return Promise.resolve({ rows });
|
|
13
|
-
}),
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
getExecutor: () => mockExecutor,
|
|
18
|
-
} as any;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('introspect', () => {
|
|
22
|
-
it('returns empty state for database with no tables', async () => {
|
|
23
|
-
const mockDb = createMockDb([[]]);
|
|
24
|
-
|
|
25
|
-
const result = await introspect(mockDb);
|
|
26
|
-
expect(result.tables).toHaveLength(0);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('returns table with columns when tables exist', async () => {
|
|
30
|
-
const tableRows = [{ table_name: 'sales__invoice' }];
|
|
31
|
-
const columnRows = [
|
|
32
|
-
{
|
|
33
|
-
column_name: 'id',
|
|
34
|
-
data_type: 'uuid',
|
|
35
|
-
is_nullable: 'NO',
|
|
36
|
-
column_default: 'gen_random_uuid()',
|
|
37
|
-
character_maximum_length: null,
|
|
38
|
-
numeric_precision: null,
|
|
39
|
-
numeric_scale: null,
|
|
40
|
-
udt_name: 'uuid',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
column_name: 'title',
|
|
44
|
-
data_type: 'character varying',
|
|
45
|
-
is_nullable: 'YES',
|
|
46
|
-
column_default: null,
|
|
47
|
-
character_maximum_length: 255,
|
|
48
|
-
numeric_precision: null,
|
|
49
|
-
numeric_scale: null,
|
|
50
|
-
udt_name: 'varchar',
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
const fkRows: any[] = [];
|
|
54
|
-
const indexRows: any[] = [];
|
|
55
|
-
const checkRows: any[] = [];
|
|
56
|
-
|
|
57
|
-
const mockDb = createMockDb([tableRows, columnRows, fkRows, indexRows, checkRows]);
|
|
58
|
-
|
|
59
|
-
const result = await introspect(mockDb);
|
|
60
|
-
expect(result.tables).toHaveLength(1);
|
|
61
|
-
expect(result.tables[0].name).toBe('sales__invoice');
|
|
62
|
-
expect(result.tables[0].columns).toHaveLength(2);
|
|
63
|
-
expect(result.tables[0].columns[0].name).toBe('id');
|
|
64
|
-
expect(result.tables[0].columns[0].type).toBe('UUID');
|
|
65
|
-
expect(result.tables[0].columns[0].nullable).toBe(false);
|
|
66
|
-
expect(result.tables[0].columns[1].name).toBe('title');
|
|
67
|
-
expect(result.tables[0].columns[1].type).toBe('VARCHAR(255)');
|
|
68
|
-
expect(result.tables[0].columns[1].nullable).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { applySearchFilter } from '../filter-applier.js';
|
|
3
|
-
|
|
4
|
-
describe('applySearchFilter', () => {
|
|
5
|
-
function createMockQuery() {
|
|
6
|
-
const conditions: any[] = [];
|
|
7
|
-
const query = {
|
|
8
|
-
where: (arg: any) => {
|
|
9
|
-
conditions.push(arg);
|
|
10
|
-
return query;
|
|
11
|
-
},
|
|
12
|
-
getConditions: () => conditions,
|
|
13
|
-
};
|
|
14
|
-
return query;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
it('returns query unchanged when term is empty', () => {
|
|
18
|
-
const query = createMockQuery();
|
|
19
|
-
const result = applySearchFilter(query, '', ['name', 'email']);
|
|
20
|
-
expect(result).toBe(query);
|
|
21
|
-
expect(query.getConditions()).toHaveLength(0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('returns query unchanged when fields array is empty', () => {
|
|
25
|
-
const query = createMockQuery();
|
|
26
|
-
const result = applySearchFilter(query, 'test', []);
|
|
27
|
-
expect(result).toBe(query);
|
|
28
|
-
expect(query.getConditions()).toHaveLength(0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('adds a where clause for non-empty term and fields', () => {
|
|
32
|
-
const query = createMockQuery();
|
|
33
|
-
const result = applySearchFilter(query, 'test', ['name', 'email']);
|
|
34
|
-
expect(result).toBe(query);
|
|
35
|
-
expect(query.getConditions()).toHaveLength(1);
|
|
36
|
-
expect(typeof query.getConditions()[0]).toBe('function');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('escapes % and _ in search term', () => {
|
|
40
|
-
const query = createMockQuery();
|
|
41
|
-
const result = applySearchFilter(query, '100%_done', ['name']);
|
|
42
|
-
expect(result).toBe(query);
|
|
43
|
-
expect(query.getConditions()).toHaveLength(1);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { KyselyModelOps } from '../model-ops.js';
|
|
3
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
4
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
5
|
-
|
|
6
|
-
function createMockDb() {
|
|
7
|
-
const returningFn = vi.fn();
|
|
8
|
-
const executeTakeFirstOrThrowFn = vi.fn();
|
|
9
|
-
const onConflictDoUpdateSetFn = vi.fn();
|
|
10
|
-
const onConflictColumnsFn = vi.fn();
|
|
11
|
-
const onConflictFn = vi.fn();
|
|
12
|
-
const valuesFn = vi.fn();
|
|
13
|
-
const returningAllFn = vi.fn();
|
|
14
|
-
const insertIntoFn = vi.fn();
|
|
15
|
-
|
|
16
|
-
executeTakeFirstOrThrowFn.mockResolvedValue({ next_val: 1 });
|
|
17
|
-
returningFn.mockReturnValue({ executeTakeFirstOrThrow: executeTakeFirstOrThrowFn });
|
|
18
|
-
onConflictDoUpdateSetFn.mockReturnValue({ returning: returningFn });
|
|
19
|
-
onConflictColumnsFn.mockReturnValue({ doUpdateSet: onConflictDoUpdateSetFn });
|
|
20
|
-
onConflictFn.mockImplementation((cb: any) => {
|
|
21
|
-
const oc = { columns: onConflictColumnsFn };
|
|
22
|
-
return cb(oc);
|
|
23
|
-
});
|
|
24
|
-
valuesFn.mockReturnValue({ onConflict: onConflictFn });
|
|
25
|
-
|
|
26
|
-
const insertReturningAllExecute = vi.fn().mockResolvedValue({ id: 'abc', name: 'INV-00001' });
|
|
27
|
-
returningAllFn.mockReturnValue({ executeTakeFirstOrThrow: insertReturningAllExecute });
|
|
28
|
-
|
|
29
|
-
insertIntoFn.mockImplementation((table: string) => {
|
|
30
|
-
if (table === 'naming_sequence') {
|
|
31
|
-
return { values: valuesFn };
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
values: vi.fn().mockReturnValue({ returningAll: returningAllFn }),
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
db: {
|
|
40
|
-
insertInto: insertIntoFn,
|
|
41
|
-
selectFrom: vi.fn(),
|
|
42
|
-
updateTable: vi.fn(),
|
|
43
|
-
deleteFrom: vi.fn(),
|
|
44
|
-
},
|
|
45
|
-
insertIntoFn,
|
|
46
|
-
executeTakeFirstOrThrowFn,
|
|
47
|
-
valuesFn,
|
|
48
|
-
insertReturningAllExecute,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createModel(fields: ResolvedModel['fields']): ResolvedModel {
|
|
53
|
-
return {
|
|
54
|
-
qualifiedName: 'sales.invoice',
|
|
55
|
-
app: 'sales',
|
|
56
|
-
module: 'sales',
|
|
57
|
-
name: 'invoice',
|
|
58
|
-
auditLog: false,
|
|
59
|
-
traits: [],
|
|
60
|
-
fields,
|
|
61
|
-
indexes: [],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const mockRegistry = { getModel: vi.fn() } as unknown as SchemaRegistry;
|
|
66
|
-
|
|
67
|
-
describe('KyselyModelOps sequence field', () => {
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
vi.clearAllMocks();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('assigns sequence value on create when field is empty', async () => {
|
|
73
|
-
const { db, insertReturningAllExecute } = createMockDb();
|
|
74
|
-
insertReturningAllExecute.mockResolvedValue({ id: 'abc', invoice_number: 'INV-00001' });
|
|
75
|
-
|
|
76
|
-
const model = createModel([
|
|
77
|
-
{
|
|
78
|
-
name: 'invoice_number',
|
|
79
|
-
config: { type: 'sequence', prefix: 'INV-', digits: 5 },
|
|
80
|
-
provenance: { source: 'base' },
|
|
81
|
-
},
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
85
|
-
const data: Record<string, unknown> = { customer: 'cust-1' };
|
|
86
|
-
await ops.create(data);
|
|
87
|
-
|
|
88
|
-
expect(data.invoice_number).toBe('INV-00001');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('does not override sequence value if already provided', async () => {
|
|
92
|
-
const { db, insertIntoFn } = createMockDb();
|
|
93
|
-
|
|
94
|
-
const model = createModel([
|
|
95
|
-
{
|
|
96
|
-
name: 'invoice_number',
|
|
97
|
-
config: { type: 'sequence', prefix: 'INV-', digits: 5 },
|
|
98
|
-
provenance: { source: 'base' },
|
|
99
|
-
},
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
103
|
-
const data: Record<string, unknown> = { invoice_number: 'CUSTOM-001', customer: 'cust-1' };
|
|
104
|
-
await ops.create(data);
|
|
105
|
-
|
|
106
|
-
expect(data.invoice_number).toBe('CUSTOM-001');
|
|
107
|
-
const namingCalls = insertIntoFn.mock.calls.filter((c: any) => c[0] === 'naming_sequence');
|
|
108
|
-
expect(namingCalls).toHaveLength(0);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('formats sequence without prefix when prefix is omitted', async () => {
|
|
112
|
-
const { db, executeTakeFirstOrThrowFn } = createMockDb();
|
|
113
|
-
executeTakeFirstOrThrowFn.mockResolvedValue({ next_val: 42 });
|
|
114
|
-
|
|
115
|
-
const model = createModel([
|
|
116
|
-
{
|
|
117
|
-
name: 'code',
|
|
118
|
-
config: { type: 'sequence', digits: 4 },
|
|
119
|
-
provenance: { source: 'base' },
|
|
120
|
-
},
|
|
121
|
-
]);
|
|
122
|
-
|
|
123
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
124
|
-
const data: Record<string, unknown> = {};
|
|
125
|
-
await ops.create(data);
|
|
126
|
-
|
|
127
|
-
expect(data.code).toBe('0042');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('formats sequence without padding when digits is omitted', async () => {
|
|
131
|
-
const { db, executeTakeFirstOrThrowFn } = createMockDb();
|
|
132
|
-
executeTakeFirstOrThrowFn.mockResolvedValue({ next_val: 7 });
|
|
133
|
-
|
|
134
|
-
const model = createModel([
|
|
135
|
-
{
|
|
136
|
-
name: 'ref',
|
|
137
|
-
config: { type: 'sequence', prefix: 'REF-' },
|
|
138
|
-
provenance: { source: 'base' },
|
|
139
|
-
},
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
143
|
-
const data: Record<string, unknown> = {};
|
|
144
|
-
await ops.create(data);
|
|
145
|
-
|
|
146
|
-
expect(data.ref).toBe('REF-7');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('handles multiple sequence fields on the same model', async () => {
|
|
150
|
-
const { db, executeTakeFirstOrThrowFn } = createMockDb();
|
|
151
|
-
let callCount = 0;
|
|
152
|
-
executeTakeFirstOrThrowFn.mockImplementation(() => {
|
|
153
|
-
callCount++;
|
|
154
|
-
return Promise.resolve({ next_val: callCount });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const model = createModel([
|
|
158
|
-
{
|
|
159
|
-
name: 'invoice_number',
|
|
160
|
-
config: { type: 'sequence', prefix: 'INV-', digits: 5 },
|
|
161
|
-
provenance: { source: 'base' },
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
name: 'receipt_number',
|
|
165
|
-
config: { type: 'sequence', prefix: 'REC-', digits: 3 },
|
|
166
|
-
provenance: { source: 'base' },
|
|
167
|
-
},
|
|
168
|
-
]);
|
|
169
|
-
|
|
170
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
171
|
-
const data: Record<string, unknown> = {};
|
|
172
|
-
await ops.create(data);
|
|
173
|
-
|
|
174
|
-
expect(data.invoice_number).toBe('INV-00001');
|
|
175
|
-
expect(data.receipt_number).toBe('REC-002');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('skips sequence logic when model has no sequence fields', async () => {
|
|
179
|
-
const { db, insertIntoFn } = createMockDb();
|
|
180
|
-
|
|
181
|
-
const model = createModel([
|
|
182
|
-
{
|
|
183
|
-
name: 'title',
|
|
184
|
-
config: { type: 'string' },
|
|
185
|
-
provenance: { source: 'base' },
|
|
186
|
-
},
|
|
187
|
-
]);
|
|
188
|
-
|
|
189
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
190
|
-
const data: Record<string, unknown> = { title: 'Test' };
|
|
191
|
-
await ops.create(data);
|
|
192
|
-
|
|
193
|
-
const namingCalls = insertIntoFn.mock.calls.filter((c: any) => c[0] === 'naming_sequence');
|
|
194
|
-
expect(namingCalls).toHaveLength(0);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('increments correctly for subsequent calls', async () => {
|
|
198
|
-
const { db, executeTakeFirstOrThrowFn } = createMockDb();
|
|
199
|
-
executeTakeFirstOrThrowFn
|
|
200
|
-
.mockResolvedValueOnce({ next_val: 1 })
|
|
201
|
-
.mockResolvedValueOnce({ next_val: 2 });
|
|
202
|
-
|
|
203
|
-
const model = createModel([
|
|
204
|
-
{
|
|
205
|
-
name: 'order_number',
|
|
206
|
-
config: { type: 'sequence', prefix: 'ORD-', digits: 6 },
|
|
207
|
-
provenance: { source: 'base' },
|
|
208
|
-
},
|
|
209
|
-
]);
|
|
210
|
-
|
|
211
|
-
const ops = new KyselyModelOps({ db, model, registry: mockRegistry });
|
|
212
|
-
|
|
213
|
-
const data1: Record<string, unknown> = {};
|
|
214
|
-
await ops.create(data1);
|
|
215
|
-
expect(data1.order_number).toBe('ORD-000001');
|
|
216
|
-
|
|
217
|
-
const data2: Record<string, unknown> = {};
|
|
218
|
-
await ops.create(data2);
|
|
219
|
-
expect(data2.order_number).toBe('ORD-000002');
|
|
220
|
-
});
|
|
221
|
-
});
|