@mikro-orm/core 7.1.0-dev.5 → 7.1.0-dev.50
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 +87 -12
- package/EntityManager.js +252 -40
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +45 -7
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +6 -3
- package/entity/Collection.js +15 -4
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +103 -30
- package/entity/EntityRepository.d.ts +27 -9
- package/entity/EntityRepository.js +12 -0
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +101 -21
- package/entity/defineEntity.js +17 -6
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +12 -0
- package/metadata/MetadataDiscovery.js +173 -22
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +237 -1
- package/metadata/Routine.d.ts +74 -0
- package/metadata/Routine.js +140 -0
- package/metadata/index.d.ts +1 -0
- package/metadata/index.js +1 -0
- package/metadata/types.d.ts +78 -9
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +18 -3
- package/platforms/Platform.js +58 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +399 -22
- package/typings.js +43 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +20 -5
- package/utils/AbstractMigrator.js +263 -28
- package/utils/AbstractSchemaGenerator.d.ts +1 -1
- package/utils/AbstractSchemaGenerator.js +4 -1
- package/utils/Configuration.d.ts +30 -0
- package/utils/Configuration.js +45 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/EntityComparator.js +1 -1
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/partition-utils.d.ts +17 -0
- package/utils/partition-utils.js +79 -0
- package/utils/upsert-utils.d.ts +2 -0
- package/utils/upsert-utils.js +26 -1
package/utils/Configuration.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Logger, type LoggerNamespace, type LoggerOptions } from '../loggin
|
|
|
6
6
|
import type { EntityManager } from '../EntityManager.js';
|
|
7
7
|
import type { Platform } from '../platforms/Platform.js';
|
|
8
8
|
import type { EntitySchema } from '../metadata/EntitySchema.js';
|
|
9
|
+
import { Routine } from '../metadata/Routine.js';
|
|
9
10
|
import { MetadataProvider } from '../metadata/MetadataProvider.js';
|
|
10
11
|
import type { MetadataStorage } from '../metadata/MetadataStorage.js';
|
|
11
12
|
import type { EventSubscriber } from '../events/EventSubscriber.js';
|
|
@@ -27,6 +28,10 @@ export declare class Configuration<D extends IDatabaseDriver = IDatabaseDriver,
|
|
|
27
28
|
get<T extends keyof Options<D, EM>, U extends Options<D, EM>[T]>(key: T, defaultValue?: U): U;
|
|
28
29
|
/** Returns all configuration options. */
|
|
29
30
|
getAll(): Options<D, EM>;
|
|
31
|
+
/** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
|
|
32
|
+
getRoutines(): readonly Routine[];
|
|
33
|
+
hasRoutine(routine: Routine): boolean;
|
|
34
|
+
private normaliseRoutines;
|
|
30
35
|
/**
|
|
31
36
|
* Overrides specified configuration value.
|
|
32
37
|
*/
|
|
@@ -238,6 +243,24 @@ export type MigrationsOptions = {
|
|
|
238
243
|
fileName?: (timestamp: string, name?: string) => string;
|
|
239
244
|
/** List of migration classes or objects to use instead of file-based discovery. */
|
|
240
245
|
migrationsList?: (MigrationObject | Constructor<Migration>)[];
|
|
246
|
+
/**
|
|
247
|
+
* Target schema used when running migrations. When set, the driver's "set current schema" statement
|
|
248
|
+
* (e.g. `SET search_path` on PostgreSQL) is issued before each migration, and the migration tracking
|
|
249
|
+
* table lives in this schema. Can be overridden per call via `migrator.up({ schema })`.
|
|
250
|
+
*
|
|
251
|
+
* Intended for per-deployment-one-schema setups (e.g. PR previews) and for applying a single
|
|
252
|
+
* unqualified migration to multiple schemas (see `includeWildcardSchema`). Not supported on MSSQL.
|
|
253
|
+
*/
|
|
254
|
+
schema?: string;
|
|
255
|
+
/**
|
|
256
|
+
* When set, entities with a wildcard schema (`schema: '*'`) are included in generated migrations.
|
|
257
|
+
* The emitted SQL is unqualified — and therefore safe to apply against any schema at runtime via
|
|
258
|
+
* `migrator.up({ schema })` — only when neither `options.schema` nor the ORM's `config.schema`
|
|
259
|
+
* is set. If `config.schema` is set, wildcard tables are qualified with it (useful for local
|
|
260
|
+
* dev runs); in that case, generate migrations from an environment where `config.schema` is unset.
|
|
261
|
+
* @default false
|
|
262
|
+
*/
|
|
263
|
+
includeWildcardSchema?: boolean;
|
|
241
264
|
};
|
|
242
265
|
/**
|
|
243
266
|
* Configuration options for database seeders.
|
|
@@ -390,6 +413,13 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
390
413
|
* Can be class references or instances.
|
|
391
414
|
*/
|
|
392
415
|
subscribers: Iterable<EventSubscriber | Constructor<EventSubscriber>>;
|
|
416
|
+
/**
|
|
417
|
+
* Stored procedures and functions, declared as {@link Routine} instances.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* routines: [HashUser, AddRecord]
|
|
421
|
+
*/
|
|
422
|
+
routines: Iterable<Routine>;
|
|
393
423
|
/**
|
|
394
424
|
* Global entity filters to apply.
|
|
395
425
|
* Filters are applied by default unless explicitly disabled.
|
package/utils/Configuration.js
CHANGED
|
@@ -4,6 +4,8 @@ import { NullHighlighter } from '../utils/NullHighlighter.js';
|
|
|
4
4
|
import { DefaultLogger } from '../logging/DefaultLogger.js';
|
|
5
5
|
import { colors } from '../logging/colors.js';
|
|
6
6
|
import { Utils } from '../utils/Utils.js';
|
|
7
|
+
import { Routine } from '../metadata/Routine.js';
|
|
8
|
+
import { MetadataValidator } from '../metadata/MetadataValidator.js';
|
|
7
9
|
import { MetadataProvider } from '../metadata/MetadataProvider.js';
|
|
8
10
|
import { NotFoundError } from '../errors.js';
|
|
9
11
|
import { RequestContext } from './RequestContext.js';
|
|
@@ -17,6 +19,7 @@ const DEFAULTS = {
|
|
|
17
19
|
entitiesTs: [],
|
|
18
20
|
extensions: [],
|
|
19
21
|
subscribers: [],
|
|
22
|
+
routines: [],
|
|
20
23
|
filters: {},
|
|
21
24
|
discovery: {
|
|
22
25
|
warnWhenNoEntities: true,
|
|
@@ -137,6 +140,8 @@ export class Configuration {
|
|
|
137
140
|
#platform;
|
|
138
141
|
#cache = new Map();
|
|
139
142
|
#extensions = new Map();
|
|
143
|
+
#routines = [];
|
|
144
|
+
#routinesNormalised = false;
|
|
140
145
|
constructor(options, validate = true) {
|
|
141
146
|
if (options.dynamicImportProvider) {
|
|
142
147
|
globalThis.dynamicImportProvider = options.dynamicImportProvider;
|
|
@@ -181,6 +186,42 @@ export class Configuration {
|
|
|
181
186
|
getAll() {
|
|
182
187
|
return this.#options;
|
|
183
188
|
}
|
|
189
|
+
/** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
|
|
190
|
+
getRoutines() {
|
|
191
|
+
this.normaliseRoutines();
|
|
192
|
+
return this.#routines;
|
|
193
|
+
}
|
|
194
|
+
hasRoutine(routine) {
|
|
195
|
+
this.normaliseRoutines();
|
|
196
|
+
return this.#routines.includes(routine);
|
|
197
|
+
}
|
|
198
|
+
normaliseRoutines() {
|
|
199
|
+
if (this.#routinesNormalised) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const validator = new MetadataValidator();
|
|
203
|
+
const seenKeys = new Set();
|
|
204
|
+
// Stage in a local array so a mid-loop throw doesn't leave `#routines` partially populated;
|
|
205
|
+
// a retry after the user fixes the offending entry would otherwise duplicate earlier ones.
|
|
206
|
+
const collected = [];
|
|
207
|
+
for (const item of this.#options.routines ?? []) {
|
|
208
|
+
if (!Routine.is(item)) {
|
|
209
|
+
throw new Error(`'routines' entry is not a stored routine declaration. Use a Routine class instance.`);
|
|
210
|
+
}
|
|
211
|
+
validator.validateRoutineDefinition(item);
|
|
212
|
+
// Case-fold to match `SchemaComparator.compareRoutines`, which lower-cases the key so
|
|
213
|
+
// dialect-specific folding (Oracle uppercases, PG lowercases) round-trips; otherwise a
|
|
214
|
+
// user could register `'Foo'` and `'foo'` here and have the comparator silently collapse them.
|
|
215
|
+
const key = ((item.schema ? `${item.schema}.` : '') + item.name).toLowerCase();
|
|
216
|
+
if (seenKeys.has(key)) {
|
|
217
|
+
throw new Error(`Duplicate routine '${key}' declared more than once in the 'routines' config. Routine names must be unique within a schema.`);
|
|
218
|
+
}
|
|
219
|
+
seenKeys.add(key);
|
|
220
|
+
collected.push(item);
|
|
221
|
+
}
|
|
222
|
+
this.#routines.push(...collected);
|
|
223
|
+
this.#routinesNormalised = true;
|
|
224
|
+
}
|
|
184
225
|
/**
|
|
185
226
|
* Overrides specified configuration value.
|
|
186
227
|
*/
|
|
@@ -324,6 +365,10 @@ export class Configuration {
|
|
|
324
365
|
metadataCache.enabled ??= useCache;
|
|
325
366
|
this.#options.clientUrl ??= this.#platform.getDefaultClientUrl();
|
|
326
367
|
this.#options.implicitTransactions ??= this.#platform.usesImplicitTransactions();
|
|
368
|
+
// Eagerly trigger routine validation so errors surface at init time, not on first call.
|
|
369
|
+
if (validate) {
|
|
370
|
+
this.getRoutines();
|
|
371
|
+
}
|
|
327
372
|
if (validate && metadataCache.enabled && !metadataCache.adapter) {
|
|
328
373
|
throw new Error('No metadata cache adapter specified, please fill in `metadataCache.adapter` option or use the async MikroORM.init() method which can autoload it.');
|
|
329
374
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Constructor, Primary, Ref } from '../typings.js';
|
|
2
|
-
import { Collection, type InitCollectionOptions } from '../entity/Collection.js';
|
|
2
|
+
import { Collection, type InitCollectionOptions, type LoadCountOptions } from '../entity/Collection.js';
|
|
3
3
|
import { type EntityManager } from '../EntityManager.js';
|
|
4
4
|
import { type LoadReferenceOptions } from '../entity/Reference.js';
|
|
5
5
|
type BatchLoadFn<K, V> = (keys: readonly K[]) => PromiseLike<ArrayLike<V | Error>>;
|
|
@@ -44,6 +44,15 @@ export declare class DataloaderUtils {
|
|
|
44
44
|
* makes one query per entity and maps each input collection to the corresponding result.
|
|
45
45
|
*/
|
|
46
46
|
static getManyToManyColBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<InitCollectionOptions<any, any>, 'dataloader'>?], any>;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
|
|
49
|
+
* by entity and relation, issues a single grouped count query per entity+options combination
|
|
50
|
+
* via `em.countBy()`, and maps each input collection to the corresponding count.
|
|
51
|
+
*
|
|
52
|
+
* For 1:M relations, groups by the FK property on the target entity.
|
|
53
|
+
* For M:N relations, groups by the owner FK on the pivot entity.
|
|
54
|
+
*/
|
|
55
|
+
static getCountBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<LoadCountOptions<any>, 'dataloader' | 'refresh'>?], number>;
|
|
47
56
|
static getDataLoader(): Promise<Constructor<{
|
|
48
57
|
load: (...args: unknown[]) => Promise<unknown>;
|
|
49
58
|
}>>;
|
package/utils/DataloaderUtils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Collection } from '../entity/Collection.js';
|
|
2
2
|
import { helper } from '../entity/wrap.js';
|
|
3
3
|
import { Reference } from '../entity/Reference.js';
|
|
4
|
+
import { ReferenceKind } from '../enums.js';
|
|
4
5
|
import { Utils } from './Utils.js';
|
|
5
6
|
export class DataloaderUtils {
|
|
6
7
|
static DataLoader;
|
|
@@ -216,6 +217,83 @@ export class DataloaderUtils {
|
|
|
216
217
|
return ret;
|
|
217
218
|
};
|
|
218
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
|
|
222
|
+
* by entity and relation, issues a single grouped count query per entity+options combination
|
|
223
|
+
* via `em.countBy()`, and maps each input collection to the corresponding count.
|
|
224
|
+
*
|
|
225
|
+
* For 1:M relations, groups by the FK property on the target entity.
|
|
226
|
+
* For M:N relations, groups by the owner FK on the pivot entity.
|
|
227
|
+
*/
|
|
228
|
+
static getCountBatchLoadFn(em) {
|
|
229
|
+
return async (collsWithOpts) => {
|
|
230
|
+
const groups = new Map();
|
|
231
|
+
const keys = [];
|
|
232
|
+
for (const [col, opts] of collsWithOpts) {
|
|
233
|
+
const prop = col.property;
|
|
234
|
+
let fkProp;
|
|
235
|
+
let countByClass;
|
|
236
|
+
let targetFilterProp;
|
|
237
|
+
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
238
|
+
fkProp = prop.mappedBy;
|
|
239
|
+
countByClass = prop.targetMeta.class;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// M:N: group by the owner FK on the pivot entity
|
|
243
|
+
const pivotMeta = em.getMetadata().get(prop.pivotEntity);
|
|
244
|
+
const ownerPivotProp = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
245
|
+
const targetPivotProp = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
246
|
+
fkProp = ownerPivotProp.name;
|
|
247
|
+
countByClass = pivotMeta.class;
|
|
248
|
+
targetFilterProp = targetPivotProp.name;
|
|
249
|
+
}
|
|
250
|
+
// Include the owner-side uniqueName in the key so that two distinct owner entity types
|
|
251
|
+
// sharing a relation with the same property name (e.g. `Author.books` and `Publisher.books`)
|
|
252
|
+
// don't collide into a single batch that would use one owner's FK mapping for the other.
|
|
253
|
+
const ownerUniqueName = helper(col.owner).__meta.uniqueName;
|
|
254
|
+
const key = `${ownerUniqueName}.${prop.name}|${JSON.stringify(opts ?? {})}`;
|
|
255
|
+
keys.push(key);
|
|
256
|
+
let group = groups.get(key);
|
|
257
|
+
if (!group) {
|
|
258
|
+
group = { fkProp, countByClass, targetFilterProp, ownerPKs: new Map(), opts: opts ?? {} };
|
|
259
|
+
groups.set(key, group);
|
|
260
|
+
}
|
|
261
|
+
const pk = helper(col.owner).getPrimaryKey();
|
|
262
|
+
group.ownerPKs.set(JSON.stringify(pk), pk);
|
|
263
|
+
}
|
|
264
|
+
const promises = Array.from(groups.entries()).map(async ([key, group]) => {
|
|
265
|
+
const allPKs = Array.from(group.ownerPKs.values());
|
|
266
|
+
const { where, ...countOpts } = group.opts;
|
|
267
|
+
const conditions = [{ [group.fkProp]: { $in: allPKs } }];
|
|
268
|
+
if (where) {
|
|
269
|
+
conditions.push(where);
|
|
270
|
+
}
|
|
271
|
+
// For M:N, apply the target entity's filters through the pivot's target relation
|
|
272
|
+
if (group.targetFilterProp) {
|
|
273
|
+
const targetMeta = em.getMetadata().find(group.countByClass);
|
|
274
|
+
const targetRelMeta = targetMeta.properties[group.targetFilterProp]?.targetMeta;
|
|
275
|
+
if (targetRelMeta) {
|
|
276
|
+
const filterCond = await em.applyFilters(targetRelMeta.class, {}, countOpts.filters, 'read');
|
|
277
|
+
if (filterCond && Object.keys(filterCond).length > 0) {
|
|
278
|
+
conditions.push({ [group.targetFilterProp]: filterCond });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const fkWhere = conditions.length === 1 ? conditions[0] : { $and: conditions };
|
|
283
|
+
const counts = await em.countBy(group.countByClass, group.fkProp, {
|
|
284
|
+
where: fkWhere,
|
|
285
|
+
...countOpts,
|
|
286
|
+
});
|
|
287
|
+
return [key, counts];
|
|
288
|
+
});
|
|
289
|
+
const resultsMap = new Map(await Promise.all(promises));
|
|
290
|
+
return collsWithOpts.map(([col], i) => {
|
|
291
|
+
const pk = helper(col.owner).getPrimaryKey();
|
|
292
|
+
const counts = resultsMap.get(keys[i]);
|
|
293
|
+
return counts?.[String(pk)] ?? 0;
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
}
|
|
219
297
|
static async getDataLoader() {
|
|
220
298
|
if (this.DataLoader) {
|
|
221
299
|
return this.DataLoader;
|
|
@@ -536,7 +536,7 @@ export class EntityComparator {
|
|
|
536
536
|
ret += ` && entity${path.map(k => this.wrap(k)).join('')}?.isInitialized()`;
|
|
537
537
|
}
|
|
538
538
|
ret += `) {\n`;
|
|
539
|
-
if (['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
|
|
539
|
+
if (!prop.array && ['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
|
|
540
540
|
return ret + ` ret${dataKey} = entity${entityKey}${unwrap};\n }\n`;
|
|
541
541
|
}
|
|
542
542
|
if (prop.kind === ReferenceKind.EMBEDDED) {
|
package/utils/QueryHelper.d.ts
CHANGED
|
@@ -6,6 +6,22 @@ import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
|
|
|
6
6
|
/** @internal */
|
|
7
7
|
export declare class QueryHelper {
|
|
8
8
|
static readonly SUPPORTED_OPERATORS: string[];
|
|
9
|
+
/**
|
|
10
|
+
* True when the property has multiple polymorph target types. Covers two structurally-equivalent
|
|
11
|
+
* shapes routed through the same loading path:
|
|
12
|
+
* 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
|
|
13
|
+
* target types, shared pivot with target-side discriminator).
|
|
14
|
+
* 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
|
|
15
|
+
* (many owner types pointing at one target, viewed from the target where "owners" looks
|
|
16
|
+
* like a union of multiple types).
|
|
17
|
+
*
|
|
18
|
+
* Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
|
|
19
|
+
* by discriminator and hydrates each target class separately.
|
|
20
|
+
*/
|
|
21
|
+
static isUnionTargetPolymorphic(prop: {
|
|
22
|
+
polymorphic?: boolean;
|
|
23
|
+
polymorphTargets?: readonly unknown[];
|
|
24
|
+
}): boolean;
|
|
9
25
|
/**
|
|
10
26
|
* Finds the discriminator value (key) for a given entity class in a discriminator map.
|
|
11
27
|
* Walks up the prototype chain so TPT subclasses resolve to their root's key.
|
package/utils/QueryHelper.js
CHANGED
|
@@ -7,6 +7,21 @@ import { isRaw, Raw } from './RawQueryFragment.js';
|
|
|
7
7
|
/** @internal */
|
|
8
8
|
export class QueryHelper {
|
|
9
9
|
static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
|
|
10
|
+
/**
|
|
11
|
+
* True when the property has multiple polymorph target types. Covers two structurally-equivalent
|
|
12
|
+
* shapes routed through the same loading path:
|
|
13
|
+
* 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
|
|
14
|
+
* target types, shared pivot with target-side discriminator).
|
|
15
|
+
* 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
|
|
16
|
+
* (many owner types pointing at one target, viewed from the target where "owners" looks
|
|
17
|
+
* like a union of multiple types).
|
|
18
|
+
*
|
|
19
|
+
* Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
|
|
20
|
+
* by discriminator and hydrates each target class separately.
|
|
21
|
+
*/
|
|
22
|
+
static isUnionTargetPolymorphic(prop) {
|
|
23
|
+
return !!prop.polymorphic && (prop.polymorphTargets?.length ?? 0) > 1;
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Finds the discriminator value (key) for a given entity class in a discriminator map.
|
|
12
27
|
* Walks up the prototype chain so TPT subclasses resolve to their root's key.
|
|
@@ -136,6 +136,8 @@ export class TransactionManager {
|
|
|
136
136
|
cloneEventManager: true,
|
|
137
137
|
disableTransactions: options.ignoreNestedTransactions,
|
|
138
138
|
loggerContext: options.loggerContext,
|
|
139
|
+
signal: options.signal,
|
|
140
|
+
inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
|
|
139
141
|
});
|
|
140
142
|
}
|
|
141
143
|
/**
|
package/utils/Utils.js
CHANGED
|
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
|
|
|
141
141
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
142
142
|
export class Utils {
|
|
143
143
|
static PK_SEPARATOR = '~~~';
|
|
144
|
-
static #ORM_VERSION = '7.1.0-dev.
|
|
144
|
+
static #ORM_VERSION = '7.1.0-dev.50';
|
|
145
145
|
/**
|
|
146
146
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
147
147
|
*/
|
package/utils/fs-utils.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ export interface FsUtils {
|
|
|
13
13
|
normalizePath(...parts: string[]): string;
|
|
14
14
|
relativePath(path: string, relativeTo: string): string;
|
|
15
15
|
absolutePath(path: string, baseDir?: string): string;
|
|
16
|
+
readFile(path: string): Promise<string>;
|
|
16
17
|
writeFile(path: string, data: string, options?: Record<string, any>): Promise<void>;
|
|
18
|
+
unlink(path: string): Promise<void>;
|
|
17
19
|
dynamicImport<T = any>(id: string): Promise<T>;
|
|
18
20
|
}
|
|
19
21
|
export declare const fs: FsUtils;
|
package/utils/fs-utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, globSync as nodeGlobSync, mkdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { writeFile as nodeWriteFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile as nodeReadFile, unlink as nodeUnlink, writeFile as nodeWriteFile } from 'node:fs/promises';
|
|
3
3
|
import { isAbsolute, join, normalize, relative } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { Utils } from './Utils.js';
|
|
@@ -181,9 +181,15 @@ export const fs = {
|
|
|
181
181
|
}
|
|
182
182
|
return this.normalizePath(path);
|
|
183
183
|
},
|
|
184
|
+
async readFile(path) {
|
|
185
|
+
return nodeReadFile(path, 'utf-8');
|
|
186
|
+
},
|
|
184
187
|
async writeFile(path, data, options) {
|
|
185
188
|
await nodeWriteFile(path, data, options);
|
|
186
189
|
},
|
|
190
|
+
async unlink(path) {
|
|
191
|
+
await nodeUnlink(path);
|
|
192
|
+
},
|
|
187
193
|
async dynamicImport(id) {
|
|
188
194
|
/* v8 ignore next */
|
|
189
195
|
const specifier = id.startsWith('file://') ? id : pathToFileURL(id).href;
|
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
3
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
4
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
5
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
6
|
+
*
|
|
7
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
8
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePartitionNameForComparison(name: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
13
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
14
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
15
|
+
* as an opaque expression.
|
|
16
|
+
*/
|
|
17
|
+
export declare function splitCommaSeparatedIdentifiers(value: string): string[] | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function normalizeIdentifierSegment(segment) {
|
|
2
|
+
const trimmed = segment.trim();
|
|
3
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
4
|
+
return trimmed.slice(1, -1).replaceAll('""', '"');
|
|
5
|
+
}
|
|
6
|
+
return trimmed.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
|
|
10
|
+
* resolution: unquoted segments fold to lower case, quoted segments preserve case and
|
|
11
|
+
* un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
|
|
12
|
+
* and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
|
|
13
|
+
*
|
|
14
|
+
* `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
|
|
15
|
+
* that would otherwise only surface as runtime PG "relation already exists" errors.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizePartitionNameForComparison(name) {
|
|
18
|
+
const trimmed = name.trim();
|
|
19
|
+
let depth = 0;
|
|
20
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
21
|
+
const ch = trimmed[i];
|
|
22
|
+
if (ch === '"') {
|
|
23
|
+
if (trimmed[i + 1] === '"') {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
depth = depth === 0 ? 1 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '.' && depth === 0) {
|
|
31
|
+
return `${normalizeIdentifierSegment(trimmed.slice(0, i))}.${normalizeIdentifierSegment(trimmed.slice(i + 1))}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `.${normalizeIdentifierSegment(trimmed)}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
|
|
38
|
+
* commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
|
|
39
|
+
* comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
|
|
40
|
+
* as an opaque expression.
|
|
41
|
+
*/
|
|
42
|
+
export function splitCommaSeparatedIdentifiers(value) {
|
|
43
|
+
const segments = [];
|
|
44
|
+
let buffer = '';
|
|
45
|
+
let inQuote = false;
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
const ch = value[i];
|
|
48
|
+
if (ch === '"') {
|
|
49
|
+
if (inQuote && value[i + 1] === '"') {
|
|
50
|
+
buffer += '""';
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
inQuote = !inQuote;
|
|
55
|
+
buffer += ch;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!inQuote) {
|
|
59
|
+
if (ch === ',') {
|
|
60
|
+
segments.push(buffer);
|
|
61
|
+
buffer = '';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!/[\w.\s]/.test(ch)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
buffer += ch;
|
|
69
|
+
}
|
|
70
|
+
if (inQuote) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
segments.push(buffer);
|
|
74
|
+
const trimmed = segments.map(s => s.trim());
|
|
75
|
+
if (trimmed.some(s => s === '')) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
package/utils/upsert-utils.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export declare function getWhereCondition<T extends object>(meta: EntityMetadata
|
|
|
10
10
|
where: FilterQuery<T>;
|
|
11
11
|
propIndex: number | false;
|
|
12
12
|
};
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare function resetUntouchedCollections<Entity extends object>(meta: EntityMetadata<Entity>, entity: Entity): void;
|
package/utils/upsert-utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
2
2
|
import { Utils } from './Utils.js';
|
|
3
|
+
import { ReferenceKind } from '../enums.js';
|
|
4
|
+
import { Collection } from '../entity/Collection.js';
|
|
5
|
+
import { helper } from '../entity/wrap.js';
|
|
3
6
|
function expandEmbeddedProperties(prop, key) {
|
|
4
7
|
if (prop.object) {
|
|
5
8
|
return [prop.name];
|
|
@@ -52,7 +55,9 @@ export function getOnConflictFields(meta, data, uniqueFields, options) {
|
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
const keys = Object.keys(data).flatMap(f => {
|
|
55
|
-
|
|
58
|
+
// skip explicitly listed unique fields; for raw onConflictFields we can't introspect the
|
|
59
|
+
// fragment, so merge all data keys (the user is responsible for not corrupting them).
|
|
60
|
+
if (Array.isArray(uniqueFields) && uniqueFields.includes(f)) {
|
|
56
61
|
return [];
|
|
57
62
|
}
|
|
58
63
|
const prop = meta?.properties[f];
|
|
@@ -142,3 +147,23 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
|
|
|
142
147
|
}
|
|
143
148
|
return { where, propIndex };
|
|
144
149
|
}
|
|
150
|
+
/** @internal */
|
|
151
|
+
export function resetUntouchedCollections(meta, entity) {
|
|
152
|
+
// for entities passed in via `em.create()` and then upserted, collection-kind relations
|
|
153
|
+
// were initialized as empty-but-initialized by the hydrator (newEntity=true). once the
|
|
154
|
+
// upsert resolves the entity to a possibly existing row, that state is stale and would
|
|
155
|
+
// cause `em.populate()` to skip those collections. replace them with a fresh
|
|
156
|
+
// uninitialized collection so populate can load them.
|
|
157
|
+
if (helper(entity).__originalEntityData) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const prop of meta.relations) {
|
|
161
|
+
if (prop.kind !== ReferenceKind.MANY_TO_MANY && prop.kind !== ReferenceKind.ONE_TO_MANY) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const collection = entity[prop.name];
|
|
165
|
+
if (Utils.isCollection(collection) && collection.isInitialized() && !collection.isDirty()) {
|
|
166
|
+
Collection.create(entity, prop.name, undefined, false);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|