uql-orm 0.41.0 → 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 +29 -3
- 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 +97 -38
- 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/builder/migrationBuilder.js +2 -2
- package/dist/migrate/builder/tableBuilder.js +2 -2
- package/dist/migrate/schemaGenerator.d.ts +2 -2
- package/dist/migrate/schemaGenerator.js +12 -4
- 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/schemaAST.js +1 -0
- package/dist/schema/schemaASTBuilder.js +37 -22
- package/dist/schema/types.d.ts +24 -0
- package/dist/type/entity.d.ts +85 -15
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/ddlExpression.util.d.ts +15 -0
- package/dist/util/ddlExpression.util.js +27 -0
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +2 -1
- 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/dist/util/sql.util.d.ts +2 -0
- package/dist/util/sql.util.js +4 -0
- package/package.json +2 -2
- package/dist/util/indexColumn.util.d.ts +0 -8
- package/dist/util/indexColumn.util.js +0 -30
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
-
import { getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn,
|
|
2
|
+
import { getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
|
|
3
3
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
4
4
|
// Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
|
|
5
5
|
// evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
|
|
@@ -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
|
|
@@ -66,7 +60,7 @@ export function defineIndex(entity, index) {
|
|
|
66
60
|
meta.indexes.push({
|
|
67
61
|
...index,
|
|
68
62
|
unique: index.unique ?? false,
|
|
69
|
-
where:
|
|
63
|
+
where: ddlText(index.where, 'a partial-index predicate'),
|
|
70
64
|
columns: index.columns.map(normalizeIndexColumn),
|
|
71
65
|
});
|
|
72
66
|
return meta;
|
|
@@ -124,6 +118,10 @@ export function defineEntity(entity, opts = {}) {
|
|
|
124
118
|
// after class decorators return; draining empties the bag, so whichever runs second is a no-op.
|
|
125
119
|
applyMembers(entity, ownRegistrations(entity));
|
|
126
120
|
applyMembers(entity, opts);
|
|
121
|
+
// Unnamed checks are named by the generator, as unnamed indexes are.
|
|
122
|
+
for (const check of opts.checks ?? []) {
|
|
123
|
+
(meta.checks ??= []).push({ name: check.name, expression: ddlText(check.expression, 'a check constraint') });
|
|
124
|
+
}
|
|
127
125
|
for (const index of opts.indexes ?? []) {
|
|
128
126
|
defineIndex(entity, index);
|
|
129
127
|
}
|
|
@@ -163,16 +161,51 @@ export function defineEntity(entity, opts = {}) {
|
|
|
163
161
|
meta.filters = {};
|
|
164
162
|
meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
|
|
165
163
|
}
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
168
|
-
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 } }))`);
|
|
169
167
|
}
|
|
170
|
-
meta.
|
|
168
|
+
meta.ids = ids;
|
|
171
169
|
return meta;
|
|
172
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
|
+
}
|
|
173
206
|
export function getEntities() {
|
|
174
207
|
return [...metas.entries()].reduce((acc, [key, val]) => {
|
|
175
|
-
if (val.
|
|
208
|
+
if (val.ids.length) {
|
|
176
209
|
acc.push(key);
|
|
177
210
|
}
|
|
178
211
|
return acc;
|
|
@@ -183,7 +216,7 @@ function ensureMeta(entity) {
|
|
|
183
216
|
if (meta) {
|
|
184
217
|
return meta;
|
|
185
218
|
}
|
|
186
|
-
meta = { entity,
|
|
219
|
+
meta = { entity, ids: [], fields: {}, relations: {} };
|
|
187
220
|
metas.set(entity, meta);
|
|
188
221
|
return meta;
|
|
189
222
|
}
|
|
@@ -206,7 +239,7 @@ function fillRelations(meta) {
|
|
|
206
239
|
continue;
|
|
207
240
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
208
241
|
if (relOpts.mappedBy) {
|
|
209
|
-
fillInverseSide(at, relOpts);
|
|
242
|
+
fillInverseSide(at, meta, relOpts);
|
|
210
243
|
}
|
|
211
244
|
else if (!relOpts.references) {
|
|
212
245
|
fillOwningSide(at, meta, relKey, relOpts);
|
|
@@ -230,13 +263,13 @@ function fillRelations(meta) {
|
|
|
230
263
|
}
|
|
231
264
|
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
232
265
|
const relMeta = ensureMeta(relOpts.entity());
|
|
233
|
-
const relIdKey = relMeta.id;
|
|
234
266
|
if (relOpts.through) {
|
|
235
267
|
// Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
|
|
236
|
-
// `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.
|
|
237
270
|
relOpts.references = [
|
|
238
|
-
{ local: junctionColumn(meta,
|
|
239
|
-
{ 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 })),
|
|
240
273
|
];
|
|
241
274
|
return;
|
|
242
275
|
}
|
|
@@ -244,21 +277,29 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
244
277
|
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
245
278
|
"'through' (a junction entity), or 'references' (the columns).");
|
|
246
279
|
}
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}));
|
|
249
288
|
// `typeFromReference` so schema generation resolves the referenced primary key's exact type
|
|
250
289
|
// (columnType, length, chained keys) rather than trusting the fallback, as it does for an
|
|
251
290
|
// explicit `@Field({ references })`.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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,
|
|
256
296
|
references: relOpts.entity,
|
|
297
|
+
referencedKey: foreign,
|
|
257
298
|
typeFromReference: true,
|
|
258
299
|
};
|
|
259
300
|
}
|
|
260
301
|
}
|
|
261
|
-
function fillInverseSide(at, relOpts) {
|
|
302
|
+
function fillInverseSide(at, meta, relOpts) {
|
|
262
303
|
const relEntity = relOpts.entity();
|
|
263
304
|
const relMeta = getMeta(relEntity);
|
|
264
305
|
const mappedBy = getMappedByKey(relOpts);
|
|
@@ -266,7 +307,13 @@ function fillInverseSide(at, relOpts) {
|
|
|
266
307
|
if (relOpts.references)
|
|
267
308
|
return;
|
|
268
309
|
if (relMeta.fields[mappedBy]) {
|
|
269
|
-
|
|
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 }];
|
|
270
317
|
return;
|
|
271
318
|
}
|
|
272
319
|
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
@@ -278,11 +325,12 @@ function fillInverseSide(at, relOpts) {
|
|
|
278
325
|
if (!owner.references?.length) {
|
|
279
326
|
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
|
|
280
327
|
}
|
|
281
|
-
// Two different flips: a junction
|
|
282
|
-
//
|
|
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.
|
|
283
331
|
relOpts.references =
|
|
284
332
|
relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
|
|
285
|
-
? owner.references.
|
|
333
|
+
? [...owner.references.slice(relMeta.ids.length), ...owner.references.slice(0, relMeta.ids.length)]
|
|
286
334
|
: owner.references.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
|
|
287
335
|
relOpts.through = owner.through;
|
|
288
336
|
}
|
|
@@ -299,7 +347,16 @@ function fillForeignKeyRelations(meta) {
|
|
|
299
347
|
const references = meta.fields[fieldKey]?.references;
|
|
300
348
|
if (!references || joined.has(fieldKey))
|
|
301
349
|
continue;
|
|
302
|
-
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;
|
|
303
360
|
// The relation takes the column's name minus the key it points at (`itemId` -> `item`); a column
|
|
304
361
|
// named anything else has no name to take, so it stays a plain foreign key.
|
|
305
362
|
const suffix = upperFirst(foreign);
|
|
@@ -326,16 +383,18 @@ function getMappedByKey(relOpts) {
|
|
|
326
383
|
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
327
384
|
: relOpts.mappedBy;
|
|
328
385
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
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);
|
|
332
389
|
}
|
|
333
390
|
function extendMeta(target, source) {
|
|
334
391
|
const sourceFields = { ...source.fields };
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
if (
|
|
338
|
-
|
|
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
|
+
}
|
|
339
398
|
}
|
|
340
399
|
target.fields = { ...sourceFields, ...target.fields };
|
|
341
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`
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OperationRecorder: Record operations only (for code generation)
|
|
6
6
|
* - MigrationBuilder: Execute DDL operations (for integration tests/runtime)
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { ddlText, normalizeIndexColumn } from '../../util/index.js';
|
|
9
9
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
10
10
|
import { createSchemaGenerator } from '../schemaGenerator.js';
|
|
11
11
|
import { splitSqlStatements } from './splitSqlStatements.js';
|
|
@@ -29,7 +29,7 @@ function createIndexOperation(tableName, columns, options = {}) {
|
|
|
29
29
|
...index,
|
|
30
30
|
name: name ??
|
|
31
31
|
derivedIndexName(tableName, entries.map((entry) => entry.column)),
|
|
32
|
-
where:
|
|
32
|
+
where: ddlText(index.where, 'a partial-index predicate'),
|
|
33
33
|
entries,
|
|
34
34
|
unique: unique ?? false,
|
|
35
35
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fluent API for defining tables in migrations.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { ddlText, normalizeIndexColumn } from '../../util/index.js';
|
|
7
7
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
8
8
|
import { ColumnBuilder } from './columnBuilder.js';
|
|
9
9
|
import { expr } from './expressions.js';
|
|
@@ -171,7 +171,7 @@ export class TableBuilder {
|
|
|
171
171
|
this._indexes.push({
|
|
172
172
|
...rest,
|
|
173
173
|
name: name ?? `${prefix}_${this._name}_${entries.map((entry) => entry.column).join('_')}`,
|
|
174
|
-
where:
|
|
174
|
+
where: ddlText(rest.where, 'a partial-index predicate'),
|
|
175
175
|
entries,
|
|
176
176
|
unique,
|
|
177
177
|
});
|
|
@@ -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,9 +1,9 @@
|
|
|
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';
|
|
6
|
-
import { derivedForeignKeyName } from '../util/sql.util.js';
|
|
6
|
+
import { derivedCheckName, derivedForeignKeyName } from '../util/sql.util.js';
|
|
7
7
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
8
8
|
import { indexDdlFor } from './ddl/index.js';
|
|
9
9
|
import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
|
|
@@ -247,6 +247,10 @@ export class SqlSchemaGenerator {
|
|
|
247
247
|
if (column.isUnique && !column.isPrimaryKey) {
|
|
248
248
|
def += ' UNIQUE';
|
|
249
249
|
}
|
|
250
|
+
if (column.enum?.length) {
|
|
251
|
+
const values = column.enum.map((value) => this.dialect.escape(value)).join(', ');
|
|
252
|
+
def += ` CHECK (${this.escapeId(column.name)} IN (${values}))`;
|
|
253
|
+
}
|
|
250
254
|
def += this.defaultClause(column);
|
|
251
255
|
if (column.comment) {
|
|
252
256
|
def += this.generateColumnComment(column.name, column.comment);
|
|
@@ -265,7 +269,7 @@ export class SqlSchemaGenerator {
|
|
|
265
269
|
if (field.references) {
|
|
266
270
|
const refEntity = field.references();
|
|
267
271
|
const refMeta = getMeta(refEntity);
|
|
268
|
-
const refIdField = refMeta.fields[refMeta
|
|
272
|
+
const refIdField = refMeta.fields[field.referencedKey ?? soleIdOf(refMeta, 'a foreign key')];
|
|
269
273
|
return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false }, refIdField.type);
|
|
270
274
|
}
|
|
271
275
|
// Get canonical type and convert to SQL
|
|
@@ -406,7 +410,7 @@ export class SqlSchemaGenerator {
|
|
|
406
410
|
* would create for this field, against what it reported for the existing column.
|
|
407
411
|
*/
|
|
408
412
|
fieldToColumnSchema(fieldKey, field, meta) {
|
|
409
|
-
const isPrimaryKey = field.isId === true
|
|
413
|
+
const isPrimaryKey = field.isId === true;
|
|
410
414
|
return {
|
|
411
415
|
name: this.dialect.resolveColumnName(fieldKey, field),
|
|
412
416
|
type: this.getSqlType(field, field.type),
|
|
@@ -493,6 +497,10 @@ export class SqlSchemaGenerator {
|
|
|
493
497
|
const pkCols = table.primaryKey.map((c) => this.escapeId(c.name)).join(', ');
|
|
494
498
|
constraints.push(`PRIMARY KEY (${pkCols})`);
|
|
495
499
|
}
|
|
500
|
+
(table.checks ?? []).forEach((check, i) => {
|
|
501
|
+
const name = check.name ?? derivedCheckName(table.name, i + 1);
|
|
502
|
+
constraints.push(`CONSTRAINT ${this.escapeId(name)} CHECK (${check.expression})`);
|
|
503
|
+
});
|
|
496
504
|
for (const rel of table.outgoingRelations) {
|
|
497
505
|
if (rel.from.columns.length > 0) {
|
|
498
506
|
const fromCols = rel.from.columns.map((c) => this.escapeId(c.name)).join(', ');
|
|
@@ -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
|