@mikro-orm/core 7.1.0-dev.9 → 7.1.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EntityManager.d.ts +47 -3
- package/EntityManager.js +150 -23
- package/README.md +2 -1
- package/connections/Connection.d.ts +67 -1
- package/connections/Connection.js +64 -0
- package/drivers/IDatabaseDriver.d.ts +20 -6
- package/entity/BaseEntity.d.ts +68 -1
- package/entity/BaseEntity.js +18 -0
- package/entity/Collection.d.ts +1 -1
- package/entity/Collection.js +4 -1
- package/entity/EntityAssigner.js +8 -0
- package/entity/EntityFactory.js +5 -3
- package/entity/EntityLoader.d.ts +8 -1
- package/entity/EntityLoader.js +97 -29
- package/entity/Reference.d.ts +42 -1
- package/entity/Reference.js +9 -0
- package/entity/defineEntity.d.ts +69 -16
- package/entity/defineEntity.js +17 -0
- package/entity/utils.js +4 -5
- package/enums.d.ts +8 -1
- package/index.d.ts +1 -1
- package/metadata/EntitySchema.js +18 -0
- package/metadata/MetadataDiscovery.d.ts +1 -0
- package/metadata/MetadataDiscovery.js +79 -15
- package/metadata/MetadataValidator.d.ts +26 -0
- package/metadata/MetadataValidator.js +228 -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 +75 -8
- package/package.json +1 -1
- package/platforms/Platform.d.ts +13 -3
- package/platforms/Platform.js +51 -6
- package/serialization/EntitySerializer.js +4 -3
- package/typings.d.ts +323 -21
- package/typings.js +32 -12
- package/unit-of-work/UnitOfWork.js +43 -5
- package/utils/AbstractMigrator.d.ts +9 -4
- package/utils/AbstractMigrator.js +60 -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/EntityComparator.js +1 -1
- package/utils/TransactionManager.js +2 -0
- package/utils/Utils.js +1 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { normalizePartitionNameForComparison, splitCommaSeparatedIdentifiers } from '../utils/partition-utils.js';
|
|
2
3
|
import { MetadataError } from '../errors.js';
|
|
3
4
|
import { ReferenceKind } from '../enums.js';
|
|
4
5
|
/**
|
|
@@ -28,6 +29,9 @@ export class MetadataValidator {
|
|
|
28
29
|
// Virtual entities (expression without view flag) have restrictions - no PKs, limited relation types
|
|
29
30
|
// Note: meta.virtual is set later in sync(), so we check for expression && !view here
|
|
30
31
|
if (meta.virtual || (meta.expression && !meta.view)) {
|
|
32
|
+
if (meta.partitionBy) {
|
|
33
|
+
throw new MetadataError(`Virtual entity ${meta.className} cannot define partitionBy`);
|
|
34
|
+
}
|
|
31
35
|
for (const prop of Utils.values(meta.properties)) {
|
|
32
36
|
if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED, ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
33
37
|
throw new MetadataError(`Only scalars, embedded properties and to-many relations are allowed inside virtual entity. Found '${prop.kind}' in ${meta.className}.${prop.name}`);
|
|
@@ -46,6 +50,7 @@ export class MetadataValidator {
|
|
|
46
50
|
this.validateDuplicateFieldNames(meta, options);
|
|
47
51
|
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
48
52
|
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
|
53
|
+
this.validatePartitioning(meta);
|
|
49
54
|
this.validatePropertyNames(meta);
|
|
50
55
|
for (const prop of Utils.values(meta.properties)) {
|
|
51
56
|
if (prop.kind !== ReferenceKind.SCALAR) {
|
|
@@ -57,6 +62,41 @@ export class MetadataValidator {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
}
|
|
65
|
+
validateRoutineDefinition(routine) {
|
|
66
|
+
if (typeof routine.name !== 'string' || routine.name.trim() === '') {
|
|
67
|
+
throw new MetadataError(`Routine is missing the required 'name' option.`);
|
|
68
|
+
}
|
|
69
|
+
if (!routine.type) {
|
|
70
|
+
throw new MetadataError(`Routine ${routine.name} is missing the required 'type' option ('procedure' | 'function').`);
|
|
71
|
+
}
|
|
72
|
+
if (routine.body != null && routine.expression != null) {
|
|
73
|
+
throw new MetadataError(`Routine ${routine.name} defines both 'body' and 'expression'. Use one or the other.`);
|
|
74
|
+
}
|
|
75
|
+
if (routine.body == null && routine.expression == null && routine.bodyJs == null) {
|
|
76
|
+
throw new MetadataError(`Routine ${routine.name} must define a 'body', 'expression', or 'bodyJs'.`);
|
|
77
|
+
}
|
|
78
|
+
if (routine.type === 'function' && routine.returns == null) {
|
|
79
|
+
throw new MetadataError(`Function routine ${routine.name} must declare a 'returns' option.`);
|
|
80
|
+
}
|
|
81
|
+
if (routine.type === 'procedure' && routine.bodyJs != null) {
|
|
82
|
+
throw new MetadataError(`Routine ${routine.name} declares 'bodyJs' on a procedure. JS fallbacks are only supported for functions — SQLite has no analog for stored procedures.`);
|
|
83
|
+
}
|
|
84
|
+
for (const param of routine.params) {
|
|
85
|
+
const dir = param.direction;
|
|
86
|
+
if (dir !== 'in' && dir !== 'out' && dir !== 'inout') {
|
|
87
|
+
throw new MetadataError(`Routine ${routine.name}.${param.name} has invalid direction '${dir}'. Expected 'in', 'out', or 'inout'.`);
|
|
88
|
+
}
|
|
89
|
+
if ((dir === 'out' || dir === 'inout') && !param.ref) {
|
|
90
|
+
throw new MetadataError(`Routine ${routine.name}.${param.name} is declared as '${dir}' but missing 'ref: true'. OUT/INOUT parameters must be passed as ScalarReference.`);
|
|
91
|
+
}
|
|
92
|
+
if (dir === 'in' && param.ref) {
|
|
93
|
+
throw new MetadataError(`Routine ${routine.name}.${param.name} declares 'ref: true' on an IN parameter. ScalarReference wrapping is only meaningful for OUT/INOUT parameters.`);
|
|
94
|
+
}
|
|
95
|
+
if (routine.type === 'function' && dir !== 'in') {
|
|
96
|
+
throw new MetadataError(`Function routine ${routine.name}.${param.name} declares direction '${dir}'. Functions only support IN parameters — use a procedure for OUT/INOUT semantics.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
60
100
|
validateDiscovered(discovered, options) {
|
|
61
101
|
if (discovered.length === 0 && options.warnWhenNoEntities) {
|
|
62
102
|
throw MetadataError.noEntityDiscovered();
|
|
@@ -111,6 +151,189 @@ export class MetadataValidator {
|
|
|
111
151
|
}
|
|
112
152
|
});
|
|
113
153
|
}
|
|
154
|
+
validatePartitioning(meta) {
|
|
155
|
+
if (!meta.partitionBy) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!this.hasPartitionExpression(meta.partitionBy.expression)) {
|
|
159
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: missing expression`);
|
|
160
|
+
}
|
|
161
|
+
// Inheritance (STI/TPT) and partitioning both drive table layout, so combining them would
|
|
162
|
+
// require non-trivial DDL coordination that the schema generator does not produce today.
|
|
163
|
+
const hasInheritance = !!meta.root.discriminatorColumn || meta.root.inheritanceType === 'tpt' || meta.root.inheritance === 'tpt';
|
|
164
|
+
if (hasInheritance) {
|
|
165
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: combining partitioning with inheritance is not supported`);
|
|
166
|
+
}
|
|
167
|
+
this.validatePartitionKeyConstraints(meta);
|
|
168
|
+
if (meta.partitionBy.type === 'hash') {
|
|
169
|
+
const { partitions } = meta.partitionBy;
|
|
170
|
+
if (Array.isArray(partitions)) {
|
|
171
|
+
if (partitions.length === 0) {
|
|
172
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition name list must not be empty`);
|
|
173
|
+
}
|
|
174
|
+
const blank = partitions.find(name => typeof name !== 'string' || !name.trim());
|
|
175
|
+
if (blank !== undefined) {
|
|
176
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition names must be non-empty strings`);
|
|
177
|
+
}
|
|
178
|
+
const ambiguous = partitions.find(name => !this.hasValidPartitionName(name));
|
|
179
|
+
if (ambiguous) {
|
|
180
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguous}' contains more than one '.' — use at most one '.' to separate schema from table`);
|
|
181
|
+
}
|
|
182
|
+
const duplicate = this.findDuplicatePartitionName(partitions);
|
|
183
|
+
if (duplicate !== undefined) {
|
|
184
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate hash partition name '${duplicate}'`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (typeof partitions !== 'number' || !Number.isInteger(partitions) || partitions < 1) {
|
|
189
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: hash partition count must be a positive integer`);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(meta.partitionBy.partitions) || meta.partitionBy.partitions.length === 0) {
|
|
194
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: list/range partitions must be a non-empty array`);
|
|
195
|
+
}
|
|
196
|
+
if (meta.partitionBy.partitions.some(partition => !partition.values?.trim())) {
|
|
197
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: every partition must define values`);
|
|
198
|
+
}
|
|
199
|
+
const ambiguousName = meta.partitionBy.partitions.find(partition => partition.name != null && !this.hasValidPartitionName(partition.name));
|
|
200
|
+
if (ambiguousName) {
|
|
201
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition name '${ambiguousName.name}' contains more than one '.' — use at most one '.' to separate schema from table`);
|
|
202
|
+
}
|
|
203
|
+
// Include auto-generated default names (`${tableName}_${index}`, matching
|
|
204
|
+
// `createExplicitPartitions` in the sql package) so an explicit name that collides with
|
|
205
|
+
// an unnamed peer's default is caught here rather than at DDL execution time.
|
|
206
|
+
const resolvedNames = meta.partitionBy.partitions.map((partition, index) => partition.name ?? `${meta.tableName}_${index}`);
|
|
207
|
+
const duplicate = this.findDuplicatePartitionName(resolvedNames);
|
|
208
|
+
if (duplicate !== undefined) {
|
|
209
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: duplicate partition name '${duplicate}'`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Find the first partition name whose normalized form (case-folded for unquoted segments,
|
|
214
|
+
* quoted segments preserved) has already been seen. Returns the offending name in its
|
|
215
|
+
* original form for the error message.
|
|
216
|
+
*/
|
|
217
|
+
findDuplicatePartitionName(names) {
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
for (const name of names) {
|
|
220
|
+
const normalized = normalizePartitionNameForComparison(name);
|
|
221
|
+
if (seen.has(normalized)) {
|
|
222
|
+
return name;
|
|
223
|
+
}
|
|
224
|
+
seen.add(normalized);
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Partition names may be bare (`child`), schema-qualified (`schema.child`), or use quoted
|
|
230
|
+
* identifiers (`"my.schema"."child"`). Reject anything with more than one unquoted `.`.
|
|
231
|
+
*/
|
|
232
|
+
hasValidPartitionName(name) {
|
|
233
|
+
let depth = 0;
|
|
234
|
+
let dots = 0;
|
|
235
|
+
for (let i = 0; i < name.length; i++) {
|
|
236
|
+
const ch = name[i];
|
|
237
|
+
if (ch === '"') {
|
|
238
|
+
if (name[i + 1] === '"') {
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
depth = depth === 0 ? 1 : 0;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === '.' && depth === 0) {
|
|
246
|
+
dots++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return dots <= 1;
|
|
250
|
+
}
|
|
251
|
+
hasPartitionExpression(expression) {
|
|
252
|
+
if (expression == null) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (typeof expression === 'function') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(expression)) {
|
|
259
|
+
return expression.length > 0 && expression.every(key => typeof key === 'string' && key.trim().length > 0);
|
|
260
|
+
}
|
|
261
|
+
return String(expression).trim().length > 0;
|
|
262
|
+
}
|
|
263
|
+
validatePartitionKeyConstraints(meta) {
|
|
264
|
+
const partitionFields = this.getPartitionKeyFields(meta);
|
|
265
|
+
if (!partitionFields?.length) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const primaryKeyFields = meta.root.getPrimaryProps().flatMap(prop => prop.fieldNames);
|
|
269
|
+
if (partitionFields.some(field => !primaryKeyFields.includes(field))) {
|
|
270
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: primary key must include partition key columns '${partitionFields.join("', '")}'`);
|
|
271
|
+
}
|
|
272
|
+
for (const prop of Object.values(meta.root.properties)) {
|
|
273
|
+
if (!prop.unique || !prop.fieldNames?.length) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (partitionFields.some(field => !prop.fieldNames.includes(field))) {
|
|
277
|
+
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: unique property ${meta.root.className}.${prop.name} must include partition key columns '${partitionFields.join("', '")}'`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const unique of meta.root.uniques ?? []) {
|
|
281
|
+
const fields = this.getConstraintFields(meta.root, unique.properties);
|
|
282
|
+
if (!fields?.length) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (partitionFields.some(field => !fields.includes(field))) {
|
|
286
|
+
const constraint = unique.name ? `unique constraint '${unique.name}'` : 'unique constraint';
|
|
287
|
+
throw new MetadataError(`Entity ${meta.root.className} has invalid partitionBy option: ${constraint} must include partition key columns '${partitionFields.join("', '")}'`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Returns the list of physical field names that a partition expression references, or
|
|
293
|
+
* `undefined` when the expression is opaque (callback, or raw SQL like `date_trunc('day', x)`
|
|
294
|
+
* that we cannot statically parse). Opaque expressions intentionally bypass the primary-key /
|
|
295
|
+
* unique-constraint coverage checks — users are trusted to ensure the referenced columns are
|
|
296
|
+
* part of the partition key, since PostgreSQL will surface the violation at DDL execution.
|
|
297
|
+
*/
|
|
298
|
+
getPartitionKeyFields(meta) {
|
|
299
|
+
const expression = meta.partitionBy?.expression;
|
|
300
|
+
if (!expression || typeof expression === 'function') {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
if (Array.isArray(expression)) {
|
|
304
|
+
return expression.map(key => this.resolvePartitionKeyField(meta, key));
|
|
305
|
+
}
|
|
306
|
+
const keys = splitCommaSeparatedIdentifiers(String(expression).trim());
|
|
307
|
+
if (!keys) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
return keys.map(key => this.resolvePartitionKeyField(meta, key));
|
|
311
|
+
}
|
|
312
|
+
resolvePartitionKeyField(meta, key) {
|
|
313
|
+
const trimmed = key.trim().replaceAll('"', '');
|
|
314
|
+
if (!trimmed) {
|
|
315
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: empty partition key`);
|
|
316
|
+
}
|
|
317
|
+
const prop = meta.root.properties[trimmed] ??
|
|
318
|
+
Object.values(meta.root.properties).find(candidate => candidate.fieldNames?.length === 1 && candidate.fieldNames[0] === trimmed);
|
|
319
|
+
if (!prop) {
|
|
320
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: unknown partition key '${key.trim()}'`);
|
|
321
|
+
}
|
|
322
|
+
if (prop.fieldNames?.length !== 1) {
|
|
323
|
+
throw new MetadataError(`Entity ${meta.className} has invalid partitionBy option: partition key '${key.trim()}' maps to multiple columns ('${prop.fieldNames?.join("', '")}'); list them explicitly as partition keys`);
|
|
324
|
+
}
|
|
325
|
+
return prop.fieldNames[0];
|
|
326
|
+
}
|
|
327
|
+
getConstraintFields(meta, properties) {
|
|
328
|
+
if (!properties) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const fields = Utils.asArray(properties).flatMap(propName => {
|
|
332
|
+
const prop = meta.root.properties[propName];
|
|
333
|
+
return prop?.fieldNames ?? [];
|
|
334
|
+
});
|
|
335
|
+
return fields.length > 0 ? fields : undefined;
|
|
336
|
+
}
|
|
114
337
|
validateReference(meta, prop, options) {
|
|
115
338
|
// references do have types
|
|
116
339
|
if (!prop.type) {
|
|
@@ -160,7 +383,7 @@ export class MetadataValidator {
|
|
|
160
383
|
* Composite unique constraints are not sufficient for targetKey.
|
|
161
384
|
*/
|
|
162
385
|
isPropertyUnique(prop, meta) {
|
|
163
|
-
if (prop.unique) {
|
|
386
|
+
if (prop.unique || (prop.primary && meta.primaryKeys.length === 1)) {
|
|
164
387
|
return true;
|
|
165
388
|
}
|
|
166
389
|
// Check for single-property unique constraint via @Unique decorator
|
|
@@ -358,6 +581,10 @@ export class MetadataValidator {
|
|
|
358
581
|
if (!meta.expression) {
|
|
359
582
|
throw MetadataError.viewEntityWithoutExpression(meta);
|
|
360
583
|
}
|
|
584
|
+
// Views are not partitionable - reject explicitly instead of silently ignoring
|
|
585
|
+
if (meta.partitionBy) {
|
|
586
|
+
throw new MetadataError(`View entity ${meta.className} cannot define partitionBy`);
|
|
587
|
+
}
|
|
361
588
|
// Validate indexes if present
|
|
362
589
|
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
363
590
|
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RoutineArgsOf, RoutineConfig, RoutineDataAccess, RoutineIgnoreField, RoutineKind, RoutineParamMap, RoutineProperty, RoutineReturnOf, RoutineReturns, RoutineSecurity } from '../typings.js';
|
|
2
|
+
import type { Type } from '../types/Type.js';
|
|
3
|
+
import type { Raw } from '../utils/RawQueryFragment.js';
|
|
4
|
+
/**
|
|
5
|
+
* Stored procedure or function declaration. Register instances via the `routines` config option
|
|
6
|
+
* passed to `MikroORM.init`, then call them through `em.callRoutine(routine, args)`.
|
|
7
|
+
*
|
|
8
|
+
* `TArgs` and `TReturn` are inferred from the literal config, so `em.callRoutine` is fully typed
|
|
9
|
+
* without threading generics through the call site. Reach for {@link Routine.create} when the
|
|
10
|
+
* inferred type is too loose — typically an `object` return that should be a concrete shape
|
|
11
|
+
* rather than `Dictionary`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const HashUser = new Routine({
|
|
16
|
+
* name: 'hash_user',
|
|
17
|
+
* type: 'function',
|
|
18
|
+
* params: {
|
|
19
|
+
* name: { type: 'varchar(255)' },
|
|
20
|
+
* salt: { type: 'varchar(255)' },
|
|
21
|
+
* },
|
|
22
|
+
* returns: { runtimeType: 'string', columnType: 'char(40)' },
|
|
23
|
+
* body: 'SELECT SHA1(CONCAT(name, salt))',
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* await MikroORM.init({ entities: [User], routines: [HashUser] });
|
|
27
|
+
*
|
|
28
|
+
* // args typed as `{ name: string; salt: string }`, result typed as `string`:
|
|
29
|
+
* const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class Routine<const TConfig extends RoutineConfig = RoutineConfig, TArgs = RoutineArgsOf<TConfig>, TReturn = RoutineReturnOf<TConfig>> {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly type: RoutineKind;
|
|
35
|
+
readonly schema?: string;
|
|
36
|
+
readonly comment?: string;
|
|
37
|
+
readonly security?: RoutineSecurity;
|
|
38
|
+
readonly definer?: string;
|
|
39
|
+
readonly deterministic?: boolean;
|
|
40
|
+
readonly dataAccess?: RoutineDataAccess;
|
|
41
|
+
readonly language?: string;
|
|
42
|
+
readonly body?: string | Raw | ((params: RoutineParamMap<any>, em: any) => string | Raw);
|
|
43
|
+
readonly expression?: string;
|
|
44
|
+
readonly bodyJs?: (params: any) => unknown;
|
|
45
|
+
readonly returns?: RoutineReturns;
|
|
46
|
+
readonly returnCustomType?: Type<unknown>;
|
|
47
|
+
readonly ignoreSchemaChanges?: RoutineIgnoreField[];
|
|
48
|
+
readonly params: RoutineProperty[];
|
|
49
|
+
constructor(config: TConfig);
|
|
50
|
+
/** @internal */
|
|
51
|
+
createParamMappingObject(): Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Overrides the inferred TArgs/TReturn. Omit a generic to keep inference; pass `never` in the
|
|
54
|
+
* args slot to refine only the return type.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* interface UserStats { totalOrders: number; lastOrderAt: Date }
|
|
59
|
+
* const GetStats = Routine.create<never, UserStats>({
|
|
60
|
+
* name: 'get_user_stats',
|
|
61
|
+
* type: 'function',
|
|
62
|
+
* params: { user_id: { type: 'int', runtimeType: 'number' } },
|
|
63
|
+
* returns: { runtimeType: 'object', columnType: 'json' },
|
|
64
|
+
* body: '...',
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
static create<TArgs = never, TReturn = never, const TConfig extends RoutineConfig = RoutineConfig>(config: TConfig): Routine<TConfig, [
|
|
69
|
+
TArgs
|
|
70
|
+
] extends [never] ? RoutineArgsOf<TConfig> : TArgs, [
|
|
71
|
+
TReturn
|
|
72
|
+
] extends [never] ? RoutineReturnOf<TConfig> : TReturn>;
|
|
73
|
+
static is(item: unknown): item is Routine;
|
|
74
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Resolve a `Type<unknown> | Constructor<Type<unknown>>` to an instance. Each built-in `Type` has
|
|
2
|
+
// `__mappedType` on its prototype so any constructed instance carries it; a missing brand means a
|
|
3
|
+
// raw constructor that we instantiate.
|
|
4
|
+
const toTypeInstance = (value) => {
|
|
5
|
+
return value.__mappedType ? value : new value();
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Stored procedure or function declaration. Register instances via the `routines` config option
|
|
9
|
+
* passed to `MikroORM.init`, then call them through `em.callRoutine(routine, args)`.
|
|
10
|
+
*
|
|
11
|
+
* `TArgs` and `TReturn` are inferred from the literal config, so `em.callRoutine` is fully typed
|
|
12
|
+
* without threading generics through the call site. Reach for {@link Routine.create} when the
|
|
13
|
+
* inferred type is too loose — typically an `object` return that should be a concrete shape
|
|
14
|
+
* rather than `Dictionary`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const HashUser = new Routine({
|
|
19
|
+
* name: 'hash_user',
|
|
20
|
+
* type: 'function',
|
|
21
|
+
* params: {
|
|
22
|
+
* name: { type: 'varchar(255)' },
|
|
23
|
+
* salt: { type: 'varchar(255)' },
|
|
24
|
+
* },
|
|
25
|
+
* returns: { runtimeType: 'string', columnType: 'char(40)' },
|
|
26
|
+
* body: 'SELECT SHA1(CONCAT(name, salt))',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* await MikroORM.init({ entities: [User], routines: [HashUser] });
|
|
30
|
+
*
|
|
31
|
+
* // args typed as `{ name: string; salt: string }`, result typed as `string`:
|
|
32
|
+
* const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class Routine {
|
|
36
|
+
name;
|
|
37
|
+
type;
|
|
38
|
+
schema;
|
|
39
|
+
comment;
|
|
40
|
+
security;
|
|
41
|
+
definer;
|
|
42
|
+
deterministic;
|
|
43
|
+
dataAccess;
|
|
44
|
+
language;
|
|
45
|
+
body;
|
|
46
|
+
expression;
|
|
47
|
+
bodyJs;
|
|
48
|
+
returns;
|
|
49
|
+
returnCustomType;
|
|
50
|
+
ignoreSchemaChanges;
|
|
51
|
+
params;
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.name = config.name;
|
|
54
|
+
this.type = config.type;
|
|
55
|
+
this.schema = config.schema;
|
|
56
|
+
this.comment = config.comment;
|
|
57
|
+
this.security = config.security;
|
|
58
|
+
this.definer = config.definer;
|
|
59
|
+
this.deterministic = config.deterministic;
|
|
60
|
+
this.dataAccess = config.dataAccess;
|
|
61
|
+
this.language = config.language;
|
|
62
|
+
this.body = config.body;
|
|
63
|
+
this.expression = config.expression;
|
|
64
|
+
this.bodyJs = config.bodyJs;
|
|
65
|
+
this.returns = config.returns;
|
|
66
|
+
this.ignoreSchemaChanges = config.ignoreSchemaChanges;
|
|
67
|
+
this.params = Object.entries(config.params ?? {}).map(([name, opts], index) => {
|
|
68
|
+
const explicitCustom = opts.customType ? toTypeInstance(opts.customType) : undefined;
|
|
69
|
+
const rawType = opts.type;
|
|
70
|
+
let type;
|
|
71
|
+
let customType;
|
|
72
|
+
if (rawType == null || typeof rawType === 'string') {
|
|
73
|
+
type = rawType ?? 'string';
|
|
74
|
+
customType = explicitCustom;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// A `Type` instance or constructor at `type` both defines the column (via
|
|
78
|
+
// `getColumnType` at schema-gen time) and acts as the default `customType` for
|
|
79
|
+
// marshalling. An explicit `customType` on the same param overrides the marshalling side.
|
|
80
|
+
const instance = toTypeInstance(rawType);
|
|
81
|
+
type = instance;
|
|
82
|
+
customType = explicitCustom ?? instance;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
direction: opts.direction ?? 'in',
|
|
87
|
+
type,
|
|
88
|
+
runtimeType: opts.runtimeType ?? 'any',
|
|
89
|
+
columnTypes: [typeof type === 'string' ? type : ''],
|
|
90
|
+
customType,
|
|
91
|
+
ref: opts.ref,
|
|
92
|
+
length: opts.length,
|
|
93
|
+
precision: opts.precision,
|
|
94
|
+
scale: opts.scale,
|
|
95
|
+
nullable: opts.nullable,
|
|
96
|
+
defaultRaw: opts.defaultRaw,
|
|
97
|
+
index,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
if (config.returns && typeof config.returns === 'object') {
|
|
101
|
+
// `returns: { type: SomeType }` is the canonical scalar-return shape — the Type drives
|
|
102
|
+
// both the column type (via `getColumnType` at schema-gen time) and the marshalling.
|
|
103
|
+
if ('type' in config.returns && config.returns.type) {
|
|
104
|
+
this.returnCustomType = toTypeInstance(config.returns.type);
|
|
105
|
+
}
|
|
106
|
+
else if ('customType' in config.returns && config.returns.customType) {
|
|
107
|
+
this.returnCustomType = toTypeInstance(config.returns.customType);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** @internal */
|
|
112
|
+
createParamMappingObject() {
|
|
113
|
+
return this.params.reduce((o, p) => {
|
|
114
|
+
o[p.name] = p.name;
|
|
115
|
+
return o;
|
|
116
|
+
}, {});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Overrides the inferred TArgs/TReturn. Omit a generic to keep inference; pass `never` in the
|
|
120
|
+
* args slot to refine only the return type.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* interface UserStats { totalOrders: number; lastOrderAt: Date }
|
|
125
|
+
* const GetStats = Routine.create<never, UserStats>({
|
|
126
|
+
* name: 'get_user_stats',
|
|
127
|
+
* type: 'function',
|
|
128
|
+
* params: { user_id: { type: 'int', runtimeType: 'number' } },
|
|
129
|
+
* returns: { runtimeType: 'object', columnType: 'json' },
|
|
130
|
+
* body: '...',
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
static create(config) {
|
|
135
|
+
return new Routine(config);
|
|
136
|
+
}
|
|
137
|
+
static is(item) {
|
|
138
|
+
return item instanceof Routine;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/metadata/index.d.ts
CHANGED
package/metadata/index.js
CHANGED
package/metadata/types.d.ts
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, TriggerDef } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef } from '../typings.js';
|
|
2
2
|
import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
5
5
|
import type { FilterOptions, FindOptions } from '../drivers/IDatabaseDriver.js';
|
|
6
6
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
7
|
+
/** Supported PostgreSQL partitioning strategies. */
|
|
8
|
+
export type EntityPartitionType = 'hash' | 'list' | 'range';
|
|
9
|
+
/**
|
|
10
|
+
* Partition key expression for PostgreSQL partitioned tables.
|
|
11
|
+
*
|
|
12
|
+
* You can use:
|
|
13
|
+
* - a property name, e.g. `'type'`
|
|
14
|
+
* - a list of property names for composite expressions, e.g. `['tenant', 'createdAt']`
|
|
15
|
+
* - a raw SQL expression string, e.g. `"date_trunc('month', created_at)"`
|
|
16
|
+
* - a callback that receives schema column mappings and returns SQL
|
|
17
|
+
*/
|
|
18
|
+
export type EntityPartitionExpression<E = AnyEntity> = (keyof E & string) | readonly (keyof E & string)[] | AnyString | ((columns: SchemaColumns<E>) => string);
|
|
19
|
+
/** Explicit child partition definition for PostgreSQL `list` and `range` partitioning. */
|
|
20
|
+
export interface EntityPartition<E = AnyEntity> {
|
|
21
|
+
/**
|
|
22
|
+
* Optional child table name. Defaults to `<tableName>_<index>`. A single `.` is treated as a
|
|
23
|
+
* `schema.table` separator; names with more than one `.` are rejected to avoid ambiguity.
|
|
24
|
+
*/
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Partition bound clause, either as `for values ...` or the trailing part such as `in (...)`, `from ... to ...`, or `default`. */
|
|
27
|
+
values: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* PostgreSQL table partitioning definition.
|
|
31
|
+
*
|
|
32
|
+
* - `hash` partitions generate child tables automatically from the partition count
|
|
33
|
+
* - `list` and `range` partitions require explicit child partition definitions
|
|
34
|
+
*/
|
|
35
|
+
export type EntityPartitionBy<E = AnyEntity> = {
|
|
36
|
+
type: Extract<EntityPartitionType, 'hash'>;
|
|
37
|
+
expression: EntityPartitionExpression<E>;
|
|
38
|
+
/**
|
|
39
|
+
* Hash partition fan-out: either a number (auto-named as `${tableName}_N`) or an array of
|
|
40
|
+
* explicit partition names in remainder order. Names may be `schema.name` to create the
|
|
41
|
+
* child in a non-default schema.
|
|
42
|
+
*/
|
|
43
|
+
partitions: number | readonly string[];
|
|
44
|
+
} | {
|
|
45
|
+
type: Exclude<EntityPartitionType, 'hash'>;
|
|
46
|
+
expression: EntityPartitionExpression<E>;
|
|
47
|
+
/** Explicit child partitions created in the order provided. */
|
|
48
|
+
partitions: EntityPartition<E>[];
|
|
49
|
+
};
|
|
7
50
|
export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
8
51
|
/** Override default collection/table name. Alias for `collection`. */
|
|
9
52
|
tableName?: string;
|
|
@@ -22,8 +65,10 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
22
65
|
* 3. Entity-level `@Entity({ orderBy })`
|
|
23
66
|
*/
|
|
24
67
|
orderBy?: QueryOrderMap<E> | QueryOrderMap<E>[];
|
|
25
|
-
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
|
|
26
|
-
|
|
68
|
+
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. Property name on the entity that stores the discriminator value. */
|
|
69
|
+
discriminator?: (T extends EntityClass<infer P> ? keyof P : string) | AnyString;
|
|
70
|
+
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. Override the discriminator column name (the property is named via `discriminator`; defaults to the naming-strategy-derived column). */
|
|
71
|
+
discriminatorColumn?: string;
|
|
27
72
|
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
|
|
28
73
|
discriminatorMap?: Dictionary<string>;
|
|
29
74
|
/** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
|
|
@@ -59,6 +104,13 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
59
104
|
hasTriggers?: boolean;
|
|
60
105
|
/** Database triggers to create for this entity's table. (SQL drivers only) */
|
|
61
106
|
triggers?: TriggerDef<E>[];
|
|
107
|
+
/**
|
|
108
|
+
* PostgreSQL partitioning definition for this table.
|
|
109
|
+
*
|
|
110
|
+
* Partitioned tables are tracked by the schema generator and introspected back from PostgreSQL.
|
|
111
|
+
* Changing an existing partition layout still requires a manual migration.
|
|
112
|
+
*/
|
|
113
|
+
partitionBy?: EntityPartitionBy<E>;
|
|
62
114
|
/** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
|
|
63
115
|
expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
|
|
64
116
|
/** Set {@doclink repositories#custom-repository | custom repository class}. */
|
|
@@ -331,6 +383,11 @@ export interface PropertyOptions<Owner> {
|
|
|
331
383
|
* Specify comment of column for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
|
|
332
384
|
*/
|
|
333
385
|
comment?: string;
|
|
386
|
+
/**
|
|
387
|
+
* Specify column-level collation for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
|
|
388
|
+
* Emits a `COLLATE` clause on the column definition; the accepted collation names are dialect-specific.
|
|
389
|
+
*/
|
|
390
|
+
collation?: string;
|
|
334
391
|
/** mysql only */
|
|
335
392
|
extra?: string;
|
|
336
393
|
/**
|
|
@@ -338,7 +395,7 @@ export interface PropertyOptions<Owner> {
|
|
|
338
395
|
*
|
|
339
396
|
* @see https://mikro-orm.io/docs/defining-entities#sql-generated-columns
|
|
340
397
|
*/
|
|
341
|
-
ignoreSchemaChanges?: ('type' | 'extra' | 'default')[];
|
|
398
|
+
ignoreSchemaChanges?: ('type' | 'extra' | 'default' | 'collation')[];
|
|
342
399
|
}
|
|
343
400
|
export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
|
|
344
401
|
/** Set target entity type. For polymorphic relations, pass an array of entity types. */
|
|
@@ -498,16 +555,16 @@ export interface EmbeddedOptions<Owner, Target> extends PropertyOptions<Owner> {
|
|
|
498
555
|
export interface EmbeddableOptions<Owner> {
|
|
499
556
|
/** Specify constructor parameters to be used in `em.create` or when `forceConstructor` is enabled. Those should be names of declared entity properties in the same order as your constructor uses them. The ORM tries to infer those automatically, use this option in case the inference fails. */
|
|
500
557
|
constructorParams?: (Owner extends EntityClass<infer P> ? keyof P : string)[];
|
|
501
|
-
/** For polymorphic embeddables.
|
|
558
|
+
/** For polymorphic embeddables. Property name on the embeddable class that stores the discriminator value. */
|
|
502
559
|
discriminator?: (Owner extends EntityClass<infer P> ? keyof P : string) | AnyString;
|
|
503
|
-
/** For polymorphic embeddables.
|
|
504
|
-
discriminatorColumn?:
|
|
560
|
+
/** For polymorphic embeddables. Override the discriminator column name (the property is named via `discriminator`; defaults to the naming-strategy-derived column). */
|
|
561
|
+
discriminatorColumn?: string;
|
|
505
562
|
discriminatorMap?: Dictionary<string>;
|
|
506
563
|
discriminatorValue?: number | string;
|
|
507
564
|
abstract?: boolean;
|
|
508
565
|
}
|
|
509
566
|
export interface EnumOptions<T> extends PropertyOptions<T> {
|
|
510
|
-
items?: readonly (number | string)[] | (() => Dictionary);
|
|
567
|
+
items?: readonly (number | string)[] | Dictionary | (() => Dictionary);
|
|
511
568
|
array?: boolean;
|
|
512
569
|
/** for postgres, by default it uses text column with check constraint */
|
|
513
570
|
nativeEnumName?: string;
|
|
@@ -551,6 +608,16 @@ interface BaseOptions<T, H extends string> {
|
|
|
551
608
|
include?: T extends EntityClass<infer P> ? Properties<P, H> : Properties<T, H>;
|
|
552
609
|
/** Fill factor for the index as a percentage 0-100 (PostgreSQL, MSSQL). */
|
|
553
610
|
fillFactor?: number;
|
|
611
|
+
/**
|
|
612
|
+
* Predicate for partial indexes. Object form is a `FilterQuery` and is portable across
|
|
613
|
+
* SQL drivers and MongoDB; string form is a raw SQL fragment (SQL drivers only).
|
|
614
|
+
*
|
|
615
|
+
* Native support: PostgreSQL, SQLite, MSSQL (`WHERE`), MongoDB (`partialFilterExpression`).
|
|
616
|
+
* MySQL 8.0.13+ / Oracle: emulated via `CASE WHEN` functional index (uniqueness only enforced
|
|
617
|
+
* where the predicate holds). MariaDB is not supported — it has no inline expression indexes;
|
|
618
|
+
* define a virtual generated column and index that instead.
|
|
619
|
+
*/
|
|
620
|
+
where?: string | FilterQuery<NoInfer<T> extends EntityClass<infer P> ? P : NoInfer<T>>;
|
|
554
621
|
}
|
|
555
622
|
export interface UniqueOptions<T, H extends string = string> extends BaseOptions<T, H> {
|
|
556
623
|
deferMode?: DeferMode | `${DeferMode}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.1-dev.0",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|