@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.
@@ -56,20 +56,20 @@ export default class PostgresDB {
56
56
  const files = await this.deps.getMigrationFiles(migrationsPath);
57
57
  const pending = files.filter(f => !applied.includes(f));
58
58
  if (pending.length > 0) {
59
- this.deps.log.db(`${pending.length} pending migration(s) found.`);
59
+ this.deps.log.db?.(`${pending.length} pending migration(s) found.`);
60
60
  const shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
61
61
  if (shouldApply) {
62
62
  for (const filename of pending) {
63
63
  const content = await this.deps.readFile(this.deps.path.join(migrationsPath, filename));
64
64
  const { up } = this.deps.parseMigrationFile(content);
65
65
  await this.deps.applyMigration(this.requirePool(), filename, up, this.pgConfig.migrationsTable);
66
- this.deps.log.db(`Applied migration: ${filename}`);
66
+ this.deps.log.db?.(`Applied migration: ${filename}`);
67
67
  }
68
68
  // Reload records after applying migrations
69
69
  await this.loadMemoryRecords();
70
70
  }
71
71
  else {
72
- this.deps.log.warn('Skipping pending migrations. Schema may be outdated.');
72
+ this.deps.log.warn?.('Skipping pending migrations. Schema may be outdated.');
73
73
  }
74
74
  }
75
75
  else if (files.length === 0) {
@@ -83,12 +83,12 @@ export default class PostgresDB {
83
83
  if (result) {
84
84
  const { up } = this.deps.parseMigrationFile(result.content);
85
85
  await this.deps.applyMigration(this.requirePool(), result.filename, up, this.pgConfig.migrationsTable);
86
- this.deps.log.db(`Applied migration: ${result.filename}`);
86
+ this.deps.log.db?.(`Applied migration: ${result.filename}`);
87
87
  await this.loadMemoryRecords();
88
88
  }
89
89
  }
90
90
  else {
91
- this.deps.log.warn('Skipping initial migration. Tables may not exist.');
91
+ this.deps.log.warn?.('Skipping initial migration. Tables may not exist.');
92
92
  }
93
93
  }
94
94
  }
@@ -98,8 +98,8 @@ export default class PostgresDB {
98
98
  if (Object.keys(snapshot).length > 0) {
99
99
  const drift = this.deps.detectSchemaDrift(schemas, snapshot);
100
100
  if (drift.hasChanges) {
101
- this.deps.log.warn('Schema drift detected: models have changed since the last migration.');
102
- this.deps.log.warn('Run `stonyx db:generate-migration` to create a new migration.');
101
+ this.deps.log.warn?.('Schema drift detected: models have changed since the last migration.');
102
+ this.deps.log.warn?.('Run `stonyx db:generate-migration` to create a new migration.');
103
103
  }
104
104
  }
105
105
  }
