@stonyx/orm 0.2.1-beta.87 → 0.2.1-beta.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregates.js +9 -6
- package/dist/db.js +4 -4
- package/dist/hooks.js +6 -2
- package/dist/main.js +3 -1
- package/dist/manage-record.js +6 -2
- package/dist/mysql/migration-generator.js +15 -6
- package/dist/mysql/mysql-db.js +24 -14
- package/dist/mysql/schema-introspector.js +11 -6
- package/dist/orm-request.js +19 -11
- package/dist/postgres/migration-generator.js +9 -5
- package/dist/postgres/postgres-db.js +14 -13
- package/dist/postgres/schema-introspector.js +11 -6
- package/dist/relationships.js +2 -0
- package/dist/setup-rest-server.js +4 -6
- package/dist/store.js +2 -0
- package/dist/utils.js +2 -1
- package/dist/view-resolver.js +3 -1
- package/package.json +1 -1
- package/src/aggregates.ts +9 -7
- package/src/belongs-to.ts +1 -1
- package/src/db.ts +4 -4
- package/src/hooks.ts +4 -2
- package/src/main.ts +3 -2
- package/src/manage-record.ts +5 -2
- package/src/mysql/migration-generator.ts +12 -7
- package/src/mysql/mysql-db.ts +23 -17
- package/src/mysql/schema-introspector.ts +10 -7
- package/src/orm-request.ts +19 -12
- package/src/postgres/migration-generator.ts +7 -5
- package/src/postgres/postgres-db.ts +14 -13
- package/src/postgres/schema-introspector.ts +10 -7
- package/src/relationships.ts +3 -2
- package/src/setup-rest-server.ts +7 -10
- package/src/store.ts +2 -1
- package/src/utils.ts +2 -1
- package/src/view-resolver.ts +2 -1
package/dist/relationships.js
CHANGED
|
@@ -10,6 +10,8 @@ export function getRelationships(type, sourceModel, targetModel, relationshipId)
|
|
|
10
10
|
if (!allRelationships.has(sourceModel))
|
|
11
11
|
allRelationships.set(sourceModel, new Map());
|
|
12
12
|
const modelRelationship = allRelationships.get(sourceModel);
|
|
13
|
+
if (!modelRelationship)
|
|
14
|
+
return undefined;
|
|
13
15
|
if (!modelRelationship.has(targetModel))
|
|
14
16
|
modelRelationship.set(targetModel, new Map());
|
|
15
17
|
const relationship = modelRelationship.get(targetModel);
|
|
@@ -8,7 +8,7 @@ import { dbKey } from './db.js';
|
|
|
8
8
|
import { getPluralName } from './plural-registry.js';
|
|
9
9
|
import log from 'stonyx/log';
|
|
10
10
|
export default async function (route, accessPath, metaRoute) {
|
|
11
|
-
|
|
11
|
+
const accessFiles = {};
|
|
12
12
|
try {
|
|
13
13
|
await forEachFileImport(accessPath, (accessClass) => {
|
|
14
14
|
const accessInstance = new accessClass();
|
|
@@ -32,8 +32,8 @@ export default async function (route, accessPath, metaRoute) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
|
-
log.error(error instanceof Error ? error.message : String(error));
|
|
36
|
-
log.warn('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
|
|
35
|
+
log.error?.(error instanceof Error ? error.message : String(error));
|
|
36
|
+
log.warn?.('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
|
|
37
37
|
}
|
|
38
38
|
await waitForModule('rest-server');
|
|
39
39
|
// Remove "/" prefix and name mount point accordingly
|
|
@@ -46,9 +46,7 @@ export default async function (route, accessPath, metaRoute) {
|
|
|
46
46
|
}
|
|
47
47
|
// Mount the meta route when metaRoute config is enabled
|
|
48
48
|
if (metaRoute) {
|
|
49
|
-
log.warn('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
|
|
49
|
+
log.warn?.('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
|
|
50
50
|
RestServer.instance.mountRoute(MetaRequest, { name });
|
|
51
51
|
}
|
|
52
|
-
// Cleanup references
|
|
53
|
-
accessFiles = null;
|
|
54
52
|
}
|
package/dist/store.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -6,7 +6,8 @@ export function isDbError(error) {
|
|
|
6
6
|
export function pluralize(word) {
|
|
7
7
|
if (word.includes('-')) {
|
|
8
8
|
const parts = word.split('-');
|
|
9
|
-
const
|
|
9
|
+
const last = parts.pop();
|
|
10
|
+
const pluralizedLast = basePluralize(last);
|
|
10
11
|
return [...parts, pluralizedLast].join('-');
|
|
11
12
|
}
|
|
12
13
|
return basePluralize(word);
|
package/dist/view-resolver.js
CHANGED
|
@@ -101,7 +101,9 @@ export default class ViewResolver {
|
|
|
101
101
|
if (!groups.has(key)) {
|
|
102
102
|
groups.set(key, []);
|
|
103
103
|
}
|
|
104
|
-
groups.get(key)
|
|
104
|
+
const group = groups.get(key);
|
|
105
|
+
if (group)
|
|
106
|
+
group.push(record);
|
|
105
107
|
}
|
|
106
108
|
const results = [];
|
|
107
109
|
for (const [groupKey, groupRecords] of groups) {
|
package/package.json
CHANGED
package/src/aggregates.ts
CHANGED
|
@@ -27,13 +27,15 @@ export class AggregateProperty {
|
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (this.aggregateType === 'count') return relatedRecords.length;
|
|
31
|
+
|
|
32
|
+
const field = this.field;
|
|
33
|
+
if (!field) return null;
|
|
33
34
|
|
|
35
|
+
switch (this.aggregateType) {
|
|
34
36
|
case 'sum':
|
|
35
37
|
return relatedRecords.reduce((acc, record) => {
|
|
36
|
-
const val = parseFloat(record?.__data?.[
|
|
38
|
+
const val = parseFloat(record?.__data?.[field] as string ?? record?.[field] as string);
|
|
37
39
|
return acc + (isNaN(val) ? 0 : val);
|
|
38
40
|
}, 0);
|
|
39
41
|
|
|
@@ -41,7 +43,7 @@ export class AggregateProperty {
|
|
|
41
43
|
let sum = 0;
|
|
42
44
|
let count = 0;
|
|
43
45
|
for (const record of relatedRecords) {
|
|
44
|
-
const val = parseFloat(record?.__data?.[
|
|
46
|
+
const val = parseFloat(record?.__data?.[field] as string ?? record?.[field] as string);
|
|
45
47
|
if (!isNaN(val)) {
|
|
46
48
|
sum += val;
|
|
47
49
|
count++;
|
|
@@ -53,7 +55,7 @@ export class AggregateProperty {
|
|
|
53
55
|
case 'min': {
|
|
54
56
|
let min: number | null = null;
|
|
55
57
|
for (const record of relatedRecords) {
|
|
56
|
-
const val = parseFloat(record?.__data?.[
|
|
58
|
+
const val = parseFloat(record?.__data?.[field] as string ?? record?.[field] as string);
|
|
57
59
|
if (!isNaN(val) && (min === null || val < min)) min = val;
|
|
58
60
|
}
|
|
59
61
|
return min;
|
|
@@ -62,7 +64,7 @@ export class AggregateProperty {
|
|
|
62
64
|
case 'max': {
|
|
63
65
|
let max: number | null = null;
|
|
64
66
|
for (const record of relatedRecords) {
|
|
65
|
-
const val = parseFloat(record?.__data?.[
|
|
67
|
+
const val = parseFloat(record?.__data?.[field] as string ?? record?.[field] as string);
|
|
66
68
|
if (!isNaN(val) && (max === null || val > max)) max = val;
|
|
67
69
|
}
|
|
68
70
|
return max;
|
package/src/belongs-to.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SourceRecord } from './types/orm-types.js';
|
|
|
4
4
|
|
|
5
5
|
function getOrSet<K, V>(map: Map<K, V>, key: K, defaultValue: V): V {
|
|
6
6
|
if (!map.has(key)) map.set(key, defaultValue);
|
|
7
|
-
return map.get(key)
|
|
7
|
+
return map.get(key) as V;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
interface BelongsToOptions {
|
package/src/db.ts
CHANGED
|
@@ -84,7 +84,7 @@ export default class DB {
|
|
|
84
84
|
const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
|
|
85
85
|
|
|
86
86
|
if (hasData) {
|
|
87
|
-
log.error
|
|
87
|
+
log.error?.(`DB mode mismatch: db.json contains data but mode is set to 'directory'. Run migration first:\n\n stonyx db:migrate-to-directory\n`);
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -97,7 +97,7 @@ export default class DB {
|
|
|
97
97
|
)).some(Boolean);
|
|
98
98
|
|
|
99
99
|
if (hasCollectionFiles) {
|
|
100
|
-
log.error
|
|
100
|
+
log.error?.(`DB mode mismatch: directory '${config.orm.db.directory}/' contains collection files but mode is set to 'file'. Run migration first:\n\n stonyx db:migrate-to-file\n`);
|
|
101
101
|
process.exit(1);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -176,13 +176,13 @@ export default class DB {
|
|
|
176
176
|
if (dbFileExists) await updateFile(dbFilePath, skeleton, { json: true });
|
|
177
177
|
else await createFile(dbFilePath, skeleton, { json: true });
|
|
178
178
|
|
|
179
|
-
log.db
|
|
179
|
+
log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
|
|
184
184
|
|
|
185
|
-
log.db
|
|
185
|
+
log.db?.(`DB has been successfully saved to ${file}`);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
async getRecord(): Promise<DBRecord> {
|
package/src/hooks.ts
CHANGED
|
@@ -40,7 +40,8 @@ export function beforeHook(operation: string, model: string, handler: HookHandle
|
|
|
40
40
|
if (!beforeHooks.has(key)) {
|
|
41
41
|
beforeHooks.set(key, []);
|
|
42
42
|
}
|
|
43
|
-
beforeHooks.get(key)
|
|
43
|
+
const hooks = beforeHooks.get(key);
|
|
44
|
+
if (hooks) hooks.push(handler);
|
|
44
45
|
|
|
45
46
|
// Return unsubscribe function
|
|
46
47
|
return () => {
|
|
@@ -66,7 +67,8 @@ export function afterHook(operation: string, model: string, handler: HookHandler
|
|
|
66
67
|
if (!afterHooks.has(key)) {
|
|
67
68
|
afterHooks.set(key, []);
|
|
68
69
|
}
|
|
69
|
-
afterHooks.get(key)
|
|
70
|
+
const hooks = afterHooks.get(key);
|
|
71
|
+
if (hooks) hooks.push(handler);
|
|
70
72
|
|
|
71
73
|
// Return unsubscribe function
|
|
72
74
|
return () => {
|
package/src/main.ts
CHANGED
|
@@ -187,7 +187,8 @@ export default class Orm {
|
|
|
187
187
|
static get db(): OrmDB | SqlDb {
|
|
188
188
|
if (!Orm.initialized) throw new Error('ORM has not been initialized yet');
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
if (!Orm.instance.db) throw new Error('ORM database has not been initialized');
|
|
191
|
+
return Orm.instance.db;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
getRecordClasses(modelName: string): { modelClass: unknown; serializerClass: unknown } {
|
|
@@ -218,7 +219,7 @@ export default class Orm {
|
|
|
218
219
|
this.warnings.add(message);
|
|
219
220
|
|
|
220
221
|
setTimeout(() => {
|
|
221
|
-
this.warnings.forEach(warning => log.warn
|
|
222
|
+
this.warnings.forEach(warning => log.warn?.(warning));
|
|
222
223
|
this.warnings.clear();
|
|
223
224
|
}, 0);
|
|
224
225
|
}
|
package/src/manage-record.ts
CHANGED
|
@@ -142,8 +142,11 @@ function assignRecordId(modelName: string, rawData: { [key: string]: unknown }):
|
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
const
|
|
146
|
-
|
|
145
|
+
const storeMap = store.get(modelName);
|
|
146
|
+
if (!storeMap) throw new Error(`Cannot assign record ID: model "${modelName}" not found in store`);
|
|
147
|
+
const modelStore = Array.from(storeMap.values()) as OrmRecord[];
|
|
148
|
+
const lastRecord = modelStore.at(-1);
|
|
149
|
+
rawData.id = lastRecord ? (lastRecord.id as number) + 1 : 1;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
function isStringIdModel(modelName: string): boolean {
|
|
@@ -51,9 +51,12 @@ interface GeneratedMigration {
|
|
|
51
51
|
type Snapshot = Record<string, SnapshotEntry & { isView?: boolean; viewName?: string; viewQuery?: string; source?: string }>;
|
|
52
52
|
|
|
53
53
|
export async function generateMigration(description: string = 'migration'): Promise<GeneratedMigration | null> {
|
|
54
|
-
const
|
|
54
|
+
const mysqlConfig = config.orm.mysql;
|
|
55
|
+
if (!mysqlConfig) throw new Error('MySQL configuration (config.orm.mysql) is required for migration generation');
|
|
56
|
+
const { migrationsDir } = mysqlConfig;
|
|
57
|
+
if (!migrationsDir) throw new Error('MySQL migrationsDir is required in config');
|
|
55
58
|
const rootPath = config.rootPath;
|
|
56
|
-
const migrationsPath = path.resolve(rootPath, migrationsDir
|
|
59
|
+
const migrationsPath = path.resolve(rootPath, migrationsDir);
|
|
57
60
|
|
|
58
61
|
await createDirectory(migrationsPath);
|
|
59
62
|
|
|
@@ -71,7 +74,7 @@ export async function generateMigration(description: string = 'migration'): Prom
|
|
|
71
74
|
const viewDiffPrelim = diffViewSnapshots(previousViewSnapshotPrelim, currentViewSnapshotPrelim);
|
|
72
75
|
|
|
73
76
|
if (!viewDiffPrelim.hasChanges) {
|
|
74
|
-
log.db
|
|
77
|
+
log.db?.('No schema changes detected.');
|
|
75
78
|
return null;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -104,7 +107,8 @@ export async function generateMigration(description: string = 'migration'): Prom
|
|
|
104
107
|
|
|
105
108
|
// Removed columns
|
|
106
109
|
for (const { model, column, type } of diff.removedColumns) {
|
|
107
|
-
const table = previousSnapshot[model]
|
|
110
|
+
const table = previousSnapshot[model]?.table;
|
|
111
|
+
if (!table) throw new Error(`Missing table name in snapshot for model "${model}"`);
|
|
108
112
|
upStatements.push(`ALTER TABLE \`${table}\` DROP COLUMN \`${column}\`;`);
|
|
109
113
|
downStatements.push(`ALTER TABLE \`${table}\` ADD COLUMN \`${column}\` ${type};`);
|
|
110
114
|
}
|
|
@@ -130,7 +134,8 @@ export async function generateMigration(description: string = 'migration'): Prom
|
|
|
130
134
|
|
|
131
135
|
// Removed foreign keys
|
|
132
136
|
for (const { model, column, references } of diff.removedForeignKeys) {
|
|
133
|
-
const table = previousSnapshot[model]
|
|
137
|
+
const table = previousSnapshot[model]?.table;
|
|
138
|
+
if (!table) throw new Error(`Missing table name in snapshot for model "${model}"`);
|
|
134
139
|
// Resolve FK column type from the referenced table's PK type in previous snapshot
|
|
135
140
|
const refModel = Object.entries(previousSnapshot).find(([, s]) => s.table === references.references);
|
|
136
141
|
const fkType = refModel && refModel[1].idType === 'string' ? 'VARCHAR(255)' : 'INT';
|
|
@@ -184,7 +189,7 @@ export async function generateMigration(description: string = 'migration'): Prom
|
|
|
184
189
|
const combinedHasChanges = diff.hasChanges || viewDiff.hasChanges;
|
|
185
190
|
|
|
186
191
|
if (!combinedHasChanges) {
|
|
187
|
-
log.db
|
|
192
|
+
log.db?.('No schema changes detected.');
|
|
188
193
|
return null;
|
|
189
194
|
}
|
|
190
195
|
|
|
@@ -202,7 +207,7 @@ export async function generateMigration(description: string = 'migration'): Prom
|
|
|
202
207
|
await createFile(path.join(migrationsPath, filename), content);
|
|
203
208
|
await createFile(path.join(migrationsPath, '.snapshot.json'), JSON.stringify(combinedSnapshot, null, 2));
|
|
204
209
|
|
|
205
|
-
log.db
|
|
210
|
+
log.db?.(`Migration generated: ${filename}`);
|
|
206
211
|
|
|
207
212
|
return { filename, content, snapshot: combinedSnapshot };
|
|
208
213
|
}
|
package/src/mysql/mysql-db.ts
CHANGED
|
@@ -89,7 +89,9 @@ export default class MysqlDB {
|
|
|
89
89
|
|
|
90
90
|
this.deps = { ...defaultDeps, ...deps } as MysqlDBDeps;
|
|
91
91
|
this.pool = null;
|
|
92
|
-
|
|
92
|
+
const mysqlConfig = this.deps.config.orm.mysql;
|
|
93
|
+
if (!mysqlConfig) throw new Error('MySQL configuration (config.orm.mysql) is required');
|
|
94
|
+
this.mysqlConfig = mysqlConfig;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
private requirePool(): Pool {
|
|
@@ -104,7 +106,8 @@ export default class MysqlDB {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
async startup(): Promise<void> {
|
|
107
|
-
|
|
109
|
+
if (!this.mysqlConfig.migrationsDir) throw new Error('MySQL migrationsDir is required in config');
|
|
110
|
+
const migrationsPath = this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir);
|
|
108
111
|
|
|
109
112
|
// Check for pending migrations
|
|
110
113
|
const applied = await this.deps.getAppliedMigrations(this.requirePool(), this.mysqlConfig.migrationsTable);
|
|
@@ -112,7 +115,7 @@ export default class MysqlDB {
|
|
|
112
115
|
const pending = files.filter(f => !applied.includes(f));
|
|
113
116
|
|
|
114
117
|
if (pending.length > 0) {
|
|
115
|
-
this.deps.log.db
|
|
118
|
+
this.deps.log.db?.(`${pending.length} pending migration(s) found.`);
|
|
116
119
|
|
|
117
120
|
const shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
|
|
118
121
|
|
|
@@ -122,13 +125,13 @@ export default class MysqlDB {
|
|
|
122
125
|
const { up } = this.deps.parseMigrationFile(content);
|
|
123
126
|
|
|
124
127
|
await this.deps.applyMigration(this.requirePool(), filename, up, this.mysqlConfig.migrationsTable);
|
|
125
|
-
this.deps.log.db
|
|
128
|
+
this.deps.log.db?.(`Applied migration: ${filename}`);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
// Reload records after applying migrations
|
|
129
132
|
await this.loadMemoryRecords();
|
|
130
133
|
} else {
|
|
131
|
-
this.deps.log.warn
|
|
134
|
+
this.deps.log.warn?.('Skipping pending migrations. Schema may be outdated.');
|
|
132
135
|
}
|
|
133
136
|
} else if (files.length === 0) {
|
|
134
137
|
const schemas = this.deps.introspectModels();
|
|
@@ -146,25 +149,26 @@ export default class MysqlDB {
|
|
|
146
149
|
if (result) {
|
|
147
150
|
const { up } = this.deps.parseMigrationFile(result.content);
|
|
148
151
|
await this.deps.applyMigration(this.requirePool(), result.filename, up, this.mysqlConfig.migrationsTable);
|
|
149
|
-
this.deps.log.db
|
|
152
|
+
this.deps.log.db?.(`Applied migration: ${result.filename}`);
|
|
150
153
|
await this.loadMemoryRecords();
|
|
151
154
|
}
|
|
152
155
|
} else {
|
|
153
|
-
this.deps.log.warn
|
|
156
|
+
this.deps.log.warn?.('Skipping initial migration. Tables may not exist.');
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
// Check for schema drift
|
|
159
162
|
const schemas = this.deps.introspectModels();
|
|
160
|
-
|
|
163
|
+
if (!this.mysqlConfig.migrationsDir) throw new Error('MySQL migrationsDir is required in config');
|
|
164
|
+
const snapshot = await this.deps.loadLatestSnapshot(this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir)) as Record<string, SnapshotEntry>;
|
|
161
165
|
|
|
162
166
|
if (Object.keys(snapshot).length > 0) {
|
|
163
167
|
const drift = this.deps.detectSchemaDrift(schemas, snapshot);
|
|
164
168
|
|
|
165
169
|
if (drift.hasChanges) {
|
|
166
|
-
this.deps.log.warn
|
|
167
|
-
this.deps.log.warn
|
|
170
|
+
this.deps.log.warn?.('Schema drift detected: models have changed since the last migration.');
|
|
171
|
+
this.deps.log.warn?.('Run `stonyx db:generate-migration` to create a new migration.');
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
}
|
|
@@ -191,7 +195,7 @@ export default class MysqlDB {
|
|
|
191
195
|
// Check the model's memory flag — skip non-memory models
|
|
192
196
|
const { modelClass } = Orm.instance.getRecordClasses(modelName) as { modelClass?: { memory?: boolean } };
|
|
193
197
|
if (modelClass?.memory === false) {
|
|
194
|
-
this.deps.log.db
|
|
198
|
+
this.deps.log.db?.(`Skipping memory load for '${modelName}' (memory: false)`);
|
|
195
199
|
continue;
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -209,7 +213,7 @@ export default class MysqlDB {
|
|
|
209
213
|
} catch (error) {
|
|
210
214
|
// Table may not exist yet (pre-migration) — skip gracefully
|
|
211
215
|
if (isDbError(error) && error.code === 'ER_NO_SUCH_TABLE') {
|
|
212
|
-
this.deps.log.db
|
|
216
|
+
this.deps.log.db?.(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
|
|
213
217
|
continue;
|
|
214
218
|
}
|
|
215
219
|
|
|
@@ -223,7 +227,7 @@ export default class MysqlDB {
|
|
|
223
227
|
for (const [viewName, viewSchema] of Object.entries(viewSchemas)) {
|
|
224
228
|
const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName) as { modelClass?: { memory?: boolean } };
|
|
225
229
|
if (viewClass?.memory !== true) {
|
|
226
|
-
this.deps.log.db
|
|
230
|
+
this.deps.log.db?.(`Skipping memory load for view '${viewName}' (memory: false)`);
|
|
227
231
|
continue;
|
|
228
232
|
}
|
|
229
233
|
|
|
@@ -240,7 +244,7 @@ export default class MysqlDB {
|
|
|
240
244
|
}
|
|
241
245
|
} catch (error) {
|
|
242
246
|
if (isDbError(error) && error.code === 'ER_NO_SUCH_TABLE') {
|
|
243
|
-
this.deps.log.db
|
|
247
|
+
this.deps.log.db?.(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
|
|
244
248
|
continue;
|
|
245
249
|
}
|
|
246
250
|
throw error;
|
|
@@ -314,14 +318,15 @@ export default class MysqlDB {
|
|
|
314
318
|
|
|
315
319
|
if (!schema) return [];
|
|
316
320
|
|
|
317
|
-
const
|
|
321
|
+
const resolvedSchema = schema;
|
|
322
|
+
const { sql, values } = this.deps.buildSelect(resolvedSchema.table, conditions);
|
|
318
323
|
|
|
319
324
|
try {
|
|
320
325
|
const result = await this.requirePool().execute(sql, values);
|
|
321
326
|
const rows = result[0] as Record<string, unknown>[];
|
|
322
327
|
|
|
323
328
|
const records = rows.map(row => {
|
|
324
|
-
const rawData = this._rowToRawData(row,
|
|
329
|
+
const rawData = this._rowToRawData(row, resolvedSchema);
|
|
325
330
|
return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
|
|
326
331
|
});
|
|
327
332
|
|
|
@@ -432,7 +437,8 @@ export default class MysqlDB {
|
|
|
432
437
|
if (isPendingId && result.insertId) {
|
|
433
438
|
const pendingId = record.id;
|
|
434
439
|
const realId = result.insertId;
|
|
435
|
-
const modelStore = this.deps.store.get(modelName)
|
|
440
|
+
const modelStore = this.deps.store.get(modelName);
|
|
441
|
+
if (!modelStore) throw new Error(`Model "${modelName}" not found in store during ID re-key`);
|
|
436
442
|
|
|
437
443
|
modelStore.delete(pendingId as number | string);
|
|
438
444
|
record.__data.id = realId;
|
|
@@ -74,9 +74,10 @@ export function introspectModels(): Record<string, ModelSchema> {
|
|
|
74
74
|
|
|
75
75
|
// Build foreign keys from belongsTo relationships
|
|
76
76
|
for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
|
|
77
|
+
if (!targetModelName) continue;
|
|
77
78
|
const fkColumn = `${relName}_id`;
|
|
78
79
|
foreignKeys[fkColumn] = {
|
|
79
|
-
references: sanitizeTableName(getPluralName(targetModelName
|
|
80
|
+
references: sanitizeTableName(getPluralName(targetModelName)),
|
|
80
81
|
column: 'id',
|
|
81
82
|
};
|
|
82
83
|
}
|
|
@@ -155,7 +156,7 @@ export function getTopologicalOrder(schemas: Record<string, ModelSchema>): strin
|
|
|
155
156
|
|
|
156
157
|
// Visit dependencies (belongsTo targets) first
|
|
157
158
|
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
158
|
-
visit(targetModelName
|
|
159
|
+
if (targetModelName) visit(targetModelName);
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
order.push(name);
|
|
@@ -199,11 +200,13 @@ export function introspectViews(): Record<string, ViewSchema> {
|
|
|
199
200
|
|
|
200
201
|
if (relInfo?.type === 'belongsTo') {
|
|
201
202
|
relationships.belongsTo[key] = relInfo.modelName;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
if (relInfo.modelName) {
|
|
204
|
+
const fkColumn = `${key}_id`;
|
|
205
|
+
foreignKeys[fkColumn] = {
|
|
206
|
+
references: sanitizeTableName(getPluralName(relInfo.modelName)),
|
|
207
|
+
column: 'id',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
207
210
|
} else if (relInfo?.type === 'hasMany') {
|
|
208
211
|
relationships.hasMany[key] = relInfo.modelName;
|
|
209
212
|
} else if (property instanceof ModelProperty) {
|
package/src/orm-request.ts
CHANGED
|
@@ -127,7 +127,7 @@ function buildResponse(
|
|
|
127
127
|
|
|
128
128
|
const includedRecords = collectIncludedRecords(recordOrRecords, includes);
|
|
129
129
|
if (includedRecords.length > 0) {
|
|
130
|
-
response.included = includedRecords.map(record => record.toJSON
|
|
130
|
+
response.included = includedRecords.map(record => record.toJSON?.({ baseUrl }));
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
return response;
|
|
@@ -163,7 +163,8 @@ function traverseIncludePath(
|
|
|
163
163
|
for (const relatedRecord of recordsToProcess) {
|
|
164
164
|
if (!relatedRecord) continue;
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
if (!relatedRecord.__model) continue;
|
|
167
|
+
const type = relatedRecord.__model.__name;
|
|
167
168
|
const id = relatedRecord.id as string | number;
|
|
168
169
|
|
|
169
170
|
// Initialize Set for this type if needed
|
|
@@ -292,7 +293,7 @@ export default class OrmRequest extends Request {
|
|
|
292
293
|
if (queryFilterPredicate) recordsToReturn = recordsToReturn.filter(queryFilterPredicate as (record: OrmRecord) => boolean);
|
|
293
294
|
|
|
294
295
|
const baseUrl = getBaseUrl(request);
|
|
295
|
-
const data = recordsToReturn.map(record => record.toJSON
|
|
296
|
+
const data = recordsToReturn.map(record => record.toJSON?.({ fields: modelFields, baseUrl }));
|
|
296
297
|
|
|
297
298
|
return buildResponse(data, request.query?.include, recordsToReturn, {
|
|
298
299
|
links: { self: `${baseUrl}/${pluralizedModel}` },
|
|
@@ -308,7 +309,7 @@ export default class OrmRequest extends Request {
|
|
|
308
309
|
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
309
310
|
|
|
310
311
|
const baseUrl = getBaseUrl(request);
|
|
311
|
-
return buildResponse(record.toJSON
|
|
312
|
+
return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl }), request.query?.include, record, {
|
|
312
313
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
|
|
313
314
|
baseUrl
|
|
314
315
|
});
|
|
@@ -345,7 +346,7 @@ export default class OrmRequest extends Request {
|
|
|
345
346
|
const recordAttributes = id !== undefined ? { id, ...sanitizedAttributes } : sanitizedAttributes;
|
|
346
347
|
const record = createRecord(model, recordAttributes as { [key: string]: unknown }, { serialize: false }) as unknown as OrmRecord;
|
|
347
348
|
|
|
348
|
-
return { data: record.toJSON
|
|
349
|
+
return { data: record.toJSON?.({ fields: modelFields }) };
|
|
349
350
|
};
|
|
350
351
|
|
|
351
352
|
const updateHandler: HandlerFn = async ({ body, params }) => {
|
|
@@ -381,7 +382,7 @@ export default class OrmRequest extends Request {
|
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
|
|
384
|
-
return { data: record.toJSON
|
|
385
|
+
return { data: record.toJSON?.() };
|
|
385
386
|
};
|
|
386
387
|
|
|
387
388
|
const deleteHandler: HandlerFn = ({ params }) => {
|
|
@@ -453,8 +454,9 @@ export default class OrmRequest extends Request {
|
|
|
453
454
|
const response = await handler(request, state);
|
|
454
455
|
|
|
455
456
|
// Persist to SQL database for write operations
|
|
456
|
-
|
|
457
|
-
|
|
457
|
+
const sqlDb = (Orm.instance as Orm).sqlDb;
|
|
458
|
+
if (sqlDb && WRITE_OPERATIONS.has(operation)) {
|
|
459
|
+
await sqlDb.persist(operation, this.model, context, response);
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
// Add response and relevant records to context
|
|
@@ -512,10 +514,10 @@ export default class OrmRequest extends Request {
|
|
|
512
514
|
let data: unknown;
|
|
513
515
|
if (info.isArray) {
|
|
514
516
|
// hasMany - return array
|
|
515
|
-
data = ((relatedData || []) as OrmRecord[]).map(r => r.toJSON
|
|
517
|
+
data = ((relatedData || []) as OrmRecord[]).map(r => r.toJSON?.({ baseUrl }));
|
|
516
518
|
} else {
|
|
517
519
|
// belongsTo - return single or null
|
|
518
|
-
data = relatedData ? (relatedData as OrmRecord).toJSON
|
|
520
|
+
data = relatedData ? (relatedData as OrmRecord).toJSON?.({ baseUrl }) : null;
|
|
519
521
|
}
|
|
520
522
|
|
|
521
523
|
return {
|
|
@@ -535,10 +537,15 @@ export default class OrmRequest extends Request {
|
|
|
535
537
|
let data: unknown;
|
|
536
538
|
if (info.isArray) {
|
|
537
539
|
// hasMany - return array of linkage objects
|
|
538
|
-
data = ((relatedData || []) as OrmRecord[])
|
|
540
|
+
data = ((relatedData || []) as OrmRecord[])
|
|
541
|
+
.filter((r): r is OrmRecord & { __model: { __name: string } } => Boolean(r.__model))
|
|
542
|
+
.map(r => ({ type: r.__model.__name, id: r.id }));
|
|
539
543
|
} else {
|
|
540
544
|
// belongsTo - return single linkage or null
|
|
541
|
-
|
|
545
|
+
const model = relatedData ? (relatedData as OrmRecord).__model : undefined;
|
|
546
|
+
data = model
|
|
547
|
+
? { type: model.__name, id: (relatedData as OrmRecord).id }
|
|
548
|
+
: null;
|
|
542
549
|
}
|
|
543
550
|
|
|
544
551
|
return {
|
|
@@ -68,7 +68,7 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
68
68
|
const viewDiffPrelim = diffViewSnapshots(previousViewSnapshotPrelim, currentViewSnapshotPrelim);
|
|
69
69
|
|
|
70
70
|
if (!viewDiffPrelim.hasChanges) {
|
|
71
|
-
log.db
|
|
71
|
+
log.db?.('No schema changes detected.');
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -118,7 +118,8 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
118
118
|
|
|
119
119
|
// Removed columns
|
|
120
120
|
for (const { model, column, type } of diff.removedColumns) {
|
|
121
|
-
const table = previousSnapshot[model]
|
|
121
|
+
const table = previousSnapshot[model]?.table;
|
|
122
|
+
if (!table) throw new Error(`Missing table name in snapshot for model "${model}"`);
|
|
122
123
|
upStatements.push(`ALTER TABLE "${table}" DROP COLUMN "${column}";`);
|
|
123
124
|
downStatements.push(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${type};`);
|
|
124
125
|
}
|
|
@@ -144,7 +145,8 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
144
145
|
|
|
145
146
|
// Removed foreign keys
|
|
146
147
|
for (const { model, column, references } of diff.removedForeignKeys) {
|
|
147
|
-
const table = previousSnapshot[model]
|
|
148
|
+
const table = previousSnapshot[model]?.table;
|
|
149
|
+
if (!table) throw new Error(`Missing table name in snapshot for model "${model}"`);
|
|
148
150
|
const refModel = Object.entries(previousSnapshot).find(([, s]) => s.table === references.references);
|
|
149
151
|
const fkType = refModel && refModel[1].idType === 'string' ? 'VARCHAR(255)' : 'INTEGER';
|
|
150
152
|
const constraintName = `fk_${table}_${column}`;
|
|
@@ -198,7 +200,7 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
198
200
|
const combinedHasChanges = diff.hasChanges || viewDiff.hasChanges;
|
|
199
201
|
|
|
200
202
|
if (!combinedHasChanges) {
|
|
201
|
-
log.db
|
|
203
|
+
log.db?.('No schema changes detected.');
|
|
202
204
|
return null;
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -216,7 +218,7 @@ export async function generateMigration(description: string = 'migration', confi
|
|
|
216
218
|
await createFile(path.join(migrationsPath, filename), content);
|
|
217
219
|
await createFile(path.join(migrationsPath, '.snapshot.json'), JSON.stringify(combinedSnapshot, null, 2));
|
|
218
220
|
|
|
219
|
-
log.db
|
|
221
|
+
log.db?.(`Migration generated: ${filename}`);
|
|
220
222
|
|
|
221
223
|
return { filename, content, snapshot: combinedSnapshot };
|
|
222
224
|
}
|