@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.
Files changed (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -18
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. 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,5 +0,0 @@
1
- import type { ExternalModelConfig } from './types.js';
2
-
3
- export function defineExternalModel(config: ExternalModelConfig): ExternalModelConfig {
4
- return config;
5
- }
@@ -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
- }