@stonyx/orm 0.2.1-beta.82 → 0.2.1-beta.84

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 (150) hide show
  1. package/config/environment.js +17 -0
  2. package/dist/aggregates.d.ts +21 -0
  3. package/dist/aggregates.js +90 -0
  4. package/dist/attr.d.ts +2 -0
  5. package/dist/attr.js +22 -0
  6. package/dist/belongs-to.d.ts +11 -0
  7. package/dist/belongs-to.js +58 -0
  8. package/dist/cli.d.ts +22 -0
  9. package/dist/cli.js +148 -0
  10. package/dist/commands.d.ts +7 -0
  11. package/dist/commands.js +146 -0
  12. package/dist/db.d.ts +21 -0
  13. package/dist/db.js +174 -0
  14. package/dist/exports/db.d.ts +7 -0
  15. package/{src → dist}/exports/db.js +2 -4
  16. package/dist/has-many.d.ts +11 -0
  17. package/dist/has-many.js +57 -0
  18. package/dist/hooks.d.ts +47 -0
  19. package/dist/hooks.js +106 -0
  20. package/dist/index.d.ts +14 -0
  21. package/dist/index.js +34 -0
  22. package/dist/main.d.ts +46 -0
  23. package/dist/main.js +178 -0
  24. package/dist/manage-record.d.ts +13 -0
  25. package/dist/manage-record.js +113 -0
  26. package/dist/meta-request.d.ts +6 -0
  27. package/dist/meta-request.js +52 -0
  28. package/dist/migrate.d.ts +2 -0
  29. package/dist/migrate.js +57 -0
  30. package/dist/model-property.d.ts +9 -0
  31. package/dist/model-property.js +29 -0
  32. package/dist/model.d.ts +15 -0
  33. package/dist/model.js +18 -0
  34. package/dist/mysql/connection.d.ts +14 -0
  35. package/dist/mysql/connection.js +24 -0
  36. package/dist/mysql/migration-generator.d.ts +45 -0
  37. package/dist/mysql/migration-generator.js +245 -0
  38. package/dist/mysql/migration-runner.d.ts +12 -0
  39. package/dist/mysql/migration-runner.js +83 -0
  40. package/dist/mysql/mysql-db.d.ts +100 -0
  41. package/dist/mysql/mysql-db.js +411 -0
  42. package/dist/mysql/query-builder.d.ts +10 -0
  43. package/dist/mysql/query-builder.js +44 -0
  44. package/dist/mysql/schema-introspector.d.ts +19 -0
  45. package/dist/mysql/schema-introspector.js +286 -0
  46. package/dist/mysql/type-map.d.ts +21 -0
  47. package/dist/mysql/type-map.js +36 -0
  48. package/dist/orm-request.d.ts +38 -0
  49. package/dist/orm-request.js +453 -0
  50. package/dist/plural-registry.d.ts +4 -0
  51. package/{src → dist}/plural-registry.js +3 -6
  52. package/dist/postgres/connection.d.ts +15 -0
  53. package/dist/postgres/connection.js +30 -0
  54. package/dist/postgres/migration-generator.d.ts +45 -0
  55. package/dist/postgres/migration-generator.js +257 -0
  56. package/dist/postgres/migration-runner.d.ts +10 -0
  57. package/dist/postgres/migration-runner.js +82 -0
  58. package/dist/postgres/postgres-db.d.ts +119 -0
  59. package/dist/postgres/postgres-db.js +473 -0
  60. package/dist/postgres/query-builder.d.ts +27 -0
  61. package/dist/postgres/query-builder.js +98 -0
  62. package/dist/postgres/schema-introspector.d.ts +29 -0
  63. package/dist/postgres/schema-introspector.js +309 -0
  64. package/dist/postgres/type-map.d.ts +23 -0
  65. package/dist/postgres/type-map.js +53 -0
  66. package/dist/record.d.ts +75 -0
  67. package/dist/record.js +115 -0
  68. package/dist/relationships.d.ts +10 -0
  69. package/dist/relationships.js +35 -0
  70. package/dist/serializer.d.ts +17 -0
  71. package/dist/serializer.js +130 -0
  72. package/dist/setup-rest-server.d.ts +1 -0
  73. package/dist/setup-rest-server.js +54 -0
  74. package/dist/standalone-db.d.ts +58 -0
  75. package/dist/standalone-db.js +142 -0
  76. package/dist/store.d.ts +62 -0
  77. package/dist/store.js +271 -0
  78. package/dist/timescale/query-builder.d.ts +41 -0
  79. package/dist/timescale/query-builder.js +87 -0
  80. package/dist/timescale/timescale-db.d.ts +44 -0
  81. package/dist/timescale/timescale-db.js +81 -0
  82. package/dist/transforms.d.ts +2 -0
  83. package/dist/transforms.js +17 -0
  84. package/dist/types/orm-types.d.ts +142 -0
  85. package/dist/types/orm-types.js +1 -0
  86. package/dist/utils.d.ts +5 -0
  87. package/dist/utils.js +13 -0
  88. package/dist/view-resolver.d.ts +8 -0
  89. package/dist/view-resolver.js +165 -0
  90. package/dist/view.d.ts +11 -0
  91. package/dist/view.js +18 -0
  92. package/package.json +34 -11
  93. package/src/{aggregates.js → aggregates.ts} +27 -13
  94. package/src/{attr.js → attr.ts} +2 -2
  95. package/src/{belongs-to.js → belongs-to.ts} +36 -17
  96. package/src/{cli.js → cli.ts} +17 -11
  97. package/src/{commands.js → commands.ts} +179 -170
  98. package/src/{db.js → db.ts} +35 -26
  99. package/src/exports/db.ts +7 -0
  100. package/src/has-many.ts +91 -0
  101. package/src/{hooks.js → hooks.ts} +23 -27
  102. package/src/{index.js → index.ts} +4 -4
  103. package/src/{main.js → main.ts} +64 -34
  104. package/src/{manage-record.js → manage-record.ts} +41 -22
  105. package/src/{meta-request.js → meta-request.ts} +17 -14
  106. package/src/{migrate.js → migrate.ts} +9 -9
  107. package/src/{model-property.js → model-property.ts} +12 -6
  108. package/src/{model.js → model.ts} +5 -4
  109. package/src/mysql/{connection.js → connection.ts} +43 -28
  110. package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
  111. package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
  112. package/src/mysql/{mysql-db.js → mysql-db.ts} +533 -473
  113. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  114. package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
  115. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  116. package/src/{orm-request.js → orm-request.ts} +165 -95
  117. package/src/plural-registry.ts +12 -0
  118. package/src/postgres/connection.ts +46 -0
  119. package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
  120. package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
  121. package/src/postgres/{postgres-db.js → postgres-db.ts} +199 -111
  122. package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
  123. package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
  124. package/src/postgres/{type-map.js → type-map.ts} +10 -6
  125. package/src/{record.js → record.ts} +73 -34
  126. package/src/relationships.ts +48 -0
  127. package/src/{serializer.js → serializer.ts} +44 -36
  128. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
  129. package/src/{standalone-db.js → standalone-db.ts} +33 -24
  130. package/src/{store.js → store.ts} +90 -68
  131. package/src/timescale/query-builder.ts +137 -0
  132. package/src/timescale/timescale-db.ts +107 -0
  133. package/src/transforms.ts +20 -0
  134. package/src/types/mysql2.d.ts +30 -0
  135. package/src/types/orm-types.ts +146 -0
  136. package/src/types/pg.d.ts +28 -0
  137. package/src/types/stonyx-cron.d.ts +5 -0
  138. package/src/types/stonyx-events.d.ts +4 -0
  139. package/src/types/stonyx-rest-server.d.ts +11 -0
  140. package/src/types/stonyx-utils.d.ts +33 -0
  141. package/src/types/stonyx.d.ts +21 -0
  142. package/src/utils.ts +16 -0
  143. package/src/{view-resolver.js → view-resolver.ts} +53 -28
  144. package/src/view.ts +22 -0
  145. package/src/has-many.js +0 -68
  146. package/src/postgres/connection.js +0 -30
  147. package/src/relationships.js +0 -43
  148. package/src/transforms.js +0 -20
  149. package/src/utils.js +0 -12
  150. package/src/view.js +0 -21
