uql-orm 0.24.4 → 0.24.6
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/dialect/abstractSqlDialect.d.ts +0 -1
- package/dist/dialect/abstractSqlDialect.js +7 -18
- package/dist/mongo/mongoDialect.d.ts +7 -0
- package/dist/mongo/mongoDialect.js +37 -1
- package/dist/mongo/mongodbQuerier.js +8 -3
- package/dist/querier/abstractQuerier.d.ts +0 -3
- package/dist/querier/abstractQuerier.js +10 -9
- package/dist/type/entity.d.ts +22 -8
- package/dist/type/query.d.ts +2 -0
- package/package.json +5 -5
|
@@ -79,7 +79,6 @@ export declare abstract class AbstractSqlDialect extends IndexSqlDialect impleme
|
|
|
79
79
|
protected renderWhere<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
80
80
|
compare<E>(ctx: QueryContext, entity: Type<E>, key: string, val: unknown, opts?: QueryComparisonOptions): void;
|
|
81
81
|
protected compareLogicalOperator<E>(ctx: QueryContext, entity: Type<E>, key: '$and' | '$or' | '$not' | '$nor', val: QueryWhereArray<E>, opts: QueryComparisonOptions): void;
|
|
82
|
-
/** Simple comparison operators: `getComparisonKey → op → addValue`. */
|
|
83
82
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
84
83
|
private readonly escapedColumns;
|
|
85
84
|
private static readonly NEGATE_OP_MAP;
|
|
@@ -100,23 +100,13 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
100
100
|
const meta = getMeta(entity);
|
|
101
101
|
const prefix = opts.prefix ? opts.prefix + '.' : '';
|
|
102
102
|
const escapedPrefix = this.escapeId(opts.prefix, true, true);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
selectArr = normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
111
|
-
}
|
|
112
|
-
const id = meta.id;
|
|
113
|
-
if (id && opts.prefix && !selectArr.includes(id)) {
|
|
114
|
-
selectArr = [id, ...selectArr];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
selectArr = normalizeScalarFieldSelection(meta, undefined, exclude);
|
|
119
|
-
}
|
|
103
|
+
const scalars = Array.isArray(select)
|
|
104
|
+
? select // raw SQL projections passed as QueryRaw[]
|
|
105
|
+
: normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
|
|
106
|
+
// A prefix means relations are in play: rows arrive keyed by the id and `fillToManyRelations`
|
|
107
|
+
// groups children by it, so it outlives any subtraction - `$exclude` or falsy `$select` alike.
|
|
108
|
+
const id = meta.id;
|
|
109
|
+
const selectArr = id && opts.prefix && !scalars.includes(id) ? [id, ...scalars] : scalars;
|
|
120
110
|
if (!selectArr.length) {
|
|
121
111
|
ctx.append(escapedPrefix + '*');
|
|
122
112
|
return;
|
|
@@ -387,7 +377,6 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
387
377
|
ctx.append(')');
|
|
388
378
|
}
|
|
389
379
|
}
|
|
390
|
-
/** Simple comparison operators: `getComparisonKey → op → addValue`. */
|
|
391
380
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
392
381
|
escapedColumns = new WeakMap();
|
|
393
382
|
static NEGATE_OP_MAP = new Map([
|
|
@@ -103,6 +103,13 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
103
103
|
*/
|
|
104
104
|
private pathOf;
|
|
105
105
|
aggregationPipeline<E extends Document>(entity: Type<E>, q: Query<E>, relationSummary?: RelationRequestSummary<E>, opts?: QueryOptions): MongoAggregationPipelineEntry<E>[];
|
|
106
|
+
/**
|
|
107
|
+
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
108
|
+
* the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
|
|
109
|
+
* lookups have read the join keys - projecting any earlier is what used to leave `$populate`
|
|
110
|
+
* empty, and is why the pipeline emitted no projection at all and returned every column.
|
|
111
|
+
*/
|
|
112
|
+
pipelineProjection<E extends Document>(entity: Type<E>, q: Query<E>, relationSummary?: RelationRequestSummary<E>): Record<string, 0 | 1> | undefined;
|
|
106
113
|
/**
|
|
107
114
|
* `$lookup`/`$unwind` stages for the joinable relations a query populates. Shared by the plain
|
|
108
115
|
* aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
|
|
@@ -414,8 +414,33 @@ export class MongoDialect extends AbstractDialect {
|
|
|
414
414
|
// does after an INNER JOIN. Otherwise paging first is equivalent and spares the lookups.
|
|
415
415
|
const dropsParents = relStages.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
|
|
416
416
|
pipeline.push(...(dropsParents ? [...relStages, ...pager] : [...pager, ...relStages]));
|
|
417
|
+
const projection = this.pipelineProjection(entity, q, relationSummary);
|
|
418
|
+
if (projection) {
|
|
419
|
+
pipeline.push({ $project: projection });
|
|
420
|
+
}
|
|
417
421
|
return pipeline;
|
|
418
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
|
|
425
|
+
* the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
|
|
426
|
+
* lookups have read the join keys - projecting any earlier is what used to leave `$populate`
|
|
427
|
+
* empty, and is why the pipeline emitted no projection at all and returned every column.
|
|
428
|
+
*/
|
|
429
|
+
pipelineProjection(entity, q, relationSummary) {
|
|
430
|
+
if (!q.$select && !q.$exclude) {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
const projection = this.select(entity, q.$select, q.$exclude);
|
|
434
|
+
const summary = relationSummary ?? getRelationRequestSummary(getMeta(entity), q.$populate);
|
|
435
|
+
for (const relKey of summary.joinableKeys) {
|
|
436
|
+
projection[relKey] = 1;
|
|
437
|
+
}
|
|
438
|
+
// Only ever undoes an exclusion: a relation cannot be filled onto a parent with no key.
|
|
439
|
+
if (summary.requestedKeys.length && projection[MongoDialect.ID_KEY] === 0) {
|
|
440
|
+
delete projection[MongoDialect.ID_KEY];
|
|
441
|
+
}
|
|
442
|
+
return projection;
|
|
443
|
+
}
|
|
419
444
|
/**
|
|
420
445
|
* `$lookup`/`$unwind` stages for the joinable relations a query populates. Shared by the plain
|
|
421
446
|
* aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
|
|
@@ -439,11 +464,22 @@ export class MongoDialect extends AbstractDialect {
|
|
|
439
464
|
// filters (in particular `security: true` ones) must apply even to a bare
|
|
440
465
|
// `$populate: { rel: true }`, exactly like the SQL dialects' JOIN ON-clause filters.
|
|
441
466
|
const relationFilter = this.where(relEntity, relQuery.$where ?? {}, opts);
|
|
467
|
+
// The relation's own projection runs inside the lookup, where its keys resolve against the
|
|
468
|
+
// related entity. Left out, `$populate: { rel: { $select } }` returned all of `rel`'s columns.
|
|
469
|
+
const relationProjection = this.pipelineProjection(relEntity, relQuery);
|
|
470
|
+
// MongoDB returns `_id` unless a projection subtracts it, so dropping the key from the map is
|
|
471
|
+
// how a joined document keeps its own id - as it does on the SQL dialects, and as a nested
|
|
472
|
+
// to-many fill needs.
|
|
473
|
+
delete relationProjection?.[MongoDialect.ID_KEY];
|
|
474
|
+
const lookupPipeline = [
|
|
475
|
+
...(hasKeys(relationFilter) ? [{ $match: relationFilter }] : []),
|
|
476
|
+
...(relationProjection ? [{ $project: relationProjection }] : []),
|
|
477
|
+
];
|
|
442
478
|
pipeline.push({
|
|
443
479
|
$lookup: {
|
|
444
480
|
from: this.resolveTableName(relEntity, relMeta),
|
|
445
481
|
...this.joinKeys(meta, relMeta, relOpts),
|
|
446
|
-
...(
|
|
482
|
+
...(lookupPipeline.length ? { pipeline: lookupPipeline } : {}),
|
|
447
483
|
as: relKey,
|
|
448
484
|
},
|
|
449
485
|
});
|
|
@@ -109,14 +109,19 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
109
109
|
const meta = getMeta(entity);
|
|
110
110
|
const relationSummary = getRelationRequestSummary(meta, q.$populate);
|
|
111
111
|
const scoreAlias = vectorSort.vectorSearch.$project;
|
|
112
|
-
// With relations
|
|
113
|
-
//
|
|
114
|
-
// which is why `$populate` used to come back empty under a vector sort.
|
|
112
|
+
// With relations the score is captured with `$addFields` before the lookups, and the scalar
|
|
113
|
+
// projection waits until after them: projecting any earlier drops the join keys and the joined
|
|
114
|
+
// documents, which is why `$populate` used to come back empty under a vector sort.
|
|
115
115
|
if (relationSummary.requestedKeys.length) {
|
|
116
116
|
if (scoreAlias) {
|
|
117
117
|
pipeline.push({ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } });
|
|
118
118
|
}
|
|
119
119
|
pipeline.push(...this.dialect.relationStages(entity, q, relationSummary));
|
|
120
|
+
const projection = this.dialect.pipelineProjection(entity, q, relationSummary);
|
|
121
|
+
if (projection) {
|
|
122
|
+
// `$addFields` already made the score a real field, so it projects like any other.
|
|
123
|
+
pipeline.push({ $project: scoreAlias ? { ...projection, [scoreAlias]: 1 } : projection });
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
else if (scoreAlias) {
|
|
122
127
|
const select = q.$select || q.$exclude ? this.buildScalarProjection(entity, q) : {};
|
|
@@ -6,9 +6,6 @@ import { LoggerWrapper } from '../util/index.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export declare abstract class AbstractQuerier implements Querier {
|
|
8
8
|
readonly extra?: ExtraOptions | undefined;
|
|
9
|
-
private static readonly emittedWarnings;
|
|
10
|
-
/** Clears process-wide warning deduplication. For tests only. */
|
|
11
|
-
static clearEmittedWarningsForTests(): void;
|
|
12
9
|
/**
|
|
13
10
|
* Internal promise used to queue database operations.
|
|
14
11
|
* This ensures that each operation is executed serially, preventing race conditions
|
|
@@ -7,11 +7,6 @@ import { enrichError } from './queryError.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export class AbstractQuerier {
|
|
9
9
|
extra;
|
|
10
|
-
static emittedWarnings = new Set();
|
|
11
|
-
/** Clears process-wide warning deduplication. For tests only. */
|
|
12
|
-
static clearEmittedWarningsForTests() {
|
|
13
|
-
AbstractQuerier.emittedWarnings.clear();
|
|
14
|
-
}
|
|
15
10
|
/**
|
|
16
11
|
* Internal promise used to queue database operations.
|
|
17
12
|
* This ensures that each operation is executed serially, preventing race conditions
|
|
@@ -207,8 +202,13 @@ export class AbstractQuerier {
|
|
|
207
202
|
const throughMeta = getMeta(throughEntity);
|
|
208
203
|
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts.references[1].local));
|
|
209
204
|
const ids = payload.map((it) => it[meta.id]);
|
|
205
|
+
// A relation query names the target's columns, not the join table's, so the projection and the
|
|
206
|
+
// filter belong on the populate below - resolved there against the entity that has them. Spread
|
|
207
|
+
// onto the through query they asked `ItemTag` for `Tag`'s columns: `$where`/`$sort` failed with
|
|
208
|
+
// "no such column", and `$exclude` collided with the `$select` this builds.
|
|
209
|
+
const { $select: _select, $exclude: _exclude, $where: _where, ...throughQuery } = relationQuery;
|
|
210
210
|
const throughFounds = await this.findMany(throughEntity, {
|
|
211
|
-
...
|
|
211
|
+
...throughQuery,
|
|
212
212
|
$select: {
|
|
213
213
|
[localField]: true,
|
|
214
214
|
},
|
|
@@ -219,7 +219,6 @@ export class AbstractQuerier {
|
|
|
219
219
|
},
|
|
220
220
|
},
|
|
221
221
|
$where: {
|
|
222
|
-
...relationQuery.$where,
|
|
223
222
|
[localField]: ids,
|
|
224
223
|
},
|
|
225
224
|
});
|
|
@@ -231,12 +230,14 @@ export class AbstractQuerier {
|
|
|
231
230
|
}
|
|
232
231
|
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
233
232
|
const foreignField = relOpts.references[0].foreign;
|
|
234
|
-
//
|
|
235
|
-
// `$select` form
|
|
233
|
+
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
234
|
+
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
235
|
+
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
236
236
|
const select = asSelectMap(relationQuery.$select);
|
|
237
237
|
if (select && !select[foreignField]) {
|
|
238
238
|
select[foreignField] = true;
|
|
239
239
|
}
|
|
240
|
+
delete relationQuery.$exclude?.[foreignField];
|
|
240
241
|
const ids = payload.map((it) => it[meta.id]);
|
|
241
242
|
relationQuery.$where = { ...relationQuery.$where, [foreignField]: ids };
|
|
242
243
|
const founds = await this.findMany(relEntity, relationQuery);
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -347,10 +347,19 @@ export type RelationOptionsFor<V> = Omit<RelationOptions<RelationTarget<V>>, 'en
|
|
|
347
347
|
export type MethodKey<E> = {
|
|
348
348
|
readonly [K in keyof E]-?: NonNullable<E[K]> extends (...args: never[]) => unknown ? K : never;
|
|
349
349
|
}[Key<E>];
|
|
350
|
+
/**
|
|
351
|
+
* A deferred reference to an entity class, e.g. `() => Company`.
|
|
352
|
+
*
|
|
353
|
+
* A getter rather than the class itself because decorator expressions are evaluated while the class is
|
|
354
|
+
* being defined, before its binding is initialized, so naming the class directly is a `ReferenceError`
|
|
355
|
+
* for a self-reference and for whichever side of a circular import is evaluated first - the two shapes an
|
|
356
|
+
* entity graph almost always has. Nothing about the standard decorator spec changes that; it only removed
|
|
357
|
+
* the reflected `design:type` that used to make `entity` optional.
|
|
358
|
+
*/
|
|
350
359
|
export type EntityGetter<E = any> = () => Type<E>;
|
|
351
360
|
export type CascadeType = 'persist' | 'delete';
|
|
352
361
|
export type RelationOptions<E = any> = {
|
|
353
|
-
entity
|
|
362
|
+
entity: EntityGetter<E>;
|
|
354
363
|
cardinality: RelationCardinality;
|
|
355
364
|
readonly cascade?: boolean | CascadeType;
|
|
356
365
|
mappedBy?: RelationMappedBy<E>;
|
|
@@ -362,12 +371,17 @@ export type RelationOptions<E = any> = {
|
|
|
362
371
|
references?: RelationReferences;
|
|
363
372
|
};
|
|
364
373
|
/**
|
|
365
|
-
* A relation once `getMeta` has resolved it: `
|
|
366
|
-
*
|
|
367
|
-
*
|
|
374
|
+
* A relation once `getMeta` has resolved it: `references` is filled in and `mappedBy` is the key its
|
|
375
|
+
* callback named. Consumers read this shape rather than {@link RelationOptions}, so they need no
|
|
376
|
+
* assertions - `fillRelations` establishes the invariant once, and throws where it cannot.
|
|
377
|
+
*
|
|
378
|
+
* `entity` and `through` stay {@link EntityGetter}s. Resolution could call them once and store the class,
|
|
379
|
+
* but only by keeping the authored relations in a second map: it reads them *across* entities, and a
|
|
380
|
+
* circular import can leave the entity being read mid-resolution, where telling "no such relation" apart
|
|
381
|
+
* from "declared, but an inverse side too, so neither owns the foreign key" needs the unresolved shape
|
|
382
|
+
* still there to find. A phase-split metadata map costs more than the call parentheses it saves.
|
|
368
383
|
*/
|
|
369
|
-
export type RelationMeta<E = any> = Omit<RelationOptions<E>, '
|
|
370
|
-
entity: EntityGetter<E>;
|
|
384
|
+
export type RelationMeta<E = any> = Omit<RelationOptions<E>, 'mappedBy' | 'references'> & {
|
|
371
385
|
mappedBy?: Key<E>;
|
|
372
386
|
references: RelationReferences;
|
|
373
387
|
};
|
|
@@ -379,8 +393,8 @@ type RelationOwnerJoin<E> = Required<Pick<RelationOptions<E>, 'through'>> | Requ
|
|
|
379
393
|
*/
|
|
380
394
|
type RelationJoin<E> = RelationOwnerJoin<E> | Required<Pick<RelationOptions<E>, 'mappedBy'>>;
|
|
381
395
|
type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade'>;
|
|
382
|
-
type RelationOptionsInverseSide<E> =
|
|
383
|
-
type RelationOptionsThroughOwner<E> =
|
|
396
|
+
type RelationOptionsInverseSide<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & Required<Pick<RelationOptions<E>, 'mappedBy'>>;
|
|
397
|
+
type RelationOptionsThroughOwner<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & RelationOwnerJoin<E>;
|
|
384
398
|
/**
|
|
385
399
|
* The key names of `E` as values, so `mappedBy` can be written as `(user) => user.company` instead of
|
|
386
400
|
* a string literal and survive a rename.
|
package/dist/type/query.d.ts
CHANGED
|
@@ -172,6 +172,8 @@ export type Query<E> = {
|
|
|
172
172
|
$populate?: QueryPopulate<E>;
|
|
173
173
|
/**
|
|
174
174
|
* field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.
|
|
175
|
+
* Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept
|
|
176
|
+
* regardless, since subtracting them would leave the relation unfilled.
|
|
175
177
|
*/
|
|
176
178
|
$exclude?: QueryExclude<E>;
|
|
177
179
|
/**
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.24.
|
|
6
|
+
"version": "0.24.6",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
@@ -134,9 +134,9 @@
|
|
|
134
134
|
"@tursodatabase/serverless": "^1.4.0",
|
|
135
135
|
"@types/better-sqlite3": "^9.6.0",
|
|
136
136
|
"@types/express": "^5.0.6",
|
|
137
|
-
"@types/pg": "^8.20.
|
|
137
|
+
"@types/pg": "^8.20.4",
|
|
138
138
|
"@types/ws": "^8.18.1",
|
|
139
|
-
"better-sqlite3": "^13.0.
|
|
139
|
+
"better-sqlite3": "^13.0.3",
|
|
140
140
|
"express": "^5.2.1",
|
|
141
141
|
"mariadb": "^3.5.3",
|
|
142
142
|
"mongodb": "^7.5.0",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"pg-query-stream": "^4.16.0",
|
|
146
146
|
"rxjs": "^7.8.2",
|
|
147
147
|
"sqlite-vec": "^0.1.9",
|
|
148
|
-
"ws": "^8.21.
|
|
148
|
+
"ws": "^8.21.2"
|
|
149
149
|
},
|
|
150
150
|
"author": "Roger Padilla",
|
|
151
151
|
"repository": {
|
|
@@ -198,5 +198,5 @@
|
|
|
198
198
|
"publishConfig": {
|
|
199
199
|
"access": "public"
|
|
200
200
|
},
|
|
201
|
-
"gitHead": "
|
|
201
|
+
"gitHead": "2610226954cdf152705dc47ca130f6908a91825d"
|
|
202
202
|
}
|