@mikro-orm/core 7.0.0-dev.3 → 7.0.0-dev.30
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.d.ts +50 -7
- package/EntityManager.js +141 -97
- package/MikroORM.js +0 -1
- package/README.md +1 -2
- package/cache/FileCacheAdapter.d.ts +2 -1
- package/cache/FileCacheAdapter.js +6 -4
- package/connections/Connection.d.ts +4 -2
- package/connections/Connection.js +2 -2
- package/decorators/Check.d.ts +2 -2
- package/decorators/Embeddable.d.ts +5 -5
- package/decorators/Embeddable.js +1 -1
- package/decorators/Embedded.d.ts +6 -12
- package/decorators/Entity.d.ts +20 -5
- package/decorators/Entity.js +0 -1
- package/decorators/Enum.d.ts +1 -1
- package/decorators/Formula.d.ts +1 -2
- package/decorators/Indexed.d.ts +10 -8
- package/decorators/Indexed.js +1 -1
- package/decorators/ManyToMany.d.ts +4 -2
- package/decorators/ManyToOne.d.ts +6 -2
- package/decorators/OneToMany.d.ts +4 -4
- package/decorators/OneToOne.d.ts +5 -1
- package/decorators/PrimaryKey.d.ts +2 -3
- package/decorators/Property.d.ts +1 -1
- package/decorators/Transactional.d.ts +1 -0
- package/decorators/Transactional.js +3 -3
- package/drivers/IDatabaseDriver.d.ts +8 -1
- package/entity/ArrayCollection.d.ts +4 -2
- package/entity/ArrayCollection.js +18 -6
- package/entity/Collection.d.ts +1 -2
- package/entity/Collection.js +16 -10
- package/entity/EntityAssigner.d.ts +1 -1
- package/entity/EntityAssigner.js +9 -1
- package/entity/EntityFactory.d.ts +6 -0
- package/entity/EntityFactory.js +21 -7
- package/entity/EntityHelper.js +8 -1
- package/entity/EntityLoader.d.ts +3 -2
- package/entity/EntityLoader.js +54 -35
- package/entity/EntityRepository.d.ts +1 -1
- package/entity/EntityValidator.js +1 -1
- package/entity/Reference.d.ts +8 -7
- package/entity/Reference.js +22 -1
- package/entity/WrappedEntity.js +1 -1
- package/entity/defineEntity.d.ts +537 -0
- package/entity/defineEntity.js +693 -0
- package/entity/index.d.ts +2 -0
- package/entity/index.js +2 -0
- package/entity/utils.d.ts +7 -0
- package/entity/utils.js +15 -3
- package/enums.d.ts +16 -3
- package/enums.js +13 -0
- package/errors.d.ts +6 -0
- package/errors.js +14 -0
- package/events/EventSubscriber.d.ts +3 -1
- package/hydration/ObjectHydrator.js +10 -2
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/metadata/EntitySchema.d.ts +6 -4
- package/metadata/EntitySchema.js +33 -19
- package/metadata/MetadataDiscovery.d.ts +0 -1
- package/metadata/MetadataDiscovery.js +51 -29
- package/metadata/MetadataStorage.js +1 -1
- package/metadata/MetadataValidator.js +4 -3
- package/package.json +5 -5
- package/platforms/Platform.d.ts +3 -1
- package/serialization/EntitySerializer.d.ts +2 -0
- package/serialization/EntitySerializer.js +1 -1
- package/serialization/SerializationContext.js +13 -10
- package/types/BigIntType.d.ts +9 -6
- package/types/BigIntType.js +3 -0
- package/types/BooleanType.d.ts +1 -1
- package/types/DecimalType.d.ts +6 -4
- package/types/DecimalType.js +1 -1
- package/types/DoubleType.js +1 -1
- package/typings.d.ts +72 -35
- package/typings.js +24 -4
- package/unit-of-work/ChangeSetComputer.js +3 -1
- package/unit-of-work/ChangeSetPersister.d.ts +4 -2
- package/unit-of-work/ChangeSetPersister.js +21 -11
- package/unit-of-work/UnitOfWork.d.ts +2 -1
- package/unit-of-work/UnitOfWork.js +71 -24
- package/utils/AbstractSchemaGenerator.js +5 -2
- package/utils/Configuration.d.ts +15 -5
- package/utils/Configuration.js +7 -7
- package/utils/ConfigurationLoader.d.ts +0 -2
- package/utils/ConfigurationLoader.js +2 -24
- package/utils/Cursor.d.ts +3 -3
- package/utils/Cursor.js +3 -0
- package/utils/DataloaderUtils.d.ts +7 -2
- package/utils/DataloaderUtils.js +38 -7
- package/utils/EntityComparator.d.ts +6 -2
- package/utils/EntityComparator.js +98 -59
- package/utils/QueryHelper.d.ts +6 -0
- package/utils/QueryHelper.js +48 -5
- package/utils/RawQueryFragment.d.ts +34 -0
- package/utils/RawQueryFragment.js +40 -1
- package/utils/TransactionManager.d.ts +65 -0
- package/utils/TransactionManager.js +220 -0
- package/utils/Utils.d.ts +11 -7
- package/utils/Utils.js +67 -33
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/upsert-utils.js +9 -1
package/utils/DataloaderUtils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Collection } from '../entity/Collection.js';
|
|
2
2
|
import { helper } from '../entity/wrap.js';
|
|
3
|
-
import { ReferenceKind } from '../enums.js';
|
|
4
3
|
import { Reference } from '../entity/Reference.js';
|
|
4
|
+
import { Utils } from './Utils.js';
|
|
5
5
|
export class DataloaderUtils {
|
|
6
6
|
/**
|
|
7
7
|
* Groups identified references by entity and returns a Map with the
|
|
@@ -118,7 +118,6 @@ export class DataloaderUtils {
|
|
|
118
118
|
// We need to populate the inverse side of the relationship in order to be able to later retrieve the PK(s) from its item(s)
|
|
119
119
|
populate: [
|
|
120
120
|
...(opts.populate === false ? [] : opts.populate ?? []),
|
|
121
|
-
...(opts.ref ? [':ref'] : []),
|
|
122
121
|
...Array.from(filterMap.keys()).filter(
|
|
123
122
|
// We need to do so only if the inverse side is a collection, because we can already retrieve the PK from a reference without having to load it
|
|
124
123
|
prop => em.getMetadata(className).properties[prop]?.ref !== true),
|
|
@@ -149,15 +148,11 @@ export class DataloaderUtils {
|
|
|
149
148
|
else if (target) {
|
|
150
149
|
return target === collection.owner;
|
|
151
150
|
}
|
|
152
|
-
// FIXME https://github.com/mikro-orm/mikro-orm/issues/6031
|
|
153
|
-
if (!target && collection.property.kind === ReferenceKind.MANY_TO_MANY) {
|
|
154
|
-
throw new Error(`Inverse side is required for M:N relations with dataloader: ${collection.owner.constructor.name}.${collection.property.name}`);
|
|
155
|
-
}
|
|
156
151
|
return false;
|
|
157
152
|
};
|
|
158
153
|
}
|
|
159
154
|
/**
|
|
160
|
-
* Returns the collection dataloader batchLoadFn, which aggregates collections by entity,
|
|
155
|
+
* Returns the 1:M collection dataloader batchLoadFn, which aggregates collections by entity,
|
|
161
156
|
* makes one query per entity and maps each input collection to the corresponding result.
|
|
162
157
|
*/
|
|
163
158
|
static getColBatchLoadFn(em) {
|
|
@@ -179,4 +174,40 @@ export class DataloaderUtils {
|
|
|
179
174
|
});
|
|
180
175
|
};
|
|
181
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Returns the M:N collection dataloader batchLoadFn, which aggregates collections by entity,
|
|
179
|
+
* makes one query per entity and maps each input collection to the corresponding result.
|
|
180
|
+
*/
|
|
181
|
+
static getManyToManyColBatchLoadFn(em) {
|
|
182
|
+
return async (collsWithOpts) => {
|
|
183
|
+
const groups = new Map();
|
|
184
|
+
for (const [col, opts] of collsWithOpts) {
|
|
185
|
+
const key = `${col.property.targetMeta.className}.${col.property.name}|${JSON.stringify(opts ?? {})}`;
|
|
186
|
+
const value = groups.get(key) ?? [];
|
|
187
|
+
value.push([col, opts ?? {}]);
|
|
188
|
+
groups.set(key, value);
|
|
189
|
+
}
|
|
190
|
+
const ret = [];
|
|
191
|
+
for (const group of groups.values()) {
|
|
192
|
+
const prop = group[0][0].property;
|
|
193
|
+
const options = {};
|
|
194
|
+
const wrap = (cond) => ({ [prop.name]: cond });
|
|
195
|
+
const orderBy = Utils.asArray(group[0][1]?.orderBy).map(o => wrap(o));
|
|
196
|
+
const populate = wrap(group[0][1]?.populate);
|
|
197
|
+
const owners = group.map(c => c[0].owner);
|
|
198
|
+
const $or = [];
|
|
199
|
+
// a bit of a hack, but we need to prefix the key, since we have only a column name, not a property name
|
|
200
|
+
const alias = em.config.getNamingStrategy().aliasName(prop.pivotEntity, 0);
|
|
201
|
+
const fk = `${alias}.${Utils.getPrimaryKeyHash(prop.joinColumns)}`;
|
|
202
|
+
for (const c of group) {
|
|
203
|
+
$or.push({ $and: [c[1]?.where ?? {}, { [fk]: c[0].owner }] });
|
|
204
|
+
options.refresh ??= c[1]?.refresh;
|
|
205
|
+
}
|
|
206
|
+
options.where = wrap({ $or });
|
|
207
|
+
const r = await em.getEntityLoader().findChildrenFromPivotTable(owners, prop, options, orderBy, populate, group[0][1]?.ref);
|
|
208
|
+
ret.push(...r);
|
|
209
|
+
}
|
|
210
|
+
return ret;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
182
213
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EntityData, EntityDictionary, EntityMetadata, EntityProperty, IMetadataStorage } from '../typings.js';
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
|
-
type Comparator<T> = (a: T, b: T
|
|
3
|
+
type Comparator<T> = (a: T, b: T, options?: {
|
|
4
|
+
includeInverseSides?: boolean;
|
|
5
|
+
}) => EntityData<T>;
|
|
4
6
|
type ResultMapper<T> = (result: EntityData<T>) => EntityData<T> | null;
|
|
5
7
|
type SnapshotGenerator<T> = (entity: T) => EntityData<T>;
|
|
6
8
|
type CompositeKeyPart = string | CompositeKeyPart[];
|
|
@@ -18,7 +20,9 @@ export declare class EntityComparator {
|
|
|
18
20
|
/**
|
|
19
21
|
* Computes difference between two entities.
|
|
20
22
|
*/
|
|
21
|
-
diffEntities<T>(entityName: string, a: EntityData<T>, b: EntityData<T
|
|
23
|
+
diffEntities<T>(entityName: string, a: EntityData<T>, b: EntityData<T>, options?: {
|
|
24
|
+
includeInverseSides?: boolean;
|
|
25
|
+
}): EntityData<T>;
|
|
22
26
|
matching<T>(entityName: string, a: EntityData<T>, b: EntityData<T>): boolean;
|
|
23
27
|
/**
|
|
24
28
|
* Removes ORM specific code from entities and prepares it for serializing. Used before change set computation.
|
|
@@ -20,9 +20,9 @@ export class EntityComparator {
|
|
|
20
20
|
/**
|
|
21
21
|
* Computes difference between two entities.
|
|
22
22
|
*/
|
|
23
|
-
diffEntities(entityName, a, b) {
|
|
23
|
+
diffEntities(entityName, a, b, options) {
|
|
24
24
|
const comparator = this.getEntityComparator(entityName);
|
|
25
|
-
return Utils.callCompiledFunction(comparator, a, b);
|
|
25
|
+
return Utils.callCompiledFunction(comparator, a, b, options);
|
|
26
26
|
}
|
|
27
27
|
matching(entityName, a, b) {
|
|
28
28
|
const diff = this.diffEntities(entityName, a, b);
|
|
@@ -74,7 +74,7 @@ export class EntityComparator {
|
|
|
74
74
|
lines.push(` if (entity${this.wrap(pk)} != null && (entity${this.wrap(pk)}.__entity || entity${this.wrap(pk)}.__reference)) {`);
|
|
75
75
|
lines.push(` const pk = entity${this.wrap(pk)}.__helper.getPrimaryKey();`);
|
|
76
76
|
if (meta.properties[pk].targetMeta.compositePK) {
|
|
77
|
-
lines.push(` if (typeof pk === 'object') {`);
|
|
77
|
+
lines.push(` if (typeof pk === 'object' && pk != null) {`);
|
|
78
78
|
lines.push(` return [`);
|
|
79
79
|
for (const childPK of meta.properties[pk].targetMeta.primaryKeys) {
|
|
80
80
|
lines.push(` pk${this.wrap(childPK)},`);
|
|
@@ -155,6 +155,7 @@ export class EntityComparator {
|
|
|
155
155
|
const lines = [];
|
|
156
156
|
const context = new Map();
|
|
157
157
|
context.set('getCompositeKeyValue', (val) => Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.platform)));
|
|
158
|
+
context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
|
|
158
159
|
if (meta.primaryKeys.length > 1) {
|
|
159
160
|
lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
|
|
160
161
|
meta.primaryKeys.forEach(pk => {
|
|
@@ -170,14 +171,23 @@ export class EntityComparator {
|
|
|
170
171
|
}
|
|
171
172
|
else {
|
|
172
173
|
const pk = meta.primaryKeys[0];
|
|
173
|
-
|
|
174
|
+
const prop = meta.properties[pk];
|
|
175
|
+
if (prop.kind !== ReferenceKind.SCALAR) {
|
|
174
176
|
lines.push(` if (entity${this.wrap(pk)} != null && (entity${this.wrap(pk)}.__entity || entity${this.wrap(pk)}.__reference)) return entity${this.wrap(pk)}.__helper.getSerializedPrimaryKey();`);
|
|
175
177
|
}
|
|
176
178
|
const serializedPrimaryKey = meta.props.find(p => p.serializedPrimaryKey);
|
|
177
179
|
if (serializedPrimaryKey) {
|
|
178
180
|
lines.push(` return '' + entity.${serializedPrimaryKey.name};`);
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
else if (prop.customType) {
|
|
183
|
+
const convertorKey = this.registerCustomType(meta.properties[pk], context);
|
|
184
|
+
const idx = this.tmpIndex++;
|
|
185
|
+
lines.push(` const val_${idx} = convertToDatabaseValue_${convertorKey}(entity${this.wrap(pk)});`);
|
|
186
|
+
lines.push(` return getPrimaryKeyHash(val_${idx});`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(` return '' + entity${this.wrap(pk)};`);
|
|
190
|
+
}
|
|
181
191
|
}
|
|
182
192
|
const code = `// compiled pk serializer for entity ${meta.className}\n`
|
|
183
193
|
+ `return function(entity) {\n${lines.join('\n')}\n}`;
|
|
@@ -286,57 +296,73 @@ export class EntityComparator {
|
|
|
286
296
|
lines.push(`${padding} }`);
|
|
287
297
|
};
|
|
288
298
|
lines.push(` const mapped = {};`);
|
|
289
|
-
meta
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
299
|
+
const mapEntityProperties = (meta, padding = '') => {
|
|
300
|
+
for (const prop of meta.props) {
|
|
301
|
+
if (!prop.fieldNames) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (prop.targetMeta && prop.fieldNames.length > 1) {
|
|
305
|
+
lines.push(`${padding} if (${prop.fieldNames.map(field => `typeof ${this.propName(field)} === 'undefined'`).join(' && ')}) {`);
|
|
306
|
+
lines.push(`${padding} } else if (${prop.fieldNames.map(field => `${this.propName(field)} != null`).join(' && ')}) {`);
|
|
307
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.createCompositeKeyArray(prop)};`);
|
|
308
|
+
lines.push(...prop.fieldNames.map(field => `${padding} ${this.propName(field, 'mapped')} = true;`));
|
|
309
|
+
lines.push(`${padding} } else if (${prop.fieldNames.map(field => `${this.propName(field)} == null`).join(' && ')}) {\n ret${this.wrap(prop.name)} = null;`);
|
|
310
|
+
lines.push(...prop.fieldNames.map(field => `${padding} ${this.propName(field, 'mapped')} = true;`), ' }');
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (prop.embedded && (meta.embeddable || meta.properties[prop.embedded[0]].object)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (prop.runtimeType === 'boolean') {
|
|
317
|
+
lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
|
|
318
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : !!${this.propName(prop.fieldNames[0])};`);
|
|
319
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
320
|
+
lines.push(`${padding} }`);
|
|
321
|
+
}
|
|
322
|
+
else if (prop.runtimeType === 'Date' && !this.platform.isNumericProperty(prop)) {
|
|
323
|
+
lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
|
|
324
|
+
context.set('parseDate', (value) => this.platform.parseDate(value));
|
|
325
|
+
parseDate('ret' + this.wrap(prop.name), this.propName(prop.fieldNames[0]), padding);
|
|
326
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
327
|
+
lines.push(`${padding} }`);
|
|
328
|
+
}
|
|
329
|
+
else if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
330
|
+
const idx = this.tmpIndex++;
|
|
331
|
+
context.set(`mapEmbeddedResult_${idx}`, (data) => {
|
|
332
|
+
const item = parseJsonSafe(data);
|
|
333
|
+
if (Array.isArray(item)) {
|
|
334
|
+
return item.map(row => row == null ? row : this.getResultMapper(prop.type)(row));
|
|
335
|
+
}
|
|
336
|
+
return item == null ? item : this.getResultMapper(prop.type)(item);
|
|
337
|
+
});
|
|
338
|
+
lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
|
|
339
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])} == null ? ${this.propName(prop.fieldNames[0])} : mapEmbeddedResult_${idx}(${this.propName(prop.fieldNames[0])});`);
|
|
340
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
341
|
+
lines.push(`${padding} }`);
|
|
342
|
+
}
|
|
343
|
+
else if (prop.kind !== ReferenceKind.EMBEDDED) {
|
|
344
|
+
lines.push(`${padding} if (typeof ${this.propName(prop.fieldNames[0])} !== 'undefined') {`);
|
|
345
|
+
lines.push(`${padding} ret${this.wrap(prop.name)} = ${this.propName(prop.fieldNames[0])};`);
|
|
346
|
+
lines.push(`${padding} ${this.propName(prop.fieldNames[0], 'mapped')} = true;`);
|
|
347
|
+
lines.push(`${padding} }`);
|
|
348
|
+
}
|
|
331
349
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
};
|
|
351
|
+
if (meta.polymorphs && meta.discriminatorColumn) {
|
|
352
|
+
for (const polymorph of meta.polymorphs) {
|
|
353
|
+
const first = polymorph === meta.polymorphs[0];
|
|
354
|
+
lines.push(` ${first ? '' : 'else '}if (${this.propName(meta.discriminatorColumn)} == '${polymorph.discriminatorValue}') {`);
|
|
355
|
+
mapEntityProperties(polymorph, ' ');
|
|
336
356
|
lines.push(` }`);
|
|
337
357
|
}
|
|
338
|
-
|
|
339
|
-
|
|
358
|
+
lines.push(` else {`);
|
|
359
|
+
mapEntityProperties(meta, ' ');
|
|
360
|
+
lines.push(` }`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
mapEntityProperties(meta);
|
|
364
|
+
}
|
|
365
|
+
lines.push(` for (let k in result) { if (Object.hasOwn(result, k) && !mapped[k] && ret[k] === undefined) ret[k] = result[k]; }`);
|
|
340
366
|
const code = `// compiled mapper for entity ${meta.className}\n`
|
|
341
367
|
+ `return function(result) {\n const ret = {};\n${lines.join('\n')}\n return ret;\n}`;
|
|
342
368
|
const resultMapper = Utils.createFunction(context, code);
|
|
@@ -515,17 +541,30 @@ export class EntityComparator {
|
|
|
515
541
|
context.set('compareBuffers', compareBuffers);
|
|
516
542
|
context.set('compareObjects', compareObjects);
|
|
517
543
|
context.set('equals', equals);
|
|
518
|
-
meta.comparableProps
|
|
519
|
-
|
|
520
|
-
|
|
544
|
+
for (const prop of meta.comparableProps) {
|
|
545
|
+
// skip properties that are not hydrated
|
|
546
|
+
if (prop.hydrate !== false) {
|
|
547
|
+
lines.push(this.getPropertyComparator(prop, context));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// also compare 1:1 inverse sides, important for `factory.mergeData`
|
|
551
|
+
lines.push(`if (options?.includeInverseSides) {`);
|
|
552
|
+
for (const prop of meta.bidirectionalRelations) {
|
|
553
|
+
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && prop.hydrate !== false) {
|
|
554
|
+
lines.push(this.getPropertyComparator(prop, context));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
lines.push(`}`);
|
|
521
558
|
const code = `// compiled comparator for entity ${meta.className}\n`
|
|
522
|
-
+ `return function(last, current) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
|
|
559
|
+
+ `return function(last, current, options) {\n const diff = {};\n${lines.join('\n')}\n return diff;\n}`;
|
|
523
560
|
const comparator = Utils.createFunction(context, code);
|
|
524
561
|
this.comparators.set(entityName, comparator);
|
|
525
562
|
return comparator;
|
|
526
563
|
}
|
|
527
564
|
getGenericComparator(prop, cond) {
|
|
528
|
-
return ` if (current${prop}
|
|
565
|
+
return ` if (current${prop} === null && last${prop} === undefined) {\n` +
|
|
566
|
+
` diff${prop} = current${prop};\n` +
|
|
567
|
+
` } else if (current${prop} == null && last${prop} == null) {\n\n` +
|
|
529
568
|
` } else if ((current${prop} != null && last${prop} == null) || (current${prop} == null && last${prop} != null)) {\n` +
|
|
530
569
|
` diff${prop} = current${prop};\n` +
|
|
531
570
|
` } else if (${cond}) {\n` +
|
|
@@ -591,7 +630,7 @@ export class EntityComparator {
|
|
|
591
630
|
* perf: used to generate list of comparable properties during discovery, so we speed up the runtime comparison
|
|
592
631
|
*/
|
|
593
632
|
static isComparable(prop, root) {
|
|
594
|
-
const virtual = prop.persist === false || prop.generated;
|
|
633
|
+
const virtual = prop.persist === false || (prop.generated && !prop.primary);
|
|
595
634
|
const inverse = prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
596
635
|
const discriminator = prop.name === root.discriminatorColumn;
|
|
597
636
|
const collection = prop.kind === ReferenceKind.ONE_TO_MANY || prop.kind === ReferenceKind.MANY_TO_MANY;
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Dictionary, EntityMetadata, EntityProperty, FilterDef, FilterQuery } from '../typings.js';
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
4
|
+
/** @internal */
|
|
4
5
|
export declare class QueryHelper {
|
|
5
6
|
static readonly SUPPORTED_OPERATORS: string[];
|
|
6
7
|
static processParams(params: unknown): any;
|
|
7
8
|
static processObjectParams<T extends object>(params?: T): T;
|
|
9
|
+
/**
|
|
10
|
+
* converts `{ account: { $or: [ [Object], [Object] ] } }`
|
|
11
|
+
* to `{ $or: [ { account: [Object] }, { account: [Object] } ] }`
|
|
12
|
+
*/
|
|
13
|
+
static liftGroupOperators<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): string | undefined;
|
|
8
14
|
static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
|
|
9
15
|
static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
|
|
10
16
|
static getActiveFilters(entityName: string, options: Dictionary<boolean | Dictionary> | string[] | boolean, filters: Dictionary<FilterDef>): FilterDef[];
|
package/utils/QueryHelper.js
CHANGED
|
@@ -4,6 +4,7 @@ import { GroupOperator, ReferenceKind } from '../enums.js';
|
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
6
|
import { RawQueryFragment, isRaw } from './RawQueryFragment.js';
|
|
7
|
+
/** @internal */
|
|
7
8
|
export class QueryHelper {
|
|
8
9
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
9
10
|
static processParams(params) {
|
|
@@ -33,11 +34,52 @@ export class QueryHelper {
|
|
|
33
34
|
});
|
|
34
35
|
return params;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* converts `{ account: { $or: [ [Object], [Object] ] } }`
|
|
39
|
+
* to `{ $or: [ { account: [Object] }, { account: [Object] } ] }`
|
|
40
|
+
*/
|
|
41
|
+
static liftGroupOperators(where, meta, metadata, key) {
|
|
42
|
+
if (!Utils.isPlainObject(where)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const keys = Object.keys(where);
|
|
46
|
+
const groupOperator = keys.find(k => {
|
|
47
|
+
return Utils.isGroupOperator(k) && Array.isArray(where[k]) && where[k].every(cond => {
|
|
48
|
+
return Utils.isPlainObject(cond) && Object.keys(cond).every(k2 => {
|
|
49
|
+
if (Utils.isOperator(k2, false)) {
|
|
50
|
+
if (k2 === '$not') {
|
|
51
|
+
return Object.keys(cond[k2]).every(k3 => meta.primaryKeys.includes(k3));
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return meta.primaryKeys.includes(k2);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
if (groupOperator) {
|
|
60
|
+
return groupOperator;
|
|
61
|
+
}
|
|
62
|
+
for (const k of keys) {
|
|
63
|
+
const value = where[k];
|
|
64
|
+
const prop = meta.properties[k];
|
|
65
|
+
if (!prop || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
|
|
69
|
+
if (op) {
|
|
70
|
+
delete where[k];
|
|
71
|
+
where[op] = value[op].map((v) => {
|
|
72
|
+
return { [k]: v };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
36
78
|
static inlinePrimaryKeyObjects(where, meta, metadata, key) {
|
|
37
79
|
if (Array.isArray(where)) {
|
|
38
80
|
where.forEach((item, i) => {
|
|
39
81
|
if (this.inlinePrimaryKeyObjects(item, meta, metadata, key)) {
|
|
40
|
-
where[i] = Utils.getPrimaryKeyValues(item, meta
|
|
82
|
+
where[i] = Utils.getPrimaryKeyValues(item, meta, false);
|
|
41
83
|
}
|
|
42
84
|
});
|
|
43
85
|
}
|
|
@@ -45,9 +87,9 @@ export class QueryHelper {
|
|
|
45
87
|
return false;
|
|
46
88
|
}
|
|
47
89
|
if (meta.primaryKeys.every(pk => pk in where) && Utils.getObjectKeysSize(where) === meta.primaryKeys.length) {
|
|
48
|
-
return !!key && !GroupOperator[key] && Object.keys(where).every(k => !Utils.isPlainObject(where[k]) || Object.keys(where[k]).every(v => {
|
|
90
|
+
return !!key && !GroupOperator[key] && key !== '$not' && Object.keys(where).every(k => !Utils.isPlainObject(where[k]) || Object.keys(where[k]).every(v => {
|
|
49
91
|
if (Utils.isOperator(v, false)) {
|
|
50
|
-
return
|
|
92
|
+
return true;
|
|
51
93
|
}
|
|
52
94
|
if (meta.properties[k].primary && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(meta.properties[k].kind)) {
|
|
53
95
|
return this.inlinePrimaryKeyObjects(where[k], meta.properties[k].targetMeta, metadata, v);
|
|
@@ -58,7 +100,7 @@ export class QueryHelper {
|
|
|
58
100
|
Object.keys(where).forEach(k => {
|
|
59
101
|
const meta2 = metadata.find(meta.properties[k]?.type) || meta;
|
|
60
102
|
if (this.inlinePrimaryKeyObjects(where[k], meta2, metadata, k)) {
|
|
61
|
-
where[k] = Utils.getPrimaryKeyValues(where[k], meta2
|
|
103
|
+
where[k] = Utils.getPrimaryKeyValues(where[k], meta2, true);
|
|
62
104
|
}
|
|
63
105
|
});
|
|
64
106
|
return false;
|
|
@@ -69,6 +111,7 @@ export class QueryHelper {
|
|
|
69
111
|
const meta = metadata.find(entityName);
|
|
70
112
|
// inline PK-only objects in M:N queries, so we don't join the target entity when not needed
|
|
71
113
|
if (meta && root) {
|
|
114
|
+
QueryHelper.liftGroupOperators(where, meta, metadata);
|
|
72
115
|
QueryHelper.inlinePrimaryKeyObjects(where, meta, metadata);
|
|
73
116
|
}
|
|
74
117
|
if (platform.getConfig().get('ignoreUndefinedInQuery') && where && typeof where === 'object') {
|
|
@@ -118,7 +161,7 @@ export class QueryHelper {
|
|
|
118
161
|
value = QueryHelper.processCustomType(prop, value, platform, undefined, true);
|
|
119
162
|
}
|
|
120
163
|
const isJsonProperty = prop?.customType instanceof JsonType && Utils.isPlainObject(value) && !isRaw(value) && Object.keys(value)[0] !== '$eq';
|
|
121
|
-
if (isJsonProperty) {
|
|
164
|
+
if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
|
|
122
165
|
return this.processJsonCondition(o, value, [prop.fieldNames[0]], platform, aliased);
|
|
123
166
|
}
|
|
124
167
|
if (Array.isArray(value) && !Utils.isOperator(key) && !QueryHelper.isSupportedOperator(key) && !key.includes('?') && options.type !== 'orderBy') {
|
|
@@ -72,6 +72,24 @@ export declare const ALIAS_REPLACEMENT_RE = "\\[::alias::\\]";
|
|
|
72
72
|
* ```ts
|
|
73
73
|
* @Filter({ name: 'long', cond: () => ({ [raw('length(perex)')]: { $gt: 10000 } }) })
|
|
74
74
|
* ```
|
|
75
|
+
*
|
|
76
|
+
* The `raw` helper can be used within indexes and uniques to write database-agnostic SQL expressions. In that case, you can use `'??'` to tag your database identifiers (table name, column names, index name, ...) inside your expression, and pass those identifiers as a second parameter to the `raw` helper. Internally, those will automatically be quoted according to the database in use:
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("country")
|
|
80
|
+
* // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`country`)
|
|
81
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => raw(`create index ?? on ?? (??)`, ['custom_idx_on_name', table, columns.name]) })
|
|
82
|
+
* @Entity({ schema: 'library' })
|
|
83
|
+
* export class Author { ... }
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* You can also use the `quote` tag function to write database-agnostic SQL expressions. The end-result is the same as using the `raw` function regarding database identifiers quoting, only to have a more elegant expression syntax:
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` })
|
|
90
|
+
* @Entity({ schema: 'library' })
|
|
91
|
+
* export class Author { ... }
|
|
92
|
+
* ```
|
|
75
93
|
*/
|
|
76
94
|
export declare function raw<T extends object = any, R = any>(sql: EntityKey<T> | EntityKey<T>[] | AnyString | ((alias: string) => string) | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>): R;
|
|
77
95
|
/**
|
|
@@ -96,3 +114,19 @@ export declare namespace sql {
|
|
|
96
114
|
var upper: <T extends object>(key: string | ((alias: string) => string)) => string;
|
|
97
115
|
}
|
|
98
116
|
export declare function createSqlFunction<T extends object, R = string>(func: string, key: string | ((alias: string) => string)): R;
|
|
117
|
+
/**
|
|
118
|
+
* Tag function providing quoting of db identifiers (table name, columns names, index names, ...).
|
|
119
|
+
*
|
|
120
|
+
* Within the template literal on which the tag function is applied, all placeholders are considered to be database identifiers, and will thus be quoted as so according to the database in use.
|
|
121
|
+
*
|
|
122
|
+
* ```ts
|
|
123
|
+
* // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("name")
|
|
124
|
+
* // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`name`)
|
|
125
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` })
|
|
126
|
+
* @Entity({ schema: 'library' })
|
|
127
|
+
* export class Author { ... }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare function quote(expParts: readonly string[], ...values: (string | {
|
|
131
|
+
toString(): string;
|
|
132
|
+
})[]): any;
|
|
@@ -17,7 +17,12 @@ export class RawQueryFragment {
|
|
|
17
17
|
this.#key = `[raw]: ${this.sql} (#${RawQueryFragment.#index++})`;
|
|
18
18
|
}
|
|
19
19
|
as(alias) {
|
|
20
|
-
|
|
20
|
+
// TODO: to be removed in v7
|
|
21
|
+
/* v8 ignore next 3 */
|
|
22
|
+
if (alias.startsWith('`') || alias.startsWith('"')) {
|
|
23
|
+
return new RawQueryFragment(`${this.sql} as ${alias}`, this.params);
|
|
24
|
+
}
|
|
25
|
+
return new RawQueryFragment(`${this.sql} as ??`, [...this.params, alias]);
|
|
21
26
|
}
|
|
22
27
|
valueOf() {
|
|
23
28
|
throw new Error(`Trying to modify raw SQL fragment: '${this.sql}'`);
|
|
@@ -142,6 +147,24 @@ export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
|
|
|
142
147
|
* ```ts
|
|
143
148
|
* @Filter({ name: 'long', cond: () => ({ [raw('length(perex)')]: { $gt: 10000 } }) })
|
|
144
149
|
* ```
|
|
150
|
+
*
|
|
151
|
+
* The `raw` helper can be used within indexes and uniques to write database-agnostic SQL expressions. In that case, you can use `'??'` to tag your database identifiers (table name, column names, index name, ...) inside your expression, and pass those identifiers as a second parameter to the `raw` helper. Internally, those will automatically be quoted according to the database in use:
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("country")
|
|
155
|
+
* // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`country`)
|
|
156
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => raw(`create index ?? on ?? (??)`, ['custom_idx_on_name', table, columns.name]) })
|
|
157
|
+
* @Entity({ schema: 'library' })
|
|
158
|
+
* export class Author { ... }
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* You can also use the `quote` tag function to write database-agnostic SQL expressions. The end-result is the same as using the `raw` function regarding database identifiers quoting, only to have a more elegant expression syntax:
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` })
|
|
165
|
+
* @Entity({ schema: 'library' })
|
|
166
|
+
* export class Author { ... }
|
|
167
|
+
* ```
|
|
145
168
|
*/
|
|
146
169
|
export function raw(sql, params) {
|
|
147
170
|
if (sql instanceof RawQueryFragment) {
|
|
@@ -200,3 +223,19 @@ sql.ref = (...keys) => raw('??', [keys.join('.')]);
|
|
|
200
223
|
sql.now = (length) => raw('current_timestamp' + (length == null ? '' : `(${length})`));
|
|
201
224
|
sql.lower = (key) => createSqlFunction('lower', key);
|
|
202
225
|
sql.upper = (key) => createSqlFunction('upper', key);
|
|
226
|
+
/**
|
|
227
|
+
* Tag function providing quoting of db identifiers (table name, columns names, index names, ...).
|
|
228
|
+
*
|
|
229
|
+
* Within the template literal on which the tag function is applied, all placeholders are considered to be database identifiers, and will thus be quoted as so according to the database in use.
|
|
230
|
+
*
|
|
231
|
+
* ```ts
|
|
232
|
+
* // On postgres, will produce: create index "index custom_idx_on_name" on "library.author" ("name")
|
|
233
|
+
* // On mysql, will produce: create index `index custom_idx_on_name` on `library.author` (`name`)
|
|
234
|
+
* @Index({ name: 'custom_idx_on_name', expression: (table, columns) => quote`create index ${'custom_idx_on_name'} on ${table} (${columns.name})` })
|
|
235
|
+
* @Entity({ schema: 'library' })
|
|
236
|
+
* export class Author { ... }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function quote(expParts, ...values) {
|
|
240
|
+
return raw(expParts.join('??'), values);
|
|
241
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { EntityManager } from '../EntityManager.js';
|
|
2
|
+
import { type TransactionOptions } from '../enums.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages transaction lifecycle and propagation for EntityManager.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TransactionManager {
|
|
7
|
+
private readonly em;
|
|
8
|
+
constructor(em: EntityManager);
|
|
9
|
+
/**
|
|
10
|
+
* Main entry point for handling transactional operations with propagation support.
|
|
11
|
+
*/
|
|
12
|
+
handle<T>(cb: (em: EntityManager) => T | Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Executes the callback with the specified propagation type.
|
|
15
|
+
*/
|
|
16
|
+
private executeWithPropagation;
|
|
17
|
+
/**
|
|
18
|
+
* Suspends the current transaction and returns the suspended resources.
|
|
19
|
+
*/
|
|
20
|
+
private suspendTransaction;
|
|
21
|
+
/**
|
|
22
|
+
* Resumes a previously suspended transaction.
|
|
23
|
+
*/
|
|
24
|
+
private resumeTransaction;
|
|
25
|
+
/**
|
|
26
|
+
* Executes operation without transaction context.
|
|
27
|
+
*/
|
|
28
|
+
private executeWithoutTransaction;
|
|
29
|
+
/**
|
|
30
|
+
* Creates new independent transaction, suspending any existing one.
|
|
31
|
+
*/
|
|
32
|
+
private executeWithNewTransaction;
|
|
33
|
+
/**
|
|
34
|
+
* Creates new transaction context.
|
|
35
|
+
*/
|
|
36
|
+
private createNewTransaction;
|
|
37
|
+
/**
|
|
38
|
+
* Executes nested transaction with savepoint.
|
|
39
|
+
*/
|
|
40
|
+
private executeNestedTransaction;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a fork of the EntityManager with the given options.
|
|
43
|
+
*/
|
|
44
|
+
private createFork;
|
|
45
|
+
/**
|
|
46
|
+
* Determines if changes should be propagated to the upper context.
|
|
47
|
+
*/
|
|
48
|
+
private shouldPropagateToUpperContext;
|
|
49
|
+
/**
|
|
50
|
+
* Merges entities from fork to parent EntityManager.
|
|
51
|
+
*/
|
|
52
|
+
private mergeEntitiesToParent;
|
|
53
|
+
/**
|
|
54
|
+
* Registers a deletion handler to unset entity identities after flush.
|
|
55
|
+
*/
|
|
56
|
+
private registerDeletionHandler;
|
|
57
|
+
/**
|
|
58
|
+
* Processes transaction execution.
|
|
59
|
+
*/
|
|
60
|
+
private processTransaction;
|
|
61
|
+
/**
|
|
62
|
+
* Executes transaction workflow with entity synchronization.
|
|
63
|
+
*/
|
|
64
|
+
private executeTransactionFlow;
|
|
65
|
+
}
|