@mikro-orm/core 7.1.16-dev.10 → 7.1.16-dev.12
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 +202 -42
- package/MikroORM.d.ts +4 -0
- package/MikroORM.js +9 -0
- package/README.md +1 -0
- package/cache/FileCacheAdapter.js +1 -1
- package/connections/Connection.d.ts +10 -1
- package/connections/Connection.js +9 -0
- package/drivers/DatabaseDriver.d.ts +14 -5
- package/drivers/DatabaseDriver.js +145 -55
- package/entity/Collection.js +4 -2
- package/entity/EntityLoader.js +1 -1
- package/entity/EntityRepository.d.ts +4 -5
- package/entity/EntityRepository.js +2 -1
- package/entity/defineEntity.d.ts +17 -1
- package/entity/defineEntity.js +31 -0
- package/enums.d.ts +3 -1
- package/errors.d.ts +35 -0
- package/errors.js +87 -0
- package/exceptions.d.ts +5 -0
- package/exceptions.js +5 -0
- package/index.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +3 -0
- package/metadata/MetadataDiscovery.js +94 -7
- package/metadata/types.d.ts +19 -3
- package/package.json +1 -1
- package/platforms/Platform.d.ts +19 -1
- package/platforms/Platform.js +56 -0
- package/types/BigIntType.d.ts +1 -0
- package/types/BigIntType.js +23 -0
- package/types/DateTimeType.d.ts +1 -0
- package/types/DateTimeType.js +8 -0
- package/types/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/index.d.ts +2 -2
- package/typings.d.ts +47 -0
- package/typings.js +1 -0
- package/unit-of-work/UnitOfWork.js +1 -0
- package/utils/Configuration.d.ts +21 -1
- package/utils/Configuration.js +12 -1
- package/utils/Cursor.d.ts +2 -0
- package/utils/Cursor.js +43 -33
- package/utils/QueryHelper.d.ts +12 -0
- package/utils/QueryHelper.js +63 -0
- 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.js +1 -1
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +7 -2
- package/utils/env-vars.js +2 -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/types/StringType.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
|
-
/**
|
|
3
|
-
export class
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/** @internal */
|
|
3
|
+
export class BaseStringType extends Type {
|
|
4
|
+
options;
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.options = options;
|
|
8
|
+
// a defined `compareValues` replaces the inline `!==` comparator, so only provide it when normalization is configured
|
|
9
|
+
if (options.trim || options.case) {
|
|
10
|
+
this.compareValues = (a, b) => this.normalize(a) === this.normalize(b);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
convertToDatabaseValue(value) {
|
|
14
|
+
return this.normalize(value);
|
|
6
15
|
}
|
|
7
16
|
compareAsType() {
|
|
8
17
|
return 'string';
|
|
@@ -10,6 +19,27 @@ export class StringType extends Type {
|
|
|
10
19
|
ensureComparable() {
|
|
11
20
|
return false;
|
|
12
21
|
}
|
|
22
|
+
normalize(value) {
|
|
23
|
+
if (value == null) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
if (this.options.trim) {
|
|
27
|
+
value = value.trim();
|
|
28
|
+
}
|
|
29
|
+
if (this.options.case === 'upper') {
|
|
30
|
+
return value.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
if (this.options.case === 'lower') {
|
|
33
|
+
return value.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Maps a database VARCHAR column to a JS `string`. */
|
|
39
|
+
export class StringType extends BaseStringType {
|
|
40
|
+
getColumnType(prop, platform) {
|
|
41
|
+
return platform.getVarcharTypeDeclarationSQL(prop);
|
|
42
|
+
}
|
|
13
43
|
getDefaultLength(platform) {
|
|
14
44
|
return platform.getDefaultVarcharLength();
|
|
15
45
|
}
|
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/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
|
@@ -654,6 +654,16 @@ export type SerializeDTO<T, H extends string = never, E extends string = never,
|
|
|
654
654
|
};
|
|
655
655
|
type TargetKeys<T> = T extends EntityClass<infer P> ? keyof P : keyof T;
|
|
656
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
|
+
}
|
|
657
667
|
/** Table reference object passed to formula callbacks, including alias and schema information. */
|
|
658
668
|
export type FormulaTable = {
|
|
659
669
|
alias: string;
|
|
@@ -711,6 +721,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
|
|
|
711
721
|
export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
|
|
712
722
|
/** Callback for CHECK constraint expressions. Receives column mappings and table info. */
|
|
713
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;
|
|
714
726
|
/** Callback for trigger body expressions. Receives column mappings and table info. */
|
|
715
727
|
export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
|
|
716
728
|
/**
|
|
@@ -725,6 +737,28 @@ export interface CheckConstraint<T = any> {
|
|
|
725
737
|
property?: string;
|
|
726
738
|
expression: string | Raw | CheckCallback<T>;
|
|
727
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
|
+
}
|
|
728
762
|
/** Definition of a database trigger on a table. */
|
|
729
763
|
export interface TriggerDef<T = any> {
|
|
730
764
|
/** Trigger name. Auto-generated if omitted. */
|
|
@@ -1020,6 +1054,7 @@ export interface EntityProperty<Owner = any, Target = any> {
|
|
|
1020
1054
|
fixedOrderColumn?: string;
|
|
1021
1055
|
pivotTable: string;
|
|
1022
1056
|
pivotEntity: EntityClass<Target>;
|
|
1057
|
+
through?: ThroughRelation;
|
|
1023
1058
|
joinColumns: string[];
|
|
1024
1059
|
ownColumns: string[];
|
|
1025
1060
|
inverseJoinColumns: string[];
|
|
@@ -1172,6 +1207,9 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
|
|
|
1172
1207
|
}[];
|
|
1173
1208
|
checks: CheckConstraint<Entity>[];
|
|
1174
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';
|
|
1175
1213
|
repositoryClass?: string;
|
|
1176
1214
|
repository: () => EntityClass<EntityRepository<any>>;
|
|
1177
1215
|
hooks: {
|
|
@@ -1499,6 +1537,15 @@ type FilterDefResolved<T extends object = any> = {
|
|
|
1499
1537
|
entity?: EntityName<T> | EntityName<T>[];
|
|
1500
1538
|
args?: boolean;
|
|
1501
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
|
+
};
|
|
1502
1549
|
};
|
|
1503
1550
|
/** Definition of a query filter that can be registered globally or per-entity via `@Filter()`. */
|
|
1504
1551
|
export type FilterDef<T extends EntityName | readonly EntityName[] = any> = FilterDefResolved<EntityFromInput<T>> & {
|
package/typings.js
CHANGED
|
@@ -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
|
});
|
package/utils/Configuration.d.ts
CHANGED
|
@@ -233,6 +233,12 @@ export type MigrationsOptions = {
|
|
|
233
233
|
* @default true
|
|
234
234
|
*/
|
|
235
235
|
snapshot?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Update the snapshot from the database schema when running migrations up or down.
|
|
238
|
+
* Disable to keep the snapshot managed solely by `migration:create`.
|
|
239
|
+
* @default true
|
|
240
|
+
*/
|
|
241
|
+
snapshotOnMigrate?: boolean;
|
|
236
242
|
/** Custom name for the snapshot file. */
|
|
237
243
|
snapshotName?: string;
|
|
238
244
|
/**
|
|
@@ -433,7 +439,7 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
433
439
|
*/
|
|
434
440
|
filters: Dictionary<{
|
|
435
441
|
name?: string;
|
|
436
|
-
} & Omit<FilterDef, 'name'>>;
|
|
442
|
+
} & Omit<FilterDef, 'name' | 'rls'>>;
|
|
437
443
|
/**
|
|
438
444
|
* Metadata discovery configuration options.
|
|
439
445
|
* Controls how entities are discovered and validated.
|
|
@@ -474,6 +480,12 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
474
480
|
* @default false
|
|
475
481
|
*/
|
|
476
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';
|
|
477
489
|
/**
|
|
478
490
|
* Enable verbose logging of internal operations.
|
|
479
491
|
* @default false
|
|
@@ -834,6 +846,14 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
834
846
|
* @default false
|
|
835
847
|
*/
|
|
836
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;
|
|
837
857
|
/**
|
|
838
858
|
* Table names or patterns to skip during schema generation.
|
|
839
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,
|
|
@@ -84,6 +85,7 @@ const DEFAULTS = {
|
|
|
84
85
|
dropTables: true,
|
|
85
86
|
safe: false,
|
|
86
87
|
snapshot: true,
|
|
88
|
+
snapshotOnMigrate: true,
|
|
87
89
|
emit: 'ts',
|
|
88
90
|
// mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
|
|
89
91
|
fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
|
|
@@ -93,6 +95,7 @@ const DEFAULTS = {
|
|
|
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,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/QueryHelper.d.ts
CHANGED
|
@@ -37,6 +37,18 @@ 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>;
|
package/utils/QueryHelper.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } fro
|
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
6
6
|
import { isRaw, Raw } from './RawQueryFragment.js';
|
|
7
|
+
import { MetadataError } from '../errors.js';
|
|
7
8
|
/** @internal */
|
|
8
9
|
export class QueryHelper {
|
|
9
10
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
@@ -286,6 +287,68 @@ export class QueryHelper {
|
|
|
286
287
|
return filters[f];
|
|
287
288
|
});
|
|
288
289
|
}
|
|
290
|
+
/** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
|
|
291
|
+
static RLS_SENTINEL_PREFIX = '__mikro_rls_arg__';
|
|
292
|
+
/** @internal */
|
|
293
|
+
static RLS_SENTINEL_SUFFIX = '__';
|
|
294
|
+
/**
|
|
295
|
+
* Resolves an `rls` filter's condition to a static `FilterQuery`. Function conditions are called with a proxy `args`
|
|
296
|
+
* that yields a unique sentinel per accessed argument, real `type`/`entityName` strings (validated to not affect the
|
|
297
|
+
* result), and a poison proxy or `undefined` for the remaining runtime-only parameters.
|
|
298
|
+
*
|
|
299
|
+
* @internal
|
|
300
|
+
*/
|
|
301
|
+
static resolveRlsFilterCond(filter, accessed, entityName) {
|
|
302
|
+
if (!(filter.cond instanceof Function)) {
|
|
303
|
+
return filter.cond;
|
|
304
|
+
}
|
|
305
|
+
const args = new Proxy({}, {
|
|
306
|
+
get: (_target, prop) => {
|
|
307
|
+
if (typeof prop === 'symbol') {
|
|
308
|
+
// e.g. coercing `args` itself in a template literal triggers a `Symbol.toPrimitive` lookup
|
|
309
|
+
throw MetadataError.rlsFilterUnsupportedCond(filter.name);
|
|
310
|
+
}
|
|
311
|
+
accessed.add(prop);
|
|
312
|
+
return `${this.RLS_SENTINEL_PREFIX}${prop}${this.RLS_SENTINEL_SUFFIX}`;
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
const poison = new Proxy({}, {
|
|
316
|
+
get: () => {
|
|
317
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
// property access on the poison proxies throws, but equality/truthiness checks (`type === 'read'`,
|
|
321
|
+
// `entityName === 'X'`, `options ? a : b`) cannot be trapped — vary all three across the evaluations and require
|
|
322
|
+
// identical results, so a command-, entity-, or options-dependent condition cannot silently compile one branch.
|
|
323
|
+
// `entityName` uses the real class name (plus two derived-distinct variants) so an `=== '<name>'` check diverges;
|
|
324
|
+
// `options` alternates the poison proxy and `undefined` so a truthiness check flips. `em` stays poison throughout.
|
|
325
|
+
const name = entityName ?? `${this.RLS_SENTINEL_PREFIX}entity${this.RLS_SENTINEL_SUFFIX}`;
|
|
326
|
+
const evaluate = (type, entity, options) => {
|
|
327
|
+
let result;
|
|
328
|
+
try {
|
|
329
|
+
result = filter.cond(args, type, poison, options, entity);
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
// a raw TypeError from touching the `undefined` options/em must fail closed like the poison proxy does,
|
|
333
|
+
// but the descriptive MetadataErrors thrown above are already correct — let them surface unchanged
|
|
334
|
+
if (e instanceof MetadataError) {
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
337
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
338
|
+
}
|
|
339
|
+
if (result instanceof Promise) {
|
|
340
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
};
|
|
344
|
+
const read = evaluate('read', name, poison);
|
|
345
|
+
const update = evaluate('update', `${name}\0a`, undefined);
|
|
346
|
+
const del = evaluate('delete', `${name}\0b`, poison);
|
|
347
|
+
if (JSON.stringify(read) !== JSON.stringify(update) || JSON.stringify(read) !== JSON.stringify(del)) {
|
|
348
|
+
throw MetadataError.rlsFilterDependsOnRuntimeState(filter.name);
|
|
349
|
+
}
|
|
350
|
+
return read;
|
|
351
|
+
}
|
|
289
352
|
static mergePropertyFilters(propFilters, options) {
|
|
290
353
|
if (!options || !propFilters || options === true || propFilters === true) {
|
|
291
354
|
return options ?? propFilters;
|
|
@@ -61,6 +61,12 @@ export declare const ALIAS_REPLACEMENT_RE = "\\[::alias::\\]";
|
|
|
61
61
|
* await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
|
|
62
62
|
* ```
|
|
63
63
|
*
|
|
64
|
+
* Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
64
70
|
* You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
|
|
65
71
|
*
|
|
66
72
|
* ```ts
|
|
@@ -146,6 +146,12 @@ export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
|
|
|
146
146
|
* await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
|
|
147
147
|
* ```
|
|
148
148
|
*
|
|
149
|
+
* Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
149
155
|
* You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
|
|
150
156
|
*
|
|
151
157
|
* ```ts
|
|
@@ -191,13 +197,16 @@ export function raw(sql, params) {
|
|
|
191
197
|
return Utils.getPrimaryKeyHash(sql);
|
|
192
198
|
}
|
|
193
199
|
if (typeof params === 'object' && !Array.isArray(params)) {
|
|
194
|
-
const
|
|
200
|
+
const dict = params;
|
|
195
201
|
const objectParams = [];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
// single left-to-right scan keeps values in SQL-placeholder order while `::` casts and unknown tokens stay untouched
|
|
203
|
+
sql = sql.replace(/(?<!:):([$\w]+)(:(?!:))?/g, (match, key, identifier) => {
|
|
204
|
+
if (!Object.hasOwn(dict, key)) {
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
objectParams.push(dict[key]);
|
|
208
|
+
return identifier ? '??' : '?';
|
|
209
|
+
});
|
|
201
210
|
return new RawQueryFragment(sql, objectParams);
|
|
202
211
|
}
|
|
203
212
|
return new RawQueryFragment(sql, params);
|
|
@@ -17,13 +17,13 @@ export declare class RequestContext {
|
|
|
17
17
|
* If the handler is async, the return value needs to be awaited.
|
|
18
18
|
* Uses `AsyncLocalStorage.run()`, suitable for regular express style middlewares with a `next` callback.
|
|
19
19
|
*/
|
|
20
|
-
static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: CreateContextOptions): T;
|
|
20
|
+
static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: ((name: string) => CreateContextOptions) | CreateContextOptions): T;
|
|
21
21
|
/**
|
|
22
22
|
* Creates new RequestContext instance and runs the code inside its domain.
|
|
23
23
|
* If the handler is async, the return value needs to be awaited.
|
|
24
24
|
* Uses `AsyncLocalStorage.enterWith()`, suitable for elysia style middlewares without a `next` callback.
|
|
25
25
|
*/
|
|
26
|
-
static enter(em: EntityManager | EntityManager[], options?: CreateContextOptions): void;
|
|
26
|
+
static enter(em: EntityManager | EntityManager[], options?: ((name: string) => CreateContextOptions) | CreateContextOptions): void;
|
|
27
27
|
/**
|
|
28
28
|
* Returns current RequestContext (if available).
|
|
29
29
|
*/
|
package/utils/RequestContext.js
CHANGED
|
@@ -50,10 +50,19 @@ export class RequestContext {
|
|
|
50
50
|
static createContext(em, options = {}) {
|
|
51
51
|
const forks = new Map();
|
|
52
52
|
if (Array.isArray(em)) {
|
|
53
|
-
|
|
53
|
+
if (typeof options === 'function') {
|
|
54
|
+
for (const emInstance of em) {
|
|
55
|
+
forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options(emInstance.name) }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const emInstance of em) {
|
|
60
|
+
forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options }));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
else {
|
|
56
|
-
forks.set(em.name, em.fork({ useContext: true, ...options }));
|
|
65
|
+
forks.set(em.name, em.fork({ useContext: true, ...(typeof options === 'function' ? options(em.name) : options) }));
|
|
57
66
|
}
|
|
58
67
|
return new RequestContext(forks);
|
|
59
68
|
}
|
|
@@ -239,7 +239,7 @@ export class TransactionManager {
|
|
|
239
239
|
return TransactionContext.create(fork, () => fork.getConnection().transactional(async (trx) => {
|
|
240
240
|
fork.setTransactionContext(trx);
|
|
241
241
|
return this.executeTransactionFlow(fork, cb, propagateToUpperContext, em);
|
|
242
|
-
}, { ...options, eventBroadcaster }));
|
|
242
|
+
}, { sessionContext: fork.getTransactionSessionContext(), ...options, eventBroadcaster }));
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
245
|
* Executes transaction workflow with entity synchronization.
|
package/utils/Utils.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare function parseJsonSafe<T = unknown>(value: unknown): T;
|
|
|
33
33
|
export declare class Utils {
|
|
34
34
|
#private;
|
|
35
35
|
static readonly PK_SEPARATOR = "~~~";
|
|
36
|
+
/** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
|
|
37
|
+
static getRlsSettingName(filterName: string, argName: string): string;
|
|
36
38
|
/**
|
|
37
39
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
38
40
|
*/
|