@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.
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 -25
  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,291 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { QueryParser, QueryValidationError } from '../query-parser.js';
3
- import type { ResolvedField } from '../../schema/types.js';
4
-
5
- function makeFields(): ResolvedField[] {
6
- return [
7
- { name: 'id', config: { type: 'string' }, provenance: { source: 'base' } },
8
- { name: 'status', config: { type: 'string' }, provenance: { source: 'base' } },
9
- { name: 'name', config: { type: 'string' }, provenance: { source: 'base' } },
10
- {
11
- name: 'grand_total',
12
- config: { type: 'decimal', precision: 10, scale: 2 },
13
- provenance: { source: 'base' },
14
- },
15
- { name: 'quantity', config: { type: 'int' }, provenance: { source: 'base' } },
16
- { name: 'created_at', config: { type: 'datetime' }, provenance: { source: 'base' } },
17
- { name: 'is_active', config: { type: 'boolean' }, provenance: { source: 'base' } },
18
- { name: 'deleted_at', config: { type: 'datetime' }, provenance: { source: 'base' } },
19
- { name: 'description', config: { type: 'text' }, provenance: { source: 'base' } },
20
- ];
21
- }
22
-
23
- const relations = ['customer', 'items'];
24
-
25
- describe('QueryParser', () => {
26
- describe('parseFilters', () => {
27
- it('parses equality filter', () => {
28
- const parser = new QueryParser(makeFields(), relations);
29
- const result = parser.parseFilters({ filter: { status: { eq: 'posted' } } });
30
- expect(result).toEqual([{ field: 'status', operator: 'eq', value: 'posted' }]);
31
- });
32
-
33
- it('parses gt filter on numeric field', () => {
34
- const parser = new QueryParser(makeFields(), relations);
35
- const result = parser.parseFilters({ filter: { grand_total: { gt: '1000' } } });
36
- expect(result).toEqual([{ field: 'grand_total', operator: 'gt', value: 1000 }]);
37
- });
38
-
39
- it('parses in filter with multiple values', () => {
40
- const parser = new QueryParser(makeFields(), relations);
41
- const result = parser.parseFilters({ filter: { status: { in: 'draft,posted' } } });
42
- expect(result).toEqual([{ field: 'status', operator: 'in', value: ['draft', 'posted'] }]);
43
- });
44
-
45
- it('parses like filter', () => {
46
- const parser = new QueryParser(makeFields(), relations);
47
- const result = parser.parseFilters({ filter: { name: { like: 'acme' } } });
48
- expect(result).toEqual([{ field: 'name', operator: 'like', value: 'acme' }]);
49
- });
50
-
51
- it('parses isnull filter', () => {
52
- const parser = new QueryParser(makeFields(), relations);
53
- const result = parser.parseFilters({ filter: { deleted_at: { isnull: 'true' } } });
54
- expect(result).toEqual([{ field: 'deleted_at', operator: 'isnull', value: true }]);
55
- });
56
-
57
- it('parses multiple filters', () => {
58
- const parser = new QueryParser(makeFields(), relations);
59
- const result = parser.parseFilters({
60
- filter: { status: { eq: 'posted' }, grand_total: { gte: '500' } },
61
- });
62
- expect(result).toHaveLength(2);
63
- });
64
-
65
- it('rejects unknown field', () => {
66
- const parser = new QueryParser(makeFields(), relations);
67
- expect(() => parser.parseFilters({ filter: { nonexistent: { eq: 'x' } } })).toThrow(
68
- QueryValidationError,
69
- );
70
- });
71
-
72
- it('rejects unknown operator', () => {
73
- const parser = new QueryParser(makeFields(), relations);
74
- expect(() => parser.parseFilters({ filter: { status: { badop: 'x' } } })).toThrow(
75
- QueryValidationError,
76
- );
77
- });
78
-
79
- it('rejects gt operator on string field', () => {
80
- const parser = new QueryParser(makeFields(), relations);
81
- expect(() => parser.parseFilters({ filter: { status: { gt: '5' } } })).toThrow(
82
- QueryValidationError,
83
- );
84
- });
85
-
86
- it('rejects like operator on numeric field', () => {
87
- const parser = new QueryParser(makeFields(), relations);
88
- expect(() => parser.parseFilters({ filter: { grand_total: { like: 'abc' } } })).toThrow(
89
- QueryValidationError,
90
- );
91
- });
92
-
93
- it('allows gt on date field', () => {
94
- const parser = new QueryParser(makeFields(), relations);
95
- const result = parser.parseFilters({ filter: { created_at: { gt: '2024-01-01' } } });
96
- expect(result).toEqual([{ field: 'created_at', operator: 'gt', value: '2024-01-01' }]);
97
- });
98
-
99
- it('returns empty for missing filter param', () => {
100
- const parser = new QueryParser(makeFields(), relations);
101
- expect(parser.parseFilters({})).toEqual([]);
102
- });
103
- });
104
-
105
- describe('parseSort', () => {
106
- it('parses ascending sort', () => {
107
- const parser = new QueryParser(makeFields(), relations);
108
- const result = parser.parseSort({ sort: 'created_at' });
109
- expect(result).toEqual([{ field: 'created_at', direction: 'asc' }]);
110
- });
111
-
112
- it('parses descending sort', () => {
113
- const parser = new QueryParser(makeFields(), relations);
114
- const result = parser.parseSort({ sort: '-grand_total' });
115
- expect(result).toEqual([{ field: 'grand_total', direction: 'desc' }]);
116
- });
117
-
118
- it('parses multiple sort fields', () => {
119
- const parser = new QueryParser(makeFields(), relations);
120
- const result = parser.parseSort({ sort: 'status,-created_at' });
121
- expect(result).toEqual([
122
- { field: 'status', direction: 'asc' },
123
- { field: 'created_at', direction: 'desc' },
124
- ]);
125
- });
126
-
127
- it('rejects unknown sort field', () => {
128
- const parser = new QueryParser(makeFields(), relations);
129
- expect(() => parser.parseSort({ sort: 'nonexistent' })).toThrow(QueryValidationError);
130
- });
131
-
132
- it('returns empty for missing sort param', () => {
133
- const parser = new QueryParser(makeFields(), relations);
134
- expect(parser.parseSort({})).toEqual([]);
135
- });
136
- });
137
-
138
- describe('parsePagination', () => {
139
- it('returns defaults when no params', () => {
140
- const parser = new QueryParser(makeFields(), relations);
141
- const result = parser.parsePagination({});
142
- expect(result).toEqual({ page: 1, limit: 25, offset: 0 });
143
- });
144
-
145
- it('parses explicit page and limit', () => {
146
- const parser = new QueryParser(makeFields(), relations);
147
- const result = parser.parsePagination({ page: '2', limit: '10' });
148
- expect(result).toEqual({ page: 2, limit: 10, offset: 10 });
149
- });
150
-
151
- it('caps limit at 100', () => {
152
- const parser = new QueryParser(makeFields(), relations);
153
- const result = parser.parsePagination({ limit: '500' });
154
- expect(result.limit).toBe(100);
155
- });
156
-
157
- it('floors page at 1', () => {
158
- const parser = new QueryParser(makeFields(), relations);
159
- const result = parser.parsePagination({ page: '-1' });
160
- expect(result.page).toBe(1);
161
- });
162
-
163
- it('floors limit at 1', () => {
164
- const parser = new QueryParser(makeFields(), relations);
165
- const result = parser.parsePagination({ limit: '0' });
166
- expect(result.limit).toBe(1);
167
- });
168
- });
169
-
170
- describe('parseIncludes', () => {
171
- it('parses single include', () => {
172
- const parser = new QueryParser(makeFields(), relations);
173
- const result = parser.parseIncludes({ include: 'customer' });
174
- expect(result).toEqual([{ relation: 'customer' }]);
175
- });
176
-
177
- it('parses multiple includes', () => {
178
- const parser = new QueryParser(makeFields(), relations);
179
- const result = parser.parseIncludes({ include: 'customer,items' });
180
- expect(result).toEqual([{ relation: 'customer' }, { relation: 'items' }]);
181
- });
182
-
183
- it('parses nested include', () => {
184
- const parser = new QueryParser(makeFields(), ['items']);
185
- const result = parser.parseIncludes({ include: 'items.tax_type' });
186
- expect(result).toEqual([{ relation: 'items', nested: [{ relation: 'tax_type' }] }]);
187
- });
188
-
189
- it('rejects depth > 2', () => {
190
- const parser = new QueryParser(makeFields(), ['items']);
191
- expect(() => parser.parseIncludes({ include: 'items.tax_type.category' })).toThrow(
192
- QueryValidationError,
193
- );
194
- });
195
-
196
- it('rejects unknown relation', () => {
197
- const parser = new QueryParser(makeFields(), relations);
198
- expect(() => parser.parseIncludes({ include: 'nonexistent' })).toThrow(QueryValidationError);
199
- });
200
-
201
- it('returns empty for missing include param', () => {
202
- const parser = new QueryParser(makeFields(), relations);
203
- expect(parser.parseIncludes({})).toEqual([]);
204
- });
205
- });
206
-
207
- describe('parseFields', () => {
208
- it('parses field list', () => {
209
- const parser = new QueryParser(makeFields(), relations);
210
- const result = parser.parseFields({ fields: 'id,status,grand_total' });
211
- expect(result).toEqual(['id', 'status', 'grand_total']);
212
- });
213
-
214
- it('always includes id', () => {
215
- const parser = new QueryParser(makeFields(), relations);
216
- const result = parser.parseFields({ fields: 'status' });
217
- expect(result).toContain('id');
218
- expect(result).toContain('status');
219
- });
220
-
221
- it('rejects unknown field', () => {
222
- const parser = new QueryParser(makeFields(), relations);
223
- expect(() => parser.parseFields({ fields: 'id,nonexistent' })).toThrow(QueryValidationError);
224
- });
225
-
226
- it('returns empty for missing fields param', () => {
227
- const parser = new QueryParser(makeFields(), relations);
228
- expect(parser.parseFields({})).toEqual([]);
229
- });
230
- });
231
-
232
- describe('parse (full)', () => {
233
- it('parses combined query', () => {
234
- const parser = new QueryParser(makeFields(), relations);
235
- const result = parser.parse({
236
- filter: { status: { eq: 'draft' } },
237
- sort: '-created_at',
238
- page: '2',
239
- limit: '10',
240
- include: 'customer',
241
- fields: 'id,status',
242
- });
243
- expect(result.filters).toHaveLength(1);
244
- expect(result.sort).toHaveLength(1);
245
- expect(result.pagination).toEqual({ page: 2, limit: 10, offset: 10 });
246
- expect(result.includes).toHaveLength(1);
247
- expect(result.fields).toContain('status');
248
- });
249
- });
250
-
251
- describe('parseSearch', () => {
252
- it('returns trimmed string for valid search', () => {
253
- const parser = new QueryParser(makeFields(), relations);
254
- const result = parser.parseSearch({ search: ' hello ' });
255
- expect(result).toBe('hello');
256
- });
257
-
258
- it('returns undefined for empty string', () => {
259
- const parser = new QueryParser(makeFields(), relations);
260
- expect(parser.parseSearch({ search: '' })).toBeUndefined();
261
- });
262
-
263
- it('returns undefined for whitespace-only string', () => {
264
- const parser = new QueryParser(makeFields(), relations);
265
- expect(parser.parseSearch({ search: ' ' })).toBeUndefined();
266
- });
267
-
268
- it('returns undefined when search param is missing', () => {
269
- const parser = new QueryParser(makeFields(), relations);
270
- expect(parser.parseSearch({})).toBeUndefined();
271
- });
272
-
273
- it('returns undefined for non-string values', () => {
274
- const parser = new QueryParser(makeFields(), relations);
275
- expect(parser.parseSearch({ search: 123 })).toBeUndefined();
276
- expect(parser.parseSearch({ search: null })).toBeUndefined();
277
- });
278
-
279
- it('is included in parse() result', () => {
280
- const parser = new QueryParser(makeFields(), relations);
281
- const result = parser.parse({ search: 'keyword' });
282
- expect(result.search).toBe('keyword');
283
- });
284
-
285
- it('parse() returns undefined search when not provided', () => {
286
- const parser = new QueryParser(makeFields(), relations);
287
- const result = parser.parse({});
288
- expect(result.search).toBeUndefined();
289
- });
290
- });
291
- });
@@ -1,137 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createServer } from '../server.js';
3
- import { generateRoutes } from '../route-generator.js';
4
- import type { SchemaRegistry } from '../../schema/registry.js';
5
- import type { DatabaseClient } from '../../db/client.js';
6
- import type { ResolvedModel } from '../../schema/types.js';
7
-
8
- function makeModel(module: string, name: string): ResolvedModel {
9
- return {
10
- qualifiedName: `${module}.${name}`,
11
- app: 'test',
12
- module,
13
- name,
14
- auditLog: false,
15
- traits: [],
16
- fields: [{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } }],
17
- indexes: [],
18
- };
19
- }
20
-
21
- function makeRegistry(models: ResolvedModel[]): SchemaRegistry {
22
- return {
23
- getModelsByModule: () => {
24
- const map = new Map<string, ResolvedModel[]>();
25
- for (const m of models) {
26
- const list = map.get(m.module) ?? [];
27
- list.push(m);
28
- map.set(m.module, list);
29
- }
30
- return map;
31
- },
32
- getRelationshipsForModel: () => [],
33
- getModel: (name: string) => models.find((m) => m.qualifiedName === name) ?? null,
34
- } as unknown as SchemaRegistry;
35
- }
36
-
37
- function makeMockDb(): DatabaseClient {
38
- const buildQuery = () => {
39
- const q: any = {};
40
- q.select = () => q;
41
- q.selectAll = () => q;
42
- q.where = () => q;
43
- q.orderBy = () => q;
44
- q.offset = () => q;
45
- q.limit = () => q;
46
- q.execute = async () => [];
47
- q.executeTakeFirst = async () => ({ count: '0' });
48
- q.executeTakeFirstOrThrow = async () => ({});
49
- q.values = () => q;
50
- q.set = () => q;
51
- q.returningAll = () => q;
52
- return q;
53
- };
54
-
55
- return {
56
- selectFrom: () => buildQuery(),
57
- insertInto: () => buildQuery(),
58
- updateTable: () => buildQuery(),
59
- deleteFrom: () => buildQuery(),
60
- kysely: { fn: { countAll: () => ({ as: () => 'count' }) } },
61
- } as unknown as DatabaseClient;
62
- }
63
-
64
- describe('generateRoutes', () => {
65
- it('registers all five CRUD routes per model', async () => {
66
- const models = [makeModel('sales', 'invoice')];
67
- const server = await createServer();
68
- generateRoutes(server, makeRegistry(models), makeMockDb());
69
-
70
- const list = await server.inject({ method: 'GET', url: '/api/sales/invoice' });
71
- expect(list.statusCode).toBe(200);
72
- const get = await server.inject({ method: 'GET', url: '/api/sales/invoice/1' });
73
- expect([200, 404]).toContain(get.statusCode);
74
- const post = await server.inject({
75
- method: 'POST',
76
- url: '/api/sales/invoice',
77
- headers: { 'content-type': 'application/json' },
78
- payload: '{}',
79
- });
80
- expect([201, 400]).toContain(post.statusCode);
81
- const put = await server.inject({
82
- method: 'PUT',
83
- url: '/api/sales/invoice/1',
84
- headers: { 'content-type': 'application/json' },
85
- payload: '{}',
86
- });
87
- expect([200, 404]).toContain(put.statusCode);
88
- const del = await server.inject({ method: 'DELETE', url: '/api/sales/invoice/1' });
89
- expect([204, 404]).toContain(del.statusCode);
90
- });
91
-
92
- it('registers routes for multiple models across modules', async () => {
93
- const models = [
94
- makeModel('sales', 'invoice'),
95
- makeModel('sales', 'customer'),
96
- makeModel('accounting', 'journal_entry'),
97
- ];
98
- const server = await createServer();
99
- generateRoutes(server, makeRegistry(models), makeMockDb());
100
-
101
- const r1 = await server.inject({ method: 'GET', url: '/api/sales/invoice' });
102
- expect(r1.statusCode).toBe(200);
103
- const r2 = await server.inject({ method: 'GET', url: '/api/sales/customer' });
104
- expect(r2.statusCode).toBe(200);
105
- const r3 = await server.inject({ method: 'GET', url: '/api/accounting/journal_entry' });
106
- expect(r3.statusCode).toBe(200);
107
- });
108
-
109
- it('uses correct URL pattern from qualified name', async () => {
110
- const models = [makeModel('hr', 'employee')];
111
- const server = await createServer();
112
- generateRoutes(server, makeRegistry(models), makeMockDb());
113
-
114
- const listRes = await server.inject({ method: 'GET', url: '/api/hr/employee' });
115
- expect(listRes.statusCode).toBe(200);
116
-
117
- const getRes = await server.inject({ method: 'GET', url: '/api/hr/employee/123' });
118
- expect([200, 404]).toContain(getRes.statusCode);
119
- });
120
-
121
- it('handles multi-word model names', async () => {
122
- const models = [makeModel('sales', 'invoice_item')];
123
- const server = await createServer();
124
- generateRoutes(server, makeRegistry(models), makeMockDb());
125
-
126
- const res = await server.inject({ method: 'GET', url: '/api/sales/invoice_item' });
127
- expect(res.statusCode).toBe(200);
128
- });
129
-
130
- it('registers no routes when registry is empty', async () => {
131
- const server = await createServer();
132
- generateRoutes(server, makeRegistry([]), makeMockDb());
133
-
134
- const res = await server.inject({ method: 'GET', url: '/api/anything' });
135
- expect(res.statusCode).toBe(404);
136
- });
137
- });
@@ -1,73 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createServer } from '../server.js';
3
-
4
- describe('createServer', () => {
5
- it('returns a Fastify instance with defaults', async () => {
6
- const server = await createServer();
7
- expect(server).toBeDefined();
8
- expect(server.hasRoute).toBeDefined();
9
- });
10
-
11
- it('applies custom options', async () => {
12
- const server = await createServer({ port: 4000, host: '0.0.0.0', logger: false });
13
- expect(server).toBeDefined();
14
- });
15
-
16
- it('parses JSON bodies', async () => {
17
- const server = await createServer();
18
- server.post('/test', async (req) => req.body);
19
-
20
- const res = await server.inject({
21
- method: 'POST',
22
- url: '/test',
23
- headers: { 'content-type': 'application/json' },
24
- payload: JSON.stringify({ hello: 'world' }),
25
- });
26
-
27
- expect(res.statusCode).toBe(200);
28
- expect(JSON.parse(res.body)).toEqual({ hello: 'world' });
29
- });
30
-
31
- it('generates request IDs', async () => {
32
- const server = await createServer();
33
- server.get('/test', async (req) => ({ id: req.id }));
34
-
35
- const res = await server.inject({ method: 'GET', url: '/test' });
36
- const body = JSON.parse(res.body);
37
- expect(body.id).toBeDefined();
38
- expect(body.id).toMatch(/^[0-9a-f-]{36}$/);
39
- });
40
-
41
- it('returns structured error response', async () => {
42
- const server = await createServer();
43
- server.get('/fail', async () => {
44
- const err = new Error('Something broke') as any;
45
- err.statusCode = 422;
46
- err.code = 'UNPROCESSABLE';
47
- throw err;
48
- });
49
-
50
- const res = await server.inject({ method: 'GET', url: '/fail' });
51
- expect(res.statusCode).toBe(422);
52
- const body = JSON.parse(res.body);
53
- expect(body.error.code).toBe('UNPROCESSABLE');
54
- expect(body.error.message).toBe('Something broke');
55
- });
56
-
57
- it('defaults to 500 for unhandled errors', async () => {
58
- const server = await createServer();
59
- server.get('/crash', async () => {
60
- throw new Error('Unexpected');
61
- });
62
-
63
- const res = await server.inject({ method: 'GET', url: '/crash' });
64
- expect(res.statusCode).toBe(500);
65
- const body = JSON.parse(res.body);
66
- expect(body.error.code).toBe('INTERNAL_ERROR');
67
- });
68
-
69
- it('uses custom requestIdHeader', async () => {
70
- const server = await createServer({ requestIdHeader: 'x-trace-id' });
71
- expect(server).toBeDefined();
72
- });
73
- });
@@ -1,166 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createServer } from '../server.js';
3
- import { generateRoutes } from '../route-generator.js';
4
- import type { SchemaRegistry } from '../../schema/registry.js';
5
- import type { DatabaseClient } from '../../db/client.js';
6
- import type { ResolvedModel } from '../../schema/types.js';
7
-
8
- function makeModel(): ResolvedModel {
9
- return {
10
- qualifiedName: 'sales.customer',
11
- app: 'test',
12
- module: 'sales',
13
- name: 'customer',
14
- label: 'Customer',
15
- auditLog: false,
16
- traits: [],
17
- fields: [
18
- {
19
- name: 'name',
20
- config: { type: 'string', required: true, label: 'Customer Name' },
21
- provenance: { source: 'base' },
22
- },
23
- {
24
- name: 'email',
25
- config: { type: 'string', required: true, label: 'Email' },
26
- provenance: { source: 'base' },
27
- },
28
- {
29
- name: 'status',
30
- config: { type: 'enum', options: ['Active', 'Inactive'], label: 'Status' },
31
- provenance: { source: 'base' },
32
- },
33
- {
34
- name: 'balance',
35
- config: { type: 'decimal', label: 'Balance' },
36
- provenance: { source: 'base' },
37
- },
38
- ],
39
- indexes: [],
40
- };
41
- }
42
-
43
- function makeRegistry(): SchemaRegistry {
44
- const model = makeModel();
45
- return {
46
- getModelsByModule: () => new Map([['sales', [model]]]),
47
- getRelationshipsForModel: () => [],
48
- } as unknown as SchemaRegistry;
49
- }
50
-
51
- function makeMockDb(): DatabaseClient {
52
- const buildQuery = () => {
53
- const q: any = {};
54
- q.select = () => q;
55
- q.selectAll = () => q;
56
- q.where = () => q;
57
- q.orderBy = () => q;
58
- q.offset = () => q;
59
- q.limit = () => q;
60
- q.execute = async () => [];
61
- q.executeTakeFirst = async () => ({ count: '0' });
62
- q.executeTakeFirstOrThrow = async () => ({});
63
- q.values = () => q;
64
- q.set = () => q;
65
- q.returningAll = () => q;
66
- return q;
67
- };
68
-
69
- return {
70
- selectFrom: () => buildQuery(),
71
- insertInto: () => buildQuery(),
72
- updateTable: () => buildQuery(),
73
- deleteFrom: () => buildQuery(),
74
- kysely: { fn: { countAll: () => ({ as: () => 'count' }) } },
75
- } as unknown as DatabaseClient;
76
- }
77
-
78
- describe('Swagger API Docs', () => {
79
- it('registers swagger and /api/docs/json returns valid OpenAPI document', async () => {
80
- const server = await createServer();
81
- generateRoutes(server, makeRegistry(), makeMockDb());
82
- await server.ready();
83
-
84
- const res = await server.inject({ method: 'GET', url: '/api/docs/json' });
85
- expect(res.statusCode).toBe(200);
86
- const spec = JSON.parse(res.body);
87
- expect(spec.openapi).toBe('3.1.0');
88
- expect(spec.info.title).toBe('Rangka API');
89
- expect(spec.paths).toBeDefined();
90
- });
91
-
92
- it('includes model fields in component schemas via route body', async () => {
93
- const server = await createServer();
94
- generateRoutes(server, makeRegistry(), makeMockDb());
95
- await server.ready();
96
-
97
- const res = await server.inject({ method: 'GET', url: '/api/docs/json' });
98
- const spec = JSON.parse(res.body);
99
-
100
- const postPath = spec.paths['/api/sales/customer'];
101
- expect(postPath).toBeDefined();
102
- expect(postPath.post).toBeDefined();
103
-
104
- const bodySchema = postPath.post.requestBody.content['application/json'].schema;
105
- expect(bodySchema.properties.name).toEqual({ type: 'string', description: 'Customer Name' });
106
- expect(bodySchema.properties.email).toEqual({ type: 'string', description: 'Email' });
107
- expect(bodySchema.properties.status).toEqual({
108
- type: 'string',
109
- enum: ['Active', 'Inactive'],
110
- description: 'Status',
111
- });
112
- expect(bodySchema.properties.balance).toEqual({ type: 'number', description: 'Balance' });
113
- });
114
-
115
- it('documents query parameters on list endpoints', async () => {
116
- const server = await createServer();
117
- generateRoutes(server, makeRegistry(), makeMockDb());
118
- await server.ready();
119
-
120
- const res = await server.inject({ method: 'GET', url: '/api/docs/json' });
121
- const spec = JSON.parse(res.body);
122
-
123
- const listPath = spec.paths['/api/sales/customer'];
124
- expect(listPath.get).toBeDefined();
125
- const params = listPath.get.parameters;
126
- const paramNames = params.map((p: any) => p.name);
127
- expect(paramNames).toContain('page');
128
- expect(paramNames).toContain('limit');
129
- expect(paramNames).toContain('sort');
130
- expect(paramNames).toContain('fields');
131
- expect(paramNames).toContain('include');
132
- });
133
-
134
- it('does not register docs when docs: false', async () => {
135
- const server = await createServer({ docs: false });
136
- generateRoutes(server, makeRegistry(), makeMockDb());
137
- await server.ready();
138
-
139
- const res = await server.inject({ method: 'GET', url: '/api/docs/json' });
140
- expect(res.statusCode).toBe(404);
141
- });
142
-
143
- it('serves Swagger UI HTML at /api/docs', async () => {
144
- const server = await createServer();
145
- generateRoutes(server, makeRegistry(), makeMockDb());
146
- await server.ready();
147
-
148
- const res = await server.inject({ method: 'GET', url: '/api/docs' });
149
- expect(res.statusCode).toBe(200);
150
- expect(res.headers['content-type']).toContain('text/html');
151
- expect(res.body).toContain('swagger');
152
- });
153
-
154
- it('groups endpoints by module tag', async () => {
155
- const server = await createServer({ tags: [{ name: 'sales' }] });
156
- generateRoutes(server, makeRegistry(), makeMockDb());
157
- await server.ready();
158
-
159
- const res = await server.inject({ method: 'GET', url: '/api/docs/json' });
160
- const spec = JSON.parse(res.body);
161
-
162
- const listPath = spec.paths['/api/sales/customer'];
163
- expect(listPath.get.tags).toContain('sales');
164
- expect(listPath.post.tags).toContain('sales');
165
- });
166
- });