@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,504 +0,0 @@
1
- import type { ResolvedField, ResolvedModel } from '../schema/types.js';
2
- import type {
3
- ColumnDefinition,
4
- ForeignKeyDefinition,
5
- CheckConstraintDefinition,
6
- TableDefinition,
7
- } from './types.js';
8
-
9
- /**
10
- * The complete set of database artifacts produced when mapping a model's fields to columns.
11
- */
12
- export interface FieldMappingResult {
13
- columns: ColumnDefinition[];
14
- foreignKeys: ForeignKeyDefinition[];
15
- checkConstraints: CheckConstraintDefinition[];
16
- extraTables: TableDefinition[];
17
- }
18
-
19
- // ---------------------------------------------------------------------------
20
- // Public API
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * Convert a dot-separated qualified model name (e.g. "core.User") to a
25
- * flat table name using double-underscore as separator (e.g. "core__User").
26
- */
27
- export function modelToTableName(qualifiedName: string): string {
28
- return qualifiedName.replace('.', '__');
29
- }
30
-
31
- /**
32
- * Map all fields of a resolved model into database column definitions,
33
- * foreign keys, check constraints, and any extra junction/closure tables.
34
- */
35
- export function mapFieldsToColumns(model: ResolvedModel): FieldMappingResult {
36
- const tableName = modelToTableName(model.qualifiedName);
37
- const columns: ColumnDefinition[] = [];
38
- const foreignKeys: ForeignKeyDefinition[] = [];
39
- const checkConstraints: CheckConstraintDefinition[] = [];
40
- const extraTables: TableDefinition[] = [];
41
-
42
- // Every table gets a UUID primary key
43
- columns.push({
44
- name: 'id',
45
- type: 'UUID',
46
- nullable: false,
47
- defaultValue: 'gen_random_uuid()',
48
- primaryKey: true,
49
- });
50
-
51
- // Add trait-derived columns (timestamped, etc.)
52
- // but skip any that the model already declares explicitly as a field.
53
- const traitResult = mapTraitColumns(model.traits, tableName);
54
- const explicitFieldNames = new Set(model.fields.map((f) => f.name));
55
- for (const traitColumn of traitResult.columns) {
56
- if (!explicitFieldNames.has(traitColumn.name)) {
57
- columns.push(traitColumn);
58
- }
59
- }
60
- foreignKeys.push(...traitResult.foreignKeys);
61
-
62
- // Map each user-defined field to its database representation
63
- for (const field of model.fields) {
64
- const fieldResult = mapField(field, model, tableName);
65
- columns.push(...fieldResult.columns);
66
- foreignKeys.push(...fieldResult.foreignKeys);
67
- checkConstraints.push(...fieldResult.checkConstraints);
68
- extraTables.push(...fieldResult.extraTables);
69
- }
70
-
71
- return { columns, foreignKeys, checkConstraints, extraTables };
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // Field-type mapping (main dispatcher)
76
- // ---------------------------------------------------------------------------
77
-
78
- /**
79
- * Route a single field to the appropriate column mapping logic based on its type.
80
- */
81
- function mapField(
82
- field: ResolvedField,
83
- model: ResolvedModel,
84
- tableName: string,
85
- ): FieldMappingResult {
86
- const config = field.config;
87
-
88
- switch (config.type) {
89
- case 'string':
90
- return singleColumn({
91
- name: field.name,
92
- type: `VARCHAR(${config.maxLength ?? 255})`,
93
- nullable: !config.required,
94
- defaultValue: quoteStringDefault(config.default),
95
- });
96
-
97
- case 'text':
98
- return singleColumn({
99
- name: field.name,
100
- type: 'TEXT',
101
- nullable: !config.required,
102
- defaultValue: quoteStringDefault(config.default),
103
- });
104
-
105
- case 'int':
106
- return singleColumn({
107
- name: field.name,
108
- type: 'INTEGER',
109
- nullable: !config.required,
110
- defaultValue: config.default !== undefined ? String(config.default) : undefined,
111
- });
112
-
113
- case 'decimal':
114
- return singleColumn({
115
- name: field.name,
116
- type: `DECIMAL(${config.precision ?? 18},${config.scale ?? 6})`,
117
- nullable: !config.required,
118
- defaultValue: config.default !== undefined ? String(config.default) : undefined,
119
- });
120
-
121
- case 'boolean':
122
- return singleColumn({
123
- name: field.name,
124
- type: 'BOOLEAN',
125
- nullable: !config.required,
126
- defaultValue: formatBooleanDefault(config.default),
127
- });
128
-
129
- case 'date':
130
- return singleColumn({
131
- name: field.name,
132
- type: 'DATE',
133
- nullable: !config.required,
134
- defaultValue: quoteStringDefault(config.default),
135
- });
136
-
137
- case 'datetime':
138
- return singleColumn({
139
- name: field.name,
140
- type: 'TIMESTAMPTZ',
141
- nullable: !config.required,
142
- defaultValue: quoteStringDefault(config.default),
143
- });
144
-
145
- case 'enum':
146
- return mapEnumField(field, config, tableName);
147
-
148
- case 'json':
149
- return singleColumn({
150
- name: field.name,
151
- type: 'JSONB',
152
- nullable: !config.required,
153
- });
154
-
155
- case 'code':
156
- return singleColumn({
157
- name: field.name,
158
- type: 'TEXT',
159
- nullable: !config.required,
160
- });
161
-
162
- case 'link':
163
- return mapLinkField(field, config, model, tableName);
164
-
165
- case 'dynamicLink':
166
- return mapDynamicLinkField(field, config);
167
-
168
- case 'money':
169
- return mapMoneyField(field, config);
170
-
171
- case 'tree':
172
- return mapTreeField(field, model, tableName, config.strategy);
173
-
174
- case 'sequence':
175
- return singleColumn({
176
- name: field.name,
177
- type: 'VARCHAR(255)',
178
- nullable: true,
179
- });
180
-
181
- // Relation types have no local columns -- they're handled by the related model
182
- case 'hasMany':
183
- case 'children':
184
- return emptyResult();
185
-
186
- case 'manyToMany':
187
- return buildManyToManyJunction(field, model);
188
-
189
- case 'attachment':
190
- case 'attachments':
191
- return singleColumn({
192
- name: field.name,
193
- type: 'JSONB',
194
- nullable: !config.required,
195
- });
196
-
197
- default:
198
- return emptyResult();
199
- }
200
- }
201
-
202
- // ---------------------------------------------------------------------------
203
- // Compound field mappers (produce multiple columns or constraints)
204
- // ---------------------------------------------------------------------------
205
-
206
- /** Enum fields store a VARCHAR with a CHECK constraint restricting allowed values. */
207
- function mapEnumField(
208
- field: ResolvedField,
209
- config: { required?: boolean; default?: string; options: readonly string[] },
210
- tableName: string,
211
- ): FieldMappingResult {
212
- const allowedValues = config.options.map((v) => `'${v}'`).join(', ');
213
-
214
- return {
215
- columns: [
216
- {
217
- name: field.name,
218
- type: 'VARCHAR(255)',
219
- nullable: !config.required,
220
- defaultValue: quoteStringDefault(config.default),
221
- },
222
- ],
223
- foreignKeys: [],
224
- checkConstraints: [
225
- {
226
- name: `chk_${tableName}_${field.name}`,
227
- column: field.name,
228
- expression: `${field.name} IN (${allowedValues})`,
229
- },
230
- ],
231
- extraTables: [],
232
- };
233
- }
234
-
235
- /** Link fields store a UUID foreign key pointing to another model's table. */
236
- function mapLinkField(
237
- field: ResolvedField,
238
- config: { required?: boolean; nullable?: boolean; model: string },
239
- currentModel: ResolvedModel,
240
- tableName: string,
241
- ): FieldMappingResult {
242
- const referencedTable =
243
- config.model === '__self__' ? tableName : resolveLinkedTableName(config.model, currentModel);
244
-
245
- return {
246
- columns: [
247
- {
248
- name: field.name,
249
- type: 'UUID',
250
- nullable: config.nullable === true ? true : !config.required,
251
- },
252
- ],
253
- foreignKeys: [
254
- {
255
- name: `fk_${tableName}_${field.name}`,
256
- column: field.name,
257
- referencedTable,
258
- referencedColumn: 'id',
259
- },
260
- ],
261
- checkConstraints: [],
262
- extraTables: [],
263
- };
264
- }
265
-
266
- /** Dynamic links store a model type discriminator column alongside the UUID reference. */
267
- function mapDynamicLinkField(
268
- field: ResolvedField,
269
- config: { required?: boolean; modelField: string },
270
- ): FieldMappingResult {
271
- return {
272
- columns: [
273
- {
274
- name: config.modelField,
275
- type: 'VARCHAR(255)',
276
- nullable: !config.required,
277
- },
278
- {
279
- name: field.name,
280
- type: 'UUID',
281
- nullable: !config.required,
282
- },
283
- ],
284
- foreignKeys: [],
285
- checkConstraints: [],
286
- extraTables: [],
287
- };
288
- }
289
-
290
- /** Money fields store both the raw amount and a base-currency converted amount. */
291
- function mapMoneyField(field: ResolvedField, config: { required?: boolean }): FieldMappingResult {
292
- return {
293
- columns: [
294
- {
295
- name: field.name,
296
- type: 'DECIMAL(18,6)',
297
- nullable: !config.required,
298
- },
299
- {
300
- name: `${field.name}_base`,
301
- type: 'DECIMAL(18,6)',
302
- nullable: !config.required,
303
- },
304
- ],
305
- foreignKeys: [],
306
- checkConstraints: [],
307
- extraTables: [],
308
- };
309
- }
310
-
311
- /**
312
- * Tree fields store a self-referencing parent FK plus strategy-specific columns:
313
- * - materialized_path: path (TEXT) + depth (INTEGER)
314
- * - nested_set: lft + rgt (INTEGER pair)
315
- * - closure_table: generates an extra closure junction table
316
- */
317
- function mapTreeField(
318
- field: ResolvedField,
319
- _model: ResolvedModel,
320
- tableName: string,
321
- strategy: 'materialized_path' | 'nested_set' | 'closure_table',
322
- ): FieldMappingResult {
323
- const parentColumn = field.name;
324
- const columns: ColumnDefinition[] = [{ name: parentColumn, type: 'UUID', nullable: true }];
325
-
326
- const foreignKeys: ForeignKeyDefinition[] = [
327
- {
328
- name: `fk_${tableName}_${parentColumn}`,
329
- column: parentColumn,
330
- referencedTable: tableName,
331
- referencedColumn: 'id',
332
- },
333
- ];
334
-
335
- const extraTables: TableDefinition[] = [];
336
-
337
- switch (strategy) {
338
- case 'materialized_path':
339
- columns.push(
340
- { name: 'path', type: 'TEXT', nullable: true },
341
- { name: 'depth', type: 'INTEGER', nullable: true },
342
- );
343
- break;
344
-
345
- case 'nested_set':
346
- columns.push(
347
- { name: 'lft', type: 'INTEGER', nullable: true },
348
- { name: 'rgt', type: 'INTEGER', nullable: true },
349
- );
350
- break;
351
-
352
- case 'closure_table':
353
- extraTables.push(buildClosureTable(tableName));
354
- break;
355
- }
356
-
357
- return { columns, foreignKeys, checkConstraints: [], extraTables };
358
- }
359
-
360
- /** Build the closure junction table definition for a tree with closure_table strategy. */
361
- function buildClosureTable(tableName: string): TableDefinition {
362
- return {
363
- name: `${tableName}_closure`,
364
- columns: [
365
- { name: 'ancestor_id', type: 'UUID', nullable: false },
366
- { name: 'descendant_id', type: 'UUID', nullable: false },
367
- { name: 'depth', type: 'INTEGER', nullable: false },
368
- ],
369
- foreignKeys: [
370
- {
371
- name: `fk_${tableName}_closure_ancestor`,
372
- column: 'ancestor_id',
373
- referencedTable: tableName,
374
- referencedColumn: 'id',
375
- },
376
- {
377
- name: `fk_${tableName}_closure_descendant`,
378
- column: 'descendant_id',
379
- referencedTable: tableName,
380
- referencedColumn: 'id',
381
- },
382
- ],
383
- checkConstraints: [],
384
- indexes: [],
385
- };
386
- }
387
-
388
- /** Build a junction table for a manyToMany field. */
389
- function buildManyToManyJunction(field: ResolvedField, model: ResolvedModel): FieldMappingResult {
390
- const config = field.config as { type: 'manyToMany'; model: string; through: string };
391
- const junctionTable = resolveLinkedTableName(config.through, model);
392
- const sourceTable = modelToTableName(model.qualifiedName);
393
- const targetTable = resolveLinkedTableName(config.model, model);
394
-
395
- const sourceModelName = model.name;
396
- const targetModelName = config.model.includes('.')
397
- ? config.model.split('.').pop()!
398
- : config.model;
399
-
400
- const sourceFk = `${sourceModelName}_id`;
401
- const targetFk = `${targetModelName}_id`;
402
-
403
- const junction: TableDefinition = {
404
- name: junctionTable,
405
- columns: [
406
- { name: sourceFk, type: 'UUID', nullable: false },
407
- { name: targetFk, type: 'UUID', nullable: false },
408
- ],
409
- foreignKeys: [
410
- {
411
- name: `fk_${junctionTable}_${sourceFk}`,
412
- column: sourceFk,
413
- referencedTable: sourceTable,
414
- referencedColumn: 'id',
415
- },
416
- {
417
- name: `fk_${junctionTable}_${targetFk}`,
418
- column: targetFk,
419
- referencedTable: targetTable,
420
- referencedColumn: 'id',
421
- },
422
- ],
423
- checkConstraints: [],
424
- indexes: [
425
- {
426
- name: `idx_${junctionTable}_unique`,
427
- table: junctionTable,
428
- columns: [sourceFk, targetFk],
429
- unique: true,
430
- },
431
- ],
432
- };
433
-
434
- return { columns: [], foreignKeys: [], checkConstraints: [], extraTables: [junction] };
435
- }
436
-
437
- // ---------------------------------------------------------------------------
438
- // Trait mapping
439
- // ---------------------------------------------------------------------------
440
-
441
- /**
442
- * Map model traits (e.g. "timestamped") into
443
- * the columns and foreign keys they imply.
444
- */
445
- function mapTraitColumns(
446
- traits: string[],
447
- _tableName: string,
448
- ): { columns: ColumnDefinition[]; foreignKeys: ForeignKeyDefinition[] } {
449
- const columns: ColumnDefinition[] = [];
450
- const foreignKeys: ForeignKeyDefinition[] = [];
451
-
452
- for (const trait of traits) {
453
- switch (trait) {
454
- case 'timestamped':
455
- columns.push(
456
- { name: 'created_at', type: 'TIMESTAMPTZ', nullable: false },
457
- { name: 'updated_at', type: 'TIMESTAMPTZ', nullable: false },
458
- { name: 'created_by', type: 'UUID', nullable: true },
459
- { name: 'updated_by', type: 'UUID', nullable: true },
460
- );
461
- break;
462
- }
463
- }
464
-
465
- return { columns, foreignKeys };
466
- }
467
-
468
- // ---------------------------------------------------------------------------
469
- // Utility helpers
470
- // ---------------------------------------------------------------------------
471
-
472
- /** Wrap a single column definition into a full FieldMappingResult with no extras. */
473
- function singleColumn(column: ColumnDefinition): FieldMappingResult {
474
- return { columns: [column], foreignKeys: [], checkConstraints: [], extraTables: [] };
475
- }
476
-
477
- /** Return an empty result (no columns, no constraints). Used for virtual/relation fields. */
478
- function emptyResult(): FieldMappingResult {
479
- return { columns: [], foreignKeys: [], checkConstraints: [], extraTables: [] };
480
- }
481
-
482
- /** Wrap a string default value in single quotes, or return undefined if not set. */
483
- function quoteStringDefault(value: string | undefined): string | undefined {
484
- return value !== undefined ? `'${value}'` : undefined;
485
- }
486
-
487
- /** Format a boolean default as SQL TRUE/FALSE, or return undefined if not set. */
488
- function formatBooleanDefault(value: boolean | undefined): string | undefined {
489
- if (value === undefined) return undefined;
490
- return value ? 'TRUE' : 'FALSE';
491
- }
492
-
493
- /**
494
- * Resolve a model reference to a table name. If the reference is already
495
- * fully qualified (contains a dot), convert directly. Otherwise, assume
496
- * it belongs to the same module as the current model.
497
- */
498
- function resolveLinkedTableName(modelRef: string, currentModel: ResolvedModel): string {
499
- if (modelRef.includes('.')) {
500
- return modelToTableName(modelRef);
501
- }
502
- const qualifiedName = `${currentModel.module}.${modelRef}`;
503
- return modelToTableName(qualifiedName);
504
- }
@@ -1,89 +0,0 @@
1
- import { sql } from 'kysely';
2
- import type { TranslatedFilter } from '../model-api/types.js';
3
- import { toBool } from '../helpers/coerce.js';
4
-
5
- function escapeLike(value: string): string {
6
- return value.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
7
- }
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- export function applyModelFilters(query: any, filters: TranslatedFilter[]): any {
11
- let result = query;
12
-
13
- for (const { field, operator, value } of filters) {
14
- switch (operator) {
15
- case 'eq':
16
- result = result.where(field, '=', value);
17
- break;
18
- case 'neq':
19
- result = result.where(field, '!=', value);
20
- break;
21
- case 'gt':
22
- result = result.where(field, '>', value);
23
- break;
24
- case 'gte':
25
- result = result.where(field, '>=', value);
26
- break;
27
- case 'lt':
28
- result = result.where(field, '<', value);
29
- break;
30
- case 'lte':
31
- result = result.where(field, '<=', value);
32
- break;
33
- case 'in': {
34
- const arr = value as unknown[];
35
- if (arr.length === 0) {
36
- result = result.where(sql`1 = 0`);
37
- } else {
38
- result = result.where(field, 'in', arr);
39
- }
40
- break;
41
- }
42
- case 'notIn': {
43
- const arr = value as unknown[];
44
- if (arr.length > 0) {
45
- result = result.where(field, 'not in', arr);
46
- }
47
- break;
48
- }
49
- case 'contains':
50
- result = result.where(field, 'ilike', `%${escapeLike(value as string)}%`);
51
- break;
52
- case 'startsWith':
53
- result = result.where(field, 'ilike', `${escapeLike(value as string)}%`);
54
- break;
55
- case 'endsWith':
56
- result = result.where(field, 'ilike', `%${escapeLike(value as string)}`);
57
- break;
58
- case 'is':
59
- if (value === null) {
60
- result = result.where(field, 'is', null);
61
- } else {
62
- result = result.where(field, 'is not', null);
63
- }
64
- break;
65
- case 'isnull':
66
- if (toBool(value)) {
67
- result = result.where(field, 'is', null);
68
- } else {
69
- result = result.where(field, 'is not', null);
70
- }
71
- break;
72
- case 'like':
73
- result = result.where(field, 'ilike', `%${value}%`);
74
- break;
75
- }
76
- }
77
-
78
- return result;
79
- }
80
-
81
- /* eslint-disable @typescript-eslint/no-explicit-any */
82
- export function applySearchFilter(query: any, term: string, fields: string[]): any {
83
- if (!term || fields.length === 0) return query;
84
- const escaped = `%${escapeLike(term)}%`;
85
- return query.where((eb: any) =>
86
- eb.or(fields.map((field: string) => eb(field, 'ilike', escaped))),
87
- );
88
- }
89
- /* eslint-enable @typescript-eslint/no-explicit-any */
@@ -1,40 +0,0 @@
1
- import type { Kysely } from 'kysely';
2
- import type { SchemaRegistry } from '../schema/registry.js';
3
- import type { IncludeResolver } from '../model-api/types.js';
4
- import type { AdapterRegistry } from '../plugins/adapter-registry.js';
5
- import type { ExternalFieldConfig } from '../external-model/types.js';
6
- import { resolveModelIncludes } from './model-include-resolver.js';
7
-
8
- export interface CompositeIncludeResolverConfig {
9
- registry: SchemaRegistry;
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- db: Kysely<any> | any;
12
- adapterRegistry?: AdapterRegistry;
13
- externalModelFields?: Record<string, Record<string, ExternalFieldConfig>>;
14
- }
15
-
16
- export class CompositeIncludeResolver implements IncludeResolver {
17
- private readonly registry: SchemaRegistry;
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- private readonly db: Kysely<any>;
20
- private readonly adapterRegistry?: AdapterRegistry;
21
- private readonly externalModelFields?: Record<string, Record<string, ExternalFieldConfig>>;
22
-
23
- constructor(config: CompositeIncludeResolverConfig) {
24
- this.registry = config.registry;
25
- this.db = config.db;
26
- this.adapterRegistry = config.adapterRegistry;
27
- this.externalModelFields = config.externalModelFields;
28
- }
29
-
30
- async resolve(
31
- records: Record<string, unknown>[],
32
- includes: string[],
33
- sourceModel: string,
34
- ): Promise<void> {
35
- await resolveModelIncludes(records, includes, this.registry, this.db, sourceModel, {
36
- adapterRegistry: this.adapterRegistry,
37
- externalModelFields: this.externalModelFields,
38
- });
39
- }
40
- }
package/src/db/index.ts DELETED
@@ -1,23 +0,0 @@
1
- export { DatabaseClient } from './client.js';
2
- export type { DatabaseClientConfig } from './client.js';
3
- export type {
4
- ColumnDefinition,
5
- TableDefinition,
6
- IndexDefinition,
7
- ForeignKeyDefinition,
8
- DesiredState,
9
- ActualState,
10
- DdlOperation,
11
- } from './types.js';
12
- export { mapFieldsToColumns } from './field-mapper.js';
13
- export { SchemaToDesired } from './desired-state.js';
14
- export { DiffEngine } from './diff-engine.js';
15
- export { introspect } from './introspect.js';
16
- export { autoSync } from './auto-sync.js';
17
- export { KyselyModelOps } from './model-ops.js';
18
- export type { KyselyModelOpsConfig } from './model-ops.js';
19
- export { applyModelFilters } from './filter-applier.js';
20
- export { applyScopeEnforcement } from './scope-enforcer.js';
21
- export type { ScopeEnforcementOptions } from './scope-enforcer.js';
22
- export { CompositeIncludeResolver } from './include-resolver.js';
23
- export type { CompositeIncludeResolverConfig } from './include-resolver.js';