@rangka/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -18
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
|
@@ -1,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
|
-
});
|
package/src/auth/core-models.ts
DELETED
|
@@ -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];
|
package/src/auth/core-module.ts
DELETED
|
@@ -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
|
-
}
|