@mikro-orm/core 7.3.0-dev.17 → 7.3.0-dev.18
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/EntityManager.js +127 -32
- package/drivers/DatabaseDriver.d.ts +2 -0
- package/drivers/DatabaseDriver.js +16 -3
- package/entity/EntityAssigner.js +15 -3
- package/entity/EntityFactory.d.ts +7 -0
- package/entity/EntityFactory.js +37 -0
- package/entity/EntityLoader.js +24 -9
- package/entity/defineEntity.d.ts +22 -17
- package/entity/validators.js +4 -1
- package/hydration/ObjectHydrator.js +4 -0
- package/metadata/MetadataDiscovery.d.ts +5 -0
- package/metadata/MetadataDiscovery.js +96 -25
- package/metadata/MetadataStorage.d.ts +1 -0
- package/metadata/MetadataStorage.js +20 -3
- package/metadata/MetadataValidator.js +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +4 -0
- package/platforms/Platform.js +8 -0
- package/serialization/EntitySerializer.js +52 -12
- package/typings.d.ts +21 -5
- package/typings.js +5 -2
- package/unit-of-work/ChangeSetComputer.js +2 -1
- package/unit-of-work/ChangeSetPersister.js +24 -5
- package/unit-of-work/CommitOrderCalculator.js +2 -1
- package/unit-of-work/UnitOfWork.js +20 -5
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +46 -6
- package/utils/QueryHelper.js +10 -2
- package/utils/RawQueryFragment.d.ts +5 -1
- package/utils/RawQueryFragment.js +27 -19
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +24 -8
- package/utils/clone.js +5 -7
- package/utils/fs-utils.js +19 -15
- package/utils/upsert-utils.js +2 -1
|
@@ -196,10 +196,17 @@ export class ChangeSetPersister {
|
|
|
196
196
|
}
|
|
197
197
|
async persistManagedEntities(meta, changeSets, options) {
|
|
198
198
|
const size = this.#config.get('batchSize');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
// a batch checks the version in all rows or none, so references without a loaded version go separately
|
|
200
|
+
const hasVersion = (cs) => cs.entity[meta.versionProperty] != null;
|
|
201
|
+
const groups = meta.ownsVersionProperty()
|
|
202
|
+
? [changeSets.filter(hasVersion), changeSets.filter(cs => !hasVersion(cs))]
|
|
203
|
+
: [changeSets];
|
|
204
|
+
for (const group of groups) {
|
|
205
|
+
for (let i = 0; i < group.length; i += size) {
|
|
206
|
+
const chunk = group.slice(i, i + size);
|
|
207
|
+
await this.persistManagedEntitiesBatch(meta, chunk, options);
|
|
208
|
+
await this.reloadVersionValues(meta, chunk, options);
|
|
209
|
+
}
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
checkConcurrencyKeys(meta, changeSet, cond) {
|
|
@@ -223,9 +230,14 @@ export class ChangeSetPersister {
|
|
|
223
230
|
});
|
|
224
231
|
const cond = [];
|
|
225
232
|
const payload = [];
|
|
233
|
+
// like in `updateEntity()`, but the batch needs the version in all rows or none
|
|
234
|
+
const versioned = meta.ownsVersionProperty() && changeSets.every(cs => cs.entity[meta.versionProperty] != null);
|
|
226
235
|
for (const changeSet of changeSets) {
|
|
227
236
|
const where = changeSet.getPrimaryKey(true);
|
|
228
237
|
this.checkConcurrencyKeys(meta, changeSet, where);
|
|
238
|
+
if (versioned) {
|
|
239
|
+
where[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
240
|
+
}
|
|
229
241
|
cond.push(where);
|
|
230
242
|
payload.push(changeSet.payload);
|
|
231
243
|
}
|
|
@@ -235,6 +247,12 @@ export class ChangeSetPersister {
|
|
|
235
247
|
// from those to be able to match them with `getSerializedPrimaryKey()` of the entity
|
|
236
248
|
const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
237
249
|
res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
|
|
250
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) &&
|
|
251
|
+
res.affectedRows < changeSets.length) {
|
|
252
|
+
// without `returning`, the stale entity can't be identified, so we report the first one
|
|
253
|
+
const failed = changeSets.find(cs => !map.has(helper(cs.entity).getSerializedPrimaryKey())) ?? changeSets[0];
|
|
254
|
+
throw OptimisticLockError.lockFailed(failed.entity);
|
|
255
|
+
}
|
|
238
256
|
for (const changeSet of changeSets) {
|
|
239
257
|
if (res.rows) {
|
|
240
258
|
const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
@@ -484,7 +502,8 @@ export class ChangeSetPersister {
|
|
|
484
502
|
}
|
|
485
503
|
if (upsert) {
|
|
486
504
|
for (const prop of meta.props) {
|
|
487
|
-
|
|
505
|
+
// null is not converted, the same way the hydrator handles it
|
|
506
|
+
if (prop.customType && mapped[prop.name] != null) {
|
|
488
507
|
mapped[prop.name] = prop.customType.convertToJSValue(mapped[prop.name], this.#platform);
|
|
489
508
|
}
|
|
490
509
|
}
|
|
@@ -44,7 +44,8 @@ export class CommitOrderCalculator {
|
|
|
44
44
|
if (!toOneOwner && !toManyOwner) {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// TPT tables are separate nodes and the FK references the target's own table, not the root
|
|
48
|
+
const propertyType = (prop.targetMeta?.inheritanceType === 'tpt' ? prop.targetMeta : prop.targetMeta?.root)?._id;
|
|
48
49
|
if (propertyType == null || !this.hasNode(propertyType)) {
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
@@ -600,19 +600,27 @@ export class UnitOfWork {
|
|
|
600
600
|
this.findEarlyUpdates(cs, inserts[cs.meta.uniqueName]);
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
|
+
// hashes of the inserts' PKs and unique values per type, built lazily, so matching a removed entity is a set lookup
|
|
604
|
+
const insertHashes = {};
|
|
603
605
|
for (const entity of this.#removeStack) {
|
|
604
606
|
const wrapped = helper(entity);
|
|
605
607
|
/* v8 ignore next */
|
|
606
608
|
if (wrapped.__processing) {
|
|
607
609
|
continue;
|
|
608
610
|
}
|
|
609
|
-
const
|
|
610
|
-
let
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
611
|
+
const uniqueName = wrapped.__meta.uniqueName;
|
|
612
|
+
let hashes = insertHashes[uniqueName];
|
|
613
|
+
if (!hashes) {
|
|
614
|
+
hashes = insertHashes[uniqueName] = new Set();
|
|
615
|
+
for (const cs of inserts[uniqueName] ?? []) {
|
|
616
|
+
hashes.add(cs.getSerializedPrimaryKey());
|
|
617
|
+
for (const hash of this.expandUniqueProps(cs.entity)) {
|
|
618
|
+
hashes.add(hash);
|
|
619
|
+
}
|
|
614
620
|
}
|
|
615
621
|
}
|
|
622
|
+
const deletePkHash = [wrapped.getSerializedPrimaryKey(), ...this.expandUniqueProps(entity)];
|
|
623
|
+
const type = deletePkHash.some(hash => hashes.has(hash)) ? ChangeSetType.DELETE_EARLY : ChangeSetType.DELETE;
|
|
616
624
|
this.computeChangeSet(entity, type);
|
|
617
625
|
}
|
|
618
626
|
}
|
|
@@ -1283,6 +1291,13 @@ export class UnitOfWork {
|
|
|
1283
1291
|
calc.discoverProperty({ ...prop, targetMeta }, meta._id);
|
|
1284
1292
|
}
|
|
1285
1293
|
}
|
|
1294
|
+
else if (prop.targetMeta?.inheritanceType === 'tpt') {
|
|
1295
|
+
calc.discoverProperty(prop, meta._id);
|
|
1296
|
+
// a TPT subclass delete has only a leaf change set that also removes the target row; weak edge so it can't break insert order
|
|
1297
|
+
for (const targetMeta of this.#metadata.find(prop.targetMeta.class).allTPTDescendants ?? []) {
|
|
1298
|
+
calc.discoverProperty({ ...prop, targetMeta, nullable: true }, meta._id);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1286
1301
|
else {
|
|
1287
1302
|
calc.discoverProperty(prop, meta._id);
|
|
1288
1303
|
}
|
|
@@ -19,6 +19,11 @@ export declare class EntityComparator {
|
|
|
19
19
|
diffEntities<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>, options?: {
|
|
20
20
|
includeInverseSides?: boolean;
|
|
21
21
|
}): EntityData<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
isStrictlyCompared<T>(prop: EntityProperty<T>): boolean;
|
|
22
27
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
23
28
|
matching<T extends object>(entityName: EntityName<T>, a: EntityData<T>, b: EntityData<T>): boolean;
|
|
24
29
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clone } from './clone.js';
|
|
2
2
|
import { ReferenceKind } from '../enums.js';
|
|
3
3
|
import { compareArrays, compareBooleans, compareBuffers, compareObjects, equals, parseJsonSafe, Utils, } from './Utils.js';
|
|
4
|
+
import { DateType } from '../types/DateType.js';
|
|
4
5
|
import { JsonType } from '../types/JsonType.js';
|
|
5
6
|
import { Raw } from './RawQueryFragment.js';
|
|
6
7
|
import { EntityIdentifier } from '../entity/EntityIdentifier.js';
|
|
@@ -29,6 +30,17 @@ export class EntityComparator {
|
|
|
29
30
|
const comparator = this.getEntityComparator(entityName);
|
|
30
31
|
return Utils.callCompiledFunction(comparator, a, b, options);
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns true if the comparator checks this property with `!==`, so matching values are identical primitives.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
isStrictlyCompared(prop) {
|
|
38
|
+
if (prop.kind !== ReferenceKind.SCALAR || prop.customType?.compareValues) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const type = (prop.customType ? prop.customType.compareAsType() : prop.type).toLowerCase();
|
|
42
|
+
return ['string', 'number', 'bigint'].includes(type);
|
|
43
|
+
}
|
|
32
44
|
/** Returns true if two entity snapshots are identical (no differences). */
|
|
33
45
|
matching(entityName, a, b) {
|
|
34
46
|
const diff = this.diffEntities(entityName, a, b);
|
|
@@ -355,8 +367,16 @@ export class EntityComparator {
|
|
|
355
367
|
if (prop.renamedFrom) {
|
|
356
368
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
357
369
|
defined = `${this.propName(prop.fieldNames[0])} != null`;
|
|
358
|
-
|
|
359
|
-
|
|
370
|
+
const sharesColumn = meta.props.some(p => p !== prop && p.name === prop.name && p.fieldNames?.[0] === prop.fieldNames[0]);
|
|
371
|
+
// hydrators expect mapped embeddable keys, so a shared object embeddable is mapped only for its own subtypes
|
|
372
|
+
if (sharesColumn && prop.kind === ReferenceKind.EMBEDDED && prop.object && meta.discriminatorMap) {
|
|
373
|
+
const owners = Object.entries(meta.discriminatorMap)
|
|
374
|
+
.filter(([, cls]) => this.#metadata.find(cls)?.properties[prop.name]?.targetMeta === prop.targetMeta)
|
|
375
|
+
.map(([value]) => `${this.propName(meta.discriminatorColumn)} == ${JSON.stringify(value)}`);
|
|
376
|
+
defined += ` && (${owners.join(' || ')})`;
|
|
377
|
+
}
|
|
378
|
+
else if (sharesColumn) {
|
|
379
|
+
// a column shared with a differently typed subtype is left for each subtype's hydrator to convert
|
|
360
380
|
lines.push(`${padding} if (${defined}) {`);
|
|
361
381
|
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
362
382
|
lines.push(`${padding} }`);
|
|
@@ -376,6 +396,14 @@ export class EntityComparator {
|
|
|
376
396
|
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
377
397
|
lines.push(`${padding} }`);
|
|
378
398
|
}
|
|
399
|
+
else if (prop.customType instanceof DateType) {
|
|
400
|
+
// tedious returns DATE columns as `Date`, map them to the string the entity holds, so the snapshot matches it
|
|
401
|
+
context.set('convertDateToJSValue', (value) => this.#platform.convertDateToJSValue(value));
|
|
402
|
+
lines.push(`${padding} if (${defined}) {`);
|
|
403
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} instanceof Date ? convertDateToJSValue(${this.propName(prop.fieldNames[0])}) : ${this.propName(prop.fieldNames[0])};`);
|
|
404
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
405
|
+
lines.push(`${padding} }`);
|
|
406
|
+
}
|
|
379
407
|
else if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
380
408
|
const idx = this.#tmpIndex++;
|
|
381
409
|
context.set(`mapEmbeddedResult_${idx}`, (data) => {
|
|
@@ -512,11 +540,22 @@ export class EntityComparator {
|
|
|
512
540
|
}
|
|
513
541
|
return true;
|
|
514
542
|
}
|
|
543
|
+
// a same-named property of a different kind in another polymorphic variant applies only to its own variants
|
|
544
|
+
const guard = (childProp, code) => {
|
|
545
|
+
const variants = prop.targetMeta?.polymorphs?.filter(m => m.properties[childProp.embedded[1]]) ?? [];
|
|
546
|
+
const owners = variants.filter(m => Utils.isSameKind(m.properties[childProp.embedded[1]], childProp));
|
|
547
|
+
if (owners.length === variants.length) {
|
|
548
|
+
return code;
|
|
549
|
+
}
|
|
550
|
+
const discriminator = `entity${[...path, prop.targetMeta.discriminatorColumn].map(k => this.wrap(k)).join('')}`;
|
|
551
|
+
const conds = owners.map(m => `${discriminator} == ${JSON.stringify(m.discriminatorValue)}`);
|
|
552
|
+
return `${padding} if (${conds.join(' || ')}) {\n${code}\n${padding} }`;
|
|
553
|
+
};
|
|
554
|
+
const childProps = meta.props.filter(p => p.embedded?.[0] === prop.name &&
|
|
555
|
+
// object for JSON embeddable
|
|
556
|
+
(p.object || p.persist !== false));
|
|
515
557
|
ret +=
|
|
516
|
-
|
|
517
|
-
.filter(p => p.embedded?.[0] === prop.name &&
|
|
518
|
-
// object for JSON embeddable
|
|
519
|
-
(p.object || p.persist !== false))
|
|
558
|
+
childProps
|
|
520
559
|
.map(childProp => {
|
|
521
560
|
const childDataKey = meta.embeddable || prop.object ? dataKey + this.wrap(childProp.embedded[1]) : this.wrap(childProp.name);
|
|
522
561
|
const childEntityKey = [...path, childProp.embedded[1]].map(k => this.wrap(k)).join('');
|
|
@@ -539,6 +578,7 @@ export class EntityComparator {
|
|
|
539
578
|
}
|
|
540
579
|
return `${padding} if (${childCond}) ret${childDataKey} = clone(entity${childEntityKey});`;
|
|
541
580
|
})
|
|
581
|
+
.map((code, i) => guard(childProps[i], code))
|
|
542
582
|
.join('\n') + `\n`;
|
|
543
583
|
if (this.shouldSerialize(prop, dataKey)) {
|
|
544
584
|
return `${ret + padding} ret${dataKey} = cloneEmbeddable(ret${dataKey});\n${padding}}`;
|
package/utils/QueryHelper.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DANGEROUS_PROPERTY_NAMES, Utils } from './Utils.js';
|
|
|
3
3
|
import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
|
-
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
6
|
+
import { isRaw, isSubquery, Raw } from './RawQueryFragment.js';
|
|
7
7
|
import { MetadataError, ValidationError } from '../errors.js';
|
|
8
8
|
/** @internal */
|
|
9
9
|
export class QueryHelper {
|
|
@@ -313,6 +313,13 @@ export class QueryHelper {
|
|
|
313
313
|
o[key] = { [op]: value };
|
|
314
314
|
return o;
|
|
315
315
|
}
|
|
316
|
+
// `$all` items are matched like separate `$some` payloads, so they need the same processing
|
|
317
|
+
if (key === '$all' && Array.isArray(value)) {
|
|
318
|
+
o[key] = value.map(item => Utils.isPlainObject(item)
|
|
319
|
+
? QueryHelper.processWhere({ ...options, where: item, entityName, root: false })
|
|
320
|
+
: item);
|
|
321
|
+
return o;
|
|
322
|
+
}
|
|
316
323
|
if (Utils.isPlainObject(value)) {
|
|
317
324
|
const targetMeta = customExpression ? undefined : QueryHelper.findTargetMeta(prop, key);
|
|
318
325
|
o[key] = QueryHelper.processWhere({
|
|
@@ -546,7 +553,8 @@ export class QueryHelper {
|
|
|
546
553
|
if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) {
|
|
547
554
|
return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery));
|
|
548
555
|
}
|
|
549
|
-
|
|
556
|
+
// a query builder renders as a sub-query, not as a value of the property type
|
|
557
|
+
if (isRaw(cond) || isSubquery(cond)) {
|
|
550
558
|
return cond;
|
|
551
559
|
}
|
|
552
560
|
return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { AnyString, Dictionary, EntityKey } from '../typings.js';
|
|
1
|
+
import type { AnyString, Dictionary, EntityKey, Subquery } from '../typings.js';
|
|
2
2
|
declare const rawFragmentSymbolBrand: unique symbol;
|
|
3
3
|
/** Branded symbol type used as a unique key for tracking raw SQL fragments in object properties. */
|
|
4
4
|
export type RawQueryFragmentSymbol = symbol & {
|
|
5
5
|
readonly [rawFragmentSymbolBrand]: true;
|
|
6
6
|
};
|
|
7
|
+
/** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
|
|
8
|
+
export declare const SUBQUERY_BRAND: unique symbol;
|
|
7
9
|
/** Checks whether the given value is a `RawQueryFragment` instance. */
|
|
8
10
|
export declare function isRaw(value: unknown): value is RawQueryFragment;
|
|
11
|
+
/** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
|
|
12
|
+
export declare function isSubquery(value: unknown): value is Subquery;
|
|
9
13
|
/** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
|
|
10
14
|
export declare class RawQueryFragment<Alias extends string = string> {
|
|
11
15
|
#private;
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { Utils } from './Utils.js';
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// so each CJS/ESM copy of this module independently installs it on its own
|
|
5
|
-
// prototype without publishing a global key for the marker that controls raw
|
|
6
|
-
// SQL assembly. The string is namespaced so it does not collide with property
|
|
7
|
-
// names users might independently install on their own prototypes.
|
|
8
|
-
const RAW_FRAGMENT_BRAND = '__mikroOrmRawFragment';
|
|
2
|
+
// Prototype brand of raw fragments; `Symbol.for` so package copies agree.
|
|
3
|
+
const RAW_FRAGMENT_BRAND = Symbol.for('mikro-orm.raw-fragment');
|
|
9
4
|
// Back-references from a fragment's symbol key to the fragment itself, shared
|
|
10
5
|
// across CJS/ESM module copies via globalThis: when one copy creates a fragment
|
|
11
6
|
// via `raw('…')` and stores its symbol in a where-clause object key, the other
|
|
@@ -13,6 +8,20 @@ const RAW_FRAGMENT_BRAND = '__mikroOrmRawFragment';
|
|
|
13
8
|
const REGISTRY_KEY = Symbol.for('@mikro-orm/core/RawQueryFragment.references');
|
|
14
9
|
const rawQueryReferences = (globalThis[REGISTRY_KEY] ??=
|
|
15
10
|
new WeakMap());
|
|
11
|
+
/** Prototype brand of query builders usable as sub-queries; a symbol so JSON payloads cannot carry it, `Symbol.for` so package copies agree. */
|
|
12
|
+
export const SUBQUERY_BRAND = Symbol.for('mikro-orm.subquery');
|
|
13
|
+
function hasPrototypeBrand(value, brand) {
|
|
14
|
+
// Walk the prototype chain starting from the *prototype* (not the value) so
|
|
15
|
+
// own-property spoofing from JSON payloads cannot forge the brand. Stop at
|
|
16
|
+
// `Object.prototype`, which is never branded — that bails plain objects,
|
|
17
|
+
// JSON payloads, and built-ins like `Date`/`Array`/`Map` at depth 1.
|
|
18
|
+
for (let p = Object.getPrototypeOf(value); p != null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
|
|
19
|
+
if (Object.hasOwn(p, brand)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
16
25
|
/** Checks whether the given value is a `RawQueryFragment` instance. */
|
|
17
26
|
export function isRaw(value) {
|
|
18
27
|
if (value == null || typeof value !== 'object') {
|
|
@@ -23,16 +32,11 @@ export function isRaw(value) {
|
|
|
23
32
|
if (value instanceof RawQueryFragment) {
|
|
24
33
|
return true;
|
|
25
34
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (Object.hasOwn(p, RAW_FRAGMENT_BRAND)) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
35
|
+
return hasPrototypeBrand(value, RAW_FRAGMENT_BRAND);
|
|
36
|
+
}
|
|
37
|
+
/** Checks whether the given value is a query builder usable as a sub-query (its prototype carries the `SUBQUERY_BRAND`). */
|
|
38
|
+
export function isSubquery(value) {
|
|
39
|
+
return value != null && typeof value === 'object' && hasPrototypeBrand(value, SUBQUERY_BRAND);
|
|
36
40
|
}
|
|
37
41
|
/** Represents a raw SQL fragment with optional parameters, usable as both a value and an object key via Symbol coercion. */
|
|
38
42
|
export class RawQueryFragment {
|
|
@@ -121,6 +125,10 @@ export { RawQueryFragment as Raw };
|
|
|
121
125
|
export const ALIAS_REPLACEMENT = '[::alias::]';
|
|
122
126
|
/** @internal */
|
|
123
127
|
export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
|
|
128
|
+
// a query builder param is embedded as a sub-query instead of being bound as a value
|
|
129
|
+
function toRawParam(param) {
|
|
130
|
+
return isSubquery(param) ? param.toRaw() : param;
|
|
131
|
+
}
|
|
124
132
|
/**
|
|
125
133
|
* Creates raw SQL query fragment that can be assigned to a property or part of a filter. This fragment is represented
|
|
126
134
|
* by `RawQueryFragment` class instance that can be serialized to a string, so it can be used both as an object value
|
|
@@ -204,12 +212,12 @@ export function raw(sql, params) {
|
|
|
204
212
|
if (!Object.hasOwn(dict, key)) {
|
|
205
213
|
return match;
|
|
206
214
|
}
|
|
207
|
-
objectParams.push(dict[key]);
|
|
215
|
+
objectParams.push(toRawParam(dict[key]));
|
|
208
216
|
return identifier ? '??' : '?';
|
|
209
217
|
});
|
|
210
218
|
return new RawQueryFragment(sql, objectParams);
|
|
211
219
|
}
|
|
212
|
-
return new RawQueryFragment(sql, params);
|
|
220
|
+
return new RawQueryFragment(sql, params?.map(toRawParam));
|
|
213
221
|
}
|
|
214
222
|
/**
|
|
215
223
|
* Alternative to the `raw()` helper allowing to use it as a tagged template function for the simple cases.
|
package/utils/Utils.d.ts
CHANGED
|
@@ -175,6 +175,8 @@ export declare class Utils {
|
|
|
175
175
|
static getORMVersion(): string;
|
|
176
176
|
static createFunction(context: Map<string, any>, code: string, compiledFunctions?: CompiledFunctions, key?: string): any;
|
|
177
177
|
static callCompiledFunction<T extends unknown[], R>(fn: (...args: T) => R, ...args: T): R;
|
|
178
|
+
/** @internal Same-named properties of polymorphic embeddable variants conflict when their kinds differ, to-one kinds share the column. */
|
|
179
|
+
static isSameKind(a: EntityProperty, b: EntityProperty): boolean;
|
|
178
180
|
static unwrapProperty<T>(entity: T, meta: EntityMetadata<T>, prop: EntityProperty<T>, payload?: boolean): [unknown, number[]][];
|
|
179
181
|
static setPayloadProperty<T>(entity: EntityDictionary<T>, meta: EntityMetadata<T>, prop: EntityProperty<T>, value: unknown, idx: number[]): void;
|
|
180
182
|
static tryImport<T extends Dictionary = any>({ module, warning, }: {
|
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.3.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.3.0-dev.18';
|
|
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}`;
|
|
@@ -244,7 +244,7 @@ export class Utils {
|
|
|
244
244
|
/**
|
|
245
245
|
* Merges all sources into the target recursively.
|
|
246
246
|
*/
|
|
247
|
-
static _merge(target, sources, ignoreUndefined) {
|
|
247
|
+
static _merge(target, sources, ignoreUndefined, copies) {
|
|
248
248
|
if (!sources.length) {
|
|
249
249
|
return target;
|
|
250
250
|
}
|
|
@@ -259,7 +259,13 @@ export class Utils {
|
|
|
259
259
|
}
|
|
260
260
|
if (Utils.isPlainObject(value)) {
|
|
261
261
|
if (!Utils.isObject(target[key])) {
|
|
262
|
-
|
|
262
|
+
// copy plain objects but keep class instances by reference, tracking copies so cycles terminate
|
|
263
|
+
const seen = copies ?? new WeakMap();
|
|
264
|
+
if (!seen.has(value)) {
|
|
265
|
+
seen.set(value, {});
|
|
266
|
+
Utils._merge(seen.get(value), [value], ignoreUndefined, seen);
|
|
267
|
+
}
|
|
268
|
+
target[key] = seen.get(value);
|
|
263
269
|
continue;
|
|
264
270
|
}
|
|
265
271
|
/* v8 ignore next */
|
|
@@ -651,14 +657,19 @@ export class Utils {
|
|
|
651
657
|
static isCollection(item) {
|
|
652
658
|
return Collection.isCollection(item);
|
|
653
659
|
}
|
|
654
|
-
// FNV-1a 64-bit
|
|
660
|
+
// FNV-1a 64-bit, computed on two uint32 halves instead of a BigInt. The prime is 2^40 + 0x1b3, so
|
|
661
|
+
// `h * prime mod 2^64` is `h * 0x1b3` plus the low half shifted up by 40 bits. Every partial product
|
|
662
|
+
// stays below 2^41, so the double arithmetic is exact.
|
|
655
663
|
static hash(data, length) {
|
|
656
|
-
let
|
|
664
|
+
let hi = 0xcbf29ce4;
|
|
665
|
+
let lo = 0x84222325;
|
|
657
666
|
for (let i = 0; i < data.length; i++) {
|
|
658
|
-
|
|
659
|
-
|
|
667
|
+
lo = (lo ^ data.charCodeAt(i)) >>> 0;
|
|
668
|
+
const loProduct = lo * 0x1b3;
|
|
669
|
+
hi = (((hi * 0x1b3) >>> 0) + Math.floor(loProduct / 0x100000000) + ((lo << 8) >>> 0)) >>> 0;
|
|
670
|
+
lo = loProduct >>> 0;
|
|
660
671
|
}
|
|
661
|
-
const hash =
|
|
672
|
+
const hash = hi.toString(16).padStart(8, '0') + lo.toString(16).padStart(8, '0');
|
|
662
673
|
if (length) {
|
|
663
674
|
return hash.substring(0, length);
|
|
664
675
|
}
|
|
@@ -775,6 +786,11 @@ export class Utils {
|
|
|
775
786
|
throw e;
|
|
776
787
|
}
|
|
777
788
|
}
|
|
789
|
+
/** @internal Same-named properties of polymorphic embeddable variants conflict when their kinds differ, to-one kinds share the column. */
|
|
790
|
+
static isSameKind(a, b) {
|
|
791
|
+
const toOne = (p) => p.kind === ReferenceKind.MANY_TO_ONE || p.kind === ReferenceKind.ONE_TO_ONE;
|
|
792
|
+
return a.kind === b.kind || (toOne(a) && toOne(b));
|
|
793
|
+
}
|
|
778
794
|
static unwrapProperty(entity, meta, prop, payload = false) {
|
|
779
795
|
let p = prop;
|
|
780
796
|
const path = [];
|
package/utils/clone.js
CHANGED
|
@@ -22,8 +22,8 @@ function getPropertyDescriptor(obj, prop) {
|
|
|
22
22
|
}
|
|
23
23
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
24
24
|
export function clone(parent, respectCustomCloneMethod = true) {
|
|
25
|
-
|
|
26
|
-
const
|
|
25
|
+
// already cloned objects, so shared references and cycles map to a single clone
|
|
26
|
+
const clones = new Map();
|
|
27
27
|
function _clone(parent) {
|
|
28
28
|
// cloning null always returns null
|
|
29
29
|
if (parent === null) {
|
|
@@ -92,12 +92,10 @@ export function clone(parent, respectCustomCloneMethod = true) {
|
|
|
92
92
|
proto = Object.getPrototypeOf(parent);
|
|
93
93
|
child = Object.create(proto);
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return allChildren[index];
|
|
95
|
+
if (clones.has(parent)) {
|
|
96
|
+
return clones.get(parent);
|
|
98
97
|
}
|
|
99
|
-
|
|
100
|
-
allChildren.push(child);
|
|
98
|
+
clones.set(parent, child);
|
|
101
99
|
if (parent instanceof Map) {
|
|
102
100
|
parent.forEach((value, key) => {
|
|
103
101
|
const keyChild = _clone(key);
|
package/utils/fs-utils.js
CHANGED
|
@@ -5,7 +5,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { Utils } from './Utils.js';
|
|
6
6
|
import { colors } from '../logging/colors.js';
|
|
7
7
|
let globSync = (patterns, options) => {
|
|
8
|
-
const
|
|
8
|
+
const cwd = options?.dir ? join(options.cwd ?? '', options.dir) : options?.cwd;
|
|
9
|
+
const files = nodeGlobSync(patterns, { cwd, withFileTypes: true });
|
|
9
10
|
return files.filter(f => f.isFile()).map(f => join(f.parentPath, f.name));
|
|
10
11
|
};
|
|
11
12
|
export const fs = {
|
|
@@ -13,7 +14,8 @@ export const fs = {
|
|
|
13
14
|
const tinyGlobby = await import('tinyglobby').catch(() => null);
|
|
14
15
|
if (tinyGlobby) {
|
|
15
16
|
globSync = (patterns, options) => {
|
|
16
|
-
|
|
17
|
+
const dir = options?.dir ? tinyGlobby.escapePath(options.dir.replace(/\\/g, '/')) + '/' : '';
|
|
18
|
+
patterns = Utils.asArray(patterns).map(p => dir + p.replace(/\\/g, '/'));
|
|
17
19
|
// never forward `cwd: undefined` — tinyglobby >= 0.2.16 calls `path.resolve(undefined)` and throws
|
|
18
20
|
return tinyGlobby.globSync(patterns, {
|
|
19
21
|
expandDirectories: false,
|
|
@@ -23,10 +25,10 @@ export const fs = {
|
|
|
23
25
|
}
|
|
24
26
|
},
|
|
25
27
|
pathExists(path) {
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
28
|
+
if (existsSync(path)) {
|
|
29
|
+
return true;
|
|
28
30
|
}
|
|
29
|
-
return
|
|
31
|
+
return /[*?[\]]/.test(path) && this.resolveGlob(path).length > 0;
|
|
30
32
|
},
|
|
31
33
|
ensureDir(path) {
|
|
32
34
|
if (!existsSync(path)) {
|
|
@@ -62,18 +64,20 @@ export const fs = {
|
|
|
62
64
|
if (Array.isArray(input)) {
|
|
63
65
|
return input.flatMap(paths => this.resolveGlob(paths, cwd));
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// ignore
|
|
67
|
+
// an absolute path repeats the cwd, which may contain glob characters (e.g. `~/Dropbox (Team)/app`)
|
|
68
|
+
if (isAbsolute(input)) {
|
|
69
|
+
cwd ??= process.cwd();
|
|
70
|
+
input = relative(cwd, input).replace(/\\/g, '/') || '.';
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const s = statSync(cwd ? this.normalizePath(cwd, input) : input);
|
|
74
|
+
if (s.isDirectory()) {
|
|
75
|
+
return globSync('**', { cwd, dir: input });
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
77
81
|
return globSync(input, { cwd });
|
|
78
82
|
},
|
|
79
83
|
getPackageConfig(basePath = process.cwd()) {
|
package/utils/upsert-utils.js
CHANGED
|
@@ -103,7 +103,8 @@ export function getOnConflictReturningFields(meta, data, uniqueFields, options,
|
|
|
103
103
|
if (p.lazy || p.embeddable) {
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
// a PK missing in the data is generated by the database
|
|
107
|
+
if (p.autoincrement || (p.primary && data[p.name] == null)) {
|
|
107
108
|
return true;
|
|
108
109
|
}
|
|
109
110
|
// for a raw `onConflictFields` fragment we can't introspect which columns form the conflict
|