@mikro-orm/core 7.2.0-dev.15 → 7.2.0-dev.17
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/drivers/DatabaseDriver.d.ts +14 -5
- package/drivers/DatabaseDriver.js +142 -50
- package/entity/defineEntity.d.ts +4 -0
- package/entity/defineEntity.js +4 -0
- package/errors.d.ts +4 -0
- package/errors.js +15 -0
- package/metadata/MetadataDiscovery.d.ts +2 -0
- package/metadata/MetadataDiscovery.js +57 -6
- package/metadata/types.d.ts +14 -2
- package/package.json +1 -1
- package/platforms/Platform.d.ts +8 -1
- package/platforms/Platform.js +12 -0
- package/types/BigIntType.d.ts +1 -0
- package/types/BigIntType.js +23 -0
- package/types/DateTimeType.d.ts +1 -0
- package/types/DateTimeType.js +8 -0
- package/types/Type.d.ts +11 -0
- package/typings.d.ts +11 -0
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +43 -33
- package/utils/Utils.js +1 -1
|
@@ -65,13 +65,22 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
65
65
|
where: FilterQuery<T>;
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
|
|
69
|
+
* condition have to agree on, or pagination skips rows at the null boundary. Platforms that cannot
|
|
70
|
+
* order nulls explicitly sort them as the lowest value, elsewhere the requested placement wins,
|
|
71
|
+
* defaulting to nulls last for `asc` and nulls first for `desc`.
|
|
72
|
+
*/
|
|
73
|
+
private parseCursorDirection;
|
|
74
|
+
/**
|
|
75
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
76
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
77
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
78
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
79
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
80
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
72
81
|
*/
|
|
73
82
|
private mapCursorOffset;
|
|
74
|
-
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T
|
|
83
|
+
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
|
|
75
84
|
/** @internal */
|
|
76
85
|
mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
|
|
77
86
|
protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
|
|
@@ -10,7 +10,6 @@ import { helper } from '../entity/wrap.js';
|
|
|
10
10
|
import { Reference } from '../entity/Reference.js';
|
|
11
11
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
12
12
|
import { JsonType } from '../types/JsonType.js';
|
|
13
|
-
import { DateTimeType } from '../types/DateTimeType.js';
|
|
14
13
|
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
15
14
|
import { MikroORM } from '../MikroORM.js';
|
|
16
15
|
/** Abstract base class for all database drivers, implementing common driver logic. */
|
|
@@ -146,6 +145,7 @@ export class DatabaseDriver {
|
|
|
146
145
|
const createCursor = (val, key, inverse = false) => {
|
|
147
146
|
const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
|
|
148
147
|
let offsets;
|
|
148
|
+
let fromJson = false;
|
|
149
149
|
// entity (and reference) instances are supported as cursors too, their properties are read the same way
|
|
150
150
|
if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
|
|
151
151
|
// POJO values are already JS values, extract them ordered per the definition,
|
|
@@ -158,11 +158,17 @@ export class DatabaseDriver {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
else {
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
try {
|
|
162
|
+
/* v8 ignore next */
|
|
163
|
+
offsets = def ? Cursor.decode(def) : [];
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
167
|
+
}
|
|
168
|
+
fromJson = true;
|
|
163
169
|
}
|
|
164
170
|
if (definition.length > 0 && definition.length === offsets.length) {
|
|
165
|
-
return this.createCursorCondition(definition, offsets, inverse, meta);
|
|
171
|
+
return this.createCursorCondition(definition, offsets, inverse, meta, fromJson);
|
|
166
172
|
}
|
|
167
173
|
/* v8 ignore next */
|
|
168
174
|
return {};
|
|
@@ -176,16 +182,25 @@ export class DatabaseDriver {
|
|
|
176
182
|
if (limit != null) {
|
|
177
183
|
options.limit = limit + (overfetch ? 1 : 0);
|
|
178
184
|
}
|
|
179
|
-
const createOrderBy = (prop, direction) => {
|
|
185
|
+
const createOrderBy = (prop, direction, properties = meta.properties, nullable = false) => {
|
|
186
|
+
const propMeta = properties[prop];
|
|
187
|
+
// a nullable relation or embeddable makes its joined columns null too
|
|
188
|
+
nullable ||= !!propMeta?.nullable;
|
|
180
189
|
if (Utils.isPlainObject(direction)) {
|
|
190
|
+
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
181
191
|
const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
|
|
182
|
-
Object.assign(o, createOrderBy(key, direction[key]));
|
|
192
|
+
Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}, nullable));
|
|
183
193
|
return o;
|
|
184
194
|
}, {});
|
|
185
195
|
return { [prop]: value };
|
|
186
196
|
}
|
|
187
|
-
const desc
|
|
197
|
+
const { desc, nullsFirst } = this.parseCursorDirection(direction);
|
|
188
198
|
const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
|
|
199
|
+
// the condition assumes a placement, spell it out instead of taking the database default
|
|
200
|
+
if (nullable && this.platform.supportsNullsOrdering()) {
|
|
201
|
+
const nulls = Utils.xor(nullsFirst, isLast) ? 'first' : 'last';
|
|
202
|
+
return { [prop]: `${dir} nulls ${nulls}` };
|
|
203
|
+
}
|
|
189
204
|
return { [prop]: dir };
|
|
190
205
|
};
|
|
191
206
|
// the cursor condition is created at the driver level, after the EM already converted custom types
|
|
@@ -203,12 +218,34 @@ export class DatabaseDriver {
|
|
|
203
218
|
};
|
|
204
219
|
}
|
|
205
220
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
221
|
+
* Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
|
|
222
|
+
* condition have to agree on, or pagination skips rows at the null boundary. Platforms that cannot
|
|
223
|
+
* order nulls explicitly sort them as the lowest value, elsewhere the requested placement wins,
|
|
224
|
+
* defaulting to nulls last for `asc` and nulls first for `desc`.
|
|
225
|
+
*/
|
|
226
|
+
parseCursorDirection(direction) {
|
|
227
|
+
const dir = ('' + direction).toLowerCase();
|
|
228
|
+
const desc = direction === QueryOrderNumeric.DESC || dir.startsWith('desc');
|
|
229
|
+
if (!this.platform.supportsNullsOrdering()) {
|
|
230
|
+
return { desc, nullsFirst: !desc };
|
|
231
|
+
}
|
|
232
|
+
if (dir.includes('nulls first')) {
|
|
233
|
+
return { desc, nullsFirst: true };
|
|
234
|
+
}
|
|
235
|
+
if (dir.includes('nulls last')) {
|
|
236
|
+
return { desc, nullsFirst: false };
|
|
237
|
+
}
|
|
238
|
+
return { desc, nullsFirst: desc };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
242
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
243
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
244
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
245
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
246
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
210
247
|
*/
|
|
211
|
-
mapCursorOffset(prop, value, insideJson) {
|
|
248
|
+
mapCursorOffset(prop, value, insideJson, fromJson) {
|
|
212
249
|
if (Utils.isScalarReference(value)) {
|
|
213
250
|
value = value.unwrap();
|
|
214
251
|
}
|
|
@@ -219,25 +256,65 @@ export class DatabaseDriver {
|
|
|
219
256
|
if (value == null) {
|
|
220
257
|
return value;
|
|
221
258
|
}
|
|
259
|
+
// mongo preserves native date types inside JSON documents, restored JS values compare directly
|
|
222
260
|
if (insideJson && !this.platform.preservesDatesInsideJson()) {
|
|
223
|
-
// compared against the JSON document, which holds the
|
|
261
|
+
// compared against the JSON document, which holds the JSON form of the database value
|
|
224
262
|
if (value instanceof Date) {
|
|
225
263
|
return value.toISOString();
|
|
226
264
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
265
|
+
if (prop?.customType && fromJson) {
|
|
266
|
+
const restored = prop.customType.fromJSON
|
|
267
|
+
? prop.customType.fromJSON(value, this.platform)
|
|
268
|
+
: prop.customType.convertToJSValue(value, this.platform);
|
|
269
|
+
// a restored `Date` compares against its ISO string in the document, everything else
|
|
270
|
+
// keeps the JS value and gets its single `convertToDatabaseValue` in `processWhere`
|
|
271
|
+
if (restored instanceof Date) {
|
|
272
|
+
const converted = prop.customType.convertToDatabaseValue(restored, this.platform, {
|
|
273
|
+
fromQuery: true,
|
|
274
|
+
key: prop.name,
|
|
275
|
+
mode: 'query',
|
|
276
|
+
});
|
|
277
|
+
if (converted instanceof Date) {
|
|
278
|
+
return converted.toISOString();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return restored;
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
230
284
|
}
|
|
231
|
-
if (
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
285
|
+
if (prop?.customType) {
|
|
286
|
+
if (fromJson && prop.customType.fromJSON) {
|
|
287
|
+
// the type owns its JSON round trip
|
|
288
|
+
return prop.customType.fromJSON(value, this.platform);
|
|
289
|
+
}
|
|
290
|
+
// A string is either a serialized form (string cursor) or a hand-written one (POJO).
|
|
291
|
+
// Types whose JS value is a `Date` must receive one. The rest get the string first,
|
|
292
|
+
// so any sub-millisecond precision survives.
|
|
293
|
+
if (typeof value === 'string' &&
|
|
294
|
+
(prop.runtimeType === 'Date' || this.platform.getMappedType(prop.columnTypes?.[0] ?? '').runtimeType === 'Date')) {
|
|
295
|
+
if (prop.runtimeType !== 'Date') {
|
|
296
|
+
try {
|
|
297
|
+
return prop.customType.convertToJSValue(value, this.platform);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// the type cannot read the serialized string, fall back to the `Date` form
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return prop.customType.convertToJSValue(new Date(value), this.platform);
|
|
304
|
+
}
|
|
305
|
+
// serialized non-string values still need restoring; POJO values are already JS values
|
|
306
|
+
return fromJson ? prop.customType.convertToJSValue(value, this.platform) : value;
|
|
307
|
+
}
|
|
308
|
+
if (typeof value === 'string' && prop?.runtimeType === 'Date') {
|
|
309
|
+
return new Date(value);
|
|
235
310
|
}
|
|
236
|
-
return
|
|
311
|
+
return value;
|
|
237
312
|
}
|
|
238
|
-
createCursorCondition(definition, offsets, inverse, meta) {
|
|
239
|
-
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false) => {
|
|
313
|
+
createCursorCondition(definition, offsets, inverse, meta, fromJson = false) {
|
|
314
|
+
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false, nullable = false) => {
|
|
240
315
|
const propMeta = properties[prop];
|
|
316
|
+
// a nullable relation or embeddable makes its joined columns null too
|
|
317
|
+
nullable ||= !!propMeta?.nullable;
|
|
241
318
|
if (Utils.isPlainObject(direction)) {
|
|
242
319
|
if (offset === undefined) {
|
|
243
320
|
throw CursorError.missingValue(meta.className, path);
|
|
@@ -247,40 +324,45 @@ export class DatabaseDriver {
|
|
|
247
324
|
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
248
325
|
insideJson ||=
|
|
249
326
|
(propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
else if (dirStr.includes('nulls last')) {
|
|
263
|
-
nullsFirst = false;
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
// Default: NULLS LAST for ASC, NULLS FIRST for DESC (matches most databases)
|
|
267
|
-
nullsFirst = isDesc;
|
|
327
|
+
const children = Utils.keys(direction)
|
|
328
|
+
.map(key => createCondition(key, direction[key],
|
|
329
|
+
// a null relation offset means the whole sort key is null, propagate it to the leaves
|
|
330
|
+
offset === null ? null : offset[key], eq, `${path}.${key}`, childProps ?? {}, insideJson, nullable))
|
|
331
|
+
.filter(child => Utils.hasObjectKeys(child));
|
|
332
|
+
// children comparing against nulls are group conditions, those cannot be merged into one object
|
|
333
|
+
const value = children.length > 1 && children.some(child => '$or' in child)
|
|
334
|
+
? { $and: children }
|
|
335
|
+
: children.reduce((o, child) => Object.assign(o, child), {});
|
|
336
|
+
// an unconstrained child condition must not degrade to `{ [prop]: {} }`
|
|
337
|
+
return children.length > 0 ? { [prop]: value } : {};
|
|
268
338
|
}
|
|
339
|
+
const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
|
|
269
340
|
const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
|
|
270
341
|
// For leaf-level properties, undefined means missing value
|
|
271
342
|
if (offset === undefined) {
|
|
272
343
|
throw CursorError.missingValue(meta.className, path);
|
|
273
344
|
}
|
|
274
|
-
|
|
345
|
+
// string-cursor values are client supplied, so a value the type cannot restore is an
|
|
346
|
+
// invalid cursor, letting callers map `CursorError` to a client error response
|
|
347
|
+
try {
|
|
348
|
+
offset = this.mapCursorOffset(propMeta, offset, insideJson, fromJson);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (!fromJson || error instanceof CursorError) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
355
|
+
}
|
|
275
356
|
// Handle null offset (intentional null cursor value)
|
|
276
357
|
if (offset === null) {
|
|
358
|
+
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
359
|
+
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
277
360
|
if (eq) {
|
|
278
|
-
//
|
|
279
|
-
|
|
361
|
+
// at-or-after a null sort key means every row when non-null rows follow the null block,
|
|
362
|
+
// so the tie-breaker in the `$or` sibling can reach past it
|
|
363
|
+
return hasItemsAfterNull ? {} : { [prop]: null };
|
|
280
364
|
}
|
|
281
365
|
// Strict comparison with null cursor value
|
|
282
|
-
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
283
|
-
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
284
366
|
if (hasItemsAfterNull) {
|
|
285
367
|
return { [prop]: { $ne: null } };
|
|
286
368
|
}
|
|
@@ -288,7 +370,12 @@ export class DatabaseDriver {
|
|
|
288
370
|
return { [prop]: [] };
|
|
289
371
|
}
|
|
290
372
|
// Non-null offset
|
|
291
|
-
|
|
373
|
+
const condition = { [prop]: { [operator + (eq ? 'e' : '')]: offset } };
|
|
374
|
+
// null sort keys lie past the offset here, and no comparison ever matches them
|
|
375
|
+
if (nullable && !Utils.xor(nullsFirst, inverse)) {
|
|
376
|
+
return { $or: [condition, { [prop]: null }] };
|
|
377
|
+
}
|
|
378
|
+
return condition;
|
|
292
379
|
};
|
|
293
380
|
const [order, ...otherOrders] = definition;
|
|
294
381
|
const [offset, ...otherOffsets] = offsets;
|
|
@@ -296,13 +383,18 @@ export class DatabaseDriver {
|
|
|
296
383
|
if (!otherOrders.length) {
|
|
297
384
|
return createCondition(prop, direction, offset);
|
|
298
385
|
}
|
|
299
|
-
|
|
300
|
-
|
|
386
|
+
const atOrPast = createCondition(prop, direction, offset, true);
|
|
387
|
+
const past = {
|
|
301
388
|
$or: [
|
|
302
389
|
createCondition(prop, direction, offset),
|
|
303
|
-
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta),
|
|
390
|
+
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta, fromJson),
|
|
304
391
|
],
|
|
305
392
|
};
|
|
393
|
+
// the `at or past` prefix is itself a group condition when it compares against nulls
|
|
394
|
+
if ('$or' in atOrPast) {
|
|
395
|
+
return { $and: [atOrPast, past] };
|
|
396
|
+
}
|
|
397
|
+
return { ...atOrPast, ...past };
|
|
306
398
|
}
|
|
307
399
|
/** @internal */
|
|
308
400
|
mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -148,6 +148,8 @@ export interface PropertyChain<in out Value, in out Options> {
|
|
|
148
148
|
orphanRemoval(orphanRemoval?: boolean): HasKind<Options, '1:m' | '1:1'> extends true ? PropertyChain<Value, Options> : never;
|
|
149
149
|
discriminator(discriminator: string): HasKind<Options, 'm:1' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
150
150
|
discriminatorMap(discriminatorMap: Dictionary<string>): HasKind<Options, 'm:1' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
151
|
+
/** Resolve this read-only to-one relation via a subquery on another entity (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
|
|
152
|
+
through(through: () => EntityName): HasKind<Options, 'm:1' | '1:1'> extends true ? PropertyChain<Value, Options> : never;
|
|
151
153
|
pivotTable(pivotTable: string): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
152
154
|
pivotEntity(pivotEntity: () => EntityName): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
153
155
|
fixedOrder(fixedOrder?: boolean): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
@@ -528,6 +530,8 @@ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Option
|
|
|
528
530
|
fixedOrderColumn(fixedOrderColumn: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
529
531
|
/** Override default name for pivot table (see {@doclink naming-strategy | Naming Strategy}). */
|
|
530
532
|
pivotTable(pivotTable: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
533
|
+
/** Resolve this read-only to-one relation via a subquery on another entity (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
|
|
534
|
+
through(through: () => EntityName): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
531
535
|
/** Set pivot entity for this relation (see {@doclink collections#custom-pivot-table-entity | Custom pivot table entity}). */
|
|
532
536
|
pivotEntity(pivotEntity: () => EntityName): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
533
537
|
/** Override the default database column name on the owning side (see {@doclink naming-strategy | Naming Strategy}). This option is only for simple properties represented by a single column. */
|
package/entity/defineEntity.js
CHANGED
|
@@ -403,6 +403,10 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
403
403
|
pivotTable(pivotTable) {
|
|
404
404
|
return this.assignOptions({ pivotTable });
|
|
405
405
|
}
|
|
406
|
+
/** Resolve this read-only to-one relation via a subquery on another entity (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
|
|
407
|
+
through(through) {
|
|
408
|
+
return this.assignOptions({ through });
|
|
409
|
+
}
|
|
406
410
|
/** Set pivot entity for this relation (see {@doclink collections#custom-pivot-table-entity | Custom pivot table entity}). */
|
|
407
411
|
pivotEntity(pivotEntity) {
|
|
408
412
|
return this.assignOptions({ pivotEntity });
|
package/errors.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare class ValidationError<T extends AnyEntity = AnyEntity> extends Er
|
|
|
41
41
|
export declare class CursorError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
|
|
42
42
|
static entityNotPopulated(entity: AnyEntity, prop: string): ValidationError;
|
|
43
43
|
static missingValue(entityName: string, prop: string): ValidationError;
|
|
44
|
+
static invalidCursor(entityName: string, cause: Error): CursorError;
|
|
44
45
|
}
|
|
45
46
|
/** Error thrown when an optimistic lock conflict is detected during entity persistence. */
|
|
46
47
|
export declare class OptimisticLockError<T extends AnyEntity = AnyEntity> extends ValidationError<T> {
|
|
@@ -73,6 +74,9 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
73
74
|
static targetIsAbstract(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
74
75
|
static nonPersistentCompositeProp(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
75
76
|
static propertyTargetsEntityType(meta: EntityMetadata, prop: EntityProperty, target: EntityMetadata): MetadataError;
|
|
77
|
+
static throughRelationMissingProperty(meta: EntityMetadata, prop: EntityProperty, through: EntityMetadata, side: 'owner' | 'target'): MetadataError;
|
|
78
|
+
static throughRelationCompositeTarget(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
79
|
+
static throughRelationInvalidKind(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
76
80
|
static fromMissingOption(meta: EntityMetadata, prop: EntityProperty, option: string): MetadataError;
|
|
77
81
|
static targetKeyOnManyToMany(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
|
78
82
|
static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError;
|
package/errors.js
CHANGED
|
@@ -140,6 +140,11 @@ export class CursorError extends ValidationError {
|
|
|
140
140
|
static missingValue(entityName, prop) {
|
|
141
141
|
return new CursorError(`Invalid cursor condition, value for '${entityName}.${prop}' is missing.`);
|
|
142
142
|
}
|
|
143
|
+
static invalidCursor(entityName, cause) {
|
|
144
|
+
const error = new CursorError(`Invalid cursor for entity ${entityName}: ${cause.message}`);
|
|
145
|
+
error.cause = cause;
|
|
146
|
+
return error;
|
|
147
|
+
}
|
|
143
148
|
}
|
|
144
149
|
/** Error thrown when an optimistic lock conflict is detected during entity persistence. */
|
|
145
150
|
export class OptimisticLockError extends ValidationError {
|
|
@@ -241,6 +246,16 @@ export class MetadataError extends ValidationError {
|
|
|
241
246
|
const suggestion = target.embeddable ? 'Embedded' : 'ManyToOne';
|
|
242
247
|
return this.fromMessage(meta, prop, `is defined as scalar @Property(), but its type is a discovered entity ${target.className}. Maybe you want to use @${suggestion}() decorator instead?`);
|
|
243
248
|
}
|
|
249
|
+
static throughRelationMissingProperty(meta, prop, through, side) {
|
|
250
|
+
const target = side === 'owner' ? meta.className : prop.targetMeta.className;
|
|
251
|
+
return this.fromMessage(meta, prop, `uses 'through' entity ${through.className} which has no ManyToOne property pointing to ${target}`);
|
|
252
|
+
}
|
|
253
|
+
static throughRelationCompositeTarget(meta, prop) {
|
|
254
|
+
return this.fromMessage(meta, prop, `uses 'through' option which is not supported for targets with composite primary key`);
|
|
255
|
+
}
|
|
256
|
+
static throughRelationInvalidKind(meta, prop) {
|
|
257
|
+
return this.fromMessage(meta, prop, `uses 'through' option which is only supported for ManyToOne and OneToOne relations`);
|
|
258
|
+
}
|
|
244
259
|
static fromMissingOption(meta, prop, option) {
|
|
245
260
|
return this.fromMessage(meta, prop, `is missing '${option}' option`);
|
|
246
261
|
}
|
|
@@ -126,6 +126,8 @@ export declare class MetadataDiscovery {
|
|
|
126
126
|
private initVersionProperty;
|
|
127
127
|
private initCustomType;
|
|
128
128
|
private initRelation;
|
|
129
|
+
/** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
|
|
130
|
+
private initThroughRelation;
|
|
129
131
|
private initColumnType;
|
|
130
132
|
private getMappedType;
|
|
131
133
|
private getPrefix;
|
|
@@ -168,10 +168,11 @@ export class MetadataDiscovery {
|
|
|
168
168
|
// filter names are load-bearing for RLS (policy and session variable names), backfill from the dictionary key
|
|
169
169
|
filtered.forEach(meta => Object.entries(meta.filters).forEach(([key, filter]) => (filter.name ??= key)));
|
|
170
170
|
filtered.forEach(meta => this.initPolicies(meta));
|
|
171
|
-
forEachProp((
|
|
171
|
+
forEachProp((m, p) => {
|
|
172
172
|
this.initDefaultValue(p);
|
|
173
173
|
this.inferTypeFromDefault(p);
|
|
174
174
|
this.initRelation(p);
|
|
175
|
+
this.initThroughRelation(m, p);
|
|
175
176
|
this.initColumnType(p);
|
|
176
177
|
});
|
|
177
178
|
forEachProp((m, p) => this.initIndexes(m, p));
|
|
@@ -223,11 +224,9 @@ export class MetadataDiscovery {
|
|
|
223
224
|
};
|
|
224
225
|
const missing = [];
|
|
225
226
|
this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const target = typeof
|
|
229
|
-
? pivotEntity()
|
|
230
|
-
: pivotEntity;
|
|
227
|
+
const indirect = (prop.kind === ReferenceKind.MANY_TO_MANY ? prop.pivotEntity : prop.through);
|
|
228
|
+
if (indirect) {
|
|
229
|
+
const target = typeof indirect === 'function' && !indirect.prototype ? indirect() : indirect;
|
|
231
230
|
if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
|
|
232
231
|
missing.push(target);
|
|
233
232
|
}
|
|
@@ -2078,6 +2077,58 @@ export class MetadataDiscovery {
|
|
|
2078
2077
|
}
|
|
2079
2078
|
}
|
|
2080
2079
|
}
|
|
2080
|
+
/** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
|
|
2081
|
+
initThroughRelation(meta, prop) {
|
|
2082
|
+
// already resolved, or not a through relation at all
|
|
2083
|
+
if (prop.through?.ownerProperty || !prop.through) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
2087
|
+
throw MetadataError.throughRelationInvalidKind(meta, prop);
|
|
2088
|
+
}
|
|
2089
|
+
const targetMeta = prop.targetMeta;
|
|
2090
|
+
// the subquery selects a single column
|
|
2091
|
+
if (targetMeta.compositePK) {
|
|
2092
|
+
throw MetadataError.throughRelationCompositeTarget(meta, prop);
|
|
2093
|
+
}
|
|
2094
|
+
const through = prop.through;
|
|
2095
|
+
const throughMeta = this.#metadata.get(!through.prototype ? through() : through);
|
|
2096
|
+
// a property is considered to point at an entity when it targets it or one of its parents
|
|
2097
|
+
const pointsTo = (p, m) => {
|
|
2098
|
+
const candidate = this.#metadata.find(p.target);
|
|
2099
|
+
/* v8 ignore next 3 */
|
|
2100
|
+
if (!candidate) {
|
|
2101
|
+
return false;
|
|
2102
|
+
}
|
|
2103
|
+
return candidate.class === m.class || m.class.prototype instanceof candidate.class;
|
|
2104
|
+
};
|
|
2105
|
+
const fks = Object.values(throughMeta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
|
|
2106
|
+
const ownerProp = fks.find(p => pointsTo(p, meta));
|
|
2107
|
+
if (!ownerProp) {
|
|
2108
|
+
throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'owner');
|
|
2109
|
+
}
|
|
2110
|
+
let targetProperty;
|
|
2111
|
+
const selectsTarget = throughMeta.class === targetMeta.class || throughMeta.class.prototype instanceof targetMeta.class;
|
|
2112
|
+
if (!selectsTarget) {
|
|
2113
|
+
const targetProp = fks.find(p => p !== ownerProp && pointsTo(p, targetMeta));
|
|
2114
|
+
if (!targetProp) {
|
|
2115
|
+
throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'target');
|
|
2116
|
+
}
|
|
2117
|
+
targetProperty = targetProp.name;
|
|
2118
|
+
}
|
|
2119
|
+
prop.through = {
|
|
2120
|
+
entity: throughMeta.class,
|
|
2121
|
+
where: prop.where,
|
|
2122
|
+
orderBy: prop.orderBy ? Utils.asArray(prop.orderBy) : undefined,
|
|
2123
|
+
ownerProperty: ownerProp.name,
|
|
2124
|
+
targetProperty,
|
|
2125
|
+
};
|
|
2126
|
+
// the condition and ordering apply to the `through` entity, not to the target, so they must not leak into the target joins
|
|
2127
|
+
delete prop.where;
|
|
2128
|
+
delete prop.orderBy;
|
|
2129
|
+
prop.persist = false;
|
|
2130
|
+
prop.formula = columns => this.#platform.getThroughRelationFormula(prop, columns);
|
|
2131
|
+
}
|
|
2081
2132
|
initColumnType(prop) {
|
|
2082
2133
|
this.initUnsigned(prop);
|
|
2083
2134
|
// Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
|
package/metadata/types.d.ts
CHANGED
|
@@ -431,9 +431,15 @@ interface PolymorphicOptions {
|
|
|
431
431
|
*/
|
|
432
432
|
discriminatorMap?: Dictionary<string>;
|
|
433
433
|
}
|
|
434
|
-
export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
|
|
434
|
+
export interface ManyToOneOptions<Owner, Target, Through = Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
|
|
435
435
|
/** Point to the inverse side property name. */
|
|
436
436
|
inversedBy?: (string & keyof Target) | ((e: Target) => any);
|
|
437
|
+
/** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
|
|
438
|
+
through?: () => EntityName<Through>;
|
|
439
|
+
/** Condition applied on the `through` entity. */
|
|
440
|
+
where?: FilterQuery<Through>;
|
|
441
|
+
/** Ordering applied on the `through` entity, the first matching row is used. */
|
|
442
|
+
orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
|
|
437
443
|
/** Wrap the entity in {@apilink Reference} wrapper. */
|
|
438
444
|
ref?: boolean;
|
|
439
445
|
/** Use this relation as a primary key. */
|
|
@@ -485,9 +491,15 @@ export interface OneToManyOptions<Owner, Target> extends ReferenceOptions<Owner,
|
|
|
485
491
|
/** Point to the owning side property name. */
|
|
486
492
|
mappedBy: (string & keyof Target) | ((e: Target) => any);
|
|
487
493
|
}
|
|
488
|
-
export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy'>>, PolymorphicOptions {
|
|
494
|
+
export interface OneToOneOptions<Owner, Target, Through = Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy' | 'where'>>, PolymorphicOptions {
|
|
489
495
|
/** Set this side as owning. Owning side is where the foreign key is defined. This option is not required if you use `inversedBy` or `mappedBy` to distinguish owning and inverse side. */
|
|
490
496
|
owner?: boolean;
|
|
497
|
+
/** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
|
|
498
|
+
through?: () => EntityName<Through>;
|
|
499
|
+
/** Condition for {@doclink collections#declarative-partial-loading | Declarative partial loading}, or the condition applied on the `through` entity. */
|
|
500
|
+
where?: FilterQuery<Through>;
|
|
501
|
+
/** Ordering applied on the `through` entity, the first matching row is used. */
|
|
502
|
+
orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
|
|
491
503
|
/** Point to the inverse side property name. */
|
|
492
504
|
inversedBy?: (string & keyof Target) | ((e: Target) => any);
|
|
493
505
|
/** Wrap the entity in {@apilink Reference} wrapper. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.17",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntityRepository } from '../entity/EntityRepository.js';
|
|
2
2
|
import { type NamingStrategy } from '../naming-strategy/NamingStrategy.js';
|
|
3
|
-
import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey } from '../typings.js';
|
|
3
|
+
import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey, FormulaColumns } from '../typings.js';
|
|
4
4
|
import { ExceptionConverter } from './ExceptionConverter.js';
|
|
5
5
|
import type { EntityManager } from '../EntityManager.js';
|
|
6
6
|
import type { Configuration } from '../utils/Configuration.js';
|
|
@@ -230,6 +230,8 @@ export declare abstract class Platform {
|
|
|
230
230
|
convertsJsonAutomatically(): boolean;
|
|
231
231
|
/** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
|
|
232
232
|
preservesDatesInsideJson(): boolean;
|
|
233
|
+
/** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. Platforms returning `false` always sort nulls as the lowest value. */
|
|
234
|
+
supportsNullsOrdering(): boolean;
|
|
233
235
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
|
234
236
|
convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
|
|
235
237
|
/** Converts a database JSON value to its JS representation. */
|
|
@@ -275,6 +277,11 @@ export declare abstract class Platform {
|
|
|
275
277
|
formatQuery(sql: string, params: readonly any[]): string;
|
|
276
278
|
/** Deep-clones embeddable data and tags it for JSON serialization. */
|
|
277
279
|
cloneEmbeddable<T>(data: T): T;
|
|
280
|
+
/**
|
|
281
|
+
* Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
|
|
282
|
+
* @internal
|
|
283
|
+
*/
|
|
284
|
+
getThroughRelationFormula(prop: EntityProperty, columns: FormulaColumns<any>): string;
|
|
278
285
|
/** Initializes the platform with the ORM configuration. */
|
|
279
286
|
setConfig(config: Configuration): void;
|
|
280
287
|
/** Returns the current ORM configuration. */
|
package/platforms/Platform.js
CHANGED
|
@@ -471,6 +471,10 @@ export class Platform {
|
|
|
471
471
|
preservesDatesInsideJson() {
|
|
472
472
|
return false;
|
|
473
473
|
}
|
|
474
|
+
/** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. Platforms returning `false` always sort nulls as the lowest value. */
|
|
475
|
+
supportsNullsOrdering() {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
474
478
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
|
475
479
|
convertJsonToDatabaseValue(value, context) {
|
|
476
480
|
return JSON.stringify(value);
|
|
@@ -616,6 +620,14 @@ export class Platform {
|
|
|
616
620
|
Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });
|
|
617
621
|
return copy;
|
|
618
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
|
|
625
|
+
* @internal
|
|
626
|
+
*/
|
|
627
|
+
/* v8 ignore next 3 */
|
|
628
|
+
getThroughRelationFormula(prop, columns) {
|
|
629
|
+
throw new Error(`${this.constructor.name} does not support the 'through' option of ${prop.name}`);
|
|
630
|
+
}
|
|
619
631
|
/** Initializes the platform with the ORM configuration. */
|
|
620
632
|
setConfig(config) {
|
|
621
633
|
this.config = config;
|
package/types/BigIntType.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class BigIntType<Mode extends 'bigint' | 'number' | 'string' = 'b
|
|
|
11
11
|
convertToDatabaseValue(value: JSTypeByMode<Mode> | null | undefined): string | null | undefined;
|
|
12
12
|
convertToJSValue(value: string | bigint | null | undefined): JSTypeByMode<Mode> | null | undefined;
|
|
13
13
|
toJSON(value: JSTypeByMode<Mode> | null | undefined): JSTypeByMode<Mode> | null | undefined;
|
|
14
|
+
fromJSON(value: unknown): JSTypeByMode<Mode> | null | undefined;
|
|
14
15
|
getColumnType(prop: EntityProperty, platform: Platform): string;
|
|
15
16
|
compareAsType(): string;
|
|
16
17
|
compareValues(a: string, b: string): boolean;
|
package/types/BigIntType.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
|
+
import { ValidationError } from '../errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* This type will automatically convert string values returned from the database to native JS bigints (default)
|
|
4
5
|
* or numbers (safe only for values up to `Number.MAX_SAFE_INTEGER`), or strings, depending on the `mode`.
|
|
@@ -36,6 +37,28 @@ export class BigIntType extends Type {
|
|
|
36
37
|
}
|
|
37
38
|
return this.convertToDatabaseValue(value);
|
|
38
39
|
}
|
|
40
|
+
fromJSON(value) {
|
|
41
|
+
// the serialized form is a decimal string, or a plain number in `number` mode
|
|
42
|
+
const valid = (typeof value === 'string' && /^-?\d+$/.test(value)) || (typeof value === 'number' && Number.isInteger(value));
|
|
43
|
+
if (!valid) {
|
|
44
|
+
throw ValidationError.invalidType(BigIntType, value, 'JSON');
|
|
45
|
+
}
|
|
46
|
+
switch (this.mode) {
|
|
47
|
+
case 'number': {
|
|
48
|
+
// `Number` silently rounds past `MAX_SAFE_INTEGER`, tampered cursors must fail loudly
|
|
49
|
+
const num = Number(value);
|
|
50
|
+
if (!Number.isSafeInteger(num)) {
|
|
51
|
+
throw ValidationError.invalidType(BigIntType, value, 'JSON');
|
|
52
|
+
}
|
|
53
|
+
return num;
|
|
54
|
+
}
|
|
55
|
+
case 'string':
|
|
56
|
+
return String(value);
|
|
57
|
+
case 'bigint':
|
|
58
|
+
default:
|
|
59
|
+
return BigInt(value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
getColumnType(prop, platform) {
|
|
40
63
|
return platform.getBigIntTypeDeclarationSQL(prop);
|
|
41
64
|
}
|
package/types/DateTimeType.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { EntityProperty } from '../typings.js';
|
|
|
5
5
|
export declare class DateTimeType extends Type<Date, string> {
|
|
6
6
|
getColumnType(prop: EntityProperty, platform: Platform): string;
|
|
7
7
|
compareAsType(): string;
|
|
8
|
+
fromJSON(value: unknown): Date;
|
|
8
9
|
get runtimeType(): string;
|
|
9
10
|
ensureComparable(): boolean;
|
|
10
11
|
getDefaultLength(platform: Platform): number;
|
package/types/DateTimeType.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
|
+
import { ValidationError } from '../errors.js';
|
|
2
3
|
/** Maps a database DATETIME/TIMESTAMP column to a JS `Date` object. */
|
|
3
4
|
export class DateTimeType extends Type {
|
|
4
5
|
getColumnType(prop, platform) {
|
|
@@ -7,6 +8,13 @@ export class DateTimeType extends Type {
|
|
|
7
8
|
compareAsType() {
|
|
8
9
|
return 'Date';
|
|
9
10
|
}
|
|
11
|
+
fromJSON(value) {
|
|
12
|
+
const date = new Date(value);
|
|
13
|
+
if (typeof value !== 'string' || Number.isNaN(date.getTime())) {
|
|
14
|
+
throw ValidationError.invalidType(DateTimeType, value, 'JSON');
|
|
15
|
+
}
|
|
16
|
+
return date;
|
|
17
|
+
}
|
|
10
18
|
get runtimeType() {
|
|
11
19
|
return 'Date';
|
|
12
20
|
}
|
package/types/Type.d.ts
CHANGED
|
@@ -59,6 +59,17 @@ export declare abstract class Type<JSType = string, DBType = JSType> {
|
|
|
59
59
|
* By default uses the runtime value.
|
|
60
60
|
*/
|
|
61
61
|
toJSON(value: JSType, platform: Platform): JSType | DBType;
|
|
62
|
+
/**
|
|
63
|
+
* Converts a value from its serialized JSON form back to its JS representation. Used when
|
|
64
|
+
* decoding cursor values. The input is what `toJSON` produced, after a `JSON.parse` round
|
|
65
|
+
* trip, and never an already restored JS value. Cursors are client supplied, so the value
|
|
66
|
+
* can be any JSON shape: validate it and throw for values the type cannot restore, and
|
|
67
|
+
* `findByCursor` surfaces the failure as a `CursorError`.
|
|
68
|
+
* Implementing this method also makes cursor encoding use `toJSON`. Without it, cursors
|
|
69
|
+
* carry the raw JS value, and decoding falls back to `convertToJSValue`, with type-based
|
|
70
|
+
* `Date` restoration for date-like columns.
|
|
71
|
+
*/
|
|
72
|
+
fromJSON?(value: unknown, platform: Platform): JSType;
|
|
62
73
|
/**
|
|
63
74
|
* Gets the SQL declaration snippet for a field of this type.
|
|
64
75
|
*/
|
package/typings.d.ts
CHANGED
|
@@ -653,6 +653,16 @@ export type SerializeDTO<T, H extends string = never, E extends string = never,
|
|
|
653
653
|
};
|
|
654
654
|
type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
|
|
655
655
|
type PropertyName<T> = IsUnknown<T> extends false ? TargetKeys<T> : string;
|
|
656
|
+
/** Resolved `through` option of a virtual to-one relation, populated during discovery. */
|
|
657
|
+
export interface ThroughRelation {
|
|
658
|
+
entity: EntityClass;
|
|
659
|
+
where?: FilterQuery<any>;
|
|
660
|
+
orderBy?: QueryOrderMap<any>[];
|
|
661
|
+
/** M:1 property on the `through` entity pointing back to the owner. */
|
|
662
|
+
ownerProperty: string;
|
|
663
|
+
/** M:1 property on the `through` entity pointing to the target, undefined when the target is selected directly. */
|
|
664
|
+
targetProperty?: string;
|
|
665
|
+
}
|
|
656
666
|
/** Table reference object passed to formula callbacks, including alias and schema information. */
|
|
657
667
|
export type FormulaTable = {
|
|
658
668
|
alias: string;
|
|
@@ -1043,6 +1053,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
1043
1053
|
fixedOrderColumn?: string;
|
|
1044
1054
|
pivotTable: string;
|
|
1045
1055
|
pivotEntity: EntityClass<Target>;
|
|
1056
|
+
through?: ThroughRelation;
|
|
1046
1057
|
joinColumns: string[];
|
|
1047
1058
|
ownColumns: string[];
|
|
1048
1059
|
inverseJoinColumns: string[];
|
package/utils/Cursor.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export declare class Cursor<Entity extends object, Hint extends string = never,
|
|
|
61
61
|
* Computes the cursor value for a given entity.
|
|
62
62
|
*/
|
|
63
63
|
from(entity: Entity | Loaded<Entity, Hint, Fields, Excludes>): string;
|
|
64
|
+
/** Serializes a single cursor value, walking nested directions and reading the owner's properties. */
|
|
65
|
+
private static serialize;
|
|
64
66
|
[Symbol.iterator](): IterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
|
|
65
67
|
get length(): number;
|
|
66
68
|
/**
|
package/utils/Cursor.js
CHANGED
|
@@ -58,6 +58,7 @@ export class Cursor {
|
|
|
58
58
|
hasPrevPage;
|
|
59
59
|
hasNextPage;
|
|
60
60
|
#definition;
|
|
61
|
+
#meta;
|
|
61
62
|
constructor(items, totalCount, options, meta) {
|
|
62
63
|
this.items = items;
|
|
63
64
|
this.totalCount = totalCount;
|
|
@@ -76,6 +77,7 @@ export class Cursor {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
this.#definition = Cursor.getDefinition(meta, orderBy);
|
|
80
|
+
this.#meta = meta;
|
|
79
81
|
}
|
|
80
82
|
get startCursor() {
|
|
81
83
|
if (this.items.length === 0) {
|
|
@@ -93,37 +95,46 @@ export class Cursor {
|
|
|
93
95
|
* Computes the cursor value for a given entity.
|
|
94
96
|
*/
|
|
95
97
|
from(entity) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let value = entity[prop];
|
|
109
|
-
// Allow null/undefined values in cursor - they will be handled in createCursorCondition
|
|
110
|
-
// undefined can occur with forceUndefined config option which converts null to undefined
|
|
111
|
-
if (value == null) {
|
|
112
|
-
return object ? { [prop]: null } : null;
|
|
113
|
-
}
|
|
114
|
-
if (Utils.isEntity(value, true)) {
|
|
115
|
-
value = helper(value).getPrimaryKey();
|
|
116
|
-
}
|
|
117
|
-
if (Utils.isScalarReference(value)) {
|
|
118
|
-
value = value.unwrap();
|
|
98
|
+
const value = this.#definition.map(([key, direction]) => Cursor.serialize(this.#meta.properties, entity, key, direction));
|
|
99
|
+
return Cursor.encode(value);
|
|
100
|
+
}
|
|
101
|
+
/** Serializes a single cursor value, walking nested directions and reading the owner's properties. */
|
|
102
|
+
static serialize(properties, owner, key, direction) {
|
|
103
|
+
const prop = properties[key];
|
|
104
|
+
let value = owner[key];
|
|
105
|
+
if (Utils.isPlainObject(direction)) {
|
|
106
|
+
const unwrapped = Reference.unwrapReference(value);
|
|
107
|
+
// for nested properties, an uninitialized relation means not populated
|
|
108
|
+
if (Utils.isEntity(unwrapped) && !helper(unwrapped).isInitialized()) {
|
|
109
|
+
throw CursorError.entityNotPopulated(owner, key);
|
|
119
110
|
}
|
|
120
|
-
if (object) {
|
|
121
|
-
return
|
|
111
|
+
if (unwrapped == null || typeof unwrapped !== 'object') {
|
|
112
|
+
return unwrapped;
|
|
122
113
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
const childProps = prop?.kind === ReferenceKind.EMBEDDED ? prop.embeddedProps : prop?.targetMeta?.properties;
|
|
115
|
+
return Utils.keys(direction).reduce((o, childKey) => {
|
|
116
|
+
o[childKey] = Cursor.serialize(childProps ?? {}, unwrapped, childKey, direction[childKey]);
|
|
117
|
+
return o;
|
|
118
|
+
}, {});
|
|
119
|
+
}
|
|
120
|
+
// allow null/undefined values in cursor - they will be handled in createCursorCondition
|
|
121
|
+
// undefined can occur with forceUndefined config option which converts null to undefined
|
|
122
|
+
if (value == null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (Utils.isEntity(value, true)) {
|
|
126
|
+
value = helper(value).getPrimaryKey();
|
|
127
|
+
}
|
|
128
|
+
if (Utils.isScalarReference(value)) {
|
|
129
|
+
value = value.unwrap();
|
|
130
|
+
}
|
|
131
|
+
// only types implementing `fromJSON` own their wire format, others keep the raw JS value,
|
|
132
|
+
// so their cursors stay decodable by the `convertToJSValue` fallback
|
|
133
|
+
if (prop?.customType?.fromJSON) {
|
|
134
|
+
// the platform is assigned to the type instance during discovery
|
|
135
|
+
return prop.customType.toJSON(value, prop.customType.platform);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
127
138
|
}
|
|
128
139
|
*[Symbol.iterator]() {
|
|
129
140
|
for (const item of this.items) {
|
|
@@ -138,12 +149,11 @@ export class Cursor {
|
|
|
138
149
|
*/
|
|
139
150
|
static for(meta, entity, orderBy) {
|
|
140
151
|
const definition = this.getDefinition(meta, orderBy);
|
|
141
|
-
return Cursor.encode(definition.map(([key]) => {
|
|
142
|
-
|
|
143
|
-
if (value === undefined) {
|
|
152
|
+
return Cursor.encode(definition.map(([key, direction]) => {
|
|
153
|
+
if (entity[key] === undefined) {
|
|
144
154
|
throw CursorError.missingValue(meta.className, key);
|
|
145
155
|
}
|
|
146
|
-
return
|
|
156
|
+
return this.serialize(meta.properties, entity, key, direction);
|
|
147
157
|
}));
|
|
148
158
|
}
|
|
149
159
|
static encode(value) {
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.2.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.2.0-dev.17';
|
|
157
157
|
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
158
158
|
static getRlsSettingName(filterName, argName) {
|
|
159
159
|
return `mikro.${filterName}.${argName}`;
|