@mikro-orm/core 7.2.0-dev.9 → 7.2.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.
Files changed (69) hide show
  1. package/EntityManager.d.ts +41 -7
  2. package/EntityManager.js +225 -45
  3. package/MikroORM.js +3 -0
  4. package/README.md +1 -0
  5. package/connections/Connection.d.ts +3 -1
  6. package/drivers/DatabaseDriver.d.ts +14 -5
  7. package/drivers/DatabaseDriver.js +151 -52
  8. package/drivers/IDatabaseDriver.d.ts +1 -0
  9. package/entity/Collection.js +4 -2
  10. package/entity/EntityFactory.js +6 -0
  11. package/entity/EntityLoader.d.ts +7 -1
  12. package/entity/EntityLoader.js +46 -11
  13. package/entity/EntityRepository.d.ts +4 -5
  14. package/entity/EntityRepository.js +7 -2
  15. package/entity/defineEntity.d.ts +48 -14
  16. package/entity/defineEntity.js +32 -1
  17. package/enums.d.ts +5 -1
  18. package/enums.js +2 -0
  19. package/errors.d.ts +36 -0
  20. package/errors.js +90 -0
  21. package/events/EventManager.js +6 -3
  22. package/exceptions.d.ts +5 -0
  23. package/exceptions.js +5 -0
  24. package/hydration/ObjectHydrator.d.ts +2 -0
  25. package/hydration/ObjectHydrator.js +12 -8
  26. package/index.d.ts +1 -1
  27. package/metadata/MetadataDiscovery.d.ts +3 -0
  28. package/metadata/MetadataDiscovery.js +127 -17
  29. package/metadata/MetadataStorage.js +17 -1
  30. package/metadata/types.d.ts +19 -3
  31. package/package.json +1 -1
  32. package/platforms/Platform.d.ts +22 -3
  33. package/platforms/Platform.js +59 -1
  34. package/types/BigIntType.d.ts +1 -0
  35. package/types/BigIntType.js +23 -0
  36. package/types/DateTimeType.d.ts +1 -0
  37. package/types/DateTimeType.js +8 -0
  38. package/types/StringType.d.ts +14 -3
  39. package/types/StringType.js +34 -4
  40. package/types/TextType.d.ts +2 -4
  41. package/types/TextType.js +2 -8
  42. package/types/Type.d.ts +11 -0
  43. package/types/Type.js +4 -4
  44. package/types/index.d.ts +2 -2
  45. package/typings.d.ts +56 -2
  46. package/typings.js +24 -1
  47. package/unit-of-work/ChangeSetPersister.js +17 -13
  48. package/unit-of-work/UnitOfWork.js +11 -4
  49. package/utils/Configuration.d.ts +15 -1
  50. package/utils/Configuration.js +11 -1
  51. package/utils/Cursor.d.ts +2 -0
  52. package/utils/Cursor.js +43 -33
  53. package/utils/DataloaderUtils.js +2 -1
  54. package/utils/EntityComparator.d.ts +2 -0
  55. package/utils/EntityComparator.js +7 -3
  56. package/utils/QueryHelper.d.ts +12 -0
  57. package/utils/QueryHelper.js +75 -4
  58. package/utils/RawQueryFragment.d.ts +6 -0
  59. package/utils/RawQueryFragment.js +15 -6
  60. package/utils/TransactionManager.js +1 -1
  61. package/utils/Utils.d.ts +14 -2
  62. package/utils/Utils.js +31 -4
  63. package/utils/env-vars.js +1 -0
  64. package/utils/index.d.ts +1 -0
  65. package/utils/index.js +1 -0
  66. package/utils/rls-utils.d.ts +35 -0
  67. package/utils/rls-utils.js +97 -0
  68. package/utils/upsert-utils.d.ts +9 -1
  69. package/utils/upsert-utils.js +26 -3
