uql-orm 0.52.0 → 0.54.0
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/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/cockroachdb/cockroachDialect.d.ts +2 -5
- package/dist/cockroachdb/cockroachDialect.js +2 -5
- package/dist/dialect/abstractDialect.d.ts +5 -5
- package/dist/dialect/abstractDialect.js +7 -6
- package/dist/dialect/abstractSqlDialect.d.ts +8 -3
- package/dist/dialect/abstractSqlDialect.js +14 -11
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -4
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -6
- package/dist/dialect/pgLikeSqlDialect.js +0 -2
- package/dist/dialect/vectorSqlDialect.js +2 -1
- package/dist/entity/decorator/members.d.ts +3 -10
- package/dist/entity/metadata/definition.js +0 -4
- package/dist/http/handler.js +3 -12
- package/dist/http/query.js +4 -1
- package/dist/migrate/builder/migrationBuilder.js +0 -4
- package/dist/migrate/ddl/index.d.ts +8 -0
- package/dist/migrate/ddl/index.js +11 -0
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +23 -0
- package/dist/migrate/ddl/mssqlTableDdl.js +61 -0
- package/dist/migrate/ddl/tableDdl.d.ts +34 -0
- package/dist/migrate/ddl/tableDdl.js +71 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +6 -9
- package/dist/migrate/introspection/mssqlIntrospector.js +40 -33
- package/dist/migrate/migrator.d.ts +3 -7
- package/dist/migrate/migrator.js +3 -7
- package/dist/migrate/schemaGenerator.d.ts +8 -23
- package/dist/migrate/schemaGenerator.js +29 -87
- package/dist/migrate/storage/databaseStorage.d.ts +2 -2
- package/dist/migrate/storage/databaseStorage.js +2 -2
- package/dist/mongo/mongoDialect.js +9 -14
- package/dist/mongo/mongodbQuerier.d.ts +3 -5
- package/dist/mongo/mongodbQuerier.js +18 -19
- package/dist/mssql/mssqlDialect.d.ts +14 -1
- package/dist/mssql/mssqlDialect.js +22 -7
- package/dist/querier/abstractQuerier.d.ts +17 -22
- package/dist/querier/abstractQuerier.js +92 -67
- package/dist/querier/abstractSqlQuerier.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.js +71 -87
- package/dist/schema/canonicalType.js +2 -2
- package/dist/schema/schemaASTBuilder.js +7 -7
- package/dist/sqlite/sqliteDialect.js +0 -2
- package/dist/type/dialect.d.ts +0 -8
- package/dist/type/entity.d.ts +6 -11
- package/dist/type/migration.d.ts +0 -3
- package/dist/type/query.d.ts +13 -13
- package/dist/type/query.js +0 -6
- package/dist/type/queryWhere.d.ts +7 -19
- package/dist/type/universalQuerier.d.ts +3 -3
- package/dist/type/vector.d.ts +3 -1
- package/dist/util/dialect.util.d.ts +12 -8
- package/dist/util/dialect.util.js +17 -29
- package/dist/util/field.util.d.ts +4 -16
- package/dist/util/field.util.js +6 -19
- package/dist/util/fieldOption.util.d.ts +1 -4
- package/dist/util/fieldOption.util.js +0 -2
- package/dist/util/logger.d.ts +3 -3
- package/dist/util/logger.js +3 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/raw.d.ts +3 -10
- package/dist/util/sql.util.js +2 -2
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
1
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
2
2
|
import { LoggerWrapper, type ParentJoin, type ParentPartition } from '../util/index.js';
|
|
3
|
+
/** Base class for all database queriers. */
|
|
3
4
|
export declare abstract class AbstractQuerier implements Querier {
|
|
4
5
|
readonly extra?: ExtraOptions | undefined;
|
|
5
6
|
/**
|
|
@@ -109,11 +110,12 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
109
110
|
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
110
111
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
|
|
111
112
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
* The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
|
|
114
|
+
* same rows the statement wrote.
|
|
114
115
|
*/
|
|
115
116
|
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
116
|
-
|
|
117
|
+
/** Writes `rows`, and onto each one the key the database generated for it, where it can tell. */
|
|
118
|
+
protected abstract internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
117
119
|
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
118
120
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
119
121
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -150,20 +152,9 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
150
152
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
151
153
|
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
|
|
152
154
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* Whether a row names its key decides which statement it takes, never whether the row exists: an
|
|
157
|
-
* id the caller invented is not proof of anything, and a stale one used to issue an `UPDATE` that
|
|
158
|
-
* matched nothing and reported success. A named row upserts on its own key, so it is written
|
|
159
|
-
* either way and no read can go stale between deciding and writing. An unnamed one inserts, and
|
|
160
|
-
* the database assigns the key.
|
|
161
|
-
*
|
|
162
|
-
* A composite key is always supplied by the caller, so it always takes the upsert branch - which
|
|
163
|
-
* is why nothing here special-cases one, and why this is the method that stopped refusing them.
|
|
164
|
-
*
|
|
165
|
-
* The hooks follow the statement: a named row fires `beforeUpsert`/`afterUpsert`, never the
|
|
166
|
-
* update pair, because the database picks the branch as the statement runs.
|
|
155
|
+
* Whether a row names its key decides its statement, never whether the row exists: a named row
|
|
156
|
+
* upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
|
|
157
|
+
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
167
158
|
*/
|
|
168
159
|
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
169
160
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
@@ -217,14 +208,18 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
217
208
|
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
218
209
|
private hasHook;
|
|
219
210
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
211
|
+
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
212
|
+
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
213
|
+
* `undefined`: a missing id is honest where a guessed one is not.
|
|
222
214
|
*/
|
|
215
|
+
protected idsByConflict<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, rows: EntityData<E>[]): Promise<(PrimaryKey | undefined)[]>;
|
|
223
216
|
/**
|
|
224
|
-
* Runs `write` between the event's `before`/`after` pair.
|
|
225
|
-
*
|
|
217
|
+
* Runs `write` between the event's `before`/`after` pair. The before hooks get the caller's rows, so
|
|
218
|
+
* what they assign is written; `write` and the after hooks get a copy, which by then carries what
|
|
219
|
+
* the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
|
|
226
220
|
*/
|
|
227
221
|
private hooked;
|
|
222
|
+
/** Fires the global listeners first, then the entity's own hooks. */
|
|
228
223
|
private emitHook;
|
|
229
224
|
/**
|
|
230
225
|
* Runs `task` after everything already queued on this querier, one at a time.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap,
|
|
2
|
+
import { asSelectMap, childrenOf, clone, dataKeyed, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
6
6
|
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
7
|
-
* `{ $where: id }`, and a
|
|
8
|
-
* table. An entity declares its id optional, which puts `undefined` inside
|
|
9
|
-
* HTTP layer reaches these methods with parsed JSON regardless, so the guard
|
|
7
|
+
* `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
|
|
8
|
+
* addresses the whole table. An entity declares its id optional, which puts `undefined` inside
|
|
9
|
+
* `IdValue<E>`, and the HTTP layer reaches these methods with parsed JSON regardless, so the guard
|
|
10
|
+
* belongs at runtime.
|
|
10
11
|
*
|
|
11
12
|
* Its callers are all `async` so this surfaces as a rejection on every one of them: a guard that
|
|
12
13
|
* threw synchronously from some and rejected from others would escape a caller's `.catch()`.
|
|
@@ -16,7 +17,7 @@ function assertIdValue(entity, id) {
|
|
|
16
17
|
throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
|
|
17
18
|
}
|
|
18
19
|
if (isScalarId(id)) {
|
|
19
|
-
// One value names one column, which `
|
|
20
|
+
// One value names one column, which `whereIds` refuses on a composite.
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
// Every key, or the `$where` names only some of the columns and addresses each row that agrees on
|
|
@@ -40,22 +41,27 @@ function soleParentColumn(relOpts) {
|
|
|
40
41
|
return parentJoins(relOpts, 1)[0].joined;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
44
|
+
* The id each written row is named by, in payload order. Read off the rows as written, so a key the
|
|
45
|
+
* database generated or the ORM filled is there, and a composite is named by every column of it.
|
|
45
46
|
*/
|
|
47
|
+
function writtenIds(meta, rows) {
|
|
48
|
+
return rows.map((row) => (namesKey(meta, row) ? idOf(meta, row) : undefined));
|
|
49
|
+
}
|
|
46
50
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* statement reports one. A sole key takes what the statement reported when it spoke for every row,
|
|
51
|
-
* and otherwise falls back to the key the caller supplied - a `firstId` dialect reports nothing for
|
|
52
|
-
* a batch, which is not the same as those rows having no id.
|
|
51
|
+
* Writes the key an upsert reported onto each row that named none - only the key, since an `onInsert`
|
|
52
|
+
* value was never written to a row the upsert updated. A report aligns with the rows only when it
|
|
53
|
+
* speaks for every one: a `firstId` dialect reports nothing for a batch.
|
|
53
54
|
*/
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
function adoptReportedIds(meta, rows, reported) {
|
|
56
|
+
if (meta.ids.length !== 1 || reported?.length !== rows.length) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const [idKey] = meta.ids;
|
|
60
|
+
for (let index = 0; index < rows.length; index++) {
|
|
61
|
+
rows[index][idKey] ??= reported[index];
|
|
62
|
+
}
|
|
58
63
|
}
|
|
64
|
+
/** Base class for all database queriers. */
|
|
59
65
|
export class AbstractQuerier {
|
|
60
66
|
extra;
|
|
61
67
|
/**
|
|
@@ -119,7 +125,7 @@ export class AbstractQuerier {
|
|
|
119
125
|
}
|
|
120
126
|
async findOneById(entity, id, q = {}, opts) {
|
|
121
127
|
assertIdValue(entity, id);
|
|
122
|
-
return this.findOne(entity, { ...q, $where:
|
|
128
|
+
return this.findOne(entity, { ...q, $where: { ...q.$where, ...whereIds(getMeta(entity), id) } }, opts);
|
|
123
129
|
}
|
|
124
130
|
async findOne(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
125
131
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
@@ -181,34 +187,40 @@ export class AbstractQuerier {
|
|
|
181
187
|
return id;
|
|
182
188
|
}
|
|
183
189
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
190
|
+
* The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
|
|
191
|
+
* same rows the statement wrote.
|
|
186
192
|
*/
|
|
187
193
|
async insertMany(entity, payload) {
|
|
194
|
+
if (!payload?.length) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
188
197
|
const meta = getMeta(entity);
|
|
189
|
-
return this.hooked(entity, 'Insert', payload, async () => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return
|
|
198
|
+
return this.hooked(entity, 'Insert', payload, async (rows) => {
|
|
199
|
+
fillOnFields(meta, rows, 'onInsert');
|
|
200
|
+
await this.internalInsertMany(entity, rows);
|
|
201
|
+
return writtenIds(meta, rows);
|
|
193
202
|
});
|
|
194
203
|
}
|
|
195
204
|
async updateOneById(entity, id, payload, opts) {
|
|
196
205
|
assertIdValue(entity, id);
|
|
197
|
-
return this.updateMany(entity, { $where: id }, payload, opts);
|
|
206
|
+
return this.updateMany(entity, { $where: whereIds(getMeta(entity), id) }, payload, opts);
|
|
198
207
|
}
|
|
199
208
|
async updateMany(entity, q, payload, opts) {
|
|
200
|
-
return this.hooked(entity, 'Update', [payload], () =>
|
|
209
|
+
return this.hooked(entity, 'Update', [payload], ([row]) => {
|
|
210
|
+
fillOnFields(getMeta(entity), [row], 'onUpdate');
|
|
211
|
+
return this.internalUpdateMany(entity, q, row, opts);
|
|
212
|
+
});
|
|
201
213
|
}
|
|
202
214
|
async restoreOneById(entity, id) {
|
|
203
215
|
assertIdValue(entity, id);
|
|
204
|
-
return this.restoreMany(entity, { $where: id });
|
|
216
|
+
return this.restoreMany(entity, { $where: whereIds(getMeta(entity), id) });
|
|
205
217
|
}
|
|
206
218
|
async restoreMany(entity, q) {
|
|
207
219
|
const meta = getMeta(entity);
|
|
208
220
|
if (!meta.softDelete) {
|
|
209
221
|
throw new TypeError(`'${entity.name}' has not enabled 'softDelete'`);
|
|
210
222
|
}
|
|
211
|
-
const $where =
|
|
223
|
+
const $where = { ...q.$where, [meta.softDelete]: { $ne: null } };
|
|
212
224
|
return this.updateMany(entity, { ...q, $where }, { [meta.softDelete]: null }, {
|
|
213
225
|
filters: { softDelete: false },
|
|
214
226
|
});
|
|
@@ -220,21 +232,25 @@ export class AbstractQuerier {
|
|
|
220
232
|
* was how an `@Id({ onInsert })` or an audit trail silently skipped this path.
|
|
221
233
|
*/
|
|
222
234
|
async upsertOne(entity, conflictPaths, payload) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
235
|
+
const meta = getMeta(entity);
|
|
236
|
+
return this.hooked(entity, 'Upsert', [payload], async (rows) => {
|
|
237
|
+
const { ids, changes, created } = await this.internalUpsertOne(entity, conflictPaths, rows[0]);
|
|
238
|
+
adoptReportedIds(meta, rows, ids);
|
|
239
|
+
const [id] = writtenIds(meta, rows);
|
|
226
240
|
return { id, changes, created };
|
|
227
241
|
});
|
|
228
242
|
}
|
|
229
243
|
async upsertMany(entity, conflictPaths, payload) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
const meta = getMeta(entity);
|
|
245
|
+
return this.hooked(entity, 'Upsert', payload, async (rows) => {
|
|
246
|
+
const { ids, changes } = await this.internalUpsertMany(entity, conflictPaths, rows);
|
|
247
|
+
adoptReportedIds(meta, rows, ids);
|
|
248
|
+
return { ids: writtenIds(meta, rows), changes };
|
|
233
249
|
});
|
|
234
250
|
}
|
|
235
251
|
async deleteOneById(entity, id, opts) {
|
|
236
252
|
assertIdValue(entity, id);
|
|
237
|
-
return this.deleteMany(entity, { $where: id }, opts);
|
|
253
|
+
return this.deleteMany(entity, { $where: whereIds(getMeta(entity), id) }, opts);
|
|
238
254
|
}
|
|
239
255
|
async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
|
|
240
256
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
|
|
@@ -248,7 +264,8 @@ export class AbstractQuerier {
|
|
|
248
264
|
let target = q;
|
|
249
265
|
if (doomed) {
|
|
250
266
|
const meta = getMeta(entity);
|
|
251
|
-
|
|
267
|
+
const ids = doomed.map((it) => idOf(meta, it));
|
|
268
|
+
target = { $where: whereIds(meta, ids) };
|
|
252
269
|
}
|
|
253
270
|
await this.emitHook(entity, 'beforeDelete', doomed ?? []);
|
|
254
271
|
const changes = await this.internalDeleteMany(entity, target, opts);
|
|
@@ -276,20 +293,9 @@ export class AbstractQuerier {
|
|
|
276
293
|
return id;
|
|
277
294
|
}
|
|
278
295
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* Whether a row names its key decides which statement it takes, never whether the row exists: an
|
|
283
|
-
* id the caller invented is not proof of anything, and a stale one used to issue an `UPDATE` that
|
|
284
|
-
* matched nothing and reported success. A named row upserts on its own key, so it is written
|
|
285
|
-
* either way and no read can go stale between deciding and writing. An unnamed one inserts, and
|
|
286
|
-
* the database assigns the key.
|
|
287
|
-
*
|
|
288
|
-
* A composite key is always supplied by the caller, so it always takes the upsert branch - which
|
|
289
|
-
* is why nothing here special-cases one, and why this is the method that stopped refusing them.
|
|
290
|
-
*
|
|
291
|
-
* The hooks follow the statement: a named row fires `beforeUpsert`/`afterUpsert`, never the
|
|
292
|
-
* update pair, because the database picks the branch as the statement runs.
|
|
296
|
+
* Whether a row names its key decides its statement, never whether the row exists: a named row
|
|
297
|
+
* upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
|
|
298
|
+
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
293
299
|
*/
|
|
294
300
|
async saveMany(entity, payload) {
|
|
295
301
|
const meta = getMeta(entity);
|
|
@@ -318,15 +324,15 @@ export class AbstractQuerier {
|
|
|
318
324
|
const write = async () => {
|
|
319
325
|
if (toInsert.length) {
|
|
320
326
|
const inserted = await this.insertMany(entity, toInsert.map((index) => payload[index]));
|
|
321
|
-
toInsert.
|
|
322
|
-
ids[
|
|
323
|
-
}
|
|
327
|
+
for (let position = 0; position < toInsert.length; position++) {
|
|
328
|
+
ids[toInsert[position]] = inserted[position];
|
|
329
|
+
}
|
|
324
330
|
}
|
|
325
331
|
if (toUpsert.length) {
|
|
326
332
|
const conflictPaths = Object.fromEntries(meta.ids.map((key) => [key, true]));
|
|
327
|
-
await this.upsertMany(entity, conflictPaths, toUpsert.map((index) => payload[index]));
|
|
328
|
-
for (
|
|
329
|
-
ids[
|
|
333
|
+
const { ids: upserted } = await this.upsertMany(entity, conflictPaths, toUpsert.map((index) => payload[index]));
|
|
334
|
+
for (let position = 0; position < toUpsert.length; position++) {
|
|
335
|
+
ids[toUpsert[position]] = upserted[position];
|
|
330
336
|
}
|
|
331
337
|
}
|
|
332
338
|
};
|
|
@@ -517,16 +523,15 @@ export class AbstractQuerier {
|
|
|
517
523
|
// unless this has run - and this method is `protected`, so a caller can arrive without them.
|
|
518
524
|
assertSoleId(meta, 'saving a relation');
|
|
519
525
|
const relEntity = relOpts.entity();
|
|
520
|
-
const relPayload = relValue;
|
|
521
526
|
switch (relOpts.cardinality) {
|
|
522
527
|
case '1m':
|
|
523
528
|
case 'mm':
|
|
524
|
-
return this.saveToMany(relOpts, relEntity, ids,
|
|
529
|
+
return this.saveToMany(relOpts, relEntity, ids, relValue, isUpdate);
|
|
525
530
|
case '11':
|
|
526
|
-
return this.saveOneToOne(relEntity, relOpts, ids,
|
|
531
|
+
return this.saveOneToOne(relEntity, relOpts, ids, relValue, isUpdate);
|
|
527
532
|
case 'm1':
|
|
528
|
-
if (
|
|
529
|
-
return this.saveManyToOne(entity, relEntity, relOpts, ids,
|
|
533
|
+
if (relValue)
|
|
534
|
+
return this.saveManyToOne(entity, relEntity, relOpts, ids, relValue);
|
|
530
535
|
}
|
|
531
536
|
}
|
|
532
537
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
@@ -626,19 +631,39 @@ export class AbstractQuerier {
|
|
|
626
631
|
return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
|
|
627
632
|
}
|
|
628
633
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
634
|
+
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
635
|
+
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
636
|
+
* `undefined`: a missing id is honest where a guessed one is not.
|
|
631
637
|
*/
|
|
638
|
+
async idsByConflict(entity, conflictPaths, rows) {
|
|
639
|
+
const meta = getMeta(entity);
|
|
640
|
+
const [idKey] = meta.ids;
|
|
641
|
+
const keys = getKeys(conflictPaths);
|
|
642
|
+
const q = {
|
|
643
|
+
$select: Object.fromEntries([idKey, ...keys].map((key) => [key, true])),
|
|
644
|
+
$where: { $or: rows.map((row) => Object.fromEntries(keys.map((key) => [key, row[key]]))) },
|
|
645
|
+
};
|
|
646
|
+
const found = await this.internalFindMany(entity, q, { filters: withoutSoftDeleteFilter(undefined) });
|
|
647
|
+
const byConflict = new Map();
|
|
648
|
+
for (const row of found) {
|
|
649
|
+
const key = rowKey(row, keys);
|
|
650
|
+
byConflict.set(key, byConflict.has(key) ? undefined : row[idKey]);
|
|
651
|
+
}
|
|
652
|
+
return rows.map((row) => byConflict.get(rowKey(row, keys)));
|
|
653
|
+
}
|
|
632
654
|
/**
|
|
633
|
-
* Runs `write` between the event's `before`/`after` pair.
|
|
634
|
-
*
|
|
655
|
+
* Runs `write` between the event's `before`/`after` pair. The before hooks get the caller's rows, so
|
|
656
|
+
* what they assign is written; `write` and the after hooks get a copy, which by then carries what
|
|
657
|
+
* the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
|
|
635
658
|
*/
|
|
636
659
|
async hooked(entity, event, payloads, write) {
|
|
637
660
|
await this.emitHook(entity, `before${event}`, payloads);
|
|
638
|
-
const
|
|
639
|
-
await
|
|
661
|
+
const rows = clone(payloads);
|
|
662
|
+
const result = await write(rows);
|
|
663
|
+
await this.emitHook(entity, `after${event}`, rows);
|
|
640
664
|
return result;
|
|
641
665
|
}
|
|
666
|
+
/** Fires the global listeners first, then the entity's own hooks. */
|
|
642
667
|
async emitHook(entity, event, payloads) {
|
|
643
668
|
if (!this.hasHook(entity, event))
|
|
644
669
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions,
|
|
2
|
+
import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
import { type ParentPartition } from '../util/index.js';
|
|
4
4
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
5
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
@@ -71,6 +71,13 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
71
71
|
* query, so neither can reach here.
|
|
72
72
|
*/
|
|
73
73
|
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
74
|
+
/**
|
|
75
|
+
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
76
|
+
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
77
|
+
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
78
|
+
* pair outright. Both then cost a second statement; only the counting differs.
|
|
79
|
+
*/
|
|
80
|
+
private countedSeparately;
|
|
74
81
|
/**
|
|
75
82
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
76
83
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -82,13 +89,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
82
89
|
* answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
|
|
83
90
|
* too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
|
|
84
91
|
*/
|
|
85
|
-
/**
|
|
86
|
-
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
87
|
-
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
88
|
-
* before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
|
|
89
|
-
* pair outright. Both then cost a second statement; only the counting differs.
|
|
90
|
-
*/
|
|
91
|
-
private countedSeparately;
|
|
92
92
|
protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
93
93
|
private selectRows;
|
|
94
94
|
private hydrateRows;
|
|
@@ -119,7 +119,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
119
119
|
protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
120
120
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
121
121
|
protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
122
|
-
internalInsertMany<E extends object>(entity: Type<E>,
|
|
122
|
+
internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
123
123
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
124
124
|
/** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
|
|
125
125
|
private settleIds;
|