@@ -1,325 +1,355 @@
1
- import Orm from '@stonyx/orm';
2
- import { getMysqlType } from './type-map.js';
3
- import { camelCaseToKebabCase } from '@stonyx/utils/string';
4
- import { getPluralName } from '../plural-registry.js';
5
- import { dbKey } from '../db.js';
6
- import { AggregateProperty } from '../aggregates.js';
7
-
8
- function getRelationshipInfo(property) {
9
- if (typeof property !== 'function') return null;
10
- const fnStr = property.toString();
11
- const modelName = property.__relatedModelName || null;
12
-
13
- if (fnStr.includes(`getRelationships('belongsTo',`)) return { type: 'belongsTo', modelName };
14
- if (fnStr.includes(`getRelationships('hasMany',`)) return { type: 'hasMany', modelName };
15
-
16
- return null;
17
- }
18
-
19
- function sanitizeTableName(name) {
20
- return name.replace(/[-/]/g, '_');
21
- }
22
-
23
- export function introspectModels() {
24
- const { models } = Orm.instance;
25
- const schemas = {};
26
-
27
- for (const [modelKey, modelClass] of Object.entries(models)) {
28
- const name = camelCaseToKebabCase(modelKey.slice(0, -5));
29
-
30
- if (name === dbKey) continue;
31
-
32
- const model = new modelClass(modelKey);
33
- const columns = {};
34
- const foreignKeys = {};
35
- const relationships = { belongsTo: {}, hasMany: {} };
36
- let idType = 'number';
37
-
38
- const transforms = Orm.instance.transforms;
39
-
40
- for (const [key, property] of Object.entries(model)) {
41
- if (key.startsWith('__')) continue;
42
-
43
- const relInfo = getRelationshipInfo(property);
44
-
45
- if (relInfo?.type === 'belongsTo') {
46
- relationships.belongsTo[key] = relInfo.modelName;
47
- } else if (relInfo?.type === 'hasMany') {
48
- relationships.hasMany[key] = relInfo.modelName;
49
- } else if (property?.constructor?.name === 'ModelProperty') {
50
- if (key === 'id') {
51
- idType = property.type;
52
- } else {
53
- columns[key] = getMysqlType(property.type, transforms[property.type]);
54
- }
55
- }
56
- }
57
-
58
- // Build foreign keys from belongsTo relationships
59
- for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
60
- const fkColumn = `${relName}_id`;
61
- foreignKeys[fkColumn] = {
62
- references: sanitizeTableName(getPluralName(targetModelName)),
63
- column: 'id',
64
- };
65
- }
66
-
67
- schemas[name] = {
68
- table: sanitizeTableName(getPluralName(name)),
69
- idType,
70
- columns,
71
- foreignKeys,
72
- relationships,
73
- memory: modelClass.memory === true,
74
- };
75
- }
76
-
77
- return schemas;
78
- }
79
-
80
- export function buildTableDDL(name, schema, allSchemas = {}) {
81
- const { idType, columns, foreignKeys } = schema;
82
- const table = sanitizeTableName(schema.table);
83
- const lines = [];
84
-
85
- // Primary key
86
- if (idType === 'string') {
87
- lines.push(' `id` VARCHAR(255) PRIMARY KEY');
88
- } else {
89
- lines.push(' `id` INT AUTO_INCREMENT PRIMARY KEY');
90
- }
91
-
92
- // Attribute columns
93
- for (const [col, mysqlType] of Object.entries(columns)) {
94
- lines.push(` \`${col}\` ${mysqlType}`);
95
- }
96
-
97
- // Foreign key columns
98
- for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
99
- const refIdType = getReferencedIdType(fkDef.references, allSchemas);
100
- lines.push(` \`${fkCol}\` ${refIdType}`);
101
- }
102
-
103
- // Timestamps
104
- lines.push(' `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP');
105
- lines.push(' `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
106
-
107
- // Foreign key constraints
108
- for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
109
- const refTable = sanitizeTableName(fkDef.references);
110
- lines.push(` FOREIGN KEY (\`${fkCol}\`) REFERENCES \`${refTable}\`(\`${fkDef.column}\`) ON DELETE SET NULL`);
111
- }
112
-
113
- return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${lines.join(',\n')}\n)`;
114
- }
115
-
116
- function getReferencedIdType(tableName, allSchemas) {
117
- // Look up the referenced table's PK type from schemas
118
- for (const schema of Object.values(allSchemas)) {
119
- if (schema.table === tableName) {
120
- return schema.idType === 'string' ? 'VARCHAR(255)' : 'INT';
121
- }
122
- }
123
-
124
- // Default to INT if referenced table not found in schemas
125
- return 'INT';
126
- }
127
-
128
- export function getTopologicalOrder(schemas) {
129
- const visited = new Set();
130
- const order = [];
131
-
132
- function visit(name) {
133
- if (visited.has(name)) return;
134
- visited.add(name);
135
-
136
- const schema = schemas[name];
137
- if (!schema) return;
138
-
139
- // Visit dependencies (belongsTo targets) first
140
- for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
141
- visit(targetModelName);
142
- }
143
-
144
- order.push(name);
145
- }
146
-
147
- for (const name of Object.keys(schemas)) {
148
- visit(name);
149
- }
150
-
151
- return order;
152
- }
153
-
154
- export function introspectViews() {
155
- const orm = Orm.instance;
156
- if (!orm.views) return {};
157
-
158
- const schemas = {};
159
-
160
- for (const [viewKey, viewClass] of Object.entries(orm.views)) {
161
- const name = camelCaseToKebabCase(viewKey.slice(0, -4)); // Remove 'View' suffix
162
-
163
- const source = viewClass.source;
164
- if (!source) continue;
165
-
166
- const model = new viewClass(name);
167
- const columns = {};
168
- const foreignKeys = {};
169
- const aggregates = {};
170
- const relationships = { belongsTo: {}, hasMany: {} };
171
-
172
- for (const [key, property] of Object.entries(model)) {
173
- if (key.startsWith('__')) continue;
174
- if (key === 'id') continue;
175
-
176
- if (property instanceof AggregateProperty) {
177
- aggregates[key] = property;
178
- continue;
179
- }
180
-
181
- const relInfo = getRelationshipInfo(property);
182
-
183
- if (relInfo?.type === 'belongsTo') {
184
- relationships.belongsTo[key] = relInfo.modelName;
185
- const fkColumn = `${key}_id`;
186
- foreignKeys[fkColumn] = {
187
- references: sanitizeTableName(getPluralName(relInfo.modelName)),
188
- column: 'id',
189
- };
190
- } else if (relInfo?.type === 'hasMany') {
191
- relationships.hasMany[key] = relInfo.modelName;
192
- } else if (property?.constructor?.name === 'ModelProperty') {
193
- const transforms = Orm.instance.transforms;
194
- columns[key] = getMysqlType(property.type, transforms[property.type]);
195
- }
196
- }
197
-
198
- schemas[name] = {
199
- viewName: sanitizeTableName(getPluralName(name)),
200
- source,
201
- groupBy: viewClass.groupBy || undefined,
202
- columns,
203
- foreignKeys,
204
- aggregates,
205
- relationships,
206
- isView: true,
207
- memory: viewClass.memory !== false ? false : false, // Views default to memory:false
208
- };
209
- }
210
-
211
- return schemas;
212
- }
213
-
214
- export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
215
- if (!viewSchema.source) {
216
- throw new Error(`View '${name}' must define a source model`);
217
- }
218
-
219
- const sourceModelName = viewSchema.source;
220
- const sourceSchema = modelSchemas[sourceModelName];
221
- const sourceTable = sanitizeTableName(sourceSchema
222
- ? sourceSchema.table
223
- : getPluralName(sourceModelName));
224
-
225
- const selectColumns = [];
226
- const joins = [];
227
- const hasAggregates = Object.keys(viewSchema.aggregates || {}).length > 0;
228
- const groupByField = viewSchema.groupBy;
229
-
230
- // ID column: groupBy field or source table PK
231
- if (groupByField) {
232
- selectColumns.push(`\`${sourceTable}\`.\`${groupByField}\` AS \`id\``);
233
- } else {
234
- selectColumns.push(`\`${sourceTable}\`.\`id\` AS \`id\``);
235
- }
236
-
237
- // Aggregate columns
238
- for (const [key, aggProp] of Object.entries(viewSchema.aggregates || {})) {
239
- if (aggProp.relationship === undefined) {
240
- // Field-level aggregate (groupBy views)
241
- if (aggProp.aggregateType === 'count') {
242
- selectColumns.push(`COUNT(*) AS \`${key}\``);
243
- } else {
244
- selectColumns.push(`${aggProp.mysqlFunction}(\`${sourceTable}\`.\`${aggProp.field}\`) AS \`${key}\``);
245
- }
246
- } else {
247
- // Relationship aggregate
248
- const relName = aggProp.relationship;
249
- const relTable = sanitizeTableName(getPluralName(relName));
250
-
251
- if (aggProp.aggregateType === 'count') {
252
- selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`id\`) AS \`${key}\``);
253
- } else {
254
- const field = aggProp.field;
255
- selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`${field}\`) AS \`${key}\``);
256
- }
257
-
258
- // Add LEFT JOIN for the relationship if not already added
259
- const joinKey = `${relTable}`;
260
- if (!joins.find(j => j.table === joinKey)) {
261
- const fkColumn = `${sourceModelName}_id`;
262
- joins.push({
263
- table: relTable,
264
- condition: `\`${relTable}\`.\`${fkColumn}\` = \`${sourceTable}\`.\`id\``
265
- });
266
- }
267
- }
268
- }
269
-
270
- // Regular columns (from resolve map string paths or direct attr fields)
271
- for (const [key, mysqlType] of Object.entries(viewSchema.columns || {})) {
272
- selectColumns.push(`\`${sourceTable}\`.\`${key}\` AS \`${key}\``);
273
- }
274
-
275
- // Build JOIN clauses
276
- const joinClauses = joins.map(j =>
277
- `LEFT JOIN \`${j.table}\` ON ${j.condition}`
278
- ).join('\n ');
279
-
280
- // Build GROUP BY
281
- let groupBy = '';
282
- if (groupByField) {
283
- groupBy = `\nGROUP BY \`${sourceTable}\`.\`${groupByField}\``;
284
- } else if (hasAggregates) {
285
- groupBy = `\nGROUP BY \`${sourceTable}\`.\`id\``;
286
- }
287
-
288
- const viewName = sanitizeTableName(viewSchema.viewName);
289
- const sql = `CREATE OR REPLACE VIEW \`${viewName}\` AS\nSELECT\n ${selectColumns.join(',\n ')}\nFROM \`${sourceTable}\`${joinClauses ? '\n ' + joinClauses : ''}${groupBy}`;
290
-
291
- return sql;
292
- }
293
-
294
- export function viewSchemasToSnapshot(viewSchemas) {
295
- const snapshot = {};
296
-
297
- for (const [name, schema] of Object.entries(viewSchemas)) {
298
- snapshot[name] = {
299
- viewName: schema.viewName,
300
- source: schema.source,
301
- ...(schema.groupBy ? { groupBy: schema.groupBy } : {}),
302
- columns: { ...schema.columns },
303
- foreignKeys: { ...schema.foreignKeys },
304
- isView: true,
305
- viewQuery: buildViewDDL(name, schema),
306
- };
307
- }
308
-
309
- return snapshot;
310
- }
311
-
312
- export function schemasToSnapshot(schemas) {
313
- const snapshot = {};
314
-
315
- for (const [name, schema] of Object.entries(schemas)) {
316
- snapshot[name] = {
317
- table: schema.table,
318
- idType: schema.idType,
319
- columns: { ...schema.columns },
320
- foreignKeys: { ...schema.foreignKeys },
321
- };
322
- }
323
-
324
- return snapshot;
325
- }
1
+ import Orm from '@stonyx/orm';
2
+ import { getMysqlType } from './type-map.js';
3
+ import { camelCaseToKebabCase } from '@stonyx/utils/string';
4
+ import { getPluralName } from '../plural-registry.js';
5
+ import { dbKey } from '../db.js';
6
+ import { AggregateProperty } from '../aggregates.js';
7
+ import type { ForeignKeyDef, ModelSchema, ViewSchema, SnapshotEntry } from '../types/orm-types.js';
8
+ import ModelProperty from '../model-property.js';
9
+
10
+ interface RelationshipInfo {
11
+ type: 'belongsTo' | 'hasMany';
12
+ modelName: string | null;
13
+ }
14
+
15
+ interface JoinClause {
16
+ table: string;
17
+ condition: string;
18
+ }
19
+
20
+ interface RelationshipProperty {
21
+ __relatedModelName?: string | null;
22
+ __relationshipType?: string;
23
+ }
24
+
25
+ function getRelationshipInfo(property: unknown): RelationshipInfo | null {
26
+ if (typeof property !== 'function') return null;
27
+ const relType = (property as RelationshipProperty).__relationshipType;
28
+ const modelName = (property as RelationshipProperty).__relatedModelName || null;
29
+
30
+ if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
31
+ if (relType === 'hasMany') return { type: 'hasMany', modelName };
32
+
33
+ return null;
34
+ }
35
+
36
+ function sanitizeTableName(name: string): string {
37
+ return name.replace(/[-/]/g, '_');
38
+ }
39
+
40
+ export function introspectModels(): Record<string, ModelSchema> {
41
+ const { models } = (Orm as unknown as { instance: { models: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
42
+ const schemas: Record<string, ModelSchema> = {};
43
+
44
+ for (const [modelKey, modelClass] of Object.entries(models)) {
45
+ const name = camelCaseToKebabCase(modelKey.slice(0, -5));
46
+
47
+ if (name === dbKey) continue;
48
+
49
+ const model = new (modelClass as new (key: string) => Record<string, unknown>)(modelKey);
50
+ const columns: Record<string, string> = {};
51
+ const foreignKeys: Record<string, ForeignKeyDef> = {};
52
+ const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
53
+ let idType = 'number';
54
+
55
+ const transforms = (Orm as unknown as { instance: { transforms: Record<string, unknown> } }).instance.transforms;
56
+
57
+ for (const [key, property] of Object.entries(model)) {
58
+ if (key.startsWith('__')) continue;
59
+
60
+ const relInfo = getRelationshipInfo(property);
61
+
62
+ if (relInfo?.type === 'belongsTo') {
63
+ relationships.belongsTo[key] = relInfo.modelName;
64
+ } else if (relInfo?.type === 'hasMany') {
65
+ relationships.hasMany[key] = relInfo.modelName;
66
+ } else if (property instanceof ModelProperty) {
67
+ if (key === 'id') {
68
+ idType = (property as ModelProperty).type;
69
+ } else {
70
+ columns[key] = getMysqlType((property as ModelProperty).type, transforms[(property as ModelProperty).type] as ((...args: unknown[]) => unknown) & { mysqlType?: string });
71
+ }
72
+ }
73
+ }
74
+
75
+ // Build foreign keys from belongsTo relationships
76
+ for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
77
+ const fkColumn = `${relName}_id`;
78
+ foreignKeys[fkColumn] = {
79
+ references: sanitizeTableName(getPluralName(targetModelName!)),
80
+ column: 'id',
81
+ };
82
+ }
83
+
84
+ schemas[name] = {
85
+ table: sanitizeTableName(getPluralName(name)),
86
+ idType,
87
+ columns,
88
+ foreignKeys,
89
+ relationships,
90
+ memory: (modelClass as { memory?: boolean }).memory === true,
91
+ };
92
+ }
93
+
94
+ return schemas;
95
+ }
96
+
97
+ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Record<string, ModelSchema> = {}): string {
98
+ const { idType, columns, foreignKeys } = schema;
99
+ const table = sanitizeTableName(schema.table);
100
+ const lines: string[] = [];
101
+
102
+ // Primary key
103
+ if (idType === 'string') {
104
+ lines.push(' `id` VARCHAR(255) PRIMARY KEY');
105
+ } else {
106
+ lines.push(' `id` INT AUTO_INCREMENT PRIMARY KEY');
107
+ }
108
+
109
+ // Attribute columns
110
+ for (const [col, mysqlType] of Object.entries(columns)) {
111
+ lines.push(` \`${col}\` ${mysqlType}`);
112
+ }
113
+
114
+ // Foreign key columns
115
+ for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
116
+ const refIdType = getReferencedIdType(fkDef.references, allSchemas);
117
+ lines.push(` \`${fkCol}\` ${refIdType}`);
118
+ }
119
+
120
+ // Timestamps
121
+ lines.push(' `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP');
122
+ lines.push(' `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
123
+
124
+ // Foreign key constraints
125
+ for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
126
+ const refTable = sanitizeTableName(fkDef.references);
127
+ lines.push(` FOREIGN KEY (\`${fkCol}\`) REFERENCES \`${refTable}\`(\`${fkDef.column}\`) ON DELETE SET NULL`);
128
+ }
129
+
130
+ return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${lines.join(',\n')}\n)`;
131
+ }
132
+
133
+ function getReferencedIdType(tableName: string, allSchemas: Record<string, ModelSchema>): string {
134
+ // Look up the referenced table's PK type from schemas
135
+ for (const schema of Object.values(allSchemas)) {
136
+ if (schema.table === tableName) {
137
+ return schema.idType === 'string' ? 'VARCHAR(255)' : 'INT';
138
+ }
139
+ }
140
+
141
+ // Default to INT if referenced table not found in schemas
142
+ return 'INT';
143
+ }
144
+
145
+ export function getTopologicalOrder(schemas: Record<string, ModelSchema>): string[] {
146
+ const visited = new Set<string>();
147
+ const order: string[] = [];
148
+
149
+ function visit(name: string): void {
150
+ if (visited.has(name)) return;
151
+ visited.add(name);
152
+
153
+ const schema = schemas[name];
154
+ if (!schema) return;
155
+
156
+ // Visit dependencies (belongsTo targets) first
157
+ for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
158
+ visit(targetModelName!);
159
+ }
160
+
161
+ order.push(name);
162
+ }
163
+
164
+ for (const name of Object.keys(schemas)) {
165
+ visit(name);
166
+ }
167
+
168
+ return order;
169
+ }
170
+
171
+ export function introspectViews(): Record<string, ViewSchema> {
172
+ const orm = (Orm as unknown as { instance: { views?: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
173
+ if (!orm.views) return {};
174
+
175
+ const schemas: Record<string, ViewSchema> = {};
176
+
177
+ for (const [viewKey, viewClass] of Object.entries(orm.views)) {
178
+ const name = camelCaseToKebabCase(viewKey.slice(0, -4)); // Remove 'View' suffix
179
+
180
+ const source = (viewClass as { source?: string }).source;
181
+ if (!source) continue;
182
+
183
+ const model = new (viewClass as new (name: string) => Record<string, unknown>)(name);
184
+ const columns: Record<string, string> = {};
185
+ const foreignKeys: Record<string, ForeignKeyDef> = {};
186
+ const aggregates: Record<string, AggregateProperty> = {};
187
+ const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
188
+
189
+ for (const [key, property] of Object.entries(model)) {
190
+ if (key.startsWith('__')) continue;
191
+ if (key === 'id') continue;
192
+
193
+ if (property instanceof AggregateProperty) {
194
+ aggregates[key] = property;
195
+ continue;
196
+ }
197
+
198
+ const relInfo = getRelationshipInfo(property);
199
+
200
+ if (relInfo?.type === 'belongsTo') {
201
+ relationships.belongsTo[key] = relInfo.modelName;
202
+ const fkColumn = `${key}_id`;
203
+ foreignKeys[fkColumn] = {
204
+ references: sanitizeTableName(getPluralName(relInfo.modelName!)),
205
+ column: 'id',
206
+ };
207
+ } else if (relInfo?.type === 'hasMany') {
208
+ relationships.hasMany[key] = relInfo.modelName;
209
+ } else if (property instanceof ModelProperty) {
210
+ const transforms = orm.transforms;
211
+ columns[key] = getMysqlType((property as ModelProperty).type, transforms[(property as ModelProperty).type] as ((...args: unknown[]) => unknown) & { mysqlType?: string });
212
+ }
213
+ }
214
+
215
+ schemas[name] = {
216
+ viewName: sanitizeTableName(getPluralName(name)),
217
+ source,
218
+ groupBy: (viewClass as { groupBy?: string }).groupBy || undefined,
219
+ columns,
220
+ foreignKeys,
221
+ aggregates,
222
+ relationships,
223
+ isView: true,
224
+ memory: false,
225
+ };
226
+ }
227
+
228
+ return schemas;
229
+ }
230
+
231
+ export function buildViewDDL(name: string, viewSchema: ViewSchema, modelSchemas: Record<string, ModelSchema> = {}): string {
232
+ if (!viewSchema.source) {
233
+ throw new Error(`View '${name}' must define a source model`);
234
+ }
235
+
236
+ const sourceModelName = viewSchema.source;
237
+ const sourceSchema = modelSchemas[sourceModelName];
238
+ const sourceTable = sanitizeTableName(sourceSchema
239
+ ? sourceSchema.table
240
+ : getPluralName(sourceModelName));
241
+
242
+ const selectColumns: string[] = [];
243
+ const joins: JoinClause[] = [];
244
+ const hasAggregates = Object.keys(viewSchema.aggregates || {}).length > 0;
245
+ const groupByField = viewSchema.groupBy;
246
+
247
+ // ID column: groupBy field or source table PK
248
+ if (groupByField) {
249
+ selectColumns.push(`\`${sourceTable}\`.\`${groupByField}\` AS \`id\``);
250
+ } else {
251
+ selectColumns.push(`\`${sourceTable}\`.\`id\` AS \`id\``);
252
+ }
253
+
254
+ // Aggregate columns
255
+ for (const [key, aggProp] of Object.entries(viewSchema.aggregates || {})) {
256
+ if (aggProp.relationship === undefined) {
257
+ // Field-level aggregate (groupBy views)
258
+ if (aggProp.aggregateType === 'count') {
259
+ selectColumns.push(`COUNT(*) AS \`${key}\``);
260
+ } else {
261
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${sourceTable}\`.\`${aggProp.field}\`) AS \`${key}\``);
262
+ }
263
+ } else {
264
+ // Relationship aggregate
265
+ const relName = aggProp.relationship;
266
+ const relTable = sanitizeTableName(getPluralName(relName));
267
+
268
+ if (aggProp.aggregateType === 'count') {
269
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`id\`) AS \`${key}\``);
270
+ } else {
271
+ const field = aggProp.field;
272
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`${field}\`) AS \`${key}\``);
273
+ }
274
+
275
+ // Add LEFT JOIN for the relationship if not already added
276
+ const joinKey = `${relTable}`;
277
+ if (!joins.find(j => j.table === joinKey)) {
278
+ const fkColumn = `${sourceModelName}_id`;
279
+ joins.push({
280
+ table: relTable,
281
+ condition: `\`${relTable}\`.\`${fkColumn}\` = \`${sourceTable}\`.\`id\``
282
+ });
283
+ }
284
+ }
285
+ }
286
+
287
+ // Regular columns (from resolve map string paths or direct attr fields)
288
+ for (const [key] of Object.entries(viewSchema.columns || {})) {
289
+ selectColumns.push(`\`${sourceTable}\`.\`${key}\` AS \`${key}\``);
290
+ }
291
+
292
+ // Build JOIN clauses
293
+ const joinClauses = joins.map(j =>
294
+ `LEFT JOIN \`${j.table}\` ON ${j.condition}`
295
+ ).join('\n ');
296
+
297
+ // Build GROUP BY
298
+ let groupBy = '';
299
+ if (groupByField) {
300
+ groupBy = `\nGROUP BY \`${sourceTable}\`.\`${groupByField}\``;
301
+ } else if (hasAggregates) {
302
+ groupBy = `\nGROUP BY \`${sourceTable}\`.\`id\``;
303
+ }
304
+
305
+ const viewName = sanitizeTableName(viewSchema.viewName);
306
+ const sql = `CREATE OR REPLACE VIEW \`${viewName}\` AS\nSELECT\n ${selectColumns.join(',\n ')}\nFROM \`${sourceTable}\`${joinClauses ? '\n ' + joinClauses : ''}${groupBy}`;
307
+
308
+ return sql;
309
+ }
310
+
311
+ export function viewSchemasToSnapshot(viewSchemas: Record<string, ViewSchema>): Record<string, ViewSnapshotEntry> {
312
+ const snapshot: Record<string, ViewSnapshotEntry> = {};
313
+
314
+ for (const [name, schema] of Object.entries(viewSchemas)) {
315
+ snapshot[name] = {
316
+ viewName: schema.viewName,
317
+ source: schema.source,
318
+ ...(schema.groupBy ? { groupBy: schema.groupBy } : {}),
319
+ columns: { ...schema.columns },
320
+ foreignKeys: { ...schema.foreignKeys },
321
+ isView: true,
322
+ viewQuery: buildViewDDL(name, schema),
323
+ };
324
+ }
325
+
326
+ return snapshot;
327
+ }
328
+
329
+ interface ViewSnapshotEntry {
330
+ viewName: string;
331
+ source: string;
332
+ groupBy?: string;
333
+ columns: Record<string, string>;
334
+ foreignKeys: Record<string, ForeignKeyDef>;
335
+ isView: true;
336
+ viewQuery: string;
337
+ }
338
+
339
+ export function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<string, SnapshotEntry> {
340
+ const snapshot: Record<string, SnapshotEntry> = {};
341
+
342
+ for (const [name, schema] of Object.entries(schemas)) {
343
+ snapshot[name] = {
344
+ table: schema.table,
345
+ idType: schema.idType,
346
+ columns: { ...schema.columns },
347
+ foreignKeys: { ...schema.foreignKeys },
348
+ };
349
+ }
350
+
351
+ return snapshot;
352
+ }
353
+
354
+ export type { ModelSchema, ViewSchema, ForeignKeyDef, SnapshotEntry } from '../types/orm-types.js';
355
+ export type { ViewSnapshotEntry };