@stonyx/orm 0.2.1-beta.86 → 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.
Files changed (47) hide show
  1. package/dist/aggregates.js +9 -6
  2. package/dist/db.js +4 -4
  3. package/dist/hooks.js +6 -2
  4. package/dist/main.js +3 -1
  5. package/dist/manage-record.js +6 -2
  6. package/dist/mysql/migration-generator.js +15 -6
  7. package/dist/mysql/migration-runner.js +5 -0
  8. package/dist/mysql/mysql-db.js +24 -14
  9. package/dist/mysql/schema-introspector.js +11 -6
  10. package/dist/orm-request.js +19 -11
  11. package/dist/postgres/connection.js +3 -1
  12. package/dist/postgres/migration-generator.js +9 -5
  13. package/dist/postgres/migration-runner.js +5 -0
  14. package/dist/postgres/postgres-db.js +14 -13
  15. package/dist/postgres/schema-introspector.js +11 -6
  16. package/dist/postgres/type-map.js +3 -0
  17. package/dist/relationships.js +2 -0
  18. package/dist/setup-rest-server.js +4 -6
  19. package/dist/store.js +2 -0
  20. package/dist/timescale/query-builder.d.ts +2 -0
  21. package/dist/timescale/query-builder.js +30 -2
  22. package/dist/utils.js +2 -1
  23. package/dist/view-resolver.js +3 -1
  24. package/package.json +1 -1
  25. package/src/aggregates.ts +9 -7
  26. package/src/belongs-to.ts +1 -1
  27. package/src/db.ts +4 -4
  28. package/src/hooks.ts +4 -2
  29. package/src/main.ts +3 -2
  30. package/src/manage-record.ts +5 -2
  31. package/src/mysql/migration-generator.ts +12 -7
  32. package/src/mysql/migration-runner.ts +5 -0
  33. package/src/mysql/mysql-db.ts +23 -17
  34. package/src/mysql/schema-introspector.ts +10 -7
  35. package/src/orm-request.ts +19 -12
  36. package/src/postgres/connection.ts +3 -1
  37. package/src/postgres/migration-generator.ts +7 -5
  38. package/src/postgres/migration-runner.ts +5 -0
  39. package/src/postgres/postgres-db.ts +14 -13
  40. package/src/postgres/schema-introspector.ts +10 -7
  41. package/src/postgres/type-map.ts +4 -0
  42. package/src/relationships.ts +3 -2
  43. package/src/setup-rest-server.ts +7 -10
  44. package/src/store.ts +2 -1
  45. package/src/timescale/query-builder.ts +39 -2
  46. package/src/utils.ts +2 -1
  47. package/src/view-resolver.ts +2 -1
@@ -89,7 +89,9 @@ export default class MysqlDB {
89
89
 
90
90
  this.deps = { ...defaultDeps, ...deps } as MysqlDBDeps;
91
91
  this.pool = null;
92
- this.mysqlConfig = this.deps.config.orm.mysql!;
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
- const migrationsPath = this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir!);
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!(`${pending.length} pending migration(s) found.`);
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!(`Applied migration: ${filename}`);
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!('Skipping pending migrations. Schema may be outdated.');
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!(`Applied migration: ${result.filename}`);
152
+ this.deps.log.db?.(`Applied migration: ${result.filename}`);
150
153
  await this.loadMemoryRecords();
151
154
  }