@@ -117,6 +117,7 @@ export declare class MetadataDiscovery {
117
117
  private createSchemaTable;
118
118
  private initCheckConstraints;
119
119
  private initTriggers;
120
+ private initPolicies;
120
121
  private initGeneratedColumn;
121
122
  private getDefaultVersionValue;
122
123
  private inferDefaultValue;
@@ -125,6 +126,8 @@ export declare class MetadataDiscovery {
125
126
  private initVersionProperty;
126
127
  private initCustomType;
127
128
  private initRelation;
129
+ /** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
130
+ private initThroughRelation;
128
131
  private initColumnType;
129
132
  private getMappedType;
130
133
  private getPrefix;
@@ -165,10 +165,14 @@ export class MetadataDiscovery {
165
165
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
166
166
  filtered.forEach(meta => this.initCheckConstraints(meta));
167
167
  filtered.forEach(meta => this.initTriggers(meta));
168
- forEachProp((_m, p) => {
168
+ // filter names are load-bearing for RLS (policy and session variable names), backfill from the dictionary key
169
+ filtered.forEach(meta => Object.entries(meta.filters).forEach(([key, filter]) => (filter.name ??= key)));
170
+ filtered.forEach(meta => this.initPolicies(meta));
171
+ forEachProp((m, p) => {
169
172
  this.initDefaultValue(p);
170
173
  this.inferTypeFromDefault(p);
171
174
  this.initRelation(p);
175
+ this.initThroughRelation(m, p);
172
176
  this.initColumnType(p);
173
177
  });
174
178
  forEachProp((m, p) => this.initIndexes(m, p));
@@ -220,11 +224,9 @@ export class MetadataDiscovery {
220
224
  };
221
225
  const missing = [];
222
226
  this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
223
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
224
- const pivotEntity = prop.pivotEntity;
225
- const target = typeof pivotEntity === 'function' && !pivotEntity.prototype
226
- ? pivotEntity()
227
- : pivotEntity;
227
+ const indirect = (prop.kind === ReferenceKind.MANY_TO_MANY ? prop.pivotEntity : prop.through);
228
+ if (indirect) {
229
+ const target = typeof indirect === 'function' && !indirect.prototype ? indirect() : indirect;
228
230
  if (!this.#discovered.find(m => m.className === Utils.className(target)) || !discoveredByIdentity(target)) {
229
231
  missing.push(target);
230
232
  }
@@ -441,7 +443,18 @@ export class MetadataDiscovery {
441
443
  if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
442
444
  prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
443
445
  }
444
- else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
446
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.polymorphic) {
447
+ if (prop.targetMeta) {
448
+ // same layout as `initManyToOneFields` builds later: `[discriminatorColumn, ...fkIdColumns]`
449
+ const pkFields = prop.targetMeta.getPrimaryProps().flatMap(pk => {
450
+ this.initFieldName(pk);
451
+ return pk.fieldNames;
452
+ });
453
+ const idColumns = pkFields.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, pkFields.length > 1));
454
+ prop.fieldNames = [prop.discriminatorColumn, ...idColumns];
455
+ }
456
+ }
457
+ else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
445
458
  prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
446
459
  }
447
460
  else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
@@ -451,10 +464,13 @@ export class MetadataDiscovery {
451
464
  initManyToOneFieldName(prop, name) {
452
465
  const meta2 = prop.targetMeta;
453
466
  const ret = [];
454
- for (const primaryKey of meta2.primaryKeys) {
455
- this.initFieldName(meta2.properties[primaryKey]);
456
- for (const fieldName of meta2.properties[primaryKey].fieldNames) {
457
- ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK));
467
+ // with `targetKey` on a composite PK target, derive the FK field name from that property
468
+ // instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
469
+ const referencedKeys = prop.targetKey && meta2.compositePK ? [prop.targetKey] : meta2.primaryKeys;
470
+ for (const referencedKey of referencedKeys) {
471
+ this.initFieldName(meta2.properties[referencedKey]);
472
+ for (const fieldName of meta2.properties[referencedKey].fieldNames) {
473
+ ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, !prop.targetKey && meta2.compositePK));
458
474
  }
459
475
  }
460
476
  return ret;
@@ -1055,11 +1071,18 @@ export class MetadataDiscovery {
1055
1071
  }
1056
1072
  // TPT children have their own tables that don't contain the parent's columns,
1057
1073
  // so propagating parent indexes/uniques/checks/triggers would target missing columns.
1074
+ // deep equality, as the subclass items might be copies of the base class ones (e.g. with TC39 decorators)
1058
1075
  if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
1059
- meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1060
- meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1061
- meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1062
- meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1076
+ meta.indexes = Utils.unique([...base.indexes, ...meta.indexes], Utils.equals);
1077
+ meta.uniques = Utils.unique([...base.uniques, ...meta.uniques], Utils.equals);
1078
+ meta.checks = Utils.unique([...base.checks, ...meta.checks], Utils.equals);
1079
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers], Utils.equals);
1080
+ }
1081
+ // Policies pass down only from inlined abstract bases; STI children share the root table
1082
+ // and TPT children own their tables, so both declare policies directly on the root/child.
1083
+ if (base.abstract && base.inheritanceType !== 'sti' && (meta.inheritanceType !== 'tpt' || !meta.tptParent)) {
1084
+ meta.policies = Utils.unique([...base.policies, ...meta.policies]);
1085
+ meta.rowLevelSecurity ??= base.rowLevelSecurity;
1063
1086
  }
1064
1087
  const pks = Object.values(meta.properties)
1065
1088
  .filter(p => p.primary)
@@ -1247,8 +1270,17 @@ export class MetadataDiscovery {
1247
1270
  if (embeddedProp.nullable || refInArray) {
1248
1271
  meta.properties[name].nullable = true;
1249
1272
  }
1273
+ // polymorphic relations derive their column names from the discriminator, so prefix it too
1274
+ if (meta.properties[name].polymorphic && !object) {
1275
+ meta.properties[name].discriminator = prefix + meta.properties[name].discriminator;
1276
+ meta.properties[name].discriminatorColumn = prefix + meta.properties[name].discriminatorColumn;
1277
+ }
1250
1278
  if (meta.properties[name].fieldNames) {
1251
- meta.properties[name].fieldNames[0] = prefix + meta.properties[name].fieldNames[0];
1279
+ const { fieldNames, polymorphic } = meta.properties[name];
1280
+ // polymorphic `fieldNames` hold `[discriminatorColumn, ...fkIdColumns]`, so prefix all of them
1281
+ for (let i = 0; i < (polymorphic ? fieldNames.length : 1); i++) {
1282
+ fieldNames[i] = prefix + fieldNames[i];
1283
+ }
1252
1284
  }
1253
1285
  else {
1254
1286
  const name2 = meta.properties[name].name;
@@ -1677,6 +1709,9 @@ export class MetadataDiscovery {
1677
1709
  if (prop.persist === false || prop.nativeEnumName || !prop.items?.every(item => typeof item === 'string')) {
1678
1710
  continue;
1679
1711
  }
1712
+ if (prop.customType instanceof t.json || ['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
1713
+ continue;
1714
+ }
1680
1715
  this.initFieldName(prop);
1681
1716
  let expression = null;
1682
1717
  if (prop.enum) {
@@ -1716,6 +1751,28 @@ export class MetadataDiscovery {
1716
1751
  }
1717
1752
  meta.hasTriggers = true;
1718
1753
  }
1754
+ initPolicies(meta) {
1755
+ if (meta.policies.length === 0) {
1756
+ return;
1757
+ }
1758
+ const columns = meta.createSchemaColumnMappingObject();
1759
+ const table = this.createSchemaTable(meta);
1760
+ // resolve callbacks into a copy — the defs can be shared with siblings via an inlined abstract base,
1761
+ // and resolving in place would bake the first child's table into every other child's policy
1762
+ meta.policies = meta.policies.map(policy => {
1763
+ if (!(policy.using instanceof Function) && !(policy.check instanceof Function)) {
1764
+ return policy;
1765
+ }
1766
+ const resolved = { ...policy };
1767
+ if (resolved.using instanceof Function) {
1768
+ resolved.using = resolved.using(columns, table);
1769
+ }
1770
+ if (resolved.check instanceof Function) {
1771
+ resolved.check = resolved.check(columns, table);
1772
+ }
1773
+ return resolved;
1774
+ });
1775
+ }
1719
1776
  initGeneratedColumn(meta, prop) {
1720
1777
  if (!prop.generated && prop.columnTypes) {
1721
1778
  const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
@@ -2020,6 +2077,58 @@ export class MetadataDiscovery {
2020
2077
  }
2021
2078
  }
2022
2079
  }
2080
+ /** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
2081
+ initThroughRelation(meta, prop) {
2082
+ // already resolved, or not a through relation at all
2083
+ if (prop.through?.ownerProperty || !prop.through) {
2084
+ return;
2085
+ }
2086
+ if (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
2087
+ throw MetadataError.throughRelationInvalidKind(meta, prop);
2088
+ }
2089
+ const targetMeta = prop.targetMeta;
2090
+ // the subquery selects a single column
2091
+ if (targetMeta.compositePK) {
2092
+ throw MetadataError.throughRelationCompositeTarget(meta, prop);
2093
+ }
2094
+ const through = prop.through;
2095
+ const throughMeta = this.#metadata.get(!through.prototype ? through() : through);
2096
+ // a property is considered to point at an entity when it targets it or one of its parents
2097
+ const pointsTo = (p, m) => {
2098
+ const candidate = this.#metadata.find(p.target);
2099
+ /* v8 ignore next 3 */
2100
+ if (!candidate) {
2101
+ return false;
2102
+ }
2103
+ return candidate.class === m.class || m.class.prototype instanceof candidate.class;
2104
+ };
2105
+ const fks = Object.values(throughMeta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
2106
+ const ownerProp = fks.find(p => pointsTo(p, meta));
2107
+ if (!ownerProp) {
2108
+ throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'owner');
2109
+ }
2110
+ let targetProperty;
2111
+ const selectsTarget = throughMeta.class === targetMeta.class || throughMeta.class.prototype instanceof targetMeta.class;
2112
+ if (!selectsTarget) {
2113
+ const targetProp = fks.find(p => p !== ownerProp && pointsTo(p, targetMeta));
2114
+ if (!targetProp) {
2115
+ throw MetadataError.throughRelationMissingProperty(meta, prop, throughMeta, 'target');
2116
+ }
2117
+ targetProperty = targetProp.name;
2118
+ }
2119
+ prop.through = {
2120
+ entity: throughMeta.class,
2121
+ where: prop.where,
2122
+ orderBy: prop.orderBy ? Utils.asArray(prop.orderBy) : undefined,
2123
+ ownerProperty: ownerProp.name,
2124
+ targetProperty,
2125
+ };
2126
+ // the condition and ordering apply to the `through` entity, not to the target, so they must not leak into the target joins
2127
+ delete prop.where;
2128
+ delete prop.orderBy;
2129
+ prop.persist = false;
2130
+ prop.formula = columns => this.#platform.getThroughRelationFormula(prop, columns);
2131
+ }
2023
2132
  initColumnType(prop) {
2024
2133
  this.initUnsigned(prop);
2025
2134
  // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
@@ -2080,6 +2189,7 @@ export class MetadataDiscovery {
2080
2189
  prop.columnTypes.push(...columnTypes);
2081
2190
  if (!targetMeta.compositePK || prop.targetKey) {
2082
2191
  prop.customType = referencedProp.customType;
2192
+ prop.collation ??= referencedProp.collation;
2083
2193
  }
2084
2194
  }
2085
2195
  }
