uql-orm 0.41.1 → 0.42.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/querier/httpQuerier.d.ts +4 -4
- package/dist/browser/querier/httpQuerier.js +21 -6
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +1 -1
- package/dist/dialect/abstractSqlDialect.js +26 -21
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -2
- package/dist/entity/decorator/members.d.ts +10 -2
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +18 -1
- package/dist/entity/metadata/definition.js +91 -36
- package/dist/http/handler.js +4 -4
- package/dist/maria/mariaDialect.d.ts +2 -2
- package/dist/maria/mariaDialect.js +2 -2
- package/dist/migrate/schemaGenerator.d.ts +2 -2
- package/dist/migrate/schemaGenerator.js +3 -3
- package/dist/mongo/mongoDialect.js +17 -6
- package/dist/mongo/mongodbQuerier.js +6 -3
- package/dist/querier/abstractQuerier.d.ts +16 -12
- package/dist/querier/abstractQuerier.js +77 -40
- package/dist/querier/abstractQuerierPool.d.ts +8 -8
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +12 -7
- package/dist/querier/relationCount.js +40 -31
- package/dist/schema/schemaASTBuilder.js +35 -22
- package/dist/type/entity.d.ts +57 -14
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.d.ts +6 -0
- package/dist/util/object.util.js +10 -0
- package/dist/util/relationQuery.util.d.ts +38 -7
- package/dist/util/relationQuery.util.js +50 -9
- package/dist/util/rowKey.util.d.ts +9 -0
- package/dist/util/rowKey.util.js +24 -0
- package/package.json +1 -1
|
@@ -24,12 +24,6 @@ export function defineField(entity, key, opts = {}) {
|
|
|
24
24
|
return meta;
|
|
25
25
|
}
|
|
26
26
|
export function defineId(entity, key, opts) {
|
|
27
|
-
const meta = ensureMeta(entity);
|
|
28
|
-
const id = getIdKey(meta);
|
|
29
|
-
if (id) {
|
|
30
|
-
// A subclass narrowing the inherited primary key: drop the old one so exactly one stays marked.
|
|
31
|
-
delete meta.fields[id];
|
|
32
|
-
}
|
|
33
27
|
return defineField(entity, key, { ...opts, isId: true });
|
|
34
28
|
}
|
|
35
29
|
// `RelationOptions` is parameterized by the *target* entity, which is independent of the owner `E`, so it
|
|
@@ -167,16 +161,51 @@ export function defineEntity(entity, opts = {}) {
|
|
|
167
161
|
meta.filters = {};
|
|
168
162
|
meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
|
|
169
163
|
}
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
throw TypeError(`'${entity.name}' must have
|
|
164
|
+
const ids = getIdKeys(meta);
|
|
165
|
+
if (!ids.length) {
|
|
166
|
+
throw TypeError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
|
|
173
167
|
}
|
|
174
|
-
meta.
|
|
168
|
+
meta.ids = ids;
|
|
175
169
|
return meta;
|
|
176
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Refuses an entity whose primary key is not one column, naming the path that cannot express it.
|
|
173
|
+
*
|
|
174
|
+
* Refusing rather than taking the first of several is the whole guarantee: pairing one column of a
|
|
175
|
+
* two-column key is how a statement silently addresses rows that merely agree on it.
|
|
176
|
+
*/
|
|
177
|
+
export function assertSoleId(meta, what) {
|
|
178
|
+
const { ids } = meta;
|
|
179
|
+
if (ids.length === 1) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
throw new TypeError(ids.length
|
|
183
|
+
? `'${meta.entity.name}' has a composite primary key (${ids.join(', ')}), which ${what} does not support yet.`
|
|
184
|
+
: // An entity registered with `@Field` but no `@Entity` never ran the check in `defineEntity`.
|
|
185
|
+
`'${meta.entity.name}' has no primary key, which ${what} needs.`);
|
|
186
|
+
}
|
|
187
|
+
/** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
|
|
188
|
+
export function soleIdOf(meta, what) {
|
|
189
|
+
assertSoleId(meta, what);
|
|
190
|
+
return meta.ids[0];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A row's primary key in the shape a `$where` takes: the value itself for a single key, an object
|
|
194
|
+
* carrying every key for a composite - which is exactly {@link EntityId}.
|
|
195
|
+
*
|
|
196
|
+
* What a settled write names its rows by, and what an insert hands back. Naming a composite row by
|
|
197
|
+
* one of its columns would address every row agreeing on that one.
|
|
198
|
+
*/
|
|
199
|
+
export function idOf(meta, row) {
|
|
200
|
+
const { ids } = meta;
|
|
201
|
+
if (ids.length === 1) {
|
|
202
|
+
return row[ids[0]];
|
|
203
|
+
}
|
|
204
|
+
return Object.fromEntries(ids.map((key) => [key, row[key]]));
|
|
205
|
+
}
|
|
177
206
|
export function getEntities() {
|
|
178
207
|
return [...metas.entries()].reduce((acc, [key, val]) => {
|
|
179
|
-
if (val.
|
|
208
|
+
if (val.ids.length) {
|
|
180
209
|
acc.push(key);
|
|
181
210
|
}
|
|
182
211
|
return acc;
|
|
@@ -187,7 +216,7 @@ function ensureMeta(entity) {
|
|
|
187
216
|
if (meta) {
|
|
188
217
|
return meta;
|
|
189
218
|
}
|
|
190
|
-
meta = { entity,
|
|
219
|
+
meta = { entity, ids: [], fields: {}, relations: {} };
|
|
191
220
|
metas.set(entity, meta);
|
|
192
221
|
return meta;
|
|
193
222
|
}
|
|
@@ -210,7 +239,7 @@ function fillRelations(meta) {
|
|
|
210
239
|
continue;
|
|
211
240
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
212
241
|
if (relOpts.mappedBy) {
|
|
213
|
-
fillInverseSide(at, relOpts);
|
|
242
|
+
fillInverseSide(at, meta, relOpts);
|
|
214
243
|
}
|
|
215
244
|
else if (!relOpts.references) {
|
|
216
245
|
fillOwningSide(at, meta, relKey, relOpts);
|
|
@@ -234,13 +263,13 @@ function fillRelations(meta) {
|
|
|
234
263
|
}
|
|
235
264
|
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
236
265
|
const relMeta = ensureMeta(relOpts.entity());
|
|
237
|
-
const relIdKey = relMeta.id;
|
|
238
266
|
if (relOpts.through) {
|
|
239
267
|
// Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
|
|
240
|
-
// `deleteRelations` and every dialect read them as junction columns.
|
|
268
|
+
// `deleteRelations` and every dialect read them as junction columns. A composite key contributes
|
|
269
|
+
// one pair per column of it, which is what makes the join address a whole key rather than part.
|
|
241
270
|
relOpts.references = [
|
|
242
|
-
{ local: junctionColumn(meta,
|
|
243
|
-
{ local: junctionColumn(relMeta,
|
|
271
|
+
...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
|
|
272
|
+
...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
|
|
244
273
|
];
|
|
245
274
|
return;
|
|
246
275
|
}
|
|
@@ -248,21 +277,29 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
248
277
|
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
249
278
|
"'through' (a junction entity), or 'references' (the columns).");
|
|
250
279
|
}
|
|
251
|
-
|
|
252
|
-
|
|
280
|
+
// `<rel>Id` for the one-key case it has always been; `<rel><Key>` per column otherwise. Both name a
|
|
281
|
+
// property, so both are spelled from the referenced *property* - a column name is what the naming
|
|
282
|
+
// strategy makes of this afterwards.
|
|
283
|
+
const sole = relMeta.ids.length === 1;
|
|
284
|
+
relOpts.references = relMeta.ids.map((key) => ({
|
|
285
|
+
local: sole ? `${relKey}Id` : `${relKey}${upperFirst(key)}`,
|
|
286
|
+
foreign: key,
|
|
287
|
+
}));
|
|
253
288
|
// `typeFromReference` so schema generation resolves the referenced primary key's exact type
|
|
254
289
|
// (columnType, length, chained keys) rather than trusting the fallback, as it does for an
|
|
255
290
|
// explicit `@Field({ references })`.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
const fields = meta.fields;
|
|
292
|
+
for (const { local, foreign } of relOpts.references) {
|
|
293
|
+
fields[local] ??= {
|
|
294
|
+
name: local,
|
|
295
|
+
type: relMeta.fields[foreign]?.type ?? Number,
|
|
260
296
|
references: relOpts.entity,
|
|
297
|
+
referencedKey: foreign,
|
|
261
298
|
typeFromReference: true,
|
|
262
299
|
};
|
|
263
300
|
}
|
|
264
301
|
}
|
|
265
|
-
function fillInverseSide(at, relOpts) {
|
|
302
|
+
function fillInverseSide(at, meta, relOpts) {
|
|
266
303
|
const relEntity = relOpts.entity();
|
|
267
304
|
const relMeta = getMeta(relEntity);
|
|
268
305
|
const mappedBy = getMappedByKey(relOpts);
|
|
@@ -270,7 +307,13 @@ function fillInverseSide(at, relOpts) {
|
|
|
270
307
|
if (relOpts.references)
|
|
271
308
|
return;
|
|
272
309
|
if (relMeta.fields[mappedBy]) {
|
|
273
|
-
|
|
310
|
+
if (meta.ids.length > 1) {
|
|
311
|
+
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', one column, but the primary key of ` +
|
|
312
|
+
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
313
|
+
'instead, which joins every column of the key.');
|
|
314
|
+
}
|
|
315
|
+
// `local` is this entity's own key, as in every other pair.
|
|
316
|
+
relOpts.references = [{ local: meta.ids[0], foreign: mappedBy }];
|
|
274
317
|
return;
|
|
275
318
|
}
|
|
276
319
|
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
@@ -282,11 +325,12 @@ function fillInverseSide(at, relOpts) {
|
|
|
282
325
|
if (!owner.references?.length) {
|
|
283
326
|
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
|
|
284
327
|
}
|
|
285
|
-
// Two different flips: a junction
|
|
286
|
-
//
|
|
328
|
+
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
329
|
+
// swap - `toReversed` would also reverse each group, pairing a composite's columns crosswise. A
|
|
330
|
+
// plain foreign key is one pair per key whose ends swap.
|
|
287
331
|
relOpts.references =
|
|
288
332
|
relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
|
|
289
|
-
? owner.references.
|
|
333
|
+
? [...owner.references.slice(relMeta.ids.length), ...owner.references.slice(0, relMeta.ids.length)]
|
|
290
334
|
: owner.references.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
|
|
291
335
|
relOpts.through = owner.through;
|
|
292
336
|
}
|
|
@@ -303,7 +347,16 @@ function fillForeignKeyRelations(meta) {
|
|
|
303
347
|
const references = meta.fields[fieldKey]?.references;
|
|
304
348
|
if (!references || joined.has(fieldKey))
|
|
305
349
|
continue;
|
|
306
|
-
const
|
|
350
|
+
const target = ensureMeta(references());
|
|
351
|
+
// Nothing to derive from an entity that has not registered its own fields yet.
|
|
352
|
+
if (!target.ids.length)
|
|
353
|
+
continue;
|
|
354
|
+
if (target.ids.length > 1) {
|
|
355
|
+
throw new TypeError(`'${meta.entity.name}.${fieldKey}' cannot reference '${target.entity.name}', whose primary key is composite ` +
|
|
356
|
+
`(${target.ids.join(', ')}): a column points at one. Use ` +
|
|
357
|
+
`'@ManyToOne({ entity: () => ${target.entity.name} })', which declares one column per key.`);
|
|
358
|
+
}
|
|
359
|
+
const [foreign] = target.ids;
|
|
307
360
|
// The relation takes the column's name minus the key it points at (`itemId` -> `item`); a column
|
|
308
361
|
// named anything else has no name to take, so it stays a plain foreign key.
|
|
309
362
|
const suffix = upperFirst(foreign);
|
|
@@ -330,16 +383,18 @@ function getMappedByKey(relOpts) {
|
|
|
330
383
|
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
331
384
|
: relOpts.mappedBy;
|
|
332
385
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
386
|
+
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
387
|
+
function getIdKeys(meta) {
|
|
388
|
+
return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
|
|
336
389
|
}
|
|
337
390
|
function extendMeta(target, source) {
|
|
338
391
|
const sourceFields = { ...source.fields };
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
if (
|
|
342
|
-
|
|
392
|
+
// A subclass declaring its own primary key drops the parent's - every column of it, or a composite
|
|
393
|
+
// parent would leave its remaining keys marked and silently widen the child's key.
|
|
394
|
+
if (getIdKeys(target).length) {
|
|
395
|
+
for (const key of getIdKeys(source)) {
|
|
396
|
+
delete sourceFields[key];
|
|
397
|
+
}
|
|
343
398
|
}
|
|
344
399
|
target.fields = { ...sourceFields, ...target.fields };
|
|
345
400
|
target.relations = { ...source.relations, ...target.relations };
|
package/dist/http/handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
|
-
import { getEntities, getMeta } from '../entity/index.js';
|
|
2
|
+
import { getEntities, getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { entityPath, matchRoute } from './contract.js';
|
|
4
4
|
import { parseQueryParams } from './query.js';
|
|
5
5
|
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
@@ -142,8 +142,8 @@ export function createRequestHandler(opts) {
|
|
|
142
142
|
const founds = await querier.findMany(entity, query);
|
|
143
143
|
let ids = [];
|
|
144
144
|
let count = 0;
|
|
145
|
-
if (founds.length
|
|
146
|
-
const idKey = meta
|
|
145
|
+
if (founds.length) {
|
|
146
|
+
const idKey = soleIdOf(meta, 'the HTTP handler');
|
|
147
147
|
ids = founds.map((found) => found[idKey]);
|
|
148
148
|
count = await querier.deleteMany(entity, { $where: ids }, { hardDelete });
|
|
149
149
|
}
|
|
@@ -157,7 +157,7 @@ function ok(body) {
|
|
|
157
157
|
return { status: 200, body };
|
|
158
158
|
}
|
|
159
159
|
function buildIdQuery(meta, id, query) {
|
|
160
|
-
const idKey = meta
|
|
160
|
+
const idKey = soleIdOf(meta, 'the HTTP handler');
|
|
161
161
|
const where = query.$where;
|
|
162
162
|
if (Array.isArray(where)) {
|
|
163
163
|
query.$where = { $and: [{ [idKey]: { $in: where } }, { [idKey]: id }] };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
2
|
-
import type { DialectFeatures, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
2
|
+
import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
3
|
export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
4
4
|
readonly dialectName = "mariadb";
|
|
5
5
|
readonly insertIdSource = "returning";
|
|
@@ -10,7 +10,7 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
10
10
|
* and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
|
|
11
11
|
*/
|
|
12
12
|
protected readonly featureOverrides: Partial<DialectFeatures>;
|
|
13
|
-
protected upsertReturning<E>(
|
|
13
|
+
protected upsertReturning<E>(meta: EntityMeta<E>): string;
|
|
14
14
|
/**
|
|
15
15
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
16
16
|
* reads a scalar and `JSON_EXTRACT` the subtree that the array operators need.
|
|
@@ -18,8 +18,8 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
18
18
|
vectorIndexRequiresNotNull: true,
|
|
19
19
|
indexIfNotExists: true,
|
|
20
20
|
};
|
|
21
|
-
upsertReturning(
|
|
22
|
-
return ` ${this.returningId(
|
|
21
|
+
upsertReturning(meta) {
|
|
22
|
+
return ` ${this.returningId(meta)}`;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
3
3
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
4
|
-
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
4
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
5
5
|
import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
|
|
6
6
|
import { type IndexDdl } from './ddl/index.js';
|
|
7
7
|
/**
|
|
@@ -82,7 +82,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
82
82
|
/** ` DEFAULT <sql>`, or nothing where the column declares none. Empty rather than `DEFAULT NULL`
|
|
83
83
|
* so an absent default stays absent - `defaultValue: null` is the way to ask for one. */
|
|
84
84
|
private defaultClause;
|
|
85
|
-
getSqlType(field:
|
|
85
|
+
getSqlType(field: FieldMeta, fieldType?: unknown): string;
|
|
86
86
|
/**
|
|
87
87
|
* Generate ALTER COLUMN statements (database-specific)
|
|
88
88
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
-
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } from '../schema/canonicalType.js';
|
|
4
4
|
import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
|
|
5
5
|
import { getKeys, isAutoIncrement, qualifyName } from '../util/index.js';
|
|
@@ -269,7 +269,7 @@ export class SqlSchemaGenerator {
|
|
|
269
269
|
if (field.references) {
|
|
270
270
|
const refEntity = field.references();
|
|
271
271
|
const refMeta = getMeta(refEntity);
|
|
272
|
-
const refIdField = refMeta.fields[refMeta
|
|
272
|
+
const refIdField = refMeta.fields[field.referencedKey ?? soleIdOf(refMeta, 'a foreign key')];
|
|
273
273
|
return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false }, refIdField.type);
|
|
274
274
|
}
|
|
275
275
|
// Get canonical type and convert to SQL
|
|
@@ -410,7 +410,7 @@ export class SqlSchemaGenerator {
|
|
|
410
410
|
* would create for this field, against what it reported for the existing column.
|
|
411
411
|
*/
|
|
412
412
|
fieldToColumnSchema(fieldKey, field, meta) {
|
|
413
|
-
const isPrimaryKey = field.isId === true
|
|
413
|
+
const isPrimaryKey = field.isId === true;
|
|
414
414
|
return {
|
|
415
415
|
name: this.dialect.resolveColumnName(fieldKey, field),
|
|
416
416
|
type: this.getSqlType(field, field.type),
|
|
@@ -2,7 +2,7 @@ import { ObjectId } from 'mongodb';
|
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
|
-
import { getMeta } from '../entity/index.js';
|
|
5
|
+
import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
7
|
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
|
|
8
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
@@ -44,7 +44,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
44
44
|
* the document does not use.
|
|
45
45
|
*/
|
|
46
46
|
columnOf(meta, key) {
|
|
47
|
-
if (key === MongoDialect.ID_KEY
|
|
47
|
+
if (key === MongoDialect.ID_KEY) {
|
|
48
|
+
return MongoDialect.ID_KEY;
|
|
49
|
+
}
|
|
50
|
+
if (meta.fields[key]?.isId) {
|
|
51
|
+
// A document has one `_id`. A composite key is a different document shape - a sub-document,
|
|
52
|
+
// whose field order decides equality - rather than a translation, so it is refused here, which
|
|
53
|
+
// every read reaches, and in `getPersistables`, which every write reaches.
|
|
54
|
+
assertSoleId(meta, 'MongoDB');
|
|
48
55
|
return MongoDialect.ID_KEY;
|
|
49
56
|
}
|
|
50
57
|
return super.columnOf(meta, key);
|
|
@@ -250,7 +257,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
250
257
|
*/
|
|
251
258
|
assertKnownPathRoot(meta, key) {
|
|
252
259
|
const root = key.includes('.') ? key.slice(0, key.indexOf('.')) : key;
|
|
253
|
-
if (root === MongoDialect.ID_KEY ||
|
|
260
|
+
if (root === MongoDialect.ID_KEY || meta.fields[root]) {
|
|
254
261
|
return;
|
|
255
262
|
}
|
|
256
263
|
throw new TypeError(`path ${key} does not exist in ${entityName(meta)}`);
|
|
@@ -372,7 +379,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
372
379
|
// MongoDB returns `_id` unless it is explicitly excluded, so subtracting the primary key needs
|
|
373
380
|
// `_id: 0` - the one inclusion/exclusion mix MongoDB allows - or `$exclude: { id: true }` would
|
|
374
381
|
// have no effect at all.
|
|
375
|
-
if (this.subtractsKey(meta
|
|
382
|
+
if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), selectMap, exclude)) {
|
|
376
383
|
projection[MongoDialect.ID_KEY] = 0;
|
|
377
384
|
}
|
|
378
385
|
return projection;
|
|
@@ -679,8 +686,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
679
686
|
const res = doc;
|
|
680
687
|
const _id = MongoDialect.ID_KEY;
|
|
681
688
|
if (res[_id]) {
|
|
682
|
-
|
|
683
|
-
|
|
689
|
+
const idKey = soleIdOf(meta, 'MongoDB');
|
|
690
|
+
res[idKey] = res[_id];
|
|
691
|
+
if (idKey !== _id) {
|
|
684
692
|
delete res[_id];
|
|
685
693
|
}
|
|
686
694
|
}
|
|
@@ -797,6 +805,9 @@ export class MongoDialect extends AbstractDialect {
|
|
|
797
805
|
return [{ $set: assignments }, ...(unset.size > 0 ? [{ $unset: [...unset] }] : [])];
|
|
798
806
|
}
|
|
799
807
|
getPersistables(meta, payload, callbackKey) {
|
|
808
|
+
// Not `columnOf`, which maps the primary key to `_id`: an update may not touch `_id` at all, and
|
|
809
|
+
// an insert leaves it to the driver. What that mapping refuses, a write has to refuse too.
|
|
810
|
+
assertSoleId(meta, 'MongoDB');
|
|
800
811
|
const payloads = fillOnFields(meta, payload, callbackKey);
|
|
801
812
|
// Keys are resolved per document so heterogeneous payloads keep every provided field.
|
|
802
813
|
return payloads.map((it) => filterFieldKeys(meta, it, callbackKey).reduce((acc, key) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
|
-
import { getMeta } from '../entity/index.js';
|
|
3
|
+
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
5
|
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
@@ -202,7 +202,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
202
202
|
const meta = getMeta(entity);
|
|
203
203
|
const pipeline = this.dialect.aggregationPipeline(entity, idOnlyQuery(meta, q), opts);
|
|
204
204
|
const founds = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
205
|
-
|
|
205
|
+
// `normalizeIds` has already spread a compound `_id` back into its columns, so the settled rows
|
|
206
|
+
// are named the same way every other driver names them.
|
|
207
|
+
return (this.dialect.normalizeIds(meta, founds) || []).map((found) => idOf(meta, found));
|
|
206
208
|
}
|
|
207
209
|
async internalInsertMany(entity, payloads) {
|
|
208
210
|
return this.timed('internalInsertMany', undefined, async () => {
|
|
@@ -214,8 +216,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
214
216
|
const persistables = this.dialect.getPersistables(meta, payloads, 'onInsert');
|
|
215
217
|
const { insertedIds } = await this.execute((session) => this.collection(entity).insertMany(persistables, { session }));
|
|
216
218
|
const ids = Object.values(insertedIds);
|
|
219
|
+
const idKey = soleIdOf(meta, 'insert');
|
|
217
220
|
for (const [index, it] of payloads.entries()) {
|
|
218
|
-
it[
|
|
221
|
+
it[idKey] = ids[index];
|
|
219
222
|
}
|
|
220
223
|
await this.insertRelations(entity, payloads);
|
|
221
224
|
return ids;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
-
import { LoggerWrapper } from '../util/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, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import { LoggerWrapper, type ParentJoin } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
5
5
|
* It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
|
|
@@ -30,7 +30,7 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
30
30
|
protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
|
|
31
31
|
$entity: Type<E>;
|
|
32
32
|
}), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
|
|
33
|
-
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id:
|
|
33
|
+
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: EntityId<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
34
34
|
/**
|
|
35
35
|
* Find a single record matching the query.
|
|
36
36
|
* Supports both entity-as-argument and entity-as-field patterns.
|
|
@@ -104,16 +104,16 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
104
104
|
/** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
|
|
105
105
|
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
106
106
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
107
|
-
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
108
|
-
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
109
|
-
updateOneById<E extends object>(entity: Type<E>, id:
|
|
107
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
108
|
+
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
109
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
110
110
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
111
111
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
112
|
-
restoreOneById<E extends object>(entity: Type<E>, id:
|
|
112
|
+
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
113
113
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
114
114
|
abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
115
115
|
abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
116
|
-
deleteOneById<E extends object>(entity: Type<E>, id:
|
|
116
|
+
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
117
117
|
/**
|
|
118
118
|
* Delete records matching the query. Soft-deletes when the entity has a soft-delete field (unless
|
|
119
119
|
* `opts.hardDelete`), otherwise removes the rows. Supports both entity-as-argument and entity-as-field patterns.
|
|
@@ -132,15 +132,19 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
132
132
|
*/
|
|
133
133
|
private findDoomed;
|
|
134
134
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
135
|
-
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E
|
|
136
|
-
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
135
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
136
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(IdValue<E> | undefined)[]>;
|
|
137
137
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
138
138
|
private fillToManyThroughRelation;
|
|
139
139
|
private fillToManyOneToMany;
|
|
140
|
-
protected putChildrenInParents<E>(parents: E[], children: RawRow[],
|
|
140
|
+
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
141
141
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
142
142
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
143
|
-
|
|
143
|
+
/**
|
|
144
|
+
* `EntityId` because a settled set names composite rows as objects, which is also what the parent's
|
|
145
|
+
* own delete takes - and what {@link childrenOf} reads each child's foreign key columns out of.
|
|
146
|
+
*/
|
|
147
|
+
protected deleteRelations<E extends object>(entity: Type<E>, ids: EntityId<E>[], opts?: QueryOptions): Promise<void>;
|
|
144
148
|
/**
|
|
145
149
|
* Persists `relValue` against every id in `ids`, which an update hands the whole page of rows it
|
|
146
150
|
* settled: the value is the same for all of them, so the statements are per relation rather than per
|