@rangka/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -18
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,224 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- Kysely,
4
- DummyDriver,
5
- PostgresAdapter,
6
- PostgresIntrospector,
7
- PostgresQueryCompiler,
8
- } from 'kysely';
9
- import { ModelQueryBuilder } from '../query-builder.js';
10
- import type { SchemaRegistry } from '../../schema/registry.js';
11
- import type { ResolvedModel } from '../../schema/types.js';
12
- import { KyselyModelOps } from '../../db/model-ops.js';
13
-
14
- function createTestDb(): Kysely<any> {
15
- return new Kysely<any>({
16
- dialect: {
17
- createAdapter: () => new PostgresAdapter(),
18
- createDriver: () => new DummyDriver(),
19
- createIntrospector: (db: any) => new PostgresIntrospector(db),
20
- createQueryCompiler: () => new PostgresQueryCompiler(),
21
- },
22
- });
23
- }
24
-
25
- function makeModel(qualifiedName: string, traits: string[] = []): ResolvedModel {
26
- return {
27
- qualifiedName,
28
- app: 'test',
29
- module: qualifiedName.split('.')[0],
30
- name: qualifiedName.split('.')[1],
31
- auditLog: false,
32
- traits,
33
- fields: [
34
- { name: 'id', config: { type: 'uuid' }, provenance: { source: 'base' } },
35
- { name: 'name', config: { type: 'string' }, provenance: { source: 'base' } },
36
- { name: 'status', config: { type: 'string' }, provenance: { source: 'base' } },
37
- { name: 'total', config: { type: 'money' }, provenance: { source: 'base' } },
38
- {
39
- name: 'created_at',
40
- config: { type: 'datetime' },
41
- provenance: { source: 'trait', trait: 'timestamped' },
42
- },
43
- ],
44
- indexes: [],
45
- } as ResolvedModel;
46
- }
47
-
48
- function makeRegistry(models: ResolvedModel[]): SchemaRegistry {
49
- return {
50
- getModel: (qn: string) => models.find((m) => m.qualifiedName === qn),
51
- getAllModels: () => models,
52
- getRelationshipsForModel: () => [],
53
- getFieldsForModel: (qn: string) => models.find((m) => m.qualifiedName === qn)?.fields ?? [],
54
- } as unknown as SchemaRegistry;
55
- }
56
-
57
- describe('ModelQueryBuilder', () => {
58
- const db = createTestDb();
59
- const model = makeModel('sales.Order');
60
- const registry = makeRegistry([model]);
61
- const ops = new KyselyModelOps({ db, model, registry });
62
-
63
- function createBuilder() {
64
- return new ModelQueryBuilder(ops, model, registry);
65
- }
66
-
67
- describe('compilation', () => {
68
- it('generates basic select all from table', () => {
69
- const builder = createBuilder();
70
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
71
- expect(compiled.sql).toContain('select * from "sales"."Order"');
72
- });
73
-
74
- it('applies filter', () => {
75
- const builder = createBuilder().filter({ status: 'active' });
76
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
77
- expect(compiled.sql).toContain('"status" = $1');
78
- expect(compiled.parameters).toContain('active');
79
- });
80
-
81
- it('applies multiple filters (chained)', () => {
82
- const builder = createBuilder()
83
- .filter({ status: 'active' })
84
- .filter({ total: { gt: 100 } });
85
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
86
- expect(compiled.sql).toContain('"status" = $1');
87
- expect(compiled.sql).toContain('"total" > $2');
88
- });
89
-
90
- it('applies sort ascending by default', () => {
91
- const builder = createBuilder().sort('name');
92
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
93
- expect(compiled.sql).toContain('order by "name" asc');
94
- });
95
-
96
- it('applies sort descending', () => {
97
- const builder = createBuilder().sort('created_at', 'desc');
98
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
99
- expect(compiled.sql).toContain('order by "created_at" desc');
100
- });
101
-
102
- it('applies multiple sorts in order', () => {
103
- const builder = createBuilder().sort('status', 'asc').sort('created_at', 'desc');
104
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
105
- expect(compiled.sql).toContain('order by "status" asc, "created_at" desc');
106
- });
107
-
108
- it('applies limit', () => {
109
- const builder = createBuilder().limit(10);
110
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
111
- expect(compiled.sql).toContain('limit $1');
112
- expect(compiled.parameters).toContain(10);
113
- });
114
-
115
- it('applies offset', () => {
116
- const builder = createBuilder().offset(20);
117
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
118
- expect(compiled.sql).toContain('offset $1');
119
- expect(compiled.parameters).toContain(20);
120
- });
121
-
122
- it('applies limit and offset together', () => {
123
- const builder = createBuilder().limit(10).offset(20);
124
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
125
- expect(compiled.sql).toContain('limit $1');
126
- expect(compiled.sql).toContain('offset $2');
127
- });
128
-
129
- it('applies field selection', () => {
130
- const builder = createBuilder().fields(['id', 'name', 'status']);
131
- const compiled = builder.compile() as { sql: string; parameters: unknown[] };
132
- expect(compiled.sql).toContain('"id"');
133
- expect(compiled.sql).toContain('"name"');
134
- expect(compiled.sql).toContain('"status"');
135
- expect(compiled.sql).not.toContain('*');
136
- });
137
- });
138
-
139
- describe('chaining order independence', () => {
140
- it('produces same SQL regardless of method order', () => {
141
- const a = createBuilder()
142
- .filter({ status: 'active' })
143
- .sort('name')
144
- .limit(10)
145
- .offset(5)
146
- .compile() as { sql: string };
147
-
148
- const b = createBuilder()
149
- .limit(10)
150
- .offset(5)
151
- .sort('name')
152
- .filter({ status: 'active' })
153
- .compile() as { sql: string };
154
-
155
- expect(a.sql).toBe(b.sql);
156
- });
157
- });
158
-
159
- describe('count compilation', () => {
160
- it('generates count query', () => {
161
- const builder = createBuilder().filter({ status: 'active' });
162
- const compiled = builder.compileCount() as { sql: string; parameters: unknown[] };
163
- expect(compiled.sql).toContain('count(*)');
164
- expect(compiled.sql).toContain('"status" = $1');
165
- expect(compiled.sql).not.toContain('limit');
166
- expect(compiled.sql).not.toContain('offset');
167
- expect(compiled.sql).not.toContain('order by');
168
- });
169
- });
170
-
171
- describe('immutability', () => {
172
- it('each chain call returns a new instance', () => {
173
- const base = createBuilder();
174
- const filtered = base.filter({ status: 'active' });
175
- const sorted = base.sort('name');
176
-
177
- expect(filtered).not.toBe(base);
178
- expect(sorted).not.toBe(base);
179
- expect(filtered).not.toBe(sorted);
180
-
181
- const filteredSql = (filtered.compile() as { sql: string }).sql;
182
- const sortedSql = (sorted.compile() as { sql: string }).sql;
183
- expect(filteredSql).toContain('"status"');
184
- expect(filteredSql).not.toContain('order by');
185
- expect(sortedSql).toContain('order by');
186
- expect(sortedSql).not.toContain('"status"');
187
- });
188
- });
189
-
190
- describe('unscoped', () => {
191
- it('sets unscoped flag', () => {
192
- const builder = createBuilder().unscoped();
193
- expect(builder.isUnscoped()).toBe(true);
194
- });
195
-
196
- it('defaults to scoped', () => {
197
- const builder = createBuilder();
198
- expect(builder.isUnscoped()).toBe(false);
199
- });
200
- });
201
-
202
- describe('include', () => {
203
- it('stores include relations', () => {
204
- const builder = createBuilder().include('customer').include('lineItems');
205
- expect(builder.getIncludes()).toEqual(['customer', 'lineItems']);
206
- });
207
-
208
- it('deduplicates includes', () => {
209
- const builder = createBuilder().include('customer').include('customer');
210
- expect(builder.getIncludes()).toEqual(['customer']);
211
- });
212
- });
213
-
214
- describe('soft_delete model', () => {
215
- it('adds archived_at IS NULL filter by default', () => {
216
- const softModel = makeModel('sales.Order', ['soft_delete']);
217
- const softRegistry = makeRegistry([softModel]);
218
- const softOps = new KyselyModelOps({ db, model: softModel, registry: softRegistry });
219
- const builder = new ModelQueryBuilder(softOps, softModel, softRegistry);
220
- const compiled = builder.compile() as { sql: string };
221
- expect(compiled.sql).toContain('"archived_at" is null');
222
- });
223
- });
224
- });
@@ -1,268 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- Kysely,
4
- DummyDriver,
5
- PostgresAdapter,
6
- PostgresIntrospector,
7
- PostgresQueryCompiler,
8
- } from 'kysely';
9
- import { applyScopeEnforcement, stripHiddenFields, enforceReadOnly } from '../scope-enforcer.js';
10
- import type { ResolvedModel } from '../../schema/types.js';
11
- import type { RequestContext, ScopeFilter } from '../../auth/types.js';
12
-
13
- function createTestDb() {
14
- return new Kysely<any>({
15
- dialect: {
16
- createAdapter: () => new PostgresAdapter(),
17
- createDriver: () => new DummyDriver(),
18
- createIntrospector: (db: any) => new PostgresIntrospector(db),
19
- createQueryCompiler: () => new PostgresQueryCompiler(),
20
- },
21
- });
22
- }
23
-
24
- function makeModel(qualifiedName: string, traits: string[] = []): ResolvedModel {
25
- return {
26
- qualifiedName,
27
- app: 'test',
28
- module: qualifiedName.split('.')[0],
29
- name: qualifiedName.split('.')[1],
30
- auditLog: false,
31
- traits,
32
- fields: [
33
- { name: 'id', config: { type: 'uuid' }, provenance: { source: 'base' } },
34
- { name: 'name', config: { type: 'string' }, provenance: { source: 'base' } },
35
- { name: 'secret_field', config: { type: 'string' }, provenance: { source: 'base' } },
36
- { name: 'tenant_id', config: { type: 'uuid' }, provenance: { source: 'base' } },
37
- {
38
- name: 'created_by',
39
- config: { type: 'uuid' },
40
- provenance: { source: 'trait', trait: 'timestamped' },
41
- },
42
- ],
43
- indexes: [],
44
- } as ResolvedModel;
45
- }
46
-
47
- describe('applyScopeEnforcement', () => {
48
- const db = createTestDb();
49
-
50
- it('adds scope filter WHERE clauses to query', () => {
51
- const scopeFilters: ScopeFilter[] = [{ field: 'tenant_id', operator: 'eq', value: 'tenant-1' }];
52
- const auth: RequestContext = { scopeFilters };
53
-
54
- let query = db.selectFrom('sales__Order').selectAll();
55
- query = applyScopeEnforcement(query, auth);
56
- const compiled = query.compile();
57
-
58
- expect(compiled.sql).toContain('"tenant_id" = $1');
59
- expect(compiled.parameters).toEqual(['tenant-1']);
60
- });
61
-
62
- it('adds multiple scope filters as AND conditions', () => {
63
- const scopeFilters: ScopeFilter[] = [
64
- { field: 'tenant_id', operator: 'eq', value: 'tenant-1' },
65
- { field: 'branch_id', operator: 'eq', value: 'branch-1' },
66
- ];
67
- const auth: RequestContext = { scopeFilters };
68
-
69
- let query = db.selectFrom('sales__Order').selectAll();
70
- query = applyScopeEnforcement(query, auth);
71
- const compiled = query.compile();
72
-
73
- expect(compiled.sql).toContain('"tenant_id" = $1');
74
- expect(compiled.sql).toContain('"branch_id" = $2');
75
- expect(compiled.parameters).toEqual(['tenant-1', 'branch-1']);
76
- });
77
-
78
- it('handles "in" operator for scope filters', () => {
79
- const scopeFilters: ScopeFilter[] = [
80
- { field: 'tenant_id', operator: 'in', value: ['t-1', 't-2'] },
81
- ];
82
- const auth: RequestContext = { scopeFilters };
83
-
84
- let query = db.selectFrom('sales__Order').selectAll();
85
- query = applyScopeEnforcement(query, auth);
86
- const compiled = query.compile();
87
-
88
- expect(compiled.sql).toContain('"tenant_id" in ($1, $2)');
89
- expect(compiled.parameters).toEqual(['t-1', 't-2']);
90
- });
91
-
92
- it('does nothing when no scope filters exist', () => {
93
- const auth: RequestContext = {};
94
-
95
- let query = db.selectFrom('sales__Order').selectAll();
96
- query = applyScopeEnforcement(query, auth);
97
- const compiled = query.compile();
98
-
99
- expect(compiled.sql).not.toContain('where');
100
- });
101
-
102
- it('does nothing when scopeFilters is empty array', () => {
103
- const auth: RequestContext = { scopeFilters: [] };
104
-
105
- let query = db.selectFrom('sales__Order').selectAll();
106
- query = applyScopeEnforcement(query, auth);
107
- const compiled = query.compile();
108
-
109
- expect(compiled.sql).not.toContain('where');
110
- });
111
-
112
- it('does nothing when auth is undefined', () => {
113
- let query = db.selectFrom('sales__Order').selectAll();
114
- query = applyScopeEnforcement(query, undefined);
115
- const compiled = query.compile();
116
-
117
- expect(compiled.sql).not.toContain('where');
118
- });
119
-
120
- it('adds owner-only filter when specified', () => {
121
- const auth: RequestContext = {
122
- user: { id: 'user-1', email: 'a@b.c', full_name: 'A', enabled: true, password_hash: '' },
123
- permissions: {
124
- models: {
125
- 'sales.Order': { read: 'own', write: 'own', delete: 'own' },
126
- },
127
- pages: [],
128
- version: 1,
129
- },
130
- };
131
-
132
- const model = makeModel('sales.Order');
133
- let query = db.selectFrom('sales__Order').selectAll();
134
- query = applyScopeEnforcement(query, auth, { model, checkOwnership: true });
135
- const compiled = query.compile();
136
-
137
- expect(compiled.sql).toContain('"created_by" = $1');
138
- expect(compiled.parameters).toEqual(['user-1']);
139
- });
140
-
141
- it('does not add owner filter when permission is not "own"', () => {
142
- const auth: RequestContext = {
143
- user: { id: 'user-1', email: 'a@b.c', full_name: 'A', enabled: true, password_hash: '' },
144
- permissions: {
145
- models: {
146
- 'sales.Order': { read: true, write: true, delete: true },
147
- },
148
- pages: [],
149
- version: 1,
150
- },
151
- };
152
-
153
- const model = makeModel('sales.Order');
154
- let query = db.selectFrom('sales__Order').selectAll();
155
- query = applyScopeEnforcement(query, auth, { model, checkOwnership: true });
156
- const compiled = query.compile();
157
-
158
- expect(compiled.sql).not.toContain('"created_by"');
159
- });
160
-
161
- it('does not add owner filter when model lacks created_by field', () => {
162
- const auth: RequestContext = {
163
- user: { id: 'user-1', email: 'a@b.c', full_name: 'A', enabled: true, password_hash: '' },
164
- permissions: {
165
- models: {
166
- 'sales.Order': { read: 'own', write: 'own', delete: 'own' },
167
- },
168
- pages: [],
169
- version: 1,
170
- },
171
- };
172
-
173
- const modelWithoutCreatedBy = {
174
- qualifiedName: 'sales.Order',
175
- app: 'test',
176
- module: 'sales',
177
- name: 'Order',
178
- auditLog: false,
179
- traits: [],
180
- fields: [{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } }],
181
- indexes: [],
182
- } as ResolvedModel;
183
-
184
- let query = db.selectFrom('sales__Order').selectAll();
185
- query = applyScopeEnforcement(query, auth, {
186
- model: modelWithoutCreatedBy,
187
- checkOwnership: true,
188
- });
189
- const compiled = query.compile();
190
-
191
- expect(compiled.sql).not.toContain('"created_by"');
192
- });
193
- });
194
-
195
- describe('stripHiddenFields', () => {
196
- it('removes hidden fields from a single record', () => {
197
- const record = { id: '1', name: 'Test', secret_field: 'secret' };
198
- const hidden = new Set(['secret_field']);
199
- const result = stripHiddenFields(record, hidden);
200
- expect(result).toEqual({ id: '1', name: 'Test' });
201
- });
202
-
203
- it('removes hidden fields from array of records', () => {
204
- const records = [
205
- { id: '1', name: 'A', secret_field: 'x' },
206
- { id: '2', name: 'B', secret_field: 'y' },
207
- ];
208
- const hidden = new Set(['secret_field']);
209
- const result = records.map((r) => stripHiddenFields(r, hidden));
210
- expect(result).toEqual([
211
- { id: '1', name: 'A' },
212
- { id: '2', name: 'B' },
213
- ]);
214
- });
215
-
216
- it('returns record unchanged when no fields are hidden', () => {
217
- const record = { id: '1', name: 'Test' };
218
- const hidden = new Set<string>();
219
- const result = stripHiddenFields(record, hidden);
220
- expect(result).toEqual(record);
221
- });
222
-
223
- it('returns empty object when all fields are hidden', () => {
224
- const record = { id: '1', name: 'Test' };
225
- const hidden = new Set(['id', 'name']);
226
- const result = stripHiddenFields(record, hidden);
227
- expect(result).toEqual({});
228
- });
229
-
230
- it('handles record with nested objects (does not strip nested)', () => {
231
- const record = { id: '1', meta: { secret_field: 'x' }, secret_field: 'top' };
232
- const hidden = new Set(['secret_field']);
233
- const result = stripHiddenFields(record, hidden);
234
- expect(result).toEqual({ id: '1', meta: { secret_field: 'x' } });
235
- });
236
- });
237
-
238
- describe('enforceReadOnly', () => {
239
- it('throws when writing to a read-only field', () => {
240
- const readOnly = new Set(['status']);
241
- const data = { status: 'active', name: 'Test' };
242
- expect(() => enforceReadOnly(data, readOnly)).toThrow(/read-only.*status/i);
243
- });
244
-
245
- it('does not throw when writing to non-read-only fields', () => {
246
- const readOnly = new Set(['status']);
247
- const data = { name: 'Test', email: 'a@b.c' };
248
- expect(() => enforceReadOnly(data, readOnly)).not.toThrow();
249
- });
250
-
251
- it('does not throw when readOnly set is empty', () => {
252
- const readOnly = new Set<string>();
253
- const data = { status: 'active', name: 'Test' };
254
- expect(() => enforceReadOnly(data, readOnly)).not.toThrow();
255
- });
256
-
257
- it('reports all violated fields in the error', () => {
258
- const readOnly = new Set(['status', 'level']);
259
- const data = { status: 'active', level: 5, name: 'Test' };
260
- expect(() => enforceReadOnly(data, readOnly)).toThrow(/status.*level|level.*status/);
261
- });
262
-
263
- it('ignores undefined values (not actually writing)', () => {
264
- const readOnly = new Set(['status']);
265
- const data = { status: undefined, name: 'Test' };
266
- expect(() => enforceReadOnly(data, readOnly)).not.toThrow();
267
- });
268
- });
@@ -1,28 +0,0 @@
1
- export function stripHiddenFields(
2
- record: Record<string, unknown>,
3
- hidden: Set<string>,
4
- ): Record<string, unknown> {
5
- if (hidden.size === 0) return record;
6
- const result: Record<string, unknown> = {};
7
- for (const [key, value] of Object.entries(record)) {
8
- if (!hidden.has(key)) {
9
- result[key] = value;
10
- }
11
- }
12
- return result;
13
- }
14
-
15
- export function enforceReadOnly(data: Record<string, unknown>, readOnly: Set<string>): void {
16
- if (readOnly.size === 0) return;
17
-
18
- const violated: string[] = [];
19
- for (const field of readOnly) {
20
- if (field in data && data[field] !== undefined) {
21
- violated.push(field);
22
- }
23
- }
24
-
25
- if (violated.length > 0) {
26
- throw new Error(`Cannot write to read-only fields: ${violated.join(', ')}`);
27
- }
28
- }
@@ -1 +0,0 @@
1
- export { applyModelFilters } from '../db/filter-applier.js';
@@ -1,67 +0,0 @@
1
- import type { FilterExpression, FilterOperators, TranslatedFilter } from './types.js';
2
- import { isNil } from '../helpers/coerce.js';
3
-
4
- export type { TranslatedFilter } from './types.js';
5
-
6
- const VALID_OPERATORS = new Set([
7
- 'eq',
8
- 'neq',
9
- 'gt',
10
- 'gte',
11
- 'lt',
12
- 'lte',
13
- 'in',
14
- 'notIn',
15
- 'contains',
16
- 'startsWith',
17
- 'endsWith',
18
- 'is',
19
- ]);
20
-
21
- function isOperatorObject(value: unknown): value is FilterOperators {
22
- if (isNil(value)) return false;
23
- if (typeof value !== 'object') return false;
24
- if (Array.isArray(value)) return false;
25
- if (value instanceof Date) return false;
26
- const keys = Object.keys(value);
27
- if (keys.length === 0) return false;
28
- const hasAnyKnown = keys.some((k) => VALID_OPERATORS.has(k));
29
- const hasAnyUnknown = keys.some((k) => !VALID_OPERATORS.has(k));
30
- if (hasAnyUnknown && !hasAnyKnown) {
31
- throw new Error(`Unknown filter operator: ${keys.find((k) => !VALID_OPERATORS.has(k))}`);
32
- }
33
- if (hasAnyUnknown && hasAnyKnown) {
34
- throw new Error(`Unknown filter operator: ${keys.find((k) => !VALID_OPERATORS.has(k))}`);
35
- }
36
- return true;
37
- }
38
-
39
- export function translateFilters(
40
- expression: FilterExpression | null | undefined,
41
- ): TranslatedFilter[] {
42
- if (!expression || typeof expression !== 'object') return [];
43
-
44
- const results: TranslatedFilter[] = [];
45
-
46
- for (const [field, value] of Object.entries(expression)) {
47
- if (value === null) {
48
- results.push({ field, operator: 'is', value: null });
49
- continue;
50
- }
51
-
52
- if (isOperatorObject(value)) {
53
- const ops = value as Record<string, unknown>;
54
- for (const [op, opValue] of Object.entries(ops)) {
55
- if (!VALID_OPERATORS.has(op)) {
56
- throw new Error(`Unknown filter operator: ${op}`);
57
- }
58
- results.push({ field, operator: op, value: opValue });
59
- }
60
- continue;
61
- }
62
-
63
- results.push({ field, operator: 'eq', value });
64
- }
65
-
66
- return results;
67
- }
@@ -1,2 +0,0 @@
1
- export { resolveModelIncludes } from '../db/model-include-resolver.js';
2
- export type { IncludeResolverOptions, IncludeSpec } from '../db/model-include-resolver.js';
@@ -1,86 +0,0 @@
1
- import type { ModelAccess, ModelAccessOptions, ModelOps, ModelQuery } from './types.js';
2
- import { ModelQueryBuilder } from './query-builder.js';
3
- import { KyselyModelOps } from '../db/model-ops.js';
4
- import { ExternalModelOps } from '../external-model/external-model-ops.js';
5
- import { CompositeIncludeResolver } from '../db/include-resolver.js';
6
-
7
- export type {
8
- ModelAccess,
9
- ModelQuery,
10
- ModelAccessOptions,
11
- ModelOps,
12
- FilterExpression,
13
- QueryResult,
14
- QueryResultWithMeta,
15
- QueryState,
16
- TranslatedFilter,
17
- IncludeResolver,
18
- } from './types.js';
19
- export { ModelQueryBuilder } from './query-builder.js';
20
- export { translateFilters } from './filter-translator.js';
21
- export { applyModelFilters } from './filter-applier.js';
22
- export { applyScopeEnforcement, stripHiddenFields, enforceReadOnly } from './scope-enforcer.js';
23
- export { resolveModelIncludes } from './include-resolver.js';
24
- export { CapabilityNotSupportedError } from '../external-model/mutation-executor.js';
25
-
26
- export function createModelAccess(opts: ModelAccessOptions): ModelAccess {
27
- const { db, registry, auth, adapterRegistry, externalModelFields, adapterCapabilities } = opts;
28
-
29
- const includeResolver = new CompositeIncludeResolver({
30
- registry,
31
- db,
32
- adapterRegistry,
33
- externalModelFields,
34
- });
35
-
36
- function resolveOps(modelName: string): ModelOps {
37
- const model = registry.getModel(modelName);
38
- if (!model) throw new Error(`Model not found: ${modelName}`);
39
-
40
- if (model.source) {
41
- if (!adapterRegistry) {
42
- throw new Error(
43
- `No adapter registry configured. Cannot resolve external model: ${modelName}`,
44
- );
45
- }
46
- const adapter = adapterRegistry.get(model.source);
47
- const fields = externalModelFields?.[model.qualifiedName] ?? {};
48
- const capabilities = adapterCapabilities?.[model.source] ?? ['read'];
49
- return new ExternalModelOps({
50
- adapter,
51
- adapterName: model.source,
52
- modelName: model.qualifiedName,
53
- fields,
54
- capabilities,
55
- });
56
- }
57
-
58
- return new KyselyModelOps({ db, model, registry, auth });
59
- }
60
-
61
- return {
62
- async get(modelName: string, id: string) {
63
- return resolveOps(modelName).get(id);
64
- },
65
-
66
- query(modelName: string): ModelQuery {
67
- const model = registry.getModel(modelName);
68
- if (!model) throw new Error(`Model not found: ${modelName}`);
69
- const ops = resolveOps(modelName);
70
- const qb = new ModelQueryBuilder(ops, model, registry, includeResolver);
71
- return auth ? qb.withAuth(auth) : qb;
72
- },
73
-
74
- async create(modelName: string, data: Record<string, unknown>) {
75
- return resolveOps(modelName).create(data, auth);
76
- },
77
-
78
- async update(modelName: string, id: string, data: Record<string, unknown>) {
79
- return resolveOps(modelName).update(id, data, auth);
80
- },
81
-
82
- async delete(modelName: string, id: string) {
83
- return resolveOps(modelName).delete(id, auth);
84
- },
85
- };
86
- }