@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,106 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { evaluateComputedFields } from '../computed-fields.js';
|
|
3
|
-
import type { ExternalFieldConfig } from '../types.js';
|
|
4
|
-
|
|
5
|
-
describe('evaluateComputedFields', () => {
|
|
6
|
-
it('computes a field from existing mapped fields', () => {
|
|
7
|
-
const record = { name: 'Acme', email: 'acme@test.com' };
|
|
8
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
9
|
-
name: { type: 'string' },
|
|
10
|
-
email: { type: 'string' },
|
|
11
|
-
displayName: {
|
|
12
|
-
type: 'string',
|
|
13
|
-
computed: {
|
|
14
|
-
depends: ['name', 'email'],
|
|
15
|
-
compute: (r) => r.name || r.email,
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const result = evaluateComputedFields(record, fields);
|
|
21
|
-
expect(result.displayName).toBe('Acme');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('uses fallback when primary depends field is missing', () => {
|
|
25
|
-
const record = { name: undefined, email: 'acme@test.com' };
|
|
26
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
27
|
-
name: { type: 'string' },
|
|
28
|
-
email: { type: 'string' },
|
|
29
|
-
displayName: {
|
|
30
|
-
type: 'string',
|
|
31
|
-
computed: {
|
|
32
|
-
depends: ['name', 'email'],
|
|
33
|
-
compute: (r) => r.name || r.email,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const result = evaluateComputedFields(record, fields);
|
|
39
|
-
expect(result.displayName).toBe('acme@test.com');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('does not modify non-computed fields', () => {
|
|
43
|
-
const record = { name: 'Acme', email: 'a@b.com' };
|
|
44
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
45
|
-
name: { type: 'string' },
|
|
46
|
-
email: { type: 'string' },
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const result = evaluateComputedFields(record, fields);
|
|
50
|
-
expect(result).toEqual({ name: 'Acme', email: 'a@b.com' });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('handles numeric computed fields', () => {
|
|
54
|
-
const record = { quantity: 5, price: 10 };
|
|
55
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
56
|
-
quantity: { type: 'int' },
|
|
57
|
-
price: { type: 'decimal' },
|
|
58
|
-
total: {
|
|
59
|
-
type: 'decimal',
|
|
60
|
-
computed: {
|
|
61
|
-
depends: ['quantity', 'price'],
|
|
62
|
-
compute: (r) => (r.quantity as number) * (r.price as number),
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const result = evaluateComputedFields(record, fields);
|
|
68
|
-
expect(result.total).toBe(50);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('computed fields can access other computed fields (order dependent)', () => {
|
|
72
|
-
const record = { firstName: 'John', lastName: 'Doe' };
|
|
73
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
74
|
-
firstName: { type: 'string' },
|
|
75
|
-
lastName: { type: 'string' },
|
|
76
|
-
fullName: {
|
|
77
|
-
type: 'string',
|
|
78
|
-
computed: {
|
|
79
|
-
depends: ['firstName', 'lastName'],
|
|
80
|
-
compute: (r) => `${r.firstName} ${r.lastName}`,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const result = evaluateComputedFields(record, fields);
|
|
86
|
-
expect(result.fullName).toBe('John Doe');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('does not mutate the original record', () => {
|
|
90
|
-
const record = { name: 'Acme' };
|
|
91
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
92
|
-
name: { type: 'string' },
|
|
93
|
-
upper: {
|
|
94
|
-
type: 'string',
|
|
95
|
-
computed: {
|
|
96
|
-
depends: ['name'],
|
|
97
|
-
compute: (r) => (r.name as string).toUpperCase(),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
evaluateComputedFields(record, fields);
|
|
103
|
-
expect(record).toEqual({ name: 'Acme' });
|
|
104
|
-
expect(record).not.toHaveProperty('upper');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { resolveFieldValue, mapAdapterResponse, reverseMapForWrite } from '../field-mapper.js';
|
|
3
|
-
|
|
4
|
-
describe('resolveFieldValue', () => {
|
|
5
|
-
it('resolves top-level field', () => {
|
|
6
|
-
expect(resolveFieldValue({ name: 'Acme' }, 'name')).toBe('Acme');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('resolves nested field with dot notation', () => {
|
|
10
|
-
const record = { address: { street: '123 Main St', city: 'Springfield' } };
|
|
11
|
-
expect(resolveFieldValue(record, 'address.street')).toBe('123 Main St');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('resolves deeply nested field', () => {
|
|
15
|
-
const record = { subscription: { plan: { id: 'pro' } } };
|
|
16
|
-
expect(resolveFieldValue(record, 'subscription.plan.id')).toBe('pro');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('returns undefined for missing top-level field', () => {
|
|
20
|
-
expect(resolveFieldValue({}, 'name')).toBeUndefined();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns undefined for missing nested path', () => {
|
|
24
|
-
expect(resolveFieldValue({ a: {} }, 'a.b.c')).toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('returns undefined when intermediate is null', () => {
|
|
28
|
-
expect(resolveFieldValue({ a: null }, 'a.b')).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns undefined when intermediate is a primitive', () => {
|
|
32
|
-
expect(resolveFieldValue({ a: 'string' }, 'a.b')).toBeUndefined();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('handles numeric values', () => {
|
|
36
|
-
expect(resolveFieldValue({ count: 0 }, 'count')).toBe(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('handles false values', () => {
|
|
40
|
-
expect(resolveFieldValue({ active: false }, 'active')).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('mapAdapterResponse', () => {
|
|
45
|
-
it('maps fields with 1:1 naming', () => {
|
|
46
|
-
const raw = { id: '1', email: 'test@example.com' };
|
|
47
|
-
const fields = {
|
|
48
|
-
id: { type: 'string' as const },
|
|
49
|
-
email: { type: 'string' as const },
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
expect(mapAdapterResponse(raw, fields)).toEqual({
|
|
53
|
-
id: '1',
|
|
54
|
-
email: 'test@example.com',
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('maps fields using from property', () => {
|
|
59
|
-
const raw = { metadata: { company_name: 'Acme Corp' } };
|
|
60
|
-
const fields = {
|
|
61
|
-
name: { type: 'string' as const, from: 'metadata.company_name' },
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
expect(mapAdapterResponse(raw, fields)).toEqual({ name: 'Acme Corp' });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('handles mixed direct and mapped fields', () => {
|
|
68
|
-
const raw = { id: 'cus_123', email: 'a@b.com', metadata: { company_name: 'X' } };
|
|
69
|
-
const fields = {
|
|
70
|
-
id: { type: 'string' as const },
|
|
71
|
-
email: { type: 'string' as const },
|
|
72
|
-
name: { type: 'string' as const, from: 'metadata.company_name' },
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
expect(mapAdapterResponse(raw, fields)).toEqual({
|
|
76
|
-
id: 'cus_123',
|
|
77
|
-
email: 'a@b.com',
|
|
78
|
-
name: 'X',
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('skips computed fields', () => {
|
|
83
|
-
const raw = { name: 'Acme', email: 'a@b.com' };
|
|
84
|
-
const fields = {
|
|
85
|
-
name: { type: 'string' as const },
|
|
86
|
-
email: { type: 'string' as const },
|
|
87
|
-
displayName: {
|
|
88
|
-
type: 'string' as const,
|
|
89
|
-
computed: {
|
|
90
|
-
depends: ['name', 'email'],
|
|
91
|
-
compute: (r: Record<string, unknown>) => r.name || r.email,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const result = mapAdapterResponse(raw, fields);
|
|
97
|
-
expect(result).toEqual({ name: 'Acme', email: 'a@b.com' });
|
|
98
|
-
expect(result).not.toHaveProperty('displayName');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('returns undefined for missing source fields', () => {
|
|
102
|
-
const raw = {};
|
|
103
|
-
const fields = {
|
|
104
|
-
name: { type: 'string' as const, from: 'metadata.company_name' },
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
expect(mapAdapterResponse(raw, fields)).toEqual({ name: undefined });
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('reverseMapForWrite', () => {
|
|
112
|
-
it('maps 1:1 fields directly', () => {
|
|
113
|
-
const data = { email: 'new@test.com' };
|
|
114
|
-
const fields = { email: { type: 'string' as const } };
|
|
115
|
-
|
|
116
|
-
expect(reverseMapForWrite(data, fields)).toEqual({ email: 'new@test.com' });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('reverse maps fields with from to nested structure', () => {
|
|
120
|
-
const data = { name: 'Acme Corp' };
|
|
121
|
-
const fields = {
|
|
122
|
-
name: { type: 'string' as const, from: 'metadata.company_name' },
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
expect(reverseMapForWrite(data, fields)).toEqual({
|
|
126
|
-
metadata: { company_name: 'Acme Corp' },
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('skips computed fields', () => {
|
|
131
|
-
const data = { name: 'Acme', displayName: 'Acme Corp' };
|
|
132
|
-
const fields = {
|
|
133
|
-
name: { type: 'string' as const },
|
|
134
|
-
displayName: {
|
|
135
|
-
type: 'string' as const,
|
|
136
|
-
computed: { depends: ['name'], compute: (r: Record<string, unknown>) => r.name },
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
expect(reverseMapForWrite(data, fields)).toEqual({ name: 'Acme' });
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('skips fields not in schema', () => {
|
|
144
|
-
const data = { name: 'Acme', unknown: 'value' };
|
|
145
|
-
const fields = { name: { type: 'string' as const } };
|
|
146
|
-
|
|
147
|
-
expect(reverseMapForWrite(data, fields)).toEqual({ name: 'Acme' });
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('builds nested objects for deep paths', () => {
|
|
151
|
-
const data = { plan: 'pro' };
|
|
152
|
-
const fields = {
|
|
153
|
-
plan: { type: 'string' as const, from: 'subscription.plan.id' },
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
expect(reverseMapForWrite(data, fields)).toEqual({
|
|
157
|
-
subscription: { plan: { id: 'pro' } },
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
applyInMemoryFilters,
|
|
4
|
-
applyInMemorySort,
|
|
5
|
-
applyInMemoryPagination,
|
|
6
|
-
} from '../in-memory-ops.js';
|
|
7
|
-
import type { TranslatedFilter } from '../../model-api/filter-translator.js';
|
|
8
|
-
|
|
9
|
-
describe('applyInMemoryFilters', () => {
|
|
10
|
-
const records = [
|
|
11
|
-
{ id: '1', name: 'Alice', age: 30, email: 'alice@acme.com', status: 'active' },
|
|
12
|
-
{ id: '2', name: 'Bob', age: 25, email: 'bob@test.org', status: 'inactive' },
|
|
13
|
-
{ id: '3', name: 'Charlie', age: 35, email: 'charlie@acme.com', status: 'active' },
|
|
14
|
-
{ id: '4', name: 'Diana', age: 28, email: null, status: null },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
describe('eq operator', () => {
|
|
18
|
-
it('filters by exact match', () => {
|
|
19
|
-
const filters: TranslatedFilter[] = [{ field: 'status', operator: 'eq', value: 'active' }];
|
|
20
|
-
const result = applyInMemoryFilters(records, filters);
|
|
21
|
-
expect(result).toHaveLength(2);
|
|
22
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Charlie']);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('handles numeric equality', () => {
|
|
26
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'eq', value: 25 }];
|
|
27
|
-
const result = applyInMemoryFilters(records, filters);
|
|
28
|
-
expect(result).toHaveLength(1);
|
|
29
|
-
expect(result[0].name).toBe('Bob');
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('neq operator', () => {
|
|
34
|
-
it('excludes matching records', () => {
|
|
35
|
-
const filters: TranslatedFilter[] = [{ field: 'status', operator: 'neq', value: 'active' }];
|
|
36
|
-
const result = applyInMemoryFilters(records, filters);
|
|
37
|
-
expect(result).toHaveLength(2);
|
|
38
|
-
expect(result.map((r) => r.name)).toEqual(['Bob', 'Diana']);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('comparison operators', () => {
|
|
43
|
-
it('gt filters greater than', () => {
|
|
44
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'gt', value: 28 }];
|
|
45
|
-
const result = applyInMemoryFilters(records, filters);
|
|
46
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Charlie']);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('gte includes boundary', () => {
|
|
50
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'gte', value: 28 }];
|
|
51
|
-
const result = applyInMemoryFilters(records, filters);
|
|
52
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Charlie', 'Diana']);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('lt filters less than', () => {
|
|
56
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'lt', value: 28 }];
|
|
57
|
-
const result = applyInMemoryFilters(records, filters);
|
|
58
|
-
expect(result.map((r) => r.name)).toEqual(['Bob']);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('lte includes boundary', () => {
|
|
62
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'lte', value: 28 }];
|
|
63
|
-
const result = applyInMemoryFilters(records, filters);
|
|
64
|
-
expect(result.map((r) => r.name)).toEqual(['Bob', 'Diana']);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('in / notIn operators', () => {
|
|
69
|
-
it('in matches values in array', () => {
|
|
70
|
-
const filters: TranslatedFilter[] = [
|
|
71
|
-
{ field: 'name', operator: 'in', value: ['Alice', 'Bob'] },
|
|
72
|
-
];
|
|
73
|
-
const result = applyInMemoryFilters(records, filters);
|
|
74
|
-
expect(result).toHaveLength(2);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('notIn excludes values in array', () => {
|
|
78
|
-
const filters: TranslatedFilter[] = [
|
|
79
|
-
{ field: 'name', operator: 'notIn', value: ['Alice', 'Bob'] },
|
|
80
|
-
];
|
|
81
|
-
const result = applyInMemoryFilters(records, filters);
|
|
82
|
-
expect(result.map((r) => r.name)).toEqual(['Charlie', 'Diana']);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('string operators', () => {
|
|
87
|
-
it('contains matches substring (case-insensitive)', () => {
|
|
88
|
-
const filters: TranslatedFilter[] = [{ field: 'email', operator: 'contains', value: 'acme' }];
|
|
89
|
-
const result = applyInMemoryFilters(records, filters);
|
|
90
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Charlie']);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('startsWith matches prefix (case-insensitive)', () => {
|
|
94
|
-
const filters: TranslatedFilter[] = [{ field: 'name', operator: 'startsWith', value: 'al' }];
|
|
95
|
-
const result = applyInMemoryFilters(records, filters);
|
|
96
|
-
expect(result).toHaveLength(1);
|
|
97
|
-
expect(result[0].name).toBe('Alice');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('endsWith matches suffix (case-insensitive)', () => {
|
|
101
|
-
const filters: TranslatedFilter[] = [{ field: 'email', operator: 'endsWith', value: '.org' }];
|
|
102
|
-
const result = applyInMemoryFilters(records, filters);
|
|
103
|
-
expect(result).toHaveLength(1);
|
|
104
|
-
expect(result[0].name).toBe('Bob');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('contains returns false for non-string fields', () => {
|
|
108
|
-
const filters: TranslatedFilter[] = [{ field: 'age', operator: 'contains', value: '3' }];
|
|
109
|
-
const result = applyInMemoryFilters(records, filters);
|
|
110
|
-
expect(result).toHaveLength(0);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('null check operators', () => {
|
|
115
|
-
it('is null matches null values', () => {
|
|
116
|
-
const filters: TranslatedFilter[] = [{ field: 'email', operator: 'is', value: null }];
|
|
117
|
-
const result = applyInMemoryFilters(records, filters);
|
|
118
|
-
expect(result).toHaveLength(1);
|
|
119
|
-
expect(result[0].name).toBe('Diana');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('is not_null matches non-null values', () => {
|
|
123
|
-
const filters: TranslatedFilter[] = [{ field: 'email', operator: 'is', value: 'not_null' }];
|
|
124
|
-
const result = applyInMemoryFilters(records, filters);
|
|
125
|
-
expect(result).toHaveLength(3);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('isnull true matches null values', () => {
|
|
129
|
-
const filters: TranslatedFilter[] = [{ field: 'status', operator: 'isnull', value: true }];
|
|
130
|
-
const result = applyInMemoryFilters(records, filters);
|
|
131
|
-
expect(result).toHaveLength(1);
|
|
132
|
-
expect(result[0].name).toBe('Diana');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('multiple filters (AND)', () => {
|
|
137
|
-
it('applies all filters', () => {
|
|
138
|
-
const filters: TranslatedFilter[] = [
|
|
139
|
-
{ field: 'status', operator: 'eq', value: 'active' },
|
|
140
|
-
{ field: 'age', operator: 'gt', value: 30 },
|
|
141
|
-
];
|
|
142
|
-
const result = applyInMemoryFilters(records, filters);
|
|
143
|
-
expect(result).toHaveLength(1);
|
|
144
|
-
expect(result[0].name).toBe('Charlie');
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('empty input', () => {
|
|
149
|
-
it('returns all records with empty filters', () => {
|
|
150
|
-
const result = applyInMemoryFilters(records, []);
|
|
151
|
-
expect(result).toHaveLength(4);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('returns empty array for empty records', () => {
|
|
155
|
-
const filters: TranslatedFilter[] = [{ field: 'name', operator: 'eq', value: 'test' }];
|
|
156
|
-
const result = applyInMemoryFilters([], filters);
|
|
157
|
-
expect(result).toEqual([]);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('applyInMemorySort', () => {
|
|
163
|
-
const records = [
|
|
164
|
-
{ id: '1', name: 'Charlie', age: 35 },
|
|
165
|
-
{ id: '2', name: 'Alice', age: 30 },
|
|
166
|
-
{ id: '3', name: 'Bob', age: 25 },
|
|
167
|
-
{ id: '4', name: 'Alice', age: 28 },
|
|
168
|
-
];
|
|
169
|
-
|
|
170
|
-
it('sorts ascending by string field', () => {
|
|
171
|
-
const result = applyInMemorySort(records, [{ field: 'name', direction: 'asc' }]);
|
|
172
|
-
expect(result.map((r) => r.name)).toEqual(['Alice', 'Alice', 'Bob', 'Charlie']);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('sorts descending by string field', () => {
|
|
176
|
-
const result = applyInMemorySort(records, [{ field: 'name', direction: 'desc' }]);
|
|
177
|
-
expect(result.map((r) => r.name)).toEqual(['Charlie', 'Bob', 'Alice', 'Alice']);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('sorts by numeric field', () => {
|
|
181
|
-
const result = applyInMemorySort(records, [{ field: 'age', direction: 'asc' }]);
|
|
182
|
-
expect(result.map((r) => r.age)).toEqual([25, 28, 30, 35]);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('sorts by multiple fields', () => {
|
|
186
|
-
const result = applyInMemorySort(records, [
|
|
187
|
-
{ field: 'name', direction: 'asc' },
|
|
188
|
-
{ field: 'age', direction: 'desc' },
|
|
189
|
-
]);
|
|
190
|
-
expect(result.map((r) => ({ name: r.name, age: r.age }))).toEqual([
|
|
191
|
-
{ name: 'Alice', age: 30 },
|
|
192
|
-
{ name: 'Alice', age: 28 },
|
|
193
|
-
{ name: 'Bob', age: 25 },
|
|
194
|
-
{ name: 'Charlie', age: 35 },
|
|
195
|
-
]);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('handles null values (nulls first in asc)', () => {
|
|
199
|
-
const data = [
|
|
200
|
-
{ id: '1', name: 'Bob' },
|
|
201
|
-
{ id: '2', name: null },
|
|
202
|
-
{ id: '3', name: 'Alice' },
|
|
203
|
-
];
|
|
204
|
-
const result = applyInMemorySort(data, [{ field: 'name', direction: 'asc' }]);
|
|
205
|
-
expect(result.map((r) => r.name)).toEqual([null, 'Alice', 'Bob']);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('returns same order with no sorts', () => {
|
|
209
|
-
const result = applyInMemorySort(records, []);
|
|
210
|
-
expect(result).toEqual(records);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('does not mutate original array', () => {
|
|
214
|
-
const original = [...records];
|
|
215
|
-
applyInMemorySort(records, [{ field: 'age', direction: 'asc' }]);
|
|
216
|
-
expect(records).toEqual(original);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe('applyInMemoryPagination', () => {
|
|
221
|
-
const records = Array.from({ length: 10 }, (_, i) => ({ id: String(i + 1), index: i }));
|
|
222
|
-
|
|
223
|
-
it('returns first page', () => {
|
|
224
|
-
const result = applyInMemoryPagination(records, 3, 0);
|
|
225
|
-
expect(result.map((r) => r.index)).toEqual([0, 1, 2]);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('returns second page', () => {
|
|
229
|
-
const result = applyInMemoryPagination(records, 3, 3);
|
|
230
|
-
expect(result.map((r) => r.index)).toEqual([3, 4, 5]);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('returns partial last page', () => {
|
|
234
|
-
const result = applyInMemoryPagination(records, 3, 9);
|
|
235
|
-
expect(result.map((r) => r.index)).toEqual([9]);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('returns empty when offset exceeds length', () => {
|
|
239
|
-
const result = applyInMemoryPagination(records, 3, 20);
|
|
240
|
-
expect(result).toEqual([]);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('returns all records when limit exceeds length', () => {
|
|
244
|
-
const result = applyInMemoryPagination(records, 100, 0);
|
|
245
|
-
expect(result).toHaveLength(10);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { ExternalMutationExecutor, CapabilityNotSupportedError } from '../mutation-executor.js';
|
|
3
|
-
import type { DataAdapter } from '../../plugins/types.js';
|
|
4
|
-
import type { ExternalFieldConfig } from '../types.js';
|
|
5
|
-
|
|
6
|
-
function makeAdapter(overrides?: Partial<DataAdapter>): DataAdapter {
|
|
7
|
-
return {
|
|
8
|
-
async get(_model, _id) {
|
|
9
|
-
return null;
|
|
10
|
-
},
|
|
11
|
-
...overrides,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const baseFields: Record<string, ExternalFieldConfig> = {
|
|
16
|
-
id: { type: 'string' },
|
|
17
|
-
email: { type: 'string' },
|
|
18
|
-
name: { type: 'string', from: 'metadata.company_name' },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
describe('ExternalMutationExecutor', () => {
|
|
22
|
-
describe('create', () => {
|
|
23
|
-
it('calls adapter.create with reverse-mapped data', async () => {
|
|
24
|
-
const create = vi.fn().mockResolvedValue({
|
|
25
|
-
id: 'cus_1',
|
|
26
|
-
email: 'a@b.com',
|
|
27
|
-
metadata: { company_name: 'Acme' },
|
|
28
|
-
});
|
|
29
|
-
const adapter = makeAdapter({ create });
|
|
30
|
-
const executor = new ExternalMutationExecutor({
|
|
31
|
-
adapter,
|
|
32
|
-
adapterName: 'stripe',
|
|
33
|
-
modelName: 'billing.Customer',
|
|
34
|
-
fields: baseFields,
|
|
35
|
-
capabilities: ['read', 'create'],
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const result = await executor.create({ email: 'a@b.com', name: 'Acme' });
|
|
39
|
-
|
|
40
|
-
expect(create).toHaveBeenCalledWith('billing.Customer', {
|
|
41
|
-
email: 'a@b.com',
|
|
42
|
-
metadata: { company_name: 'Acme' },
|
|
43
|
-
});
|
|
44
|
-
expect(result).toEqual({ id: 'cus_1', email: 'a@b.com', name: 'Acme' });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('throws CapabilityNotSupportedError when create not supported', async () => {
|
|
48
|
-
const adapter = makeAdapter();
|
|
49
|
-
const executor = new ExternalMutationExecutor({
|
|
50
|
-
adapter,
|
|
51
|
-
adapterName: 'stripe',
|
|
52
|
-
modelName: 'billing.Customer',
|
|
53
|
-
fields: baseFields,
|
|
54
|
-
capabilities: ['read'],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
await expect(executor.create({ email: 'test' })).rejects.toThrow(CapabilityNotSupportedError);
|
|
58
|
-
await expect(executor.create({ email: 'test' })).rejects.toThrow(
|
|
59
|
-
'Adapter "stripe" does not support operation "create"',
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('evaluates computed fields on response', async () => {
|
|
64
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
65
|
-
id: { type: 'string' },
|
|
66
|
-
name: { type: 'string' },
|
|
67
|
-
upper: {
|
|
68
|
-
type: 'string',
|
|
69
|
-
computed: { depends: ['name'], compute: (r) => (r.name as string).toUpperCase() },
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
const create = vi.fn().mockResolvedValue({ id: '1', name: 'alice' });
|
|
73
|
-
const adapter = makeAdapter({ create });
|
|
74
|
-
const executor = new ExternalMutationExecutor({
|
|
75
|
-
adapter,
|
|
76
|
-
adapterName: 'test',
|
|
77
|
-
modelName: 'test.Model',
|
|
78
|
-
fields,
|
|
79
|
-
capabilities: ['read', 'create'],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const result = await executor.create({ name: 'alice' });
|
|
83
|
-
expect(result.upper).toBe('ALICE');
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('update', () => {
|
|
88
|
-
it('calls adapter.update with reverse-mapped data', async () => {
|
|
89
|
-
const update = vi.fn().mockResolvedValue({
|
|
90
|
-
id: 'cus_1',
|
|
91
|
-
email: 'new@b.com',
|
|
92
|
-
metadata: { company_name: 'NewCo' },
|
|
93
|
-
});
|
|
94
|
-
const adapter = makeAdapter({ update });
|
|
95
|
-
const executor = new ExternalMutationExecutor({
|
|
96
|
-
adapter,
|
|
97
|
-
adapterName: 'stripe',
|
|
98
|
-
modelName: 'billing.Customer',
|
|
99
|
-
fields: baseFields,
|
|
100
|
-
capabilities: ['read', 'update'],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const result = await executor.update('cus_1', { email: 'new@b.com', name: 'NewCo' });
|
|
104
|
-
|
|
105
|
-
expect(update).toHaveBeenCalledWith('billing.Customer', 'cus_1', {
|
|
106
|
-
email: 'new@b.com',
|
|
107
|
-
metadata: { company_name: 'NewCo' },
|
|
108
|
-
});
|
|
109
|
-
expect(result).toEqual({ id: 'cus_1', email: 'new@b.com', name: 'NewCo' });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('throws CapabilityNotSupportedError when update not supported', async () => {
|
|
113
|
-
const adapter = makeAdapter();
|
|
114
|
-
const executor = new ExternalMutationExecutor({
|
|
115
|
-
adapter,
|
|
116
|
-
adapterName: 'stripe',
|
|
117
|
-
modelName: 'billing.Customer',
|
|
118
|
-
fields: baseFields,
|
|
119
|
-
capabilities: ['read'],
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await expect(executor.update('1', { email: 'test' })).rejects.toThrow(
|
|
123
|
-
CapabilityNotSupportedError,
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('delete', () => {
|
|
129
|
-
it('calls adapter.delete', async () => {
|
|
130
|
-
const del = vi.fn().mockResolvedValue(undefined);
|
|
131
|
-
const adapter = makeAdapter({ delete: del });
|
|
132
|
-
const executor = new ExternalMutationExecutor({
|
|
133
|
-
adapter,
|
|
134
|
-
adapterName: 'stripe',
|
|
135
|
-
modelName: 'billing.Customer',
|
|
136
|
-
fields: baseFields,
|
|
137
|
-
capabilities: ['read', 'delete'],
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
await executor.delete('cus_1');
|
|
141
|
-
expect(del).toHaveBeenCalledWith('billing.Customer', 'cus_1');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('throws CapabilityNotSupportedError when delete not supported', async () => {
|
|
145
|
-
const adapter = makeAdapter();
|
|
146
|
-
const executor = new ExternalMutationExecutor({
|
|
147
|
-
adapter,
|
|
148
|
-
adapterName: 'stripe',
|
|
149
|
-
modelName: 'billing.Customer',
|
|
150
|
-
fields: baseFields,
|
|
151
|
-
capabilities: ['read'],
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
await expect(executor.delete('cus_1')).rejects.toThrow(CapabilityNotSupportedError);
|
|
155
|
-
await expect(executor.delete('cus_1')).rejects.toThrow(
|
|
156
|
-
'Adapter "stripe" does not support operation "delete"',
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
});
|