152
155
  } else {
153
- this.deps.log.warn!('Skipping initial migration. Tables may not exist.');
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
- const snapshot = await this.deps.loadLatestSnapshot(this.deps.path.resolve(this.deps.config.rootPath, this.mysqlConfig.migrationsDir!)) as Record<string, SnapshotEntry>;
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!('Schema drift detected: models have changed since the last migration.');
167
- this.deps.log.warn!('Run `stonyx db:generate-migration` to create a new migration.');
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!(`Skipping memory load for '${modelName}' (memory: false)`);
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!(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
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!(`Skipping memory load for view '${viewName}' (memory: false)`);
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!(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
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 { sql, values } = this.deps.buildSelect(schema.table, conditions);
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, schema!);
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
- const fkColumn = `${key}_id`;
203
- foreignKeys[fkColumn] = {
204
- references: sanitizeTableName(getPluralName(relInfo.modelName!)),
205
- column: 'id',
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) {
@@ -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!({ baseUrl }));
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
- const type = relatedRecord.__model!.__name;
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!({ fields: modelFields, baseUrl }));
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!({ fields: modelFields, baseUrl }), request.query?.include, record, {
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!({ fields: modelFields }) };
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
- if ((Orm.instance as Orm).sqlDb && WRITE_OPERATIONS.has(operation)) {
457
- await (Orm.instance as Orm).sqlDb!.persist(operation, this.model, context, response);
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!({ baseUrl }));
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!({ baseUrl }) : null;
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[]).map(r => ({ type: r.__model!.__name, id: r.id }));
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
- data = relatedData ? { type: (relatedData as OrmRecord).__model!.__name, id: (relatedData as OrmRecord).id } : null;
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 {
@@ -1,4 +1,5 @@
1
1
  import type { Pool as PgPool } from 'pg';
2
+ import { validateIdentifier } from './query-builder.js';
2
3
 
3
4
  interface PgConfig {
4
5
  host: string;
@@ -32,7 +33,8 @@ export async function getPool(pgConfig: PgConfig, extensions: string[] = ['vecto
32
33
 
33
34
  // Enable requested PostgreSQL extensions
34
35
  for (const ext of extensions) {
35
- await pool.query(`CREATE EXTENSION IF NOT EXISTS ${ext}`);
36
+ validateIdentifier(ext, 'extension name');
37
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS "${ext}"`);
36
38
  }
37
39
 
38
40
  return pool;
@@ -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!('No schema changes detected.');
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].table!;
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].table!;
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!('No schema changes detected.');
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!(`Migration generated: ${filename}`);
221
+ log.db?.(`Migration generated: ${filename}`);
220
222
 
221
223
  return { filename, content, snapshot: combinedSnapshot };
222
224
  }
@@ -2,8 +2,10 @@ import { readFile, fileExists } from '@stonyx/utils/file';
2
2
  import path from 'path';
3
3
  import fs from 'fs/promises';
4
4
  import type { Pool, PoolClient } from 'pg';
5
+ import { validateIdentifier } from './query-builder.js';
5
6
 
6
7
  export async function ensureMigrationsTable(pool: Pool, tableName: string = '__migrations'): Promise<void> {
8
+ validateIdentifier(tableName, 'migration table name');
7
9
  await pool.query(`
8
10
  CREATE TABLE IF NOT EXISTS "${tableName}" (
9
11
  id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
@@ -14,6 +16,7 @@ export async function ensureMigrationsTable(pool: Pool, tableName: string = '__m
14
16
  }
15
17
 
16
18
  export async function getAppliedMigrations(pool: Pool, tableName: string = '__migrations'): Promise<string[]> {
19
+ validateIdentifier(tableName, 'migration table name');
17
20
  const result = await pool.query(
18
21
  `SELECT filename FROM "${tableName}" ORDER BY id ASC`
19
22
  );
@@ -51,6 +54,7 @@ export function parseMigrationFile(content: string): { up: string; down: string
51
54
  }
52
55
 
53
56
  export async function applyMigration(pool: Pool, filename: string, upSql: string, tableName: string = '__migrations'): Promise<void> {
57
+ validateIdentifier(tableName, 'migration table name');
54
58
  const client: PoolClient = await pool.connect();
55
59
 
56
60
  try {
@@ -77,6 +81,7 @@ export async function applyMigration(pool: Pool, filename: string, upSql: string
77
81
  }
78
82
 
79
83
  export async function rollbackMigration(pool: Pool, filename: string, downSql: string, tableName: string = '__migrations'): Promise<void> {
84
+ validateIdentifier(tableName, 'migration table name');
80
85
  const client: PoolClient = await pool.connect();
81
86
 
82
87
  try {
@@ -122,7 +122,7 @@ export default class PostgresDB {
122
122
  const pending = files.filter(f => !applied.includes(f));
123
123
 
124
124
  if (pending.length > 0) {
125
- this.deps.log.db!(`${pending.length} pending migration(s) found.`);
125
+ this.deps.log.db?.(`${pending.length} pending migration(s) found.`);
126
126
 
127
127
  const shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
128
128
 
@@ -132,13 +132,13 @@ export default class PostgresDB {
132
132
  const { up } = this.deps.parseMigrationFile(content);
133
133
 
134
134
  await this.deps.applyMigration(this.requirePool(), filename, up, this.pgConfig.migrationsTable as string | undefined);
135
- this.deps.log.db!(`Applied migration: ${filename}`);
135
+ this.deps.log.db?.(`Applied migration: ${filename}`);
136
136
  }
137
137
 
138
138
  // Reload records after applying migrations
139
139
  await this.loadMemoryRecords();
140
140
  } else {
141
- this.deps.log.warn!('Skipping pending migrations. Schema may be outdated.');
141
+ this.deps.log.warn?.('Skipping pending migrations. Schema may be outdated.');
142
142
  }
143
143
  } else if (files.length === 0) {
144
144
  const schemas = this.deps.introspectModels();
@@ -156,11 +156,11 @@ export default class PostgresDB {
156
156
  if (result) {
157
157
  const { up } = this.deps.parseMigrationFile(result.content);
158
158
  await this.deps.applyMigration(this.requirePool(), result.filename, up, this.pgConfig.migrationsTable as string | undefined);
159
- this.deps.log.db!(`Applied migration: ${result.filename}`);
159
+ this.deps.log.db?.(`Applied migration: ${result.filename}`);
160
160
  await this.loadMemoryRecords();
161
161
  }
162
162
  } else {
163
- this.deps.log.warn!('Skipping initial migration. Tables may not exist.');
163
+ this.deps.log.warn?.('Skipping initial migration. Tables may not exist.');
164
164
  }
165
165
  }
166
166
  }
@@ -173,8 +173,8 @@ export default class PostgresDB {
173
173
  const drift = this.deps.detectSchemaDrift(schemas, snapshot as Parameters<typeof detectSchemaDrift>[1]);
174
174
 
175
175
  if (drift.hasChanges) {
176
- this.deps.log.warn!('Schema drift detected: models have changed since the last migration.');
177
- this.deps.log.warn!('Run `stonyx db:generate-migration` to create a new migration.');
176
+ this.deps.log.warn?.('Schema drift detected: models have changed since the last migration.');
177
+ this.deps.log.warn?.('Run `stonyx db:generate-migration` to create a new migration.');
178
178
  }
179
179
  }
180
180
  }
@@ -200,7 +200,7 @@ export default class PostgresDB {
200
200
  for (const modelName of order) {
201
201
  const { modelClass } = Orm.instance.getRecordClasses(modelName) as { modelClass: { memory?: boolean } };
202
202
  if (modelClass?.memory === false) {
203
- this.deps.log.db!(`Skipping memory load for '${modelName}' (memory: false)`);
203
+ this.deps.log.db?.(`Skipping memory load for '${modelName}' (memory: false)`);
204
204
  continue;
205
205
  }
206
206
 
@@ -217,7 +217,7 @@ export default class PostgresDB {
217
217
  } catch (error) {
218
218
  // 42P01 = undefined_table (PG equivalent of ER_NO_SUCH_TABLE)
219
219
  if (isDbError(error) && error.code === '42P01') {
220
- this.deps.log.db!(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
220
+ this.deps.log.db?.(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
221
221
  continue;
222
222
  }
223
223
 
@@ -231,7 +231,7 @@ export default class PostgresDB {
231
231
  for (const [viewName, viewSchema] of Object.entries(viewSchemas)) {
232
232
  const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName) as { modelClass: { memory?: boolean } };
233
233
  if (viewClass?.memory !== true) {
234
- this.deps.log.db!(`Skipping memory load for view '${viewName}' (memory: false)`);
234
+ this.deps.log.db?.(`Skipping memory load for view '${viewName}' (memory: false)`);
235
235
  continue;
236
236
  }
237
237
 
@@ -256,7 +256,7 @@ export default class PostgresDB {
256
256
  }
257
257
  } catch (error) {
258
258
  if (isDbError(error) && error.code === '42P01') {
259
- this.deps.log.db!(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
259
+ this.deps.log.db?.(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
260
260
  continue;
261
261
  }
262
262
  throw error;
@@ -345,13 +345,14 @@ export default class PostgresDB {
345
345
 
346
346
  if (!schema) return [];
347
347
 
348
- const { sql, values } = this.deps.buildSelect(schema.table, conditions);
348
+ const resolvedSchema = schema;
349
+ const { sql, values } = this.deps.buildSelect(resolvedSchema.table, conditions);
349
350
 
350
351
  try {
351
352
  const result = await this.requirePool().query(sql, values);
352
353
 
353
354
  const records = result.rows.map(row => {
354
- const rawData = this._rowToRawData(row, schema!);
355
+ const rawData = this._rowToRawData(row, resolvedSchema);
355
356
  return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
356
357
  });
357
358
 
@@ -93,9 +93,10 @@ export function introspectModels(): Record<string, ModelSchema> {
93
93
 
94
94
  // Build foreign keys from belongsTo relationships
95
95
  for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
96
+ if (!targetModelName) continue;
96
97
  const fkColumn = `${relName}_id`;
97
98
  foreignKeys[fkColumn] = {
98
- references: sanitizeTableName(getPluralName(targetModelName!)),
99
+ references: sanitizeTableName(getPluralName(targetModelName)),
99
100
  column: 'id',
100
101
  };
101
102
  }
@@ -189,7 +190,7 @@ export function getTopologicalOrder(schemas: Record<string, ModelSchema>): strin
189
190
 
190
191
  // Visit dependencies (belongsTo targets) first
191
192
  for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
192
- visit(targetModelName!);
193
+ if (targetModelName) visit(targetModelName);
193
194
  }
194
195
 
195
196
  order.push(name);
@@ -233,11 +234,13 @@ export function introspectViews(): Record<string, ViewSchema> {
233
234
 
234
235
  if (relInfo?.type === 'belongsTo') {
235
236
  relationships.belongsTo[key] = relInfo.modelName;
236
- const fkColumn = `${key}_id`;
237
- foreignKeys[fkColumn] = {
238
- references: sanitizeTableName(getPluralName(relInfo.modelName!)),
239
- column: 'id',
240
- };
237
+ if (relInfo.modelName) {
238
+ const fkColumn = `${key}_id`;
239
+ foreignKeys[fkColumn] = {
240
+ references: sanitizeTableName(getPluralName(relInfo.modelName)),
241
+ column: 'id',
242
+ };
243
+ }
241
244
  } else if (relInfo?.type === 'hasMany') {
242
245
  relationships.hasMany[key] = relInfo.modelName;
243
246
  } else if (property instanceof ModelProperty) {
@@ -42,6 +42,10 @@ export function getPgType(attrType: string, transformFn?: TransformFn): string {
42
42
  * Returns a vector column type for the given dimensions.
43
43
  */
44
44
  export function getVectorType(dimensions: number): string {
45
+ if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 16000) {
46
+ throw new Error(`Invalid vector dimensions: ${dimensions}. Must be an integer between 1 and 16000.`);
47
+ }
48
+
45
49
  return `vector(${dimensions})`;
46
50
  }
47
51
 
@@ -13,11 +13,12 @@ export function getRelationships(type: string, sourceModel: string, targetModel:
13
13
  // create relationship map for this type of it doesn't already exist
14
14
  if (!allRelationships.has(sourceModel)) allRelationships.set(sourceModel, new Map());
15
15
 
16
- const modelRelationship = allRelationships.get(sourceModel)!;
16
+ const modelRelationship = allRelationships.get(sourceModel) as Map<string, Map<unknown, unknown>> | undefined;
17
+ if (!modelRelationship) return undefined;
17
18
 
18
19
  if (!modelRelationship.has(targetModel)) modelRelationship.set(targetModel, new Map());
19
20
 
20
- const relationship = modelRelationship.get(targetModel)!;
21
+ const relationship = modelRelationship.get(targetModel) as Map<unknown, unknown> | undefined;
21
22
 
22
23
  // TODO: Determine whether already having id should be handled differently
23
24
  //if (relationship.has(relationshipId)) return;
@@ -14,7 +14,7 @@ interface AccessInstance {
14
14
  }
15
15
 
16
16
  export default async function(route: string, accessPath: string, metaRoute: boolean): Promise<void> {
17
- let accessFiles: Record<string, (request: unknown) => unknown> | null = {};
17
+ const accessFiles: Record<string, (request: unknown) => unknown> = {};
18
18
 
19
19
  try {
20
20
  await forEachFileImport(accessPath, (accessClass: unknown) => {
@@ -31,14 +31,14 @@ export default async function(route: string, accessPath: string, metaRoute: bool
31
31
  for (const model of models === '*' ? availableModels : models) {
32
32
  if (model === dbKey) continue;
33
33
  if (!store.data.has(model)) throw new Error(`Unable to define access for Invalid Model "${model}". Model does not exist`);
34
- if (accessFiles![model]) throw new Error(`Access for model "${model}" has already been defined by another access class.`);
34
+ if (accessFiles[model]) throw new Error(`Access for model "${model}" has already been defined by another access class.`);
35
35
 
36
- accessFiles![model] = accessInstance.access;
36
+ accessFiles[model] = accessInstance.access;
37
37
  }
38
38
  });
39
39
  } catch (error) {
40
- log.error!(error instanceof Error ? error.message : String(error));
41
- log.warn!('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
40
+ log.error?.(error instanceof Error ? error.message : String(error));
41
+ log.warn?.('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
42
42
  }
43
43
 
44
44
  await waitForModule('rest-server');
@@ -47,7 +47,7 @@ export default async function(route: string, accessPath: string, metaRoute: bool
47
47
  const name = route === '/' ? 'index' : (route[0] === '/' ? route.slice(1) : route);
48
48
 
49
49
  // Configure endpoints for models and views with access configuration
50
- for (const [model, access] of Object.entries(accessFiles!)) {
50
+ for (const [model, access] of Object.entries(accessFiles)) {
51
51
  const pluralizedModel = getPluralName(model);
52
52
  const modelName = name === 'index' ? pluralizedModel : `${name}/${pluralizedModel}`;
53
53
  RestServer.instance.mountRoute(OrmRequest, { name: modelName, options: { model, access } });
@@ -55,11 +55,8 @@ export default async function(route: string, accessPath: string, metaRoute: bool
55
55
 
56
56
  // Mount the meta route when metaRoute config is enabled
57
57
  if (metaRoute) {
58
- log.warn!('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
58
+ log.warn?.('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
59
59
 
60
60
  RestServer.instance.mountRoute(MetaRequest, { name });
61
61
  }
62
-
63
- // Cleanup references
64
- accessFiles = null;
65
62
  }
package/src/store.ts CHANGED
@@ -332,7 +332,8 @@ export default class Store {
332
332
  }];
333
333
 
334
334
  while (queue.length > 0) {
335
- const item = queue.shift()!;
335
+ const item = queue.shift();
336
+ if (!item) break;
336
337
  const key = `${item.modelName}:${item.recordId}`;
337
338
 
338
339
  if (visited.has(key)) continue;