@@ -2135,7 +2245,7 @@ export class MetadataDiscovery {
2135
2245
  shouldForceConstructorUsage(meta) {
2136
2246
  const forceConstructor = this.#config.get('forceEntityConstructor');
2137
2247
  if (Array.isArray(forceConstructor)) {
2138
- return forceConstructor.some(cls => Utils.className(cls) === meta.className);
2248
+ return forceConstructor.some(cls => Utils.matchesEntity(cls, meta));
2139
2249
  }
2140
2250
  return forceConstructor;
2141
2251
  }
@@ -17,6 +17,7 @@ export class MetadataStorage {
17
17
  #idMap;
18
18
  #classNameMap;
19
19
  #uniqueNameMap;
20
+ #ambiguousNames = new Set();
20
21
  constructor(metadata = {}) {
21
22
  this.#idMap = {};
22
23
  this.#uniqueNameMap = {};
@@ -64,6 +65,10 @@ export class MetadataStorage {
64
65
  }
65
66
  /** Returns metadata for the given entity, optionally initializing it if not found. */
66
67
  get(entityName, init = false) {
68
+ // string lookups cannot be resolved when several classes were minified to the same name
69
+ if (typeof entityName === 'string' && this.#ambiguousNames.has(entityName)) {
70
+ throw MetadataError.ambiguousEntityName(entityName);
71
+ }
67
72
  const exists = this.find(entityName);
68
73
  if (exists) {
69
74
  return exists;
@@ -99,7 +104,13 @@ export class MetadataStorage {
99
104
  this.#metadataMap.set(entityName, meta);
100
105
  this.#idMap[meta._id] = meta;
101
106
  this.#uniqueNameMap[meta.uniqueName] = meta;
102
- this.#classNameMap[Utils.className(entityName)] = meta;
107
+ const className = Utils.className(entityName);
108
+ const existing = this.#classNameMap[className];
109
+ // track name collisions caused by minifiers mangling two classes to the same name
110
+ if (existing && existing !== meta && existing.class !== meta.class) {
111
+ this.#ambiguousNames.add(className);
112
+ }
113
+ this.#classNameMap[className] = meta;
103
114
  return meta;
104
115
  }
105
116
  /** Removes metadata for the given entity from all internal maps. */
@@ -110,6 +121,11 @@ export class MetadataStorage {
110
121
  delete this.#idMap[meta._id];
111
122
  delete this.#uniqueNameMap[meta.uniqueName];
112
123
  delete this.#classNameMap[meta.className];
124
+ // the name may still be ambiguous among the remaining metas
125
+ const remaining = new Set([...this.#metadataMap.values()].filter(m => m.className === meta.className).map(m => m.class));
126
+ if (remaining.size <= 1) {
127
+ this.#ambiguousNames.delete(meta.className);
128
+ }
113
129
  }
114
130
  }
115
131
  /** Decorates all entity prototypes with helper methods (e.g. init, toJSON). */
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef } from '../typings.js';
1
+ import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, SchemaColumns, TriggerDef, PolicyDef } 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';
@@ -104,6 +104,10 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
104
104
  hasTriggers?: boolean;
105
105
  /** Database triggers to create for this entity's table. (SQL drivers only) */
106
106
  triggers?: TriggerDef<E>[];
107
+ /** PostgreSQL row level security policies for this entity's table. Declaring policies implicitly enables RLS. */
108
+ policies?: PolicyDef<E>[];
109
+ /** Enables PostgreSQL row level security on this entity's table. `'force'` also enforces it for the table owner. Set to `false` to keep declared policies staged while leaving RLS disabled. */
110
+ rowLevelSecurity?: boolean | 'force';
107
111
  /**
108
112
  * PostgreSQL partitioning definition for this table.
109
113
  *
@@ -427,9 +431,15 @@ interface PolymorphicOptions {
427
431
  */
428
432
  discriminatorMap?: Dictionary<string>;
429
433
  }
430
- export interface ManyToOneOptions<Owner, Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
434
+ export interface ManyToOneOptions<Owner, Target, Through = Target> extends ReferenceOptions<Owner, Target>, PolymorphicOptions {
431
435
  /** Point to the inverse side property name. */
432
436
  inversedBy?: (string & keyof Target) | ((e: Target) => any);
437
+ /** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
438
+ through?: () => EntityName<Through>;
439
+ /** Condition applied on the `through` entity. */
440
+ where?: FilterQuery<Through>;
441
+ /** Ordering applied on the `through` entity, the first matching row is used. */
442
+ orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
433
443
  /** Wrap the entity in {@apilink Reference} wrapper. */
434
444
  ref?: boolean;
435
445
  /** Use this relation as a primary key. */
@@ -481,9 +491,15 @@ export interface OneToManyOptions<Owner, Target> extends ReferenceOptions<Owner,
481
491
  /** Point to the owning side property name. */
482
492
  mappedBy: (string & keyof Target) | ((e: Target) => any);
483
493
  }
484
- export interface OneToOneOptions<Owner, Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy'>>, PolymorphicOptions {
494
+ export interface OneToOneOptions<Owner, Target, Through = Target> extends Partial<Omit<OneToManyOptions<Owner, Target>, 'orderBy' | 'where'>>, PolymorphicOptions {
485
495
  /** Set this side as owning. Owning side is where the foreign key is defined. This option is not required if you use `inversedBy` or `mappedBy` to distinguish owning and inverse side. */
486
496
  owner?: boolean;
497
+ /** Resolve this read-only relation via a subquery on another entity: a pivot entity with FKs to both sides, or the target itself to pick a single item out of a to-many relation (see {@doclink relationships#to-one-relations-through-another-entity | To-one relations through another entity}). */
498
+ through?: () => EntityName<Through>;
499
+ /** Condition for {@doclink collections#declarative-partial-loading | Declarative partial loading}, or the condition applied on the `through` entity. */
500
+ where?: FilterQuery<Through>;
501
+ /** Ordering applied on the `through` entity, the first matching row is used. */
502
+ orderBy?: QueryOrderMap<Through> | QueryOrderMap<Through>[];
487
503
  /** Point to the inverse side property name. */
488
504
  inversedBy?: (string & keyof Target) | ((e: Target) => any);
489
505
  /** Wrap the entity in {@apilink Reference} wrapper. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.0-dev.9",
3
+ "version": "7.2.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",
@@ -1,6 +1,6 @@
1
1
  import { EntityRepository } from '../entity/EntityRepository.js';
2
2
  import { type NamingStrategy } from '../naming-strategy/NamingStrategy.js';
3
- import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey } from '../typings.js';
3
+ import type { Constructor, EntityMetadata, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, SimpleColumnMeta, FilterQuery, EntityValue, EntityKey, FormulaColumns } from '../typings.js';
4
4
  import { ExceptionConverter } from './ExceptionConverter.js';
5
5
  import type { EntityManager } from '../EntityManager.js';
6
6
  import type { Configuration } from '../utils/Configuration.js';
@@ -211,8 +211,9 @@ export declare abstract class Platform {
211
211
  getBlobDeclarationSQL(): string;
212
212
  getJsonDeclarationSQL(): string;
213
213
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | Raw;
214
- getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | Raw;
215
- processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean): FilterQuery<T>;
214
+ /** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
215
+ getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | Raw;
216
+ processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean | string): FilterQuery<T>;
216
217
  protected getJsonValueType(value: unknown): string;
217
218
  getJsonIndexDefinition(index: {
218
219
  columnNames: string[];
@@ -229,6 +230,10 @@ export declare abstract class Platform {
229
230
  convertsJsonAutomatically(): boolean;
230
231
  /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
231
232
  preservesDatesInsideJson(): boolean;
233
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
234
+ supportsNullsOrdering(): boolean;
235
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
236
+ sortsNullsLowest(): boolean;
232
237
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
233
238
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
234
239
  /** Converts a database JSON value to its JS representation. */
@@ -274,6 +279,11 @@ export declare abstract class Platform {
274
279
  formatQuery(sql: string, params: readonly any[]): string;
275
280
  /** Deep-clones embeddable data and tags it for JSON serialization. */
276
281
  cloneEmbeddable<T>(data: T): T;
282
+ /**
283
+ * Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
284
+ * @internal
285
+ */
286
+ getThroughRelationFormula(prop: EntityProperty, columns: FormulaColumns<any>): string;
277
287
  /** Initializes the platform with the ORM configuration. */
278
288
  setConfig(config: Configuration): void;
279
289
  /** Returns the current ORM configuration. */
@@ -315,6 +325,15 @@ export declare abstract class Platform {
315
325
  supportsDownMigrations(): boolean;
316
326
  /** Whether the platform supports deferred unique constraints. */
317
327
  supportsDeferredUniqueConstraints(): boolean;
328
+ /** Whether the platform supports row level security (PostgreSQL). */
329
+ supportsRowLevelSecurity(): boolean;
330
+ /** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
331
+ supportsConnectionSessionContext(): boolean;
332
+ /**
333
+ * SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
334
+ * variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
335
+ */
336
+ getCurrentSettingCast(mappedType: Type<unknown>): string | null;
318
337
  /** Platform-specific validation of entity metadata. */
319
338
  validateMetadata(meta: EntityMetadata): void;
320
339
  /**
@@ -413,6 +413,7 @@ export class Platform {
413
413
  getSearchJsonPropertySQL(path, type, aliased) {
414
414
  return path;
415
415
  }
416
+ /** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
416
417
  getSearchJsonPropertyKey(path, type, aliased, value) {
417
418
  return path.join('.');
418
419
  }
@@ -424,7 +425,8 @@ export class Platform {
424
425
  return o;
425
426
  }
426
427
  if (path.length === 1) {
427
- o[path[0]] = value;
428
+ const key = typeof alias === 'string' ? `${alias}.${path[0]}` : path[0];
429
+ o[key] = value;
428
430
  return o;
429
431
  }
430
432
  const type = this.getJsonValueType(value);
@@ -469,6 +471,14 @@ export class Platform {
469
471
  preservesDatesInsideJson() {
470
472
  return false;
471
473
  }
474
+ /** Whether `nulls first`/`nulls last` can be requested in an `orderBy`. */
475
+ supportsNullsOrdering() {
476
+ return true;
477
+ }
478
+ /** Where nulls land when an `orderBy` requests no explicit placement: lowest (`asc` puts them first) or highest. */
479
+ sortsNullsLowest() {
480
+ return false;
481
+ }
472
482
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
473
483
  convertJsonToDatabaseValue(value, context) {
474
484
  return JSON.stringify(value);
@@ -614,6 +624,14 @@ export class Platform {
614
624
  Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });
615
625
  return copy;
616
626
  }
627
+ /**
628
+ * Builds the correlated subquery used as the formula of a virtual to-one relation defined via `through`.
629
+ * @internal
630
+ */
631
+ /* v8 ignore next 3 */
632
+ getThroughRelationFormula(prop, columns) {
633
+ throw new Error(`${this.constructor.name} does not support the 'through' option of ${prop.name}`);
634
+ }
617
635
  /** Initializes the platform with the ORM configuration. */
618
636
  setConfig(config) {
619
637
  this.config = config;
@@ -720,11 +738,51 @@ export class Platform {
720
738
  supportsDeferredUniqueConstraints() {
721
739
  return true;
722
740
  }
741
+ /** Whether the platform supports row level security (PostgreSQL). */
742
+ supportsRowLevelSecurity() {
743
+ return false;
744
+ }
745
+ /** Whether the driver can apply the session context on every pooled connection acquire (`sessionContext: 'connection'`). */
746
+ supportsConnectionSessionContext() {
747
+ return false;
748
+ }
749
+ /**
750
+ * SQL cast suffix (e.g. `'::uuid'`, or `''` when none is needed) applied when an RLS filter reads a session
751
+ * variable via `current_setting()` as the given column type, or `null` if the type has no automatic cast.
752
+ */
753
+ getCurrentSettingCast(mappedType) {
754
+ return null;
755
+ }
723
756
  /** Platform-specific validation of entity metadata. */
724
757
  validateMetadata(meta) {
725
758
  if (meta.partitionBy && !this.supportsPartitionedTables()) {
726
759
  throw new MetadataError(`Entity ${meta.className} uses partitionBy, but ${this.constructor.name} does not support partitioned tables`);
727
760
  }
761
+ const declaresRls = meta.policies.length > 0 || !!meta.rowLevelSecurity;
762
+ if (declaresRls && !this.supportsRowLevelSecurity()) {
763
+ throw MetadataError.rowLevelSecurityNotSupportedByDriver(meta);
764
+ }
765
+ // STI hierarchies share a single table, so only the root may declare policies; `root` is optional-chained
766
+ // as `validateMetadata` is public API and tolerates partially populated metadata
767
+ if (declaresRls && meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
768
+ throw MetadataError.rowLevelSecurityOnNonRootStiEntity(meta);
769
+ }
770
+ if (meta.root?.inheritanceType === 'sti' && meta.root !== meta) {
771
+ for (const filter of Object.values(meta.filters)) {
772
+ // inherited root filters share the def object; only defs declared on the child itself are a problem,
773
+ // as non-root STI metas never reach the schema generator and the policy would silently not exist
774
+ if (filter.rls && meta.root.filters[filter.name] !== filter) {
775
+ throw MetadataError.rlsFilterOnNonRootStiEntity(meta, filter.name);
776
+ }
777
+ }
778
+ }
779
+ if (!this.supportsRowLevelSecurity()) {
780
+ for (const filter of Object.values(meta.filters)) {
781
+ if (filter.rls) {
782
+ throw MetadataError.rlsFilterNotSupportedByDriver(meta, filter.name);
783
+ }
784
+ }
785
+ }
728
786
  }
729
787
  /**
730
788
  * Generates a custom order by statement given a set of in order values, eg.
@@ -11,6 +11,7 @@ export declare class BigIntType<Mode extends 'bigint' | 'number' | 'string' = 'b
11
11
  convertToDatabaseValue(value: JSTypeByMode<Mode> | null | undefined): string | null | undefined;
12
12
  convertToJSValue(value: string | bigint | null | undefined): JSTypeByMode<Mode> | null | undefined;
13
13
  toJSON(value: JSTypeByMode<Mode> | null | undefined): JSTypeByMode<Mode> | null | undefined;
14
+ fromJSON(value: unknown): JSTypeByMode<Mode> | null | undefined;
14
15
  getColumnType(prop: EntityProperty, platform: Platform): string;
15
16
  compareAsType(): string;
16
17
  compareValues(a: string, b: string): boolean;
@@ -1,4 +1,5 @@
1
1
  import { Type } from './Type.js';
2
+ import { ValidationError } from '../errors.js';
2
3
  /**
3
4
  * This type will automatically convert string values returned from the database to native JS bigints (default)
4
5
  * or numbers (safe only for values up to `Number.MAX_SAFE_INTEGER`), or strings, depending on the `mode`.
@@ -36,6 +37,28 @@ export class BigIntType extends Type {
36
37
  }
37
38
  return this.convertToDatabaseValue(value);
38
39
  }
40
+ fromJSON(value) {
41
+ // the serialized form is a decimal string, or a plain number in `number` mode
42
+ const valid = (typeof value === 'string' && /^-?\d+$/.test(value)) || (typeof value === 'number' && Number.isInteger(value));
43
+ if (!valid) {
44
+ throw ValidationError.invalidType(BigIntType, value, 'JSON');
45
+ }
46
+ switch (this.mode) {
47
+ case 'number': {
48
+ // `Number` silently rounds past `MAX_SAFE_INTEGER`, tampered cursors must fail loudly
49
+ const num = Number(value);
50
+ if (!Number.isSafeInteger(num)) {
51
+ throw ValidationError.invalidType(BigIntType, value, 'JSON');
52
+ }
53
+ return num;
54
+ }
55
+ case 'string':
56
+ return String(value);
57
+ case 'bigint':
58
+ default:
59
+ return BigInt(value);
60
+ }
61
+ }
39
62
  getColumnType(prop, platform) {
40
63
  return platform.getBigIntTypeDeclarationSQL(prop);
41
64
  }
@@ -5,6 +5,7 @@ import type { EntityProperty } from '../typings.js';
5
5
  export declare class DateTimeType extends Type<Date, string> {
6
6
  getColumnType(prop: EntityProperty, platform: Platform): string;
7
7
  compareAsType(): string;
8
+ fromJSON(value: unknown): Date;
8
9
  get runtimeType(): string;
9
10
  ensureComparable(): boolean;
10
11
  getDefaultLength(platform: Platform): number;
@@ -1,4 +1,5 @@
1
1
  import { Type } from './Type.js';
2
+ import { ValidationError } from '../errors.js';
2
3
  /** Maps a database DATETIME/TIMESTAMP column to a JS `Date` object. */
3
4
  export class DateTimeType extends Type {
4
5
  getColumnType(prop, platform) {
@@ -7,6 +8,13 @@ export class DateTimeType extends Type {
7
8
  compareAsType() {
8
9
  return 'Date';
9
10
  }
11
+ fromJSON(value) {
12
+ const date = new Date(value);
13
+ if (typeof value !== 'string' || Number.isNaN(date.getTime())) {
14
+ throw ValidationError.invalidType(DateTimeType, value, 'JSON');
15
+ }
16
+ return date;
17
+ }
10
18
  get runtimeType() {
11
19
  return 'Date';
12
20
  }
@@ -1,10 +1,21 @@
1
1
  import { Type } from './Type.js';
2
2
  import type { Platform } from '../platforms/Platform.js';
3
3
  import type { EntityProperty } from '../typings.js';
4
- /** Maps a database VARCHAR column to a JS `string`. */
5
- export declare class StringType extends Type<string | null | undefined, string | null | undefined> {
6
- getColumnType(prop: EntityProperty, platform: Platform): string;
4
+ export interface StringTypeOptions {
5
+ trim?: boolean;
6
+ case?: 'upper' | 'lower';
7
+ }
8
+ /** @internal */
9
+ export declare abstract class BaseStringType extends Type<string | null | undefined, string | null | undefined> {
10
+ readonly options: StringTypeOptions;
11
+ constructor(options?: StringTypeOptions);
12
+ convertToDatabaseValue(value: string | null | undefined): string | null | undefined;
7
13
  compareAsType(): string;
8
14
  ensureComparable(): boolean;
15
+ private normalize;
16
+ }
17
+ /** Maps a database VARCHAR column to a JS `string`. */
18
+ export declare class StringType extends BaseStringType {
19
+ getColumnType(prop: EntityProperty, platform: Platform): string;
9
20
  getDefaultLength(platform: Platform): number;
10
21
  }
@@ -1,8 +1,17 @@
1
1
  import { Type } from './Type.js';
2
- /** Maps a database VARCHAR column to a JS `string`. */
3
- export class StringType extends Type {
4
- getColumnType(prop, platform) {
5
- return platform.getVarcharTypeDeclarationSQL(prop);
2
+ /** @internal */
3
+ export class BaseStringType extends Type {
4
+ options;
5
+ constructor(options = {}) {
6
+ super();
7
+ this.options = options;
8
+ // a defined `compareValues` replaces the inline `!==` comparator, so only provide it when normalization is configured
9
+ if (options.trim || options.case) {
10
+ this.compareValues = (a, b) => this.normalize(a) === this.normalize(b);
11
+ }
12
+ }
13
+ convertToDatabaseValue(value) {
14
+ return this.normalize(value);
6
15
  }
7
16
  compareAsType() {
8
17
  return 'string';
@@ -10,6 +19,27 @@ export class StringType extends Type {
10
19
  ensureComparable() {
11
20
  return false;
12
21
  }
22
+ normalize(value) {
23
+ if (value == null) {
24
+ return value;
25
+ }
26
+ if (this.options.trim) {
27
+ value = value.trim();
28
+ }
29
+ if (this.options.case === 'upper') {
30
+ return value.toUpperCase();
31
+ }
32
+ if (this.options.case === 'lower') {
33
+ return value.toLowerCase();
34
+ }
35
+ return value;
36
+ }
37
+ }
38
+ /** Maps a database VARCHAR column to a JS `string`. */
39
+ export class StringType extends BaseStringType {
40
+ getColumnType(prop, platform) {
41
+ return platform.getVarcharTypeDeclarationSQL(prop);
42
+ }
13
43
  getDefaultLength(platform) {
14
44
  return platform.getDefaultVarcharLength();
15
45
  }