@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 +9 -3
- package/dist/manage-record.js +5 -3
- package/dist/orm-request.js +23 -12
- package/dist/store.js +30 -17
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +3 -0
- package/package.json +1 -1
- package/src/db.ts +12 -5
- package/src/manage-record.ts +7 -5
- package/src/orm-request.ts +22 -14
- package/src/store.ts +28 -18
- package/src/utils.ts +5 -0
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
|
}
|
package/dist/manage-record.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/orm-request.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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)
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
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 =
|
|
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
|
-
|
|
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 })
|
|
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 })
|
|
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 })
|
|
230
|
+
return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
|
|
224
231
|
}
|
|
225
232
|
}
|
package/src/manage-record.ts
CHANGED
|
@@ -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
|
|
51
|
-
return existingRecord
|
|
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
|
|
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())
|
|
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
|
}
|
package/src/orm-request.ts
CHANGED
|
@@ -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 } =
|
|
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
|
|
161
|
-
: [relatedRecords
|
|
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)
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
if (!
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
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
|
-
|
|
318
|
+
value.__model.__name
|
|
309
319
|
)) {
|
|
310
|
-
children.push({ childRecord: value
|
|
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('-')) {
|