uql-orm 0.47.0 → 0.48.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 +2 -2
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js.map +2 -2
- package/dist/dialect/abstractDialect.d.ts +35 -1
- package/dist/dialect/abstractDialect.js +29 -0
- package/dist/dialect/abstractSqlDialect.d.ts +13 -5
- package/dist/dialect/abstractSqlDialect.js +45 -49
- package/dist/entity/decorator/members.d.ts +2 -0
- package/dist/entity/decorator/members.js +2 -0
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +11 -2
- package/dist/entity/metadata/definition.js +11 -0
- package/dist/mongo/mongoDialect.d.ts +36 -6
- package/dist/mongo/mongoDialect.js +116 -31
- package/dist/mongo/mongodbQuerier.d.ts +8 -3
- package/dist/mongo/mongodbQuerier.js +30 -11
- package/dist/querier/abstractQuerier.d.ts +33 -4
- package/dist/querier/abstractQuerier.js +92 -43
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +3 -2
- package/dist/querier/abstractSqlQuerier.js +132 -31
- package/dist/querier/relationCount.js +15 -9
- package/dist/type/entity.d.ts +1 -1
- package/dist/type/queryRaw.d.ts +1 -1
- package/dist/type/queryWhere.d.ts +11 -1
- package/dist/type/universalQuerier.d.ts +2 -2
- package/dist/util/dialect.util.d.ts +7 -0
- package/dist/util/dialect.util.js +14 -0
- package/dist/util/fieldOption.util.d.ts +1 -1
- package/dist/util/fieldOption.util.js +1 -1
- package/dist/util/relationQuery.util.d.ts +9 -6
- package/dist/util/relationQuery.util.js +11 -11
- package/dist/util/rowKey.util.d.ts +18 -4
- package/dist/util/rowKey.util.js +29 -5
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Document, type Filter,
|
|
1
|
+
import { type Document, type Filter, type Sort, type UpdateFilter } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
3
|
import type { DialectFeatures, EntityData, EntityMeta, FieldValue, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPopulate, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
|
|
4
4
|
import { type CallbackKey } from '../util/index.js';
|
|
@@ -43,11 +43,21 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
43
43
|
/** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
|
|
44
44
|
constrainsRelations<E extends Document>(entity: Type<E>, where: QueryWhere<E> | undefined): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Renders a `$where` tree without applying entity filters (used for same-scope
|
|
46
|
+
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
47
47
|
* recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
|
|
48
48
|
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
49
49
|
*/
|
|
50
50
|
private renderFilter;
|
|
51
|
+
/**
|
|
52
|
+
* Renders `$and`/`$or`/`$not`/`$nor` into `filter`. MongoDB has no root-level `$not`, so both
|
|
53
|
+
* negating operators become its `$nor`, which is exactly `NOT (a OR b)` - and by De Morgan that
|
|
54
|
+
* makes a `$nor` list its clauses directly while a `$not` wraps them in one `$and` first.
|
|
55
|
+
*
|
|
56
|
+
* Clauses that render to nothing are dropped and an empty operator emits no key at all: MongoDB
|
|
57
|
+
* rejects an empty `$and`/`$or`/`$nor` outright, where the SQL dialects contribute no term.
|
|
58
|
+
* Negations accumulate into the one `$nor`, since `NOT a AND NOT b` is `$nor: [a, b]`.
|
|
59
|
+
*/
|
|
60
|
+
private appendLogicalOperator;
|
|
51
61
|
/**
|
|
52
62
|
* Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
|
|
53
63
|
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
@@ -177,9 +187,20 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
177
187
|
private joinKeys;
|
|
178
188
|
/** `[column, key]` for the fields whose stored name differs from their property name, memoized per entity. */
|
|
179
189
|
private renamedColumns;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
private referenceKeys;
|
|
191
|
+
normalizeIds<E extends Document>(meta: EntityMeta<E>, docs: Document[] | undefined): E[] | undefined;
|
|
192
|
+
/** `doc` is the wire shape - `_id`, stored names, `ObjectId`s - and what comes back is the code's. */
|
|
193
|
+
normalizeId<E extends Document>(meta: EntityMeta<E>, doc: Document | undefined): E | undefined;
|
|
194
|
+
/**
|
|
195
|
+
* The seam into the driver: a key, or a reference to one, as MongoDB stores it. A 24-hex string
|
|
196
|
+
* becomes an `ObjectId`, so a write agrees with the filter that will later look for it; anything
|
|
197
|
+
* else - a UUID, a number, an `ObjectId` already - is stored as given, which is how those keys keep
|
|
198
|
+
* their value. Strictly 24-hex: the driver also accepts any 12-byte string, and coercing one of
|
|
199
|
+
* those turned an ordinary short key into a foreign `ObjectId`. Arrays convert element-wise.
|
|
200
|
+
*/
|
|
201
|
+
toWireId(value: unknown): unknown;
|
|
202
|
+
/** The seam out of the driver: an `ObjectId` becomes its hex string, the type the code declares. */
|
|
203
|
+
fromWireId(value: unknown): unknown;
|
|
183
204
|
getPersistable<E extends Document>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): Partial<E>;
|
|
184
205
|
/** One MongoDB update document's operators, grouped by kind and keyed by dotted path. */
|
|
185
206
|
private groupUpdateOperators;
|
|
@@ -201,6 +222,16 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
201
222
|
* `$literal` so a string starting with `$` stays data rather than becoming a field reference.
|
|
202
223
|
*/
|
|
203
224
|
private getUpdatePipeline;
|
|
225
|
+
/**
|
|
226
|
+
* Refuses a key the caller left to MongoDB that MongoDB cannot mint one of.
|
|
227
|
+
*
|
|
228
|
+
* The only key a server generates is an `ObjectId`, which {@link fromWireId} hands back as its hex
|
|
229
|
+
* string - so a key declared `String` is satisfiable and one declared `Number` is not. Answering a
|
|
230
|
+
* numeric declaration with a string is the lie this exists to refuse: the field says `number`, the
|
|
231
|
+
* value is not one, and every consumer that indexes or compares by it is quietly wrong. Prisma
|
|
232
|
+
* refuses the same shape at its schema, and this is the first moment uql can.
|
|
233
|
+
*/
|
|
234
|
+
private assertMintableKey;
|
|
204
235
|
getPersistables<E extends Document>(meta: EntityMeta<E>, payload: EntityData<E> | EntityData<E>[], callbackKey: CallbackKey): Partial<E>[];
|
|
205
236
|
/**
|
|
206
237
|
* Build MongoDB aggregation pipeline stages from a QueryAggregate.
|
|
@@ -253,7 +284,6 @@ type MongoAggregationUnwind = {
|
|
|
253
284
|
readonly path?: string;
|
|
254
285
|
readonly preserveNullAndEmptyArrays?: boolean;
|
|
255
286
|
};
|
|
256
|
-
type IdValue = string | ObjectId;
|
|
257
287
|
export type ExtractedVectorSort<E> = {
|
|
258
288
|
readonly vectorKey: string;
|
|
259
289
|
readonly vectorSearch: QueryVectorSearch;
|
|
@@ -4,7 +4,7 @@ import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '..
|
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, getMeta, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
7
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, columnFamily, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
9
9
|
export const mongoDialectFeatures = {
|
|
10
10
|
explicitJsonCast: false,
|
|
@@ -24,6 +24,12 @@ export const mongoDialectFeatures = {
|
|
|
24
24
|
supportsTimestamptz: false,
|
|
25
25
|
defaultStringAsText: false,
|
|
26
26
|
};
|
|
27
|
+
/** What `toWireId` converts: the hex spelling of an `ObjectId`, and nothing looser. */
|
|
28
|
+
const HEX_24 = /^[0-9a-f]{24}$/i;
|
|
29
|
+
/** How a declared column type reads in a message: `Number`, `uuid` - not its whole source. */
|
|
30
|
+
function declaredTypeName(type) {
|
|
31
|
+
return typeof type === 'function' ? type.name : String(type);
|
|
32
|
+
}
|
|
27
33
|
export class MongoDialect extends AbstractDialect {
|
|
28
34
|
featureDefaults = mongoDialectFeatures;
|
|
29
35
|
dialectName = 'mongodb';
|
|
@@ -83,12 +89,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
83
89
|
}
|
|
84
90
|
const meta = getMeta(entity);
|
|
85
91
|
const whereMap = buildQueryWhereAsMap(meta, where);
|
|
86
|
-
return someKey(whereMap, (key) => key
|
|
87
|
-
? whereMap[key].some((it) => this.constrainsRelations(entity, it))
|
|
92
|
+
return someKey(whereMap, (key) => MongoDialect.isGroupOp(key)
|
|
93
|
+
? (whereMap[key] ?? []).some((it) => this.constrainsRelations(entity, it))
|
|
88
94
|
: Boolean(meta.relations[key]));
|
|
89
95
|
}
|
|
90
96
|
/**
|
|
91
|
-
* Renders a `$where` tree without applying entity filters (used for same-scope
|
|
97
|
+
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
92
98
|
* recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
|
|
93
99
|
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
94
100
|
*/
|
|
@@ -99,13 +105,8 @@ export class MongoDialect extends AbstractDialect {
|
|
|
99
105
|
for (const [rawKey, rawVal] of Object.entries(whereMap)) {
|
|
100
106
|
let key = rawKey;
|
|
101
107
|
let val = rawVal;
|
|
102
|
-
if (key
|
|
103
|
-
filter
|
|
104
|
-
// A `QueryRaw` here would recurse forever: `buildQueryWhereAsMap` re-wraps it as
|
|
105
|
-
// `{ $and: [raw] }`, which lands back on this branch.
|
|
106
|
-
this.assertNoRaw(filterIt);
|
|
107
|
-
return this.renderFilter(entity, filterIt, opts, lookups);
|
|
108
|
-
});
|
|
108
|
+
if (MongoDialect.isGroupOp(key)) {
|
|
109
|
+
this.appendLogicalOperator(filter, entity, key, val, opts, lookups);
|
|
109
110
|
}
|
|
110
111
|
else if (key === '$text') {
|
|
111
112
|
// MongoDB's text index declares which fields it covers, so `$fields` cannot narrow the search
|
|
@@ -122,9 +123,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
122
123
|
else {
|
|
123
124
|
this.assertNoRaw(val);
|
|
124
125
|
this.assertKnownPathRoot(meta, key);
|
|
126
|
+
const isReference = !!meta.fields[key]?.references;
|
|
125
127
|
key = this.pathOf(meta, key);
|
|
126
|
-
if (key === MongoDialect.ID_KEY) {
|
|
127
|
-
val = this.
|
|
128
|
+
if ((key === MongoDialect.ID_KEY || isReference) && !isOperatorObject(val)) {
|
|
129
|
+
val = this.toWireId(val);
|
|
128
130
|
}
|
|
129
131
|
if (isOperatorObject(val)) {
|
|
130
132
|
val = this.transformOperators(val);
|
|
@@ -137,6 +139,35 @@ export class MongoDialect extends AbstractDialect {
|
|
|
137
139
|
}
|
|
138
140
|
return filter;
|
|
139
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Renders `$and`/`$or`/`$not`/`$nor` into `filter`. MongoDB has no root-level `$not`, so both
|
|
144
|
+
* negating operators become its `$nor`, which is exactly `NOT (a OR b)` - and by De Morgan that
|
|
145
|
+
* makes a `$nor` list its clauses directly while a `$not` wraps them in one `$and` first.
|
|
146
|
+
*
|
|
147
|
+
* Clauses that render to nothing are dropped and an empty operator emits no key at all: MongoDB
|
|
148
|
+
* rejects an empty `$and`/`$or`/`$nor` outright, where the SQL dialects contribute no term.
|
|
149
|
+
* Negations accumulate into the one `$nor`, since `NOT a AND NOT b` is `$nor: [a, b]`.
|
|
150
|
+
*/
|
|
151
|
+
appendLogicalOperator(filter, entity, key, val, opts, lookups) {
|
|
152
|
+
const { join, negate } = MongoDialect.GROUP_OPS[key];
|
|
153
|
+
const parts = MongoDialect.groupClauses(key, val)
|
|
154
|
+
.map((filterIt) => {
|
|
155
|
+
// A `QueryRaw` here would recurse forever: `buildQueryWhereAsMap` re-wraps it as
|
|
156
|
+
// `{ $and: [raw] }`, which lands back on this branch.
|
|
157
|
+
this.assertNoRaw(filterIt);
|
|
158
|
+
return this.renderFilter(entity, filterIt, opts, lookups);
|
|
159
|
+
})
|
|
160
|
+
.filter((part) => Object.keys(part).length > 0);
|
|
161
|
+
if (!parts.length) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!negate) {
|
|
165
|
+
filter[key] = parts;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const negated = join === '$and' && parts.length > 1 ? [{ $and: parts }] : parts;
|
|
169
|
+
filter['$nor'] = [...(filter['$nor'] ?? []), ...negated];
|
|
170
|
+
}
|
|
140
171
|
/**
|
|
141
172
|
* Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
|
|
142
173
|
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
@@ -689,20 +720,31 @@ export class MongoDialect extends AbstractDialect {
|
|
|
689
720
|
}
|
|
690
721
|
return renamed;
|
|
691
722
|
}
|
|
723
|
+
referenceKeys(meta) {
|
|
724
|
+
let keys = this.#referenceKeys.get(meta);
|
|
725
|
+
if (!keys) {
|
|
726
|
+
keys = getKeys(meta.fields).filter((key) => meta.fields[key]?.references);
|
|
727
|
+
this.#referenceKeys.set(meta, keys);
|
|
728
|
+
}
|
|
729
|
+
return keys;
|
|
730
|
+
}
|
|
692
731
|
// Keyed by the meta object itself; entity metadata is immutable once defined.
|
|
693
732
|
#renamedColumns = new WeakMap();
|
|
733
|
+
#referenceKeys = new WeakMap();
|
|
694
734
|
normalizeIds(meta, docs) {
|
|
695
735
|
return docs?.map((doc) => this.normalizeId(meta, doc));
|
|
696
736
|
}
|
|
737
|
+
/** `doc` is the wire shape - `_id`, stored names, `ObjectId`s - and what comes back is the code's. */
|
|
697
738
|
normalizeId(meta, doc) {
|
|
698
739
|
if (!doc) {
|
|
699
740
|
return doc;
|
|
700
741
|
}
|
|
701
742
|
const res = doc;
|
|
702
743
|
const _id = MongoDialect.ID_KEY;
|
|
703
|
-
|
|
744
|
+
// `!== undefined`, not truthiness: `0` is a key MongoDB accepts and a truthy test dropped it.
|
|
745
|
+
if (res[_id] !== undefined) {
|
|
704
746
|
const idKey = soleIdOf(meta, 'MongoDB');
|
|
705
|
-
res[idKey] = res[_id];
|
|
747
|
+
res[idKey] = this.fromWireId(res[_id]);
|
|
706
748
|
if (idKey !== _id) {
|
|
707
749
|
delete res[_id];
|
|
708
750
|
}
|
|
@@ -715,6 +757,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
715
757
|
delete res[column];
|
|
716
758
|
}
|
|
717
759
|
}
|
|
760
|
+
// After the rename, so a renamed reference is converted under the name the code reads.
|
|
761
|
+
for (const key of this.referenceKeys(meta)) {
|
|
762
|
+
if (res[key] !== undefined) {
|
|
763
|
+
res[key] = this.fromWireId(res[key]);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
718
766
|
const relKeys = getKeys(meta.relations).filter((key) => res[key]);
|
|
719
767
|
for (const relKey of relKeys) {
|
|
720
768
|
const relOpts = meta.relations[relKey];
|
|
@@ -727,16 +775,22 @@ export class MongoDialect extends AbstractDialect {
|
|
|
727
775
|
}
|
|
728
776
|
return res;
|
|
729
777
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
778
|
+
/**
|
|
779
|
+
* The seam into the driver: a key, or a reference to one, as MongoDB stores it. A 24-hex string
|
|
780
|
+
* becomes an `ObjectId`, so a write agrees with the filter that will later look for it; anything
|
|
781
|
+
* else - a UUID, a number, an `ObjectId` already - is stored as given, which is how those keys keep
|
|
782
|
+
* their value. Strictly 24-hex: the driver also accepts any 12-byte string, and coercing one of
|
|
783
|
+
* those turned an ordinary short key into a foreign `ObjectId`. Arrays convert element-wise.
|
|
784
|
+
*/
|
|
785
|
+
toWireId(value) {
|
|
786
|
+
if (Array.isArray(value)) {
|
|
787
|
+
return value.map((it) => this.toWireId(it));
|
|
739
788
|
}
|
|
789
|
+
return typeof value === 'string' && HEX_24.test(value) ? new ObjectId(value) : value;
|
|
790
|
+
}
|
|
791
|
+
/** The seam out of the driver: an `ObjectId` becomes its hex string, the type the code declares. */
|
|
792
|
+
fromWireId(value) {
|
|
793
|
+
return value instanceof ObjectId ? value.toHexString() : value;
|
|
740
794
|
}
|
|
741
795
|
getPersistable(meta, payload, callbackKey) {
|
|
742
796
|
return this.getPersistables(meta, payload, callbackKey)[0];
|
|
@@ -819,17 +873,48 @@ export class MongoDialect extends AbstractDialect {
|
|
|
819
873
|
}
|
|
820
874
|
return [{ $set: assignments }, ...(unset.size > 0 ? [{ $unset: [...unset] }] : [])];
|
|
821
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Refuses a key the caller left to MongoDB that MongoDB cannot mint one of.
|
|
878
|
+
*
|
|
879
|
+
* The only key a server generates is an `ObjectId`, which {@link fromWireId} hands back as its hex
|
|
880
|
+
* string - so a key declared `String` is satisfiable and one declared `Number` is not. Answering a
|
|
881
|
+
* numeric declaration with a string is the lie this exists to refuse: the field says `number`, the
|
|
882
|
+
* value is not one, and every consumer that indexes or compares by it is quietly wrong. Prisma
|
|
883
|
+
* refuses the same shape at its schema, and this is the first moment uql can.
|
|
884
|
+
*/
|
|
885
|
+
assertMintableKey(meta, field) {
|
|
886
|
+
if (columnFamily(field.type) === 'string') {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
throw new TypeError(`'${entityName(meta)}.${meta.ids[0]}' is declared '${declaredTypeName(field.type)}' and left to the ` +
|
|
890
|
+
'database, which MongoDB cannot do: the only key it generates is an ObjectId, read back as a string. ' +
|
|
891
|
+
"Declare the key as a string, or give it an 'onInsert' generator.");
|
|
892
|
+
}
|
|
822
893
|
getPersistables(meta, payload, callbackKey) {
|
|
823
|
-
//
|
|
824
|
-
// an insert leaves it to the driver. What that mapping refuses, a write has to refuse too.
|
|
894
|
+
// What `columnOf` refuses, a write has to refuse too.
|
|
825
895
|
assertSoleId(meta, 'MongoDB');
|
|
896
|
+
const [idKey] = meta.ids;
|
|
826
897
|
const payloads = fillOnFields(meta, payload, callbackKey);
|
|
827
898
|
// Keys are resolved per document so heterogeneous payloads keep every provided field.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
899
|
+
const inserting = callbackKey === 'onInsert';
|
|
900
|
+
return payloads.map((it) => {
|
|
901
|
+
// The key is `_id` on an insert and immutable on an update, so it is left out of one. It used
|
|
902
|
+
// to land under its own name beside the `_id` the driver minted: a supplied id, or one an
|
|
903
|
+
// `onInsert` generated, was written and unreachable by the value the caller held.
|
|
904
|
+
const named = inserting && it[idKey] != null;
|
|
905
|
+
if (inserting && !named) {
|
|
906
|
+
// Nothing named the key, so the database is being asked to mint one.
|
|
907
|
+
this.assertMintableKey(meta, meta.fields[idKey]);
|
|
908
|
+
}
|
|
909
|
+
const doc = named ? { [MongoDialect.ID_KEY]: this.toWireId(it[idKey]) } : {};
|
|
910
|
+
for (const key of filterFieldKeys(meta, it, callbackKey)) {
|
|
911
|
+
if (key === idKey)
|
|
912
|
+
continue;
|
|
913
|
+
const field = meta.fields[key];
|
|
914
|
+
doc[this.resolveColumnName(key, field)] = field.references ? this.toWireId(it[key]) : it[key];
|
|
915
|
+
}
|
|
916
|
+
return doc;
|
|
917
|
+
});
|
|
833
918
|
}
|
|
834
919
|
/**
|
|
835
920
|
* Build MongoDB aggregation pipeline stages from a QueryAggregate.
|
|
@@ -58,13 +58,18 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
58
58
|
private settleIds;
|
|
59
59
|
internalInsertMany<E extends Document>(entity: Type<E>, payloads: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
60
60
|
internalUpdateMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
61
|
+
/**
|
|
62
|
+
* `_id` is immutable, so a key the payload names can only be written on the insert branch of an
|
|
63
|
+
* upsert; in `$set` it would refuse every matched document. Everything else updates either way.
|
|
64
|
+
*/
|
|
65
|
+
private upsertUpdate;
|
|
61
66
|
private buildConflictFilter;
|
|
62
|
-
|
|
63
|
-
firstId:
|
|
67
|
+
protected internalUpsertOne<E extends Document>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<{
|
|
68
|
+
firstId: PrimaryKey | undefined;
|
|
64
69
|
changes: number;
|
|
65
70
|
created: boolean;
|
|
66
71
|
}>;
|
|
67
|
-
|
|
72
|
+
protected internalUpsertMany<E extends Document>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<{
|
|
68
73
|
changes: number;
|
|
69
74
|
ids?: undefined;
|
|
70
75
|
firstId?: undefined;
|
|
@@ -81,7 +81,11 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
81
81
|
*/
|
|
82
82
|
async internalFindManyPerParent(entity, q, { joins, parents }) {
|
|
83
83
|
const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
|
|
84
|
-
// A vector sort
|
|
84
|
+
// A vector sort is not a degraded fallback here, it is the only expressible form: `$vectorSearch`
|
|
85
|
+
// has to be the first stage of a pipeline, so it cannot be one of N `$unionWith` branches. Read a
|
|
86
|
+
// parent at a time it stays correct, because `buildVectorSearchStage` passes the query's `$where`
|
|
87
|
+
// - which carries this parent's key - into the search as its filter, so each parent gets its own
|
|
88
|
+
// nearest rather than a share of the collection's.
|
|
85
89
|
if (this.dialect.extractVectorSort(q.$sort)) {
|
|
86
90
|
return this.readEachInTurn(entity, queries);
|
|
87
91
|
}
|
|
@@ -264,7 +268,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
264
268
|
const meta = getMeta(entity);
|
|
265
269
|
const persistables = this.dialect.getPersistables(meta, payloads, 'onInsert');
|
|
266
270
|
const { insertedIds } = await this.execute((session) => this.collection(entity).insertMany(persistables, { session }));
|
|
267
|
-
const ids = Object.values(insertedIds);
|
|
271
|
+
const ids = Object.values(insertedIds).map((id) => this.dialect.fromWireId(id));
|
|
268
272
|
const idKey = soleIdOf(meta, 'insert');
|
|
269
273
|
for (const [index, it] of payloads.entries()) {
|
|
270
274
|
it[idKey] = ids[index];
|
|
@@ -282,7 +286,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
282
286
|
// relation condition has nowhere to go, and MongoDB takes no page on a write, so a paged one
|
|
283
287
|
// has to name the rows it picked rather than touching every match.
|
|
284
288
|
const where = this.dialect.constrainsRelations(entity, qm.$where) || isPagedQuery(qm)
|
|
285
|
-
? { _id: { $in: await this.settleIds(entity, qm, opts) } }
|
|
289
|
+
? { _id: { $in: this.dialect.toWireId(await this.settleIds(entity, qm, opts)) } }
|
|
286
290
|
: this.dialect.where(entity, qm.$where, opts);
|
|
287
291
|
// Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
|
|
288
292
|
const update = this.dialect.getUpdateFilter(persistable);
|
|
@@ -293,6 +297,21 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
293
297
|
return matchedCount;
|
|
294
298
|
});
|
|
295
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* `_id` is immutable, so a key the payload names can only be written on the insert branch of an
|
|
302
|
+
* upsert; in `$set` it would refuse every matched document. Everything else updates either way.
|
|
303
|
+
*/
|
|
304
|
+
upsertUpdate(persistable) {
|
|
305
|
+
const { _id, ...rest } = persistable;
|
|
306
|
+
const update = {};
|
|
307
|
+
if (hasKeys(rest)) {
|
|
308
|
+
update['$set'] = rest;
|
|
309
|
+
}
|
|
310
|
+
if (_id !== undefined) {
|
|
311
|
+
update['$setOnInsert'] = { _id };
|
|
312
|
+
}
|
|
313
|
+
return update;
|
|
314
|
+
}
|
|
296
315
|
buildConflictFilter(entity, conflictPaths, item) {
|
|
297
316
|
const where = getKeys(conflictPaths).reduce((acc, key) => {
|
|
298
317
|
acc[key] = item[key];
|
|
@@ -300,26 +319,26 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
300
319
|
}, {});
|
|
301
320
|
return this.dialect.where(entity, where);
|
|
302
321
|
}
|
|
303
|
-
async
|
|
322
|
+
async internalUpsertOne(entity, conflictPaths, payload) {
|
|
304
323
|
return this.timed('upsertOne', undefined, async () => {
|
|
305
324
|
payload = clone(payload);
|
|
306
325
|
const meta = getMeta(entity);
|
|
307
326
|
const persistable = this.dialect.getPersistable(meta, payload, 'onInsert');
|
|
308
327
|
const filter = this.buildConflictFilter(entity, conflictPaths, payload);
|
|
309
|
-
const update =
|
|
328
|
+
const update = this.upsertUpdate(persistable);
|
|
310
329
|
const res = await this.execute((session) => this.collection(entity).findOneAndUpdate(filter, update, {
|
|
311
330
|
upsert: true,
|
|
312
331
|
returnDocument: 'after',
|
|
313
332
|
includeResultMetadata: true,
|
|
314
333
|
session,
|
|
315
334
|
}));
|
|
316
|
-
const firstId = res?.value?._id;
|
|
335
|
+
const firstId = this.dialect.fromWireId(res?.value?._id);
|
|
317
336
|
// `updatedExisting` is false when a new document was inserted (upserted).
|
|
318
337
|
const created = res?.lastErrorObject?.['updatedExisting'] === false;
|
|
319
338
|
return { firstId, changes: firstId ? 1 : 0, created };
|
|
320
339
|
});
|
|
321
340
|
}
|
|
322
|
-
async
|
|
341
|
+
async internalUpsertMany(entity, conflictPaths, payload) {
|
|
323
342
|
return this.timed('upsertMany', undefined, async () => {
|
|
324
343
|
if (!payload?.length) {
|
|
325
344
|
return { changes: 0 };
|
|
@@ -329,7 +348,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
329
348
|
const operations = payload.map((item) => {
|
|
330
349
|
const persistable = this.dialect.getPersistable(meta, item, 'onInsert');
|
|
331
350
|
const filter = this.buildConflictFilter(entity, conflictPaths, item);
|
|
332
|
-
const update =
|
|
351
|
+
const update = this.upsertUpdate(persistable);
|
|
333
352
|
return {
|
|
334
353
|
updateOne: {
|
|
335
354
|
filter,
|
|
@@ -344,7 +363,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
344
363
|
// updated document's `_id` isn't in the response, so it's simply not represented here - same
|
|
345
364
|
// "exact where knowable, absent otherwise" convention `RETURNING`-based SQL dialects use for
|
|
346
365
|
// rows that hit `DO NOTHING`.
|
|
347
|
-
const ids = Object.values(res.upsertedIds);
|
|
366
|
+
const ids = Object.values(res.upsertedIds).map((id) => this.dialect.fromWireId(id));
|
|
348
367
|
return { changes, ids, firstId: ids[0] };
|
|
349
368
|
});
|
|
350
369
|
}
|
|
@@ -366,13 +385,13 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
366
385
|
// Stamp the mapped column: reads filter on it, so a `@Field({ name })` mismatch here would
|
|
367
386
|
// report a successful delete and leave the row visible.
|
|
368
387
|
const softDeleteColumn = this.dialect.resolveColumnName(meta.softDelete, field);
|
|
369
|
-
const updateResult = await this.execute((session) => this.collection(entity).updateMany({ _id: { $in: ids } }, { $set: { [softDeleteColumn]: getSoftDeleteValue(field) } }, {
|
|
388
|
+
const updateResult = await this.execute((session) => this.collection(entity).updateMany({ _id: { $in: this.dialect.toWireId(ids) } }, { $set: { [softDeleteColumn]: getSoftDeleteValue(field) } }, {
|
|
370
389
|
session,
|
|
371
390
|
}));
|
|
372
391
|
changes = updateResult.matchedCount;
|
|
373
392
|
}
|
|
374
393
|
else {
|
|
375
|
-
const deleteResult = await this.execute((session) => this.collection(entity).deleteMany({ _id: { $in: ids } }, { session }));
|
|
394
|
+
const deleteResult = await this.execute((session) => this.collection(entity).deleteMany({ _id: { $in: this.dialect.toWireId(ids) } }, { session }));
|
|
376
395
|
changes = deleteResult.deletedCount;
|
|
377
396
|
}
|
|
378
397
|
await this.deleteRelations(entity, ids, opts);
|
|
@@ -119,8 +119,16 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
119
119
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
120
120
|
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
121
121
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
/**
|
|
123
|
+
* `beforeUpsert`/`afterUpsert` rather than the insert's or the update's pair: the database decides
|
|
124
|
+
* which branch each row takes as the statement runs, so neither of those could be fired honestly -
|
|
125
|
+
* but the upsert itself is a fact known before and after, and a row written with no hook at all
|
|
126
|
+
* was how an `@Id({ onInsert })` or an audit trail silently skipped this path.
|
|
127
|
+
*/
|
|
128
|
+
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
129
|
+
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
130
|
+
protected abstract internalUpsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
131
|
+
protected abstract internalUpsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
124
132
|
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
125
133
|
/**
|
|
126
134
|
* Delete records matching the query. Soft-deletes when the entity has a soft-delete field (unless
|
|
@@ -140,8 +148,24 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
140
148
|
*/
|
|
141
149
|
private findDoomed;
|
|
142
150
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
143
|
-
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<
|
|
144
|
-
|
|
151
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<EntityId<E> | undefined>;
|
|
152
|
+
/**
|
|
153
|
+
* Insert or update, as the name has always promised - and now as one statement per kind rather
|
|
154
|
+
* than a guess.
|
|
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.
|
|
167
|
+
*/
|
|
168
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(EntityId<E> | undefined)[]>;
|
|
145
169
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
146
170
|
private fillToManyThroughRelation;
|
|
147
171
|
private fillToManyOneToMany;
|
|
@@ -196,6 +220,11 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
196
220
|
* Emit a lifecycle hook event for the given entity.
|
|
197
221
|
* Fires global listeners first, then entity-level hooks.
|
|
198
222
|
*/
|
|
223
|
+
/**
|
|
224
|
+
* Runs `write` between the event's `before`/`after` pair. Every hooked write is this shape, and
|
|
225
|
+
* each one spelled out was a place the pair could drift - `upsert` had none at all for a release.
|
|
226
|
+
*/
|
|
227
|
+
private hooked;
|
|
199
228
|
private emitHook;
|
|
200
229
|
/**
|
|
201
230
|
* Runs `task` after everything already queued on this querier, one at a time.
|