@stonyx/orm 0.2.1-beta.87 → 0.2.1-beta.89

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.
@@ -18,19 +18,22 @@ export class AggregateProperty {
18
18
  return null;
19
19
  return 0;
20
20
  }
21
+ if (this.aggregateType === 'count')
22
+ return relatedRecords.length;
23
+ const field = this.field;
24
+ if (!field)
25
+ return null;
21
26
  switch (this.aggregateType) {
22
- case 'count':
23
- return relatedRecords.length;
24
27
  case 'sum':
25
28
  return relatedRecords.reduce((acc, record) => {
26
- const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
29
+ const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
27
30
  return acc + (isNaN(val) ? 0 : val);
28
31
  }, 0);
29
32
  case 'avg': {
30
33
  let sum = 0;
31
34
  let count = 0;
32
35
  for (const record of relatedRecords) {
33
- const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
36
+ const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
34
37
  if (!isNaN(val)) {
35
38
  sum += val;
36
39
  count++;
@@ -41,7 +44,7 @@ export class AggregateProperty {
41
44
  case 'min': {
42
45
  let min = null;
43
46
  for (const record of relatedRecords) {
44
- const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
47
+ const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
45
48
  if (!isNaN(val) && (min === null || val < min))
46
49
  min = val;
47
50
  }
@@ -50,7 +53,7 @@ export class AggregateProperty {
50
53
  case 'max': {
51
54
  let max = null;
52
55
  for (const record of relatedRecords) {
53
- const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
56
+ const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
54
57
  if (!isNaN(val) && (max === null || val > max))
55
58
  max = val;
56
59
  }
package/dist/db.js CHANGED
@@ -21,6 +21,12 @@ import { createRecord } from './manage-record.js';
21
21
  import { createFile, createDirectory, updateFile, readFile, fileExists } from '@stonyx/utils/file';
22
22
  import path from 'path';
23
23
  export const dbKey = '__db';
24
+ function asDBRecord(value) {
25
+ if (typeof value !== 'object' || value === null || typeof value.format !== 'function') {
26
+ throw new Error('createRecord did not return a valid DBRecord');
27
+ }
28
+ return value;
29
+ }
24
30
  export default class DB {
25
31
  static instance;
26
32
  record;
@@ -66,7 +72,7 @@ export default class DB {
66
72
  const data = await readFile(dbFilePath, { json: true });
67
73
  const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
68
74
  if (hasData) {
69
- 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`);
75
+ 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`);
70
76
  process.exit(1);
71
77
  }
72
78
  }
@@ -76,7 +82,7 @@ export default class DB {
76
82
  if (dirExists) {
77
83
  const hasCollectionFiles = (await Promise.all(collectionKeys.map(key => fileExists(path.join(dirPath, `${key}.json`))))).some(Boolean);
78
84
  if (hasCollectionFiles) {
79
- 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`);
85
+ 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`);
80
86
  process.exit(1);
81
87
  }
82
88
  }
@@ -138,11 +144,11 @@ export default class DB {
138
144
  await updateFile(dbFilePath, skeleton, { json: true });
139
145
  else
140
146
  await createFile(dbFilePath, skeleton, { json: true });
141
- log.db(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
147
+ log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
142
148
  return;
143
149
  }
144
150
  await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
145
- log.db(`DB has been successfully saved to ${file}`);
151
+ log.db?.(`DB has been successfully saved to ${file}`);
146
152
  }
147
153
  async getRecord() {
148
154
  const { mode } = config.orm.db;
@@ -153,7 +159,7 @@ export default class DB {
153
159
  async getRecordFromFile() {
154
160
  const { file } = config.orm.db;
155
161
  const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
156
- return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
162
+ return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
157
163
  }
158
164
  async getRecordFromDirectory() {
159
165
  const dirPath = this.getDirPath();
@@ -161,7 +167,7 @@ export default class DB {
161
167
  const dirExists = await fileExists(dirPath);
162
168
  if (!dirExists) {
163
169
  const data = await this.create();
164
- return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
170
+ return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
165
171
  }
166
172
  const assembled = {};
167
173
  await Promise.all(collectionKeys.map(async (key) => {
@@ -169,6 +175,6 @@ export default class DB {
169
175
  const exists = await fileExists(filePath);
170
176
  assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
171
177
  }));
172
- return createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false });
178
+ return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
173
179
  }
174
180
  }
package/dist/hooks.js CHANGED
@@ -31,7 +31,9 @@ export function beforeHook(operation, model, handler) {
31
31
  if (!beforeHooks.has(key)) {
32
32
  beforeHooks.set(key, []);
33
33
  }
34
- beforeHooks.get(key).push(handler);
34
+ const hooks = beforeHooks.get(key);
35
+ if (hooks)
36
+ hooks.push(handler);
35
37
  // Return unsubscribe function
36
38
  return () => {
37
39
  const hooks = beforeHooks.get(key);
@@ -56,7 +58,9 @@ export function afterHook(operation, model, handler) {
56
58
  if (!afterHooks.has(key)) {
57
59
  afterHooks.set(key, []);
58
60
  }
59
- afterHooks.get(key).push(handler);
61
+ const hooks = afterHooks.get(key);
62
+ if (hooks)
63
+ hooks.push(handler);
60
64
  // Return unsubscribe function
61
65
  return () => {
62
66
  const hooks = afterHooks.get(key);
package/dist/main.js CHANGED
@@ -145,6 +145,8 @@ export default class Orm {
145
145
  static get db() {
146
146
  if (!Orm.initialized)
147
147
  throw new Error('ORM has not been initialized yet');
148
+ if (!Orm.instance.db)
149
+ throw new Error('ORM database has not been initialized');
148
150
  return Orm.instance.db;
149
151
  }
150
152
  getRecordClasses(modelName) {
@@ -170,7 +172,7 @@ export default class Orm {
170
172
  warn(message) {
171
173
  this.warnings.add(message);
172
174
  setTimeout(() => {
173
- this.warnings.forEach(warning => log.warn(warning));
175
+ this.warnings.forEach(warning => log.warn?.(warning));
174
176
  this.warnings.clear();
175
177
  }, 0);
176
178
  }
@@ -1,6 +1,7 @@
1
1
  import Orm, { store } from '@stonyx/orm';
2
2
  import OrmRecord from './record.js';
3
3
  import { getGlobalRegistry, getPendingRegistry, getPendingBelongsToRegistry, getBelongsToRegistry, getHasManyRegistry } from './relationships.js';
4
+ import { isOrmRecord } from './utils.js';
4
5
  const defaultOptions = {
5
6
  isDbRecord: false,
6
7
  serialize: true,
@@ -23,7 +24,7 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
23
24
  throw new Error(`Model store for '${modelName}' is not registered. Ensure the model is defined before creating records.`);
24
25
  assignRecordId(modelName, rawData);
25
26
  const existingRecord = modelStore.get(rawData.id);
26
- if (existingRecord) {
27
+ if (existingRecord instanceof OrmRecord) {
27
28
  // Update the existing record with new data so the last entry wins
28
29
  updateRecord(existingRecord, rawData, { ...options, update: true });
29
30
  return existingRecord;
@@ -52,7 +53,8 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
52
53
  }
53
54
  // Fulfill pending belongsTo relationships
54
55
  const pendingBelongsToQueue = getPendingBelongsToRegistry();
55
- const pendingBelongsTo = pendingBelongsToQueue.get(modelName)?.get(record.id);
56
+ const pendingBelongsToRaw = pendingBelongsToQueue.get(modelName)?.get(record.id);
57
+ const pendingBelongsTo = Array.isArray(pendingBelongsToRaw) ? pendingBelongsToRaw : undefined;
56
58
  if (pendingBelongsTo) {
57
59
  const belongsToReg = getBelongsToRegistry();
58
60
  const hasManyReg = getHasManyRegistry();
@@ -105,8 +107,12 @@ function assignRecordId(modelName, rawData) {
105
107
  rawData.__pendingSqlId = true;
106
108
  return;
107
109
  }
108
- const modelStore = Array.from(store.get(modelName).values());
109
- rawData.id = modelStore.length ? modelStore.at(-1).id + 1 : 1;
110
+ const storeMap = store.get(modelName);
111
+ if (!storeMap)
112
+ throw new Error(`Cannot assign record ID: model "${modelName}" not found in store`);
113
+ const modelStore = Array.from(storeMap.values()).filter(isOrmRecord);
114
+ const lastRecord = modelStore.at(-1);
115
+ rawData.id = lastRecord ? lastRecord.id + 1 : 1;
110
116
  }
111
117
  function isStringIdModel(modelName) {
112
118
  const modelClass = Orm.instance.getRecordClasses(modelName).modelClass;
@@ -4,7 +4,12 @@ import path from 'path';
4
4
  import config from 'stonyx/config';
5
5
  import log from 'stonyx/log';
6
6
  export async function generateMigration(description = 'migration') {
7
- const { migrationsDir } = config.orm.mysql;
7
+ const mysqlConfig = config.orm.mysql;
8
+ if (!mysqlConfig)
9
+ throw new Error('MySQL configuration (config.orm.mysql) is required for migration generation');
10
+ const { migrationsDir } = mysqlConfig;
11
+ if (!migrationsDir)
12
+ throw new Error('MySQL migrationsDir is required in config');
8
13
  const rootPath = config.rootPath;
9
14
  const migrationsPath = path.resolve(rootPath, migrationsDir);
10
15
  await createDirectory(migrationsPath);
@@ -20,7 +25,7 @@ export async function generateMigration(description = 'migration') {
20
25
  const previousViewSnapshotPrelim = extractViewsFromSnapshot(previousSnapshot);
21
26
  const viewDiffPrelim = diffViewSnapshots(previousViewSnapshotPrelim, currentViewSnapshotPrelim);
22
27
  if (!viewDiffPrelim.hasChanges) {
23
- log.db('No schema changes detected.');
28
+ log.db?.('No schema changes detected.');
24
29
  return null;
25
30
  }
26
31
  }
@@ -47,7 +52,9 @@ export async function generateMigration(description = 'migration') {
47
52
  }
48
53
  // Removed columns
49
54
  for (const { model, column, type } of diff.removedColumns) {
50
- const table = previousSnapshot[model].table;
55
+ const table = previousSnapshot[model]?.table;
56
+ if (!table)
57
+ throw new Error(`Missing table name in snapshot for model "${model}"`);
51
58
  upStatements.push(`ALTER TABLE \`${table}\` DROP COLUMN \`${column}\`;`);
52
59
  downStatements.push(`ALTER TABLE \`${table}\` ADD COLUMN \`${column}\` ${type};`);
53
60
  }
@@ -70,7 +77,9 @@ export async function generateMigration(description = 'migration') {
70
77
  }
71
78
  // Removed foreign keys
72
79
  for (const { model, column, references } of diff.removedForeignKeys) {
73
- const table = previousSnapshot[model].table;
80
+ const table = previousSnapshot[model]?.table;
81
+ if (!table)
82
+ throw new Error(`Missing table name in snapshot for model "${model}"`);
74
83
  // Resolve FK column type from the referenced table's PK type in previous snapshot
75
84
  const refModel = Object.entries(previousSnapshot).find(([, s]) => s.table === references.references);
76
85
  const fkType = refModel && refModel[1].idType === 'string' ? 'VARCHAR(255)' : 'INT';
@@ -119,7 +128,7 @@ export async function generateMigration(description = 'migration') {
119
128
  }
120
129
  const combinedHasChanges = diff.hasChanges || viewDiff.hasChanges;
121
130
  if (!combinedHasChanges) {
122
- log.db('No schema changes detected.');
131
+ log.db?.('No schema changes detected.');
123
132
  return null;
124
133
  }
125
134
  // Merge view snapshot into the main snapshot
@@ -133,7 +142,7 @@ export async function generateMigration(description = 'migration') {
133
142
  const content = `-- UP\n${upStatements.join('\n')}\n\n-- DOWN\n${downStatements.join('\n')}\n`;
134
143
  await createFile(path.join(migrationsPath, filename), content);
135
144
  await createFile(path.join(migrationsPath, '.snapshot.json'), JSON.stringify(combinedSnapshot, null, 2));
136
- log.db(`Migration generated: ${filename}`);
145
+ log.db?.(`Migration generated: ${filename}`);
137
146
  return { filename, content, snapshot: combinedSnapshot };
138
147
  }
139
148
  export async function loadLatestSnapshot(migrationsPath) {
@@ -32,7 +32,10 @@ export default class MysqlDB {
32
32
  MysqlDB.instance = this;
33
33
  this.deps = { ...defaultDeps, ...deps };
34
34
  this.pool = null;
35
- this.mysqlConfig = this.deps.config.orm.mysql;
35
+ const mysqlConfig = this.deps.config.orm.mysql;
36
+ if (!mysqlConfig)
37
+ throw new Error('MySQL configuration (config.orm.mysql) is required');
38
+ this.mysqlConfig = mysqlConfig;
36
39
  }
37
40
  requirePool() {
38
41
  if (!this.pool)
@@ -45,26 +48,28 @@ export default class MysqlDB {
45
48
  await this.loadMemoryRecords();
46
49
  }
47
50
  async startup() {
51
+ if (!this.mysqlConfig.migrationsDir)
52
+ throw new Error('MySQL migrationsDir is required in config');
48
53
  const migrationsPath = this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir);
49
54
  // Check for pending migrations
50
55
  const applied = await this.deps.getAppliedMigrations(this.requirePool(), this.mysqlConfig.migrationsTable);
51
56
  const files = await this.deps.getMigrationFiles(migrationsPath);
52
57
  const pending = files.filter(f => !applied.includes(f));
53
58
  if (pending.length > 0) {
54
- this.deps.log.db(`${pending.length} pending migration(s) found.`);
59
+ this.deps.log.db?.(`${pending.length} pending migration(s) found.`);
55
60
  const shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
56
61
  if (shouldApply) {
57
62
  for (const filename of pending) {
58
63
  const content = await this.deps.readFile(this.deps.path.join(migrationsPath, filename));
59
64
  const { up } = this.deps.parseMigrationFile(content);
60
65
  await this.deps.applyMigration(this.requirePool(), filename, up, this.mysqlConfig.migrationsTable);
61
- this.deps.log.db(`Applied migration: ${filename}`);
66
+ this.deps.log.db?.(`Applied migration: ${filename}`);
62
67
  }
63
68
  // Reload records after applying migrations
64
69
  await this.loadMemoryRecords();
65
70
  }
66
71
  else {
67
- this.deps.log.warn('Skipping pending migrations. Schema may be outdated.');
72
+ this.deps.log.warn?.('Skipping pending migrations. Schema may be outdated.');
68
73
  }
69
74
  }
70
75
  else if (files.length === 0) {
@@ -78,23 +83,25 @@ export default class MysqlDB {
78
83
  if (result) {
79
84
  const { up } = this.deps.parseMigrationFile(result.content);
80
85
  await this.deps.applyMigration(this.requirePool(), result.filename, up, this.mysqlConfig.migrationsTable);
81
- this.deps.log.db(`Applied migration: ${result.filename}`);
86
+ this.deps.log.db?.(`Applied migration: ${result.filename}`);
82
87
  await this.loadMemoryRecords();
83
88
  }
84
89
  }
85
90
  else {
86
- this.deps.log.warn('Skipping initial migration. Tables may not exist.');
91
+ this.deps.log.warn?.('Skipping initial migration. Tables may not exist.');
87
92
  }
88
93
  }
89
94
  }
90
95
  // Check for schema drift
91
96
  const schemas = this.deps.introspectModels();
97
+ if (!this.mysqlConfig.migrationsDir)
98
+ throw new Error('MySQL migrationsDir is required in config');
92
99
  const snapshot = await this.deps.loadLatestSnapshot(this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir));
93
100
  if (Object.keys(snapshot).length > 0) {
94
101
  const drift = this.deps.detectSchemaDrift(schemas, snapshot);
95
102
  if (drift.hasChanges) {
96
- this.deps.log.warn('Schema drift detected: models have changed since the last migration.');
97
- this.deps.log.warn('Run `stonyx db:generate-migration` to create a new migration.');
103
+ this.deps.log.warn?.('Schema drift detected: models have changed since the last migration.');
104
+ this.deps.log.warn?.('Run `stonyx db:generate-migration` to create a new migration.');
98
105
  }
99
106
  }
100
107
  }
@@ -117,7 +124,7 @@ export default class MysqlDB {
117
124
  // Check the model's memory flag — skip non-memory models
118
125
  const { modelClass } = Orm.instance.getRecordClasses(modelName);
119
126
  if (modelClass?.memory === false) {
120
- this.deps.log.db(`Skipping memory load for '${modelName}' (memory: false)`);
127
+ this.deps.log.db?.(`Skipping memory load for '${modelName}' (memory: false)`);
121
128
  continue;
122
129
  }
123
130
  const schema = schemas[modelName];
@@ -133,7 +140,7 @@ export default class MysqlDB {
133
140
  catch (error) {
134
141
  // Table may not exist yet (pre-migration) — skip gracefully
135
142
  if (isDbError(error) && error.code === 'ER_NO_SUCH_TABLE') {
136
- this.deps.log.db(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
143
+ this.deps.log.db?.(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
137
144
  continue;
138
145
  }
139
146
  throw error;
@@ -144,7 +151,7 @@ export default class MysqlDB {
144
151
  for (const [viewName, viewSchema] of Object.entries(viewSchemas)) {
145
152
  const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName);
146
153
  if (viewClass?.memory !== true) {
147
- this.deps.log.db(`Skipping memory load for view '${viewName}' (memory: false)`);
154
+ this.deps.log.db?.(`Skipping memory load for view '${viewName}' (memory: false)`);
148
155
  continue;
149
156
  }
150
157
  const schema = { table: viewSchema.viewName, columns: viewSchema.columns || {}, foreignKeys: viewSchema.foreignKeys || {} };
@@ -159,7 +166,7 @@ export default class MysqlDB {
159
166
  }
160
167
  catch (error) {
161
168
  if (isDbError(error) && error.code === 'ER_NO_SUCH_TABLE') {
162
- this.deps.log.db(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
169
+ this.deps.log.db?.(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
163
170
  continue;
164
171
  }
165
172
  throw error;
@@ -224,12 +231,13 @@ export default class MysqlDB {
224
231
  }
225
232
  if (!schema)
226
233
  return [];
227
- const { sql, values } = this.deps.buildSelect(schema.table, conditions);
234
+ const resolvedSchema = schema;
235
+ const { sql, values } = this.deps.buildSelect(resolvedSchema.table, conditions);
228
236
  try {
229
237
  const result = await this.requirePool().execute(sql, values);
230
238
  const rows = result[0];
231
239
  const records = rows.map(row => {
232
- const rawData = this._rowToRawData(row, schema);
240
+ const rawData = this._rowToRawData(row, resolvedSchema);
233
241
  return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
234
242
  });
235
243
  // Don't let memory:false records accumulate in the store
@@ -328,6 +336,8 @@ export default class MysqlDB {
328
336
  const pendingId = record.id;
329
337
  const realId = result.insertId;
330
338
  const modelStore = this.deps.store.get(modelName);
339
+ if (!modelStore)
340
+ throw new Error(`Model "${modelName}" not found in store during ID re-key`);
331
341
  modelStore.delete(pendingId);
332
342
  record.__data.id = realId;
333
343
  record.id = realId;
@@ -53,6 +53,8 @@ export function introspectModels() {
53
53
  }
54
54
  // Build foreign keys from belongsTo relationships
55
55
  for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
56
+ if (!targetModelName)
57
+ continue;
56
58
  const fkColumn = `${relName}_id`;
57
59
  foreignKeys[fkColumn] = {
58
60
  references: sanitizeTableName(getPluralName(targetModelName)),
@@ -122,7 +124,8 @@ export function getTopologicalOrder(schemas) {
122
124
  return;
123
125
  // Visit dependencies (belongsTo targets) first
124
126
  for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
125
- visit(targetModelName);
127
+ if (targetModelName)
128
+ visit(targetModelName);
126
129
  }
127
130
  order.push(name);
128
131
  }
@@ -158,11 +161,13 @@ export function introspectViews() {
158
161
  const relInfo = getRelationshipInfo(property);
159
162
  if (relInfo?.type === 'belongsTo') {
160
163
  relationships.belongsTo[key] = relInfo.modelName;
161
- const fkColumn = `${key}_id`;
162
- foreignKeys[fkColumn] = {
163
- references: sanitizeTableName(getPluralName(relInfo.modelName)),
164
- column: 'id',
165
- };
164
+ if (relInfo.modelName) {
165
+ const fkColumn = `${key}_id`;
166
+ foreignKeys[fkColumn] = {
167
+ references: sanitizeTableName(getPluralName(relInfo.modelName)),
168
+ column: 'id',
169
+ };
170
+ }
166
171
  }
167
172
  else if (relInfo?.type === 'hasMany') {
168
173
  relationships.hasMany[key] = relInfo.modelName;
@@ -4,6 +4,7 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
4
4
  import { getPluralName } from './plural-registry.js';
5
5
  import { getBeforeHooks, getAfterHooks } from './hooks.js';
6
6
  import config from 'stonyx/config';
7
+ import { isOrmRecord } from './utils.js';
7
8
  const methodAccessMap = {
8
9
  GET: 'read',
9
10
  POST: 'create',
@@ -69,7 +70,7 @@ function buildResponse(data, includeParam, recordOrRecords, options = {}) {
69
70
  return response;
70
71
  const includedRecords = collectIncludedRecords(recordOrRecords, includes);
71
72
  if (includedRecords.length > 0) {
72
- response.included = includedRecords.map(record => record.toJSON({ baseUrl }));
73
+ response.included = includedRecords.map(record => record.toJSON?.({ baseUrl }));
73
74
  }
74
75
  return response;
75
76
  }
@@ -91,11 +92,13 @@ function traverseIncludePath(currentRecords, includePath, depth, seen, included)
91
92
  continue;
92
93
  // Handle both belongsTo (single) and hasMany (array)
93
94
  const recordsToProcess = Array.isArray(relatedRecords)
94
- ? relatedRecords
95
- : [relatedRecords];
95
+ ? relatedRecords.filter(isOrmRecord)
96
+ : isOrmRecord(relatedRecords) ? [relatedRecords] : [];
96
97
  for (const relatedRecord of recordsToProcess) {
97
98
  if (!relatedRecord)
98
99
  continue;
100
+ if (!relatedRecord.__model)
101
+ continue;
99
102
  const type = relatedRecord.__model.__name;
100
103
  const id = relatedRecord.id;
101
104
  // Initialize Set for this type if needed
@@ -196,7 +199,7 @@ export default class OrmRequest extends Request {
196
199
  const modelRelationships = getModelRelationships(model);
197
200
  // Define raw handlers first
198
201
  const getCollectionHandler = async (request, { filter: accessFilter }) => {
199
- const allRecords = await store.findAll(model);
202
+ const allRecords = (await store.findAll(model)).filter(isOrmRecord);
200
203
  const queryFilters = parseFilters(request.query);
201
204
  const queryFilterPredicate = createFilterPredicate(queryFilters);
202
205
  const fieldsMap = parseFields(request.query);
@@ -207,7 +210,7 @@ export default class OrmRequest extends Request {
207
210
  if (queryFilterPredicate)
208
211
  recordsToReturn = recordsToReturn.filter(queryFilterPredicate);
209
212
  const baseUrl = getBaseUrl(request);
210
- const data = recordsToReturn.map(record => record.toJSON({ fields: modelFields, baseUrl }));
213
+ const data = recordsToReturn.map(record => record.toJSON?.({ fields: modelFields, baseUrl }));
211
214
  return buildResponse(data, request.query?.include, recordsToReturn, {
212
215
  links: { self: `${baseUrl}/${pluralizedModel}` },
213
216
  baseUrl
@@ -220,7 +223,7 @@ export default class OrmRequest extends Request {
220
223
  const fieldsMap = parseFields(request.query);
221
224
  const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
222
225
  const baseUrl = getBaseUrl(request);
223
- return buildResponse(record.toJSON({ fields: modelFields, baseUrl }), request.query?.include, record, {
226
+ return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl }), request.query?.include, record, {
224
227
  links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
225
228
  baseUrl
226
229
  });
@@ -245,11 +248,17 @@ export default class OrmRequest extends Request {
245
248
  }
246
249
  }
247
250
  const recordAttributes = id !== undefined ? { id, ...sanitizedAttributes } : sanitizedAttributes;
248
- const record = createRecord(model, recordAttributes, { serialize: false });
249
- return { data: record.toJSON({ fields: modelFields }) };
251
+ const created = createRecord(model, recordAttributes, { serialize: false });
252
+ const record = isOrmRecord(created) ? created : null;
253
+ if (!record)
254
+ return 500;
255
+ return { data: record.toJSON?.({ fields: modelFields }) };
250
256
  };
251
257
  const updateHandler = async ({ body, params }) => {
252
- const record = await store.find(model, getId(params));
258
+ const found = await store.find(model, getId(params));
259
+ if (!found || !isOrmRecord(found))
260
+ return 404;
261
+ const record = found;
253
262
  const { attributes, relationships: rels } = (body?.data || {});
254
263
  if (!attributes && !rels)
255
264
  return 400; // Bad request
@@ -277,7 +286,7 @@ export default class OrmRequest extends Request {
277
286
  updateRecord(record, relUpdates);
278
287
  }
279
288
  }
280
- return { data: record.toJSON() };
289
+ return { data: record.toJSON?.() };
281
290
  };
282
291
  const deleteHandler = ({ params }) => {
283
292
  store.remove(model, getId(params));
@@ -340,8 +349,9 @@ export default class OrmRequest extends Request {
340
349
  // Execute main handler
341
350
  const response = await handler(request, state);
342
351
  // Persist to SQL database for write operations
343
- if (Orm.instance.sqlDb && WRITE_OPERATIONS.has(operation)) {
344
- await Orm.instance.sqlDb.persist(operation, this.model, context, response);
352
+ const sqlDb = Orm.instance.sqlDb;
353
+ if (sqlDb && WRITE_OPERATIONS.has(operation)) {
354
+ await sqlDb.persist(operation, this.model, context, response);
345
355
  }
346
356
  // Add response and relevant records to context
347
357
  context.response = response;
@@ -390,11 +400,12 @@ export default class OrmRequest extends Request {
390
400
  let data;
391
401
  if (info.isArray) {
392
402
  // hasMany - return array
393
- data = (relatedData || []).map(r => r.toJSON({ baseUrl }));
403
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
404
+ data = related.map(r => r.toJSON?.({ baseUrl }));
394
405
  }
395
406
  else {
396
407
  // belongsTo - return single or null
397
- data = relatedData ? relatedData.toJSON({ baseUrl }) : null;
408
+ data = isOrmRecord(relatedData) ? relatedData.toJSON?.({ baseUrl }) : null;
398
409
  }
399
410
  return {
400
411
  links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}/${dasherizedName}` },
@@ -411,11 +422,19 @@ export default class OrmRequest extends Request {
411
422
  let data;
412
423
  if (info.isArray) {
413
424
  // hasMany - return array of linkage objects
414
- data = (relatedData || []).map(r => ({ type: r.__model.__name, id: r.id }));
425
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
426
+ data = related
427
+ .filter((r) => Boolean(r.__model))
428
+ .map(r => ({ type: r.__model.__name, id: r.id }));
415
429
  }
416
430
  else {
417
431
  // belongsTo - return single linkage or null
418
- data = relatedData ? { type: relatedData.__model.__name, id: relatedData.id } : null;
432
+ if (isOrmRecord(relatedData) && relatedData.__model) {
433
+ data = { type: relatedData.__model.__name, id: relatedData.id };
434
+ }
435
+ else {
436
+ data = null;
437
+ }
419
438
  }
420
439
  return {
421
440
  links: {
@@ -19,7 +19,7 @@ export async function generateMigration(description = 'migration', configKey = '
19
19
  const previousViewSnapshotPrelim = extractViewsFromSnapshot(previousSnapshot);
20
20
  const viewDiffPrelim = diffViewSnapshots(previousViewSnapshotPrelim, currentViewSnapshotPrelim);
21
21
  if (!viewDiffPrelim.hasChanges) {
22
- log.db('No schema changes detected.');
22
+ log.db?.('No schema changes detected.');
23
23
  return null;
24
24
  }
25
25
  }
@@ -59,7 +59,9 @@ export async function generateMigration(description = 'migration', configKey = '
59
59
  }
60
60
  // Removed columns
61
61
  for (const { model, column, type } of diff.removedColumns) {
62
- const table = previousSnapshot[model].table;
62
+ const table = previousSnapshot[model]?.table;
63
+ if (!table)
64
+ throw new Error(`Missing table name in snapshot for model "${model}"`);
63
65
  upStatements.push(`ALTER TABLE "${table}" DROP COLUMN "${column}";`);
64
66
  downStatements.push(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${type};`);
65
67
  }
@@ -82,7 +84,9 @@ export async function generateMigration(description = 'migration', configKey = '
82
84
  }
83
85
  // Removed foreign keys
84
86
  for (const { model, column, references } of diff.removedForeignKeys) {
85
- const table = previousSnapshot[model].table;
87
+ const table = previousSnapshot[model]?.table;
88
+ if (!table)
89
+ throw new Error(`Missing table name in snapshot for model "${model}"`);
86
90
  const refModel = Object.entries(previousSnapshot).find(([, s]) => s.table === references.references);
87
91
  const fkType = refModel && refModel[1].idType === 'string' ? 'VARCHAR(255)' : 'INTEGER';
88
92
  const constraintName = `fk_${table}_${column}`;
@@ -131,7 +135,7 @@ export async function generateMigration(description = 'migration', configKey = '
131
135
  }
132
136
  const combinedHasChanges = diff.hasChanges || viewDiff.hasChanges;
133
137
  if (!combinedHasChanges) {
134
- log.db('No schema changes detected.');
138
+ log.db?.('No schema changes detected.');
135
139
  return null;
136
140
  }
137
141
  // Merge view snapshot into the main snapshot
@@ -145,7 +149,7 @@ export async function generateMigration(description = 'migration', configKey = '
145
149
  const content = `-- UP\n${upStatements.join('\n')}\n\n-- DOWN\n${downStatements.join('\n')}\n`;
146
150
  await createFile(path.join(migrationsPath, filename), content);
147
151
  await createFile(path.join(migrationsPath, '.snapshot.json'), JSON.stringify(combinedSnapshot, null, 2));
148
- log.db(`Migration generated: ${filename}`);
152
+ log.db?.(`Migration generated: ${filename}`);
149
153
  return { filename, content, snapshot: combinedSnapshot };
150
154
  }
151
155
  export async function loadLatestSnapshot(migrationsPath) {