@stonyx/orm 0.2.1-beta.88 → 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.
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;
@@ -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
  }
@@ -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();
@@ -108,7 +110,7 @@ function assignRecordId(modelName, rawData) {
108
110
  const storeMap = store.get(modelName);
109
111
  if (!storeMap)
110
112
  throw new Error(`Cannot assign record ID: model "${modelName}" not found in store`);
111
- const modelStore = Array.from(storeMap.values());
113
+ const modelStore = Array.from(storeMap.values()).filter(isOrmRecord);
112
114
  const lastRecord = modelStore.at(-1);
113
115
  rawData.id = lastRecord ? lastRecord.id + 1 : 1;
114
116
  }
@@ -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',
@@ -91,8 +92,8 @@ 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;
@@ -198,7 +199,7 @@ export default class OrmRequest extends Request {
198
199
  const modelRelationships = getModelRelationships(model);
199
200
  // Define raw handlers first
200
201
  const getCollectionHandler = async (request, { filter: accessFilter }) => {
201
- const allRecords = await store.findAll(model);
202
+ const allRecords = (await store.findAll(model)).filter(isOrmRecord);
202
203
  const queryFilters = parseFilters(request.query);
203
204
  const queryFilterPredicate = createFilterPredicate(queryFilters);
204
205
  const fieldsMap = parseFields(request.query);
@@ -247,11 +248,17 @@ export default class OrmRequest extends Request {
247
248
  }
248
249
  }
249
250
  const recordAttributes = id !== undefined ? { id, ...sanitizedAttributes } : sanitizedAttributes;
250
- const record = createRecord(model, recordAttributes, { serialize: false });
251
+ const created = createRecord(model, recordAttributes, { serialize: false });
252
+ const record = isOrmRecord(created) ? created : null;
253
+ if (!record)
254
+ return 500;
251
255
  return { data: record.toJSON?.({ fields: modelFields }) };
252
256
  };
