@mikro-orm/core 7.1.0-dev.2 → 7.1.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +63 -11
- package/EntityManager.js +180 -21
- package/drivers/DatabaseDriver.d.ts +1 -0
- package/drivers/DatabaseDriver.js +3 -0
- package/drivers/IDatabaseDriver.d.ts +44 -1
- 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/EntityFactory.js +20 -1
- package/entity/EntityLoader.d.ts +3 -1
- package/entity/EntityLoader.js +39 -16
- 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 +84 -15
- package/entity/defineEntity.js +10 -6
- package/entity/utils.js +4 -5
- 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 +3 -0
- package/metadata/MetadataDiscovery.d.ts +12 -0
- package/metadata/MetadataDiscovery.js +165 -15
- package/metadata/MetadataValidator.d.ts +24 -0
- package/metadata/MetadataValidator.js +202 -1
- package/metadata/types.d.ts +71 -3
- 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 +8 -1
- package/platforms/Platform.js +38 -3
- package/typings.d.ts +139 -10
- package/typings.js +21 -4
- package/utils/AbstractMigrator.d.ts +11 -1
- package/utils/AbstractMigrator.js +203 -0
- package/utils/Configuration.d.ts +7 -0
- package/utils/Configuration.js +1 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationRow, MigratorEvent } from '../typings.js';
|
|
1
|
+
import type { Constructor, IMigrationGenerator, IMigrationRunner, IMigrator, IMigratorStorage, MaybePromise, Migration, MigrationInfo, MigrationResult, MigrationRow, MigratorEvent } from '../typings.js';
|
|
2
2
|
import type { Transaction } from '../connections/Connection.js';
|
|
3
3
|
import type { Configuration, MigrationsOptions } from './Configuration.js';
|
|
4
4
|
import type { EntityManagerType, IDatabaseDriver } from '../drivers/IDatabaseDriver.js';
|
|
@@ -70,6 +70,16 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
|
|
|
70
70
|
* @inheritDoc
|
|
71
71
|
*/
|
|
72
72
|
down(options?: string | string[] | Omit<MigrateOptions, 'from'>): Promise<MigrationInfo[]>;
|
|
73
|
+
/**
|
|
74
|
+
* @inheritDoc
|
|
75
|
+
*/
|
|
76
|
+
rollup(migrations?: string[]): Promise<MigrationResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Extracts the body of a method from migration source code using brace counting.
|
|
79
|
+
* Returns the raw lines between the opening and closing braces, or empty string if not found.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
private extractMethodBody;
|
|
73
83
|
abstract getStorage(): IMigratorStorage;
|
|
74
84
|
/**
|
|
75
85
|
* @inheritDoc
|
|
@@ -63,6 +63,209 @@ export class AbstractMigrator {
|
|
|
63
63
|
async down(options) {
|
|
64
64
|
return this.runMigrations('down', options);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @inheritDoc
|
|
68
|
+
*/
|
|
69
|
+
async rollup(migrations) {
|
|
70
|
+
await this.init();
|
|
71
|
+
const { fs } = await import('@mikro-orm/core/fs-utils');
|
|
72
|
+
const all = await this.discoverMigrations();
|
|
73
|
+
const executedSet = new Set(await this.storage.executed());
|
|
74
|
+
let toRollup;
|
|
75
|
+
if (migrations && migrations.length > 0) {
|
|
76
|
+
const requested = new Set(migrations.map(m => this.getMigrationFilename(m)));
|
|
77
|
+
toRollup = all.filter(m => requested.has(m.name));
|
|
78
|
+
const found = new Set(toRollup.map(m => m.name));
|
|
79
|
+
const notFound = [...requested].filter(name => !found.has(name));
|
|
80
|
+
if (notFound.length > 0) {
|
|
81
|
+
throw new Error(`Migrations not found: ${notFound.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
const notExecuted = toRollup.filter(m => !executedSet.has(m.name));
|
|
84
|
+
if (notExecuted.length > 0) {
|
|
85
|
+
throw new Error(`Cannot roll up migrations that have not been executed: ${notExecuted.map(m => m.name).join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
toRollup = all.filter(m => executedSet.has(m.name));
|
|
90
|
+
}
|
|
91
|
+
if (toRollup.length < 2) {
|
|
92
|
+
throw new Error('At least 2 executed migrations are required for rollup');
|
|
93
|
+
}
|
|
94
|
+
const withoutPath = toRollup.filter(m => !m.path);
|
|
95
|
+
if (withoutPath.length > 0) {
|
|
96
|
+
throw new Error(`Cannot roll up migrations without file paths (class-based migrations): ${withoutPath.map(m => m.name).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
const upBodies = [];
|
|
99
|
+
const downBodies = [];
|
|
100
|
+
const placeholder = `__mikro_orm_rollup_${Date.now()}__`;
|
|
101
|
+
for (const migration of toRollup) {
|
|
102
|
+
const source = await fs.readFile(migration.path);
|
|
103
|
+
const upBody = this.extractMethodBody(source, 'up');
|
|
104
|
+
const downBody = this.extractMethodBody(source, 'down');
|
|
105
|
+
if (upBody) {
|
|
106
|
+
upBodies.push(` // --- merged from ${migration.name} ---\n${upBody}`);
|
|
107
|
+
}
|
|
108
|
+
if (downBody) {
|
|
109
|
+
downBodies.unshift(` // --- merged from ${migration.name} ---\n${downBody}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const diff = {
|
|
113
|
+
up: [placeholder],
|
|
114
|
+
down: downBodies.length > 0 ? [placeholder] : [],
|
|
115
|
+
};
|
|
116
|
+
const [templateCode, fileName] = await this.generator.generate(diff);
|
|
117
|
+
const placeholderRe = new RegExp(`^.*${placeholder}.*$`, 'm');
|
|
118
|
+
let code = templateCode.replace(placeholderRe, upBodies.join('\n'));
|
|
119
|
+
if (downBodies.length > 0) {
|
|
120
|
+
code = code.replace(placeholderRe, downBodies.join('\n'));
|
|
121
|
+
}
|
|
122
|
+
await fs.writeFile(fs.normalizePath(this.absolutePath, fileName), code, { flush: true });
|
|
123
|
+
const updateStorage = async () => {
|
|
124
|
+
for (const migration of toRollup) {
|
|
125
|
+
await this.storage.unlogMigration({ name: migration.name });
|
|
126
|
+
}
|
|
127
|
+
await this.storage.logMigration({ name: fileName.replace(/\.[jt]s$/, '') });
|
|
128
|
+
};
|
|
129
|
+
if (this.options.transactional) {
|
|
130
|
+
await this.driver.getConnection().transactional(async (trx) => {
|
|
131
|
+
this.storage.setMasterMigration(trx);
|
|
132
|
+
try {
|
|
133
|
+
await updateStorage();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.storage.unsetMasterMigration();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await updateStorage();
|
|
142
|
+
}
|
|
143
|
+
await Promise.all(toRollup.map(migration => fs.unlink(migration.path)));
|
|
144
|
+
return { fileName, code, diff: { up: [], down: [] } };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extracts the body of a method from migration source code using brace counting.
|
|
148
|
+
* Returns the raw lines between the opening and closing braces, or empty string if not found.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
extractMethodBody(source, methodName) {
|
|
152
|
+
const lines = source.split('\n');
|
|
153
|
+
// match method declarations, not occurrences in comments/strings — require preceding whitespace or keyword
|
|
154
|
+
const methodPattern = new RegExp(`^\\s+(?:override\\s+|async\\s+)*${methodName}\\s*\\(`);
|
|
155
|
+
let methodLine = -1;
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
if (methodPattern.test(lines[i])) {
|
|
158
|
+
methodLine = i;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (methodLine === -1) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
let braceCount = 0;
|
|
166
|
+
let bodyStart = -1;
|
|
167
|
+
let bodyEnd = -1;
|
|
168
|
+
let bodyStartCol = -1;
|
|
169
|
+
let bodyEndCol = -1;
|
|
170
|
+
let inBacktick = false;
|
|
171
|
+
let inBlockComment = false;
|
|
172
|
+
// stack tracks brace depth at which each template expression `${...}` was entered
|
|
173
|
+
const templateExprStack = [];
|
|
174
|
+
for (let i = methodLine; i < lines.length; i++) {
|
|
175
|
+
const line = lines[i];
|
|
176
|
+
for (let j = 0; j < line.length; j++) {
|
|
177
|
+
// handle multi-line block comments
|
|
178
|
+
if (inBlockComment) {
|
|
179
|
+
if (line[j] === '*' && j + 1 < line.length && line[j + 1] === '/') {
|
|
180
|
+
inBlockComment = false;
|
|
181
|
+
j++;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// handle multi-line template literals
|
|
186
|
+
if (inBacktick) {
|
|
187
|
+
if (line[j] === '\\') {
|
|
188
|
+
j++;
|
|
189
|
+
}
|
|
190
|
+
else if (line[j] === '`') {
|
|
191
|
+
inBacktick = false;
|
|
192
|
+
}
|
|
193
|
+
else if (line[j] === '$' && j + 1 < line.length && line[j + 1] === '{') {
|
|
194
|
+
// entering template expression — resume brace counting
|
|
195
|
+
templateExprStack.push(braceCount);
|
|
196
|
+
inBacktick = false;
|
|
197
|
+
j++; // skip the {
|
|
198
|
+
braceCount++;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const ch = line[j];
|
|
203
|
+
// single/double quoted strings (single-line only)
|
|
204
|
+
if (ch === "'" || ch === '"') {
|
|
205
|
+
const quote = ch;
|
|
206
|
+
j++;
|
|
207
|
+
while (j < line.length) {
|
|
208
|
+
if (line[j] === '\\') {
|
|
209
|
+
j++;
|
|
210
|
+
}
|
|
211
|
+
else if (line[j] === quote) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
j++;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// template literal start
|
|
219
|
+
if (ch === '`') {
|
|
220
|
+
inBacktick = true;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// single-line comment
|
|
224
|
+
if (ch === '/' && j + 1 < line.length && line[j + 1] === '/') {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
// block comment start
|
|
228
|
+
if (ch === '/' && j + 1 < line.length && line[j + 1] === '*') {
|
|
229
|
+
inBlockComment = true;
|
|
230
|
+
j++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === '{') {
|
|
234
|
+
if (braceCount === 0) {
|
|
235
|
+
bodyStart = i;
|
|
236
|
+
bodyStartCol = j + 1;
|
|
237
|
+
}
|
|
238
|
+
braceCount++;
|
|
239
|
+
}
|
|
240
|
+
else if (ch === '}') {
|
|
241
|
+
braceCount--;
|
|
242
|
+
// closing a template expression — re-enter backtick mode
|
|
243
|
+
if (templateExprStack.length > 0 && braceCount === templateExprStack[templateExprStack.length - 1]) {
|
|
244
|
+
templateExprStack.pop();
|
|
245
|
+
inBacktick = true;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (braceCount === 0) {
|
|
249
|
+
bodyEnd = i;
|
|
250
|
+
bodyEndCol = j;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (bodyEnd !== -1) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (bodyStart === -1 || bodyEnd === -1) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
// single-line method body: extract content between braces on the same line
|
|
263
|
+
if (bodyStart === bodyEnd) {
|
|
264
|
+
const content = lines[bodyStart].slice(bodyStartCol, bodyEndCol).trim();
|
|
265
|
+
return content ? ` ${content}` : '';
|
|
266
|
+
}
|
|
267
|
+
return lines.slice(bodyStart + 1, bodyEnd).join('\n');
|
|
268
|
+
}
|
|
66
269
|
/**
|
|
67
270
|
* @inheritDoc
|
|
68
271
|
*/
|
package/utils/Configuration.d.ts
CHANGED
|
@@ -529,6 +529,13 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
|
|
|
529
529
|
* @default false
|
|
530
530
|
*/
|
|
531
531
|
forceUndefined: boolean;
|
|
532
|
+
/**
|
|
533
|
+
* Initialize nullable properties to `null` (or `undefined` when `forceUndefined` is set)
|
|
534
|
+
* during `em.create()` when they are not provided in the data. Without this option,
|
|
535
|
+
* nullable properties remain `undefined` until the entity is loaded from the database.
|
|
536
|
+
* @default false
|
|
537
|
+
*/
|
|
538
|
+
initNullableProperties: boolean;
|
|
532
539
|
/**
|
|
533
540
|
* Property `onCreate` hooks are normally executed during `flush` operation.
|
|
534
541
|
* With this option, they will be processed early inside `em.create()` method.
|
package/utils/Configuration.js
CHANGED
|
@@ -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;
|
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.
|
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.21';
|
|
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
|
+
}
|