@stonyx/orm 0.3.2-beta.15 → 0.3.2-beta.150
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 +402 -10
- package/config/environment.js +99 -12
- package/dist/commands.js +34 -0
- package/dist/dynamodb/connection.d.ts +31 -0
- package/dist/dynamodb/connection.js +28 -0
- package/dist/dynamodb/dynamodb-db.d.ts +142 -0
- package/dist/dynamodb/dynamodb-db.js +596 -0
- package/dist/dynamodb/operation-builder.d.ts +76 -0
- package/dist/dynamodb/operation-builder.js +116 -0
- package/dist/dynamodb/type-map.d.ts +31 -0
- package/dist/dynamodb/type-map.js +48 -0
- package/dist/main.js +10 -0
- package/dist/manage-record.js +34 -3
- package/dist/mysql/connection.d.ts +1 -0
- package/dist/mysql/mysql-db.d.ts +8 -0
- package/dist/mysql/mysql-db.js +44 -10
- package/dist/orm-request.d.ts +60 -0
- package/dist/orm-request.js +644 -47
- package/dist/postgres/connection.d.ts +1 -0
- package/dist/postgres/connection.js +8 -6
- package/dist/postgres/postgres-db.d.ts +8 -0
- package/dist/postgres/postgres-db.js +44 -10
- package/dist/record.js +7 -5
- package/dist/relationships.js +1 -1
- package/dist/serializer.js +38 -2
- package/dist/store.d.ts +13 -1
- package/dist/store.js +65 -6
- package/dist/types/orm-types.d.ts +11 -0
- package/package.json +16 -7
- package/src/commands.ts +43 -0
- package/src/dynamodb/connection.ts +50 -0
- package/src/dynamodb/dynamodb-db.ts +811 -0
- package/src/dynamodb/operation-builder.ts +202 -0
- package/src/dynamodb/type-map.ts +54 -0
- package/src/main.ts +10 -0
- package/src/manage-record.ts +41 -9
- package/src/mysql/connection.ts +1 -0
- package/src/mysql/mysql-db.ts +44 -12
- package/src/orm-request.ts +654 -45
- package/src/postgres/connection.ts +10 -6
- package/src/postgres/postgres-db.ts +44 -12
- package/src/record.ts +8 -5
- package/src/relationships.ts +1 -1
- package/src/serializer.ts +39 -2
- package/src/store.ts +68 -6
- package/src/types/orm-types.ts +12 -0
- package/src/types/stonyx.d.ts +7 -1
- package/config/environment.ts +0 -91
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DynamoDB operation parameter builders.
|
|
3
|
+
*
|
|
4
|
+
* Each function returns a plain-object "params" bag that can be passed
|
|
5
|
+
* directly to the corresponding DocumentClient command
|
|
6
|
+
* (PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand, QueryCommand).
|
|
7
|
+
*
|
|
8
|
+
* All functions are pure — no SDK imports here; the caller wraps params
|
|
9
|
+
* in the appropriate Command class.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface PutItemParams {
|
|
13
|
+
TableName: string;
|
|
14
|
+
Item: Record<string, unknown>;
|
|
15
|
+
ConditionExpression?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GetItemParams {
|
|
19
|
+
TableName: string;
|
|
20
|
+
Key: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UpdateItemParams {
|
|
24
|
+
TableName: string;
|
|
25
|
+
Key: Record<string, unknown>;
|
|
26
|
+
UpdateExpression: string;
|
|
27
|
+
ExpressionAttributeNames: Record<string, string>;
|
|
28
|
+
ExpressionAttributeValues: Record<string, unknown>;
|
|
29
|
+
ReturnValues: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DeleteItemParams {
|
|
33
|
+
TableName: string;
|
|
34
|
+
Key: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScanParams {
|
|
38
|
+
TableName: string;
|
|
39
|
+
FilterExpression?: string;
|
|
40
|
+
ExpressionAttributeNames?: Record<string, string>;
|
|
41
|
+
ExpressionAttributeValues?: Record<string, unknown>;
|
|
42
|
+
ExclusiveStartKey?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface QueryParams {
|
|
46
|
+
TableName: string;
|
|
47
|
+
IndexName: string;
|
|
48
|
+
KeyConditionExpression: string;
|
|
49
|
+
ExpressionAttributeNames: Record<string, string>;
|
|
50
|
+
ExpressionAttributeValues: Record<string, unknown>;
|
|
51
|
+
ExclusiveStartKey?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* PutItem — optionally with a condition expression.
|
|
56
|
+
*
|
|
57
|
+
* Pass conditionExpression = 'attribute_not_exists(id)' to enforce uniqueness.
|
|
58
|
+
*/
|
|
59
|
+
export function buildPutItem(
|
|
60
|
+
tableName: string,
|
|
61
|
+
item: Record<string, unknown>,
|
|
62
|
+
conditionExpression?: string,
|
|
63
|
+
): PutItemParams {
|
|
64
|
+
const params: PutItemParams = { TableName: tableName, Item: item };
|
|
65
|
+
if (conditionExpression) params.ConditionExpression = conditionExpression;
|
|
66
|
+
return params;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* GetItem by primary key.
|
|
71
|
+
*/
|
|
72
|
+
export function buildGetItem(
|
|
73
|
+
tableName: string,
|
|
74
|
+
key: Record<string, unknown>,
|
|
75
|
+
): GetItemParams {
|
|
76
|
+
return { TableName: tableName, Key: key };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* UpdateItem with a SET expression built from the `updates` object.
|
|
81
|
+
* Only the supplied attributes are updated (diff-based call site).
|
|
82
|
+
*/
|
|
83
|
+
export function buildUpdateItem(
|
|
84
|
+
tableName: string,
|
|
85
|
+
key: Record<string, unknown>,
|
|
86
|
+
updates: Record<string, unknown>,
|
|
87
|
+
): UpdateItemParams {
|
|
88
|
+
const names: Record<string, string> = {};
|
|
89
|
+
const values: Record<string, unknown> = {};
|
|
90
|
+
const setClauses: string[] = [];
|
|
91
|
+
|
|
92
|
+
for (const [attr, val] of Object.entries(updates)) {
|
|
93
|
+
const nameAlias = `#${attr}`;
|
|
94
|
+
const valAlias = `:${attr}`;
|
|
95
|
+
names[nameAlias] = attr;
|
|
96
|
+
values[valAlias] = val;
|
|
97
|
+
setClauses.push(`${nameAlias} = ${valAlias}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
TableName: tableName,
|
|
102
|
+
Key: key,
|
|
103
|
+
UpdateExpression: `SET ${setClauses.join(', ')}`,
|
|
104
|
+
ExpressionAttributeNames: names,
|
|
105
|
+
ExpressionAttributeValues: values,
|
|
106
|
+
ReturnValues: 'NONE',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* DeleteItem by primary key.
|
|
112
|
+
*/
|
|
113
|
+
export function buildDeleteItem(
|
|
114
|
+
tableName: string,
|
|
115
|
+
key: Record<string, unknown>,
|
|
116
|
+
): DeleteItemParams {
|
|
117
|
+
return { TableName: tableName, Key: key };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* ScanCommand params.
|
|
122
|
+
* If conditions are supplied they are rendered as a FilterExpression using AND.
|
|
123
|
+
*/
|
|
124
|
+
export function buildScan(
|
|
125
|
+
tableName: string,
|
|
126
|
+
conditions?: Record<string, unknown>,
|
|
127
|
+
exclusiveStartKey?: Record<string, unknown>,
|
|
128
|
+
): ScanParams {
|
|
129
|
+
const params: ScanParams = { TableName: tableName };
|
|
130
|
+
|
|
131
|
+
if (exclusiveStartKey) params.ExclusiveStartKey = exclusiveStartKey;
|
|
132
|
+
|
|
133
|
+
if (conditions && Object.keys(conditions).length > 0) {
|
|
134
|
+
const validEntries = Object.entries(conditions).filter(
|
|
135
|
+
([, val]) => val !== undefined && val !== null,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (validEntries.length > 0) {
|
|
139
|
+
const names: Record<string, string> = {};
|
|
140
|
+
const values: Record<string, unknown> = {};
|
|
141
|
+
const clauses: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const [attr, val] of validEntries) {
|
|
144
|
+
const nameAlias = `#${attr}`;
|
|
145
|
+
const valAlias = `:${attr}`;
|
|
146
|
+
names[nameAlias] = attr;
|
|
147
|
+
values[valAlias] = val;
|
|
148
|
+
clauses.push(`${nameAlias} = ${valAlias}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
params.FilterExpression = clauses.join(' AND ');
|
|
152
|
+
params.ExpressionAttributeNames = names;
|
|
153
|
+
params.ExpressionAttributeValues = values;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return params;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* QueryCommand params for a GSI.
|
|
162
|
+
* keyConditions must be in the form { attrName: value } and will be rendered
|
|
163
|
+
* as equality expressions joined by AND.
|
|
164
|
+
*/
|
|
165
|
+
export function buildQuery(
|
|
166
|
+
tableName: string,
|
|
167
|
+
indexName: string,
|
|
168
|
+
keyConditions: Record<string, unknown>,
|
|
169
|
+
exclusiveStartKey?: Record<string, unknown>,
|
|
170
|
+
): QueryParams {
|
|
171
|
+
const validEntries = Object.entries(keyConditions).filter(
|
|
172
|
+
([, val]) => val !== undefined && val !== null,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (validEntries.length === 0) {
|
|
176
|
+
throw new Error('buildQuery: all keyCondition values are undefined/null');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const names: Record<string, string> = {};
|
|
180
|
+
const values: Record<string, unknown> = {};
|
|
181
|
+
const clauses: string[] = [];
|
|
182
|
+
|
|
183
|
+
for (const [attr, val] of validEntries) {
|
|
184
|
+
const nameAlias = `#${attr}`;
|
|
185
|
+
const valAlias = `:${attr}`;
|
|
186
|
+
names[nameAlias] = attr;
|
|
187
|
+
values[valAlias] = val;
|
|
188
|
+
clauses.push(`${nameAlias} = ${valAlias}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const params: QueryParams = {
|
|
192
|
+
TableName: tableName,
|
|
193
|
+
IndexName: indexName,
|
|
194
|
+
KeyConditionExpression: clauses.join(' AND '),
|
|
195
|
+
ExpressionAttributeNames: names,
|
|
196
|
+
ExpressionAttributeValues: values,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (exclusiveStartKey) params.ExclusiveStartKey = exclusiveStartKey;
|
|
200
|
+
|
|
201
|
+
return params;
|
|
202
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps ORM attribute types to DynamoDB scalar attribute types.
|
|
3
|
+
* DynamoDB DocumentClient auto-marshalls JS objects, so most values
|
|
4
|
+
* are sent as their native JS types. This map is used by the
|
|
5
|
+
* schema-introspector and startup provisioner for table/GSI creation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type DynamoScalarType = 'S' | 'N' | 'BOOL';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DynamoDB attribute-type string for a given ORM attr type.
|
|
12
|
+
* - string → S
|
|
13
|
+
* - number / float → N (stored as Number; DocumentClient handles it)
|
|
14
|
+
* - boolean → BOOL
|
|
15
|
+
* - date → S (ISO-8601 string — enables range queries)
|
|
16
|
+
* - timestamp → N (milliseconds since epoch)
|
|
17
|
+
* - passthrough/trim/etc → S (safe default)
|
|
18
|
+
*
|
|
19
|
+
* For key schema declarations only `S` and `N` are valid; BOOL
|
|
20
|
+
* is legal for attributes but never for a PK/SK.
|
|
21
|
+
*/
|
|
22
|
+
const typeMap: Record<string, DynamoScalarType> = {
|
|
23
|
+
string: 'S',
|
|
24
|
+
number: 'N',
|
|
25
|
+
float: 'N',
|
|
26
|
+
boolean: 'BOOL',
|
|
27
|
+
date: 'S',
|
|
28
|
+
timestamp: 'N',
|
|
29
|
+
passthrough: 'S',
|
|
30
|
+
trim: 'S',
|
|
31
|
+
uppercase: 'S',
|
|
32
|
+
ceil: 'N',
|
|
33
|
+
floor: 'N',
|
|
34
|
+
round: 'N',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the DynamoDB attribute type for a given ORM type string.
|
|
39
|
+
* Defaults to 'S' for any unknown/custom type.
|
|
40
|
+
*/
|
|
41
|
+
export function getDynamoType(attrType: string): DynamoScalarType {
|
|
42
|
+
return typeMap[attrType] ?? 'S';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the DynamoDB key type ('S' | 'N') for use in KeySchema.
|
|
47
|
+
* BOOL cannot be a key attribute; anything that maps to BOOL falls back to 'S'.
|
|
48
|
+
*/
|
|
49
|
+
export function getDynamoKeyType(attrType: string): 'S' | 'N' {
|
|
50
|
+
const t = getDynamoType(attrType);
|
|
51
|
+
return t === 'N' ? 'N' : 'S';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default typeMap;
|
package/src/main.ts
CHANGED
|
@@ -90,6 +90,11 @@ export default class Orm {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async init(): Promise<void> {
|
|
93
|
+
// Self-register so log.db works even when @stonyx/orm is in the
|
|
94
|
+
// consumer's `dependencies` (stonyx loader only merges devDependencies).
|
|
95
|
+
const { logColor = 'white', logMethod = 'db' } = config.orm;
|
|
96
|
+
log.defineType(logMethod, logColor);
|
|
97
|
+
|
|
93
98
|
const { paths, restServer } = config.orm;
|
|
94
99
|
|
|
95
100
|
const promises: Promise<unknown>[] = ['Model', 'Serializer', 'Transform'].map(type => {
|
|
@@ -159,6 +164,11 @@ export default class Orm {
|
|
|
159
164
|
this.sqlDb = new MysqlDB() as SqlDb;
|
|
160
165
|
this.db = this.sqlDb;
|
|
161
166
|
promises.push(this.sqlDb.init());
|
|
167
|
+
} else if (config.orm.dynamodb) {
|
|
168
|
+
const { default: DynamoDBDB } = await import('./dynamodb/dynamodb-db.js');
|
|
169
|
+
this.sqlDb = new DynamoDBDB() as SqlDb;
|
|
170
|
+
this.db = this.sqlDb;
|
|
171
|
+
promises.push(this.sqlDb.init());
|
|
162
172
|
} else if (this.options.dbType !== 'none') {
|
|
163
173
|
const db = new DB();
|
|
164
174
|
this.db = db;
|
package/src/manage-record.ts
CHANGED
|
@@ -79,6 +79,28 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
79
79
|
pendingHasMany.splice(0);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// FK-based inverse hasMany wiring — when a child record is created with a
|
|
83
|
+
// foreign-key field (e.g. `owner: 'owner-1'` on an animal), find any parent
|
|
84
|
+
// whose hasMany registry targets this model and push the child into the
|
|
85
|
+
// parent's shared array. This covers edge cases where the child is created
|
|
86
|
+
// in a separate async frame without a belongsTo handler firing.
|
|
87
|
+
const hasManyReg = getHasManyRegistry();
|
|
88
|
+
if (hasManyReg) {
|
|
89
|
+
for (const [parentModelName, targetMap] of hasManyReg) {
|
|
90
|
+
const childArrayMap = targetMap.get(modelName);
|
|
91
|
+
if (!childArrayMap) continue;
|
|
92
|
+
|
|
93
|
+
// Check if rawData contains a FK field matching the parent model name
|
|
94
|
+
const fkValue = rawData[parentModelName];
|
|
95
|
+
if (fkValue === undefined || fkValue === null) continue;
|
|
96
|
+
|
|
97
|
+
const parentArray = childArrayMap.get(fkValue);
|
|
98
|
+
if (parentArray && !parentArray.includes(record)) {
|
|
99
|
+
parentArray.push(record);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
// Fulfill pending belongsTo relationships
|
|
83
105
|
const pendingBelongsToQueue = getPendingBelongsToRegistry();
|
|
84
106
|
const pendingBelongsToRaw = pendingBelongsToQueue.get(modelName)?.get(record.id);
|
|
@@ -86,7 +108,7 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
86
108
|
|
|
87
109
|
if (pendingBelongsTo) {
|
|
88
110
|
const belongsToReg = getBelongsToRegistry();
|
|
89
|
-
const
|
|
111
|
+
const pendingHasManyReg = getHasManyRegistry();
|
|
90
112
|
|
|
91
113
|
for (const { sourceRecord, sourceModelName, relationshipKey, relationshipId } of pendingBelongsTo) {
|
|
92
114
|
// Update the belongsTo relationship on the source record
|
|
@@ -103,7 +125,7 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
// Wire inverse hasMany if it exists
|
|
106
|
-
const inverseHasMany =
|
|
128
|
+
const inverseHasMany = pendingHasManyReg.get(modelName)?.get(sourceModelName)?.get(record.id);
|
|
107
129
|
|
|
108
130
|
if (inverseHasMany && !inverseHasMany.includes(sourceRecord)) {
|
|
109
131
|
inverseHasMany.push(sourceRecord);
|
|
@@ -117,15 +139,25 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
|
|
|
117
139
|
// Auto-persist to SQL — skip for DB loads (isDbRecord) and relationship resolution (_relationshipKey)
|
|
118
140
|
const shouldPersist = orm?.sqlDb && !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
|
|
119
141
|
if (shouldPersist) {
|
|
142
|
+
// Capture ID before persist — SQL adapters re-key pending IDs to real DB IDs,
|
|
143
|
+
// but relationship registries were keyed with this original ID
|
|
144
|
+
const registryId = record.id;
|
|
120
145
|
const response = { data: { id: record.id } };
|
|
121
|
-
orm!.sqlDb!.persist('create', modelName, { rawData }, response)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
orm!.sqlDb!.persist('create', modelName, { rawData }, response)
|
|
147
|
+
.catch((err: unknown) => {
|
|
148
|
+
orm!.emitPersistError({
|
|
149
|
+
operation: 'create',
|
|
150
|
+
modelName,
|
|
151
|
+
recordId: record.id,
|
|
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
|
+
}
|
|
127
160
|
});
|
|
128
|
-
});
|
|
129
161
|
}
|
|
130
162
|
|
|
131
163
|
return record;
|
package/src/mysql/connection.ts
CHANGED
package/src/mysql/mysql-db.ts
CHANGED
|
@@ -84,6 +84,15 @@ export default class MysqlDB {
|
|
|
84
84
|
pool!: Pool | null;
|
|
85
85
|
mysqlConfig!: MysqlConfig;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Promise-chain mutex for write serialization (#156).
|
|
89
|
+
* All persist() calls chain through this single queue so concurrent
|
|
90
|
+
* fire-and-forget writes never produce parallel InnoDB transactions
|
|
91
|
+
* on FK-linked rows (which cause deadlocks).
|
|
92
|
+
* Reads are NOT affected — only persist() serializes.
|
|
93
|
+
*/
|
|
94
|
+
private _writeQueue: Promise<void> = Promise.resolve();
|
|
95
|
+
|
|
87
96
|
constructor(deps: Partial<MysqlDBDeps> = {}) {
|
|
88
97
|
if (MysqlDB.instance) return MysqlDB.instance;
|
|
89
98
|
MysqlDB.instance = this;
|
|
@@ -118,7 +127,15 @@ export default class MysqlDB {
|
|
|
118
127
|
if (pending.length > 0) {
|
|
119
128
|
this.deps.log.db?.(`${pending.length} pending migration(s) found.`);
|
|
120
129
|
|
|
121
|
-
|
|
130
|
+
let shouldApply: boolean;
|
|
131
|
+
if (this.mysqlConfig.autoMigrate === true) {
|
|
132
|
+
shouldApply = true;
|
|
133
|
+
} else if (this.mysqlConfig.autoMigrate === false) {
|
|
134
|
+
shouldApply = false;
|
|
135
|
+
this.deps.log.warn?.(`autoMigrate is false — skipping ${pending.length} pending migration(s).`);
|
|
136
|
+
} else {
|
|
137
|
+
shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
|
|
138
|
+
}
|
|
122
139
|
|
|
123
140
|
if (shouldApply) {
|
|
124
141
|
for (const filename of pending) {
|
|
@@ -139,9 +156,17 @@ export default class MysqlDB {
|
|
|
139
156
|
const modelCount = Object.keys(schemas).length;
|
|
140
157
|
|
|
141
158
|
if (modelCount > 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
let shouldGenerate: boolean;
|
|
160
|
+
if (this.mysqlConfig.autoMigrate === true) {
|
|
161
|
+
shouldGenerate = true;
|
|
162
|
+
} else if (this.mysqlConfig.autoMigrate === false) {
|
|
163
|
+
shouldGenerate = false;
|
|
164
|
+
this.deps.log.warn?.(`autoMigrate is false — skipping initial migration generation for ${modelCount} model(s).`);
|
|
165
|
+
} else {
|
|
166
|
+
shouldGenerate = await this.deps.confirm(
|
|
167
|
+
`No migrations found but ${modelCount} model(s) detected. Generate and apply initial migration?`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
145
170
|
|
|
146
171
|
if (shouldGenerate) {
|
|
147
172
|
const { generateMigration } = await import('./migration-generator.js');
|
|
@@ -398,14 +423,21 @@ export default class MysqlDB {
|
|
|
398
423
|
const Orm = (await import('@stonyx/orm')).default;
|
|
399
424
|
if ((Orm as unknown as { instance?: { isView?: (name: string) => boolean } }).instance?.isView?.(modelName)) return;
|
|
400
425
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
426
|
+
const work = async () => {
|
|
427
|
+
switch (operation) {
|
|
428
|
+
case 'create':
|
|
429
|
+
return this._persistCreate(modelName, context, response);
|
|
430
|
+
case 'update':
|
|
431
|
+
return this._persistUpdate(modelName, context, response);
|
|
432
|
+
case 'delete':
|
|
433
|
+
return this._persistDelete(modelName, context);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Chain through the write queue — .then(work, work) ensures the queue
|
|
438
|
+
// advances even when a previous persist rejects (#156).
|
|
439
|
+
this._writeQueue = this._writeQueue.then(work, work);
|
|
440
|
+
return this._writeQueue;
|
|
409
441
|
}
|
|
410
442
|
|
|
411
443
|
private async _persistCreate(modelName: string, context: PersistContext, response: PersistResponse): Promise<void> {
|