@mikro-orm/core 7.2.0-dev.9 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +41 -7
- package/EntityManager.js +225 -45
- package/MikroORM.js +3 -0
- package/README.md +1 -0
- package/connections/Connection.d.ts +3 -1
- package/drivers/DatabaseDriver.d.ts +14 -5
- package/drivers/DatabaseDriver.js +151 -52
- 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 +12 -8
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +3 -0
- package/metadata/MetadataDiscovery.js +127 -17
- package/metadata/MetadataStorage.js +17 -1
- package/metadata/types.d.ts +19 -3
- package/package.json +1 -1
- package/platforms/Platform.d.ts +22 -3
- package/platforms/Platform.js +59 -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 +56 -2
- package/typings.js +24 -1
- package/unit-of-work/ChangeSetPersister.js +17 -13
- package/unit-of-work/UnitOfWork.js +11 -4
- package/utils/Configuration.d.ts +15 -1
- package/utils/Configuration.js +11 -1
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +43 -33
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +7 -3
- package/utils/QueryHelper.d.ts +12 -0
- package/utils/QueryHelper.js +75 -4
- package/utils/RawQueryFragment.d.ts +6 -0
- package/utils/RawQueryFragment.js +15 -6
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +14 -2
- package/utils/Utils.js +31 -4
- 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
package/types/TextType.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseStringType } from './StringType.js';
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import type { EntityProperty } from '../typings.js';
|
|
4
4
|
/** Maps a database TEXT column (unbounded length) to a JS `string`. */
|
|
5
|
-
export declare class TextType extends
|
|
5
|
+
export declare class TextType extends BaseStringType {
|
|
6
6
|
getColumnType(prop: EntityProperty, platform: Platform): string;
|
|
7
|
-
compareAsType(): string;
|
|
8
|
-
ensureComparable(): boolean;
|
|
9
7
|
}
|
package/types/TextType.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseStringType } from './StringType.js';
|
|
2
2
|
/** Maps a database TEXT column (unbounded length) to a JS `string`. */
|
|
3
|
-
export class TextType extends
|
|
3
|
+
export class TextType extends BaseStringType {
|
|
4
4
|
getColumnType(prop, platform) {
|
|
5
5
|
return platform.getTextTypeDeclarationSQL(prop);
|
|
6
6
|
}
|
|
7
|
-
compareAsType() {
|
|
8
|
-
return 'string';
|
|
9
|
-
}
|
|
10
|
-
ensureComparable() {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
7
|
}
|
package/types/Type.d.ts
CHANGED
|
@@ -59,6 +59,17 @@ export declare abstract class Type<JSType = string, DBType = JSType> {
|
|
|
59
59
|
* By default uses the runtime value.
|
|
60
60
|
*/
|
|
61
61
|
toJSON(value: JSType, platform: Platform): JSType | DBType;
|
|
62
|
+
/**
|
|
63
|
+
* Converts a value from its serialized JSON form back to its JS representation. Used when
|
|
64
|
+
* decoding cursor values. The input is what `toJSON` produced, after a `JSON.parse` round
|
|
65
|
+
* trip, and never an already restored JS value. Cursors are client supplied, so the value
|
|
66
|
+
* can be any JSON shape: validate it and throw for values the type cannot restore, and
|
|
67
|
+
* `findByCursor` surfaces the failure as a `CursorError`.
|
|
68
|
+
* Implementing this method also makes cursor encoding use `toJSON`. Without it, cursors
|
|
69
|
+
* carry the raw JS value, and decoding falls back to `convertToJSValue`, with type-based
|
|
70
|
+
* `Date` restoration for date-like columns.
|
|
71
|
+
*/
|
|
72
|
+
fromJSON?(value: unknown, platform: Platform): JSType;
|
|
62
73
|
/**
|
|
63
74
|
* Gets the SQL declaration snippet for a field of this type.
|
|
64
75
|
*/
|
package/types/Type.js
CHANGED
|
@@ -55,11 +55,11 @@ export class Type {
|
|
|
55
55
|
return prop.columnTypes?.[0] ?? platform.getTextTypeDeclarationSQL(prop);
|
|
56
56
|
}
|
|
57
57
|
static getType(cls) {
|
|
58
|
-
|
|
59
|
-
if (!Type.types.has(
|
|
60
|
-
Type.types.set(
|
|
58
|
+
// keyed by class reference, as minifiers can mangle two classes to the same name
|
|
59
|
+
if (!Type.types.has(cls)) {
|
|
60
|
+
Type.types.set(cls, new cls());
|
|
61
61
|
}
|
|
62
|
-
return Type.types.get(
|
|
62
|
+
return Type.types.get(cls);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Checks whether the argument is instance of `Type`.
|
package/types/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { IntervalType } from './IntervalType.js';
|
|
|
15
15
|
import { JsonType } from './JsonType.js';
|
|
16
16
|
import { MediumIntType } from './MediumIntType.js';
|
|
17
17
|
import { SmallIntType } from './SmallIntType.js';
|
|
18
|
-
import { StringType } from './StringType.js';
|
|
18
|
+
import { type StringTypeOptions, StringType } from './StringType.js';
|
|
19
19
|
import { TextType } from './TextType.js';
|
|
20
20
|
import { TimeType } from './TimeType.js';
|
|
21
21
|
import { TinyIntType } from './TinyIntType.js';
|
|
@@ -23,7 +23,7 @@ import { type IType, type TransformContext, Type } from './Type.js';
|
|
|
23
23
|
import { Uint8ArrayType } from './Uint8ArrayType.js';
|
|
24
24
|
import { UnknownType } from './UnknownType.js';
|
|
25
25
|
import { UuidType } from './UuidType.js';
|
|
26
|
-
export type { TransformContext, IType };
|
|
26
|
+
export type { TransformContext, IType, StringTypeOptions };
|
|
27
27
|
export { Type, DateType, TimeType, DateTimeType, BigIntType, BlobType, Uint8ArrayType, ArrayType, EnumArrayType, EnumType, JsonType, IntegerType, SmallIntType, TinyIntType, MediumIntType, FloatType, DoubleType, BooleanType, DecimalType, StringType, UuidType, TextType, UnknownType, IntervalType, CharacterType, };
|
|
28
28
|
/** Registry of all built-in type constructors, keyed by their short name (e.g., `types.integer`, `types.uuid`). */
|
|
29
29
|
export declare const types: {
|
package/typings.d.ts
CHANGED
|
@@ -314,6 +314,7 @@ export type OperatorMap<T> = {
|
|
|
314
314
|
$ne?: ExpandScalar<T> | readonly ExpandScalar<T>[] | Subquery;
|
|
315
315
|
$in?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
|
|
316
316
|
$nin?: readonly ExpandScalar<T>[] | readonly Primary<T>[] | Raw | Subquery;
|
|
317
|
+
$all?: readonly ExpandQuery<T>[];
|
|
317
318
|
$not?: ExpandQuery<T>;
|
|
318
319
|
$none?: ExpandQuery<T>;
|
|
319
320
|
$some?: ExpandQuery<T>;
|
|
@@ -478,12 +479,12 @@ type NonArrayObject = object & {
|
|
|
478
479
|
export type EntityDataProp<T, C extends boolean> = T extends Date ? string | Date : T extends Scalar ? T : T extends ScalarReference<infer U> ? EntityDataProp<U, C> : T extends {
|
|
479
480
|
__runtime?: infer Runtime;
|
|
480
481
|
__raw?: infer Raw;
|
|
481
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
482
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
482
483
|
/** Like `EntityDataProp` but used in `RequiredEntityData` context with required/optional key distinction. */
|
|
483
484
|
export type RequiredEntityDataProp<T, O, C extends boolean> = T extends Date ? string | Date : Exclude<T, null> extends RequiredNullable.Brand ? T | null : T extends Scalar ? T : T extends ScalarReference<infer U> ? RequiredEntityDataProp<U, O, C> : T extends {
|
|
484
485
|
__runtime?: infer Runtime;
|
|
485
486
|
__raw?: infer Raw;
|
|
486
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
487
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
487
488
|
/** Nested entity data shape for embedded or related entities within `EntityData`. */
|
|
488
489
|
export type EntityDataNested<T, C extends boolean = false> = T extends undefined ? never : T extends any[] ? Readonly<T> : EntityData<T, C> | ExpandEntityProp<T, C>;
|
|
489
490
|
type UnwrapScalarRef<T> = T extends ScalarReference<infer U> ? U : T;
|
|
@@ -653,6 +654,16 @@ export type SerializeDTO<T, H extends string = never, E extends string = never,
|
|
|
653
654
|
};
|
|
654
655
|
type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
|
|
655
656
|
type PropertyName<T> = IsUnknown<T> extends false ? TargetKeys<T> : string;
|
|
657
|
+
/** Resolved `through` option of a virtual to-one relation, populated during discovery. */
|
|
658
|
+
export interface ThroughRelation {
|
|
659
|
+
entity: EntityClass;
|
|
660
|
+
where?: FilterQuery<any>;
|
|
661
|
+
orderBy?: QueryOrderMap<any>[];
|
|
662
|
+
/** M:1 property on the `through` entity pointing back to the owner. */
|
|
663
|
+
ownerProperty: string;
|
|
664
|
+
/** M:1 property on the `through` entity pointing to the target, undefined when the target is selected directly. */
|
|
665
|
+
targetProperty?: string;
|
|
666
|
+
}
|
|
656
667
|
/** Table reference object passed to formula callbacks, including alias and schema information. */
|
|
657
668
|
export type FormulaTable = {
|
|
658
669
|
alias: string;
|
|
@@ -710,6 +721,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
|
|
|
710
721
|
export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
|
|
711
722
|
/** Callback for CHECK constraint expressions. Receives column mappings and table info. */
|
|
712
723
|
export type CheckCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
|
|
724
|
+
/** Callback for row level security policy expressions. Receives column mappings and table info. */
|
|
725
|
+
export type PolicyCallback<T> = (columns: SchemaColumns<T>, table: SchemaTable) => string | Raw;
|
|
713
726
|
/** Callback for trigger body expressions. Receives column mappings and table info. */
|
|
714
727
|
export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
715
728
|
/**
|
|
@@ -724,6 +737,28 @@ export interface CheckConstraint<T = any> {
|
|
|
724
737
|
property?: string;
|
|
725
738
|
expression: string | Raw | CheckCallback<T>;
|
|
726
739
|
}
|
|
740
|
+
/** Definition of a PostgreSQL row level security policy on a table. */
|
|
741
|
+
export interface PolicyDef<T = any> {
|
|
742
|
+
/** Policy name. Auto-generated if omitted. */
|
|
743
|
+
name?: string;
|
|
744
|
+
/** DML command the policy applies to. Defaults to `'all'`. */
|
|
745
|
+
command?: 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
746
|
+
/** Whether the policy is permissive (OR-combined) or restrictive (AND-combined). Defaults to `'permissive'`. */
|
|
747
|
+
type?: 'permissive' | 'restrictive';
|
|
748
|
+
/** Database roles the policy applies to. Defaults to `PUBLIC`. */
|
|
749
|
+
roles?: string[];
|
|
750
|
+
/** `USING` expression filtering visible rows. Can be a string, Raw query, or callback receiving column name mappings. */
|
|
751
|
+
using?: string | Raw | PolicyCallback<T>;
|
|
752
|
+
/** `WITH CHECK` expression validating written rows. Can be a string, Raw query, or callback receiving column name mappings. */
|
|
753
|
+
check?: string | Raw | PolicyCallback<T>;
|
|
754
|
+
}
|
|
755
|
+
/** Per-context database session state applied for row level security (session variables and role). */
|
|
756
|
+
export interface SessionContext {
|
|
757
|
+
/** Session variables set via `set_config`, typically referenced by RLS policies through `current_setting()`. `Date` values are serialized to ISO 8601. */
|
|
758
|
+
variables?: Dictionary<string | number | boolean | Date>;
|
|
759
|
+
/** Database role to switch to for the duration of the context (`set local role` / `set role`). */
|
|
760
|
+
role?: string;
|
|
761
|
+
}
|
|
727
762
|
/** Definition of a database trigger on a table. */
|
|
728
763
|
export interface TriggerDef<T = any> {
|
|
729
764
|
/** Trigger name. Auto-generated if omitted. */
|
|
@@ -1019,6 +1054,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
1019
1054
|
fixedOrderColumn?: string;
|
|
1020
1055
|
pivotTable: string;
|
|
1021
1056
|
pivotEntity: EntityClass<Target>;
|
|
1057
|
+
through?: ThroughRelation;
|
|
1022
1058
|
joinColumns: string[];
|
|
1023
1059
|
ownColumns: string[];
|
|
1024
1060
|
inverseJoinColumns: string[];
|
|
@@ -1050,6 +1086,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
|
|
|
1050
1086
|
constructor(meta?: Partial<EntityMetadata>);
|
|
1051
1087
|
addProperty(prop: Partial<EntityProperty<Entity>>): void;
|
|
1052
1088
|
removeProperty(name: string, sync?: boolean): void;
|
|
1089
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
1090
|
+
ownsVersionProperty(): boolean;
|
|
1091
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
1092
|
+
getOwnConcurrencyCheckKeys(): EntityKey<Entity>[];
|
|
1093
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
1094
|
+
hasOptimisticLock(): boolean;
|
|
1053
1095
|
getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
|
|
1054
1096
|
getPrimaryProp(): EntityProperty<Entity>;
|
|
1055
1097
|
/**
|
|
@@ -1165,6 +1207,9 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1165
1207
|
}[];
|
|
1166
1208
|
checks: CheckConstraint<Entity>[];
|
|
1167
1209
|
triggers: TriggerDef<Entity>[];
|
|
1210
|
+
policies: PolicyDef<Entity>[];
|
|
1211
|
+
/** Enables row level security on the table. `'force'` also enables it for the table owner. Implied by non-empty `policies`, unless set to `false`, which keeps the policies staged but RLS disabled. */
|
|
1212
|
+
rowLevelSecurity?: boolean | 'force';
|
|
1168
1213
|
repositoryClass?: string;
|
|
1169
1214
|
repository: () => EntityClass<EntityRepository<any>>;
|
|
1170
1215
|
hooks: {
|
|
@@ -1492,6 +1537,15 @@ type FilterDefResolved<T extends object = any> = {
|
|
|
1492
1537
|
entity?: EntityName<T> | EntityName<T>[];
|
|
1493
1538
|
args?: boolean;
|
|
1494
1539
|
strict?: boolean;
|
|
1540
|
+
/**
|
|
1541
|
+
* Also materializes this filter as a PostgreSQL row level security policy on the entity's table, and stages the
|
|
1542
|
+
* matching session variables when its params are enabled via `em.setFilterParams()`. The `cond` must be compilable
|
|
1543
|
+
* to a static expression (no access to `em`/`type`/`options`, not async). Each referenced argument maps to a session
|
|
1544
|
+
* variable named `mikro.<filterName>.<argName>`; pass `{ setting }` to override that name for a single-argument filter.
|
|
1545
|
+
*/
|
|
1546
|
+
rls?: boolean | {
|
|
1547
|
+
setting?: string;
|
|
1548
|
+
};
|
|
1495
1549
|
};
|
|
1496
1550
|
/** Definition of a query filter that can be registered globally or per-entity via `@Filter()`. */
|
|
1497
1551
|
export type FilterDef<T extends EntityName | readonly EntityName[] = any> = FilterDefResolved<EntityFromInput<T>> & {
|
package/typings.js
CHANGED
|
@@ -49,6 +49,7 @@ export class EntityMetadata {
|
|
|
49
49
|
this.uniques = [];
|
|
50
50
|
this.checks = [];
|
|
51
51
|
this.triggers = [];
|
|
52
|
+
this.policies = [];
|
|
52
53
|
this.referencingProperties = [];
|
|
53
54
|
this.concurrencyCheckKeys = new Set();
|
|
54
55
|
Object.assign(this, meta);
|
|
@@ -79,6 +80,25 @@ export class EntityMetadata {
|
|
|
79
80
|
this.sync();
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
84
|
+
ownsVersionProperty() {
|
|
85
|
+
if (!this.versionProperty) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return this.inheritanceType !== 'tpt' || !this.ownProps || this.ownProps.some(p => p.name === this.versionProperty);
|
|
89
|
+
}
|
|
90
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
91
|
+
getOwnConcurrencyCheckKeys() {
|
|
92
|
+
const keys = [...this.concurrencyCheckKeys];
|
|
93
|
+
if (this.inheritanceType !== 'tpt' || !this.ownProps) {
|
|
94
|
+
return keys;
|
|
95
|
+
}
|
|
96
|
+
return keys.filter(key => this.ownProps.some(p => p.name === key));
|
|
97
|
+
}
|
|
98
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
99
|
+
hasOptimisticLock() {
|
|
100
|
+
return this.ownsVersionProperty() || this.getOwnConcurrencyCheckKeys().length > 0;
|
|
101
|
+
}
|
|
82
102
|
getPrimaryProps(flatten = false) {
|
|
83
103
|
const pks = this.primaryKeys.map(pk => this.properties[pk]);
|
|
84
104
|
if (flatten) {
|
|
@@ -216,7 +236,10 @@ export class EntityMetadata {
|
|
|
216
236
|
if (config) {
|
|
217
237
|
const platform = config.getPlatform();
|
|
218
238
|
for (const prop of this.props) {
|
|
219
|
-
if (prop.enum &&
|
|
239
|
+
if (prop.enum &&
|
|
240
|
+
!prop.nativeEnumName &&
|
|
241
|
+
prop.items?.every(item => typeof item === 'string') &&
|
|
242
|
+
!['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
|
|
220
243
|
const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check');
|
|
221
244
|
const exists = this.checks.findIndex(check => check.name === name);
|
|
222
245
|
if (exists !== -1) {
|
|
@@ -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
|
}
|
|
@@ -303,32 +304,33 @@ export class ChangeSetPersister {
|
|
|
303
304
|
options = this.prepareOptions(meta, options, {
|
|
304
305
|
convertCustomTypes: false,
|
|
305
306
|
});
|
|
306
|
-
if (meta.
|
|
307
|
-
(!meta.
|
|
307
|
+
if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
|
|
308
|
+
(!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
|
|
308
309
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
309
310
|
}
|
|
310
|
-
if (meta.
|
|
311
|
+
if (meta.ownsVersionProperty()) {
|
|
311
312
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
312
313
|
}
|
|
313
314
|
this.checkConcurrencyKeys(meta, changeSet, cond);
|
|
314
315
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
315
316
|
}
|
|
316
317
|
async checkOptimisticLocks(meta, changeSets, options) {
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
|
|
319
|
+
if (concurrencyCheckKeys.length === 0 &&
|
|
320
|
+
(!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
|
|
319
321
|
return;
|
|
320
322
|
}
|
|
321
323
|
// skip entity references as they don't have version values loaded
|
|
322
324
|
changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
|
|
325
|
+
const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
|
|
323
326
|
const $or = changeSets.map(cs => {
|
|
324
|
-
const cond = Utils.getPrimaryKeyCond(cs.originalEntity,
|
|
325
|
-
if (meta.
|
|
327
|
+
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
|
|
328
|
+
if (meta.ownsVersionProperty()) {
|
|
326
329
|
// @ts-ignore
|
|
327
330
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
328
331
|
}
|
|
329
332
|
return cond;
|
|
330
333
|
});
|
|
331
|
-
const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
332
334
|
options = this.prepareOptions(meta, options, {
|
|
333
335
|
fields: primaryKeys,
|
|
334
336
|
orderBy: meta.primaryKeys.reduce((o, pk) => {
|
|
@@ -336,7 +338,9 @@ export class ChangeSetPersister {
|
|
|
336
338
|
return o;
|
|
337
339
|
}, {}),
|
|
338
340
|
});
|
|
339
|
-
|
|
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);
|
|
340
344
|
if (res.length !== changeSets.length) {
|
|
341
345
|
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
342
346
|
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
@@ -347,7 +351,7 @@ export class ChangeSetPersister {
|
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
checkOptimisticLock(meta, changeSet, res) {
|
|
350
|
-
if ((meta.
|
|
354
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
|
|
351
355
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
352
356
|
}
|
|
353
357
|
}
|
|
@@ -356,7 +360,7 @@ export class ChangeSetPersister {
|
|
|
356
360
|
* so we use a single query in case of both versioning and default values is used.
|
|
357
361
|
*/
|
|
358
362
|
async reloadVersionValues(meta, changeSets, options) {
|
|
359
|
-
const reloadProps = meta.
|
|
363
|
+
const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
|
|
360
364
|
if (changeSets[0].type === ChangeSetType.CREATE) {
|
|
361
365
|
for (const prop of meta.props) {
|
|
362
366
|
if (prop.persist === false) {
|
|
@@ -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];
|
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,
|
|
@@ -94,6 +95,7 @@ const DEFAULTS = {
|
|
|
94
95
|
ignoreSchema: [],
|
|
95
96
|
ignoreTriggers: false,
|
|
96
97
|
ignoreRoutines: false,
|
|
98
|
+
ignorePolicies: false,
|
|
97
99
|
skipTables: [],
|
|
98
100
|
skipViews: [],
|
|
99
101
|
skipColumns: {},
|
|
@@ -393,7 +395,15 @@ export class Configuration {
|
|
|
393
395
|
}
|
|
394
396
|
this.#options.schema ??= this.#platform.getDefaultSchemaName();
|
|
395
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
|
+
}
|
|
396
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
|
+
}
|
|
397
407
|
this.#options.filters[key].default ??= true;
|
|
398
408
|
});
|
|
399
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,12 +149,11 @@ export class Cursor {
|
|
|
138
149
|
*/
|
|
139
150
|
static for(meta, entity, orderBy) {
|
|
140
151
|
const definition = this.getDefinition(meta, orderBy);
|
|
141
|
-
return Cursor.encode(definition.map(([key]) => {
|
|
142
|
-
|
|
143
|
-
if (value === undefined) {
|
|
152
|
+
return Cursor.encode(definition.map(([key, direction]) => {
|
|
153
|
+
if (entity[key] === undefined) {
|
|
144
154
|
throw CursorError.missingValue(meta.className, key);
|
|
145
155
|
}
|
|
146
|
-
return
|
|
156
|
+
return this.serialize(meta.properties, entity, key, direction);
|
|
147
157
|
}));
|
|
148
158
|
}
|
|
149
159
|
static encode(value) {
|
package/utils/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.
|