@@ -121,7 +121,7 @@ export default class PostgresDB {
121
121
  for (const modelName of order) {
122
122
  const { modelClass } = Orm.instance.getRecordClasses(modelName);
123
123
  if (modelClass?.memory === false) {
124
- this.deps.log.db(`Skipping memory load for '${modelName}' (memory: false)`);
124
+ this.deps.log.db?.(`Skipping memory load for '${modelName}' (memory: false)`);
125
125
  continue;
126
126
  }
127
127
  const schema = schemas[modelName];
@@ -136,7 +136,7 @@ export default class PostgresDB {
136
136
  catch (error) {
137
137
  // 42P01 = undefined_table (PG equivalent of ER_NO_SUCH_TABLE)
138
138
  if (isDbError(error) && error.code === '42P01') {
139
- this.deps.log.db(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
139
+ this.deps.log.db?.(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
140
140
  continue;
141
141
  }
142
142
  throw error;
@@ -147,7 +147,7 @@ export default class PostgresDB {
147
147
  for (const [viewName, viewSchema] of Object.entries(viewSchemas)) {
148
148
  const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName);
149
149
  if (viewClass?.memory !== true) {
150
- this.deps.log.db(`Skipping memory load for view '${viewName}' (memory: false)`);
150
+ this.deps.log.db?.(`Skipping memory load for view '${viewName}' (memory: false)`);
151
151
  continue;
152
152
  }
153
153
  const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
@@ -170,7 +170,7 @@ export default class PostgresDB {
170
170
  }
171
171
  catch (error) {
172
172
  if (isDbError(error) && error.code === '42P01') {
173
- this.deps.log.db(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
173
+ this.deps.log.db?.(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
174
174
  continue;
175
175
  }
176
176
  throw error;
@@ -250,11 +250,12 @@ export default class PostgresDB {
250
250
  }
251
251
  if (!schema)
252
252
  return [];
253
- const { sql, values } = this.deps.buildSelect(schema.table, conditions);
253
+ const resolvedSchema = schema;
254
+ const { sql, values } = this.deps.buildSelect(resolvedSchema.table, conditions);
254
255
  try {
255
256
  const result = await this.requirePool().query(sql, values);
256
257
  const records = result.rows.map(row => {
257
- const rawData = this._rowToRawData(row, schema);
258
+ const rawData = this._rowToRawData(row, resolvedSchema);
258
259
  return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
259
260
  });
260
261
  for (const record of records) {
@@ -60,6 +60,8 @@ export function introspectModels() {
60
60
  }
61
61
  // Build foreign keys from belongsTo relationships
62
62
  for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
63
+ if (!targetModelName)
64
+ continue;
63
65
  const fkColumn = `${relName}_id`;
64
66
  foreignKeys[fkColumn] = {
65
67
  references: sanitizeTableName(getPluralName(targetModelName)),
@@ -139,7 +141,8 @@ export function getTopologicalOrder(schemas) {
139
141
  return;
140
142
  // Visit dependencies (belongsTo targets) first
141
143
  for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
142
- visit(targetModelName);
144
+ if (targetModelName)
145
+ visit(targetModelName);
143
146
  }
144
147
  order.push(name);
145
148
  }
@@ -175,11 +178,13 @@ export function introspectViews() {
175
178
  const relInfo = getRelationshipInfo(property);
176
179
  if (relInfo?.type === 'belongsTo') {
177
180
  relationships.belongsTo[key] = relInfo.modelName;
178
- const fkColumn = `${key}_id`;
179
- foreignKeys[fkColumn] = {
180
- references: sanitizeTableName(getPluralName(relInfo.modelName)),
181
- column: 'id',
182
- };
181
+ if (relInfo.modelName) {
182
+ const fkColumn = `${key}_id`;
183
+ foreignKeys[fkColumn] = {
184
+ references: sanitizeTableName(getPluralName(relInfo.modelName)),
185
+ column: 'id',
186
+ };
187
+ }
183
188
  }
184
189
  else if (relInfo?.type === 'hasMany') {
185
190
  relationships.hasMany[key] = relInfo.modelName;
@@ -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
- let accessFiles = {};
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
@@ -1,6 +1,9 @@
1
1
  import Orm, { relationships } from '@stonyx/orm';
2
2
  import { TYPES, getHasManyRegistry, getBelongsToRegistry, getPendingRegistry } from './relationships.js';
3
3
  import ViewResolver from './view-resolver.js';
4
+ function isStoreRecord(value) {
5
+ return typeof value === 'object' && value !== null && '__data' in value;
6
+ }
4
7
  export default class Store {
5
8
  static instance;
6
9
  data = new Map();
@@ -55,7 +58,7 @@ export default class Store {
55
58
  const records = await resolver.resolveAll();
56
59
  if (!conditions || Object.keys(conditions).length === 0)
57
60
  return records;
58
- return records.filter((record) => Object.entries(conditions).every(([key, value]) => record.__data[key] === value));
61
+ return records.filter((record) => Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value));
59
62
  }
60
63
  // For memory: true models without conditions, return from store
61
64
  if (this._isMemoryModel(modelName) && !conditions) {
@@ -73,7 +76,7 @@ export default class Store {
73
76
  const records = Array.from(modelStore.values());
74
77
  if (!conditions || Object.keys(conditions).length === 0)
75
78
  return records;
76
- return records.filter((record) => Object.entries(conditions).every(([key, value]) => record.__data[key] === value));
79
+ return records.filter((record) => Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value));
77
80
  }
78
81
  /**
79
82
  * Async query — always hits MySQL, never reads from memory cache.
@@ -90,7 +93,7 @@ export default class Store {
90
93
  const records = Array.from(modelStore.values());
91
94
  if (Object.keys(conditions).length === 0)
92
95
  return records;
93
- return records.filter((record) => Object.entries(conditions).every(([key, value]) => record.__data[key] === value));
96
+ return records.filter((record) => Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value));
94
97
  }
95
98
  /**
96
99
  * Check if a model is configured for in-memory storage.
@@ -119,11 +122,14 @@ export default class Store {
119
122
  console.warn(`[Store] Cannot unload record: model "${model}" not found in store`);
120
123
  return;
121
124
  }
122
- const record = modelStore.get(id);
123
- if (!record) {
125
+ if (typeof id !== 'string' && typeof id !== 'number')
126
+ return;
127
+ const raw = modelStore.get(id);
128
+ if (!raw || !isStoreRecord(raw)) {
124
129
  console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store`);
125
130
  return;
126
131
  }
132
+ const record = raw;
127
133
  const { toUnload, visited } = options.includeChildren
128
134
  ? this._buildUnloadQueue(record, options)
129
135
  : { toUnload: [{ record, modelName: model, recordId: id }], visited: new Set([`${model}:${id}`]) };
@@ -148,8 +154,11 @@ export default class Store {
148
154
  this.unloadRecord(model, id, options);
149
155
  }
150
156
  }
151
- for (const relationshipType of TYPES)
152
- relationships.get(relationshipType).delete(model);
157
+ for (const relationshipType of TYPES) {
158
+ const reg = relationships.get(relationshipType);
159
+ if (reg instanceof Map)
160
+ reg.delete(model);
161
+ }
153
162
  }
154
163
  _removeFromHasManyArrays(modelName, recordId, visited) {
155
164
  const hasManyRegistry = getHasManyRegistry();
@@ -162,7 +171,7 @@ export default class Store {
162
171
  // Don't modify arrays of records being deleted
163
172
  if (visited.has(sourceKey))
164
173
  continue;
165
- const index = hasManyArray.findIndex(r => r && r.id === recordId);
174
+ const index = hasManyArray.findIndex(r => r && isStoreRecord(r) && r.id === recordId);
166
175
  if (index !== -1)
167
176
  hasManyArray.splice(index, 1);
168
177
  }
@@ -175,16 +184,20 @@ export default class Store {
175
184
  if (!targetModelMap)
176
185
  continue;
177
186
  for (const [sourceRecordId, belongsToRecord] of targetModelMap) {
178
- if (belongsToRecord && belongsToRecord.id === recordId) {
187
+ if (belongsToRecord && isStoreRecord(belongsToRecord) && belongsToRecord.id === recordId) {
179
188
  const sourceKey = `${sourceModel}:${sourceRecordId}`;
180
189
  if (visited.has(sourceKey))
181
190
  continue;
182
191
  targetModelMap.set(sourceRecordId, null);
183
- const sourceRecord = this.get(sourceModel, sourceRecordId);
184
- if (sourceRecord && sourceRecord.__relationships) {
185
- for (const [key, value] of Object.entries(sourceRecord.__relationships)) {
186
- if (value && value.id === recordId) {
187
- sourceRecord.__relationships[key] = null;
192
+ if (typeof sourceRecordId !== 'string' && typeof sourceRecordId !== 'number')
193
+ continue;
194
+ const sourceRaw = this.get(sourceModel, sourceRecordId);
195
+ if (!sourceRaw || !isStoreRecord(sourceRaw))
196
+ continue;
197
+ if (sourceRaw.__relationships) {
198
+ for (const [key, value] of Object.entries(sourceRaw.__relationships)) {
199
+ if (value && isStoreRecord(value) && value.id === recordId) {
200
+ sourceRaw.__relationships[key] = null;
188
201
  }
189
202
  }
190
203
  }
@@ -219,11 +232,11 @@ export default class Store {
219
232
  // hasMany children - always include
220
233
  if (Array.isArray(value)) {
221
234
  for (const childRecord of value) {
222
- if (childRecord)
223
- children.push({ childRecord: childRecord, relationshipKey: key, type: 'hasMany' });
235
+ if (childRecord && isStoreRecord(childRecord))
236
+ children.push({ childRecord, relationshipKey: key, type: 'hasMany' });
224
237
  }
225
238
  }
226
- else if (value && !this._isBidirectionalRelationship(record.__model.__name, value.__model.__name)) {
239
+ else if (value && isStoreRecord(value) && value.__model && !this._isBidirectionalRelationship(record.__model.__name, value.__model.__name)) {
227
240
  children.push({ childRecord: value, relationshipKey: key, type: 'belongsTo' });
228
241
  }
229
242
  }
@@ -245,6 +258,8 @@ export default class Store {
245
258
  }];
246
259
  while (queue.length > 0) {
247
260
  const item = queue.shift();
261
+ if (!item)
262
+ break;
248
263
  const key = `${item.modelName}:${item.recordId}`;
249
264
  if (visited.has(key))
250
265
  continue;
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import type { OrmRecord } from './types/orm-types.js';
1
2
  export declare function isDbError(error: unknown): error is {
2
3
  code: string;
3
4
  message: string;
4
5
  };
6
+ export declare function isOrmRecord(value: unknown): value is OrmRecord;
5
7
  export declare function pluralize(word: string): string;
package/dist/utils.js CHANGED
@@ -2,11 +2,15 @@ import { pluralize as basePluralize } from '@stonyx/utils/string';
2
2
  export function isDbError(error) {
3
3
  return typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' && 'message' in error && typeof error.message === 'string';
4
4
  }
5
+ export function isOrmRecord(value) {
6
+ return typeof value === 'object' && value !== null && '__data' in value && '__relationships' in value;
7
+ }
5
8
  // Wrapper to handle dasherized model names (e.g., "access-link" → "access-links")
6
9
  export function pluralize(word) {
7
10
  if (word.includes('-')) {
8
11
  const parts = word.split('-');
9
- const pluralizedLast = basePluralize(parts.pop());
12
+ const last = parts.pop();
13
+ const pluralizedLast = basePluralize(last);
10
14
  return [...parts, pluralizedLast].join('-');
11
15
  }
12
16
  return basePluralize(word);
@@ -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).push(record);
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
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.87",
7
+ "version": "0.2.1-beta.89",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
package/src/aggregates.ts CHANGED
@@ -27,13 +27,15 @@ export class AggregateProperty {
27
27
  return 0;
28
28
  }
29
29
 
30
- switch (this.aggregateType) {
31
- case 'count':
32
- return relatedRecords.length;
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?.[this.field!] as string ?? record?.[this.field!] as string);
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?.[this.field!] as string ?? record?.[this.field!] as string);
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?.[this.field!] as string ?? record?.[this.field!] as string);
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?.[this.field!] as string ?? record?.[this.field!] as string);
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
@@ -29,6 +29,13 @@ interface DBRecord {
29
29
  [key: string]: unknown;
30
30
  }
31
31
 
32
+ function asDBRecord(value: unknown): DBRecord {
33
+ if (typeof value !== 'object' || value === null || typeof (value as DBRecord).format !== 'function') {
34
+ throw new Error('createRecord did not return a valid DBRecord');
35
+ }
36
+ return value as DBRecord;
37
+ }
38
+
32
39
  export default class DB {
33
40
  static instance: DB;
34
41
  record!: DBRecord;
@@ -49,7 +56,7 @@ export default class DB {
49
56
  }
50
57
 
51
58
  getCollectionKeys(): string[] {
52
- const SchemaClass = (Orm.instance as Orm).models[`${dbKey}Model`] as new () => Record<string, unknown>;
59
+ const SchemaClass = Orm.instance.models[`${dbKey}Model`] as new () => Record<string, unknown>;
53
60
  const instance = new SchemaClass();
54
61
  const keys: string[] = [];
55
62
 
@@ -84,7 +91,7 @@ export default class DB {
84
91
  const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
85
92
 
86
93
  if (hasData) {
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`);
94
+ 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
95
  process.exit(1);
89
96
  }
90
97
  }
@@ -97,7 +104,7 @@ export default class DB {
97
104
  )).some(Boolean);
98
105
 
99
106
  if (hasCollectionFiles) {
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`);
107
+ 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
108
  process.exit(1);
102
109
  }
103
110
  }
@@ -108,7 +115,7 @@ export default class DB {
108
115
  const { autosave, saveInterval } = config.orm.db;
109
116
 
110
117
  store.set(dbKey, new Map());
111
- (Orm.instance as Orm).models[`${dbKey}Model`] = await this.getSchema();
118
+ Orm.instance.models[`${dbKey}Model`] = await this.getSchema();
112
119
 
113
120
  await this.validateMode();
114
121
  this.record = await this.getRecord();
@@ -176,13 +183,13 @@ export default class DB {
176
183
  if (dbFileExists) await updateFile(dbFilePath, skeleton, { json: true });
177
184
  else await createFile(dbFilePath, skeleton, { json: true });
178
185
 
179
- log.db!(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
186
+ log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
180
187
  return;
181
188
  }
182
189
 
183
190
  await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
184
191
 
185
- log.db!(`DB has been successfully saved to ${file}`);
192
+ log.db?.(`DB has been successfully saved to ${file}`);
186
193
  }
187
194
 
188
195
  async getRecord(): Promise<DBRecord> {
@@ -198,7 +205,7 @@ export default class DB {
198
205
 
199
206
  const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
200
207
 
201
- return createRecord(dbKey, data as Record<string, unknown>, { isDbRecord: true, serialize: false, transform: false }) as unknown as DBRecord;
208
+ return asDBRecord(createRecord(dbKey, data as Record<string, unknown>, { isDbRecord: true, serialize: false, transform: false }));
202
209
  }
203
210
 
204
211
  async getRecordFromDirectory(): Promise<DBRecord> {
@@ -208,7 +215,7 @@ export default class DB {
208
215
 
209
216
  if (!dirExists) {
210
217
  const data = await this.create();
211
- return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }) as unknown as DBRecord;
218
+ return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
212
219
  }
213
220
 
214
221
  const assembled: Record<string, unknown> = {};
@@ -220,6 +227,6 @@ export default class DB {
220
227
  assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
221
228
  }));
222
229
 
223
- return createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }) as unknown as DBRecord;
230
+ return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
224
231
  }
225
232
  }
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)!.push(handler);
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)!.push(handler);
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
- return Orm.instance.db!;
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!(warning));
222
+ this.warnings.forEach(warning => log.warn?.(warning));
222
223
  this.warnings.clear();
223
224
  }, 0);
224
225
  }
@@ -2,6 +2,7 @@ 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
4
  import type Serializer from './serializer.js';
5
+ import { isOrmRecord } from './utils.js';
5
6
 
6
7
  interface CreateRecordOptions {
7
8
  isDbRecord?: boolean;
@@ -45,10 +46,10 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
45
46
  assignRecordId(modelName, rawData);
46
47
  const existingRecord = modelStore.get(rawData.id as number | string);
47
48
 
48
- if (existingRecord) {
49
+ if (existingRecord instanceof OrmRecord) {
49
50
  // Update the existing record with new data so the last entry wins
50
- updateRecord(existingRecord as OrmRecord, rawData, { ...options, update: true });
51
- return existingRecord as OrmRecord;
51
+ updateRecord(existingRecord, rawData, { ...options, update: true });
52
+ return existingRecord;
52
53
  }
53
54
 
54
55
  const recordClasses = orm.getRecordClasses(modelName);
@@ -77,7 +78,8 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
77
78
 
78
79
  // Fulfill pending belongsTo relationships
79
80
  const pendingBelongsToQueue = getPendingBelongsToRegistry();
80
- const pendingBelongsTo = pendingBelongsToQueue.get(modelName)?.get(record.id) as PendingBelongsToEntry[] | undefined;
81
+ const pendingBelongsToRaw = pendingBelongsToQueue.get(modelName)?.get(record.id);
82
+ const pendingBelongsTo = Array.isArray(pendingBelongsToRaw) ? pendingBelongsToRaw as PendingBelongsToEntry[] : undefined;
81
83
 
82
84
  if (pendingBelongsTo) {
83
85
  const belongsToReg = getBelongsToRegistry();
@@ -142,8 +144,11 @@ function assignRecordId(modelName: string, rawData: { [key: string]: unknown }):
142
144
  return;
143
145
  }
144
146
 
145
- const modelStore = Array.from(store.get(modelName)!.values()) as OrmRecord[];
146
- rawData.id = modelStore.length ? (modelStore.at(-1)!.id as number) + 1 : 1;
147
+ const storeMap = store.get(modelName);
148
+ if (!storeMap) throw new Error(`Cannot assign record ID: model "${modelName}" not found in store`);
149
+ const modelStore = Array.from(storeMap.values()).filter(isOrmRecord);
150
+ const lastRecord = modelStore.at(-1);
151
+ rawData.id = lastRecord ? (lastRecord.id as number) + 1 : 1;
147
152
  }
148
153
 
149
154
  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 { migrationsDir } = config.orm.mysql!;
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!('No schema changes detected.');
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].table!;
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].table!;
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!('No schema changes detected.');
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!(`Migration generated: ${filename}`);
210
+ log.db?.(`Migration generated: ${filename}`);
206
211
 
207
212
  return { filename, content, snapshot: combinedSnapshot };
208
213
  }