@stonyx/orm 0.2.5-alpha.0 → 0.3.1

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 (166) hide show
  1. package/README.md +482 -15
  2. package/config/environment.js +63 -6
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +75 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +257 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +475 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +280 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +296 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/schema-helpers.d.ts +20 -0
  72. package/dist/schema-helpers.js +48 -0
  73. package/dist/serializer.d.ts +17 -0
  74. package/dist/serializer.js +136 -0
  75. package/dist/setup-rest-server.d.ts +1 -0
  76. package/dist/setup-rest-server.js +52 -0
  77. package/dist/standalone-db.d.ts +58 -0
  78. package/dist/standalone-db.js +142 -0
  79. package/dist/store.d.ts +62 -0
  80. package/dist/store.js +286 -0
  81. package/dist/timescale/query-builder.d.ts +43 -0
  82. package/dist/timescale/query-builder.js +115 -0
  83. package/dist/timescale/timescale-db.d.ts +45 -0
  84. package/dist/timescale/timescale-db.js +84 -0
  85. package/dist/transforms.d.ts +2 -0
  86. package/dist/transforms.js +17 -0
  87. package/dist/types/orm-types.d.ts +153 -0
  88. package/dist/types/orm-types.js +1 -0
  89. package/dist/utils.d.ts +7 -0
  90. package/dist/utils.js +17 -0
  91. package/dist/view-resolver.d.ts +8 -0
  92. package/dist/view-resolver.js +171 -0
  93. package/dist/view.d.ts +11 -0
  94. package/dist/view.js +18 -0
  95. package/package.json +64 -11
  96. package/src/aggregates.ts +109 -0
  97. package/src/{attr.js → attr.ts} +2 -2
  98. package/src/belongs-to.ts +90 -0
  99. package/src/cli.ts +183 -0
  100. package/src/commands.ts +179 -0
  101. package/src/db.ts +232 -0
  102. package/src/exports/db.ts +7 -0
  103. package/src/has-many.ts +92 -0
  104. package/src/hooks.ts +151 -0
  105. package/src/{index.js → index.ts} +12 -2
  106. package/src/main.ts +229 -0
  107. package/src/manage-record.ts +161 -0
  108. package/src/{meta-request.js → meta-request.ts} +17 -14
  109. package/src/migrate.ts +72 -0
  110. package/src/model-property.ts +35 -0
  111. package/src/model.ts +21 -0
  112. package/src/mysql/connection.ts +43 -0
  113. package/src/mysql/migration-generator.ts +337 -0
  114. package/src/mysql/migration-runner.ts +121 -0
  115. package/src/mysql/mysql-db.ts +543 -0
  116. package/src/mysql/query-builder.ts +69 -0
  117. package/src/mysql/schema-introspector.ts +310 -0
  118. package/src/mysql/type-map.ts +42 -0
  119. package/src/orm-request.ts +582 -0
  120. package/src/plural-registry.ts +12 -0
  121. package/src/postgres/connection.ts +48 -0
  122. package/src/postgres/migration-generator.ts +370 -0
  123. package/src/postgres/migration-runner.ts +115 -0
  124. package/src/postgres/postgres-db.ts +616 -0
  125. package/src/postgres/query-builder.ts +148 -0
  126. package/src/postgres/schema-introspector.ts +360 -0
  127. package/src/postgres/type-map.ts +61 -0
  128. package/src/record.ts +186 -0
  129. package/src/relationships.ts +54 -0
  130. package/src/schema-helpers.ts +59 -0
  131. package/src/serializer.ts +161 -0
  132. package/src/setup-rest-server.ts +62 -0
  133. package/src/standalone-db.ts +185 -0
  134. package/src/store.ts +373 -0
  135. package/src/timescale/query-builder.ts +174 -0
  136. package/src/timescale/timescale-db.ts +119 -0
  137. package/src/transforms.ts +20 -0
  138. package/src/types/mysql2.d.ts +49 -0
  139. package/src/types/orm-types.ts +158 -0
  140. package/src/types/pg.d.ts +32 -0
  141. package/src/types/stonyx-cron.d.ts +5 -0
  142. package/src/types/stonyx-events.d.ts +4 -0
  143. package/src/types/stonyx-rest-server.d.ts +16 -0
  144. package/src/types/stonyx-utils.d.ts +33 -0
  145. package/src/types/stonyx.d.ts +21 -0
  146. package/src/utils.ts +22 -0
  147. package/src/view-resolver.ts +211 -0
  148. package/src/view.ts +22 -0
  149. package/.claude/project-structure.md +0 -578
  150. package/.github/workflows/ci.yml +0 -36
  151. package/.github/workflows/publish.yml +0 -143
  152. package/src/belongs-to.js +0 -63
  153. package/src/db.js +0 -80
  154. package/src/has-many.js +0 -61
  155. package/src/main.js +0 -119
  156. package/src/manage-record.js +0 -103
  157. package/src/model-property.js +0 -29
  158. package/src/model.js +0 -9
  159. package/src/orm-request.js +0 -249
  160. package/src/record.js +0 -100
  161. package/src/relationships.js +0 -43
  162. package/src/serializer.js +0 -138
  163. package/src/setup-rest-server.js +0 -57
  164. package/src/store.js +0 -211
  165. package/src/transforms.js +0 -20
  166. package/stonyx-bootstrap.cjs +0 -30