253
257
  const updateHandler = async ({ body, params }) => {
254
- 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;
255
262
  const { attributes, relationships: rels } = (body?.data || {});
256
263
  if (!attributes && !rels)
257
264
  return 400; // Bad request
@@ -393,11 +400,12 @@ export default class OrmRequest extends Request {
393
400
  let data;
394
401
  if (info.isArray) {
395
402
  // hasMany - return array
396
- data = (relatedData || []).map(r => r.toJSON?.({ baseUrl }));
403
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
404
+ data = related.map(r => r.toJSON?.({ baseUrl }));
397
405
  }
398
406
  else {
399
407
  // belongsTo - return single or null
400
- data = relatedData ? relatedData.toJSON?.({ baseUrl }) : null;
408
+ data = isOrmRecord(relatedData) ? relatedData.toJSON?.({ baseUrl }) : null;
401
409
  }
402
410
  return {
403
411
  links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}/${dasherizedName}` },
@@ -414,16 +422,19 @@ export default class OrmRequest extends Request {
414
422
  let data;
415
423
  if (info.isArray) {
416
424
  // hasMany - return array of linkage objects
417
- data = (relatedData || [])
425
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
426
+ data = related
418
427
  .filter((r) => Boolean(r.__model))
419
428
  .map(r => ({ type: r.__model.__name, id: r.id }));
420
429
  }
421
430
  else {
422
431
  // belongsTo - return single linkage or null
423
- const model = relatedData ? relatedData.__model : undefined;
424
- data = model
425
- ? { type: model.__name, id: relatedData.id }
426
- : null;
432
+ if (isOrmRecord(relatedData) && relatedData.__model) {
433
+ data = { type: relatedData.__model.__name, id: relatedData.id };
434
+ }
435
+ else {
436
+ data = null;
437
+ }
427
438
  }
428
439
  return {
429
440
  links: {
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
  }
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,6 +2,9 @@ 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('-')) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.88",
7
+ "version": "0.2.1-beta.89",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
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
 
@@ -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();
@@ -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
  }
@@ -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();
@@ -144,7 +146,7 @@ function assignRecordId(modelName: string, rawData: { [key: string]: unknown }):
144
146
 
145
147
  const storeMap = store.get(modelName);
146
148
  if (!storeMap) throw new Error(`Cannot assign record ID: model "${modelName}" not found in store`);
147
- const modelStore = Array.from(storeMap.values()) as OrmRecord[];
149
+ const modelStore = Array.from(storeMap.values()).filter(isOrmRecord);
148
150
  const lastRecord = modelStore.at(-1);
149
151
  rawData.id = lastRecord ? (lastRecord.id as number) + 1 : 1;
150
152
  }
@@ -5,6 +5,7 @@ import { getPluralName } from './plural-registry.js';
5
5
  import { getBeforeHooks, getAfterHooks } from './hooks.js';
6
6
  import config from 'stonyx/config';
7
7
  import type { OrmRecord } from './types/orm-types.js';
8
+ import { isOrmRecord } from './utils.js';
8
9
 
9
10
  interface OrmRequest$ extends Request {
10
11
  protocol?: string;
@@ -74,7 +75,7 @@ function getRelationshipInfo(property: unknown): RelationshipInfo | null {
74
75
 
75
76
  // Helper to introspect model relationships
76
77
  function getModelRelationships(modelName: string): { [key: string]: RelationshipInfo } {
77
- const { modelClass } = (Orm.instance as Orm).getRecordClasses(modelName);
78
+ const { modelClass } = Orm.instance.getRecordClasses(modelName);
78
79
  if (!modelClass) return {};
79
80
 
80
81
  const model = new (modelClass as new (name: string) => { [key: string]: unknown })(modelName);
@@ -157,8 +158,8 @@ function traverseIncludePath(
157
158
 
158
159
  // Handle both belongsTo (single) and hasMany (array)
159
160
  const recordsToProcess: OrmRecord[] = Array.isArray(relatedRecords)
160
- ? relatedRecords as OrmRecord[]
161
- : [relatedRecords as OrmRecord];
161
+ ? relatedRecords.filter(isOrmRecord)
162
+ : isOrmRecord(relatedRecords) ? [relatedRecords] : [];
162
163
 
163
164
  for (const relatedRecord of recordsToProcess) {
164
165
  if (!relatedRecord) continue;
@@ -281,7 +282,7 @@ export default class OrmRequest extends Request {
281
282
 
282
283
  // Define raw handlers first
283
284
  const getCollectionHandler: HandlerFn = async (request, { filter: accessFilter }) => {
284
- const allRecords = await store.findAll(model) as OrmRecord[];
285
+ const allRecords = (await store.findAll(model)).filter(isOrmRecord);
285
286
 
286
287
  const queryFilters = parseFilters(request.query);
287
288
  const queryFilterPredicate = createFilterPredicate(queryFilters);
@@ -344,13 +345,17 @@ export default class OrmRequest extends Request {
344
345
  }
345
346
 
346
347
  const recordAttributes = id !== undefined ? { id, ...sanitizedAttributes } : sanitizedAttributes;
347
- const record = createRecord(model, recordAttributes as { [key: string]: unknown }, { serialize: false }) as unknown as OrmRecord;
348
+ const created = createRecord(model, recordAttributes as { [key: string]: unknown }, { serialize: false });
349
+ const record = isOrmRecord(created) ? created : null;
350
+ if (!record) return 500;
348
351
 
349
352
  return { data: record.toJSON?.({ fields: modelFields }) };
350
353
  };
351
354
 
352
355
  const updateHandler: HandlerFn = async ({ body, params }) => {
353
- const record = await store.find(model, getId(params)) as OrmRecord;
356
+ const found = await store.find(model, getId(params));
357
+ if (!found || !isOrmRecord(found)) return 404;
358
+ const record = found;
354
359
  const { attributes, relationships: rels } = (body?.data || {}) as {
355
360
  attributes?: { [key: string]: unknown };
356
361
  relationships?: { [key: string]: { data?: { id?: string | number } } };
@@ -454,7 +459,7 @@ export default class OrmRequest extends Request {
454
459
  const response = await handler(request, state);
455
460
 
456
461
  // Persist to SQL database for write operations
457
- const sqlDb = (Orm.instance as Orm).sqlDb;
462
+ const sqlDb = Orm.instance.sqlDb;
458
463
  if (sqlDb && WRITE_OPERATIONS.has(operation)) {
459
464
  await sqlDb.persist(operation, this.model, context, response);
460
465
  }
@@ -514,10 +519,11 @@ export default class OrmRequest extends Request {
514
519
  let data: unknown;
515
520
  if (info.isArray) {
516
521
  // hasMany - return array
517
- data = ((relatedData || []) as OrmRecord[]).map(r => r.toJSON?.({ baseUrl }));
522
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
523
+ data = related.map(r => r.toJSON?.({ baseUrl }));
518
524
  } else {
519
525
  // belongsTo - return single or null
520
- data = relatedData ? (relatedData as OrmRecord).toJSON?.({ baseUrl }) : null;
526
+ data = isOrmRecord(relatedData) ? relatedData.toJSON?.({ baseUrl }) : null;
521
527
  }
522
528
 
523
529
  return {
@@ -537,15 +543,17 @@ export default class OrmRequest extends Request {
537
543
  let data: unknown;
538
544
  if (info.isArray) {
539
545
  // hasMany - return array of linkage objects
540
- data = ((relatedData || []) as OrmRecord[])
546
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
547
+ data = related
541
548
  .filter((r): r is OrmRecord & { __model: { __name: string } } => Boolean(r.__model))
542
549
  .map(r => ({ type: r.__model.__name, id: r.id }));
543
550
  } else {
544
551
  // belongsTo - return single linkage or null
545
- const model = relatedData ? (relatedData as OrmRecord).__model : undefined;
546
- data = model
547
- ? { type: model.__name, id: (relatedData as OrmRecord).id }
548
- : null;
552
+ if (isOrmRecord(relatedData) && relatedData.__model) {
553
+ data = { type: relatedData.__model.__name, id: relatedData.id };
554
+ } else {
555
+ data = null;
556
+ }
549
557
  }
550
558
 
551
559
  return {
package/src/store.ts CHANGED
@@ -30,6 +30,10 @@ interface StoreRecord {
30
30
  [key: string]: unknown;
31
31
  }
32
32
 
33
+ function isStoreRecord(value: unknown): value is StoreRecord {
34
+ return typeof value === 'object' && value !== null && '__data' in value;
35
+ }
36
+
33
37
  export default class Store {
34
38
  static instance: Store | undefined;
35
39
 
@@ -103,7 +107,7 @@ export default class Store {
103
107
  if (!conditions || Object.keys(conditions).length === 0) return records;
104
108
 
105
109
  return records.filter((record: unknown) =>
106
- Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
110
+ Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value)
107
111
  );
108
112
  }
109
113
 
@@ -127,7 +131,7 @@ export default class Store {
127
131
  if (!conditions || Object.keys(conditions).length === 0) return records;
128
132
 
129
133
  return records.filter((record: unknown) =>
130
- Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
134
+ Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value)
131
135
  );
132
136
  }
133
137
 
@@ -149,7 +153,7 @@ export default class Store {
149
153
  if (Object.keys(conditions).length === 0) return records;
150
154
 
151
155
  return records.filter((record: unknown) =>
152
- Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
156
+ Object.entries(conditions).every(([key, value]) => isStoreRecord(record) && record.__data[key] === value)
153
157
  );
154
158
  }
155
159
 
@@ -185,12 +189,13 @@ export default class Store {
185
189
  return;
186
190
  }
187
191
 
188
- const record = modelStore.get(id as string | number) as StoreRecord | undefined;
189
-
190
- if (!record) {
192
+ if (typeof id !== 'string' && typeof id !== 'number') return;
193
+ const raw = modelStore.get(id);
194
+ if (!raw || !isStoreRecord(raw)) {
191
195
  console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store`);
192
196
  return;
193
197
  }
