@mikro-orm/core 7.1.0-dev.1 → 7.1.0-dev.11

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 (49) hide show
  1. package/EntityManager.d.ts +63 -11
  2. package/EntityManager.js +163 -18
  3. package/drivers/DatabaseDriver.d.ts +1 -0
  4. package/drivers/DatabaseDriver.js +3 -0
  5. package/drivers/IDatabaseDriver.d.ts +30 -1
  6. package/entity/BaseEntity.d.ts +70 -3
  7. package/entity/BaseEntity.js +18 -0
  8. package/entity/Collection.d.ts +6 -3
  9. package/entity/Collection.js +15 -4
  10. package/entity/EntityFactory.js +20 -1
  11. package/entity/EntityLoader.d.ts +3 -1
  12. package/entity/EntityLoader.js +39 -16
  13. package/entity/EntityRepository.d.ts +27 -9
  14. package/entity/EntityRepository.js +12 -0
  15. package/entity/Reference.d.ts +42 -1
  16. package/entity/Reference.js +9 -0
  17. package/entity/WrappedEntity.d.ts +2 -2
  18. package/entity/defineEntity.d.ts +81 -14
  19. package/entity/defineEntity.js +10 -6
  20. package/entity/utils.js +4 -5
  21. package/errors.d.ts +2 -0
  22. package/errors.js +4 -0
  23. package/index.d.ts +2 -2
  24. package/index.js +1 -1
  25. package/metadata/MetadataDiscovery.d.ts +11 -0
  26. package/metadata/MetadataDiscovery.js +127 -9
  27. package/metadata/MetadataValidator.js +9 -0
  28. package/metadata/types.d.ts +10 -2
  29. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  30. package/naming-strategy/NamingStrategy.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/platforms/Platform.d.ts +5 -0
  33. package/platforms/Platform.js +7 -0
  34. package/serialization/EntitySerializer.d.ts +18 -6
  35. package/serialization/EntitySerializer.js +31 -5
  36. package/typings.d.ts +152 -14
  37. package/typings.js +19 -2
  38. package/utils/AbstractMigrator.d.ts +11 -1
  39. package/utils/AbstractMigrator.js +203 -0
  40. package/utils/Configuration.d.ts +7 -0
  41. package/utils/Configuration.js +1 -0
  42. package/utils/DataloaderUtils.d.ts +10 -1
  43. package/utils/DataloaderUtils.js +78 -0
  44. package/utils/QueryHelper.d.ts +16 -0
  45. package/utils/QueryHelper.js +15 -0
  46. package/utils/Utils.d.ts +1 -1
  47. package/utils/Utils.js +1 -1
  48. package/utils/fs-utils.d.ts +2 -0
  49. package/utils/fs-utils.js +7 -1
@@ -1,6 +1,6 @@
1
1
  import type { EntityManager } from '../EntityManager.js';
2
2
  import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions } from '../metadata/types.js';
3
- import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, IndexCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise } from '../typings.js';
3
+ import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, LazyRef, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints } from '../typings.js';
4
4
  import type { Raw } from '../utils/RawQueryFragment.js';
5
5
  import type { ScalarReference } from './Reference.js';
6
6
  import type { SerializeOptions } from '../serialization/EntitySerializer.js';
@@ -12,7 +12,7 @@ import type { Collection } from './Collection.js';
12
12
  import type { FilterOptions, FindOptions, FindOneOptions } from '../drivers/IDatabaseDriver.js';
13
13
  /** Union of all option keys supported across all property definition types (scalar, enum, embedded, relations). */
14
14
  export type UniversalPropertyKeys = keyof PropertyOptions<any> | keyof EnumOptions<any> | keyof EmbeddedOptions<any, any> | keyof ReferenceOptions<any, any> | keyof ManyToOneOptions<any, any> | keyof OneToManyOptions<any, any> | keyof OneToOneOptions<any, any> | keyof ManyToManyOptions<any, any>;
