@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,284 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { ExternalQueryExecutor } from '../query-executor.js';
|
|
3
|
-
import type { DataAdapter, AdapterCapability } 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('ExternalQueryExecutor', () => {
|
|
22
|
-
describe('execGet', () => {
|
|
23
|
-
it('returns null when adapter returns null', async () => {
|
|
24
|
-
const adapter = makeAdapter({
|
|
25
|
-
async get() {
|
|
26
|
-
return null;
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
const executor = new ExternalQueryExecutor({
|
|
30
|
-
adapter,
|
|
31
|
-
modelName: 'billing.Customer',
|
|
32
|
-
fields: baseFields,
|
|
33
|
-
capabilities: ['read'],
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(await executor.execGet('non-existent')).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('returns mapped record', async () => {
|
|
40
|
-
const adapter = makeAdapter({
|
|
41
|
-
async get() {
|
|
42
|
-
return { id: 'cus_1', email: 'a@b.com', metadata: { company_name: 'Acme' } };
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
const executor = new ExternalQueryExecutor({
|
|
46
|
-
adapter,
|
|
47
|
-
modelName: 'billing.Customer',
|
|
48
|
-
fields: baseFields,
|
|
49
|
-
capabilities: ['read'],
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const result = await executor.execGet('cus_1');
|
|
53
|
-
expect(result).toEqual({ id: 'cus_1', email: 'a@b.com', name: 'Acme' });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('evaluates computed fields', async () => {
|
|
57
|
-
const fields: Record<string, ExternalFieldConfig> = {
|
|
58
|
-
id: { type: 'string' },
|
|
59
|
-
name: { type: 'string' },
|
|
60
|
-
upper: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
computed: { depends: ['name'], compute: (r) => (r.name as string).toUpperCase() },
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
const adapter = makeAdapter({
|
|
66
|
-
async get() {
|
|
67
|
-
return { id: '1', name: 'alice' };
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
const executor = new ExternalQueryExecutor({
|
|
71
|
-
adapter,
|
|
72
|
-
modelName: 'test.Model',
|
|
73
|
-
fields,
|
|
74
|
-
capabilities: ['read'],
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const result = await executor.execGet('1');
|
|
78
|
-
expect(result?.upper).toBe('ALICE');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('passes model name to adapter', async () => {
|
|
82
|
-
const get = vi.fn().mockResolvedValue({ id: '1' });
|
|
83
|
-
const adapter = makeAdapter({ get });
|
|
84
|
-
const executor = new ExternalQueryExecutor({
|
|
85
|
-
adapter,
|
|
86
|
-
modelName: 'billing.Customer',
|
|
87
|
-
fields: { id: { type: 'string' } },
|
|
88
|
-
capabilities: ['read'],
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
await executor.execGet('cus_1');
|
|
92
|
-
expect(get).toHaveBeenCalledWith('billing.Customer', 'cus_1');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('execList', () => {
|
|
97
|
-
it('calls adapter.list when capability is available', async () => {
|
|
98
|
-
const list = vi.fn().mockResolvedValue({
|
|
99
|
-
data: [
|
|
100
|
-
{ id: '1', email: 'a@b.com', metadata: { company_name: 'A' } },
|
|
101
|
-
{ id: '2', email: 'c@d.com', metadata: { company_name: 'B' } },
|
|
102
|
-
],
|
|
103
|
-
total: 2,
|
|
104
|
-
});
|
|
105
|
-
const adapter = makeAdapter({ list });
|
|
106
|
-
const executor = new ExternalQueryExecutor({
|
|
107
|
-
adapter,
|
|
108
|
-
modelName: 'billing.Customer',
|
|
109
|
-
fields: baseFields,
|
|
110
|
-
capabilities: ['read', 'list'],
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const result = await executor.execList({
|
|
114
|
-
filters: [],
|
|
115
|
-
sorts: [],
|
|
116
|
-
fieldNames: [],
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
expect(result.data).toHaveLength(2);
|
|
120
|
-
expect(result.data[0]).toEqual({ id: '1', email: 'a@b.com', name: 'A' });
|
|
121
|
-
expect(result.total).toBe(2);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('passes filters to adapter when filter capability exists', async () => {
|
|
125
|
-
const list = vi.fn().mockResolvedValue({ data: [], total: 0 });
|
|
126
|
-
const adapter = makeAdapter({ list });
|
|
127
|
-
const capabilities: AdapterCapability[] = ['read', 'list', 'filter'];
|
|
128
|
-
const executor = new ExternalQueryExecutor({
|
|
129
|
-
adapter,
|
|
130
|
-
modelName: 'billing.Customer',
|
|
131
|
-
fields: baseFields,
|
|
132
|
-
capabilities,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
await executor.execList({
|
|
136
|
-
filters: [{ field: 'email', operator: 'eq', value: 'test@test.com' }],
|
|
137
|
-
sorts: [],
|
|
138
|
-
fieldNames: [],
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
expect(list).toHaveBeenCalledWith(
|
|
142
|
-
'billing.Customer',
|
|
143
|
-
expect.objectContaining({
|
|
144
|
-
filters: [{ field: 'email', operator: 'eq', value: 'test@test.com' }],
|
|
145
|
-
}),
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('applies in-memory filter when adapter lacks filter capability', async () => {
|
|
150
|
-
const list = vi.fn().mockResolvedValue({
|
|
151
|
-
data: [
|
|
152
|
-
{ id: '1', email: 'alice@acme.com', metadata: { company_name: 'Acme' } },
|
|
153
|
-
{ id: '2', email: 'bob@other.com', metadata: { company_name: 'Other' } },
|
|
154
|
-
],
|
|
155
|
-
total: 2,
|
|
156
|
-
});
|
|
157
|
-
const adapter = makeAdapter({ list });
|
|
158
|
-
const executor = new ExternalQueryExecutor({
|
|
159
|
-
adapter,
|
|
160
|
-
modelName: 'billing.Customer',
|
|
161
|
-
fields: baseFields,
|
|
162
|
-
capabilities: ['read', 'list'],
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const result = await executor.execList({
|
|
166
|
-
filters: [{ field: 'email', operator: 'contains', value: 'acme' }],
|
|
167
|
-
sorts: [],
|
|
168
|
-
fieldNames: [],
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(result.data).toHaveLength(1);
|
|
172
|
-
expect(result.data[0].email).toBe('alice@acme.com');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('applies in-memory sort when adapter lacks sort capability', async () => {
|
|
176
|
-
const list = vi.fn().mockResolvedValue({
|
|
177
|
-
data: [
|
|
178
|
-
{ id: '2', email: 'b@b.com', metadata: { company_name: 'B' } },
|
|
179
|
-
{ id: '1', email: 'a@a.com', metadata: { company_name: 'A' } },
|
|
180
|
-
],
|
|
181
|
-
total: 2,
|
|
182
|
-
});
|
|
183
|
-
const adapter = makeAdapter({ list });
|
|
184
|
-
const executor = new ExternalQueryExecutor({
|
|
185
|
-
adapter,
|
|
186
|
-
modelName: 'billing.Customer',
|
|
187
|
-
fields: baseFields,
|
|
188
|
-
capabilities: ['read', 'list'],
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const result = await executor.execList({
|
|
192
|
-
filters: [],
|
|
193
|
-
sorts: [{ field: 'email', direction: 'asc' }],
|
|
194
|
-
fieldNames: [],
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
expect(result.data[0].email).toBe('a@a.com');
|
|
198
|
-
expect(result.data[1].email).toBe('b@b.com');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('returns empty data when no list capability', async () => {
|
|
202
|
-
const adapter = makeAdapter();
|
|
203
|
-
const executor = new ExternalQueryExecutor({
|
|
204
|
-
adapter,
|
|
205
|
-
modelName: 'billing.Customer',
|
|
206
|
-
fields: baseFields,
|
|
207
|
-
capabilities: ['read'],
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const result = await executor.execList({
|
|
211
|
-
filters: [],
|
|
212
|
-
sorts: [],
|
|
213
|
-
fieldNames: [],
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(result.data).toEqual([]);
|
|
217
|
-
expect(result.total).toBe(0);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('uses default limit of 25', async () => {
|
|
221
|
-
const list = vi.fn().mockResolvedValue({ data: [], total: 0 });
|
|
222
|
-
const adapter = makeAdapter({ list });
|
|
223
|
-
const executor = new ExternalQueryExecutor({
|
|
224
|
-
adapter,
|
|
225
|
-
modelName: 'billing.Customer',
|
|
226
|
-
fields: baseFields,
|
|
227
|
-
capabilities: ['read', 'list'],
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
await executor.execList({ filters: [], sorts: [], fieldNames: [] });
|
|
231
|
-
expect(list).toHaveBeenCalledWith(
|
|
232
|
-
'billing.Customer',
|
|
233
|
-
expect.objectContaining({ pageSize: 25 }),
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('respects custom limit', async () => {
|
|
238
|
-
const list = vi.fn().mockResolvedValue({ data: [], total: 0 });
|
|
239
|
-
const adapter = makeAdapter({ list });
|
|
240
|
-
const executor = new ExternalQueryExecutor({
|
|
241
|
-
adapter,
|
|
242
|
-
modelName: 'billing.Customer',
|
|
243
|
-
fields: baseFields,
|
|
244
|
-
capabilities: ['read', 'list'],
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
await executor.execList({ filters: [], sorts: [], fieldNames: [], limitVal: 10 });
|
|
248
|
-
expect(list).toHaveBeenCalledWith(
|
|
249
|
-
'billing.Customer',
|
|
250
|
-
expect.objectContaining({ pageSize: 10 }),
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('execCount', () => {
|
|
256
|
-
it('returns total from list result', async () => {
|
|
257
|
-
const list = vi.fn().mockResolvedValue({ data: [{ id: '1' }, { id: '2' }], total: 50 });
|
|
258
|
-
const adapter = makeAdapter({ list });
|
|
259
|
-
const executor = new ExternalQueryExecutor({
|
|
260
|
-
adapter,
|
|
261
|
-
modelName: 'billing.Customer',
|
|
262
|
-
fields: { id: { type: 'string' } },
|
|
263
|
-
capabilities: ['read', 'list'],
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const count = await executor.execCount({ filters: [], sorts: [], fieldNames: [] });
|
|
267
|
-
expect(count).toBe(50);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('falls back to data length when total not provided', async () => {
|
|
271
|
-
const list = vi.fn().mockResolvedValue({ data: [{ id: '1' }, { id: '2' }] });
|
|
272
|
-
const adapter = makeAdapter({ list });
|
|
273
|
-
const executor = new ExternalQueryExecutor({
|
|
274
|
-
adapter,
|
|
275
|
-
modelName: 'billing.Customer',
|
|
276
|
-
fields: { id: { type: 'string' } },
|
|
277
|
-
capabilities: ['read', 'list'],
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const count = await executor.execCount({ filters: [], sorts: [], fieldNames: [] });
|
|
281
|
-
expect(count).toBe(2);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { externalModelToResolved } from '../schema-converter.js';
|
|
3
|
-
import type { ExternalModelConfig } from '../types.js';
|
|
4
|
-
|
|
5
|
-
describe('externalModelToResolved', () => {
|
|
6
|
-
const baseConfig: ExternalModelConfig = {
|
|
7
|
-
name: 'Customer',
|
|
8
|
-
source: 'stripe',
|
|
9
|
-
label: 'Stripe Customer',
|
|
10
|
-
fields: {
|
|
11
|
-
id: { type: 'string' },
|
|
12
|
-
email: { type: 'string' },
|
|
13
|
-
name: { type: 'string', from: 'metadata.company_name' },
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
it('produces correct qualifiedName', () => {
|
|
18
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
19
|
-
expect(model.qualifiedName).toBe('billing.Customer');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('sets source from config', () => {
|
|
23
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
24
|
-
expect(model.source).toBe('stripe');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('sets app and module', () => {
|
|
28
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
29
|
-
expect(model.app).toBe('billing-app');
|
|
30
|
-
expect(model.module).toBe('billing');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('sets name and label', () => {
|
|
34
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
35
|
-
expect(model.name).toBe('Customer');
|
|
36
|
-
expect(model.label).toBe('Stripe Customer');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('has empty traits', () => {
|
|
40
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
41
|
-
expect(model.traits).toEqual([]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('has empty indexes', () => {
|
|
45
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
46
|
-
expect(model.indexes).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('sets auditLog to false', () => {
|
|
50
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
51
|
-
expect(model.auditLog).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('converts fields to ResolvedField array', () => {
|
|
55
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
56
|
-
expect(model.fields).toHaveLength(3);
|
|
57
|
-
|
|
58
|
-
const idField = model.fields.find((f) => f.name === 'id');
|
|
59
|
-
expect(idField).toEqual({
|
|
60
|
-
name: 'id',
|
|
61
|
-
config: { type: 'string', label: undefined, required: undefined },
|
|
62
|
-
provenance: { source: 'base' },
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('preserves field type and label', () => {
|
|
67
|
-
const config: ExternalModelConfig = {
|
|
68
|
-
name: 'Invoice',
|
|
69
|
-
source: 'stripe',
|
|
70
|
-
fields: {
|
|
71
|
-
amount: { type: 'decimal', label: 'Amount', required: true },
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const { model } = externalModelToResolved(config, 'app', 'billing');
|
|
76
|
-
const amountField = model.fields.find((f) => f.name === 'amount');
|
|
77
|
-
expect(amountField?.config).toEqual({ type: 'decimal', label: 'Amount', required: true });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('sets provenance to base for all fields', () => {
|
|
81
|
-
const { model } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
82
|
-
for (const field of model.fields) {
|
|
83
|
-
expect(field.provenance).toEqual({ source: 'base' });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('handles config without label', () => {
|
|
88
|
-
const config: ExternalModelConfig = {
|
|
89
|
-
name: 'Product',
|
|
90
|
-
source: 'shopify',
|
|
91
|
-
fields: { id: { type: 'string' } },
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const { model } = externalModelToResolved(config, 'app', 'store');
|
|
95
|
-
expect(model.label).toBeUndefined();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('relationships', () => {
|
|
99
|
-
it('returns empty relationships when no relationship fields', () => {
|
|
100
|
-
const { relationships } = externalModelToResolved(baseConfig, 'billing-app', 'billing');
|
|
101
|
-
expect(relationships).toEqual([]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('extracts link relationships from fields', () => {
|
|
105
|
-
const config: ExternalModelConfig = {
|
|
106
|
-
name: 'Invoice',
|
|
107
|
-
source: 'stripe',
|
|
108
|
-
fields: {
|
|
109
|
-
id: { type: 'string' },
|
|
110
|
-
order: {
|
|
111
|
-
type: 'string',
|
|
112
|
-
from: 'metadata.order_id',
|
|
113
|
-
relationship: { type: 'link', model: 'sales.Order' },
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const { relationships } = externalModelToResolved(config, 'app', 'billing');
|
|
119
|
-
expect(relationships).toHaveLength(1);
|
|
120
|
-
expect(relationships[0]).toEqual({
|
|
121
|
-
type: 'link',
|
|
122
|
-
from: 'billing.Invoice',
|
|
123
|
-
field: 'order',
|
|
124
|
-
to: 'sales.Order',
|
|
125
|
-
foreignKey: undefined,
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('extracts hasMany relationships with foreignKey', () => {
|
|
130
|
-
const config: ExternalModelConfig = {
|
|
131
|
-
name: 'Customer',
|
|
132
|
-
source: 'stripe',
|
|
133
|
-
fields: {
|
|
134
|
-
id: { type: 'string' },
|
|
135
|
-
invoices: {
|
|
136
|
-
type: 'json',
|
|
137
|
-
relationship: { type: 'hasMany', model: 'billing.Invoice', foreignKey: 'customer_id' },
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const { relationships } = externalModelToResolved(config, 'app', 'billing');
|
|
143
|
-
expect(relationships).toHaveLength(1);
|
|
144
|
-
expect(relationships[0]).toEqual({
|
|
145
|
-
type: 'hasMany',
|
|
146
|
-
from: 'billing.Customer',
|
|
147
|
-
field: 'invoices',
|
|
148
|
-
to: 'billing.Invoice',
|
|
149
|
-
foreignKey: 'customer_id',
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('extracts multiple relationships', () => {
|
|
154
|
-
const config: ExternalModelConfig = {
|
|
155
|
-
name: 'Order',
|
|
156
|
-
source: 'stripe',
|
|
157
|
-
fields: {
|
|
158
|
-
id: { type: 'string' },
|
|
159
|
-
customer: {
|
|
160
|
-
type: 'string',
|
|
161
|
-
relationship: { type: 'link', model: 'billing.Customer' },
|
|
162
|
-
},
|
|
163
|
-
items: {
|
|
164
|
-
type: 'json',
|
|
165
|
-
relationship: { type: 'hasMany', model: 'billing.LineItem', foreignKey: 'order_id' },
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const { relationships } = externalModelToResolved(config, 'app', 'billing');
|
|
171
|
-
expect(relationships).toHaveLength(2);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ExternalFieldConfig } from './types.js';
|
|
2
|
-
|
|
3
|
-
export function evaluateComputedFields(
|
|
4
|
-
record: Record<string, unknown>,
|
|
5
|
-
fields: Record<string, ExternalFieldConfig>,
|
|
6
|
-
): Record<string, unknown> {
|
|
7
|
-
const result = { ...record };
|
|
8
|
-
|
|
9
|
-
for (const [fieldName, config] of Object.entries(fields)) {
|
|
10
|
-
if (!config.computed) continue;
|
|
11
|
-
result[fieldName] = config.computed.compute(result);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return result;
|
|
15
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { ModelOps, QueryState, QueryResult, QueryResultWithMeta } from '../model-api/types.js';
|
|
2
|
-
import type { RequestContext } from '../auth/types.js';
|
|
3
|
-
import type { DataAdapter, AdapterCapability } from '../plugins/types.js';
|
|
4
|
-
import type { ExternalFieldConfig } from './types.js';
|
|
5
|
-
import { ExternalQueryExecutor } from './query-executor.js';
|
|
6
|
-
import { ExternalMutationExecutor, CapabilityNotSupportedError } from './mutation-executor.js';
|
|
7
|
-
|
|
8
|
-
export { CapabilityNotSupportedError };
|
|
9
|
-
|
|
10
|
-
export interface ExternalModelOpsConfig {
|
|
11
|
-
adapter: DataAdapter;
|
|
12
|
-
adapterName: string;
|
|
13
|
-
modelName: string;
|
|
14
|
-
fields: Record<string, ExternalFieldConfig>;
|
|
15
|
-
capabilities: AdapterCapability[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class ExternalModelOps implements ModelOps {
|
|
19
|
-
private readonly queryExecutor: ExternalQueryExecutor;
|
|
20
|
-
private readonly mutationExecutor: ExternalMutationExecutor;
|
|
21
|
-
|
|
22
|
-
constructor(config: ExternalModelOpsConfig) {
|
|
23
|
-
this.queryExecutor = new ExternalQueryExecutor({
|
|
24
|
-
adapter: config.adapter,
|
|
25
|
-
modelName: config.modelName,
|
|
26
|
-
fields: config.fields,
|
|
27
|
-
capabilities: config.capabilities,
|
|
28
|
-
});
|
|
29
|
-
this.mutationExecutor = new ExternalMutationExecutor({
|
|
30
|
-
adapter: config.adapter,
|
|
31
|
-
adapterName: config.adapterName,
|
|
32
|
-
modelName: config.modelName,
|
|
33
|
-
fields: config.fields,
|
|
34
|
-
capabilities: config.capabilities,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async find(state: QueryState): Promise<QueryResult> {
|
|
39
|
-
const result = await this.queryExecutor.execList({
|
|
40
|
-
filters: state.filters,
|
|
41
|
-
sorts: state.sorts,
|
|
42
|
-
fieldNames: state.fieldNames,
|
|
43
|
-
limitVal: state.limitVal,
|
|
44
|
-
offsetVal: state.offsetVal,
|
|
45
|
-
pageVal: state.pageVal,
|
|
46
|
-
});
|
|
47
|
-
return { data: result.data, total: result.total, hasMore: result.hasMore };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async findWithMeta(state: QueryState): Promise<QueryResultWithMeta> {
|
|
51
|
-
const limit = state.limitVal ?? 25;
|
|
52
|
-
const page = state.pageVal ?? 1;
|
|
53
|
-
const result = await this.queryExecutor.execList({
|
|
54
|
-
filters: state.filters,
|
|
55
|
-
sorts: state.sorts,
|
|
56
|
-
fieldNames: state.fieldNames,
|
|
57
|
-
limitVal: limit,
|
|
58
|
-
pageVal: page,
|
|
59
|
-
});
|
|
60
|
-
const total = result.total ?? result.data.length;
|
|
61
|
-
const totalPages = Math.ceil(total / limit);
|
|
62
|
-
return { data: result.data, meta: { total, page, limit, totalPages } };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async findOne(state: QueryState): Promise<Record<string, unknown> | null> {
|
|
66
|
-
const result = await this.queryExecutor.execList({
|
|
67
|
-
filters: state.filters,
|
|
68
|
-
sorts: state.sorts,
|
|
69
|
-
fieldNames: state.fieldNames,
|
|
70
|
-
limitVal: 1,
|
|
71
|
-
});
|
|
72
|
-
return result.data[0] ?? null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async count(state: QueryState): Promise<number> {
|
|
76
|
-
return this.queryExecutor.execCount({
|
|
77
|
-
filters: state.filters,
|
|
78
|
-
sorts: state.sorts,
|
|
79
|
-
fieldNames: state.fieldNames,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async get(id: string): Promise<Record<string, unknown> | null> {
|
|
84
|
-
return this.queryExecutor.execGet(id);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async create(
|
|
88
|
-
data: Record<string, unknown>,
|
|
89
|
-
_auth?: RequestContext,
|
|
90
|
-
): Promise<Record<string, unknown>> {
|
|
91
|
-
return this.mutationExecutor.create(data);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async update(
|
|
95
|
-
id: string,
|
|
96
|
-
data: Record<string, unknown>,
|
|
97
|
-
_auth?: RequestContext,
|
|
98
|
-
): Promise<Record<string, unknown>> {
|
|
99
|
-
return this.mutationExecutor.update(id, data);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async delete(id: string, _auth?: RequestContext): Promise<Record<string, unknown>> {
|
|
103
|
-
const record = await this.get(id);
|
|
104
|
-
if (!record) throw new Error(`Record not found: ${id}`);
|
|
105
|
-
await this.mutationExecutor.delete(id);
|
|
106
|
-
return record;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { ExternalFieldConfig } from './types.js';
|
|
2
|
-
|
|
3
|
-
export function resolveFieldValue(record: Record<string, unknown>, path: string): unknown {
|
|
4
|
-
const parts = path.split('.');
|
|
5
|
-
let current: unknown = record;
|
|
6
|
-
|
|
7
|
-
for (const part of parts) {
|
|
8
|
-
if (current == null || typeof current !== 'object') return undefined;
|
|
9
|
-
current = (current as Record<string, unknown>)[part];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return current;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function mapAdapterResponse(
|
|
16
|
-
raw: Record<string, unknown>,
|
|
17
|
-
fields: Record<string, ExternalFieldConfig>,
|
|
18
|
-
): Record<string, unknown> {
|
|
19
|
-
const result: Record<string, unknown> = {};
|
|
20
|
-
|
|
21
|
-
for (const [fieldName, config] of Object.entries(fields)) {
|
|
22
|
-
if (config.computed) continue;
|
|
23
|
-
|
|
24
|
-
const sourcePath = config.from ?? fieldName;
|
|
25
|
-
result[fieldName] = resolveFieldValue(raw, sourcePath);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function reverseMapForWrite(
|
|
32
|
-
data: Record<string, unknown>,
|
|
33
|
-
fields: Record<string, ExternalFieldConfig>,
|
|
34
|
-
): Record<string, unknown> {
|
|
35
|
-
const result: Record<string, unknown> = {};
|
|
36
|
-
|
|
37
|
-
for (const [fieldName, value] of Object.entries(data)) {
|
|
38
|
-
const config = fields[fieldName];
|
|
39
|
-
if (!config || config.computed) continue;
|
|
40
|
-
|
|
41
|
-
const targetPath = config.from ?? fieldName;
|
|
42
|
-
|
|
43
|
-
if (targetPath.includes('.')) {
|
|
44
|
-
setNestedValue(result, targetPath, value);
|
|
45
|
-
} else {
|
|
46
|
-
result[targetPath] = value;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
54
|
-
const parts = path.split('.');
|
|
55
|
-
let current: Record<string, unknown> = obj;
|
|
56
|
-
|
|
57
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
58
|
-
const part = parts[i];
|
|
59
|
-
if (current[part] == null || typeof current[part] !== 'object') {
|
|
60
|
-
current[part] = {};
|
|
61
|
-
}
|
|
62
|
-
current = current[part] as Record<string, unknown>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
current[parts[parts.length - 1]] = value;
|
|
66
|
-
}
|