@mikro-orm/core 7.2.0-dev.2 → 7.2.0-dev.20
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 +34 -5
- 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
package/MikroORM.d.ts
CHANGED
|
@@ -82,6 +82,10 @@ export declare class MikroORM<Driver extends IDatabaseDriver = IDatabaseDriver,
|
|
|
82
82
|
* Closes the database connection.
|
|
83
83
|
*/
|
|
84
84
|
close(force?: boolean): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Closes the database connection, allows using the ORM instance with `await using`.
|
|
87
|
+
*/
|
|
88
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
85
89
|
/**
|
|
86
90
|
* Gets the `MetadataStorage`.
|
|
87
91
|
*/
|
package/MikroORM.js
CHANGED
|
@@ -2,6 +2,7 @@ import { MetadataDiscovery } from './metadata/MetadataDiscovery.js';
|
|
|
2
2
|
import { MetadataStorage } from './metadata/MetadataStorage.js';
|
|
3
3
|
import { Configuration } from './utils/Configuration.js';
|
|
4
4
|
import { loadEnvironmentVars } from './utils/env-vars.js';
|
|
5
|
+
import { clearRlsFilterDefsCache } from './utils/rls-utils.js';
|
|
5
6
|
import { Utils } from './utils/Utils.js';
|
|
6
7
|
import { colors } from './logging/colors.js';
|
|
7
8
|
async function tryRegisterExtension(name, pkg, extensions) {
|
|
@@ -159,6 +160,12 @@ export class MikroORM {
|
|
|
159
160
|
await this.config.getMetadataCacheAdapter()?.close?.();
|
|
160
161
|
await this.config.getResultCacheAdapter()?.close?.();
|
|
161
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Closes the database connection, allows using the ORM instance with `await using`.
|
|
165
|
+
*/
|
|
166
|
+
async [Symbol.asyncDispose]() {
|
|
167
|
+
await this.close();
|
|
168
|
+
}
|
|
162
169
|
/**
|
|
163
170
|
* Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter.
|
|
164
171
|
*/
|
|
@@ -189,6 +196,8 @@ export class MikroORM {
|
|
|
189
196
|
meta.root = this.#metadata.get(meta.root.class);
|
|
190
197
|
}
|
|
191
198
|
this.#metadata.decorate(this.em);
|
|
199
|
+
// the newly discovered entities may declare `rls` filters the cached lookup was built without
|
|
200
|
+
clearRlsFilterDefsCache(this.#metadata);
|
|
192
201
|
}
|
|
193
202
|
/**
|
|
194
203
|
* Gets the SchemaGenerator.
|
package/cache/CacheAdapter.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Interface for async-capable cache storage used by result cache and metadata cache. */
|
|
2
2
|
export interface CacheAdapter {
|
|
3
3
|
/**
|
|
4
|
-
* Gets the items under `name` key from the cache.
|
|
4
|
+
* Gets the items under `name` key from the cache. When `origin` is provided, adapters that
|
|
5
|
+
* track the entry origin should ignore entries cached from a different source file.
|
|
5
6
|
*/
|
|
6
|
-
get<T = any>(name: string): T | Promise<T | undefined> | undefined;
|
|
7
|
+
get<T = any>(name: string, origin?: string): T | Promise<T | undefined> | undefined;
|
|
7
8
|
/**
|
|
8
9
|
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
|
|
9
10
|
*/
|
|
@@ -24,9 +25,10 @@ export interface CacheAdapter {
|
|
|
24
25
|
/** Synchronous variant of CacheAdapter, used for metadata cache where async access is not needed. */
|
|
25
26
|
export interface SyncCacheAdapter extends CacheAdapter {
|
|
26
27
|
/**
|
|
27
|
-
* Gets the items under `name` key from the cache.
|
|
28
|
+
* Gets the items under `name` key from the cache. When `origin` is provided, adapters that
|
|
29
|
+
* track the entry origin should ignore entries cached from a different source file.
|
|
28
30
|
*/
|
|
29
|
-
get<T = any>(name: string): T | undefined;
|
|
31
|
+
get<T = any>(name: string, origin?: string): T | undefined;
|
|
30
32
|
/**
|
|
31
33
|
* Sets the item to the cache. `origin` is used for cache invalidation and should reflect the change in data.
|
|
32
34
|
*/
|
|
@@ -16,12 +16,17 @@ export class FileCacheAdapter {
|
|
|
16
16
|
/**
|
|
17
17
|
* @inheritDoc
|
|
18
18
|
*/
|
|
19
|
-
get(name) {
|
|
19
|
+
get(name, origin) {
|
|
20
20
|
const path = this.path(name);
|
|
21
21
|
if (!existsSync(path)) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
const payload = fs.readJSONSync(path);
|
|
25
|
+
// Two classes with the same name share the cache key, ignore entries cached
|
|
26
|
+
// from a different source file.
|
|
27
|
+
if (origin && fs.absolutePath(payload.origin, this.#baseDir) !== fs.absolutePath(origin, this.#baseDir)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
25
30
|
const hash = this.getHash(payload.origin);
|
|
26
31
|
if (!hash || payload.hash !== hash) {
|
|
27
32
|
return null;
|
|
@@ -71,7 +76,7 @@ export class FileCacheAdapter {
|
|
|
71
76
|
let path = typeof this.#options.combined === 'string' ? this.#options.combined : './metadata.json';
|
|
72
77
|
path = fs.normalizePath(this.#options.cacheDir, path);
|
|
73
78
|
this.#options.combined = path; // override in the options, so we can log it from the CLI in `cache:generate` command
|
|
74
|
-
writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined));
|
|
79
|
+
writeFileSync(path, JSON.stringify(this.#cache, null, this.#pretty ? 2 : undefined), { flush: true });
|
|
75
80
|
return path;
|
|
76
81
|
}
|
|
77
82
|
path(name) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
|
|
2
2
|
import type { LogContext, Logger } from '../logging/Logger.js';
|
|
3
3
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
4
|
-
import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
|
|
4
|
+
import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty, SessionContext } from '../typings.js';
|
|
5
5
|
import type { Routine } from '../metadata/Routine.js';
|
|
6
6
|
import type { Platform } from '../platforms/Platform.js';
|
|
7
7
|
import type { Type } from '../types/Type.js';
|
|
@@ -51,6 +51,13 @@ export declare abstract class Connection {
|
|
|
51
51
|
* This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
|
|
52
52
|
*/
|
|
53
53
|
executeDump(dump: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Returns the underlying database client the connection drives — e.g. the `pg` pool, the
|
|
56
|
+
* `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
|
|
57
|
+
* Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
|
|
58
|
+
* leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
|
|
59
|
+
*/
|
|
60
|
+
getNativeClient(): Promise<unknown>;
|
|
54
61
|
protected onConnect(): Promise<void>;
|
|
55
62
|
/** Executes a callback inside a transaction, committing on success and rolling back on failure. */
|
|
56
63
|
transactional<T>(cb: (trx: Transaction) => Promise<T>, options?: {
|
|
@@ -59,6 +66,7 @@ export declare abstract class Connection {
|
|
|
59
66
|
ctx?: Transaction;
|
|
60
67
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
61
68
|
loggerContext?: LogContext;
|
|
69
|
+
sessionContext?: SessionContext;
|
|
62
70
|
}): Promise<T>;
|
|
63
71
|
/** Begins a new database transaction and returns the transaction context. */
|
|
64
72
|
begin(options?: {
|
|
@@ -67,6 +75,7 @@ export declare abstract class Connection {
|
|
|
67
75
|
ctx?: Transaction;
|
|
68
76
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
69
77
|
loggerContext?: LogContext;
|
|
78
|
+
sessionContext?: SessionContext;
|
|
70
79
|
}): Promise<Transaction>;
|
|
71
80
|
/** Commits the given transaction. */
|
|
72
81
|
commit(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
@@ -65,6 +65,15 @@ export class Connection {
|
|
|
65
65
|
async executeDump(dump) {
|
|
66
66
|
throw new Error(`Executing SQL dumps is not supported by current driver`);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the underlying database client the connection drives — e.g. the `pg` pool, the
|
|
70
|
+
* `better-sqlite3` database, or the `PGlite` instance — for vendor APIs MikroORM does not wrap.
|
|
71
|
+
* Each driver narrows the return type to its own client. Its lifecycle belongs to the ORM, so
|
|
72
|
+
* leave closing it to `orm.close()` unless you supplied the client yourself via `driverOptions`.
|
|
73
|
+
*/
|
|
74
|
+
async getNativeClient() {
|
|
75
|
+
throw new Error(`Accessing the native client is not supported by current driver`);
|
|
76
|
+
}
|
|
68
77
|
async onConnect() {
|
|
69
78
|
const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
|
|
70
79
|
if (this.type === 'write' && schemaGenerator) {
|
|
@@ -64,7 +64,23 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
|
|
|
64
64
|
orderBy: OrderDefinition<T>[];
|
|
65
65
|
where: FilterQuery<T>;
|
|
66
66
|
};
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
|
|
69
|
+
* condition have to agree on, or pagination skips rows at the null boundary. A placement the caller
|
|
70
|
+
* asked for wins where the platform can honor it, anything else follows the platform's own default,
|
|
71
|
+
* which is what the untouched `orderBy` will get.
|
|
72
|
+
*/
|
|
73
|
+
private parseCursorDirection;
|
|
74
|
+
/**
|
|
75
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
76
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
77
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
78
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
79
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
80
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
81
|
+
*/
|
|
82
|
+
private mapCursorOffset;
|
|
83
|
+
protected createCursorCondition<T extends object>(definition: (readonly [keyof T & string, QueryOrder])[], offsets: Dictionary[], inverse: boolean, meta: EntityMetadata<T>, fromJson?: boolean): FilterQuery<T>;
|
|
68
84
|
/** @internal */
|
|
69
85
|
mapDataToFieldNames(data: Dictionary, stringifyJsonArrays: boolean, properties?: Record<string, EntityProperty>, convertCustomTypes?: boolean, object?: boolean): Dictionary;
|
|
70
86
|
protected inlineEmbeddables<T extends object>(meta: EntityMetadata<T>, data: T, where?: boolean): void;
|
|
@@ -7,8 +7,10 @@ import { EntityManager } from '../EntityManager.js';
|
|
|
7
7
|
import { CursorError, ValidationError } from '../errors.js';
|
|
8
8
|
import { DriverException } from '../exceptions.js';
|
|
9
9
|
import { helper } from '../entity/wrap.js';
|
|
10
|
+
import { Reference } from '../entity/Reference.js';
|
|
10
11
|
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
11
12
|
import { JsonType } from '../types/JsonType.js';
|
|
13
|
+
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
12
14
|
import { MikroORM } from '../MikroORM.js';
|
|
13
15
|
/** Abstract base class for all database drivers, implementing common driver logic. */
|
|
14
16
|
export class DatabaseDriver {
|
|
@@ -141,14 +143,32 @@ export class DatabaseDriver {
|
|
|
141
143
|
return !!val && typeof val === 'object' && key in val;
|
|
142
144
|
};
|
|
143
145
|
const createCursor = (val, key, inverse = false) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
const def = Reference.unwrapReference((isCursor(val, key) ? val[key] : val));
|
|
147
|
+
let offsets;
|
|
148
|
+
let fromJson = false;
|
|
149
|
+
// entity (and reference) instances are supported as cursors too, their properties are read the same way
|
|
150
|
+
if (Utils.isPlainObject(def) || Utils.isEntity(def)) {
|
|
151
|
+
// POJO values are already JS values, extract them ordered per the definition,
|
|
152
|
+
// without the JSON round trip `Cursor.for` + `Cursor.decode` would impose
|
|
153
|
+
offsets = definition.map(([key]) => {
|
|
154
|
+
if (def[key] === undefined) {
|
|
155
|
+
throw CursorError.missingValue(meta.className, key);
|
|
156
|
+
}
|
|
157
|
+
return def[key];
|
|
158
|
+
});
|
|
147
159
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
else {
|
|
161
|
+
try {
|
|
162
|
+
/* v8 ignore next */
|
|
163
|
+
offsets = def ? Cursor.decode(def) : [];
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
167
|
+
}
|
|
168
|
+
fromJson = true;
|
|
169
|
+
}
|
|
170
|
+
if (definition.length > 0 && definition.length === offsets.length) {
|
|
171
|
+
return this.createCursorCondition(definition, offsets, inverse, meta, fromJson);
|
|
152
172
|
}
|
|
153
173
|
/* v8 ignore next */
|
|
154
174
|
return {};
|
|
@@ -162,62 +182,194 @@ export class DatabaseDriver {
|
|
|
162
182
|
if (limit != null) {
|
|
163
183
|
options.limit = limit + (overfetch ? 1 : 0);
|
|
164
184
|
}
|
|
165
|
-
const createOrderBy = (prop, direction) => {
|
|
185
|
+
const createOrderBy = (prop, direction, properties = meta.properties) => {
|
|
186
|
+
const propMeta = properties[prop];
|
|
166
187
|
if (Utils.isPlainObject(direction)) {
|
|
188
|
+
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
167
189
|
const value = Utils.getObjectQueryKeys(direction).reduce((o, key) => {
|
|
168
|
-
Object.assign(o, createOrderBy(key, direction[key]));
|
|
190
|
+
Object.assign(o, createOrderBy(key, direction[key], childProps ?? {}));
|
|
169
191
|
return o;
|
|
170
192
|
}, {});
|
|
171
193
|
return { [prop]: value };
|
|
172
194
|
}
|
|
173
|
-
const desc
|
|
195
|
+
const { desc, nullsFirst, explicit } = this.parseCursorDirection(direction);
|
|
174
196
|
const dir = Utils.xor(desc, isLast) ? 'desc' : 'asc';
|
|
197
|
+
// only a requested placement is spelled out, an unqualified direction already lands where the
|
|
198
|
+
// condition expects it; backward pagination reverses the ordering, so the placement flips too
|
|
199
|
+
if (explicit) {
|
|
200
|
+
const nulls = Utils.xor(nullsFirst, isLast) ? 'first' : 'last';
|
|
201
|
+
return { [prop]: `${dir} nulls ${nulls}` };
|
|
202
|
+
}
|
|
175
203
|
return { [prop]: dir };
|
|
176
204
|
};
|
|
205
|
+
// the cursor condition is created at the driver level, after the EM already converted custom types
|
|
206
|
+
// in the user `where`, so we need to run the same conversion over it explicitly
|
|
207
|
+
const where = QueryHelper.processWhere({
|
|
208
|
+
where: ($and.length > 1 ? { $and } : { ...$and[0] }),
|
|
209
|
+
entityName: meta.class,
|
|
210
|
+
metadata: this.metadata,
|
|
211
|
+
platform: this.platform,
|
|
212
|
+
convertCustomTypes: options.convertCustomTypes,
|
|
213
|
+
});
|
|
177
214
|
return {
|
|
178
215
|
orderBy: definition.map(([prop, direction]) => createOrderBy(prop, direction)),
|
|
179
|
-
where
|
|
216
|
+
where,
|
|
180
217
|
};
|
|
181
218
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Resolves a leaf `orderBy` direction into the two flags the rewritten `orderBy` and the cursor
|
|
221
|
+
* condition have to agree on, or pagination skips rows at the null boundary. A placement the caller
|
|
222
|
+
* asked for wins where the platform can honor it, anything else follows the platform's own default,
|
|
223
|
+
* which is what the untouched `orderBy` will get.
|
|
224
|
+
*/
|
|
225
|
+
parseCursorDirection(direction) {
|
|
226
|
+
const dir = ('' + direction).toLowerCase();
|
|
227
|
+
const desc = direction === QueryOrderNumeric.DESC || dir.startsWith('desc');
|
|
228
|
+
const nullsFirst = dir.includes('nulls first');
|
|
229
|
+
const explicit = (nullsFirst || dir.includes('nulls last')) && this.platform.supportsNullsOrdering();
|
|
230
|
+
if (!explicit) {
|
|
231
|
+
return { desc, nullsFirst: this.platform.sortsNullsLowest() ? !desc : desc, explicit };
|
|
232
|
+
}
|
|
233
|
+
return { desc, nullsFirst, explicit };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Restores the JS value of a single cursor offset. String-cursor values (`fromJson`) are decoded
|
|
237
|
+
* JSON: types implementing `fromJSON` own the round trip, other custom types restore via
|
|
238
|
+
* `convertToJSValue`, with `Date` healing for date-like columns based on the property type (never
|
|
239
|
+
* based on the string shape alone). POJO values are already JS values and only date-like strings
|
|
240
|
+
* are healed. Values compared against a JSON document keep their serialized form instead, unless
|
|
241
|
+
* the platform preserves native date types inside JSON documents (mongo).
|
|
242
|
+
*/
|
|
243
|
+
mapCursorOffset(prop, value, insideJson, fromJson) {
|
|
244
|
+
if (Utils.isScalarReference(value)) {
|
|
245
|
+
value = value.unwrap();
|
|
246
|
+
}
|
|
247
|
+
// scalar direction on a relation orders by its primary key
|
|
248
|
+
if (Utils.isEntity(value, true)) {
|
|
249
|
+
value = helper(value).getPrimaryKey();
|
|
250
|
+
}
|
|
251
|
+
if (value == null) {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
// mongo preserves native date types inside JSON documents, restored JS values compare directly
|
|
255
|
+
if (insideJson && !this.platform.preservesDatesInsideJson()) {
|
|
256
|
+
// compared against the JSON document, which holds the JSON form of the database value
|
|
257
|
+
if (value instanceof Date) {
|
|
258
|
+
return value.toISOString();
|
|
259
|
+
}
|
|
260
|
+
if (prop?.customType && fromJson) {
|
|
261
|
+
const restored = prop.customType.fromJSON
|
|
262
|
+
? prop.customType.fromJSON(value, this.platform)
|
|
263
|
+
: prop.customType.convertToJSValue(value, this.platform);
|
|
264
|
+
// a restored `Date` compares against its ISO string in the document, everything else
|
|
265
|
+
// keeps the JS value and gets its single `convertToDatabaseValue` in `processWhere`
|
|
266
|
+
if (restored instanceof Date) {
|
|
267
|
+
const converted = prop.customType.convertToDatabaseValue(restored, this.platform, {
|
|
268
|
+
fromQuery: true,
|
|
269
|
+
key: prop.name,
|
|
270
|
+
mode: 'query',
|
|
271
|
+
});
|
|
272
|
+
if (converted instanceof Date) {
|
|
273
|
+
return converted.toISOString();
|
|
274
|
+
}
|
|
187
275
|
}
|
|
188
|
-
|
|
189
|
-
Object.assign(o, createCondition(key, direction[key], offset?.[key], eq, `${path}.${key}`));
|
|
190
|
-
return o;
|
|
191
|
-
}, {});
|
|
192
|
-
return { [prop]: value };
|
|
276
|
+
return restored;
|
|
193
277
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
if (prop?.customType) {
|
|
281
|
+
if (fromJson && prop.customType.fromJSON) {
|
|
282
|
+
// the type owns its JSON round trip
|
|
283
|
+
return prop.customType.fromJSON(value, this.platform);
|
|
199
284
|
}
|
|
200
|
-
|
|
201
|
-
|
|
285
|
+
// A string is either a serialized form (string cursor) or a hand-written one (POJO).
|
|
286
|
+
// Types whose JS value is a `Date` must receive one. The rest get the string first,
|
|
287
|
+
// so any sub-millisecond precision survives.
|
|
288
|
+
if (typeof value === 'string' &&
|
|
289
|
+
(prop.runtimeType === 'Date' || this.platform.getMappedType(prop.columnTypes?.[0] ?? '').runtimeType === 'Date')) {
|
|
290
|
+
if (prop.runtimeType !== 'Date') {
|
|
291
|
+
try {
|
|
292
|
+
return prop.customType.convertToJSValue(value, this.platform);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// the type cannot read the serialized string, fall back to the `Date` form
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return prop.customType.convertToJSValue(new Date(value), this.platform);
|
|
202
299
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
300
|
+
// serialized non-string values still need restoring; POJO values are already JS values
|
|
301
|
+
return fromJson ? prop.customType.convertToJSValue(value, this.platform) : value;
|
|
302
|
+
}
|
|
303
|
+
if (typeof value === 'string' && prop?.runtimeType === 'Date') {
|
|
304
|
+
return new Date(value);
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
createCursorCondition(definition, offsets, inverse, meta, fromJson = false) {
|
|
309
|
+
const createCondition = (prop, direction, offset, eq = false, path = prop, properties = meta.properties, insideJson = false, nullable = false) => {
|
|
310
|
+
const propMeta = properties[prop];
|
|
311
|
+
// nullable relations and embeddables null out their joined columns, and a formula can yield null unannounced
|
|
312
|
+
nullable ||= !!propMeta?.nullable || !!propMeta?.formula;
|
|
313
|
+
if (Utils.isPlainObject(direction)) {
|
|
314
|
+
if (offset === undefined) {
|
|
315
|
+
throw CursorError.missingValue(meta.className, path);
|
|
316
|
+
}
|
|
317
|
+
// POJO cursors can carry entity, reference or embeddable class instances, read their properties directly
|
|
318
|
+
offset = Reference.unwrapReference(offset);
|
|
319
|
+
const childProps = propMeta?.kind === ReferenceKind.EMBEDDED ? propMeta.embeddedProps : propMeta?.targetMeta?.properties;
|
|
320
|
+
insideJson ||=
|
|
321
|
+
(propMeta?.kind === ReferenceKind.EMBEDDED && !!propMeta.object) || propMeta?.customType instanceof JsonType;
|
|
322
|
+
const keys = Utils.keys(direction);
|
|
323
|
+
const child = (key, childEq) => createCondition(key, direction[key],
|
|
324
|
+
// a null relation offset means the whole sort key is null, propagate it to the leaves
|
|
325
|
+
offset === null ? null : offset[key], childEq, `${path}.${key}`, childProps ?? {}, insideJson, nullable);
|
|
326
|
+
// the group's own keys are a keyset in their own right, so they decompose the same way the
|
|
327
|
+
// top level does; merging them into one object would compare every key independently instead
|
|
328
|
+
const lex = (index) => {
|
|
329
|
+
const key = keys[index];
|
|
330
|
+
if (index === keys.length - 1) {
|
|
331
|
+
return child(key, eq);
|
|
332
|
+
}
|
|
333
|
+
const atOrPast = child(key, true);
|
|
334
|
+
const tail = lex(index + 1);
|
|
335
|
+
// an unconstrained tail matches anything, leaving only the `at or past` prefix to constrain
|
|
336
|
+
const past = Utils.hasObjectKeys(tail) ? { $or: [child(key, false), tail] } : {};
|
|
337
|
+
if (!Utils.hasObjectKeys(past)) {
|
|
338
|
+
return atOrPast;
|
|
339
|
+
}
|
|
340
|
+
return Utils.hasObjectKeys(atOrPast) ? { $and: [atOrPast, past] } : past;
|
|
341
|
+
};
|
|
342
|
+
const value = keys.length > 0 ? lex(0) : {};
|
|
343
|
+
// an unconstrained group condition must not degrade to `{ [prop]: {} }`
|
|
344
|
+
return Utils.hasObjectKeys(value) ? { [prop]: value } : {};
|
|
206
345
|
}
|
|
346
|
+
const { desc: isDesc, nullsFirst } = this.parseCursorDirection(direction);
|
|
207
347
|
const operator = Utils.xor(isDesc, inverse) ? '$lt' : '$gt';
|
|
208
348
|
// For leaf-level properties, undefined means missing value
|
|
209
349
|
if (offset === undefined) {
|
|
210
350
|
throw CursorError.missingValue(meta.className, path);
|
|
211
351
|
}
|
|
352
|
+
// string-cursor values are client supplied, so a value the type cannot restore is an
|
|
353
|
+
// invalid cursor, letting callers map `CursorError` to a client error response
|
|
354
|
+
try {
|
|
355
|
+
offset = this.mapCursorOffset(propMeta, offset, insideJson, fromJson);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
if (!fromJson || error instanceof CursorError) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
throw CursorError.invalidCursor(meta.className, error);
|
|
362
|
+
}
|
|
212
363
|
// Handle null offset (intentional null cursor value)
|
|
213
364
|
if (offset === null) {
|
|
365
|
+
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
366
|
+
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
214
367
|
if (eq) {
|
|
215
|
-
//
|
|
216
|
-
|
|
368
|
+
// at-or-after a null sort key means every row when non-null rows follow the null block,
|
|
369
|
+
// so the tie-breaker in the `$or` sibling can reach past it
|
|
370
|
+
return hasItemsAfterNull ? {} : { [prop]: null };
|
|
217
371
|
}
|
|
218
372
|
// Strict comparison with null cursor value
|
|
219
|
-
// hasItemsAfterNull: forward + nullsFirst, or backward + nullsLast
|
|
220
|
-
const hasItemsAfterNull = Utils.xor(nullsFirst, inverse);
|
|
221
373
|
if (hasItemsAfterNull) {
|
|
222
374
|
return { [prop]: { $ne: null } };
|
|
223
375
|
}
|
|
@@ -225,7 +377,12 @@ export class DatabaseDriver {
|
|
|
225
377
|
return { [prop]: [] };
|
|
226
378
|
}
|
|
227
379
|
// Non-null offset
|
|
228
|
-
|
|
380
|
+
const condition = { [prop]: { [operator + (eq ? 'e' : '')]: offset } };
|
|
381
|
+
// null sort keys lie past the offset here, and no comparison ever matches them
|
|
382
|
+
if (nullable && !Utils.xor(nullsFirst, inverse)) {
|
|
383
|
+
return { $or: [condition, { [prop]: null }] };
|
|
384
|
+
}
|
|
385
|
+
return condition;
|
|
229
386
|
};
|
|
230
387
|
const [order, ...otherOrders] = definition;
|
|
231
388
|
const [offset, ...otherOffsets] = offsets;
|
|
@@ -233,13 +390,18 @@ export class DatabaseDriver {
|
|
|
233
390
|
if (!otherOrders.length) {
|
|
234
391
|
return createCondition(prop, direction, offset);
|
|
235
392
|
}
|
|
236
|
-
|
|
237
|
-
|
|
393
|
+
const atOrPast = createCondition(prop, direction, offset, true);
|
|
394
|
+
const past = {
|
|
238
395
|
$or: [
|
|
239
396
|
createCondition(prop, direction, offset),
|
|
240
|
-
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta),
|
|
397
|
+
this.createCursorCondition(otherOrders, otherOffsets, inverse, meta, fromJson),
|
|
241
398
|
],
|
|
242
399
|
};
|
|
400
|
+
// the `at or past` prefix is itself a group condition when it compares against nulls
|
|
401
|
+
if ('$or' in atOrPast) {
|
|
402
|
+
return { $and: [atOrPast, past] };
|
|
403
|
+
}
|
|
404
|
+
return { ...atOrPast, ...past };
|
|
243
405
|
}
|
|
244
406
|
/** @internal */
|
|
245
407
|
mapDataToFieldNames(data, stringifyJsonArrays, properties, convertCustomTypes, object) {
|
|
@@ -359,8 +521,8 @@ export class DatabaseDriver {
|
|
|
359
521
|
const props = prop.embeddedProps;
|
|
360
522
|
let unknownProp = false;
|
|
361
523
|
Object.keys(data[prop.name]).forEach(kk => {
|
|
362
|
-
// explicitly allow `$exists`, `$eq`, `$ne` and `$
|
|
363
|
-
const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch'].includes(f));
|
|
524
|
+
// explicitly allow `$exists`, `$eq`, `$ne`, `$elemMatch` and `$all` operators here as they can't be misused this way
|
|
525
|
+
const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && !['$exists', '$ne', '$eq', '$elemMatch', '$all'].includes(f));
|
|
364
526
|
if (operator) {
|
|
365
527
|
throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.class, prop.name, data);
|
|
366
528
|
}
|
|
@@ -345,6 +345,7 @@ export interface CountByOptions<T extends object> {
|
|
|
345
345
|
filters?: FilterOptions;
|
|
346
346
|
having?: FilterQuery<T>;
|
|
347
347
|
schema?: string;
|
|
348
|
+
connectionType?: ConnectionType;
|
|
348
349
|
flushMode?: FlushMode | `${FlushMode}`;
|
|
349
350
|
loggerContext?: LogContext;
|
|
350
351
|
logging?: LoggingOptions;
|
package/entity/Collection.js
CHANGED
|
@@ -117,9 +117,11 @@ export class Collection {
|
|
|
117
117
|
opts.orderBy = QueryHelper.mergeOrderBy(opts.orderBy, this.property.orderBy, this.property.targetMeta?.orderBy);
|
|
118
118
|
options.populate = (await em.preparePopulate(this.property.targetMeta.class, options));
|
|
119
119
|
const cond = (await em.applyFilters(this.property.targetMeta.class, where, options.filters ?? {}, 'read'));
|
|
120
|
-
|
|
120
|
+
// fall back to the ambient transaction context, or `withSessionContext` would wrap the pivot load in a
|
|
121
|
+
// second concurrent transaction (a deadlock with a single-connection pool) instead of joining the open one
|
|
122
|
+
const map = await em.withSessionContext(ctx ?? em.getTransactionContext(), trx => em
|
|
121
123
|
.getDriver()
|
|
122
|
-
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy,
|
|
124
|
+
.loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy, trx, options));
|
|
123
125
|
items = map[helper(this.owner).getSerializedPrimaryKey()].map((item) => em.merge(this.property.targetMeta.class, item, { convertCustomTypes: true }));
|
|
124
126
|
await em.populate(items, options.populate, options);
|
|
125
127
|
}
|
package/entity/EntityFactory.js
CHANGED
|
@@ -77,6 +77,12 @@ export class EntityFactory {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
data = { ...data };
|
|
80
|
+
if (options.newEntity && meta2.root.inheritanceType === 'sti' && meta2.discriminatorValue != null) {
|
|
81
|
+
const prop = meta2.properties[meta2.root.discriminatorColumn];
|
|
82
|
+
if (prop && prop.userDefined !== false) {
|
|
83
|
+
data[prop.name] ??= meta2.discriminatorValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
const entity = exists ?? this.createEntity(data, meta2, options);
|
|
81
87
|
wrapped = helper(entity);
|
|
82
88
|
wrapped.__processing = true;
|
package/entity/EntityLoader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, ObjectQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
|
|
2
2
|
import type { EntityManager } from '../EntityManager.js';
|
|
3
3
|
import { LoadStrategy, type LockMode, type PopulateHint, PopulatePath, type QueryOrderMap } from '../enums.js';
|
|
4
4
|
import type { InflightQueryAbortStrategy } from '../connections/Connection.js';
|
|
@@ -14,6 +14,8 @@ export interface EntityLoaderOptions<Entity, Fields extends string = never, Excl
|
|
|
14
14
|
where?: FilterQuery<Entity>;
|
|
15
15
|
/** Controls how `where` conditions are applied to populated relations. */
|
|
16
16
|
populateWhere?: PopulateHint | `${PopulateHint}`;
|
|
17
|
+
/** @see FindOptions.populateFilter */
|
|
18
|
+
populateFilter?: ObjectQuery<Entity>;
|
|
17
19
|
/** Ordering for populated relations. */
|
|
18
20
|
orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
|
|
19
21
|
/** Whether to reload already loaded entities. */
|
|
@@ -77,6 +79,10 @@ export declare class EntityLoader {
|
|
|
77
79
|
/** @internal */
|
|
78
80
|
findChildrenFromPivotTable<Entity extends object>(filtered: Entity[], prop: EntityProperty<Entity>, options: Required<EntityLoaderOptions<Entity>>, orderBy?: QueryOrderMap<Entity>[], populate?: PopulateOptions<Entity>, pivotJoin?: boolean): Promise<AnyEntity[][]>;
|
|
79
81
|
private extractChildCondition;
|
|
82
|
+
/** Extracts the part of `options.populateFilter` that applies to the given relation. */
|
|
83
|
+
private extractChildPopulateFilter;
|
|
84
|
+
/** Splits an extracted populate filter into the conditions on the populated entity and those on its own relations. */
|
|
85
|
+
private splitPopulateFilter;
|
|
80
86
|
private buildFields;
|
|
81
87
|
private getChildReferences;
|
|
82
88
|
private filterCollections;
|