15
- type BuilderExtraKeys = '~options' | '~type' | '$type' | 'strictNullable';
15
+ type BuilderExtraKeys = '~options' | '~type' | '$type' | 'strictNullable' | 'lazyRef';
16
16
  type ExcludeKeys = 'entity' | 'items';
17
17
  type BuilderKeys = Exclude<UniversalPropertyKeys, ExcludeKeys> | BuilderExtraKeys;
18
18
  type IncludeKeysForProperty = Exclude<keyof PropertyOptions<any>, ExcludeKeys> | BuilderExtraKeys;
@@ -36,9 +36,23 @@ export interface PropertyChain<Value, Options> {
36
36
  nullable: true;
37
37
  strictNullable: true;
38
38
  }>;
39
- ref(): PropertyChain<Value, Omit<Options, 'ref'> & {
39
+ ref(): Options extends {
40
+ lazyRef: true;
41
+ } ? never : PropertyChain<Value, Omit<Options, 'ref'> & {
40
42
  ref: true;
41
43
  }>;
44
+ /**
45
+ * Mark a to-one relation (`m:1`/`1:1`) as a {@apilink LazyRef} — direct entity at runtime, type-restricted
46
+ * until `Loaded<>` narrows it. Type-level only, no `ref: true` wrapping. Not compatible with `.ref()` or
47
+ * `.mapToPk()` — those chains are rejected in both directions at compile time.
48
+ */
49
+ lazyRef(): HasKind<Options, 'm:1' | '1:1'> extends true ? Options extends {
50
+ ref: true;
51
+ } | {
52
+ mapToPk: true;
53
+ } ? never : PropertyChain<Value, Omit<Options, 'lazyRef'> & {
54
+ lazyRef: true;
55
+ }> : never;
42
56
  primary(): PropertyChain<Value, Omit<Options, 'primary'> & {
43
57
  primary: true;
44
58
  }>;
@@ -104,8 +118,16 @@ export interface PropertyChain<Value, Options> {
104
118
  customOrder(...customOrder: string[] | number[] | boolean[]): PropertyChain<Value, Options>;
105
119
  extra(extra: string): PropertyChain<Value, Options>;
106
120
  ignoreSchemaChanges(...ignoreSchemaChanges: ('type' | 'extra' | 'default')[]): PropertyChain<Value, Options>;
107
- index(index?: boolean | string): PropertyChain<Value, Options>;
108
- unique(unique?: boolean | string): PropertyChain<Value, Options>;
121
+ /** Explicitly specify index on a property. When a string name is passed, it enables type-safe `using` in `FindOptions`. */
122
+ index<N extends string>(name: N): PropertyChain<Value, Omit<Options, 'index'> & {
123
+ index: N;
124
+ }>;
125
+ index(index?: boolean): PropertyChain<Value, Options>;
126
+ /** Set column as unique. When a string name is passed, it enables type-safe `using` in `FindOptions`. (SQL only) */
127
+ unique<N extends string>(name: N): PropertyChain<Value, Omit<Options, 'unique'> & {
128
+ unique: N;
129
+ }>;
130
+ unique(unique?: boolean): PropertyChain<Value, Options>;
109
131
  comment(comment: string): PropertyChain<Value, Options>;
110
132
  accessor(accessor?: string | boolean): PropertyChain<Value, Options>;
111
133
  eager(eager?: boolean): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
@@ -117,7 +139,9 @@ export interface PropertyChain<Value, Options> {
117
139
  owner(): HasKind<Options, '1:1' | 'm:n'> extends true ? PropertyChain<Value, Omit<Options, 'owner'> & {
118
140
  owner: true;
119
141
  }> : never;
120
- mapToPk(): HasKind<Options, 'm:1' | '1:1'> extends true ? PropertyChain<Value, Omit<Options, 'mapToPk'> & {
142
+ mapToPk(): HasKind<Options, 'm:1' | '1:1'> extends true ? Options extends {
143
+ lazyRef: true;
144
+ } ? never : PropertyChain<Value, Omit<Options, 'mapToPk'> & {
121
145
  mapToPk: true;
122
146
  }> : never;
123
147
  orphanRemoval(orphanRemoval?: boolean): HasKind<Options, '1:m' | '1:1'> extends true ? PropertyChain<Value, Options> : never;
@@ -127,9 +151,13 @@ export interface PropertyChain<Value, Options> {
127
151
  pivotEntity(pivotEntity: () => EntityName): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
128
152
  fixedOrder(fixedOrder?: boolean): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
129
153
  fixedOrderColumn(fixedOrderColumn: string): HasKind<Options, 'm:n'> extends true ? PropertyChain<Value, Options> : never;
130
- array(): HasKind<Options, 'embedded' | 'enum'> extends true ? PropertyChain<Value, Omit<Options, 'array'> & {
154
+ array(): Options extends {
155
+ kind: infer K extends string;
156
+ } ? K extends 'embedded' | 'enum' ? PropertyChain<Value, Omit<Options, 'array'> & {
131
157
  array: true;
132
- }> : never;
158
+ }> : never : PropertyChain<Value, Omit<Options, 'array'> & {
159
+ array: true;
160
+ }>;
133
161
  prefix(prefix: string | boolean): HasKind<Options, 'embedded'> extends true ? PropertyChain<Value, Options> : never;
134
162
  prefixMode(prefixMode: EmbeddedPrefixMode): HasKind<Options, 'embedded'> extends true ? PropertyChain<Value, Options> : never;
135
163
  object(object?: boolean): HasKind<Options, 'embedded'> extends true ? PropertyChain<Value, Options> : never;
@@ -313,9 +341,24 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
313
341
  /**
314
342
  * Enable `ScalarReference` wrapper for lazy values. Use this in combination with `lazy: true` to have a type-safe accessor object in place of the value.
315
343
  */
316
- ref(): UniversalPropertyOptionsBuilder<Value, Omit<Options, 'ref'> & {
344
+ ref(): Options extends {
345
+ lazyRef: true;
346
+ } ? never : UniversalPropertyOptionsBuilder<Value, Omit<Options, 'ref'> & {
317
347
  ref: true;
318
348
  }, IncludeKeys>;
349
+ /**
350
+ * Mark a to-one relation (`m:1`/`1:1`) as a {@apilink LazyRef} — the runtime value stays a plain entity
351
+ * (no `Reference` wrapper) but the TypeScript type restricts access to the primary key until `Loaded<>`
352
+ * narrows it. Type-level only; does not set `ref: true`. Not compatible with `.ref()` or `.mapToPk()` —
353
+ * those chains are rejected in both directions at compile time.
354
+ */
355
+ lazyRef(): HasKind<Options, 'm:1' | '1:1'> extends true ? Options extends {
356
+ ref: true;
357
+ } | {
358
+ mapToPk: true;
359
+ } ? never : UniversalPropertyOptionsBuilder<Value, Omit<Options, 'lazyRef'> & {
360
+ lazyRef: true;
361
+ }, IncludeKeys> : never;
319
362
  /**
320
363
  * Set to true to omit the property when {@link https://mikro-orm.io/docs/serializing Serializing}.
321
364
  */
@@ -334,12 +377,20 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
334
377
  concurrencyCheck(concurrencyCheck?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
335
378
  /**
336
379
  * Explicitly specify index on a property.
380
+ * When a string name is passed, it is captured as a literal type for use with the `using` option in `FindOptions`.
337
381
  */
338
- index(index?: boolean | string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
382
+ index<N extends string>(name: N): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'index'> & {
383
+ index: N;
384
+ }, IncludeKeys>, IncludeKeys>;
385
+ index(index?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
339
386
  /**
340
387
  * Set column as unique for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
388
+ * When a string name is passed, it is captured as a literal type for use with the `using` option in `FindOptions`.
341
389
  */
342
- unique(unique?: boolean | string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
390
+ unique<N extends string>(name: N): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'unique'> & {
391
+ unique: N;
392
+ }, IncludeKeys>, IncludeKeys>;
393
+ unique(unique?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
343
394
  /**
344
395
  * Specify column with check constraints. (Postgres driver only)
345
396
  *
@@ -492,7 +543,9 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
492
543
  /** What to do when the reference to the target entity gets updated. */
493
544
  updateRule(updateRule: 'cascade' | 'no action' | 'set null' | 'set default' | AnyString): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
494
545
  /** Map this relation to the primary key value instead of an entity. */
495
- mapToPk(): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'mapToPk'> & {
546
+ mapToPk(): Options extends {
547
+ lazyRef: true;
548
+ } ? never : Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'mapToPk'> & {
496
549
  mapToPk: true;
497
550
  }, IncludeKeys>, IncludeKeys>;
498
551
  /** Set the constraint type. Immediate constraints are checked for each statement, while deferred ones are only checked at the end of the transaction. Only for postgres unique constraints. */
@@ -556,7 +609,7 @@ export type PropertyBuilders = {
556
609
  /** Own keys + base entity keys (when TBase is not `never`). Guards against `keyof never = string | number | symbol`. */
557
610
  type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends true ? never : keyof TBase);
558
611
  /** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
559
- export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'repository' | 'filters' | 'orderBy'> {
612
+ export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
560
613
  name: TName;
561
614
  tableName?: TTableName;
562
615
  extends?: {
@@ -609,6 +662,15 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
609
662
  fillFactor?: number;
610
663
  disabled?: boolean;
611
664
  }[];
665
+ triggers?: {
666
+ name?: string;
667
+ timing: 'before' | 'after' | 'instead of';
668
+ events: ('insert' | 'update' | 'delete' | 'truncate')[];
669
+ forEach?: 'row' | 'statement';
670
+ body?: string | Raw | TriggerCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
671
+ when?: string;
672
+ expression?: string;
673
+ }[];
612
674
  }
613
675
  /** Defines an entity schema using property builders, with full type inference from the property definitions. */
614
676
  export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
@@ -665,7 +727,9 @@ export type InferEntityFromProperties<Properties extends Record<string, any>, PK
665
727
  [Config]?: DefineConfig<{
666
728
  forceObject: true;
667
729
  }>;
668
- } : {});
730
+ } : {}) & {
731
+ [IndexHints]?: [Properties];
732
+ };
669
733
  type InferCombinedPrimaryKey<Properties extends Record<string, any>, PK, Base> = PK extends undefined ? CombinePrimaryKeys<InferPrimaryKey<Properties>, ExtractBasePrimaryKey<Base>> : PK;
670
734
  type ExtractBasePrimaryKey<Base> = Base extends {
671
735
  [PrimaryKeyProp]?: infer BasePK;
@@ -707,6 +771,9 @@ type MaybeNullable<Value, Options> = Options extends {
707
771
  type MaybeRelationRef<Value, Options> = Options extends {
708
772
  mapToPk: true;
709
773
  } ? Value : Options extends {
774
+ lazyRef: true;
775
+ kind: '1:1' | 'm:1';
776
+ } ? Value extends object ? LazyRef<Value> : never : Options extends {
710
777
  ref: true;
711
778
  kind: '1:1';
712
779
  } ? Value extends object ? Ref<Value> : never : Options extends {
@@ -173,6 +173,16 @@ export class UniversalPropertyOptionsBuilder {
173
173
  ref() {
174
174
  return this.assignOptions({ ref: true });
175
175
  }
176
+ /**
177
+ * Mark a to-one relation (`m:1`/`1:1`) as a {@apilink LazyRef} — the runtime value stays a plain entity
178
+ * (no `Reference` wrapper) but the TypeScript type restricts access to the primary key until `Loaded<>`
179
+ * narrows it. Type-level only; does not set `ref: true`. Not compatible with `.ref()` or `.mapToPk()` —
180
+ * those chains are rejected in both directions at compile time.
181
+ */
182
+ lazyRef() {
183
+ // purely type-level — no runtime metadata change
184
+ return this;
185
+ }
176
186
  /**
177
187
  * Set to true to omit the property when {@link https://mikro-orm.io/docs/serializing Serializing}.
178
188
  */
@@ -191,15 +201,9 @@ export class UniversalPropertyOptionsBuilder {
191
201
  concurrencyCheck(concurrencyCheck = true) {
192
202
  return this.assignOptions({ concurrencyCheck });
193
203
  }
194
- /**
195
- * Explicitly specify index on a property.
196
- */
197
204
  index(index = true) {
198
205
  return this.assignOptions({ index });
199
206
  }
200
- /**
201
- * Set column as unique for {@link https://mikro-orm.io/docs/schema-generator Schema Generator}. (SQL only)
202
- */
203
207
  unique(unique = true) {
204
208
  return this.assignOptions({ unique });
205
209
  }
package/entity/utils.js CHANGED
@@ -94,11 +94,10 @@ export function applyPopulateHints(populate, hints) {
94
94
  if (!entry) {
95
95
  continue;
96
96
  }
97
- if (hint.strategy != null) {
98
- entry.strategy = hint.strategy;
99
- }
100
- if (hint.joinType != null) {
101
- entry.joinType = hint.joinType;
97
+ for (const key of Object.keys(hint)) {
98
+ if (hint[key] != null) {
99
+ entry[key] = hint[key];
100
+ }
102
101
  }
103
102
  }
104
103
  }
package/errors.d.ts CHANGED
@@ -74,6 +74,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
74
74
  static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
75
75
  static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
76
76
  static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
78
+ static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
79
  private static fromMessage;
78
80
  }
79
81
  /** Error thrown when an entity lookup fails to find the expected result. */
package/errors.js CHANGED
@@ -243,6 +243,10 @@ export class MetadataError extends ValidationError {
243
243
  static tptNotSupportedByDriver(meta) {
244
244
  return new MetadataError(`Entity ${meta.className} uses TPT (Table-Per-Type) inheritance which is not supported by the current driver. TPT requires SQL JOINs and is only available with SQL drivers.`);
245
245
  }
246
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
247
+ static triggersNotSupportedByDriver(meta) {
248
+ return new MetadataError(`Entity ${meta.className} defines database triggers which are not supported by the current driver. Triggers are only available with SQL drivers.`);
249
+ }
246
250
  static fromMessage(meta, prop, message) {
247
251
  return new MetadataError(`${meta.className}.${prop.name} ${message}`);
248
252
  }
package/index.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * @packageDocumentation
3
3
  * @module core
4
4
  */
5
- export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
5
+ export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @packageDocumentation
3
3
  * @module core
4
4
  */
5
- export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, } from './typings.js';
5
+ export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
6
  export * from './enums.js';
7
7
  export * from './errors.js';
8
8
  export * from './exceptions.js';
@@ -54,6 +54,16 @@ export declare class MetadataDiscovery {
54
54
  * Define properties for a polymorphic pivot table.
55
55
  */
56
56
  private definePolymorphicPivotProperties;
57
+ /**
58
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
59
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
60
+ *
61
+ * Pivot shape:
62
+ * (owner_fk..., discriminator_column, target_fk...)
63
+ * - owner side is a normal M:1 to the single owner entity
64
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
65
+ */
66
+ private defineUnionTargetPolymorphicPivotProperties;
57
67
  /**
58
68
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
59
69
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -105,6 +115,7 @@ export declare class MetadataDiscovery {
105
115
  private initAutoincrement;
106
116
  private createSchemaTable;
107
117
  private initCheckConstraints;
118
+ private initTriggers;
108
119
  private initGeneratedColumn;
109
120
  private getDefaultVersionValue;
110
121
  private inferDefaultValue;
@@ -159,6 +159,7 @@ export class MetadataDiscovery {
159
159
  forEachProp((m, p) => this.initGeneratedColumn(m, p));
160
160
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
161
161
  filtered.forEach(meta => this.initCheckConstraints(meta));
162
+ filtered.forEach(meta => this.initTriggers(meta));
162
163
  forEachProp((_m, p) => {
163
164
  this.initDefaultValue(p);
164
165
  this.inferTypeFromDefault(p);
@@ -479,17 +480,28 @@ export class MetadataDiscovery {
479
480
  prop.polymorphic = prop2.polymorphic;
480
481
  prop.discriminator = prop2.discriminator;
481
482
  prop.discriminatorColumn = prop2.discriminatorColumn;
482
- prop.discriminatorValue = prop2.discriminatorValue;
483
+ // For a union-target pivot each inverse side sits on one specific target class, so its
484
+ // discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
485
+ prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
483
486
  }
484
487
  prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
485
- // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
486
- if (prop.polymorphic && prop.discriminator) {
488
+ // Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
489
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
490
+ if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
491
+ // Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
487
492
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
488
493
  }
489
494
  else {
490
495
  prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
491
496
  }
492
- prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
497
+ if (isUnionTargetMN) {
498
+ // Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
499
+ const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
500
+ prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
501
+ }
502
+ else {
503
+ prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
504
+ }
493
505
  }
494
506
  isExplicitTableName(meta) {
495
507
  return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
@@ -583,6 +595,24 @@ export class MetadataDiscovery {
583
595
  if (prop.inversedBy) {
584
596
  prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
585
597
  }
598
+ // Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
599
+ // owner prop. Covers three cases:
600
+ // - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
601
+ // - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
602
+ // - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
603
+ const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
604
+ ? prop.polymorphTargets
605
+ : [prop.targetMeta];
606
+ for (const targetMeta of inverseCandidates) {
607
+ for (const inverseProp of Object.values(targetMeta.properties)) {
608
+ if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
609
+ inverseProp.mappedBy === prop.name &&
610
+ !inverseProp.pivotEntity) {
611
+ inverseProp.pivotEntity = pivotMeta.class;
612
+ inverseProp.pivotTable = pivotMeta.tableName;
613
+ }
614
+ }
615
+ }
586
616
  return pivotMeta;
587
617
  });
588
618
  }
@@ -721,8 +751,12 @@ export class MetadataDiscovery {
721
751
  }
722
752
  }
723
753
  }
724
- // For polymorphic M:N, create discriminator column and polymorphic FK
725
- if (prop.polymorphic && prop.discriminatorColumn) {
754
+ // Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
755
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
756
+ this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
757
+ }
758
+ else if (prop.polymorphic && prop.discriminatorColumn) {
759
+ // Rails-style polymorphic M:N: multiple owners share the pivot, single target type
726
760
  this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
727
761
  }
728
762
  else {
@@ -809,6 +843,33 @@ export class MetadataDiscovery {
809
843
  pivotMeta.polymorphicDiscriminatorMap ??= {};
810
844
  pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
811
845
  }
846
+ /**
847
+ * Mirror of definePolymorphicPivotProperties for union-target M:N
848
+ * (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
849
+ *
850
+ * Pivot shape:
851
+ * (owner_fk..., discriminator_column, target_fk...)
852
+ * - owner side is a normal M:1 to the single owner entity
853
+ * - target side is a discriminator column + per-target-type virtual M:1 relations
854
+ */
855
+ defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
856
+ const discriminatorColumn = prop.discriminatorColumn;
857
+ const targets = prop.polymorphTargets;
858
+ pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
859
+ const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
860
+ this.initFieldName(discriminatorProp);
861
+ pivotMeta.properties[discriminatorColumn] = discriminatorProp;
862
+ const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
863
+ pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
864
+ pivotMeta.polymorphicDiscriminatorMap ??= {};
865
+ for (const targetMeta of targets) {
866
+ const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
867
+ const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
868
+ relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
869
+ pivotMeta.properties[relationName] = relation;
870
+ pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
871
+ }
872
+ }
812
873
  /**
813
874
  * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
814
875
  * This enables single-query join loading for inverse-side polymorphic M:N.
@@ -961,6 +1022,7 @@ export class MetadataDiscovery {
961
1022
  meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
962
1023
  meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
963
1024
  meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1025
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
964
1026
  const pks = Object.values(meta.properties)
965
1027
  .filter(p => p.primary)
966
1028
  .map(p => p.name);
@@ -1045,11 +1107,15 @@ export class MetadataDiscovery {
1045
1107
  prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
1046
1108
  prop.createForeignKeyConstraint = false;
1047
1109
  const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
1048
- if (isToOne) {
1110
+ const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
1111
+ if (isToOne || isUnionTargetMN) {
1049
1112
  const types = prop.type.split(/ ?\| ?/);
1050
1113
  prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
1051
1114
  prop.targetMeta = prop.polymorphTargets[0];
1052
1115
  prop.referencedPKs = prop.targetMeta?.primaryKeys;
1116
+ if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
1117
+ throw new MetadataError(`${meta.className}.${prop.name} union-target polymorphic M:N requires at least two target entity types; use a regular M:N relation for a single target.`);
1118
+ }
1053
1119
  }
1054
1120
  if (prop.discriminatorMap) {
1055
1121
  const normalizedMap = {};
@@ -1065,7 +1131,7 @@ export class MetadataDiscovery {
1065
1131
  }
1066
1132
  prop.discriminatorMap = normalizedMap;
1067
1133
  }
1068
- else if (isToOne) {
1134
+ else if (isToOne || isUnionTargetMN) {
1069
1135
  prop.discriminatorMap = {};
1070
1136
  const tableNameToTarget = new Map();
1071
1137
  for (const target of prop.polymorphTargets) {
@@ -1520,6 +1586,27 @@ export class MetadataDiscovery {
1520
1586
  }
1521
1587
  }
1522
1588
  }
1589
+ initTriggers(meta) {
1590
+ if (meta.triggers.length === 0) {
1591
+ return;
1592
+ }
1593
+ const columns = meta.createSchemaColumnMappingObject();
1594
+ const table = this.createSchemaTable(meta);
1595
+ for (const trigger of meta.triggers) {
1596
+ if (trigger.body && trigger.expression) {
1597
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} defines both 'body' and 'expression'. Use one or the other.`);
1598
+ }
1599
+ if (!trigger.body && !trigger.expression) {
1600
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} must define either 'body' or 'expression'.`);
1601
+ }
1602
+ trigger.name ??= this.#namingStrategy.indexName(meta.tableName, trigger.events, 'trigger');
1603
+ trigger.forEach ??= 'row';
1604
+ if (trigger.body instanceof Function) {
1605
+ trigger.body = trigger.body(columns, table);
1606
+ }
1607
+ }
1608
+ meta.hasTriggers = true;
1609
+ }
1523
1610
  initGeneratedColumn(meta, prop) {
1524
1611
  if (!prop.generated && prop.columnTypes) {
1525
1612
  const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
@@ -1646,7 +1733,14 @@ export class MetadataDiscovery {
1646
1733
  return (!Type.getType(t[type]).ensureComparable(meta, prop) && (prop.type === t[type] || brand === type));
1647
1734
  });
1648
1735
  if (type) {
1649
- prop.type = type === 'datetime' ? 'Date' : type;
1736
+ if (prop.array) {
1737
+ // built-in type + array: true — force-create instance for ArrayType wrapping
1738
+ prop.customType = new prop.type();
1739
+ prop.type = prop.customType.constructor.name;
1740
+ }
1741
+ else {
1742
+ prop.type = type === 'datetime' ? 'Date' : type;
1743
+ }
1650
1744
  }
1651
1745
  else {
1652
1746
  prop.customType = new prop.type();
@@ -1656,6 +1750,30 @@ export class MetadataDiscovery {
1656
1750
  if (simple) {
1657
1751
  return;
1658
1752
  }
1753
+ if (prop.array &&
1754
+ prop.customType &&
1755
+ !(prop.customType instanceof t.array) &&
1756
+ !(prop.customType instanceof t.enumArray) &&
1757
+ prop.kind === ReferenceKind.SCALAR) {
1758
+ const innerType = prop.customType;
1759
+ innerType.platform = this.#platform;
1760
+ innerType.meta = meta;
1761
+ innerType.prop = prop;
1762
+ if (!prop.columnTypes) {
1763
+ const arrayDecl = this.#platform.getArrayDeclarationSQL();
1764
+ if (arrayDecl.endsWith('[]')) {
1765
+ // native array support (e.g. postgres) — use inner type's column type with [] suffix
1766
+ prop.columnTypes = [innerType.getColumnType(prop, this.#platform) + '[]'];
1767
+ }
1768
+ else {
1769
+ // non-native (e.g. mysql, sqlite) — store as text/clob
1770
+ prop.columnTypes = [arrayDecl];
1771
+ }
1772
+ }
1773
+ const platform = this.#platform;
1774
+ prop.customType = new t.array(v => innerType.convertToJSValue(v, platform), v => innerType.convertToDatabaseValue(v, platform));
1775
+ prop.type = prop.customType.constructor.name;
1776
+ }
1659
1777
  if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
1660
1778
  prop.customType = new t.json();
1661
1779
  }
@@ -171,6 +171,15 @@ export class MetadataValidator {
171
171
  }
172
172
  validatePolymorphicTargets(meta, prop) {
173
173
  const targets = prop.polymorphTargets;
174
+ // Union-target M:N stores one scalar target FK per pivot row, so composite-PK targets
175
+ // can't round-trip through this schema.
176
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && targets.length > 1) {
177
+ for (const target of targets) {
178
+ if (target.compositePK) {
179
+ throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `${target.className} has a composite primary key; union-target polymorphic M:N does not support composite-PK targets.`);
180
+ }
181
+ }
182
+ }
174
183
  // Validate targetKey exists and is compatible across all targets
175
184
  if (prop.targetKey) {
176
185
  for (const target of targets) {
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
1
+ import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, 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';
@@ -57,6 +57,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
57
57
  };
58
58
  /** Used to make ORM aware of externally defined triggers. This is needed for MS SQL Server multi inserts, ignored in other dialects. */
59
59
  hasTriggers?: boolean;
60
+ /** Database triggers to create for this entity's table. (SQL drivers only) */
61
+ triggers?: TriggerDef<E>[];
60
62
  /** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
61
63
  expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
62
64
  /** Set {@doclink repositories#custom-repository | custom repository class}. */
@@ -202,6 +204,12 @@ export interface PropertyOptions<Owner> {
202
204
  * @see https://mikro-orm.io/docs/defining-entities#lazy-scalar-properties
203
205
  */
204
206
  lazy?: boolean;
207
+ /**
208
+ * Marks this property as an array. When used with a custom type, the type is automatically
209
+ * wrapped in an `ArrayType` that delegates element conversion to the inner type.
210
+ * The column type is inferred as `innerType[]` (e.g. `int[]`, `time[]`).
211
+ */
212
+ array?: boolean;
205
213
  /**
206
214
  * Set true to define entity's unique primary key identifier.
207
215
  * Alias for `@PrimaryKey()` decorator
@@ -334,7 +342,7 @@ export interface PropertyOptions<Owner> {
334
342
  }
335
343
  export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
336
344
  /** Set target entity type. For polymorphic relations, pass an array of entity types. */
337
- entity?: () => EntityName<Target> | EntityName<Target>[];
345
+ entity?: () => EntityName<Target> | EntityName[];
338
346
  /** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
339
347
  cascade?: Cascade[];
340
348
  /** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
@@ -4,7 +4,7 @@ import { type ReferenceKind } from '../enums.js';
4
4
  export declare abstract class AbstractNamingStrategy implements NamingStrategy {
5
5
  getClassName(file: string, separator?: string): string;
6
6
  classToMigrationName(timestamp: string, customMigrationName?: string): string;
7
- indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
7
+ indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
@@ -75,7 +75,7 @@ export interface NamingStrategy {
75
75
  /**
76
76
  * Returns key/constraint name for the given type. Some drivers might not support all the types (e.g. mysql and sqlite enforce the PK name).
77
77
  */
78
- indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
78
+ indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
79
79
  /**
80
80
  * Returns alias name for given entity. The alias needs to be unique across the query, which is by default
81
81
  * ensured via appended index parameter. It is optional to use it as long as you ensure it will be unique.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.1",
3
+ "version": "7.1.0-dev.11",
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",