@@ -0,0 +1,69 @@
1
+ interface QueryResult {
2
+ sql: string;
3
+ values: unknown[];
4
+ }
5
+
6
+ const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
7
+
8
+ export function validateIdentifier(name: string, context: string = 'identifier'): string {
9
+ if (!name || typeof name !== 'string' || !SAFE_IDENTIFIER.test(name)) {
10
+ throw new Error(`Invalid SQL ${context}: "${name}". Identifiers must match ${SAFE_IDENTIFIER}`);
11
+ }
12
+
13
+ return name;
14
+ }
15
+
16
+ export function buildInsert(table: string, data: Record<string, unknown>): QueryResult {
17
+ validateIdentifier(table, 'table name');
18
+
19
+ const keys = Object.keys(data);
20
+ keys.forEach(k => validateIdentifier(k, 'column name'));
21
+
22
+ const placeholders = keys.map(() => '?');
23
+ const values = keys.map(k => data[k]);
24
+
25
+ const sql = `INSERT INTO \`${table}\` (${keys.map(k => `\`${k}\``).join(', ')}) VALUES (${placeholders.join(', ')})`;
26
+
27
+ return { sql, values };
28
+ }
29
+
30
+ export function buildUpdate(table: string, id: unknown, data: Record<string, unknown>): QueryResult {
31
+ validateIdentifier(table, 'table name');
32
+
33
+ const keys = Object.keys(data);
34
+ keys.forEach(k => validateIdentifier(k, 'column name'));
35
+
36
+ const setClauses = keys.map(k => `\`${k}\` = ?`);
37
+ const values = [...keys.map(k => data[k]), id];
38
+
39
+ const sql = `UPDATE \`${table}\` SET ${setClauses.join(', ')} WHERE \`id\` = ?`;
40
+
41
+ return { sql, values };
42
+ }
43
+
44
+ export function buildDelete(table: string, id: unknown): QueryResult {
45
+ validateIdentifier(table, 'table name');
46
+
47
+ return {
48
+ sql: `DELETE FROM \`${table}\` WHERE \`id\` = ?`,
49
+ values: [id],
50
+ };
51
+ }
52
+
53
+ export function buildSelect(table: string, conditions?: Record<string, unknown>): QueryResult {
54
+ validateIdentifier(table, 'table name');
55
+
56
+ if (!conditions || Object.keys(conditions).length === 0) {
57
+ return { sql: `SELECT * FROM \`${table}\``, values: [] };
58
+ }
59
+
60
+ const keys = Object.keys(conditions);
61
+ keys.forEach(k => validateIdentifier(k, 'column name'));
62
+
63
+ const whereClauses = keys.map(k => `\`${k}\` = ?`);
64
+ const values = keys.map(k => conditions[k]);
65
+
66
+ const sql = `SELECT * FROM \`${table}\` WHERE ${whereClauses.join(' AND ')}`;
67
+
68
+ return { sql, values };
69
+ }
@@ -0,0 +1,310 @@
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 { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
8
+ import type { ForeignKeyDef, ModelSchema, ViewSchema, SnapshotEntry } from '../types/orm-types.js';
9
+ import ModelProperty from '../model-property.js';
10
+
11
+ interface JoinClause {
12
+ table: string;
13
+ condition: string;
14
+ }
15
+
16
+ export function introspectModels(): Record<string, ModelSchema> {
17
+ const { models } = (Orm as unknown as { instance: { models: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
18
+ const schemas: Record<string, ModelSchema> = {};
19
+
20
+ for (const [modelKey, modelClass] of Object.entries(models)) {
21
+ const name = camelCaseToKebabCase(modelKey.slice(0, -5));
22
+
23
+ if (name === dbKey) continue;
24
+
25
+ const model = new (modelClass as new (key: string) => Record<string, unknown>)(modelKey);
26
+ const columns: Record<string, string> = {};
27
+ const foreignKeys: Record<string, ForeignKeyDef> = {};
28
+ const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
29
+ let idType = 'number';
30
+
31
+ const transforms = (Orm as unknown as { instance: { transforms: Record<string, unknown> } }).instance.transforms;
32
+
33
+ for (const [key, property] of Object.entries(model)) {
34
+ if (key.startsWith('__')) continue;
35
+
36
+ const relInfo = getRelationshipInfo(property);
37
+
38
+ if (relInfo?.type === 'belongsTo') {
39
+ relationships.belongsTo[key] = relInfo.modelName;
40
+ } else if (relInfo?.type === 'hasMany') {
41
+ relationships.hasMany[key] = relInfo.modelName;
42
+ } else if (property instanceof ModelProperty) {
43
+ if (key === 'id') {
44
+ idType = (property as ModelProperty).type;
45
+ } else {
46
+ columns[key] = getMysqlType((property as ModelProperty).type, transforms[(property as ModelProperty).type] as ((...args: unknown[]) => unknown) & { mysqlType?: string });
47
+ }
48
+ }
49
+ }
50
+
51
+ // Build foreign keys from belongsTo relationships
52
+ for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
53
+ if (!targetModelName) continue;
54
+ const fkColumn = `${relName}_id`;
55
+ foreignKeys[fkColumn] = {
56
+ references: sanitizeTableName(getPluralName(targetModelName)),
57
+ column: 'id',
58
+ };
59
+ }
60
+
61
+ schemas[name] = {
62
+ table: sanitizeTableName(getPluralName(name)),
63
+ idType,
64
+ columns,
65
+ foreignKeys,
66
+ relationships,
67
+ memory: (modelClass as { memory?: boolean }).memory === true,
68
+ };
69
+ }
70
+
71
+ return schemas;
72
+ }
73
+
74
+ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Record<string, ModelSchema> = {}): string {
75
+ const { idType, columns, foreignKeys } = schema;
76
+ const table = sanitizeTableName(schema.table);
77
+ const lines: string[] = [];
78
+
79
+ // Primary key
80
+ if (idType === 'string') {
81
+ lines.push(' `id` VARCHAR(255) PRIMARY KEY');
82
+ } else {
83
+ lines.push(' `id` INT AUTO_INCREMENT PRIMARY KEY');
84
+ }
85
+
86
+ // Attribute columns
87
+ for (const [col, mysqlType] of Object.entries(columns)) {
88
+ lines.push(` \`${col}\` ${mysqlType}`);
89
+ }
90
+
91
+ // Foreign key columns
92
+ for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
93
+ const refIdType = getReferencedIdType(fkDef.references, allSchemas);
94
+ lines.push(` \`${fkCol}\` ${refIdType}`);
95
+ }
96
+
97
+ // Timestamps
98
+ lines.push(' `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP');
99
+ lines.push(' `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
100
+
101
+ // Foreign key constraints
102
+ for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
103
+ const refTable = sanitizeTableName(fkDef.references);
104
+ lines.push(` FOREIGN KEY (\`${fkCol}\`) REFERENCES \`${refTable}\`(\`${fkDef.column}\`) ON DELETE SET NULL`);
105
+ }
106
+
107
+ return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${lines.join(',\n')}\n)`;
108
+ }
109
+
110
+ function getReferencedIdType(tableName: string, allSchemas: Record<string, ModelSchema>): string {
111
+ // Look up the referenced table's PK type from schemas
112
+ for (const schema of Object.values(allSchemas)) {
113
+ if (schema.table === tableName) {
114
+ return schema.idType === 'string' ? 'VARCHAR(255)' : 'INT';
115
+ }
116
+ }
117
+
118
+ // Default to INT if referenced table not found in schemas
119
+ return 'INT';
120
+ }
121
+
122
+ export { getTopologicalOrder } from '../schema-helpers.js';
123
+
124
+ export function introspectViews(): Record<string, ViewSchema> {
125
+ const orm = (Orm as unknown as { instance: { views?: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
126
+ if (!orm.views) return {};
127
+
128
+ const schemas: Record<string, ViewSchema> = {};
129
+
130
+ for (const [viewKey, viewClass] of Object.entries(orm.views)) {
131
+ const name = camelCaseToKebabCase(viewKey.slice(0, -4)); // Remove 'View' suffix
132
+
133
+ const source = (viewClass as { source?: string }).source;
134
+ if (!source) continue;
135
+
136
+ const model = new (viewClass as new (name: string) => Record<string, unknown>)(name);
137
+ const columns: Record<string, string> = {};
138
+ const foreignKeys: Record<string, ForeignKeyDef> = {};
139
+ const aggregates: Record<string, AggregateProperty> = {};
140
+ const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
141
+
142
+ for (const [key, property] of Object.entries(model)) {
143
+ if (key.startsWith('__')) continue;
144
+ if (key === 'id') continue;
145
+
146
+ if (property instanceof AggregateProperty) {
147
+ aggregates[key] = property;
148
+ continue;
149
+ }
150
+
151
+ const relInfo = getRelationshipInfo(property);
152
+
153
+ if (relInfo?.type === 'belongsTo') {
154
+ relationships.belongsTo[key] = relInfo.modelName;
155
+ if (relInfo.modelName) {
156
+ const fkColumn = `${key}_id`;
157
+ foreignKeys[fkColumn] = {
158
+ references: sanitizeTableName(getPluralName(relInfo.modelName)),
159
+ column: 'id',
160
+ };
161
+ }
162
+ } else if (relInfo?.type === 'hasMany') {
163
+ relationships.hasMany[key] = relInfo.modelName;
164
+ } else if (property instanceof ModelProperty) {
165
+ const transforms = orm.transforms;
166
+ columns[key] = getMysqlType((property as ModelProperty).type, transforms[(property as ModelProperty).type] as ((...args: unknown[]) => unknown) & { mysqlType?: string });
167
+ }
168
+ }
169
+
170
+ schemas[name] = {
171
+ viewName: sanitizeTableName(getPluralName(name)),
172
+ source,
173
+ groupBy: (viewClass as { groupBy?: string }).groupBy || undefined,
174
+ columns,
175
+ foreignKeys,
176
+ aggregates,
177
+ relationships,
178
+ isView: true,
179
+ memory: false,
180
+ };
181
+ }
182
+
183
+ return schemas;
184
+ }
185
+
186
+ export function buildViewDDL(name: string, viewSchema: ViewSchema, modelSchemas: Record<string, ModelSchema> = {}): string {
187
+ if (!viewSchema.source) {
188
+ throw new Error(`View '${name}' must define a source model`);
189
+ }
190
+
191
+ const sourceModelName = viewSchema.source;
192
+ const sourceSchema = modelSchemas[sourceModelName];
193
+ const sourceTable = sanitizeTableName(sourceSchema
194
+ ? sourceSchema.table
195
+ : getPluralName(sourceModelName));
196
+
197
+ const selectColumns: string[] = [];
198
+ const joins: JoinClause[] = [];
199
+ const hasAggregates = Object.keys(viewSchema.aggregates || {}).length > 0;
200
+ const groupByField = viewSchema.groupBy;
201
+
202
+ // ID column: groupBy field or source table PK
203
+ if (groupByField) {
204
+ selectColumns.push(`\`${sourceTable}\`.\`${groupByField}\` AS \`id\``);
205
+ } else {
206
+ selectColumns.push(`\`${sourceTable}\`.\`id\` AS \`id\``);
207
+ }
208
+
209
+ // Aggregate columns
210
+ for (const [key, aggProp] of Object.entries(viewSchema.aggregates || {})) {
211
+ if (aggProp.relationship === undefined) {
212
+ // Field-level aggregate (groupBy views)
213
+ if (aggProp.aggregateType === 'count') {
214
+ selectColumns.push(`COUNT(*) AS \`${key}\``);
215
+ } else {
216
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${sourceTable}\`.\`${aggProp.field}\`) AS \`${key}\``);
217
+ }
218
+ } else {
219
+ // Relationship aggregate
220
+ const relName = aggProp.relationship;
221
+ const relTable = sanitizeTableName(getPluralName(relName));
222
+
223
+ if (aggProp.aggregateType === 'count') {
224
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`id\`) AS \`${key}\``);
225
+ } else {
226
+ const field = aggProp.field;
227
+ selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`${field}\`) AS \`${key}\``);
228
+ }
229
+
230
+ // Add LEFT JOIN for the relationship if not already added
231
+ const joinKey = `${relTable}`;
232
+ if (!joins.find(j => j.table === joinKey)) {
233
+ const fkColumn = `${sourceModelName}_id`;
234
+ joins.push({
235
+ table: relTable,
236
+ condition: `\`${relTable}\`.\`${fkColumn}\` = \`${sourceTable}\`.\`id\``
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ // Regular columns (from resolve map string paths or direct attr fields)
243
+ for (const [key] of Object.entries(viewSchema.columns || {})) {
244
+ selectColumns.push(`\`${sourceTable}\`.\`${key}\` AS \`${key}\``);
245
+ }
246
+
247
+ // Build JOIN clauses
248
+ const joinClauses = joins.map(j =>
249
+ `LEFT JOIN \`${j.table}\` ON ${j.condition}`
250
+ ).join('\n ');
251
+
252
+ // Build GROUP BY
253
+ let groupBy = '';
254
+ if (groupByField) {
255
+ groupBy = `\nGROUP BY \`${sourceTable}\`.\`${groupByField}\``;
256
+ } else if (hasAggregates) {
257
+ groupBy = `\nGROUP BY \`${sourceTable}\`.\`id\``;
258
+ }
259
+
260
+ const viewName = sanitizeTableName(viewSchema.viewName);
261
+ const sql = `CREATE OR REPLACE VIEW \`${viewName}\` AS\nSELECT\n ${selectColumns.join(',\n ')}\nFROM \`${sourceTable}\`${joinClauses ? '\n ' + joinClauses : ''}${groupBy}`;
262
+
263
+ return sql;
264
+ }
265
+
266
+ export function viewSchemasToSnapshot(viewSchemas: Record<string, ViewSchema>): Record<string, ViewSnapshotEntry> {
267
+ const snapshot: Record<string, ViewSnapshotEntry> = {};
268
+
269
+ for (const [name, schema] of Object.entries(viewSchemas)) {
270
+ snapshot[name] = {
271
+ viewName: schema.viewName,
272
+ source: schema.source,
273
+ ...(schema.groupBy ? { groupBy: schema.groupBy } : {}),
274
+ columns: { ...schema.columns },
275
+ foreignKeys: { ...schema.foreignKeys },
276
+ isView: true,
277
+ viewQuery: buildViewDDL(name, schema),
278
+ };
279
+ }
280
+
281
+ return snapshot;
282
+ }
283
+
284
+ interface ViewSnapshotEntry {
285
+ viewName: string;
286
+ source: string;
287
+ groupBy?: string;
288
+ columns: Record<string, string>;
289
+ foreignKeys: Record<string, ForeignKeyDef>;
290
+ isView: true;
291
+ viewQuery: string;
292
+ }
293
+
294
+ export function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<string, SnapshotEntry> {
295
+ const snapshot: Record<string, SnapshotEntry> = {};
296
+
297
+ for (const [name, schema] of Object.entries(schemas)) {
298
+ snapshot[name] = {
299
+ table: schema.table,
300
+ idType: schema.idType,
301
+ columns: { ...schema.columns },
302
+ foreignKeys: { ...schema.foreignKeys },
303
+ };
304
+ }
305
+
306
+ return snapshot;
307
+ }
308
+
309
+ export type { ModelSchema, ViewSchema, ForeignKeyDef, SnapshotEntry } from '../types/orm-types.js';
310
+ export type { ViewSnapshotEntry };
@@ -0,0 +1,42 @@
1
+ interface TransformFn {
2
+ mysqlType?: string;
3
+ (...args: unknown[]): unknown;
4
+ }
5
+
6
+ const typeMap: Record<string, string> = {
7
+ string: 'VARCHAR(255)',
8
+ number: 'INT',
9
+ float: 'FLOAT',
10
+ boolean: 'TINYINT(1)',
11
+ date: 'DATETIME',
12
+ timestamp: 'BIGINT',
13
+ passthrough: 'TEXT',
14
+ trim: 'VARCHAR(255)',
15
+ uppercase: 'VARCHAR(255)',
16
+ ceil: 'INT',
17
+ floor: 'INT',
18
+ round: 'INT',
19
+ };
20
+
21
+ /**
22
+ * Resolves a Stonyx ORM attribute type to a MySQL column type.
23
+ *
24
+ * For built-in types, returns the mapped MySQL type directly.
25
+ *
26
+ * For custom transforms (e.g. an `animal` transform that maps strings to ints):
27
+ * - If the transform function exports a `mysqlType` property, that value is used.
28
+ * Example: `const transform = (v) => codeMap[v]; transform.mysqlType = 'INT'; export default transform;`
29
+ * - Otherwise, defaults to JSON. Values are JSON-stringified on write and
30
+ * JSON-parsed on read. This handles primitives and plain objects correctly.
31
+ * Class instances will be reduced to plain objects — if a custom transform
32
+ * produces class instances, it must declare a `mysqlType` and handle
33
+ * serialization itself.
34
+ */
35
+ export function getMysqlType(attrType: string, transformFn?: TransformFn): string {
36
+ if (typeMap[attrType]) return typeMap[attrType];
37
+ if (transformFn?.mysqlType) return transformFn.mysqlType;
38
+
39
+ return 'JSON';
40
+ }
41
+
42
+ export default typeMap;