@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.21
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 +42 -8
- package/EntityManager.js +256 -70
- package/MikroORM.d.ts +4 -0
- package/MikroORM.js +9 -0
- package/cache/CacheAdapter.d.ts +6 -4
- package/cache/FileCacheAdapter.d.ts +1 -1
- package/cache/FileCacheAdapter.js +7 -2
- package/connections/Connection.d.ts +10 -1
- package/connections/Connection.js +9 -0
- package/drivers/DatabaseDriver.d.ts +17 -1
- package/drivers/DatabaseDriver.js +203 -41
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/Collection.js +4 -2
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.d.ts +7 -1
- package/entity/EntityLoader.js +46 -11
- package/entity/EntityRepository.d.ts +4 -5
- package/entity/EntityRepository.js +7 -2
- package/entity/defineEntity.d.ts +48 -14
- package/entity/defineEntity.js +32 -1
- package/enums.d.ts +5 -1
- package/enums.js +2 -0
- package/errors.d.ts +36 -0
- package/errors.js +90 -0
- package/events/EventManager.js +6 -3
- package/exceptions.d.ts +5 -0
- package/exceptions.js +5 -0
- package/hydration/ObjectHydrator.d.ts +2 -0
- package/hydration/ObjectHydrator.js +15 -8
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.js +5 -2
- package/metadata/MetadataDiscovery.d.ts +3 -0
- package/metadata/MetadataDiscovery.js +161 -28
- package/metadata/MetadataProvider.js +1 -1
- package/metadata/MetadataStorage.d.ts +4 -3
- package/metadata/MetadataStorage.js +32 -2
- package/metadata/Routine.js +4 -1
- package/metadata/types.d.ts +19 -3
- package/naming-strategy/AbstractNamingStrategy.js +2 -1
- package/naming-strategy/NamingStrategy.d.ts +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +24 -3
- package/platforms/Platform.js +63 -1
- 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/StringType.d.ts +14 -3
- package/types/StringType.js +34 -4
- package/types/TextType.d.ts +2 -4
- package/types/TextType.js +2 -8
- package/types/Type.d.ts +11 -0
- package/types/Type.js +4 -4
- package/types/index.d.ts +2 -2
- package/typings.d.ts +58 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSet.js +10 -7
- package/unit-of-work/ChangeSetPersister.js +32 -20
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/AbstractMigrator.d.ts +1 -1
- package/utils/AbstractMigrator.js +3 -2
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +13 -2
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +44 -39
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +11 -4
- package/utils/QueryHelper.d.ts +17 -0
- package/utils/QueryHelper.js +100 -4
- package/utils/RawQueryFragment.d.ts +6 -0
- package/utils/RawQueryFragment.js +15 -6
- package/utils/RequestContext.d.ts +2 -2
- package/utils/RequestContext.js +11 -2
- package/utils/TransactionManager.d.ts +6 -0
- package/utils/TransactionManager.js +36 -3
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +36 -6
- package/utils/clone.js +6 -0
- package/utils/env-vars.js +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/rls-utils.d.ts +35 -0
- package/utils/rls-utils.js +97 -0
- package/utils/upsert-utils.d.ts +9 -1
- package/utils/upsert-utils.js +26 -3
|
@@ -3,7 +3,7 @@ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
|
3
3
|
import { helper } from '../entity/wrap.js';
|
|
4
4
|
import { ChangeSetType } from './ChangeSet.js';
|
|
5
5
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
6
|
-
import { Utils } from '../utils/Utils.js';
|
|
6
|
+
import { equals, Utils } from '../utils/Utils.js';
|
|
7
7
|
import { OptimisticLockError, ValidationError } from '../errors.js';
|
|
8
8
|
import { ReferenceKind } from '../enums.js';
|
|
9
9
|
/** @internal Executes change sets against the database, handling inserts, updates, and deletes. */
|
|
@@ -204,13 +204,14 @@ export class ChangeSetPersister {
|
|
|
204
204
|
}
|
|
205
205
|
checkConcurrencyKeys(meta, changeSet, cond) {
|
|
206
206
|
const tmp = [];
|
|
207
|
-
|
|
207
|
+
const keys = meta.getOwnConcurrencyCheckKeys();
|
|
208
|
+
for (const key of keys) {
|
|
208
209
|
cond[key] = changeSet.originalEntity[key];
|
|
209
210
|
if (changeSet.payload[key]) {
|
|
210
211
|
tmp.push(key);
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
|
-
if (tmp.length === 0 &&
|
|
214
|
+
if (tmp.length === 0 && keys.length > 0) {
|
|
214
215
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
215
216
|
}
|
|
216
217
|
}
|
|
@@ -230,11 +231,15 @@ export class ChangeSetPersister {
|
|
|
230
231
|
}
|
|
231
232
|
const res = await this.#driver.nativeUpdateMany(meta.class, cond, payload, options);
|
|
232
233
|
const map = new Map();
|
|
233
|
-
|
|
234
|
+
// returning rows are not mapped yet, so they are keyed by field names - we need to build the hash
|
|
235
|
+
// from those to be able to match them with `getSerializedPrimaryKey()` of the entity
|
|
236
|
+
const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
237
|
+
res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
|
|
234
238
|
for (const changeSet of changeSets) {
|
|
235
239
|
if (res.rows) {
|
|
236
240
|
const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
237
|
-
|
|
241
|
+
// STI batches can mix child types, so map through the change set's own metadata
|
|
242
|
+
this.mapReturnedValues(changeSet.entity, changeSet.payload, row, changeSet.meta);
|
|
238
243
|
}
|
|
239
244
|
changeSet.persisted = true;
|
|
240
245
|
}
|
|
@@ -299,32 +304,33 @@ export class ChangeSetPersister {
|
|
|
299
304
|
options = this.prepareOptions(meta, options, {
|
|
300
305
|
convertCustomTypes: false,
|
|
301
306
|
});
|
|
302
|
-
if (meta.
|
|
303
|
-
(!meta.
|
|
307
|
+
if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
|
|
308
|
+
(!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
|
|
304
309
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
305
310
|
}
|
|
306
|
-
if (meta.
|
|
311
|
+
if (meta.ownsVersionProperty()) {
|
|
307
312
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
308
313
|
}
|
|
309
314
|
this.checkConcurrencyKeys(meta, changeSet, cond);
|
|
310
315
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
311
316
|
}
|
|
312
317
|
async checkOptimisticLocks(meta, changeSets, options) {
|
|
313
|
-
|
|
314
|
-
|
|
318
|
+
const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
|
|
319
|
+
if (concurrencyCheckKeys.length === 0 &&
|
|
320
|
+
(!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
|
|
315
321
|
return;
|
|
316
322
|
}
|
|
317
323
|
// skip entity references as they don't have version values loaded
|
|
318
324
|
changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
|
|
325
|
+
const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
|
|
319
326
|
const $or = changeSets.map(cs => {
|
|
320
|
-
const cond = Utils.getPrimaryKeyCond(cs.originalEntity,
|
|
321
|
-
if (meta.
|
|
327
|
+
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
|
|
328
|
+
if (meta.ownsVersionProperty()) {
|
|
322
329
|
// @ts-ignore
|
|
323
330
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
324
331
|
}
|
|
325
332
|
return cond;
|
|
326
333
|
});
|
|
327
|
-
const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
328
334
|
options = this.prepareOptions(meta, options, {
|
|
329
335
|
fields: primaryKeys,
|
|
330
336
|
orderBy: meta.primaryKeys.reduce((o, pk) => {
|
|
@@ -332,9 +338,12 @@ export class ChangeSetPersister {
|
|
|
332
338
|
return o;
|
|
333
339
|
}, {}),
|
|
334
340
|
});
|
|
335
|
-
|
|
341
|
+
// TPT tables query their own metadata, as the version column might not live on the root table
|
|
342
|
+
const target = meta.inheritanceType === 'tpt' ? meta : meta.root;
|
|
343
|
+
const res = await this.#driver.find(target.class, { $or }, options);
|
|
336
344
|
if (res.length !== changeSets.length) {
|
|
337
|
-
|
|
345
|
+
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
346
|
+
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
338
347
|
const entity = changeSets.find(cs => {
|
|
339
348
|
return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
|
|
340
349
|
}).entity;
|
|
@@ -342,7 +351,7 @@ export class ChangeSetPersister {
|
|
|
342
351
|
}
|
|
343
352
|
}
|
|
344
353
|
checkOptimisticLock(meta, changeSet, res) {
|
|
345
|
-
if ((meta.
|
|
354
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
|
|
346
355
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
347
356
|
}
|
|
348
357
|
}
|
|
@@ -351,7 +360,7 @@ export class ChangeSetPersister {
|
|
|
351
360
|
* so we use a single query in case of both versioning and default values is used.
|
|
352
361
|
*/
|
|
353
362
|
async reloadVersionValues(meta, changeSets, options) {
|
|
354
|
-
const reloadProps = meta.
|
|
363
|
+
const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
|
|
355
364
|
if (changeSets[0].type === ChangeSetType.CREATE) {
|
|
356
365
|
for (const prop of meta.props) {
|
|
357
366
|
if (prop.persist === false) {
|
|
@@ -375,7 +384,8 @@ export class ChangeSetPersister {
|
|
|
375
384
|
changeSets.forEach(cs => {
|
|
376
385
|
Utils.keys(cs.payload).forEach(k => {
|
|
377
386
|
if (isRaw(cs.payload[k]) && isRaw(cs.entity[k])) {
|
|
378
|
-
|
|
387
|
+
// STI batches can mix child types, so the property might not exist on `meta`
|
|
388
|
+
returning.add(cs.meta.properties[k]);
|
|
379
389
|
}
|
|
380
390
|
});
|
|
381
391
|
});
|
|
@@ -400,12 +410,14 @@ export class ChangeSetPersister {
|
|
|
400
410
|
options = this.prepareOptions(meta, options, {
|
|
401
411
|
fields: Utils.unique(reloadProps.map(prop => prop.name)),
|
|
402
412
|
});
|
|
403
|
-
|
|
413
|
+
// a mixed STI batch shares one table but the child discriminator would filter out the siblings
|
|
414
|
+
const target = changeSets.some(cs => cs.meta !== meta) && meta.root.discriminatorColumn ? meta.root : meta;
|
|
415
|
+
const data = await this.#driver.find(target.class, { [pk]: { $in: pks } }, options);
|
|
404
416
|
const map = new Map();
|
|
405
417
|
data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, false, this.#platform, true), item));
|
|
406
418
|
for (const changeSet of changeSets) {
|
|
407
419
|
const data = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
|
|
408
|
-
this.#hydrator.hydrate(changeSet.entity, meta, data, this.#factory, 'full', false, true);
|
|
420
|
+
this.#hydrator.hydrate(changeSet.entity, changeSet.meta, data, this.#factory, 'full', false, true);
|
|
409
421
|
Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
|
|
410
422
|
}
|
|
411
423
|
}
|
|
@@ -124,7 +124,6 @@ export class UnitOfWork {
|
|
|
124
124
|
if (options?.newEntity) {
|
|
125
125
|
return entity;
|
|
126
126
|
}
|
|
127
|
-
const forceUndefined = this.#em.config.get('forceUndefined');
|
|
128
127
|
const wrapped = helper(entity);
|
|
129
128
|
if (options?.loaded && wrapped.__initialized && !wrapped.__onLoadFired) {
|
|
130
129
|
this.#loadedEntities.add(entity);
|
|
@@ -332,6 +331,7 @@ export class UnitOfWork {
|
|
|
332
331
|
let parentCs = changeSet.tptChangeSets.find(pc => pc.meta === current);
|
|
333
332
|
if (!parentCs) {
|
|
334
333
|
parentCs = new ChangeSet(entity, changeSet.type, {}, current);
|
|
334
|
+
parentCs.originalEntity = changeSet.originalEntity;
|
|
335
335
|
changeSet.tptChangeSets.splice(idx, 0, parentCs);
|
|
336
336
|
}
|
|
337
337
|
idx++;
|
|
@@ -472,6 +472,7 @@ export class UnitOfWork {
|
|
|
472
472
|
const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
|
|
473
473
|
await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
|
|
474
474
|
ctx: oldTx,
|
|
475
|
+
sessionContext: this.#em.getTransactionSessionContext(),
|
|
475
476
|
eventBroadcaster: new TransactionEventBroadcaster(this.#em),
|
|
476
477
|
loggerContext,
|
|
477
478
|
});
|
|
@@ -527,6 +528,10 @@ export class UnitOfWork {
|
|
|
527
528
|
if (prop.formula) {
|
|
528
529
|
delete referrer[prop.name];
|
|
529
530
|
}
|
|
531
|
+
else if (prop.primary) {
|
|
532
|
+
// the referrer's primary key contains the removed entity, so its identity cannot survive the removal
|
|
533
|
+
this.unsetIdentity(referrer);
|
|
534
|
+
}
|
|
530
535
|
else {
|
|
531
536
|
delete helper(referrer).__data[prop.name];
|
|
532
537
|
}
|
|
@@ -702,13 +707,14 @@ export class UnitOfWork {
|
|
|
702
707
|
payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
|
|
703
708
|
}
|
|
704
709
|
}
|
|
705
|
-
|
|
710
|
+
// the table declaring the version property or a concurrency check still needs its bump and lock check
|
|
711
|
+
if (!isCreate && Object.keys(payload).length === 0 && !current.hasOptimisticLock()) {
|
|
706
712
|
current = current.tptParent;
|
|
707
713
|
continue;
|
|
708
714
|
}
|
|
709
715
|
const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
|
|
716
|
+
cs.originalEntity = originalChangeSet.originalEntity;
|
|
710
717
|
if (current === meta) {
|
|
711
|
-
cs.originalEntity = originalChangeSet.originalEntity;
|
|
712
718
|
leafCs = cs;
|
|
713
719
|
}
|
|
714
720
|
else {
|
|
@@ -1205,7 +1211,8 @@ export class UnitOfWork {
|
|
|
1205
1211
|
const addToGroup = (cs) => {
|
|
1206
1212
|
// Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
|
|
1207
1213
|
if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) &&
|
|
1208
|
-
!Utils.hasObjectKeys(cs.payload)
|
|
1214
|
+
!Utils.hasObjectKeys(cs.payload) &&
|
|
1215
|
+
!cs.meta.hasOptimisticLock()) {
|
|
1209
1216
|
return;
|
|
1210
1217
|
}
|
|
1211
1218
|
const group = groups[cs.type];
|
|
@@ -103,7 +103,7 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
103
103
|
name: string;
|
|
104
104
|
path: string;
|
|
105
105
|
}): RunnableMigration;
|
|
106
|
-
protected initialize(MigrationClass: Constructor<Migration>, name
|
|
106
|
+
protected initialize(MigrationClass: Constructor<Migration>, name?: string): RunnableMigration;
|
|
107
107
|
/**
|
|
108
108
|
* Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
|
|
109
109
|
* If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
|
|
@@ -358,7 +358,8 @@ export class AbstractMigrator {
|
|
|
358
358
|
initialize(MigrationClass, name) {
|
|
359
359
|
const instance = new MigrationClass(this.driver, this.config);
|
|
360
360
|
return {
|
|
361
|
-
name
|
|
361
|
+
// the constructor name is the last resort, minifiers can mangle it (e.g. when bundling)
|
|
362
|
+
name: this.storage.getMigrationName(name ?? instance.name ?? MigrationClass.name),
|
|
362
363
|
up: afterRun => this.runner.run(instance, 'up', afterRun),
|
|
363
364
|
down: afterRun => this.runner.run(instance, 'down', afterRun),
|
|
364
365
|
};
|
|
@@ -409,7 +410,7 @@ export class AbstractMigrator {
|
|
|
409
410
|
if (this.options.migrationsList) {
|
|
410
411
|
return this.options.migrationsList.map(migration => {
|
|
411
412
|
if (typeof migration === 'function') {
|
|
412
|
-
return this.initialize(migration
|
|
413
|
+
return this.initialize(migration);
|
|
413
414
|
}
|
|
414
415
|
return this.initialize(migration.class, migration.name);
|
|
415
416
|
});
|
package/utils/Configuration.d.ts
CHANGED
|
@@ -439,7 +439,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
439
439
|
*/
|
|
440
440
|
filters: Dictionary<{
|
|
441
441
|
name?: string;
|
|
442
|
-
} & Omit<FilterDef, 'name'>>;
|
|
442
|
+
} & Omit<FilterDef, 'name' | 'rls'>>;
|
|
443
443
|
/**
|
|
444
444
|
* Metadata discovery configuration options.
|
|
445
445
|
* Controls how entities are discovered and validated.
|
|
@@ -480,6 +480,12 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
480
480
|
* @default false
|
|
481
481
|
*/
|
|
482
482
|
disableTransactions?: boolean;
|
|
483
|
+
/**
|
|
484
|
+
* How `em.setSessionContext()` session variables/role are applied for row level security.
|
|
485
|
+
* `'transaction'` (default) emits `set_config(..., true)` inside each transaction; `'connection'` applies them on every pooled connection acquire (PostgreSQL only).
|
|
486
|
+
* @default 'transaction'
|
|
487
|
+
*/
|
|
488
|
+
sessionContext?: 'transaction' | 'connection';
|
|
483
489
|
/**
|
|
484
490
|
* Enable verbose logging of internal operations.
|
|
485
491
|
* @default false
|
|
@@ -840,6 +846,14 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
840
846
|
* @default false
|
|
841
847
|
*/
|
|
842
848
|
ignoreRoutines?: boolean;
|
|
849
|
+
/**
|
|
850
|
+
* Leave row level security policies unmanaged. Declared policies are still created and RLS is still enabled or
|
|
851
|
+
* forced based on the entity metadata, but existing policies are never dropped or altered and RLS is never
|
|
852
|
+
* disabled or unforced — use this to protect hand-written policies from being removed when they are not
|
|
853
|
+
* mirrored in the entity definitions.
|
|
854
|
+
* @default false
|
|
855
|
+
*/
|
|
856
|
+
ignorePolicies?: boolean;
|
|
843
857
|
/**
|
|
844
858
|
* Table names or patterns to skip during schema generation.
|
|
845
859
|
* @default []
|
package/utils/Configuration.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Utils } from '../utils/Utils.js';
|
|
|
7
7
|
import { Routine } from '../metadata/Routine.js';
|
|
8
8
|
import { MetadataValidator } from '../metadata/MetadataValidator.js';
|
|
9
9
|
import { MetadataProvider } from '../metadata/MetadataProvider.js';
|
|
10
|
-
import { NotFoundError } from '../errors.js';
|
|
10
|
+
import { MetadataError, NotFoundError, ValidationError } from '../errors.js';
|
|
11
11
|
import { RequestContext } from './RequestContext.js';
|
|
12
12
|
import { DataloaderType, FlushMode, LoadStrategy, PopulateHint } from '../enums.js';
|
|
13
13
|
import { MemoryCacheAdapter } from '../cache/MemoryCacheAdapter.js';
|
|
@@ -71,6 +71,7 @@ const DEFAULTS = {
|
|
|
71
71
|
ensureDatabase: true,
|
|
72
72
|
ensureIndexes: false,
|
|
73
73
|
batchSize: 300,
|
|
74
|
+
sessionContext: 'transaction',
|
|
74
75
|
debug: false,
|
|
75
76
|
ignoreDeprecations: false,
|
|
76
77
|
verbose: false,
|
|
@@ -86,13 +87,15 @@ const DEFAULTS = {
|
|
|
86
87
|
snapshot: true,
|
|
87
88
|
snapshotOnMigrate: true,
|
|
88
89
|
emit: 'ts',
|
|
89
|
-
|
|
90
|
+
// mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
|
|
91
|
+
fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
|
|
90
92
|
},
|
|
91
93
|
schemaGenerator: {
|
|
92
94
|
createForeignKeyConstraints: true,
|
|
93
95
|
ignoreSchema: [],
|
|
94
96
|
ignoreTriggers: false,
|
|
95
97
|
ignoreRoutines: false,
|
|
98
|
+
ignorePolicies: false,
|
|
96
99
|
skipTables: [],
|
|
97
100
|
skipViews: [],
|
|
98
101
|
skipColumns: {},
|
|
@@ -392,7 +395,15 @@ export class Configuration {
|
|
|
392
395
|
}
|
|
393
396
|
this.#options.schema ??= this.#platform.getDefaultSchemaName();
|
|
394
397
|
this.#options.charset ??= this.#platform.getDefaultCharset();
|
|
398
|
+
// fail closed instead of silently applying no session state on drivers without the reserve hook (e.g. pglite)
|
|
399
|
+
if (this.#options.sessionContext === 'connection' && !this.#platform.supportsConnectionSessionContext()) {
|
|
400
|
+
throw ValidationError.connectionSessionContextNotSupported();
|
|
401
|
+
}
|
|
395
402
|
Object.keys(this.#options.filters).forEach(key => {
|
|
403
|
+
// global filters have no entity to attach a policy to, so `rls` is only valid on entity-scoped filters
|
|
404
|
+
if (this.#options.filters[key].rls) {
|
|
405
|
+
throw MetadataError.rlsFilterMustBeEntityScoped(key);
|
|
406
|
+
}
|
|
396
407
|
this.#options.filters[key].default ??= true;
|
|
397
408
|
});
|
|
398
409
|
if (!this.#options.filtersOnRelations) {
|
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,24 +149,18 @@ 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) {
|
|
150
160
|
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
151
161
|
}
|
|
152
162
|
static decode(value) {
|
|
153
|
-
return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'))
|
|
154
|
-
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/.exec(value)) {
|
|
155
|
-
return new Date(value);
|
|
156
|
-
}
|
|
157
|
-
return value;
|
|
158
|
-
});
|
|
163
|
+
return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
159
164
|
}
|
|
160
165
|
static getDefinition(meta, orderBy) {
|
|
161
166
|
return Utils.asArray(orderBy).flatMap(order => {
|
package/utils/DataloaderUtils.js
CHANGED
|
@@ -197,7 +197,8 @@ export class DataloaderUtils {
|
|
|
197
197
|
const prop = group[0][0].property;
|
|
198
198
|
const options = {};
|
|
199
199
|
const wrap = (cond) => ({ [prop.name]: cond });
|
|
200
|
-
|
|
200
|
+
// `findChildrenFromPivotTable` expects the `orderBy` relative to the target entity, so no wrapping here
|
|
201
|
+
const orderBy = Utils.asArray(group[0][1]?.orderBy);
|
|
201
202
|
const populate = wrap(group[0][1]?.populate);
|
|
202
203
|
const owners = group.map(c => c[0].owner);
|
|
203
204
|
const $or = [];
|
|
@@ -80,6 +80,8 @@ export declare class EntityComparator {
|
|
|
80
80
|
private getGenericComparator;
|
|
81
81
|
private getPropertyComparator;
|
|
82
82
|
private wrap;
|
|
83
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
84
|
+
private quote;
|
|
83
85
|
private safeKey;
|
|
84
86
|
/**
|
|
85
87
|
* Sets the toArray helper in the context if not already set.
|
|
@@ -164,7 +164,9 @@ export class EntityComparator {
|
|
|
164
164
|
const lines = [];
|
|
165
165
|
const context = new Map();
|
|
166
166
|
context.set('isEntityOrRef', (val) => Utils.isEntity(val, true));
|
|
167
|
-
context.set('getCompositeKeyValue', (val) =>
|
|
167
|
+
context.set('getCompositeKeyValue', (val) =>
|
|
168
|
+
// deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
|
|
169
|
+
Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform), true));
|
|
168
170
|
context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
|
|
169
171
|
if (meta.primaryKeys.length > 1) {
|
|
170
172
|
lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
|
|
@@ -447,6 +449,7 @@ export class EntityComparator {
|
|
|
447
449
|
const ret = [];
|
|
448
450
|
const padding = ' '.repeat(level * 2);
|
|
449
451
|
const idx = this.#tmpIndex++;
|
|
452
|
+
ret.push(`${padding}if (entity${entityKey} === null) ret${dataKey} = null;`);
|
|
450
453
|
ret.push(`${padding}if (Array.isArray(entity${entityKey})) {`);
|
|
451
454
|
ret.push(`${padding} ret${dataKey} = [];`);
|
|
452
455
|
ret.push(`${padding} entity${entityKey}.forEach((_, idx_${idx}) => {`);
|
|
@@ -584,7 +587,7 @@ export class EntityComparator {
|
|
|
584
587
|
}
|
|
585
588
|
}
|
|
586
589
|
else if (prop.polymorphic) {
|
|
587
|
-
const discriminatorMapKey = `discriminatorMapReverse_${prop.name}`;
|
|
590
|
+
const discriminatorMapKey = `discriminatorMapReverse_${this.safeKey(prop.name)}`;
|
|
588
591
|
const reverseMap = new Map();
|
|
589
592
|
for (const [key, value] of Object.entries(prop.discriminatorMap)) {
|
|
590
593
|
reverseMap.set(value, key);
|
|
@@ -755,10 +758,14 @@ export class EntityComparator {
|
|
|
755
758
|
return this.getGenericComparator(this.wrap(prop.name), `!equals(last${this.wrap(prop.name)}, current${this.wrap(prop.name)})`);
|
|
756
759
|
}
|
|
757
760
|
wrap(key) {
|
|
758
|
-
if (/^\[
|
|
761
|
+
if (/^\[idx_\d+]$/.exec(key)) {
|
|
759
762
|
return key;
|
|
760
763
|
}
|
|
761
|
-
return /^\w+$/.exec(key) ? `.${key}` : `[
|
|
764
|
+
return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
|
|
765
|
+
}
|
|
766
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
767
|
+
quote(key) {
|
|
768
|
+
return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
762
769
|
}
|
|
763
770
|
safeKey(key) {
|
|
764
771
|
return key.replace(/\W/g, '_');
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -37,9 +37,26 @@ export declare class QueryHelper {
|
|
|
37
37
|
static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
|
|
38
38
|
static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
|
|
39
39
|
static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
|
|
40
|
+
/** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
|
|
41
|
+
static readonly RLS_SENTINEL_PREFIX = "__mikro_rls_arg__";
|
|
42
|
+
/** @internal */
|
|
43
|
+
static readonly RLS_SENTINEL_SUFFIX = "__";
|
|
44
|
+
/**
|
|
45
|
+
* Resolves an `rls` filter's condition to a static `FilterQuery`. Function conditions are called with a proxy `args`
|
|
46
|
+
* that yields a unique sentinel per accessed argument, real `type`/`entityName` strings (validated to not affect the
|
|
47
|
+
* result), and a poison proxy or `undefined` for the remaining runtime-only parameters.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
static resolveRlsFilterCond(filter: FilterDef, accessed: Set<string>, entityName?: string): Dictionary;
|
|
40
52
|
static mergePropertyFilters(propFilters: FilterOptions | undefined, options: FilterOptions | undefined): FilterOptions | undefined;
|
|
41
53
|
static isFilterActive<T>(meta: EntityMetadata<T>, filterName: string, filter: FilterDef, options: Dictionary<boolean | Dictionary>): boolean;
|
|
42
54
|
static processCustomType<T extends object>(prop: EntityProperty<T>, cond: FilterQuery<T>, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
|
|
57
|
+
* resolve, so the custom types have to be applied positionally instead.
|
|
58
|
+
*/
|
|
59
|
+
private static processCompositeCustomTypes;
|
|
43
60
|
private static isSupportedOperator;
|
|
44
61
|
private static processJsonCondition;
|
|
45
62
|
static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;
|