198
+ const record = raw;
194
199
 
195
200
  const { toUnload, visited } = options.includeChildren
196
201
  ? this._buildUnloadQueue(record, options)
@@ -224,7 +229,10 @@ export default class Store {
224
229
  }
225
230
  }
226
231
 
227
- for (const relationshipType of TYPES) (relationships.get(relationshipType) as Map<string, unknown>).delete(model);
232
+ for (const relationshipType of TYPES) {
233
+ const reg = relationships.get(relationshipType);
234
+ if (reg instanceof Map) reg.delete(model);
235
+ }
228
236
  }
229
237
 
230
238
  private _removeFromHasManyArrays(modelName: string, recordId: unknown, visited: Set<string>): void {
@@ -240,7 +248,7 @@ export default class Store {
240
248
  // Don't modify arrays of records being deleted
241
249
  if (visited.has(sourceKey)) continue;
242
250
 
243
- const index = hasManyArray.findIndex(r => r && (r as StoreRecord).id === recordId);
251
+ const index = hasManyArray.findIndex(r => r && isStoreRecord(r) && r.id === recordId);
244
252
  if (index !== -1) hasManyArray.splice(index, 1);
245
253
  }
246
254
  }
@@ -254,17 +262,19 @@ export default class Store {
254
262
  if (!targetModelMap) continue;
255
263
 
256
264
  for (const [sourceRecordId, belongsToRecord] of targetModelMap) {
257
- if (belongsToRecord && (belongsToRecord as StoreRecord).id === recordId) {
265
+ if (belongsToRecord && isStoreRecord(belongsToRecord) && belongsToRecord.id === recordId) {
258
266
  const sourceKey = `${sourceModel}:${sourceRecordId}`;
259
267
 
260
268
  if (visited.has(sourceKey)) continue;
261
269
  targetModelMap.set(sourceRecordId, null);
262
270
 
263
- const sourceRecord = this.get(sourceModel, sourceRecordId as string | number) as StoreRecord | undefined;
264
- if (sourceRecord && sourceRecord.__relationships) {
265
- for (const [key, value] of Object.entries(sourceRecord.__relationships)) {
266
- if (value && (value as StoreRecord).id === recordId) {
267
- sourceRecord.__relationships[key] = null;
271
+ if (typeof sourceRecordId !== 'string' && typeof sourceRecordId !== 'number') continue;
272
+ const sourceRaw = this.get(sourceModel, sourceRecordId);
273
+ if (!sourceRaw || !isStoreRecord(sourceRaw)) continue;
274
+ if (sourceRaw.__relationships) {
275
+ for (const [key, value] of Object.entries(sourceRaw.__relationships)) {
276
+ if (value && isStoreRecord(value) && value.id === recordId) {
277
+ sourceRaw.__relationships[key] = null;
268
278
  }
269
279
  }
270
280
  }
@@ -301,13 +311,13 @@ export default class Store {
301
311
  // hasMany children - always include
302
312
  if (Array.isArray(value)) {
303
313
  for (const childRecord of value) {
304
- if (childRecord) children.push({ childRecord: childRecord as StoreRecord, relationshipKey: key, type: 'hasMany' });
314
+ if (childRecord && isStoreRecord(childRecord)) children.push({ childRecord, relationshipKey: key, type: 'hasMany' });
305
315
  }
306
- } else if (value && !this._isBidirectionalRelationship(
316
+ } else if (value && isStoreRecord(value) && value.__model && !this._isBidirectionalRelationship(
307
317
  record.__model.__name,
308
- (value as StoreRecord).__model.__name
318
+ value.__model.__name
309
319
  )) {
310
- children.push({ childRecord: value as StoreRecord, relationshipKey: key, type: 'belongsTo' });
320
+ children.push({ childRecord: value, relationshipKey: key, type: 'belongsTo' });
311
321
  }
312
322
  }
313
323
 
package/src/utils.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { pluralize as basePluralize } from '@stonyx/utils/string';
2
+ import type { OrmRecord } from './types/orm-types.js';
2
3
 
3
4
  export function isDbError(error: unknown): error is { code: string; message: string } {
4
5
  return typeof error === 'object' && error !== null && 'code' in error && typeof (error as Record<string, unknown>).code === 'string' && 'message' in error && typeof (error as Record<string, unknown>).message === 'string';
5
6
  }
6
7
 
8
+ export function isOrmRecord(value: unknown): value is OrmRecord {
9
+ return typeof value === 'object' && value !== null && '__data' in value && '__relationships' in value;
10
+ }
11
+
7
12
  // Wrapper to handle dasherized model names (e.g., "access-link" → "access-links")
8
13
  export function pluralize(word: string): string {
9
14
  if (word.includes('-')) {