@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,297 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ScopeRegistry, ScopeResolutionError } from '../scope-registry.js';
3
- import { SchemaRegistry } from '../../schema/registry.js';
4
- import type { ResolvedModel } from '../../schema/types.js';
5
- import type { ModuleConfig } from '@rangka/shared';
6
-
7
- function makeModel(overrides: Partial<ResolvedModel> & { qualifiedName: string }): ResolvedModel {
8
- return {
9
- app: overrides.module ?? 'test',
10
- module: overrides.module ?? 'test',
11
- name: overrides.qualifiedName.split('.')[1],
12
- auditLog: false,
13
- traits: [],
14
- fields: [],
15
- indexes: [],
16
- ...overrides,
17
- };
18
- }
19
-
20
- function linkField(name: string, model: string) {
21
- return {
22
- name,
23
- config: { type: 'link' as const, model },
24
- provenance: { source: 'base' as const },
25
- };
26
- }
27
-
28
- describe('ScopeRegistry', () => {
29
- describe('scope registration', () => {
30
- it('registers scopes from modules', () => {
31
- const modules: ModuleConfig[] = [
32
- {
33
- name: 'core',
34
- label: 'Core',
35
- scopes: {
36
- company: { model: 'core.company', default: 'user.default_company', switchable: true },
37
- },
38
- },
39
- ];
40
- const schemaRegistry = new SchemaRegistry([
41
- makeModel({ qualifiedName: 'core.company', module: 'core', fields: [] }),
42
- ]);
43
-
44
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
45
-
46
- const scope = scopeRegistry.getScope('company');
47
- expect(scope).toBeDefined();
48
- expect(scope!.name).toBe('company');
49
- expect(scope!.definition.model).toBe('core.company');
50
- expect(scope!.module).toBe('core');
51
- });
52
-
53
- it('registers multiple scopes from different modules', () => {
54
- const modules: ModuleConfig[] = [
55
- {
56
- name: 'core',
57
- label: 'Core',
58
- scopes: {
59
- company: { model: 'core.company', default: 'user.default_company' },
60
- },
61
- },
62
- {
63
- name: 'cms',
64
- label: 'CMS',
65
- scopes: {
66
- workspace: { model: 'cms.workspace', default: 'user.default_workspace' },
67
- },
68
- },
69
- ];
70
- const schemaRegistry = new SchemaRegistry([
71
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
72
- makeModel({ qualifiedName: 'cms.workspace', module: 'cms' }),
73
- ]);
74
-
75
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
76
-
77
- expect(scopeRegistry.getAllScopes()).toHaveLength(2);
78
- expect(scopeRegistry.getScope('company')).toBeDefined();
79
- expect(scopeRegistry.getScope('workspace')).toBeDefined();
80
- });
81
-
82
- it('throws on duplicate scope name across modules', () => {
83
- const modules: ModuleConfig[] = [
84
- {
85
- name: 'core',
86
- label: 'Core',
87
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
88
- },
89
- {
90
- name: 'sales',
91
- label: 'Sales',
92
- scopes: { company: { model: 'sales.company', default: 'user.company' } },
93
- },
94
- ];
95
- const schemaRegistry = new SchemaRegistry([
96
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
97
- makeModel({ qualifiedName: 'sales.company', module: 'sales' }),
98
- ]);
99
-
100
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(ScopeResolutionError);
101
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(
102
- /Scope "company" declared by module "sales" conflicts/,
103
- );
104
- });
105
-
106
- it('handles modules without scopes', () => {
107
- const modules: ModuleConfig[] = [
108
- { name: 'core', label: 'Core' },
109
- { name: 'sales', label: 'Sales' },
110
- ];
111
- const schemaRegistry = new SchemaRegistry([]);
112
-
113
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
114
- expect(scopeRegistry.getAllScopes()).toHaveLength(0);
115
- });
116
- });
117
-
118
- describe('model binding resolution', () => {
119
- it('resolves scope column from link field targeting scope model', () => {
120
- const modules: ModuleConfig[] = [
121
- {
122
- name: 'core',
123
- label: 'Core',
124
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
125
- },
126
- ];
127
- const invoiceModel = makeModel({
128
- qualifiedName: 'sales.invoice',
129
- module: 'sales',
130
- scope: 'company',
131
- fields: [linkField('company', 'core.company'), linkField('customer', 'sales.customer')],
132
- });
133
- const schemaRegistry = new SchemaRegistry([
134
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
135
- makeModel({ qualifiedName: 'sales.customer', module: 'sales' }),
136
- invoiceModel,
137
- ]);
138
-
139
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
140
-
141
- const binding = scopeRegistry.getModelBinding('sales.invoice');
142
- expect(binding).toBeDefined();
143
- expect(binding!.scopeName).toBe('company');
144
- expect(binding!.column).toBe('company');
145
- expect(binding!.scopeModel).toBe('core.company');
146
- });
147
-
148
- it('returns undefined for models without scope', () => {
149
- const modules: ModuleConfig[] = [
150
- {
151
- name: 'core',
152
- label: 'Core',
153
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
154
- },
155
- ];
156
- const customerModel = makeModel({
157
- qualifiedName: 'sales.customer',
158
- module: 'sales',
159
- fields: [linkField('company', 'core.company')],
160
- });
161
- const schemaRegistry = new SchemaRegistry([
162
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
163
- customerModel,
164
- ]);
165
-
166
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
167
- expect(scopeRegistry.getModelBinding('sales.customer')).toBeUndefined();
168
- expect(scopeRegistry.isModelScoped('sales.customer')).toBe(false);
169
- });
170
-
171
- it('isModelScoped returns true for scoped models', () => {
172
- const modules: ModuleConfig[] = [
173
- {
174
- name: 'core',
175
- label: 'Core',
176
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
177
- },
178
- ];
179
- const invoiceModel = makeModel({
180
- qualifiedName: 'sales.invoice',
181
- module: 'sales',
182
- scope: 'company',
183
- fields: [linkField('company', 'core.company')],
184
- });
185
- const schemaRegistry = new SchemaRegistry([
186
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
187
- invoiceModel,
188
- ]);
189
-
190
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
191
- expect(scopeRegistry.isModelScoped('sales.invoice')).toBe(true);
192
- });
193
-
194
- it('throws when model references undefined scope', () => {
195
- const modules: ModuleConfig[] = [{ name: 'core', label: 'Core' }];
196
- const invoiceModel = makeModel({
197
- qualifiedName: 'sales.invoice',
198
- module: 'sales',
199
- scope: 'company',
200
- fields: [linkField('company', 'core.company')],
201
- });
202
- const schemaRegistry = new SchemaRegistry([
203
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
204
- invoiceModel,
205
- ]);
206
-
207
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(ScopeResolutionError);
208
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(
209
- /references scope "company" which is not defined/,
210
- );
211
- });
212
-
213
- it('throws when model has no link field to scope model', () => {
214
- const modules: ModuleConfig[] = [
215
- {
216
- name: 'core',
217
- label: 'Core',
218
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
219
- },
220
- ];
221
- const invoiceModel = makeModel({
222
- qualifiedName: 'sales.invoice',
223
- module: 'sales',
224
- scope: 'company',
225
- fields: [
226
- { name: 'total', config: { type: 'number' } as any, provenance: { source: 'base' } },
227
- ],
228
- });
229
- const schemaRegistry = new SchemaRegistry([
230
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
231
- invoiceModel,
232
- ]);
233
-
234
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(ScopeResolutionError);
235
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(
236
- /has no link field to "core.company"/,
237
- );
238
- });
239
-
240
- it('throws when model has multiple link fields to scope model without explicit field', () => {
241
- const modules: ModuleConfig[] = [
242
- {
243
- name: 'core',
244
- label: 'Core',
245
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
246
- },
247
- ];
248
- const transferModel = makeModel({
249
- qualifiedName: 'accounting.transfer',
250
- module: 'accounting',
251
- scope: 'company',
252
- fields: [
253
- linkField('source_company', 'core.company'),
254
- linkField('target_company', 'core.company'),
255
- ],
256
- });
257
- const schemaRegistry = new SchemaRegistry([
258
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
259
- transferModel,
260
- ]);
261
-
262
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(ScopeResolutionError);
263
- expect(() => new ScopeRegistry(modules, schemaRegistry)).toThrow(
264
- /multiple link fields.*source_company, target_company/,
265
- );
266
- });
267
-
268
- it('uses explicit field override when model has multiple links', () => {
269
- const modules: ModuleConfig[] = [
270
- {
271
- name: 'core',
272
- label: 'Core',
273
- scopes: { company: { model: 'core.company', default: 'user.default_company' } },
274
- },
275
- ];
276
- const transferModel = makeModel({
277
- qualifiedName: 'accounting.transfer',
278
- module: 'accounting',
279
- scope: { name: 'company', field: 'source_company' },
280
- fields: [
281
- linkField('source_company', 'core.company'),
282
- linkField('target_company', 'core.company'),
283
- ],
284
- });
285
- const schemaRegistry = new SchemaRegistry([
286
- makeModel({ qualifiedName: 'core.company', module: 'core' }),
287
- transferModel,
288
- ]);
289
-
290
- const scopeRegistry = new ScopeRegistry(modules, schemaRegistry);
291
- const binding = scopeRegistry.getModelBinding('accounting.transfer');
292
- expect(binding).toBeDefined();
293
- expect(binding!.column).toBe('source_company');
294
- expect(binding!.scopeName).toBe('company');
295
- });
296
- });
297
- });
@@ -1,66 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { applyScopeFiltersToQuery } from '../scopes.js';
3
-
4
- describe('scope system', () => {
5
- describe('applyScopeFiltersToQuery', () => {
6
- it('applies eq filters to query', () => {
7
- const wheres: Array<[string, string, unknown]> = [];
8
- const fakeQuery = {
9
- where(field: string, op: string, value: unknown) {
10
- wheres.push([field, op, value]);
11
- return fakeQuery;
12
- },
13
- };
14
-
15
- const result = applyScopeFiltersToQuery(fakeQuery, [
16
- { field: 'company', operator: 'eq', value: 'C-001' },
17
- ]);
18
-
19
- expect(wheres).toEqual([['company', '=', 'C-001']]);
20
- expect(result).toBe(fakeQuery);
21
- });
22
-
23
- it('applies in filters to query', () => {
24
- const wheres: Array<[string, string, unknown]> = [];
25
- const fakeQuery = {
26
- where(field: string, op: string, value: unknown) {
27
- wheres.push([field, op, value]);
28
- return fakeQuery;
29
- },
30
- };
31
-
32
- const result = applyScopeFiltersToQuery(fakeQuery, [
33
- { field: 'territory', operator: 'in', value: ['North', 'East'] },
34
- ]);
35
-
36
- expect(wheres).toEqual([['territory', 'in', ['North', 'East']]]);
37
- expect(result).toBe(fakeQuery);
38
- });
39
-
40
- it('applies multiple filters sequentially', () => {
41
- const wheres: Array<[string, string, unknown]> = [];
42
- const fakeQuery = {
43
- where(field: string, op: string, value: unknown) {
44
- wheres.push([field, op, value]);
45
- return fakeQuery;
46
- },
47
- };
48
-
49
- applyScopeFiltersToQuery(fakeQuery, [
50
- { field: 'company', operator: 'eq', value: 'C-001' },
51
- { field: 'territory', operator: 'in', value: ['North'] },
52
- ]);
53
-
54
- expect(wheres).toEqual([
55
- ['company', '=', 'C-001'],
56
- ['territory', 'in', ['North']],
57
- ]);
58
- });
59
-
60
- it('returns query unchanged when no filters', () => {
61
- const fakeQuery = { where: () => fakeQuery };
62
- const result = applyScopeFiltersToQuery(fakeQuery, []);
63
- expect(result).toBe(fakeQuery);
64
- });
65
- });
66
- });
@@ -1,214 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { PermissionRegistry } from '../permission-registry.js';
3
- import { createAuthHook, createSessionHandler, generateToken } from '../session.js';
4
- import { hashPassword } from '../password.js';
5
- import { createTestServer } from '../../__tests__/helpers.js';
6
-
7
- function createMockDb(data: { users: any[]; sessions: any[]; userRoles: any[]; roles: any[] }) {
8
- return {
9
- selectFrom(table: string) {
10
- return {
11
- selectAll() {
12
- return this;
13
- },
14
- select(..._args: any[]) {
15
- return this;
16
- },
17
- where(field: string, op: string, value: any) {
18
- const store =
19
- table === 'core.session'
20
- ? data.sessions
21
- : table === 'core.user'
22
- ? data.users
23
- : table === 'core.user_role'
24
- ? data.userRoles
25
- : data.roles;
26
-
27
- const filtered = store.filter((r: any) => {
28
- if (op === '=') return r[field] === value;
29
- if (op === 'in') return value.includes(r[field]);
30
- return false;
31
- });
32
- return {
33
- executeTakeFirst: async () => filtered[0] ?? undefined,
34
- execute: async () => filtered,
35
- where(f2: string, o2: string, v2: any) {
36
- const f = filtered.filter((r: any) => {
37
- if (o2 === '=') return r[f2] === v2;
38
- if (o2 === 'in') return v2.includes(r[f2]);
39
- return false;
40
- });
41
- return { executeTakeFirst: async () => f[0], execute: async () => f };
42
- },
43
- };
44
- },
45
- executeTakeFirst: async () => undefined,
46
- execute: async () => [],
47
- };
48
- },
49
- insertInto(_table: string) {
50
- return {
51
- values(vals: any) {
52
- return {
53
- returningAll() {
54
- return { executeTakeFirstOrThrow: async () => ({ id: 'new-id', ...vals }) };
55
- },
56
- execute: async () => {},
57
- };
58
- },
59
- };
60
- },
61
- deleteFrom(_table: string) {
62
- return {
63
- where() {
64
- return { execute: async () => {} };
65
- },
66
- };
67
- },
68
- } as any;
69
- }
70
-
71
- describe('session auth hook', () => {
72
- let permissionRegistry: PermissionRegistry;
73
-
74
- beforeEach(() => {
75
- permissionRegistry = new PermissionRegistry();
76
- permissionRegistry.registerRoles({ Admin: { label: 'Admin', models: {} } }, 'core');
77
- });
78
-
79
- it('returns 401 for missing Authorization header', async () => {
80
- const db = createMockDb({ users: [], sessions: [], userRoles: [], roles: [] });
81
- const server = createTestServer();
82
- server.get('/test', {
83
- onRequest: createAuthHook(db, permissionRegistry),
84
- handler: async () => ({ ok: true }),
85
- });
86
-
87
- const res = await server.inject({ method: 'GET', url: '/test' });
88
- expect(res.statusCode).toBe(401);
89
- expect(res.json().error.code).toBe('UNAUTHORIZED');
90
- });
91
-
92
- it('returns 401 for invalid token', async () => {
93
- const db = createMockDb({ users: [], sessions: [], userRoles: [], roles: [] });
94
- const server = createTestServer();
95
- server.get('/test', {
96
- onRequest: createAuthHook(db, permissionRegistry),
97
- handler: async () => ({ ok: true }),
98
- });
99
-
100
- const res = await server.inject({
101
- method: 'GET',
102
- url: '/test',
103
- headers: { authorization: 'Bearer invalid-token' },
104
- });
105
- expect(res.statusCode).toBe(401);
106
- });
107
-
108
- it('returns 401 for expired token', async () => {
109
- const expired = new Date(Date.now() - 1000).toISOString();
110
- const db = createMockDb({
111
- users: [{ id: 'u1', email: 'a@b.com', enabled: true }],
112
- sessions: [{ id: 's1', token: 'tok', user_id: 'u1', expires_at: expired }],
113
- userRoles: [],
114
- roles: [],
115
- });
116
- const server = createTestServer();
117
- server.get('/test', {
118
- onRequest: createAuthHook(db, permissionRegistry),
119
- handler: async () => ({ ok: true }),
120
- });
121
-
122
- const res = await server.inject({
123
- method: 'GET',
124
- url: '/test',
125
- headers: { authorization: 'Bearer tok' },
126
- });
127
- expect(res.statusCode).toBe(401);
128
- expect(res.json().error.message).toContain('expired');
129
- });
130
-
131
- it('authenticates valid token and attaches context', async () => {
132
- const future = new Date(Date.now() + 60000).toISOString();
133
- const db = createMockDb({
134
- users: [{ id: 'u1', email: 'a@b.com', full_name: 'Test', enabled: true, password_hash: 'x' }],
135
- sessions: [{ id: 's1', token: 'valid-tok', user_id: 'u1', expires_at: future }],
136
- userRoles: [{ user_id: 'u1', role_id: 'r1' }],
137
- roles: [{ id: 'r1', name: 'Admin' }],
138
- });
139
- const server = createTestServer();
140
- server.get('/test', {
141
- onRequest: createAuthHook(db, permissionRegistry),
142
- handler: async (request) => {
143
- const ctx = (request as any).authContext;
144
- return { email: ctx.user.email };
145
- },
146
- });
147
-
148
- const res = await server.inject({
149
- method: 'GET',
150
- url: '/test',
151
- headers: { authorization: 'Bearer valid-tok' },
152
- });
153
- expect(res.statusCode).toBe(200);
154
- expect(res.json().email).toBe('a@b.com');
155
- });
156
- });
157
-
158
- describe('session create endpoint', () => {
159
- it('returns 401 for invalid credentials', async () => {
160
- const db = createMockDb({ users: [], sessions: [], userRoles: [], roles: [] });
161
- const server = createTestServer();
162
- server.post('/api/core/session', createSessionHandler(db));
163
-
164
- const res = await server.inject({
165
- method: 'POST',
166
- url: '/api/core/session',
167
- payload: { email: 'bad@test.com', password: 'wrong' },
168
- });
169
- expect(res.statusCode).toBe(401);
170
- });
171
-
172
- it('returns 400 for missing fields', async () => {
173
- const db = createMockDb({ users: [], sessions: [], userRoles: [], roles: [] });
174
- const server = createTestServer();
175
- server.post('/api/core/session', createSessionHandler(db));
176
-
177
- const res = await server.inject({
178
- method: 'POST',
179
- url: '/api/core/session',
180
- payload: {},
181
- });
182
- expect(res.statusCode).toBe(400);
183
- });
184
-
185
- it('creates session for valid credentials', async () => {
186
- const pw = hashPassword('secret');
187
- const db = createMockDb({
188
- users: [
189
- { id: 'u1', email: 'user@test.com', full_name: 'User', enabled: true, password_hash: pw },
190
- ],
191
- sessions: [],
192
- userRoles: [],
193
- roles: [],
194
- });
195
- const server = createTestServer();
196
- server.post('/api/core/session', createSessionHandler(db));
197
-
198
- const res = await server.inject({
199
- method: 'POST',
200
- url: '/api/core/session',
201
- payload: { email: 'user@test.com', password: 'secret' },
202
- });
203
- expect(res.statusCode).toBe(201);
204
- expect(res.json().data.token).toBeDefined();
205
- });
206
- });
207
-
208
- describe('generateToken', () => {
209
- it('generates a 64-character hex string', () => {
210
- const token = generateToken();
211
- expect(token).toHaveLength(64);
212
- expect(/^[a-f0-9]+$/.test(token)).toBe(true);
213
- });
214
- });
@@ -1,52 +0,0 @@
1
- import { defineModel, field } from '@rangka/shared';
2
-
3
- export const userSchema = defineModel({
4
- name: 'user',
5
- label: 'User',
6
- fields: {
7
- email: field.string({ required: true }),
8
- password_hash: field.string({ required: true }),
9
- full_name: field.string({ required: true }),
10
- enabled: field.boolean({ default: true }),
11
- },
12
- indexes: [{ fields: ['email'], unique: true }],
13
- });
14
-
15
- export const roleSchema = defineModel({
16
- name: 'role',
17
- label: 'Role',
18
- fields: {
19
- name: field.string({ required: true }),
20
- inherits: field.json({}),
21
- permissions: field.json({}),
22
- },
23
- indexes: [{ fields: ['name'], unique: true }],
24
- });
25
-
26
- export const userRoleSchema = defineModel({
27
- name: 'user_role',
28
- label: 'User Role',
29
- fields: {
30
- user_id: field.link('core.user', { required: true }),
31
- role_id: field.link('core.role', { required: true }),
32
- },
33
- indexes: [{ fields: ['user_id', 'role_id'], unique: true }],
34
- });
35
-
36
- export const sessionSchema = defineModel({
37
- name: 'session',
38
- label: 'Session',
39
- fields: {
40
- token: field.string({ required: true }),
41
- user_id: field.link('core.user', { required: true }),
42
- expires_at: field.datetime({ required: true }),
43
- created_at: field.datetime({ required: true }),
44
- },
45
- indexes: [
46
- { fields: ['token'], unique: true },
47
- { fields: ['user_id'] },
48
- { fields: ['expires_at'] },
49
- ],
50
- });
51
-
52
- export const coreSchemas = [userSchema, roleSchema, userRoleSchema, sessionSchema];
@@ -1,59 +0,0 @@
1
- import type { DiscoveredApp } from '../boot/types.js';
2
- import type { ResolvedModel, ResolvedField } from '../schema/types.js';
3
- import { coreSchemas } from './core-models.js';
4
- import type { FieldConfig } from '@rangka/shared';
5
-
6
- function schemaToResolvedModel(schema: {
7
- name: string;
8
- label?: string;
9
- fields: Record<string, FieldConfig>;
10
- indexes?: Array<{ fields: string[]; unique?: boolean }>;
11
- }): ResolvedModel {
12
- const fields: ResolvedField[] = [
13
- {
14
- name: 'id',
15
- config: { type: 'string', required: true } as FieldConfig,
16
- provenance: { source: 'base' },
17
- },
18
- ...Object.entries(schema.fields).map(([name, config]) => ({
19
- name,
20
- config,
21
- provenance: { source: 'base' as const },
22
- })),
23
- ];
24
-
25
- return {
26
- qualifiedName: `core.${schema.name}`,
27
- app: 'core',
28
- module: 'core',
29
- name: schema.name,
30
- label: schema.label,
31
- auditLog: false,
32
- traits: [],
33
- fields,
34
- indexes: schema.indexes,
35
- };
36
- }
37
-
38
- export function getCoreModels(): ResolvedModel[] {
39
- return coreSchemas.map(schemaToResolvedModel);
40
- }
41
-
42
- export function getCoreApp(): DiscoveredApp {
43
- return {
44
- packageInfo: {
45
- packageName: '@rangka/core',
46
- path: '__builtin__',
47
- rangka: { type: 'app', entrypoint: 'index.js' },
48
- },
49
- config: {
50
- name: 'core',
51
- label: 'Core',
52
- },
53
- schemas: coreSchemas.map((s) => ({
54
- module: 'core',
55
- schema: s,
56
- })),
57
- extensions: [],
58
- };
59
- }