@stonyx/orm 0.3.2-alpha.124 → 0.3.2-alpha.126
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/README.md +2 -0
- package/config/environment.js +4 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/main.d.ts +1 -0
- package/dist/manage-record.d.ts +4 -0
- package/dist/manage-record.js +72 -42
- package/dist/mysql/connection.d.ts +2 -0
- package/dist/mysql/mysql-db.d.ts +32 -10
- package/dist/mysql/mysql-db.js +218 -58
- package/dist/mysql/query-builder.d.ts +1 -0
- package/dist/mysql/query-builder.js +17 -0
- package/dist/postgres/postgres-db.d.ts +1 -0
- package/dist/postgres/postgres-db.js +1 -0
- package/package.json +6 -6
- package/src/index.ts +2 -2
- package/src/main.ts +1 -0
- package/src/manage-record.ts +89 -42
- package/src/mysql/connection.ts +2 -0
- package/src/mysql/mysql-db.ts +242 -56
- package/src/mysql/query-builder.ts +21 -0
- package/src/postgres/postgres-db.ts +1 -0
|
@@ -4,6 +4,7 @@ interface QueryResult {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function validateIdentifier(name: string, context?: string): string;
|
|
6
6
|
export declare function buildInsert(table: string, data: Record<string, unknown>): QueryResult;
|
|
7
|
+
export declare function buildInsertMany(table: string, rows: Record<string, unknown>[]): QueryResult;
|
|
7
8
|
export declare function buildUpdate(table: string, id: unknown, data: Record<string, unknown>): QueryResult;
|
|
8
9
|
export declare function buildDelete(table: string, id: unknown): QueryResult;
|
|
9
10
|
export declare function buildSelect(table: string, conditions?: Record<string, unknown>): QueryResult;
|
|
@@ -14,6 +14,23 @@ export function buildInsert(table, data) {
|
|
|
14
14
|
const sql = `INSERT INTO \`${table}\` (${keys.map(k => `\`${k}\``).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
15
15
|
return { sql, values };
|
|
16
16
|
}
|
|
17
|
+
export function buildInsertMany(table, rows) {
|
|
18
|
+
validateIdentifier(table, 'table name');
|
|
19
|
+
const columns = [...new Set(rows.flatMap(row => Object.keys(row)))];
|
|
20
|
+
columns.forEach(column => validateIdentifier(column, 'column name'));
|
|
21
|
+
const values = [];
|
|
22
|
+
const tuples = rows.map(row => {
|
|
23
|
+
const cells = columns.map(column => {
|
|
24
|
+
if (!Object.hasOwn(row, column))
|
|
25
|
+
return 'DEFAULT';
|
|
26
|
+
values.push(row[column]);
|
|
27
|
+
return '?';
|
|
28
|
+
});
|
|
29
|
+
return `(${cells.join(', ')})`;
|
|
30
|
+
});
|
|
31
|
+
const sql = `INSERT INTO \`${table}\` (${columns.map(column => `\`${column}\``).join(', ')}) VALUES ${tuples.join(', ')}`;
|
|
32
|
+
return { sql, values };
|
|
33
|
+
}
|
|
17
34
|
export function buildUpdate(table, id, data) {
|
|
18
35
|
validateIdentifier(table, 'table name');
|
|
19
36
|
const keys = Object.keys(data);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.2-alpha.
|
|
7
|
+
"version": "0.3.2-alpha.126",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"type": "module",
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stonyx/cron": "0.2.1-beta.
|
|
65
|
-
"@stonyx/events": "0.1.1-beta.
|
|
66
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
67
|
-
"stonyx": "0.2.3-beta.
|
|
64
|
+
"@stonyx/cron": "0.2.1-beta.175",
|
|
65
|
+
"@stonyx/events": "0.1.1-beta.74",
|
|
66
|
+
"@stonyx/utils": "0.2.3-beta.30",
|
|
67
|
+
"stonyx": "0.2.3-beta.109"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stonyx/rest-server": "0.2.1-beta.
|
|
94
|
+
"@stonyx/rest-server": "0.2.1-beta.178",
|
|
95
95
|
"@types/node": "^25.6.0",
|
|
96
96
|
"mysql2": "^3.20.0",
|
|
97
97
|
"pg": "^8.20.0",
|
package/src/index.ts
CHANGED
|
@@ -21,14 +21,14 @@ import Serializer from './serializer.js';
|
|
|
21
21
|
import attr from './attr.js';
|
|
22
22
|
import belongsTo from './belongs-to.js';
|
|
23
23
|
import hasMany from './has-many.js';
|
|
24
|
-
import { createRecord, updateRecord } from './manage-record.js';
|
|
24
|
+
import { createRecord, updateRecord, createRecordAndPersist, updateRecordAndPersist } from './manage-record.js';
|
|
25
25
|
import { count, avg, sum, min, max } from './aggregates.js';
|
|
26
26
|
|
|
27
27
|
export { default } from './main.js';
|
|
28
28
|
export { store, relationships } from './main.js';
|
|
29
29
|
export type { PersistErrorDetail } from './main.js';
|
|
30
30
|
export { Model, View, Serializer }; // base classes
|
|
31
|
-
export { attr, belongsTo, hasMany, createRecord, updateRecord };
|
|
31
|
+
export { attr, belongsTo, hasMany, createRecord, updateRecord, createRecordAndPersist, updateRecordAndPersist };
|
|
32
32
|
export { count, avg, sum, min, max }; // aggregate helpers
|
|
33
33
|
export { default as normalizeRecordId } from './normalize-record-id.js'; // canonical URL-id -> record-id resolution (#270)
|
|
34
34
|
export type { OrmRequest$ as OrmAccessRequest } from './orm-request.js'; // the request access() is handed, incl. recordId (#270)
|
package/src/main.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface SqlDb {
|
|
|
34
34
|
startup(): Promise<void>;
|
|
35
35
|
shutdown(): Promise<void>;
|
|
36
36
|
persist(operation: string, model: string, context: unknown, response: unknown): Promise<void>;
|
|
37
|
+
persistResolvesAfterWrite?: boolean;
|
|
37
38
|
findRecord(modelName: string, id: unknown): Promise<unknown>;
|
|
38
39
|
findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
39
40
|
}
|
package/src/manage-record.ts
CHANGED
|
@@ -28,7 +28,73 @@ const defaultOptions: CreateRecordOptions = {
|
|
|
28
28
|
|
|
29
29
|
let pendingIdCounter = 0;
|
|
30
30
|
|
|
31
|
+
interface RecordWrite<Result> {
|
|
32
|
+
result: Result;
|
|
33
|
+
written: Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type AwaitedOperation = 'create' | 'update';
|
|
37
|
+
|
|
38
|
+
const writeNotRequested = Promise.resolve();
|
|
39
|
+
|
|
40
|
+
function ignoreReportedFailure(): void {}
|
|
41
|
+
|
|
42
|
+
function persistRequested(options: CreateRecordOptions, userOptions: CreateRecordOptions): boolean {
|
|
43
|
+
return !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reportPersistFailure(operation: AwaitedOperation, modelName: string, recordId: unknown, failure: unknown): Error {
|
|
47
|
+
const error = failure instanceof Error ? failure : new Error(String(failure));
|
|
48
|
+
Orm.instance.emitPersistError({ operation, modelName, recordId, error });
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
export function createRecord(modelName: string, rawData: { [key: string]: unknown } = {}, userOptions: CreateRecordOptions = {}): OrmRecord {
|
|
53
|
+
const { result: record, written } = createRecordWrite(modelName, rawData, userOptions);
|
|
54
|
+
written.catch(ignoreReportedFailure);
|
|
55
|
+
return record;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function updateRecord(record: OrmRecord, rawData: unknown, userOptions: CreateRecordOptions = {}): void {
|
|
59
|
+
updateRecordWrite(record, rawData, userOptions).written.catch(ignoreReportedFailure);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function createRecordAndPersist(modelName: string, rawData: { [key: string]: unknown } = {}, userOptions: CreateRecordOptions = {}): Promise<OrmRecord> {
|
|
63
|
+
assertWriteCanBeAwaited();
|
|
64
|
+
const { result: record, written } = createRecordWrite(modelName, rawData, userOptions);
|
|
65
|
+
await written;
|
|
66
|
+
await saveJsonDatabase('create', modelName, record.id, { ...defaultOptions, ...userOptions }, userOptions);
|
|
67
|
+
return record;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function updateRecordAndPersist(record: OrmRecord, rawData: unknown, userOptions: CreateRecordOptions = {}): Promise<void> {
|
|
71
|
+
assertWriteCanBeAwaited();
|
|
72
|
+
await updateRecordWrite(record, rawData, userOptions).written;
|
|
73
|
+
await saveJsonDatabase('update', record?.__model?.__name ?? '', record.id, { ...defaultOptions, ...userOptions }, userOptions);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertWriteCanBeAwaited(): void {
|
|
77
|
+
const sqlDb = Orm.instance?.sqlDb as { persistResolvesAfterWrite?: boolean } | undefined;
|
|
78
|
+
if (sqlDb && sqlDb.persistResolvesAfterWrite !== true) {
|
|
79
|
+
throw new Error('The configured database adapter cannot confirm a write has landed, so createRecordAndPersist/updateRecordAndPersist are unavailable for it (DynamoDB: abofs/stonyx-orm#299)');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function saveJsonDatabase(operation: AwaitedOperation, modelName: string, recordId: unknown, options: CreateRecordOptions, userOptions: CreateRecordOptions): Promise<void> {
|
|
84
|
+
const orm = Orm.instance;
|
|
85
|
+
if (orm.sqlDb || !persistRequested(options, userOptions)) return;
|
|
86
|
+
|
|
87
|
+
const jsonDatabase = orm.db as { save?: () => Promise<void> } | undefined;
|
|
88
|
+
if (!jsonDatabase?.save) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await jsonDatabase.save();
|
|
92
|
+
} catch (failure) {
|
|
93
|
+
throw reportPersistFailure(operation, modelName, recordId, failure);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createRecordWrite(modelName: string, rawData: { [key: string]: unknown }, userOptions: CreateRecordOptions): RecordWrite<OrmRecord> {
|
|
32
98
|
const orm = Orm.instance;
|
|
33
99
|
const { initialized } = Orm;
|
|
34
100
|
const options = { ...defaultOptions, ...userOptions };
|
|
@@ -50,9 +116,7 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
50
116
|
const existingRecord = modelStore.get(rawData.id as number | string);
|
|
51
117
|
|
|
52
118
|
if (existingRecord instanceof OrmRecord) {
|
|
53
|
-
|
|
54
|
-
updateRecord(existingRecord, rawData, { ...options, update: true });
|
|
55
|
-
return existingRecord;
|
|
119
|
+
return { result: existingRecord, written: updateRecordWrite(existingRecord, rawData, { ...options, update: true }).written };
|
|
56
120
|
}
|
|
57
121
|
|
|
58
122
|
const recordClasses = orm.getRecordClasses(modelName);
|
|
@@ -136,37 +200,26 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
136
200
|
pendingBelongsTo.length = 0;
|
|
137
201
|
}
|
|
138
202
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
153
|
-
});
|
|
154
|
-
})
|
|
155
|
-
.finally(() => {
|
|
156
|
-
// Evict non-memory records after persist to prevent unbounded heap growth (stonyx#81)
|
|
157
|
-
if (store._memoryResolver && !store._memoryResolver(modelName)) {
|
|
158
|
-
store.evictRecord(modelName, record.id, registryId);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
203
|
+
if (!orm?.sqlDb || !persistRequested(options, userOptions)) return { result: record, written: writeNotRequested };
|
|
204
|
+
|
|
205
|
+
const registryIdBeforePersist = record.id;
|
|
206
|
+
const response = { data: { id: record.id } };
|
|
207
|
+
const written = orm.sqlDb.persist('create', modelName, { rawData }, response)
|
|
208
|
+
.catch((failure: unknown) => {
|
|
209
|
+
throw reportPersistFailure('create', modelName, record.id, failure);
|
|
210
|
+
})
|
|
211
|
+
.finally(() => {
|
|
212
|
+
if (store._memoryResolver && !store._memoryResolver(modelName)) {
|
|
213
|
+
store.evictRecord(modelName, record.id, registryIdBeforePersist);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
162
216
|
|
|
163
|
-
return record;
|
|
217
|
+
return { result: record, written };
|
|
164
218
|
}
|
|
165
219
|
|
|
166
|
-
|
|
220
|
+
function updateRecordWrite(record: OrmRecord, rawData: unknown, userOptions: CreateRecordOptions): RecordWrite<void> {
|
|
167
221
|
if (!rawData) throw new Error('rawData must be passed in to updateRecord call');
|
|
168
222
|
|
|
169
|
-
// Guard: read-only views cannot be updated
|
|
170
223
|
const modelName = record?.__model?.__name;
|
|
171
224
|
if (modelName && Orm.instance?.isView?.(modelName)) {
|
|
172
225
|
throw new Error(`Cannot update records for read-only view '${modelName}'`);
|
|
@@ -174,24 +227,18 @@ export function updateRecord(record: OrmRecord, rawData: unknown, userOptions: C
|
|
|
174
227
|
|
|
175
228
|
const options = { ...defaultOptions, ...userOptions, update: true };
|
|
176
229
|
|
|
177
|
-
// Capture old state before update for SQL diff
|
|
178
230
|
const oldState = record.__data ? JSON.parse(JSON.stringify(record.__data)) : {};
|
|
179
231
|
|
|
180
232
|
record.serialize(rawData, options);
|
|
181
233
|
|
|
182
|
-
// Auto-persist to SQL — skip for DB loads (isDbRecord) and relationship resolution (_relationshipKey)
|
|
183
234
|
const orm = Orm.instance;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
}
|
|
235
|
+
if (!orm?.sqlDb || !modelName || !persistRequested(options, userOptions)) return { result: undefined, written: writeNotRequested };
|
|
236
|
+
|
|
237
|
+
const written = orm.sqlDb.persist('update', modelName, { record, oldState }, {}).catch((failure: unknown) => {
|
|
238
|
+
throw reportPersistFailure('update', modelName, record.id, failure);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return { result: undefined, written };
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
/**
|