diff --git a/CHANGELOG.md b/CHANGELOG.md index 6111a119e3d6..899f10a1b729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,41 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + + +### Bug Fixes + +* **core:** allow filter condition callbacks without arguments ([5b3401f](https://github.com/mikro-orm/mikro-orm/commit/5b3401f28cbfcc4e78707fb8110be418a695932a)), closes [#847](https://github.com/mikro-orm/mikro-orm/issues/847) +* **core:** allow filter condition callbacks without arguments ([da8fbfc](https://github.com/mikro-orm/mikro-orm/commit/da8fbfc5aa7c2a4a3b58325b4874125d2f67d2c1)), closes [#847](https://github.com/mikro-orm/mikro-orm/issues/847) +* **core:** allow querying `ArrayType` with a value ([e505358](https://github.com/mikro-orm/mikro-orm/commit/e50535816f318ff0c0c5edf68270920ff2cef520)), closes [#844](https://github.com/mikro-orm/mikro-orm/issues/844) +* **core:** improve metadata validation of STI relations ([0b97af8](https://github.com/mikro-orm/mikro-orm/commit/0b97af8404fd557836f8afae9cce255aca083873)), closes [#845](https://github.com/mikro-orm/mikro-orm/issues/845) +* **core:** update filter typing to allow async condition ([#848](https://github.com/mikro-orm/mikro-orm/issues/848)) ([2188f62](https://github.com/mikro-orm/mikro-orm/commit/2188f621163bebe9bbb74d1b693871fe22017d38)) +* **deps:** update dependency escaya to ^0.0.44 ([#839](https://github.com/mikro-orm/mikro-orm/issues/839)) ([fedb41c](https://github.com/mikro-orm/mikro-orm/commit/fedb41cc53eeafb3e3e1540c46acc5f8f58f43b2)) +* **deps:** update dependency escaya to ^0.0.45 ([#842](https://github.com/mikro-orm/mikro-orm/issues/842)) ([d9f9f05](https://github.com/mikro-orm/mikro-orm/commit/d9f9f0572a1bb1d5a5567a46b835fa1877cb3806)) +* **query-builder:** fix mapping of 1:1 inverse sides ([a46281e](https://github.com/mikro-orm/mikro-orm/commit/a46281e0d8de6385e2c49fd250d284293421f2dc)), closes [#849](https://github.com/mikro-orm/mikro-orm/issues/849) +* **query-builder:** fix mapping of nested 1:1 properties ([9799e70](https://github.com/mikro-orm/mikro-orm/commit/9799e70bd7235695f4f1e55b25fe61bbc158eb38)) + + +### Features + +* **core:** allow setting loading strategy globally ([e4378ee](https://github.com/mikro-orm/mikro-orm/commit/e4378ee6dca5607a82e2bff3450e18f0a6668354)), closes [#834](https://github.com/mikro-orm/mikro-orm/issues/834) +* **migrations:** allow providing transaction context ([1089c86](https://github.com/mikro-orm/mikro-orm/commit/1089c861afcb31703a0dbdc82edf9674b2dd1576)), closes [#851](https://github.com/mikro-orm/mikro-orm/issues/851) + + +### Performance Improvements + +* move reference to metadata to entity prototype + more improvements ([#843](https://github.com/mikro-orm/mikro-orm/issues/843)) ([f71e4c2](https://github.com/mikro-orm/mikro-orm/commit/f71e4c2b8dd0bbfb0658dc8a366444ec1a49c187)), closes [#732](https://github.com/mikro-orm/mikro-orm/issues/732) + + +### Reverts + +* Revert "refactor: return `target` from decorator definition" ([e021617](https://github.com/mikro-orm/mikro-orm/commit/e02161774a904748cfa13e683fb2ce93d66403ca)) + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/docs/docs/filters.md b/docs/docs/filters.md index b6fde9337531..10909771f15d 100644 --- a/docs/docs/filters.md +++ b/docs/docs/filters.md @@ -59,6 +59,23 @@ const books = await orm.em.find(Book, {}, { }); ``` +### Filters without parameters + +If we want to have a filter condition that do not need arguments, but we want +to access the `type` parameter, we will need to explicitly set `args: false`, +otherwise error will be raised due to missing parameters: + +```ts +@Filter({ + name: 'withoutParams', + cond(_, type) { + return { ... }; + }, + args: false, + default: true, +}) +``` + ## Global filters We can also register filters dynamically via `EntityManager` API. We call such filters diff --git a/docs/docs/loading-strategies.md b/docs/docs/loading-strategies.md index 03f4a03516e9..301a638cbe76 100644 --- a/docs/docs/loading-strategies.md +++ b/docs/docs/loading-strategies.md @@ -70,3 +70,16 @@ const author = await orm.em.findOne(Author, 1, { populate: { books: [LoadStrategy.JOINED, { publisher: LoadStrategy.JOINED }] } }); ``` + +## Changing the loading strategy globally + +You can use `loadStrategy` option in the ORM config: + +```ts +MikroORM.init({ + loadStrategy: LoadStrategy.JOINED, +}); +``` + +This value will be used as the default, specifying the loading strategy on +property level has precedence, as well as specifying it in the `FindOptions`. diff --git a/docs/docs/migrations.md b/docs/docs/migrations.md index e46a249a4672..a1694cb8d815 100644 --- a/docs/docs/migrations.md +++ b/docs/docs/migrations.md @@ -123,6 +123,16 @@ Then run this script via `ts-node` (or compile it to plain JS and use `node`): $ ts-node migrate ``` +## Providing transaction context + +In some cases you might want to control the transaction context yourself: + +```ts +await orm.em.transactional(async em => { + await migrator.up({ transaction: em.getTransactionContext() }); +}); +``` + ## Importing migrations statically If you do not want to dynamically import a folder (e.g. when bundling your code with webpack) you can import migrations diff --git a/docs/versioned_docs/version-4.0/filters.md b/docs/versioned_docs/version-4.0/filters.md index b6fde9337531..10909771f15d 100644 --- a/docs/versioned_docs/version-4.0/filters.md +++ b/docs/versioned_docs/version-4.0/filters.md @@ -59,6 +59,23 @@ const books = await orm.em.find(Book, {}, { }); ``` +### Filters without parameters + +If we want to have a filter condition that do not need arguments, but we want +to access the `type` parameter, we will need to explicitly set `args: false`, +otherwise error will be raised due to missing parameters: + +```ts +@Filter({ + name: 'withoutParams', + cond(_, type) { + return { ... }; + }, + args: false, + default: true, +}) +``` + ## Global filters We can also register filters dynamically via `EntityManager` API. We call such filters diff --git a/docs/versioned_docs/version-4.0/loading-strategies.md b/docs/versioned_docs/version-4.0/loading-strategies.md index 03f4a03516e9..301a638cbe76 100644 --- a/docs/versioned_docs/version-4.0/loading-strategies.md +++ b/docs/versioned_docs/version-4.0/loading-strategies.md @@ -70,3 +70,16 @@ const author = await orm.em.findOne(Author, 1, { populate: { books: [LoadStrategy.JOINED, { publisher: LoadStrategy.JOINED }] } }); ``` + +## Changing the loading strategy globally + +You can use `loadStrategy` option in the ORM config: + +```ts +MikroORM.init({ + loadStrategy: LoadStrategy.JOINED, +}); +``` + +This value will be used as the default, specifying the loading strategy on +property level has precedence, as well as specifying it in the `FindOptions`. diff --git a/docs/versioned_docs/version-4.0/migrations.md b/docs/versioned_docs/version-4.0/migrations.md index e46a249a4672..e4015361cab7 100644 --- a/docs/versioned_docs/version-4.0/migrations.md +++ b/docs/versioned_docs/version-4.0/migrations.md @@ -123,6 +123,16 @@ Then run this script via `ts-node` (or compile it to plain JS and use `node`): $ ts-node migrate ``` +## Providing transaction context + +In some cases you might want to control the transaction context yourself: + +```ts +await orm.em.transactional(async em => { + await migrator.up({ transaction: em.getTransactionContext() }); +}); +``` + ## Importing migrations statically If you do not want to dynamically import a folder (e.g. when bundling your code with webpack) you can import migrations @@ -143,7 +153,7 @@ await MikroORM.init({ }); ``` -With the help of (webpacks context module api)[https://webpack.js.org/guides/dependency-management/#context-module-api] +With the help of [webpack's context module api](https://webpack.js.org/guides/dependency-management/#context-module-api) we can dynamically import the migrations making it possible to import all files in a folder. ```typescript diff --git a/lerna.json b/lerna.json index 8b062b5e8d07..47a7f481a347 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "4.0.3", + "version": "4.0.4", "command": { "version": { "conventionalCommits": true, diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 07ddaf7e6205..3ab66a0c6907 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/cli + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/cli diff --git a/packages/cli/package.json b/packages/cli/package.json index 491833aafbbb..e5bc42fbd750 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/cli", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -50,10 +50,10 @@ "access": "public" }, "dependencies": { - "@mikro-orm/core": "^4.0.3", - "@mikro-orm/entity-generator": "^4.0.3", - "@mikro-orm/knex": "^4.0.3", - "@mikro-orm/migrations": "^4.0.3", + "@mikro-orm/core": "^4.0.4", + "@mikro-orm/entity-generator": "^4.0.4", + "@mikro-orm/knex": "^4.0.4", + "@mikro-orm/migrations": "^4.0.4", "ansi-colors": "^4.1.1", "cli-table3": "^0.6.0", "fs-extra": "^9.0.1", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 0388e86c4572..a1a608ce0a6c 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,6 +3,39 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + + +### Bug Fixes + +* **core:** allow filter condition callbacks without arguments ([5b3401f](https://github.com/mikro-orm/mikro-orm/commit/5b3401f28cbfcc4e78707fb8110be418a695932a)), closes [#847](https://github.com/mikro-orm/mikro-orm/issues/847) +* **core:** allow filter condition callbacks without arguments ([da8fbfc](https://github.com/mikro-orm/mikro-orm/commit/da8fbfc5aa7c2a4a3b58325b4874125d2f67d2c1)), closes [#847](https://github.com/mikro-orm/mikro-orm/issues/847) +* **core:** allow querying `ArrayType` with a value ([e505358](https://github.com/mikro-orm/mikro-orm/commit/e50535816f318ff0c0c5edf68270920ff2cef520)), closes [#844](https://github.com/mikro-orm/mikro-orm/issues/844) +* **core:** improve metadata validation of STI relations ([0b97af8](https://github.com/mikro-orm/mikro-orm/commit/0b97af8404fd557836f8afae9cce255aca083873)), closes [#845](https://github.com/mikro-orm/mikro-orm/issues/845) +* **core:** update filter typing to allow async condition ([#848](https://github.com/mikro-orm/mikro-orm/issues/848)) ([2188f62](https://github.com/mikro-orm/mikro-orm/commit/2188f621163bebe9bbb74d1b693871fe22017d38)) +* **deps:** update dependency escaya to ^0.0.44 ([#839](https://github.com/mikro-orm/mikro-orm/issues/839)) ([fedb41c](https://github.com/mikro-orm/mikro-orm/commit/fedb41cc53eeafb3e3e1540c46acc5f8f58f43b2)) +* **deps:** update dependency escaya to ^0.0.45 ([#842](https://github.com/mikro-orm/mikro-orm/issues/842)) ([d9f9f05](https://github.com/mikro-orm/mikro-orm/commit/d9f9f0572a1bb1d5a5567a46b835fa1877cb3806)) +* **query-builder:** fix mapping of nested 1:1 properties ([9799e70](https://github.com/mikro-orm/mikro-orm/commit/9799e70bd7235695f4f1e55b25fe61bbc158eb38)) + + +### Features + +* **core:** allow setting loading strategy globally ([e4378ee](https://github.com/mikro-orm/mikro-orm/commit/e4378ee6dca5607a82e2bff3450e18f0a6668354)), closes [#834](https://github.com/mikro-orm/mikro-orm/issues/834) + + +### Performance Improvements + +* move reference to metadata to entity prototype + more improvements ([#843](https://github.com/mikro-orm/mikro-orm/issues/843)) ([f71e4c2](https://github.com/mikro-orm/mikro-orm/commit/f71e4c2b8dd0bbfb0658dc8a366444ec1a49c187)), closes [#732](https://github.com/mikro-orm/mikro-orm/issues/732) + + +### Reverts + +* Revert "refactor: return `target` from decorator definition" ([e021617](https://github.com/mikro-orm/mikro-orm/commit/e02161774a904748cfa13e683fb2ce93d66403ca)) + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/packages/core/package.json b/packages/core/package.json index b4f839a02ec3..c79cbe3e9a3e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/core", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -49,7 +49,7 @@ "dependencies": { "ansi-colors": "^4.1.1", "clone": "^2.1.2", - "escaya": "^0.0.42", + "escaya": "^0.0.45", "fast-deep-equal": "^3.1.3", "fs-extra": "^9.0.1", "globby": "^11.0.1", diff --git a/packages/core/src/EntityManager.ts b/packages/core/src/EntityManager.ts index 1cebfd90f2c2..0e8eb13b8e8d 100644 --- a/packages/core/src/EntityManager.ts +++ b/packages/core/src/EntityManager.ts @@ -157,7 +157,7 @@ export class EntityManager { if (filter.cond instanceof Function) { const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.filterParams[filter.name]; - if (!args) { + if (!args && filter.cond.length > 0 && filter.args !== false) { throw new Error(`No arguments provided for filter '${filter.name}'`); } @@ -504,6 +504,11 @@ export class EntityManager { * The entity will be entered into the database at or before transaction commit or as a result of the flush operation. */ persist(entity: AnyEntity | Reference | (AnyEntity | Reference)[]): this { + if (Utils.isEntity(entity)) { + this.getUnitOfWork().persist(entity); + return this; + } + const entities = Utils.asArray(entity); for (const ent of entities) { @@ -737,7 +742,7 @@ export class EntityManager { const ret: PopulateOptions[] = this.entityLoader.normalizePopulate(entityName, populate as true); return ret.map(field => { - field.strategy = strategy ?? field.strategy; + field.strategy = strategy ?? field.strategy ?? this.config.get('loadStrategy'); return field; }); } @@ -745,7 +750,7 @@ export class EntityManager { private preparePopulateObject>(meta: EntityMetadata, populate: PopulateMap, strategy?: LoadStrategy): PopulateOptions[] { return Object.keys(populate).map(field => { const prop = meta.properties[field]; - const fieldStrategy = strategy ?? (Utils.isString(populate[field]) ? populate[field] : prop.strategy); + const fieldStrategy = strategy ?? (Utils.isString(populate[field]) ? populate[field] : prop.strategy) ?? this.config.get('loadStrategy'); if (populate[field] === true) { return { field, strategy: fieldStrategy }; diff --git a/packages/core/src/decorators/Embedded.ts b/packages/core/src/decorators/Embedded.ts index 790d4b49fe05..22cbea32ff9d 100644 --- a/packages/core/src/decorators/Embedded.ts +++ b/packages/core/src/decorators/Embedded.ts @@ -11,6 +11,8 @@ export function Embedded(options: EmbeddedOptions | (() => AnyEntity) = {}) { Utils.defaultValue(options, 'prefix', true); const property = { name: propertyName, reference: ReferenceType.EMBEDDED } as EntityProperty; meta.properties[propertyName] = Object.assign(property, options); + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/Enum.ts b/packages/core/src/decorators/Enum.ts index 7a53b900c855..8943ca409d6a 100644 --- a/packages/core/src/decorators/Enum.ts +++ b/packages/core/src/decorators/Enum.ts @@ -2,12 +2,15 @@ import { MetadataStorage } from '../metadata'; import { ReferenceType } from '../enums'; import { PropertyOptions } from './Property'; import { EntityProperty, AnyEntity, Dictionary } from '../typings'; +import { Utils } from '../utils/Utils'; export function Enum(options: EnumOptions | (() => Dictionary) = {}) { return function (target: AnyEntity, propertyName: string) { const meta = MetadataStorage.getMetadataFromDecorator(target.constructor); options = options instanceof Function ? { items: options } : options; meta.properties[propertyName] = Object.assign({ name: propertyName, reference: ReferenceType.SCALAR, enum: true }, options) as EntityProperty; + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/Formula.ts b/packages/core/src/decorators/Formula.ts index db34de8b9f3f..296b9e0ee944 100644 --- a/packages/core/src/decorators/Formula.ts +++ b/packages/core/src/decorators/Formula.ts @@ -1,10 +1,13 @@ import { MetadataStorage } from '../metadata'; import { ReferenceType } from '../enums'; import { EntityProperty, AnyEntity } from '../typings'; +import { Utils } from '../utils/Utils'; export function Formula(formula: string | ((alias: string) => string)) { return function (target: AnyEntity, propertyName: string) { const meta = MetadataStorage.getMetadataFromDecorator(target.constructor); meta.properties[propertyName] = { name: propertyName, reference: ReferenceType.SCALAR, persist: false, formula } as EntityProperty; + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/Indexed.ts b/packages/core/src/decorators/Indexed.ts index 6f597cb488a3..e02209a075e8 100644 --- a/packages/core/src/decorators/Indexed.ts +++ b/packages/core/src/decorators/Indexed.ts @@ -1,5 +1,6 @@ import { MetadataStorage } from '../metadata'; import { AnyEntity, Dictionary } from '../typings'; +import { Utils } from '../utils/Utils'; function createDecorator(options: IndexOptions | UniqueOptions, unique: boolean) { return function (target: AnyEntity, propertyName?: string) { @@ -7,6 +8,12 @@ function createDecorator(options: IndexOptions | UniqueOptions, unique: boolean) options.properties = options.properties || propertyName; const key = unique ? 'uniques' : 'indexes'; meta[key].push(options as Required); + + if (!propertyName) { + return target; + } + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/ManyToMany.ts b/packages/core/src/decorators/ManyToMany.ts index e30781ed598c..d4b410f6046a 100644 --- a/packages/core/src/decorators/ManyToMany.ts +++ b/packages/core/src/decorators/ManyToMany.ts @@ -15,6 +15,8 @@ export function ManyToMany( MetadataValidator.validateSingleDecorator(meta, propertyName, ReferenceType.MANY_TO_MANY); const property = { name: propertyName, reference: ReferenceType.MANY_TO_MANY } as EntityProperty; meta.properties[propertyName] = Object.assign(meta.properties[propertyName] ?? {}, property, options); + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/ManyToOne.ts b/packages/core/src/decorators/ManyToOne.ts index 6851d8fa4543..275b35118fc9 100644 --- a/packages/core/src/decorators/ManyToOne.ts +++ b/packages/core/src/decorators/ManyToOne.ts @@ -14,6 +14,8 @@ export function ManyToOne( MetadataValidator.validateSingleDecorator(meta, propertyName, ReferenceType.MANY_TO_ONE); const property = { name: propertyName, reference: ReferenceType.MANY_TO_ONE } as EntityProperty; meta.properties[propertyName] = Object.assign(meta.properties[propertyName] ?? {}, property, options); + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/OneToMany.ts b/packages/core/src/decorators/OneToMany.ts index 6d26adfbec97..7b5a011db3b0 100644 --- a/packages/core/src/decorators/OneToMany.ts +++ b/packages/core/src/decorators/OneToMany.ts @@ -16,6 +16,8 @@ export function createOneToDecorator( MetadataValidator.validateSingleDecorator(meta, propertyName, reference); const property = { name: propertyName, reference } as EntityProperty; meta.properties[propertyName] = Object.assign(meta.properties[propertyName] ?? {}, property, options); + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/PrimaryKey.ts b/packages/core/src/decorators/PrimaryKey.ts index dc0d4ed328b6..aa61daea0969 100644 --- a/packages/core/src/decorators/PrimaryKey.ts +++ b/packages/core/src/decorators/PrimaryKey.ts @@ -2,6 +2,7 @@ import { MetadataStorage, MetadataValidator } from '../metadata'; import { ReferenceType } from '../enums'; import { PropertyOptions } from './Property'; import { AnyEntity, EntityProperty } from '../typings'; +import { Utils } from '../utils/Utils'; function createDecorator(options: PrimaryKeyOptions | SerializedPrimaryKeyOptions, serialized: boolean) { return function (target: AnyEntity, propertyName: string) { @@ -10,6 +11,8 @@ function createDecorator(options: PrimaryKeyOptions | SerializedPrimaryKey const k = serialized ? 'serializedPrimaryKey' as const : 'primary' as const; options[k] = true; meta.properties[propertyName] = Object.assign({ name: propertyName, reference: ReferenceType.SCALAR }, options) as EntityProperty; + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/decorators/Property.ts b/packages/core/src/decorators/Property.ts index 7ac8a3f098f9..1c9225967dc6 100644 --- a/packages/core/src/decorators/Property.ts +++ b/packages/core/src/decorators/Property.ts @@ -29,6 +29,8 @@ export function Property(options: PropertyOptions = {}) { } meta.properties[prop.name] = prop; + + return Utils.propertyDecoratorReturnValue(); }; } diff --git a/packages/core/src/drivers/DatabaseDriver.ts b/packages/core/src/drivers/DatabaseDriver.ts index 3d7274db55ef..283de1bd935e 100644 --- a/packages/core/src/drivers/DatabaseDriver.ts +++ b/packages/core/src/drivers/DatabaseDriver.ts @@ -62,7 +62,7 @@ export abstract class DatabaseDriver implements IDatabaseD const ret = Object.assign({}, result) as any; - Object.values(meta.properties).forEach(prop => { + meta.props.forEach(prop => { if (prop.fieldNames && prop.fieldNames.length > 1 && prop.fieldNames.every(joinColumn => Utils.isDefined(ret[joinColumn], true))) { const temp: any[] = []; prop.fieldNames.forEach(joinColumn => { @@ -134,7 +134,7 @@ export abstract class DatabaseDriver implements IDatabaseD } }); - Object.values(meta.properties).forEach(prop => { + meta.props.forEach(prop => { if (prop.reference === ReferenceType.EMBEDDED && Utils.isObject(data[prop.name])) { const props = prop.embeddedProps; diff --git a/packages/core/src/entity/ArrayCollection.ts b/packages/core/src/entity/ArrayCollection.ts index 6eeee52a8e60..f60912095c5b 100644 --- a/packages/core/src/entity/ArrayCollection.ts +++ b/packages/core/src/entity/ArrayCollection.ts @@ -29,7 +29,7 @@ export class ArrayCollection { toArray(): Dictionary[] { return this.getItems().map((item: AnyEntity) => { - const meta = item.__helper!.__meta; + const meta = item.__meta!; const args = [...meta.toJsonParams.map(() => undefined), [this.property.name]]; return wrap(item).toJSON(...args); @@ -47,7 +47,7 @@ export class ArrayCollection { return []; } - field = field || (this.items[0] as AnyEntity).__helper!.__meta.serializedPrimaryKey; + field = field || (this.items[0] as AnyEntity).__meta!.serializedPrimaryKey; return this.getItems().map(i => i[field as keyof T]) as unknown as U[]; } @@ -135,7 +135,7 @@ export class ArrayCollection { */ get property(): EntityProperty { if (!this._property) { - const meta = this.owner.__helper!.__meta; + const meta = this.owner.__meta!; const field = Object.keys(meta.properties).find(k => this.owner[k] === this); this._property = meta.properties[field!]; } diff --git a/packages/core/src/entity/Collection.ts b/packages/core/src/entity/Collection.ts index dc317c4a7156..5b1056903cf1 100644 --- a/packages/core/src/entity/Collection.ts +++ b/packages/core/src/entity/Collection.ts @@ -231,7 +231,7 @@ export class Collection extends ArrayCollection { private createManyToManyCondition(cond: Dictionary) { if (this.property.owner || this.property.pivotTable) { - const pk = (this.items[0] as AnyEntity).__helper!.__meta.primaryKeys[0]; // we know there is at least one item as it was checked in load method + const pk = (this.items[0] as AnyEntity).__meta!.primaryKeys[0]; // we know there is at least one item as it was checked in load method cond[pk] = { $in: this.items.map((item: AnyEntity) => item.__helper!.__primaryKey) }; } else { cond[this.property.mappedBy] = this.owner.__helper!.__primaryKey; diff --git a/packages/core/src/entity/EntityAssigner.ts b/packages/core/src/entity/EntityAssigner.ts index 0ef488337cdf..649bcea44ea4 100644 --- a/packages/core/src/entity/EntityAssigner.ts +++ b/packages/core/src/entity/EntityAssigner.ts @@ -5,6 +5,9 @@ import { AnyEntity, EntityData, EntityMetadata, EntityProperty } from '../typing import { Utils } from '../utils/Utils'; import { Reference } from './Reference'; import { ReferenceType, SCALAR_TYPES } from '../enums'; +import { EntityValidator } from './EntityValidator'; + +const validator = new EntityValidator(false); export class EntityAssigner { @@ -13,10 +16,8 @@ export class EntityAssigner { static assign>(entity: T, data: EntityData, onlyProperties: AssignOptions | boolean = false): T { const options = (typeof onlyProperties === 'boolean' ? { onlyProperties } : onlyProperties); const wrapped = entity.__helper!; + const meta = entity.__meta!; const em = options.em || wrapped.__em; - const meta = wrapped.__meta; - const validator = wrapped.__internal.getValidator(); - const platform = wrapped.__internal.getDriver().getPlatform(); const props = meta.properties; Object.keys(data).forEach(prop => { @@ -29,7 +30,7 @@ export class EntityAssigner { let value = data[prop as keyof EntityData]; if (options.convertCustomTypes && customType && props[prop].reference === ReferenceType.SCALAR && !Utils.isEntity(data)) { - value = props[prop].customType.convertToJSValue(value, platform); + value = props[prop].customType.convertToJSValue(value, entity.__platform); } if ([ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(props[prop]?.reference) && Utils.isDefined(value, true) && EntityAssigner.validateEM(em)) { @@ -71,7 +72,7 @@ export class EntityAssigner { return; } - const meta2 = entity[prop.name].__helper!.__meta as EntityMetadata; + const meta2 = entity[prop.name].__meta! as EntityMetadata; const prop2 = meta2.properties[prop.inversedBy || prop.mappedBy]; if (prop2 && !entity[prop.name][prop2.name]) { diff --git a/packages/core/src/entity/EntityFactory.ts b/packages/core/src/entity/EntityFactory.ts index 794ac653d2c3..1dd804c41cb5 100644 --- a/packages/core/src/entity/EntityFactory.ts +++ b/packages/core/src/entity/EntityFactory.ts @@ -1,5 +1,5 @@ import { Utils } from '../utils/Utils'; -import { Dictionary, EntityData, EntityMetadata, EntityName, EntityProperty, New, Populate, Primary, AnyEntity } from '../typings'; +import { AnyEntity, Dictionary, EntityData, EntityMetadata, EntityName, EntityProperty, New, Populate, Primary } from '../typings'; import { UnitOfWork } from '../unit-of-work'; import { EntityManager } from '../EntityManager'; import { EventType, ReferenceType } from '../enums'; @@ -16,9 +16,11 @@ export interface FactoryOptions { export class EntityFactory { private readonly driver = this.em.getDriver(); + private readonly platform = this.driver.getPlatform(); private readonly config = this.em.config; private readonly metadata = this.em.getMetadata(); private readonly hydrator = this.config.getHydrator(this, this.em); + private readonly eventManager = this.em.getEventManager(); constructor(private readonly unitOfWork: UnitOfWork, private readonly em: EntityManager) { } @@ -63,7 +65,7 @@ export class EntityFactory { id = Utils.getPrimaryKeyCondFromArray(id, meta.primaryKeys); } - const pks = Utils.getOrderedPrimaryKeys(id, meta, this.driver.getPlatform(), options.convertCustomTypes); + const pks = Utils.getOrderedPrimaryKeys(id, meta, this.platform, options.convertCustomTypes); if (Utils.isPrimaryKey(id)) { id = { [meta.primaryKeys[0]]: id as Primary }; @@ -87,7 +89,8 @@ export class EntityFactory { // creates new instance via constructor as this is the new entity const entity = new Entity(...params); // perf: create the helper instance early to bypass the double getter defined on the prototype in EntityHelper - Object.defineProperty(entity, '__helper', { value: new WrappedEntity(entity as T, this.em, meta) }); + const helper = new WrappedEntity(entity); + Object.defineProperty(entity, '__helper', { value: helper }); return entity; } @@ -95,7 +98,8 @@ export class EntityFactory { // creates new entity instance, bypassing constructor call as its already persisted entity const entity = Object.create(meta.class.prototype) as T & AnyEntity; // perf: create the helper instance early to bypass the double getter defined on the prototype in EntityHelper - Object.defineProperty(entity, '__helper', { value: new WrappedEntity(entity as T, this.em, meta) }); + const helper = new WrappedEntity(entity as T); + Object.defineProperty(entity, '__helper', { value: helper }); entity.__helper!.__managed = true; this.hydrator.hydrateReference(entity, meta, data, options.convertCustomTypes); @@ -119,21 +123,19 @@ export class EntityFactory { return undefined; } - const pks = Utils.getOrderedPrimaryKeys(data as Dictionary, meta, this.driver.getPlatform(), convertCustomTypes); + const pks = Utils.getOrderedPrimaryKeys(data as Dictionary, meta, this.platform, convertCustomTypes); return this.unitOfWork.getById(meta.name!, pks); } private processDiscriminatorColumn(meta: EntityMetadata, data: EntityData): EntityMetadata { - const root = Utils.getRootEntity(this.metadata, meta); - - if (!root.discriminatorColumn) { + if (!meta.root.discriminatorColumn) { return meta; } - const prop = meta.properties[root.discriminatorColumn]; + const prop = meta.properties[meta.root.discriminatorColumn]; const value = data[prop.name]; - const type = root.discriminatorMap![value]; + const type = meta.root.discriminatorMap![value]; meta = type ? this.metadata.find(type)! : meta; // `prop.userDefined` is either `undefined` or `false` @@ -148,14 +150,13 @@ export class EntityFactory { * denormalize PK to value required by driver (e.g. ObjectId) */ private denormalizePrimaryKey(data: EntityData, primaryKey: string, prop: EntityProperty): void { - const platform = this.driver.getPlatform(); - const pk = platform.getSerializedPrimaryKeyField(primaryKey); + const pk = this.platform.getSerializedPrimaryKeyField(primaryKey); if (Utils.isDefined(data[pk], true) || Utils.isDefined(data[primaryKey], true)) { let id = data[pk] || data[primaryKey]; if (prop.type.toLowerCase() === 'objectid') { - id = platform.denormalizePrimaryKey(id); + id = this.platform.denormalizePrimaryKey(id); } delete data[pk]; @@ -198,7 +199,7 @@ export class EntityFactory { hooks.forEach(hook => (entity[hook] as unknown as () => void)()); } - this.em.getEventManager().dispatchEvent(EventType.onInit, { entity, em: this.em }); + this.eventManager.dispatchEvent(EventType.onInit, { entity, em: this.em }); } } diff --git a/packages/core/src/entity/EntityHelper.ts b/packages/core/src/entity/EntityHelper.ts index a9588f5727c4..deb9a7b8dd79 100644 --- a/packages/core/src/entity/EntityHelper.ts +++ b/packages/core/src/entity/EntityHelper.ts @@ -22,7 +22,7 @@ export class EntityHelper { EntityHelper.defineIdProperty(meta, em.getDriver().getPlatform()); } - EntityHelper.defineBaseProperties(meta, meta.prototype, em); + EntityHelper.defineBaseProperties(meta, meta.prototype, em.getDriver().getPlatform()); const prototype = meta.prototype as Dictionary; if (em.config.get('propagateToOneOwner')) { @@ -50,17 +50,17 @@ export class EntityHelper { }); } - private static defineBaseProperties>(meta: EntityMetadata, prototype: T, em: EntityManager) { + private static defineBaseProperties>(meta: EntityMetadata, prototype: T, platform: Platform) { Object.defineProperties(prototype, { __entity: { value: true }, + __meta: { value: meta }, + __platform: { value: platform }, __helper: { - get(): string { - if (!this.___helper) { - const helper = new WrappedEntity(this, em, meta); - Object.defineProperty(this, '___helper', { value: helper, writable: true }); - } + get(): WrappedEntity { + const helper = new WrappedEntity(this); + Object.defineProperty(this, '__helper', { value: helper, writable: true }); - return this.___helper; + return helper; }, }, }); diff --git a/packages/core/src/entity/EntityLoader.ts b/packages/core/src/entity/EntityLoader.ts index 38edf61228f5..fbfbf090b404 100644 --- a/packages/core/src/entity/EntityLoader.ts +++ b/packages/core/src/entity/EntityLoader.ts @@ -59,6 +59,7 @@ export class EntityLoader { populate = this.lookupEagerLoadedRelationships(entityName, populate); } + // convert nested `field` with dot syntax to PopulateOptions with children array populate.forEach(p => { if (!p.field.includes('.')) { return; @@ -68,23 +69,52 @@ export class EntityLoader { p.field = f; p.children = p.children || []; const prop = this.metadata.find(entityName)!.properties[f]; - p.children.push(this.expandNestedPopulate(prop.type, parts)); + p.children.push(this.expandNestedPopulate(prop.type, parts, p.strategy)); }); - return populate; + // merge same fields + return this.mergeNestedPopulate(populate); + } + + /** + * merge multiple populates for the same entity with different children + */ + private mergeNestedPopulate(populate: PopulateOptions[]): PopulateOptions[] { + const tmp = populate.reduce((ret, item) => { + if (!ret[item.field]) { + ret[item.field] = item; + return ret; + } + + if (!ret[item.field].children && item.children) { + ret[item.field].children = item.children; + } else if (ret[item.field].children && item.children) { + ret[item.field].children!.push(...item.children!); + } + + return ret; + }, {} as Dictionary>); + + return Object.values(tmp).map(item => { + if (item.children) { + item.children = this.mergeNestedPopulate(item.children); + } + + return item; + }); } /** * Expands `books.perex` like populate to use `children` array instead of the dot syntax */ - private expandNestedPopulate(entityName: string, parts: string[]): PopulateOptions { + private expandNestedPopulate(entityName: string, parts: string[], strategy?: LoadStrategy): PopulateOptions { const meta = this.metadata.find(entityName)!; const field = parts.shift()!; const prop = meta.properties[field]; - const ret = { field } as PopulateOptions; + const ret = { field, strategy } as PopulateOptions; if (parts.length > 0) { - ret.children = [this.expandNestedPopulate(prop.type, parts)]; + ret.children = [this.expandNestedPopulate(prop.type, parts, strategy)]; } return ret; @@ -177,7 +207,7 @@ export class EntityLoader { return []; } - const ids = Utils.unique(children.map(e => Utils.getPrimaryKeyValues(e, e.__helper!.__meta.primaryKeys, true))); + const ids = Utils.unique(children.map(e => Utils.getPrimaryKeyValues(e, e.__meta!.primaryKeys, true))); const where = { ...QueryHelper.processWhere({ [fk]: { $in: ids } }, meta.name!, this.metadata, this.driver.getPlatform()), ...(options.where as Dictionary) } as FilterQuery; return this.em.find(prop.type, where, { @@ -285,21 +315,19 @@ export class EntityLoader { const ret: PopulateOptions[] = []; const meta = this.metadata.find(entityName)!; - Object.values(meta.properties) - .filter(prop => prop.reference !== ReferenceType.SCALAR) - .forEach(prop => { - const prefixed = prefix ? `${prefix}.${prop.name}` : prop.name; - const nested = this.lookupAllRelationships(prop.type, prefixed, visited); - - if (nested.length > 0) { - ret.push(...nested); - } else { - ret.push({ - field: prefixed, - strategy: LoadStrategy.SELECT_IN, - }); - } - }); + meta.relations.forEach(prop => { + const prefixed = prefix ? `${prefix}.${prop.name}` : prop.name; + const nested = this.lookupAllRelationships(prop.type, prefixed, visited); + + if (nested.length > 0) { + ret.push(...nested); + } else { + ret.push({ + field: prefixed, + strategy: this.em.config.get('loadStrategy'), + }); + } + }); return ret; } @@ -312,7 +340,7 @@ export class EntityLoader { visited.push(entityName); const meta = this.metadata.find(entityName)!; - Object.values(meta.properties) + meta.relations .filter(prop => prop.eager) .forEach(prop => { const prefixed = prefix ? `${prefix}.${prop.name}` : prop.name; @@ -323,7 +351,7 @@ export class EntityLoader { } else { populate.push({ field: prefixed, - strategy: LoadStrategy.SELECT_IN, + strategy: this.em.config.get('loadStrategy'), }); } }); diff --git a/packages/core/src/entity/EntityTransformer.ts b/packages/core/src/entity/EntityTransformer.ts index fa883cda7a90..27b85f7b2f70 100644 --- a/packages/core/src/entity/EntityTransformer.ts +++ b/packages/core/src/entity/EntityTransformer.ts @@ -1,6 +1,6 @@ import { ArrayCollection } from './ArrayCollection'; import { Collection } from './Collection'; -import { AnyEntity, EntityData, EntityMetadata, EntityProperty, IPrimaryKey } from '../typings'; +import { AnyEntity, EntityData, EntityMetadata, IPrimaryKey } from '../typings'; import { Reference } from './Reference'; import { wrap } from './wrap'; import { Platform } from '../platforms'; @@ -10,8 +10,7 @@ export class EntityTransformer { static toObject>(entity: T, ignoreFields: string[] = [], visited = new WeakSet()): EntityData { const wrapped = entity.__helper!; - const platform = wrapped.__internal.getDriver().getPlatform(); - const meta = wrapped.__meta; + const meta = entity.__meta!; const ret = {} as EntityData; meta.primaryKeys @@ -22,14 +21,14 @@ export class EntityTransformer { if (meta.properties[pk].serializer) { value = meta.properties[pk].serializer!(entity[pk]); } else if (Utils.isEntity(entity[pk], true)) { - value = EntityTransformer.processEntity(pk, entity, ignoreFields, visited); + value = EntityTransformer.processEntity(pk, entity, ignoreFields, entity.__platform!, visited); } else { - value = platform.normalizePrimaryKey(Utils.getPrimaryKeyValue(entity, [pk])); + value = entity.__platform!.normalizePrimaryKey(Utils.getPrimaryKeyValue(entity, [pk])); } return [pk, value] as [string & keyof T, string]; }) - .forEach(([pk, value]) => ret[this.propertyName(meta, pk, platform)] = value as unknown as T[keyof T]); + .forEach(([pk, value]) => ret[this.propertyName(meta, pk, entity.__platform!)] = value as unknown as T[keyof T]); if ((!wrapped.isInitialized() && wrapped.hasPrimaryKey()) || visited.has(entity)) { return ret; @@ -45,12 +44,12 @@ export class EntityTransformer { .forEach(([prop, value]) => ret[this.propertyName(meta, prop as keyof T & string)] = value as T[keyof T]); // decorated getters - Object.values>(meta.properties) + meta.props .filter(prop => prop.getter && !prop.hidden && typeof entity[prop.name] !== 'undefined') .forEach(prop => ret[this.propertyName(meta, prop.name)] = entity[prop.name]); // decorated get methods - Object.values>(meta.properties) + meta.props .filter(prop => prop.getterName && !prop.hidden && entity[prop.getterName] as unknown instanceof Function) .forEach(prop => ret[this.propertyName(meta, prop.name)] = (entity[prop.getterName!] as unknown as () => void)()); @@ -77,7 +76,6 @@ export class EntityTransformer { private static processProperty>(prop: keyof T & string, entity: T, ignoreFields: string[], visited: WeakSet): T[keyof T] | undefined { const wrapped = entity.__helper!; const property = wrapped.__meta.properties[prop]; - const platform = wrapped.__internal.getDriver().getPlatform(); /* istanbul ignore next */ const serializer = property?.serializer; @@ -90,7 +88,7 @@ export class EntityTransformer { const customType = property?.customType; if (customType) { - return customType.toJSON(entity[prop], platform); + return customType.toJSON(entity[prop], entity.__platform!); } if (entity[prop] as unknown instanceof ArrayCollection) { @@ -98,13 +96,13 @@ export class EntityTransformer { } if (Utils.isEntity(entity[prop], true)) { - return EntityTransformer.processEntity(prop, entity, ignoreFields, visited); + return EntityTransformer.processEntity(prop, entity, ignoreFields, entity.__platform!, visited); } return entity[prop]; } - private static processEntity>(prop: keyof T, entity: T, ignoreFields: string[], visited: WeakSet): T[keyof T] | undefined { + private static processEntity>(prop: keyof T, entity: T, ignoreFields: string[], platform: Platform, visited: WeakSet): T[keyof T] | undefined { const child = entity[prop] as unknown as T | Reference; const wrapped = (child as T).__helper!; @@ -113,7 +111,7 @@ export class EntityTransformer { return wrap(child).toJSON(...args) as T[keyof T]; } - return wrapped.__internal.getDriver().getPlatform().normalizePrimaryKey(wrapped.__primaryKey as unknown as IPrimaryKey) as unknown as T[keyof T]; + return platform.normalizePrimaryKey(wrapped.__primaryKey as unknown as IPrimaryKey) as unknown as T[keyof T]; } private static processCollection>(prop: keyof T, entity: T): T[keyof T] | undefined { diff --git a/packages/core/src/entity/EntityValidator.ts b/packages/core/src/entity/EntityValidator.ts index 0a0af19ebd22..a8f624590a49 100644 --- a/packages/core/src/entity/EntityValidator.ts +++ b/packages/core/src/entity/EntityValidator.ts @@ -1,6 +1,6 @@ import { EntityData, EntityMetadata, EntityProperty, FilterQuery, AnyEntity } from '../typings'; import { ReferenceType } from '../enums'; -import { Utils } from '../utils'; +import { Utils } from '../utils/Utils'; import { ValidationError } from '../errors'; export class EntityValidator { @@ -8,7 +8,7 @@ export class EntityValidator { constructor(private strict: boolean) { } validate>(entity: T, payload: any, meta: EntityMetadata): void { - Object.values(meta.properties).forEach(prop => { + meta.props.forEach(prop => { if ([ReferenceType.ONE_TO_MANY, ReferenceType.MANY_TO_MANY].includes(prop.reference)) { this.validateCollection(entity, prop); } diff --git a/packages/core/src/entity/Reference.ts b/packages/core/src/entity/Reference.ts index eedc6b83cd5a..a8512956c0fe 100644 --- a/packages/core/src/entity/Reference.ts +++ b/packages/core/src/entity/Reference.ts @@ -7,10 +7,10 @@ export class Reference> { constructor(private entity: T) { this.set(entity); - const wrapped = this.entity.__helper!; + const meta = this.entity.__meta!; Object.defineProperty(this, '__reference', { value: true }); - wrapped.__meta.primaryKeys.forEach(primaryKey => { + meta.primaryKeys.forEach(primaryKey => { Object.defineProperty(this, primaryKey, { get() { return this.entity[primaryKey]; @@ -18,8 +18,8 @@ export class Reference> { }); }); - if (wrapped.__meta.serializedPrimaryKey && wrapped.__meta.primaryKeys[0] !== wrapped.__meta.serializedPrimaryKey) { - Object.defineProperty(this, wrapped.__meta.serializedPrimaryKey, { + if (meta.serializedPrimaryKey && meta.primaryKeys[0] !== meta.serializedPrimaryKey) { + Object.defineProperty(this, meta.serializedPrimaryKey, { get() { return this.entity.__helper!.__serializedPrimaryKey; }, @@ -80,6 +80,8 @@ export class Reference> { } this.entity = entity; + Object.defineProperty(this, '__meta', { value: this.entity.__meta!, writable: true }); + Object.defineProperty(this, '__platform', { value: this.entity.__platform!, writable: true }); Object.defineProperty(this, '__helper', { value: this.entity.__helper!, writable: true }); Object.defineProperty(this, '$', { value: this.entity, writable: true }); Object.defineProperty(this, 'get', { value: () => this.entity, writable: true }); @@ -91,7 +93,7 @@ export class Reference> { getEntity(): T { if (!this.isInitialized()) { - throw new Error(`Reference<${this.entity.__helper!.__meta.name}> ${(this.entity.__helper!.__primaryKey as Primary)} not initialized`); + throw new Error(`Reference<${this.entity.__meta!.name}> ${(this.entity.__helper!.__primaryKey as Primary)} not initialized`); } return this.entity; diff --git a/packages/core/src/entity/WrappedEntity.ts b/packages/core/src/entity/WrappedEntity.ts index 977b8d547dfd..49d9004d7a5b 100644 --- a/packages/core/src/entity/WrappedEntity.ts +++ b/packages/core/src/entity/WrappedEntity.ts @@ -6,6 +6,7 @@ import { AssignOptions, EntityAssigner } from './EntityAssigner'; import { Utils } from '../utils/Utils'; import { LockMode } from '../enums'; import { ValidationError } from '../errors'; +import { Platform } from '../platforms/Platform'; export class WrappedEntity, PK extends keyof T> { @@ -21,9 +22,7 @@ export class WrappedEntity, PK extends keyof T> { /** holds wrapped primary key so we can compute change set without eager commit */ __identifier?: EntityData; - constructor(private readonly entity: T, - readonly __internal: EntityManager, - readonly __meta: EntityMetadata) { } + constructor(private readonly entity: T) { } isInitialized(): boolean { return this.__initialized; @@ -56,16 +55,13 @@ export class WrappedEntity, PK extends keyof T> { } async init

= Populate>(populated = true, populate?: P, lockMode?: LockMode): Promise { - const wrapped = this.entity.__helper!; - const em = wrapped.__em; - - if (!em) { + if (!this.__em) { throw ValidationError.entityNotManaged(this.entity); } - await em.findOne(this.entity.constructor.name, this.entity, { refresh: true, lockMode, populate }); - wrapped.populated(populated); - wrapped.__lazyInitialized = true; + await this.__em.findOne(this.entity.constructor.name, this.entity, { refresh: true, lockMode, populate }); + this.populated(populated); + this.__lazyInitialized = true; return this.entity; } @@ -77,6 +73,14 @@ export class WrappedEntity, PK extends keyof T> { }); } + get __meta(): EntityMetadata { + return this.entity.__meta!; + } + + get __platform(): Platform { + return this.entity.__platform!; + } + get __primaryKey(): Primary { return Utils.getPrimaryKeyValue(this.entity, this.__meta.primaryKeys); } diff --git a/packages/core/src/events/EventManager.ts b/packages/core/src/events/EventManager.ts index 7f9eff432f0f..7e11a9090d0b 100644 --- a/packages/core/src/events/EventManager.ts +++ b/packages/core/src/events/EventManager.ts @@ -29,7 +29,7 @@ export class EventManager { const entity: T = (args as EventArgs).entity; // execute lifecycle hooks first - const hooks = (entity && entity.__helper!.__meta.hooks[event]) || []; + const hooks = (entity && entity.__meta!.hooks[event]) || []; listeners.push(...hooks.map(hook => [hook, entity] as [EventType, EventSubscriber])); for (const listener of this.listeners[event] || []) { @@ -49,7 +49,7 @@ export class EventManager { hasListeners>(event: EventType, entity?: T): boolean { /* istanbul ignore next */ - const hasHooks = entity?.__helper!.__meta.hooks[event]?.length; + const hasHooks = entity?.__meta!.hooks[event]?.length; if (hasHooks) { return true; diff --git a/packages/core/src/hydration/Hydrator.ts b/packages/core/src/hydration/Hydrator.ts index aca37769a892..305b427dc189 100644 --- a/packages/core/src/hydration/Hydrator.ts +++ b/packages/core/src/hydration/Hydrator.ts @@ -1,5 +1,4 @@ import { EntityManager } from '../EntityManager'; -import { Utils } from '../utils/Utils'; import { AnyEntity, EntityData, EntityMetadata, EntityProperty } from '../typings'; import { EntityFactory } from '../entity'; @@ -33,15 +32,14 @@ export abstract class Hydrator { private getProperties>(meta: EntityMetadata, entity: T): EntityProperty[] { const metadata = this.em.getMetadata(); - const root = Utils.getRootEntity(metadata, meta); - if (root.discriminatorColumn) { + if (meta.root.discriminatorColumn) { meta = metadata.find(entity.constructor.name)!; } - return Object.values(meta.properties).filter(prop => { + return meta.props.filter(prop => { // `prop.userDefined` is either `undefined` or `false` - const discriminator = root.discriminatorColumn === prop.name && prop.userDefined === false; + const discriminator = meta.root.discriminatorColumn === prop.name && prop.userDefined === false; return !prop.inherited && !discriminator && !prop.embedded; }); } diff --git a/packages/core/src/hydration/ObjectHydrator.ts b/packages/core/src/hydration/ObjectHydrator.ts index 4831d7628c9d..405ab964985b 100644 --- a/packages/core/src/hydration/ObjectHydrator.ts +++ b/packages/core/src/hydration/ObjectHydrator.ts @@ -39,7 +39,7 @@ export class ObjectHydrator extends Hydrator { private hydrateEmbeddable>(entity: T, prop: EntityProperty, data: EntityData): void { const value: Dictionary = {}; - Object.values(entity.__helper!.__meta.properties).filter(p => p.embedded?.[0] === prop.name).forEach(childProp => { + entity.__meta!.props.filter(p => p.embedded?.[0] === prop.name).forEach(childProp => { value[childProp.embedded![1]] = data[childProp.name]; }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 04ad3fd6e2db..4c5cfd47e763 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,7 @@ export { Constructor, Dictionary, PrimaryKeyType, Primary, IPrimaryKey, FilterQuery, IWrappedEntity, EntityName, EntityData, Highlighter, AnyEntity, EntityProperty, EntityMetadata, QBFilterQuery, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, - GetRepository, EntityRepositoryType, MigrationObject, + GetRepository, EntityRepositoryType, MigrationObject, DeepPartial, } from './typings'; export * from './enums'; export * from './errors'; diff --git a/packages/core/src/metadata/EntitySchema.ts b/packages/core/src/metadata/EntitySchema.ts index 9e4673545d16..549627021ffd 100644 --- a/packages/core/src/metadata/EntitySchema.ts +++ b/packages/core/src/metadata/EntitySchema.ts @@ -1,10 +1,10 @@ -import { AnyEntity, Constructor, Dictionary, EntityMetadata, EntityName, EntityProperty, ExpandProperty, NonFunctionPropertyNames } from '../typings'; +import { AnyEntity, Constructor, DeepPartial, Dictionary, EntityMetadata, EntityName, EntityProperty, ExpandProperty, NonFunctionPropertyNames } from '../typings'; import { EmbeddedOptions, EnumOptions, IndexOptions, ManyToManyOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, PrimaryKeyOptions, PropertyOptions, SerializedPrimaryKeyOptions, UniqueOptions, } from '../decorators'; import { BaseEntity, EntityRepository } from '../entity'; -import { Cascade, ReferenceType, LoadStrategy } from '../enums'; +import { Cascade, ReferenceType } from '../enums'; import { Type } from '../types'; import { Utils } from '../utils'; @@ -41,7 +41,7 @@ export class EntitySchema = AnyEntity, U extends AnyEntit Object.assign(this._meta, { className: meta.name, properties: {}, hooks: {}, filters: {}, primaryKeys: [], indexes: [], uniques: [] }, meta); } - static fromMetadata = AnyEntity, U extends AnyEntity | undefined = undefined>(meta: EntityMetadata): EntitySchema { + static fromMetadata = AnyEntity, U extends AnyEntity | undefined = undefined>(meta: EntityMetadata | DeepPartial>): EntitySchema { const schema = new EntitySchema(meta as Metadata); schema.internal = true; @@ -225,6 +225,7 @@ export class EntitySchema = AnyEntity, U extends AnyEntit this.initProperties(); this.initPrimaryKeys(); + this._meta.props = Object.values(this._meta.properties); this.initialized = true; return this; @@ -310,7 +311,6 @@ export class EntitySchema = AnyEntity, U extends AnyEntit return { reference, cascade: [Cascade.PERSIST, Cascade.MERGE], - strategy: LoadStrategy.SELECT_IN, ...options, }; } diff --git a/packages/core/src/metadata/MetadataDiscovery.ts b/packages/core/src/metadata/MetadataDiscovery.ts index d81d6ee1b2b1..f8ae91bcf7c8 100644 --- a/packages/core/src/metadata/MetadataDiscovery.ts +++ b/packages/core/src/metadata/MetadataDiscovery.ts @@ -12,6 +12,7 @@ import { Cascade, ReferenceType } from '../enums'; import { MetadataError } from '../errors'; import { Platform } from '../platforms'; import { ArrayType, BlobType, Type } from '../types'; +import { EntityComparator } from '../utils/EntityComparator'; export class MetadataDiscovery { @@ -47,7 +48,14 @@ export class MetadataDiscovery { filtered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initUnsigned(prop))); filtered.forEach(meta => this.autoWireBidirectionalProperties(meta)); filtered.forEach(meta => this.discovered.push(...this.processEntity(meta))); - this.discovered.forEach(meta => Object.values(meta.properties).forEach(prop => this.initIndexes(meta, prop))); + + this.discovered.forEach(meta => { + const root = Utils.getRootEntity(this.metadata, meta); + meta.props = Object.values(meta.properties); + meta.relations = meta.props.filter(prop => prop.reference !== ReferenceType.SCALAR && prop.reference !== ReferenceType.EMBEDDED); + meta.comparableProps = meta.props.filter(prop => EntityComparator.isComparable(prop, root)); + meta.props.forEach(prop => this.initIndexes(meta, prop)); + }); const diff = Date.now() - startTime; this.logger.log('discovery', `- entity discovery finished, found ${c.green('' + this.discovered.length)} entities, took ${c.green(`${diff} ms`)}`); @@ -186,6 +194,7 @@ export class MetadataDiscovery { this.logger.log('discovery', `- processing entity ${c.cyan((entity as EntityClass).name)}${c.grey(path ? ` (${path})` : '')}`); const schema = this.getSchema(entity as Constructor); const meta = schema.init().meta; + const root = Utils.getRootEntity(this.metadata, meta); this.metadata.set(meta.className, meta); schema.meta.path = Utils.relativePath(path || meta.path, this.config.get('baseDir')); const cache = meta.useCache && meta.path && await this.cache.get(meta.className + extname(meta.path)); @@ -193,6 +202,7 @@ export class MetadataDiscovery { if (cache) { this.logger.log('discovery', `- using cached metadata for entity ${c.cyan(meta.className)}`); this.metadataProvider.loadFromCache(meta, cache); + meta.root = root; this.discovered.push(meta); return; @@ -203,12 +213,12 @@ export class MetadataDiscovery { } if (!meta.collection && meta.name) { - const root = Utils.getRootEntity(this.metadata, meta); const entityName = root.discriminatorColumn ? root.name : meta.name; meta.collection = this.namingStrategy.classToTableName(entityName!); } await this.saveToCache(meta); + meta.root = root; this.discovered.push(meta); } @@ -551,46 +561,44 @@ export class MetadataDiscovery { } private initSingleTableInheritance(meta: EntityMetadata): void { - const root = Utils.getRootEntity(this.metadata, meta); - - if (!root.discriminatorColumn) { + if (!meta.root.discriminatorColumn) { return; } - if (!root.discriminatorMap) { - root.discriminatorMap = {} as Dictionary; - const children = Object.values(this.metadata.getAll()).filter(m => Utils.getRootEntity(this.metadata, m) === root); + if (!meta.root.discriminatorMap) { + meta.root.discriminatorMap = {} as Dictionary; + const children = Object.values(this.metadata.getAll()).filter(m => m.root === meta.root); children.forEach(m => { const name = m.discriminatorValue || this.namingStrategy.classToTableName(m.className); - root.discriminatorMap![name] = m.className; + meta.root.discriminatorMap![name] = m.className; }); } - meta.discriminatorValue = Object.entries(root.discriminatorMap!).find(([, className]) => className === meta.className)?.[0]; + meta.discriminatorValue = Object.entries(meta.root.discriminatorMap!).find(([, className]) => className === meta.className)?.[0]; - if (!root.properties[root.discriminatorColumn]) { - root.properties[root.discriminatorColumn] = this.createDiscriminatorProperty(root); + if (!meta.root.properties[meta.root.discriminatorColumn]) { + meta.root.properties[meta.root.discriminatorColumn] = this.createDiscriminatorProperty(meta.root); } - Utils.defaultValue(root.properties[root.discriminatorColumn], 'items', Object.keys(root.discriminatorMap)); - Utils.defaultValue(root.properties[root.discriminatorColumn], 'index', true); + Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'items', Object.keys(meta.root.discriminatorMap)); + Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'index', true); - if (root === meta) { + if (meta.root === meta) { return; } Object.values(meta.properties).forEach(prop => { - const exists = root.properties[prop.name]; - root.properties[prop.name] = Utils.copy(prop); - root.properties[prop.name].nullable = true; + const exists = meta.root.properties[prop.name]; + meta.root.properties[prop.name] = Utils.copy(prop); + meta.root.properties[prop.name].nullable = true; if (!exists) { - root.properties[prop.name].inherited = true; + meta.root.properties[prop.name].inherited = true; } }); - root.indexes = Utils.unique([...root.indexes, ...meta.indexes]); - root.uniques = Utils.unique([...root.uniques, ...meta.uniques]); + meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]); + meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]); } private createDiscriminatorProperty(meta: EntityMetadata): EntityProperty { diff --git a/packages/core/src/metadata/MetadataValidator.ts b/packages/core/src/metadata/MetadataValidator.ts index 5357a76a29d6..a1d5a6add4f1 100644 --- a/packages/core/src/metadata/MetadataValidator.ts +++ b/packages/core/src/metadata/MetadataValidator.ts @@ -80,38 +80,41 @@ export class MetadataValidator { private validateBidirectional(meta: EntityMetadata, prop: EntityProperty, metadata: MetadataStorage): void { if (prop.inversedBy) { const inverse = metadata.get(prop.type).properties[prop.inversedBy]; - this.validateOwningSide(meta, prop, inverse); + this.validateOwningSide(meta, prop, inverse, metadata); } else if (prop.mappedBy) { const inverse = metadata.get(prop.type).properties[prop.mappedBy]; - this.validateInverseSide(meta, prop, inverse); + this.validateInverseSide(meta, prop, inverse, metadata); } } - private validateOwningSide(meta: EntityMetadata, prop: EntityProperty, inverse: EntityProperty): void { + private validateOwningSide(meta: EntityMetadata, prop: EntityProperty, inverse: EntityProperty, metadata: MetadataStorage): void { // has correct `inversedBy` on owning side if (!inverse) { throw MetadataError.fromWrongReference(meta, prop, 'inversedBy'); } + /* istanbul ignore next */ + const targetClassName = metadata.find(inverse.type)?.root.className; + // has correct `inversedBy` reference type - if (inverse.type !== meta.className) { + if (inverse.type !== meta.className && targetClassName !== meta.root.className) { throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse); } - // inversed side is not defined as owner + // inverse side is not defined as owner if (inverse.inversedBy) { throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy'); } } - private validateInverseSide(meta: EntityMetadata, prop: EntityProperty, owner: EntityProperty): void { + private validateInverseSide(meta: EntityMetadata, prop: EntityProperty, owner: EntityProperty, metadata: MetadataStorage): void { // has correct `mappedBy` on inverse side if (prop.mappedBy && !owner) { throw MetadataError.fromWrongReference(meta, prop, 'mappedBy'); } // has correct `mappedBy` reference type - if (owner.type !== meta.className) { + if (owner.type !== meta.className && metadata.find(owner.type)?.root.className !== meta.root.className) { throw MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner); } diff --git a/packages/core/src/types/ArrayType.ts b/packages/core/src/types/ArrayType.ts index 6a82794ccdfa..fc2bdad36f2c 100644 --- a/packages/core/src/types/ArrayType.ts +++ b/packages/core/src/types/ArrayType.ts @@ -10,8 +10,8 @@ export class ArrayType extends Type { /** * Converts a value from its JS representation to its database representation of this type. */ - convertToDatabaseValue(value: JSType | DBType, platform: Platform): DBType { + convertToDatabaseValue(value: JSType | DBType, platform: Platform, fromQuery?: boolean): DBType { return value as DBType; } diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index d56de0418a89..cbbd596e4f2d 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -2,12 +2,21 @@ import { Cascade, EventType, LoadStrategy, QueryOrder, ReferenceType, LockMode } import { AssignOptions, Collection, EntityRepository, EntityIdentifier, IdentifiedReference, Reference } from './entity'; import { EntitySchema } from './metadata'; import { Type } from './types'; +import { Platform } from './platforms'; export type Constructor = new (...args: any[]) => T; export type Dictionary = { [k: string]: T }; // eslint-disable-next-line @typescript-eslint/ban-types export type NonFunctionPropertyNames = NonNullable<{ [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>; +export type DeepPartial = T & { + [P in keyof T]?: T[P] extends (infer U)[] + ? DeepPartial[] + : T[P] extends Readonly[] + ? Readonly>[] + : DeepPartial +}; + export const EntityRepositoryType = Symbol('EntityRepositoryType'); export const PrimaryKeyType = Symbol('PrimaryKeyType'); export type Primary = T extends { [PrimaryKeyType]: infer PK } @@ -74,7 +83,7 @@ export interface IWrappedEntityInternal, PK extends keyof __meta: EntityMetadata; __data: Dictionary; __em?: any; // we cannot have `EntityManager` here as that causes a cycle - __internal: any; // we cannot have `EntityManager` here as that causes a cycle + __platform: Platform; __initialized: boolean; __originalEntityData?: EntityData; __identifier?: EntityIdentifier; @@ -86,7 +95,14 @@ export interface IWrappedEntityInternal, PK extends keyof __serializedPrimaryKey: string & keyof T; } -export type AnyEntity = { [K in keyof T]?: T[K] } & { [PrimaryKeyType]?: unknown; [EntityRepositoryType]?: unknown; __helper?: IWrappedEntityInternal }; +export type AnyEntity = { [K in keyof T]?: T[K] } & { + [PrimaryKeyType]?: unknown; + [EntityRepositoryType]?: unknown; + __helper?: IWrappedEntityInternal; + __meta?: EntityMetadata; + __platform?: Platform; +}; + // eslint-disable-next-line @typescript-eslint/ban-types export type EntityClass> = Function & { prototype: T }; export type EntityClassGroup> = { entity: EntityClass; schema: EntityMetadata | EntitySchema }; @@ -173,6 +189,9 @@ export interface EntityMetadata = any> { versionProperty: keyof T & string; serializedPrimaryKey: keyof T & string; properties: { [K in keyof T & string]: EntityProperty }; + props: EntityProperty[]; + relations: EntityProperty[]; + comparableProps: EntityProperty[]; // for EntityComparator indexes: { properties: string | string[]; name?: string; type?: string; options?: Dictionary }[]; uniques: { properties: string | string[]; name?: string; options?: Dictionary }[]; customRepository: () => Constructor>; @@ -184,6 +203,7 @@ export interface EntityMetadata = any> { filters: Dictionary>; comment?: string; readonly?: boolean; + root: EntityMetadata; } export interface ISchemaGenerator { @@ -229,9 +249,10 @@ export interface MigrationObject { export type FilterDef> = { name: string; - cond: FilterQuery | ((args: Dictionary, type: 'read' | 'update' | 'delete') => FilterQuery); + cond: FilterQuery | ((args: Dictionary, type: 'read' | 'update' | 'delete') => FilterQuery | Promise>); default?: boolean; entity?: string[]; + args?: boolean; }; export type ExpandProperty = T extends Reference ? NonNullable : T extends Collection ? NonNullable : NonNullable; diff --git a/packages/core/src/unit-of-work/ChangeSetComputer.ts b/packages/core/src/unit-of-work/ChangeSetComputer.ts index 234c95e877f4..58d9bc2991a7 100644 --- a/packages/core/src/unit-of-work/ChangeSetComputer.ts +++ b/packages/core/src/unit-of-work/ChangeSetComputer.ts @@ -1,6 +1,6 @@ import { Configuration, Utils } from '../utils'; import { MetadataStorage } from '../metadata'; -import { AnyEntity, EntityData, EntityProperty, Primary } from '../typings'; +import { AnyEntity, EntityData, EntityProperty } from '../typings'; import { ChangeSet, ChangeSetType } from './ChangeSet'; import { Collection, EntityValidator } from '../entity'; import { Platform } from '../platforms'; @@ -12,7 +12,7 @@ export class ChangeSetComputer { private readonly comparator = new EntityComparator(this.metadata, this.platform); constructor(private readonly validator: EntityValidator, - private readonly collectionUpdates: Collection[], + private readonly collectionUpdates: Set>, private readonly removeStack: Set, private readonly metadata: MetadataStorage, private readonly platform: Platform, @@ -39,7 +39,7 @@ export class ChangeSetComputer { this.validator.validate(changeSet.entity, changeSet.payload, meta); } - for (const prop of Object.values(meta.properties)) { + for (const prop of meta.relations) { this.processProperty(changeSet, prop); } @@ -68,10 +68,6 @@ export class ChangeSetComputer { } else if (prop.reference !== ReferenceType.SCALAR && target) { // m:1 or 1:1 this.processToOne(prop, changeSet); } - - if (prop.reference === ReferenceType.ONE_TO_ONE) { - this.processOneToOne(prop, changeSet); - } } private processToOne>(prop: EntityProperty, changeSet: ChangeSet): void { @@ -99,21 +95,10 @@ export class ChangeSetComputer { } if (prop.owner || target.getItems(false).filter(item => !item.__helper!.__initialized).length > 0) { - this.collectionUpdates.push(target); + this.collectionUpdates.add(target); } else { target.setDirty(false); // inverse side with only populated items, nothing to persist } } - private processOneToOne>(prop: EntityProperty, changeSet: ChangeSet): void { - // check diff, if we had a value on 1:1 before and now it changed (nulled or replaced), we need to trigger orphan removal - const data = changeSet.entity.__helper!.__originalEntityData as EntityData; - const em = changeSet.entity.__helper!.__em; - - if (prop.orphanRemoval && data && data[prop.name] && prop.name in changeSet.payload && em) { - const orphan = em.getReference(prop.type, data[prop.name] as Primary); - em.getUnitOfWork().scheduleOrphanRemoval(orphan); - } - } - } diff --git a/packages/core/src/unit-of-work/ChangeSetPersister.ts b/packages/core/src/unit-of-work/ChangeSetPersister.ts index 1db9da21b6ee..2dab627cf329 100644 --- a/packages/core/src/unit-of-work/ChangeSetPersister.ts +++ b/packages/core/src/unit-of-work/ChangeSetPersister.ts @@ -40,7 +40,7 @@ export class ChangeSetPersister { } async executeDeletes>(changeSets: ChangeSet[], ctx?: Transaction): Promise { - const meta = changeSets[0].entity.__helper!.__meta; + const meta = changeSets[0].entity.__meta!; const pk = Utils.getPrimaryKeyHash(meta.primaryKeys); if (meta.compositePK) { @@ -55,7 +55,7 @@ export class ChangeSetPersister { private processProperties>(changeSet: ChangeSet): void { const meta = this.metadata.find(changeSet.name)!; - for (const prop of Object.values(meta.properties)) { + for (const prop of meta.props) { this.processProperty(changeSet, prop); } } @@ -107,7 +107,7 @@ export class ChangeSetPersister { } changeSet.entity.__helper!.populated(); - Object.values(meta.properties).forEach(prop => { + meta.relations.forEach(prop => { const value = changeSet.entity[prop.name]; if (Utils.isEntity(value, true)) { @@ -165,7 +165,7 @@ export class ChangeSetPersister { changeSet.entity[prop.name] = changeSet.payload[prop.name] = prop.onCreate(changeSet.entity); if (prop.primary) { - this.mapPrimaryKey(changeSet.entity.__helper!.__meta, changeSet.entity[prop.name] as unknown as IPrimaryKey, changeSet); + this.mapPrimaryKey(changeSet.entity.__meta!, changeSet.entity[prop.name] as unknown as IPrimaryKey, changeSet); } } @@ -185,12 +185,12 @@ export class ChangeSetPersister { */ private mapReturnedValues>(changeSet: ChangeSet, res: QueryResult, meta: EntityMetadata): void { if (res.row && Object.keys(res.row).length > 0) { - const data = Object.values(meta.properties).reduce((data, prop) => { + const data = meta.props.reduce((ret, prop) => { if (prop.fieldNames && res.row![prop.fieldNames[0]] && !Utils.isDefined(changeSet.entity[prop.name], true)) { - data[prop.name] = changeSet.payload[prop.name] = res.row![prop.fieldNames[0]]; + ret[prop.name] = changeSet.payload[prop.name] = res.row![prop.fieldNames[0]]; } - return data; + return ret; }, {} as Dictionary); this.hydrator.hydrate(changeSet.entity, meta, data as EntityData, false, true); } diff --git a/packages/core/src/unit-of-work/UnitOfWork.ts b/packages/core/src/unit-of-work/UnitOfWork.ts index 7c31dbb1dbe3..31329bb6d7b5 100644 --- a/packages/core/src/unit-of-work/UnitOfWork.ts +++ b/packages/core/src/unit-of-work/UnitOfWork.ts @@ -18,8 +18,8 @@ export class UnitOfWork { private readonly persistStack = new Set(); private readonly removeStack = new Set(); private readonly orphanRemoveStack = new Set(); - private readonly changeSets: ChangeSet[] = []; - private readonly collectionUpdates: Collection[] = []; + private readonly changeSets = new Map>(); + private readonly collectionUpdates = new Set>(); private readonly extraUpdates = new Set<[AnyEntity, string, AnyEntity | Reference]>(); private readonly metadata = this.em.getMetadata(); private readonly platform = this.em.getDriver().getPlatform(); @@ -32,6 +32,7 @@ export class UnitOfWork { constructor(private readonly em: EntityManager) { } merge>(entity: T, visited = new WeakSet(), mergeData = true): void { + const meta = entity.__meta!; const wrapped = entity.__helper!; wrapped.__em = this.em; @@ -44,8 +45,7 @@ export class UnitOfWork { return; } - const root = Utils.getRootEntity(this.metadata, wrapped.__meta); - this.identityMap.set(`${root.name}-${wrapped.__serializedPrimaryKey}`, entity); + this.identityMap.set(`${meta.root.name}-${wrapped.__serializedPrimaryKey}`, entity); if (mergeData || !entity.__helper!.__originalEntityData) { entity.__helper!.__originalEntityData = this.comparator.prepareEntity(entity); @@ -58,7 +58,7 @@ export class UnitOfWork { * @internal */ registerManaged>(entity: T, data?: EntityData, refresh?: boolean, newEntity?: boolean): T { - const root = Utils.getRootEntity(this.metadata, entity.__helper!.__meta); + const root = entity.__meta!.root; this.identityMap.set(`${root.name}-${entity.__helper!.__serializedPrimaryKey}`, entity); if (newEntity) { @@ -78,8 +78,8 @@ export class UnitOfWork { * Returns entity from the identity map. For composite keys, you need to pass an array of PKs in the same order as they are defined in `meta.primaryKeys`. */ getById>(entityName: string, id: Primary | Primary[]): T { - const root = Utils.getRootEntity(this.metadata, this.metadata.find(entityName)!); - const hash = Utils.getPrimaryKeyHash(Utils.asArray(id) as string[]); + const root = this.metadata.find(entityName)!.root; + const hash = Array.isArray(id) ? Utils.getPrimaryKeyHash(id as string[]) : id; const token = `${root.name}-${hash}`; return this.identityMap.get(token) as T; @@ -134,11 +134,11 @@ export class UnitOfWork { } getChangeSets(): ChangeSet[] { - return this.changeSets; + return [...this.changeSets.values()]; } getCollectionUpdates(): Collection[] { - return this.collectionUpdates; + return [...this.collectionUpdates]; } getExtraUpdates(): Set<[AnyEntity, string, (AnyEntity | Reference)]> { @@ -153,22 +153,24 @@ export class UnitOfWork { } this.initIdentifier(entity); - this.changeSets.push(cs); + this.checkOrphanRemoval(cs); + this.changeSets.set(entity, cs); this.persistStack.delete(entity); entity.__helper!.__originalEntityData = this.comparator.prepareEntity(entity); } recomputeSingleChangeSet>(entity: T): void { - const idx = this.changeSets.findIndex(cs => cs.entity === entity); + const changeSet = this.changeSets.get(entity); - if (idx === -1) { + if (!changeSet) { return; } const cs = this.changeSetComputer.computeChangeSet(entity); if (cs) { - Object.assign(this.changeSets[idx].payload, cs.payload); + this.checkOrphanRemoval(cs); + Object.assign(changeSet.payload, cs.payload); entity.__helper!.__originalEntityData = this.comparator.prepareEntity(entity); } } @@ -212,7 +214,7 @@ export class UnitOfWork { await this.eventManager.dispatchEvent(EventType.onFlush, { em: this.em, uow: this }); // nothing to do, do not start transaction - if (this.changeSets.length === 0 && this.collectionUpdates.length === 0 && this.extraUpdates.size === 0) { + if (this.changeSets.size === 0 && this.collectionUpdates.size === 0 && this.extraUpdates.size === 0) { await this.eventManager.dispatchEvent(EventType.afterFlush, { em: this.em, uow: this }); this.postCommitCleanup(); @@ -254,14 +256,13 @@ export class UnitOfWork { unsetIdentity(entity: AnyEntity): void { const wrapped = entity.__helper!; - const root = Utils.getRootEntity(this.metadata, wrapped.__meta); - this.identityMap.delete(`${root.name}-${wrapped.__serializedPrimaryKey}`); + this.identityMap.delete(`${entity.__meta!.root.name}-${wrapped.__serializedPrimaryKey}`); delete wrapped.__identifier; delete wrapped.__originalEntityData; } computeChangeSets(): void { - this.changeSets.length = 0; + this.changeSets.clear(); for (const entity of this.identityMap.values()) { if (!this.removeStack.has(entity) && !this.orphanRemoveStack.has(entity)) { @@ -280,7 +281,7 @@ export class UnitOfWork { for (const entity of this.removeStack) { const meta = this.metadata.find(entity.constructor.name)!; - this.changeSets.push({ entity, type: ChangeSetType.DELETE, name: meta.name, collection: meta.collection, payload: {} } as ChangeSet); + this.changeSets.set(entity, { entity, type: ChangeSetType.DELETE, name: meta.name, collection: meta.collection, payload: {} } as ChangeSet); } } @@ -306,20 +307,36 @@ export class UnitOfWork { this.initIdentifier(entity); - for (const prop of Object.values(wrapped.__meta.properties)) { + for (const prop of entity.__meta!.relations) { const reference = Reference.unwrapReference(entity[prop.name]); this.processReference(entity, prop, reference, visited); } - const changeSet = this.changeSetComputer.computeChangeSet(entity); + const changeSet = this.changeSetComputer.computeChangeSet(entity); if (changeSet) { - this.changeSets.push(changeSet); + this.checkOrphanRemoval(changeSet); + this.changeSets.set(entity, changeSet); this.persistStack.delete(entity); wrapped.__originalEntityData = changeSet.payload; } } + private checkOrphanRemoval>(changeSet: ChangeSet): void { + const meta = this.metadata.find(changeSet.name)!; + const props = meta.relations.filter(prop => prop.reference === ReferenceType.ONE_TO_ONE); + + for (const prop of props) { + // check diff, if we had a value on 1:1 before and now it changed (nulled or replaced), we need to trigger orphan removal + const data = changeSet.entity.__helper!.__originalEntityData; + + if (prop.orphanRemoval && data && data[prop.name] && prop.name in changeSet.payload) { + const orphan = this.getById(prop.type, data[prop.name]); + this.scheduleOrphanRemoval(orphan); + } + } + } + private initIdentifier>(entity: T): void { const wrapped = entity.__helper!; @@ -381,8 +398,8 @@ export class UnitOfWork { this.persistStack.clear(); this.removeStack.clear(); this.orphanRemoveStack.clear(); - this.changeSets.length = 0; - this.collectionUpdates.length = 0; + this.changeSets.clear(); + this.collectionUpdates.clear(); this.extraUpdates.clear(); this.working = false; } @@ -400,9 +417,7 @@ export class UnitOfWork { case Cascade.REMOVE: this.remove(entity, visited); break; } - const meta = this.metadata.find(entity.constructor.name)!; - - for (const prop of Object.values(meta.properties).filter(prop => prop.reference !== ReferenceType.SCALAR)) { + for (const prop of entity.__meta!.relations) { this.cascadeReference(entity, prop, type, visited, options); } } @@ -535,8 +550,12 @@ export class UnitOfWork { return; } + const props = changeSets[0].entity.__meta!.relations.filter(prop => { + return (prop.reference === ReferenceType.ONE_TO_ONE && prop.owner) || prop.reference === ReferenceType.MANY_TO_ONE; + }); + for (const changeSet of changeSets) { - this.findExtraUpdates(changeSet); + this.findExtraUpdates(changeSet, props); await this.runHooks(EventType.beforeCreate, changeSet, true); } @@ -548,20 +567,21 @@ export class UnitOfWork { } } - private findExtraUpdates>(changeSet: ChangeSet): void { - Object.values(changeSet.entity.__helper!.__meta.properties) - .filter(prop => (prop.reference === ReferenceType.ONE_TO_ONE && prop.owner) || prop.reference === ReferenceType.MANY_TO_ONE) - .filter(prop => changeSet.entity[prop.name]) - .forEach(prop => { - const cs = this.changeSets.find(cs => cs.entity === Reference.unwrapReference(changeSet.entity[prop.name])); - const isScheduledForInsert = cs && cs.type === ChangeSetType.CREATE && !cs.persisted; + private findExtraUpdates>(changeSet: ChangeSet, props: EntityProperty[]): void { + for (const prop of props) { + if (!changeSet.entity[prop.name]) { + continue; + } - if (isScheduledForInsert) { - this.extraUpdates.add([changeSet.entity, prop.name, changeSet.entity[prop.name]]); - delete changeSet.entity[prop.name]; - delete changeSet.payload[prop.name]; - } - }); + const cs = this.changeSets.get(Reference.unwrapReference(changeSet.entity[prop.name])); + const isScheduledForInsert = cs && cs.type === ChangeSetType.CREATE && !cs.persisted; + + if (isScheduledForInsert) { + this.extraUpdates.add([changeSet.entity, prop.name, changeSet.entity[prop.name]]); + delete changeSet.entity[prop.name]; + delete changeSet.payload[prop.name]; + } + } } private async commitUpdateChangeSets>(changeSets: ChangeSet[], ctx?: Transaction): Promise { @@ -619,20 +639,18 @@ export class UnitOfWork { private getCommitOrder(): string[] { const calc = new CommitOrderCalculator(); - const types = Utils.unique(this.changeSets.map(cs => cs.name)); - types.forEach(entityName => calc.addNode(entityName)); - let entityName = types.pop(); + const set = new Set(); + this.changeSets.forEach(cs => set.add(cs.name)); + set.forEach(entityName => calc.addNode(entityName)); - while (entityName) { - for (const prop of Object.values(this.metadata.find(entityName)!.properties)) { + for (const entityName of set) { + for (const prop of this.metadata.find(entityName)!.props) { if (!calc.hasNode(prop.type)) { continue; } this.addCommitDependency(calc, prop, entityName); } - - entityName = types.pop(); } return calc.sort(); diff --git a/packages/core/src/utils/Configuration.ts b/packages/core/src/utils/Configuration.ts index ce8a17e39824..6e5e53d368a2 100644 --- a/packages/core/src/utils/Configuration.ts +++ b/packages/core/src/utils/Configuration.ts @@ -19,6 +19,7 @@ import { IDatabaseDriver } from '../drivers/IDatabaseDriver'; import { EntityOptions } from '../decorators'; import { NotFoundError } from '../errors'; import { RequestContext } from './RequestContext'; +import { LoadStrategy } from '../enums'; export class Configuration { @@ -42,6 +43,7 @@ export class Configuration { findOneOrFailHandler: (entityName: string, where: Dictionary | IPrimaryKey) => NotFoundError.findOneFailed(entityName, where), baseDir: process.cwd(), hydrator: ObjectHydrator, + loadStrategy: LoadStrategy.SELECT_IN, autoJoinOneToOneOwner: true, propagateToOneOwner: true, populateAfterFlush: false, @@ -327,6 +329,7 @@ export interface MikroORMOptions ex timezone?: string; ensureIndexes: boolean; hydrator: { new (factory: EntityFactory, em: EntityManager): Hydrator }; + loadStrategy: LoadStrategy; entityRepository?: Constructor>; replicas?: Partial[]; strict: boolean; diff --git a/packages/core/src/utils/EntityComparator.ts b/packages/core/src/utils/EntityComparator.ts index 4eaa2cc0fdaf..878f813674fa 100644 --- a/packages/core/src/utils/EntityComparator.ts +++ b/packages/core/src/utils/EntityComparator.ts @@ -25,21 +25,20 @@ export class EntityComparator { } const meta = this.metadata.get(entity.constructor.name); - const root = Utils.getRootEntity(this.metadata, meta); const ret = {} as EntityData; if (meta.discriminatorValue) { - ret[root.discriminatorColumn as keyof T] = meta.discriminatorValue as unknown as T[keyof T]; + ret[meta.root.discriminatorColumn as keyof T] = meta.discriminatorValue as unknown as T[keyof T]; } - // copy all props, ignore collections and references, process custom types - Object.values>(meta.properties).forEach(prop => { - if (this.shouldIgnoreProperty(entity, prop, root)) { + // copy all comparable props, ignore collections and references, process custom types + meta.comparableProps.forEach(prop => { + if (this.shouldIgnoreProperty(entity, prop)) { return; } if (prop.reference === ReferenceType.EMBEDDED) { - return Object.values(meta.properties).filter(p => p.embedded?.[0] === prop.name).forEach(childProp => { + return meta.props.filter(p => p.embedded?.[0] === prop.name).forEach(childProp => { ret[childProp.name as keyof T] = Utils.copy(entity[prop.name][childProp.embedded![1]]); }); } @@ -74,23 +73,35 @@ export class EntityComparator { return ret; } - private shouldIgnoreProperty>(entity: T, prop: EntityProperty, root: EntityMetadata) { - if (!(prop.name in entity) || prop.persist === false) { + /** + * should be used only for `meta.comparableProps` that are defined based on the static `isComparable` helper + */ + private shouldIgnoreProperty>(entity: T, prop: EntityProperty) { + if (!(prop.name in entity)) { return true; } const value = entity[prop.name]; - const collection = Utils.isCollection(value); const noPkRef = Utils.isEntity(value, true) && !value.__helper!.hasPrimaryKey(); const noPkProp = prop.primary && !Utils.isDefined(value, true); - const inverse = prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner; - const discriminator = prop.name === root.discriminatorColumn; // bidirectional 1:1 and m:1 fields are defined as setters, we need to check for `undefined` explicitly const isSetter = [ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy); const emptyRef = isSetter && value === undefined; - return collection || noPkProp || noPkRef || inverse || discriminator || emptyRef || prop.version; + return noPkProp || noPkRef || emptyRef || prop.version; + } + + /** + * perf: used to generate list of comparable properties during discovery, so we speed up the runtime comparison + */ + static isComparable>(prop: EntityProperty, root: EntityMetadata) { + const virtual = prop.persist === false; + const inverse = prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner; + const discriminator = prop.name === root.discriminatorColumn; + const collection = prop.reference === ReferenceType.ONE_TO_MANY || prop.reference === ReferenceType.MANY_TO_MANY; + + return !virtual && !collection && !inverse && !discriminator && !prop.version; } } diff --git a/packages/core/src/utils/QueryHelper.ts b/packages/core/src/utils/QueryHelper.ts index 5ba80424fdeb..ceb25467c6ca 100644 --- a/packages/core/src/utils/QueryHelper.ts +++ b/packages/core/src/utils/QueryHelper.ts @@ -108,7 +108,7 @@ export class QueryHelper { } if (prop?.customType && convertCustomTypes) { - value = QueryHelper.processCustomType(prop, value, platform); + value = QueryHelper.processCustomType(prop, value, platform, undefined, true); } if (Array.isArray(value) && !QueryHelper.isSupportedOperator(key) && !key.includes('?')) { @@ -165,19 +165,19 @@ export class QueryHelper { return filter.default || filterName in options; } - static processCustomType(prop: EntityProperty, cond: FilterQuery, platform: Platform, key?: string): FilterQuery { + static processCustomType(prop: EntityProperty, cond: FilterQuery, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery { if (Utils.isPlainObject(cond)) { return Object.keys(cond).reduce((o, k) => { - o[k] = QueryHelper.processCustomType(prop, cond[k], platform, k); + o[k] = QueryHelper.processCustomType(prop, cond[k], platform, k, fromQuery); return o; }, {}); } if (Array.isArray(cond) && !(key && ARRAY_OPERATORS.includes(key))) { - return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key)) as FilterQuery; + return cond.map(v => QueryHelper.processCustomType(prop, v, platform, key, fromQuery)) as FilterQuery; } - return prop.customType.convertToDatabaseValue(cond, platform); + return prop.customType.convertToDatabaseValue(cond, platform, fromQuery); } private static processEntity(entity: AnyEntity, root?: boolean): any { diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index efc72c234b54..8ba455ffab83 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -573,4 +573,13 @@ export class Utils { } } + /** + * @see https://github.com/mikro-orm/mikro-orm/issues/840 + */ + static propertyDecoratorReturnValue(): any { + if (process.env.BABEL_DECORATORS_COMPAT) { + return {}; + } + } + } diff --git a/packages/entity-generator/CHANGELOG.md b/packages/entity-generator/CHANGELOG.md index e91e0878027b..0d349addd1e5 100644 --- a/packages/entity-generator/CHANGELOG.md +++ b/packages/entity-generator/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/entity-generator + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/entity-generator diff --git a/packages/entity-generator/package.json b/packages/entity-generator/package.json index ca683da0f246..5e288a3e8e69 100644 --- a/packages/entity-generator/package.json +++ b/packages/entity-generator/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/entity-generator", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,11 +47,11 @@ "access": "public" }, "dependencies": { - "@mikro-orm/knex": "^4.0.3", + "@mikro-orm/knex": "^4.0.4", "fs-extra": "^9.0.1" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/knex/CHANGELOG.md b/packages/knex/CHANGELOG.md index c29e8ef95777..58e55b3d2f84 100644 --- a/packages/knex/CHANGELOG.md +++ b/packages/knex/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + + +### Bug Fixes + +* **query-builder:** fix mapping of 1:1 inverse sides ([a46281e](https://github.com/mikro-orm/mikro-orm/commit/a46281e0d8de6385e2c49fd250d284293421f2dc)), closes [#849](https://github.com/mikro-orm/mikro-orm/issues/849) +* **query-builder:** fix mapping of nested 1:1 properties ([9799e70](https://github.com/mikro-orm/mikro-orm/commit/9799e70bd7235695f4f1e55b25fe61bbc158eb38)) + + +### Performance Improvements + +* move reference to metadata to entity prototype + more improvements ([#843](https://github.com/mikro-orm/mikro-orm/issues/843)) ([f71e4c2](https://github.com/mikro-orm/mikro-orm/commit/f71e4c2b8dd0bbfb0658dc8a366444ec1a49c187)), closes [#732](https://github.com/mikro-orm/mikro-orm/issues/732) + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/packages/knex/package.json b/packages/knex/package.json index 2719e86d9551..138c0226d87f 100644 --- a/packages/knex/package.json +++ b/packages/knex/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/knex", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -51,7 +51,7 @@ "knex": "^0.21.1" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0", diff --git a/packages/knex/src/AbstractSqlDriver.ts b/packages/knex/src/AbstractSqlDriver.ts index 2fc999e343ad..97c11a4e3e3b 100644 --- a/packages/knex/src/AbstractSqlDriver.ts +++ b/packages/knex/src/AbstractSqlDriver.ts @@ -115,7 +115,7 @@ export abstract class AbstractSqlDriver this.shouldHaveColumn(prop, populate)) .forEach(prop => { if (prop.fieldNames.length > 1) { // composite keys @@ -134,10 +134,10 @@ export abstract class AbstractSqlDriver field); - const toPopulate: PopulateOptions[] = Object.values(meta.properties) + const toPopulate: PopulateOptions[] = meta.relations .filter(prop => prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner && !relationsToPopulate.includes(prop.name)) .map(prop => ({ field: prop.name, strategy: prop.strategy })); @@ -360,18 +360,18 @@ export abstract class AbstractSqlDriver rows[0]) as T[]; } - protected getFieldsForJoinedLoad>(qb: QueryBuilder, meta: EntityMetadata, populate: PopulateOptions[] = [], parentTableAlias?: string, parentJoinPath?: string): Field[] { + protected getFieldsForJoinedLoad>(qb: QueryBuilder, meta: EntityMetadata, populate: PopulateOptions[] = [], parentTableAlias?: string, parentJoinPath?: string): Field[] { const fields: Field[] = []; const joinedProps = this.joinedProps(meta, populate); // alias all fields in the primary table - Object.values>(meta.properties) + meta.props .filter(prop => this.shouldHaveColumn(prop, populate)) .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias))); joinedProps.forEach(relation => { const prop = meta.properties[relation.field]; - const meta2 = this.metadata.find(prop.type)!; + const meta2 = this.metadata.find(prop.type)!; const tableAlias = qb.getNextAlias(prop.name); const field = parentTableAlias ? `${parentTableAlias}.${prop.name}` : prop.name; const path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`; @@ -477,7 +477,7 @@ export abstract class AbstractSqlDriver>(meta: EntityMetadata, populate: PopulateOptions[], joinedProps: PopulateOptions[], qb: QueryBuilder, fields?: Field[]): Field[] { - const lazyProps = Object.values>(meta.properties).filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all)); + const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all)); const hasExplicitFields = !!fields; if (fields) { @@ -485,12 +485,12 @@ export abstract class AbstractSqlDriver 0) { fields = this.getFieldsForJoinedLoad(qb, meta, populate); } else if (lazyProps.length > 0) { - const props = Object.values>(meta.properties).filter(prop => this.shouldHaveColumn(prop, populate, false)); + const props = meta.props.filter(prop => this.shouldHaveColumn(prop, populate, false)); fields = Utils.flatten(props.filter(p => !lazyProps.includes(p)).map(p => p.fieldNames)); } if (fields && !hasExplicitFields) { - Object.values>(meta.properties) + meta.props .filter(prop => prop.formula) .forEach(prop => { const alias = qb.ref(qb.alias).toString(); diff --git a/packages/knex/src/query/CriteriaNode.ts b/packages/knex/src/query/CriteriaNode.ts index 3509809000fa..fcdbedc0ce05 100644 --- a/packages/knex/src/query/CriteriaNode.ts +++ b/packages/knex/src/query/CriteriaNode.ts @@ -20,7 +20,7 @@ export class CriteriaNode { if (meta && key) { Utils.splitPrimaryKeys(key).forEach(k => { - this.prop = Object.values(meta.properties).find(prop => prop.name === k || (prop.fieldNames || []).includes(k)); + this.prop = meta.props.find(prop => prop.name === k || (prop.fieldNames || []).includes(k)); if (validate && !this.prop && !k.includes('.') && !Utils.isOperator(k) && !CriteriaNode.isCustomExpression(k)) { throw new Error(`Trying to query by not existing property ${entityName}.${k}`); @@ -54,7 +54,7 @@ export class CriteriaNode { switch (type) { case ReferenceType.MANY_TO_ONE: return false; - case ReferenceType.ONE_TO_ONE: return /* istanbul ignore next */ !this.prop!.owner && !this.parent?.parent; + case ReferenceType.ONE_TO_ONE: return !this.prop!.owner; case ReferenceType.ONE_TO_MANY: return scalar || operator; case ReferenceType.MANY_TO_MANY: return scalar || operator; default: return false; @@ -62,8 +62,9 @@ export class CriteriaNode { } renameFieldToPK(qb: IQueryBuilder): string { + const alias = qb.getAliasForJoinPath(this.getPath()); + if (this.prop!.reference === ReferenceType.MANY_TO_MANY) { - const alias = qb.getAliasForJoinPath(this.getPath()); return Utils.getPrimaryKeyHash(this.prop!.inverseJoinColumns.map(col => `${alias}.${col}`)); } @@ -71,24 +72,23 @@ export class CriteriaNode { return Utils.getPrimaryKeyHash(this.prop!.joinColumns); } - const meta = this.metadata.find(this.prop!.type)!; - const alias = qb.getAliasForJoinPath(this.getPath()); - const pks = Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames)); - - return Utils.getPrimaryKeyHash(pks.map(col => `${alias}.${col}`)); + return Utils.getPrimaryKeyHash(this.prop!.referencedColumnNames.map(col => `${alias}.${col}`)); } getPath(): string { + const parentPath = this.parent?.getPath(); let ret = this.parent && this.prop ? this.prop.name : this.entityName; + if (parentPath && this.prop?.reference === ReferenceType.SCALAR) { + return parentPath; + } + if (this.parent && Array.isArray(this.parent.payload) && this.parent.parent && !this.key) { ret = this.parent.parent.key!; } - const parentPath = this.parent?.getPath(); - if (parentPath) { - ret = this.parent!.getPath() + '.' + ret; + ret = parentPath + '.' + ret; } else if (this.parent?.entityName && ret) { ret = this.parent.entityName + '.' + ret; } @@ -113,7 +113,7 @@ export class CriteriaNode { } getPivotPath(path: string): string { - return path + '[pivot]'; + return `${path}[pivot]`; } [inspect.custom]() { diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index eb393abd00bc..fb893d382385 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -1,4 +1,4 @@ -import { ReferenceType, Utils } from '@mikro-orm/core'; +import { Dictionary, ReferenceType, Utils } from '@mikro-orm/core'; import { CriteriaNode } from './CriteriaNode'; import { IQueryBuilder } from '../typings'; import { QueryType } from './enums'; @@ -25,13 +25,8 @@ export class ObjectCriteriaNode extends CriteriaNode { const virtual = childNode.prop?.persist === false; if (childNode.shouldInline(payload)) { - const operators = Object.keys(payload).filter(k => Utils.isOperator(k, false)); - operators.forEach(op => { - const tmp = payload[op]; - delete payload[op]; - payload[`${alias}.${field}`] = { [op]: tmp, ...(payload[`${alias}.${field}`] || {}) }; - }); - Object.assign(o, payload); + const childAlias = qb.getAliasForJoinPath(childNode.getPath()); + this.inlineChildPayload(o, payload, field, alias, childAlias); } else if (childNode.shouldRename(payload)) { o[childNode.renameFieldToPK(qb)] = payload; } else if (virtual || operator || customExpression || field.includes('.') || ![QueryType.SELECT, QueryType.COUNT].includes(qb.type)) { @@ -70,6 +65,20 @@ export class ObjectCriteriaNode extends CriteriaNode { return !!this.prop && this.prop.reference !== ReferenceType.SCALAR && !scalar && !operator; } + private inlineChildPayload(o: Dictionary, payload: Dictionary, field: string, alias?: string, childAlias?: string) { + for (const k of Object.keys(payload)) { + if (Utils.isOperator(k, false)) { + const tmp = payload[k]; + delete payload[k]; + o[`${alias}.${field}`] = { [k]: tmp, ...(o[`${alias}.${field}`] || {}) }; + } else if (this.isPrefixed(k) || Utils.isOperator(k) || !childAlias) { + o[k] = payload[k]; + } else { + o[`${childAlias}.${k}`] = payload[k]; + } + } + } + private shouldAutoJoin(nestedAlias: string | undefined): boolean { if (!this.prop || !this.parent) { return false; @@ -92,7 +101,7 @@ export class ObjectCriteriaNode extends CriteriaNode { if (this.prop!.reference === ReferenceType.MANY_TO_MANY && (scalar || operator)) { qb.join(field, nestedAlias, undefined, 'pivotJoin', this.getPath()); } else { - const prev = qb._fields!.slice(); + const prev = qb._fields?.slice(); qb.join(field, nestedAlias, undefined, 'leftJoin', this.getPath()); qb._fields = prev; } @@ -100,4 +109,8 @@ export class ObjectCriteriaNode extends CriteriaNode { return nestedAlias; } + private isPrefixed(field: string): boolean { + return !!field.match(/\w+\./); + } + } diff --git a/packages/knex/src/query/QueryBuilder.ts b/packages/knex/src/query/QueryBuilder.ts index dd0c7995f728..d238a98505b8 100644 --- a/packages/knex/src/query/QueryBuilder.ts +++ b/packages/knex/src/query/QueryBuilder.ts @@ -370,7 +370,7 @@ export class QueryBuilder = AnyEntity> { pivotAlias = oldPivotAlias ?? `e${this.aliasCounter++}`; } - const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond); + const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path); Object.assign(this._joins, joins); this._aliasMap[pivotAlias] = prop.pivotTable; } else if (prop.reference === ReferenceType.ONE_TO_ONE) { @@ -379,7 +379,9 @@ export class QueryBuilder = AnyEntity> { this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond); } - this._joins[aliasedName].path = path; + if (!this._joins[aliasedName].path && path) { + this._joins[aliasedName].path = path; + } } private prepareFields, U extends string | Raw = string | Raw>(fields: Field[], type: 'where' | 'groupBy' | 'sub-query' = 'where'): U[] { @@ -500,7 +502,7 @@ export class QueryBuilder = AnyEntity> { }); if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.alias}.*`))) { - Object.values(meta.properties) + meta.props .filter(prop => prop.formula) .forEach(prop => { const alias = this.knex.ref(this.alias).toString(); @@ -559,8 +561,8 @@ export class QueryBuilder = AnyEntity> { private autoJoinPivotTable(field: string): void { const pivotMeta = this.metadata.find(field)!; - const owner = Object.values(pivotMeta.properties).find(prop => prop.reference === ReferenceType.MANY_TO_ONE && prop.owner)!; - const inverse = Object.values(pivotMeta.properties).find(prop => prop.reference === ReferenceType.MANY_TO_ONE && !prop.owner)!; + const owner = pivotMeta.props.find(prop => prop.reference === ReferenceType.MANY_TO_ONE && prop.owner)!; + const inverse = pivotMeta.props.find(prop => prop.reference === ReferenceType.MANY_TO_ONE && !prop.owner)!; const prop = this._cond[pivotMeta.name + '.' + owner.name] || this._orderBy[pivotMeta.name + '.' + owner.name] ? inverse : owner; const pivotAlias = this.getNextAlias(); diff --git a/packages/knex/src/query/QueryBuilderHelper.ts b/packages/knex/src/query/QueryBuilderHelper.ts index 7ce77ff55354..47d09d7d1432 100644 --- a/packages/knex/src/query/QueryBuilderHelper.ts +++ b/packages/knex/src/query/QueryBuilderHelper.ts @@ -89,13 +89,14 @@ export class QueryBuilderHelper { joinOneToReference(prop: EntityProperty, ownerAlias: string, alias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary = {}): JoinOptions { const meta = this.metadata.find(prop.type)!; const prop2 = meta.properties[prop.mappedBy || prop.inversedBy]; + const table = this.getTableName(prop.type); + const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns; + const inverseJoinColumns = prop.referencedColumnNames; + const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames; return { - prop, type, cond, ownerAlias, alias, - table: this.getTableName(prop.type), - joinColumns: prop.owner ? meta.primaryKeys : prop2.joinColumns, - inverseJoinColumns: prop.owner ? meta.primaryKeys : prop.referencedColumnNames, - primaryKeys: prop.owner ? prop.joinColumns : prop2.referencedColumnNames, + prop, type, cond, ownerAlias, alias, table, + joinColumns, inverseJoinColumns, primaryKeys, }; } @@ -108,7 +109,7 @@ export class QueryBuilderHelper { }; } - joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary): Dictionary { + joinManyToManyReference(prop: EntityProperty, ownerAlias: string, alias: string, pivotAlias: string, type: 'leftJoin' | 'innerJoin' | 'pivotJoin', cond: Dictionary, path?: string): Dictionary { const ret = { [`${ownerAlias}.${prop.name}`]: { prop, type, cond, ownerAlias, @@ -121,6 +122,10 @@ export class QueryBuilderHelper { } as JoinOptions, }; + if (path) { + ret[`${ownerAlias}.${prop.name}`].path = path.endsWith('[pivot]') ? path : `${path}[pivot]`; + } + if (type === 'pivotJoin') { return ret; } @@ -128,6 +133,10 @@ export class QueryBuilderHelper { const prop2 = this.metadata.find(prop.pivotTable)!.properties[prop.type + (prop.owner ? '_inverse' : '_owner')]; ret[`${pivotAlias}.${prop2.name}`] = this.joinManyToOneReference(prop2, pivotAlias, alias, type); + if (path) { + ret[`${pivotAlias}.${prop2.name}`].path = path; + } + return ret; } @@ -413,7 +422,7 @@ export class QueryBuilderHelper { const useReturningStatement = type === QueryType.INSERT && this.platform.usesReturningStatement() && meta && !meta.compositePK; if (useReturningStatement) { - const returningProps = Object.values(meta!.properties).filter(prop => prop.primary || prop.defaultRaw); + const returningProps = meta!.props.filter(prop => prop.primary || prop.defaultRaw); qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames))); } } diff --git a/packages/mariadb/CHANGELOG.md b/packages/mariadb/CHANGELOG.md index a7d083132606..c4d23439db81 100644 --- a/packages/mariadb/CHANGELOG.md +++ b/packages/mariadb/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/mariadb + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/mariadb diff --git a/packages/mariadb/package.json b/packages/mariadb/package.json index dd55733a856e..80fc0a528567 100644 --- a/packages/mariadb/package.json +++ b/packages/mariadb/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/mariadb", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,11 +47,11 @@ "access": "public" }, "dependencies": { - "@mikro-orm/mysql-base": "^4.0.3", + "@mikro-orm/mysql-base": "^4.0.4", "mariadb": "^2.4.0" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/migrations/CHANGELOG.md b/packages/migrations/CHANGELOG.md index 1a223c392fc0..204a8e1c9217 100644 --- a/packages/migrations/CHANGELOG.md +++ b/packages/migrations/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + + +### Features + +* **migrations:** allow providing transaction context ([1089c86](https://github.com/mikro-orm/mikro-orm/commit/1089c861afcb31703a0dbdc82edf9674b2dd1576)), closes [#851](https://github.com/mikro-orm/mikro-orm/issues/851) + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/migrations diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 27f0563203ce..6ec0e8a1c439 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/migrations", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,13 +47,13 @@ "access": "public" }, "dependencies": { - "@mikro-orm/knex": "^4.0.3", + "@mikro-orm/knex": "^4.0.4", "@types/umzug": "^2.2.3", "fs-extra": "^9.0.1", "umzug": "^2.3.0" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/migrations/src/Migrator.ts b/packages/migrations/src/Migrator.ts index 83e19e3058f7..d94a36d134fd 100644 --- a/packages/migrations/src/Migrator.ts +++ b/packages/migrations/src/Migrator.ts @@ -1,5 +1,5 @@ import umzug, { Umzug, migrationsList } from 'umzug'; -import { Utils, Constructor, Dictionary } from '@mikro-orm/core'; +import { Utils, Constructor, Dictionary, Transaction } from '@mikro-orm/core'; import { SchemaGenerator, EntityManager } from '@mikro-orm/knex'; import { Migration } from './Migration'; import { MigrationRunner } from './MigrationRunner'; @@ -159,15 +159,21 @@ export class Migrator { return this.umzug[method](this.prefix(options as string[])); } - return this.driver.getConnection().transactional(async trx => { - this.runner.setMasterMigration(trx); - this.storage.setMasterMigration(trx); - const ret = await this.umzug[method](this.prefix(options as string[])); - this.runner.unsetMasterMigration(); - this.storage.unsetMasterMigration(); + if (Utils.isObject(options) && options.transaction) { + return this.runInTransaction(options.transaction, method, options); + } - return ret; - }); + return this.driver.getConnection().transactional(trx => this.runInTransaction(trx, method, options)); + } + + private async runInTransaction(trx: Transaction, method: 'up' | 'down', options: string | string[] | undefined | MigrateOptions) { + this.runner.setMasterMigration(trx); + this.storage.setMasterMigration(trx); + const ret = await this.umzug[method](this.prefix(options as string[])); + this.runner.unsetMasterMigration(); + this.storage.unsetMasterMigration(); + + return ret; } } diff --git a/packages/migrations/src/typings.ts b/packages/migrations/src/typings.ts index 8102931be621..88d91b3faaeb 100644 --- a/packages/migrations/src/typings.ts +++ b/packages/migrations/src/typings.ts @@ -1,3 +1,5 @@ +import { Transaction } from '@mikro-orm/core'; + declare module 'umzug' { interface MigrationDefinitionWithName extends UmzugMigration { @@ -11,6 +13,6 @@ declare module 'umzug' { } export type UmzugMigration = { name?: string; path?: string; file: string }; -export type MigrateOptions = { from?: string | number; to?: string | number; migrations?: string[] }; +export type MigrateOptions = { from?: string | number; to?: string | number; migrations?: string[]; transaction?: Transaction }; export type MigrationResult = { fileName: string; code: string; diff: string[] }; export type MigrationRow = { name: string; executed_at: Date }; diff --git a/packages/mikro-orm/CHANGELOG.md b/packages/mikro-orm/CHANGELOG.md index 7b2843e271f6..335bc0f6875a 100644 --- a/packages/mikro-orm/CHANGELOG.md +++ b/packages/mikro-orm/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package mikro-orm + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package mikro-orm diff --git a/packages/mikro-orm/package.json b/packages/mikro-orm/package.json index 569419f39071..d7a0a6cc66aa 100644 --- a/packages/mikro-orm/package.json +++ b/packages/mikro-orm/package.json @@ -1,6 +1,6 @@ { "name": "mikro-orm", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,11 +47,11 @@ "access": "public" }, "dependencies": { - "@mikro-orm/cli": "^4.0.3", - "@mikro-orm/core": "^4.0.3", - "@mikro-orm/entity-generator": "^4.0.3", - "@mikro-orm/migrations": "^4.0.3", - "@mikro-orm/reflection": "^4.0.3" + "@mikro-orm/cli": "^4.0.4", + "@mikro-orm/core": "^4.0.4", + "@mikro-orm/entity-generator": "^4.0.4", + "@mikro-orm/migrations": "^4.0.4", + "@mikro-orm/reflection": "^4.0.4" }, "peerDependencies": { "@mikro-orm/mariadb": "^4.0.0", diff --git a/packages/mongodb/CHANGELOG.md b/packages/mongodb/CHANGELOG.md index 6e0e3eb1a436..142f633ac197 100644 --- a/packages/mongodb/CHANGELOG.md +++ b/packages/mongodb/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/mongodb + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/mongodb diff --git a/packages/mongodb/package.json b/packages/mongodb/package.json index 19e1a240162d..a0a4b43e84f7 100644 --- a/packages/mongodb/package.json +++ b/packages/mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/mongodb", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -51,7 +51,7 @@ "mongodb": "^3.6.0" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/mysql-base/CHANGELOG.md b/packages/mysql-base/CHANGELOG.md index 9e50af22aa23..9ed2e6fa1f46 100644 --- a/packages/mysql-base/CHANGELOG.md +++ b/packages/mysql-base/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/mysql-base + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/packages/mysql-base/package.json b/packages/mysql-base/package.json index 9906adc2ed40..c007b1618967 100644 --- a/packages/mysql-base/package.json +++ b/packages/mysql-base/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/mysql-base", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,10 +47,10 @@ "access": "public" }, "dependencies": { - "@mikro-orm/knex": "^4.0.3" + "@mikro-orm/knex": "^4.0.4" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0", diff --git a/packages/mysql/CHANGELOG.md b/packages/mysql/CHANGELOG.md index 1c3ea6c0cfa8..575027fd7c76 100644 --- a/packages/mysql/CHANGELOG.md +++ b/packages/mysql/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/mysql + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/packages/mysql/package.json b/packages/mysql/package.json index 8134fb3015ca..5dab5834ae6f 100644 --- a/packages/mysql/package.json +++ b/packages/mysql/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/mysql", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,11 +47,11 @@ "access": "public" }, "dependencies": { - "@mikro-orm/mysql-base": "^4.0.3", + "@mikro-orm/mysql-base": "^4.0.4", "mysql2": "sidorares/node-mysql2#master" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/postgresql/CHANGELOG.md b/packages/postgresql/CHANGELOG.md index e88ca3084247..8930574b556b 100644 --- a/packages/postgresql/CHANGELOG.md +++ b/packages/postgresql/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/postgresql + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/postgresql diff --git a/packages/postgresql/package.json b/packages/postgresql/package.json index 94ac5e36d406..e3608df00239 100644 --- a/packages/postgresql/package.json +++ b/packages/postgresql/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/postgresql", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,11 +47,11 @@ "access": "public" }, "dependencies": { - "@mikro-orm/knex": "^4.0.3", + "@mikro-orm/knex": "^4.0.4", "pg": "^8.2.1" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/reflection/CHANGELOG.md b/packages/reflection/CHANGELOG.md index f2f33f65b3ca..0faae58b0a93 100644 --- a/packages/reflection/CHANGELOG.md +++ b/packages/reflection/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/reflection + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) **Note:** Version bump only for package @mikro-orm/reflection diff --git a/packages/reflection/package.json b/packages/reflection/package.json index 7836d1bdd67c..7d526f2b1038 100644 --- a/packages/reflection/package.json +++ b/packages/reflection/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/reflection", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -51,7 +51,7 @@ "ts-morph": "^8.0.0" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/packages/sqlite/CHANGELOG.md b/packages/sqlite/CHANGELOG.md index 79ca37b2365e..fbc6648ce2dd 100644 --- a/packages/sqlite/CHANGELOG.md +++ b/packages/sqlite/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [4.0.4](https://github.com/mikro-orm/mikro-orm/compare/v4.0.3...v4.0.4) (2020-09-19) + +**Note:** Version bump only for package @mikro-orm/sqlite + + + + + ## [4.0.3](https://github.com/mikro-orm/mikro-orm/compare/v4.0.2...v4.0.3) (2020-09-15) diff --git a/packages/sqlite/package.json b/packages/sqlite/package.json index 2bae789ceaec..0261eeac28ae 100644 --- a/packages/sqlite/package.json +++ b/packages/sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@mikro-orm/sqlite", - "version": "4.0.3", + "version": "4.0.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.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -47,12 +47,12 @@ "access": "public" }, "dependencies": { - "@mikro-orm/knex": "^4.0.3", + "@mikro-orm/knex": "^4.0.4", "fs-extra": "^9.0.1", "sqlite3": "^4.2.0" }, "devDependencies": { - "@mikro-orm/core": "^4.0.3" + "@mikro-orm/core": "^4.0.4" }, "peerDependencies": { "@mikro-orm/core": "^4.0.0" diff --git a/tests/EntityFactory.test.ts b/tests/EntityFactory.test.ts index f23df542a55e..efeec596910c 100644 --- a/tests/EntityFactory.test.ts +++ b/tests/EntityFactory.test.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'mongodb'; -import { MikroORM, Collection, EntityFactory, MetadataDiscovery, ReferenceType, wrap, Logger } from '@mikro-orm/core'; +import { MikroORM, Collection, EntityFactory, ReferenceType, wrap, Logger } from '@mikro-orm/core'; import { Book, Author, Publisher, Test, BookTag } from './entities'; import { initORMMongo, wipeDatabase } from './bootstrap'; import { AuthorRepository } from './repositories/AuthorRepository'; @@ -12,8 +12,7 @@ describe('EntityFactory', () => { beforeAll(async () => { orm = await initORMMongo(); - await new MetadataDiscovery(orm.getMetadata(), orm.em.getDriver().getPlatform(), orm.config).discover(); - factory = new EntityFactory(orm.em.getUnitOfWork(), orm.em); + factory = orm.em.getEntityFactory(); expect(orm.config.getNamingStrategy().referenceColumnName()).toBe('_id'); }); beforeEach(async () => wipeDatabase(orm.em)); diff --git a/tests/EntityHelper.mongo.test.ts b/tests/EntityHelper.mongo.test.ts index 667af7980bff..244b07b277b0 100644 --- a/tests/EntityHelper.mongo.test.ts +++ b/tests/EntityHelper.mongo.test.ts @@ -79,6 +79,7 @@ describe('EntityHelperMongo', () => { expect(wrap(god, true).__populated).toBeUndefined(); god.populated(); expect(wrap(god, true).__populated).toBe(true); + expect(wrap(god, true).__platform).toBe(orm.em.getDriver().getPlatform()); const ref = god.toReference(); expect(ref).toBeInstanceOf(Reference); diff --git a/tests/EntityManager.mongo.test.ts b/tests/EntityManager.mongo.test.ts index 75226ae71699..1f087a4202fb 100644 --- a/tests/EntityManager.mongo.test.ts +++ b/tests/EntityManager.mongo.test.ts @@ -2005,6 +2005,7 @@ describe('EntityManagerMongo', () => { const em = orm.em.fork(); em.addFilter('writtenBy', args => ({ author: args.author }), Book, false); em.addFilter('tenant', args => ({ tenant: args.tenant })); + em.addFilter('withoutParams2', () => ({})); em.addFilter('fresh', { createdAt: { $gte: new Date('2020-01-01') } }, [Author, Book], false); const author1 = new Author('n1', 'e1'); @@ -2110,6 +2111,9 @@ describe('EntityManagerMongo', () => { expect(mock.mock.calls[1][0]).toMatch(`'$set': { metaArray: [ 'a', 'b' ] }`); expect(mock.mock.calls[4][0]).toMatch(`'$set': { metaArray: [ 'a', 'b', 'c' ] }`); + + const b1 = await orm.em.findOne(Book, { metaArray: 'a' }); + expect(b1).not.toBeNull(); }); // this should run in ~600ms (when running single test locally) diff --git a/tests/EntityManager.mysql.test.ts b/tests/EntityManager.mysql.test.ts index 415e1232729a..3cc615aed159 100644 --- a/tests/EntityManager.mysql.test.ts +++ b/tests/EntityManager.mysql.test.ts @@ -1840,9 +1840,9 @@ describe('EntityManagerMySql', () => { await expect(orm.em.count(Book2, [book1.uuid, book2.uuid, book3.uuid])).resolves.toBe(3); // this test was causing TS recursion errors without the type argument // see https://github.com/mikro-orm/mikro-orm/issues/124 and https://github.com/mikro-orm/mikro-orm/issues/208 - await expect(orm.em.count(Book2, [book1, book2, book3])).resolves.toBe(3); - await expect(orm.em.count(Book2, [book1, book2, book3])).resolves.toBe(3); - const a = await orm.em.find(Book2, [book1, book2, book3]) as Book2[]; + await expect(orm.em.count(Book2, [book1, book2, book3])).resolves.toBe(3); + await expect(orm.em.count(Book2, [book1, book2, book3])).resolves.toBe(3); + const a = await orm.em.find(Book2, [book1, book2, book3]) as Book2[]; await expect(orm.em.getRepository(Book2).count([book1, book2, book3])).resolves.toBe(3); }); @@ -1875,7 +1875,7 @@ describe('EntityManagerMySql', () => { 'from `book2` as `e0` ' + 'left join `author2` as `e1` on `e0`.`author_id` = `e1`.`id` ' + 'left join `test2` as `e2` on `e0`.`uuid_pk` = `e2`.`book_uuid_pk` ' + // auto-joined 1:1 to get test id as book is inverse side - 'where `e1`.`name` = ?'); + 'where `e0`.`author_id` is not null and `e1`.`name` = ?'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1890,7 +1890,7 @@ describe('EntityManagerMySql', () => { 'left join `book2` as `e2` on `e1`.`favourite_book_uuid_pk` = `e2`.`uuid_pk` ' + 'left join `author2` as `e3` on `e2`.`author_id` = `e3`.`id` ' + 'left join `test2` as `e4` on `e0`.`uuid_pk` = `e4`.`book_uuid_pk` ' + - 'where `e3`.`name` = ?'); + 'where `e0`.`author_id` is not null and `e3`.`name` = ?'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1903,7 +1903,7 @@ describe('EntityManagerMySql', () => { 'from `book2` as `e0` ' + 'left join `author2` as `e1` on `e0`.`author_id` = `e1`.`id` ' + 'left join `test2` as `e2` on `e0`.`uuid_pk` = `e2`.`book_uuid_pk` ' + - 'where `e1`.`favourite_book_uuid_pk` = ?'); + 'where `e0`.`author_id` is not null and `e1`.`favourite_book_uuid_pk` = ?'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1918,7 +1918,7 @@ describe('EntityManagerMySql', () => { 'left join `book2` as `e2` on `e1`.`favourite_book_uuid_pk` = `e2`.`uuid_pk` ' + 'left join `author2` as `e3` on `e2`.`author_id` = `e3`.`id` ' + 'left join `test2` as `e4` on `e0`.`uuid_pk` = `e4`.`book_uuid_pk` ' + - 'where `e3`.`name` = ?'); + 'where `e0`.`author_id` is not null and `e3`.`name` = ?'); }); test('partial selects', async () => { @@ -2281,7 +2281,7 @@ describe('EntityManagerMySql', () => { 'from `book2` as `e0` ' + 'left join `author2` as `e1` on `e0`.`author_id` = `e1`.`id` ' + 'left join `test2` as `e2` on `e0`.`uuid_pk` = `e2`.`book_uuid_pk` ' + - 'where `e1`.`name` = ? and `e0`.`author_id` is not null limit ?'); + 'where `e0`.`author_id` is not null and `e1`.`name` = ? limit ?'); }); test('custom types', async () => { diff --git a/tests/EntityManager.postgre.test.ts b/tests/EntityManager.postgre.test.ts index 7fdb8c9c21b0..a6a4999aa668 100644 --- a/tests/EntityManager.postgre.test.ts +++ b/tests/EntityManager.postgre.test.ts @@ -1204,7 +1204,7 @@ describe('EntityManagerPostgre', () => { expect(mock.mock.calls[0][0]).toMatch('select "e0".*, "e0".price * 1.19 as "price_taxed" ' + 'from "book2" as "e0" ' + 'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' + - 'where "e1"."name" = $1'); + 'where "e0"."author_id" is not null and "e1"."name" = $1'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1216,7 +1216,7 @@ describe('EntityManagerPostgre', () => { 'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' + 'left join "book2" as "e2" on "e1"."favourite_book_uuid_pk" = "e2"."uuid_pk" ' + 'left join "author2" as "e3" on "e2"."author_id" = "e3"."id" ' + - 'where "e3"."name" = $1'); + 'where "e0"."author_id" is not null and "e3"."name" = $1'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1226,7 +1226,7 @@ describe('EntityManagerPostgre', () => { expect(mock.mock.calls[0][0]).toMatch('select "e0".*, "e0".price * 1.19 as "price_taxed" ' + 'from "book2" as "e0" ' + 'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' + - 'where "e1"."favourite_book_uuid_pk" = $1'); + 'where "e0"."author_id" is not null and "e1"."favourite_book_uuid_pk" = $1'); orm.em.clear(); mock.mock.calls.length = 0; @@ -1238,7 +1238,7 @@ describe('EntityManagerPostgre', () => { 'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' + 'left join "book2" as "e2" on "e1"."favourite_book_uuid_pk" = "e2"."uuid_pk" ' + 'left join "author2" as "e3" on "e2"."author_id" = "e3"."id" ' + - 'where "e3"."name" = $1'); + 'where "e0"."author_id" is not null and "e3"."name" = $1'); }); test('datetime is stored in correct timezone', async () => { diff --git a/tests/MetadataValidator.test.ts b/tests/MetadataValidator.test.ts index 6ae7deb089f9..f83562f3ef63 100644 --- a/tests/MetadataValidator.test.ts +++ b/tests/MetadataValidator.test.ts @@ -8,6 +8,7 @@ describe('MetadataValidator', () => { test('validates entity definition', async () => { const meta = { Author: { name: 'Author', className: 'Author', properties: {} } } as any; + meta.Author.root = meta.Author; expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author entity is missing @PrimaryKey()'); // many to one @@ -20,6 +21,7 @@ describe('MetadataValidator', () => { // one to many meta.Test = { name: 'Test', className: 'Test', properties: {} }; + meta.Test.root = meta.Test; meta.Author.properties.tests = { name: 'tests', reference: ReferenceType.ONE_TO_MANY, type: 'Test', mappedBy: 'foo' }; expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.tests has unknown 'mappedBy' reference: Test.foo`); @@ -37,6 +39,7 @@ describe('MetadataValidator', () => { // many to many inversedBy meta.Book = { name: 'Book', className: 'Book', properties: {} }; + meta.Book.root = meta.Book; meta.Author.properties.books = { name: 'books', reference: ReferenceType.MANY_TO_MANY, type: 'Book', inversedBy: 'bar' }; expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.books has unknown 'inversedBy' reference: Book.bar`); @@ -67,10 +70,12 @@ describe('MetadataValidator', () => { // one to one inversedBy meta.Bar = { name: 'Bar', className: 'Bar', properties: {} }; + meta.Bar.root = meta.Bar; meta.Foo.properties.bar = { name: 'bar', reference: ReferenceType.ONE_TO_ONE, type: 'Bar', inversedBy: 'bar' }; expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Foo.bar has unknown 'inversedBy' reference: Bar.bar`); meta.Foo.properties.bar.inversedBy = 'foo'; + meta.Foo.root = meta.Foo; meta.Bar.properties.foo = { name: 'foo', reference: ReferenceType.ONE_TO_ONE, type: 'FooBar', inversedBy: 'bar' }; expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Foo.bar has wrong 'inversedBy' reference type: FooBar instead of Foo`); diff --git a/tests/Migrator.test.ts b/tests/Migrator.test.ts index f134fb529aa6..43236769404f 100644 --- a/tests/Migrator.test.ts +++ b/tests/Migrator.test.ts @@ -77,7 +77,9 @@ describe('Migrator', () => { }); test('generate initial migration', async () => { + await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!); const getExecutedMigrationsMock = jest.spyOn(Migrator.prototype, 'getExecutedMigrations'); + const getPendingMigrationsMock = jest.spyOn(Migrator.prototype, 'getPendingMigrations'); getExecutedMigrationsMock.mockResolvedValueOnce(['test.ts']); const migrator = new Migrator(orm.em); const err = 'Initial migration cannot be created, as some migrations already exist'; @@ -89,6 +91,7 @@ describe('Migrator', () => { const dateMock = jest.spyOn(Date.prototype, 'toISOString'); dateMock.mockReturnValue('2019-10-13T21:48:13.382Z'); + getPendingMigrationsMock.mockResolvedValueOnce([]); const migration = await migrator.createMigration(undefined, false, true); expect(logMigrationMock).toBeCalledWith('Migration20191013214813.ts'); expect(migration).toMatchSnapshot('initial-migration-dump'); @@ -191,7 +194,6 @@ describe('Migrator', () => { const path = process.cwd() + '/temp/migrations'; const migration = await migrator.createMigration(path, true); - await writeFile(path + '/' + migration.fileName, migration.code.replace(`'mikro-orm'`, `'@mikro-orm/migrations'`)); const migratorMock = jest.spyOn(Migration.prototype, 'down'); migratorMock.mockImplementation(async () => void 0); @@ -217,6 +219,34 @@ describe('Migrator', () => { expect(calls).toMatchSnapshot('all-or-nothing'); }); + test('up/down with explicit transaction', async () => { + await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!); + const migrator = new Migrator(orm.em); + const path = process.cwd() + '/temp/migrations'; + + const migration = await migrator.createMigration(path, true); + const migratorMock = jest.spyOn(Migration.prototype, 'down'); + migratorMock.mockImplementation(async () => void 0); + + const mock = jest.fn(); + const logger = new Logger(mock, ['query']); + Object.assign(orm.config, { logger }); + + await orm.em.transactional(async em => { + await migrator.up({ transaction: em.getTransactionContext() }); + await migrator.down({ transaction: em.getTransactionContext() }); + }); + + await remove(path + '/' + migration.fileName); + const calls = mock.mock.calls.map(call => { + return call[0] + .replace(/ \[took \d+ ms]/, '') + .replace(/\[query] /, '') + .replace(/ trx\d+/, 'trx_xx'); + }); + expect(calls).toMatchSnapshot('explicit-tx'); + }); + test('up/down params [all or nothing disabled]', async () => { await orm.em.getKnex().schema.dropTableIfExists(orm.config.get('migrations').tableName!); const migrator = new Migrator(orm.em); diff --git a/tests/QueryBuilder.test.ts b/tests/QueryBuilder.test.ts index 1a3590e64105..613666621a2d 100644 --- a/tests/QueryBuilder.test.ts +++ b/tests/QueryBuilder.test.ts @@ -102,7 +102,7 @@ describe('QueryBuilder', () => { const qb2 = orm.em.createQueryBuilder(Book2); qb2.select('*').where({ author: { $ne: null, name: 'Jon Snow' } }); - expect(qb2.getQuery()).toEqual('select `e0`.*, `e0`.price * 1.19 as `price_taxed` from `book2` as `e0` left join `author2` as `e1` on `e0`.`author_id` = `e1`.`id` where `e1`.`name` = ? and `e0`.`author_id` is not null'); + expect(qb2.getQuery()).toEqual('select `e0`.*, `e0`.price * 1.19 as `price_taxed` from `book2` as `e0` left join `author2` as `e1` on `e0`.`author_id` = `e1`.`id` where `e0`.`author_id` is not null and `e1`.`name` = ?'); expect(qb2.getParams()).toEqual(['Jon Snow']); }); @@ -1633,6 +1633,26 @@ describe('QueryBuilder', () => { } }); + test('joining 1:1 inverse inside $and condition (GH issue 849)', async () => { + const sql0 = orm.em.createQueryBuilder(FooBaz2).select('*').where({ bar: 123 }).getQuery(); + expect(sql0).toBe('select `e0`.*, `e1`.`id` as `bar_id` from `foo_baz2` as `e0` left join `foo_bar2` as `e1` on `e0`.`id` = `e1`.`baz_id` where `e1`.`id` = ?'); + const expected = 'select `e0`.* from `foo_baz2` as `e0` left join `foo_bar2` as `e1` on `e0`.`id` = `e1`.`baz_id` where `e1`.`id` in (?)'; + const sql1 = orm.em.createQueryBuilder(FooBaz2).where({ bar: [123] }).getQuery(); + expect(sql1).toBe(expected); + const sql2 = orm.em.createQueryBuilder(FooBaz2).where({ bar: { id: [123] } }).getQuery(); + expect(sql2).toBe(expected); + const sql3 = orm.em.createQueryBuilder(FooBaz2).where({ bar: { id: { $in: [123] } } }).getQuery(); + expect(sql3).toBe(expected); + const sql4 = orm.em.createQueryBuilder(FooBaz2).where({ $and: [{ bar: { id: { $in: [123] } } }] }).getQuery(); + expect(sql4).toBe(expected); + const sql5 = orm.em.createQueryBuilder(FooBaz2).where({ $and: [{ bar: [123] }] }).getQuery(); + expect(sql5).toBe(expected); + const sql6 = orm.em.createQueryBuilder(FooBaz2).where({ $and: [{ bar: { id: [123] } }] }).getQuery(); + expect(sql6).toBe(expected); + const sql7 = orm.em.createQueryBuilder(Test2).select('*').where({ book: { $in: ['123'] } }).getQuery(); + expect(sql7).toBe('select `e0`.* from `test2` as `e0` where `e0`.`book_uuid_pk` in (?)'); + }); + afterAll(async () => orm.close(true)); }); diff --git a/tests/SchemaGenerator.test.ts b/tests/SchemaGenerator.test.ts index a7c854c0c325..eebaea19b385 100644 --- a/tests/SchemaGenerator.test.ts +++ b/tests/SchemaGenerator.test.ts @@ -100,7 +100,7 @@ describe('SchemaGenerator', () => { const meta = orm.getMetadata(); const generator = new SchemaGenerator(orm.em); - const newTableMeta = { + const newTableMeta = EntitySchema.fromMetadata({ properties: { id: { reference: ReferenceType.SCALAR, @@ -142,7 +142,7 @@ describe('SchemaGenerator', () => { uniques: [], collection: 'new_table', primaryKey: 'id', - } as any; + } as any).init().meta; meta.set('NewTable', newTableMeta); await generator.getUpdateSchemaSQL(false); await expect(generator.getUpdateSchemaSQL(false)).resolves.toMatchSnapshot('mysql-update-schema-create-table'); @@ -423,7 +423,7 @@ describe('SchemaGenerator', () => { const meta = orm.getMetadata(); const generator = new SchemaGenerator(orm.em as EntityManager); - const newTableMeta = { + const newTableMeta = EntitySchema.fromMetadata({ properties: { id: { reference: ReferenceType.SCALAR, @@ -465,7 +465,7 @@ describe('SchemaGenerator', () => { hooks: {}, indexes: [], uniques: [], - } as any; + } as any).init().meta; meta.set('NewTable', newTableMeta); const authorMeta = meta.get('Author2'); authorMeta.properties.termsAccepted.defaultRaw = 'false'; diff --git a/tests/__snapshots__/Migrator.test.ts.snap b/tests/__snapshots__/Migrator.test.ts.snap index 71bc905392a8..e94b77608c10 100644 --- a/tests/__snapshots__/Migrator.test.ts.snap +++ b/tests/__snapshots__/Migrator.test.ts.snap @@ -545,3 +545,24 @@ Array [ "commit (via write connection '127.0.0.1')", ] `; + +exports[`Migrator up/down with explicit transaction: explicit-tx 1`] = ` +Array [ + "begin (via write connection '127.0.0.1')", + "select table_name as table_name from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema() (via write connection '127.0.0.1')", + "create table \`mikro_orm_migrations\` (\`id\` int unsigned not null auto_increment primary key, \`name\` varchar(255), \`executed_at\` datetime default current_timestamp) (via write connection '127.0.0.1')", + "select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')", + "select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')", + "savepointtrx_xx (via write connection '127.0.0.1')", + "select 1 (via write connection '127.0.0.1')", + "release savepointtrx_xx (via write connection '127.0.0.1')", + "insert into \`mikro_orm_migrations\` (\`name\`) values (?) (via write connection '127.0.0.1')", + "select table_name as table_name from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema() (via write connection '127.0.0.1')", + "select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')", + "select * from \`mikro_orm_migrations\` order by \`id\` asc (via write connection '127.0.0.1')", + "savepointtrx_xx (via write connection '127.0.0.1')", + "release savepointtrx_xx (via write connection '127.0.0.1')", + "delete from \`mikro_orm_migrations\` where \`name\` = ? (via write connection '127.0.0.1')", + "commit (via write connection '127.0.0.1')", +] +`; diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index 2fa9305e1cf9..31208a133fcc 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -1,5 +1,5 @@ import 'reflect-metadata'; -import { EntityManager, JavaScriptMetadataProvider, MikroORM, Options, Utils } from '@mikro-orm/core'; +import { EntityManager, JavaScriptMetadataProvider, LoadStrategy, MikroORM, Options, Utils } from '@mikro-orm/core'; import { AbstractSqlDriver, SchemaGenerator, SqlEntityManager, SqlEntityRepository } from '@mikro-orm/knex'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { MongoDriver } from '@mikro-orm/mongodb'; @@ -87,7 +87,7 @@ export async function initORMMySql; } -export async function initORMPostgreSql() { +export async function initORMPostgreSql(loadStrategy = LoadStrategy.SELECT_IN) { const orm = await MikroORM.init({ entities: [Author2, Address2, Book2, BookTag2, Publisher2, Test2, FooBar2, FooBaz2, FooParam2, Label2, Configuration2, BaseEntity2, BaseEntity22], dbName: `mikro_orm_test`, @@ -98,6 +98,7 @@ export async function initORMPostgreSql() { autoJoinOneToOneOwner: false, logger: i => i, cache: { enabled: true }, + loadStrategy, }); const schemaGenerator = new SchemaGenerator(orm.em); diff --git a/tests/decorators.test.ts b/tests/decorators.test.ts index ae71a11d0fe6..ef064c9352a9 100644 --- a/tests/decorators.test.ts +++ b/tests/decorators.test.ts @@ -54,4 +54,15 @@ describe('decorators', () => { expect(storage[key].properties.test3).toMatchObject({ reference: ReferenceType.SCALAR, name: 'test3' }); }); + test('babel support', () => { + const ret1 = Property()(new Test5(), 'test3'); + expect(ret1).toBeUndefined(); + process.env.BABEL_DECORATORS_COMPAT = 'true'; + const ret2 = Property()(new Test5(), 'test3'); + expect(ret2).not.toBeUndefined(); + delete process.env.BABEL_DECORATORS_COMPAT; + const ret3 = Property()(new Test5(), 'test3'); + expect(ret3).toBeUndefined(); + }); + }); diff --git a/tests/entities/Author.ts b/tests/entities/Author.ts index 68e43aa8e948..0bde4855c98c 100644 --- a/tests/entities/Author.ts +++ b/tests/entities/Author.ts @@ -1,5 +1,5 @@ import { - AfterCreate, AfterDelete, AfterUpdate, BeforeCreate, BeforeDelete, BeforeUpdate, DateType, Collection, + AfterCreate, AfterDelete, AfterUpdate, BeforeCreate, BeforeDelete, BeforeUpdate, DateType, Collection, Filter, Cascade, Entity, ManyToMany, ManyToOne, OneToMany, Property, Index, Unique, EntityAssigner, EntityRepositoryType, } from '@mikro-orm/core'; @@ -9,6 +9,15 @@ import { BaseEntity } from './BaseEntity'; @Entity({ customRepository: () => AuthorRepository }) @Index({ name: 'custom_idx_1', properties: ['name', 'email'] }) +@Filter({ + name: 'withoutParams1', + cond(_, type) { + expect(['read', 'update', 'delete'].includes(type)).toBe(true); + return {}; + }, + args: false, + default: true, +}) export class Author extends BaseEntity { [EntityRepositoryType]: AuthorRepository; diff --git a/tests/issues/GH845.test.ts b/tests/issues/GH845.test.ts new file mode 100644 index 000000000000..c691db38674e --- /dev/null +++ b/tests/issues/GH845.test.ts @@ -0,0 +1,108 @@ +import { Entity, MikroORM, PrimaryKey, Property, OneToMany, ManyToOne, Collection, QueryOrder } from '@mikro-orm/core'; +import { SqliteDriver } from '@mikro-orm/sqlite'; + +abstract class Base { + + @PrimaryKey() + id!: number; + +} + +@Entity({ + discriminatorColumn: 'type', + discriminatorMap: { + Child1: 'Child1', + Child2: 'Child2', + }, +}) +class Parent extends Base { + + @Property() + type!: string; + + @OneToMany('Relation1', 'parent') + qaInfo = new Collection(this); + +} + +@Entity() +class Relation1 extends Base { + + @ManyToOne('Parent') + parent!: Parent; + +} + +@Entity() +class Child1 extends Parent { + + @OneToMany('Child1Specific', 'child1') + rel = new Collection(this); + +} + +@Entity() +class Child1Specific extends Base { + + @ManyToOne() + child1!: Child1; + +} + +@Entity() +class Child2 extends Parent {} + +describe('GH issue 845', () => { + + let orm: MikroORM; + + beforeAll(async () => { + orm = await MikroORM.init({ + entities: [Base, Relation1, Child1Specific, Parent, Child1, Child2], + dbName: ':memory:', + type: 'sqlite', + }); + await orm.getSchemaGenerator().createSchema(); + }); + + afterAll(async () => { + await orm.close(true); + }); + + test(`GH issue 845`, async () => { + expect(true).toBe(true); + const c1 = new Child1(); + const c2 = new Child2(); + c1.rel.add(new Child1Specific()); + c1.rel.add(new Child1Specific()); + c1.rel.add(new Child1Specific()); + c2.qaInfo.add(new Relation1()); + c2.qaInfo.add(new Relation1()); + c2.qaInfo.add(new Relation1()); + await orm.em.persistAndFlush([c1, c2]); + orm.em.clear(); + + const parents = await orm.em.find(Parent, {}, ['qaInfo.parent', 'rel'], { type: QueryOrder.ASC }); + expect(parents[0]).toBeInstanceOf(Child1); + expect(parents[0].type).toBe('Child1'); + expect(parents[0].qaInfo.length).toBe(0); + expect((parents[0] as Child1).rel.length).toBe(3); + expect((parents[0] as Child1).rel[0]).toBeInstanceOf(Child1Specific); + expect((parents[0] as Child1).rel[0].child1).toBeInstanceOf(Child1); + expect((parents[0] as Child1).rel[1]).toBeInstanceOf(Child1Specific); + expect((parents[0] as Child1).rel[1].child1).toBeInstanceOf(Child1); + expect((parents[0] as Child1).rel[2]).toBeInstanceOf(Child1Specific); + expect((parents[0] as Child1).rel[2].child1).toBeInstanceOf(Child1); + expect(parents[1]).toBeInstanceOf(Child2); + expect(parents[1].type).toBe('Child2'); + expect(parents[1].qaInfo.length).toBe(3); + expect(parents[1].qaInfo[0]).toBeInstanceOf(Relation1); + expect(parents[1].qaInfo[0].parent).toBeInstanceOf(Child2); + expect(parents[1].qaInfo[1]).toBeInstanceOf(Relation1); + expect(parents[1].qaInfo[1].parent).toBeInstanceOf(Child2); + expect(parents[1].qaInfo[2]).toBeInstanceOf(Relation1); + expect(parents[1].qaInfo[2].parent).toBeInstanceOf(Child2); + expect((parents[1] as Child1).rel).toBeUndefined(); + }); + +}); diff --git a/tests/joined-strategy.postgre.test.ts b/tests/joined-strategy.postgre.test.ts index af38f328b942..e3112f187d62 100644 --- a/tests/joined-strategy.postgre.test.ts +++ b/tests/joined-strategy.postgre.test.ts @@ -7,7 +7,7 @@ describe('Joined loading strategy', () => { let orm: MikroORM; - beforeAll(async () => orm = await initORMPostgreSql()); + beforeAll(async () => orm = await initORMPostgreSql(LoadStrategy.JOINED)); beforeEach(async () => wipeDatabasePostgreSql(orm.em)); afterAll(async () => orm.close(true)); @@ -65,7 +65,7 @@ describe('Joined loading strategy', () => { await orm.em.persistAndFlush(author); orm.em.clear(); - const b1 = await orm.em.findOneOrFail(Book2, stranger, { populate: { author: LoadStrategy.JOINED } }); + const b1 = await orm.em.findOneOrFail(Book2, stranger, { populate: { author: true } }); expect(b1.title).toEqual('The Stranger'); expect(b1.author.name).toEqual('Albert Camus'); }); @@ -80,7 +80,7 @@ describe('Joined loading strategy', () => { await orm.em.persistAndFlush([a1, a2, a3]); orm.em.clear(); - const books = await orm.em.find(Book2, {}, { populate: { author: LoadStrategy.JOINED } }); + const books = await orm.em.find(Book2, {}, { populate: { author: true } }); expect(books).toHaveLength(6); expect(books[0].title).toBe('The Stranger 1'); expect(books[0].author.name).toBe('Albert Camus 1'); @@ -122,7 +122,7 @@ describe('Joined loading strategy', () => { orm.em.clear(); mock.mock.calls.length = 0; - await orm.em.findOneOrFail(Author2, { id: author2.id }, { populate: { books: LoadStrategy.JOINED } }); + await orm.em.findOneOrFail(Author2, { id: author2.id }, { populate: { books: true } }); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' + '"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."price" as "b1_price", "b1".price * 1.19 as "b1_price_taxed", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' + @@ -132,7 +132,7 @@ describe('Joined loading strategy', () => { orm.em.clear(); mock.mock.calls.length = 0; - await orm.em.findOneOrFail(Author2, { id: author2.id }, { populate: { books: { perex: true } }, strategy: LoadStrategy.JOINED }); + await orm.em.findOneOrFail(Author2, { id: author2.id }, { populate: { books: { perex: true } } }); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' + '"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."perex" as "b1_perex", "b1"."price" as "b1_price", "b1".price * 1.19 as "b1_price_taxed", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' + @@ -183,7 +183,7 @@ describe('Joined loading strategy', () => { orm.em.clear(); mock.mock.calls.length = 0; - await orm.em.find(Author2, { id: author2.id }, { populate: { books: { perex: true } }, strategy: LoadStrategy.JOINED }); + await orm.em.find(Author2, { id: author2.id }, { populate: { books: { perex: true } } }); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' + '"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."perex" as "b1_perex", "b1"."price" as "b1_price", "b1".price * 1.19 as "b1_price_taxed", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' + @@ -285,7 +285,7 @@ describe('Joined loading strategy', () => { orm.em.clear(); const connMock = jest.spyOn(AbstractSqlConnection.prototype, 'execute'); - const b1 = (await orm.em.findOne(FooBar2, { id: bar.id }, { populate: { baz: LoadStrategy.JOINED } }))!; + const b1 = (await orm.em.findOne(FooBar2, { id: bar.id }, { populate: { baz: true } }))!; expect(connMock).toBeCalledTimes(1); expect(b1.baz).toBeInstanceOf(FooBaz2); expect(b1.baz!.id).toBe(baz.id); @@ -310,7 +310,7 @@ describe('Joined loading strategy', () => { expect(b0.bar).toBeUndefined(); orm.em.clear(); - const b1 = (await orm.em.findOne(FooBaz2, { id: baz.id }, { populate: { bar: LoadStrategy.JOINED } }))!; + const b1 = (await orm.em.findOne(FooBaz2, { id: baz.id }, { populate: ['bar'], strategy: LoadStrategy.JOINED }))!; expect(mock.mock.calls).toHaveLength(2); expect(mock.mock.calls[1][0]).toMatch('select "e0"."id", "e0"."name", "e0"."version", ' + '"b1"."id" as "b1_id", "b1"."name" as "b1_name", "b1"."baz_id" as "b1_baz_id", "b1"."foo_bar_id" as "b1_foo_bar_id", "b1"."version" as "b1_version", "b1"."blob" as "b1_blob", "b1"."array" as "b1_array", "b1"."object" as "b1_object", (select 123) as "b1_random", "b1"."id" as "bar_id" ' + @@ -323,7 +323,7 @@ describe('Joined loading strategy', () => { expect(wrap(b1).toJSON()).toMatchObject({ bar: { id: bar.id, baz: baz.id, name: 'bar' } }); orm.em.clear(); - const b2 = (await orm.em.findOne(FooBaz2, { bar: bar.id }, { populate: { bar: LoadStrategy.JOINED } }))!; + const b2 = (await orm.em.findOne(FooBaz2, { bar: bar.id }, { populate: { bar: true } }))!; expect(mock.mock.calls).toHaveLength(3); expect(mock.mock.calls[2][0]).toMatch('select "e0"."id", "e0"."name", "e0"."version", ' + '"b1"."id" as "b1_id", "b1"."name" as "b1_name", "b1"."baz_id" as "b1_baz_id", "b1"."foo_bar_id" as "b1_foo_bar_id", "b1"."version" as "b1_version", "b1"."blob" as "b1_blob", "b1"."array" as "b1_array", "b1"."object" as "b1_object", (select 123) as "b1_random", "b1"."id" as "bar_id" ' + @@ -367,11 +367,11 @@ describe('Joined loading strategy', () => { populate: { books: { author: true, - publisher: { tests: LoadStrategy.JOINED }, + publisher: { tests: true }, }, }, - strategy: LoadStrategy.JOINED, orderBy: { books: { publisher: { tests: { name: 'asc' } } } }, // TODO should be implicit as we have fixed order there + strategy: LoadStrategy.JOINED, }); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."name", ' + @@ -441,7 +441,7 @@ describe('Joined loading strategy', () => { const mock = jest.fn(); const logger = new Logger(mock, ['query']); Object.assign(orm.config, { logger }); - const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: { perex: true, author: LoadStrategy.JOINED } }); + const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, { populate: { perex: true, author: true } }); expect(res1).toHaveLength(3); expect(res1[0].test).toBeUndefined(); expect(mock.mock.calls.length).toBe(1); @@ -449,11 +449,11 @@ describe('Joined loading strategy', () => { '"a1"."id" as "a1_id", "a1"."created_at" as "a1_created_at", "a1"."updated_at" as "a1_updated_at", "a1"."name" as "a1_name", "a1"."email" as "a1_email", "a1"."age" as "a1_age", "a1"."terms_accepted" as "a1_terms_accepted", "a1"."optional" as "a1_optional", "a1"."identities" as "a1_identities", "a1"."born" as "a1_born", "a1"."born_time" as "a1_born_time", "a1"."favourite_book_uuid_pk" as "a1_favourite_book_uuid_pk", "a1"."favourite_author_id" as "a1_favourite_author_id", "e0".price * 1.19 as "price_taxed" ' + 'from "book2" as "e0" ' + 'left join "author2" as "a1" on "e0"."author_id" = "a1"."id" ' + - 'where "a1"."name" = $1'); + 'where "e0"."author_id" is not null and "a1"."name" = $1'); orm.em.clear(); mock.mock.calls.length = 0; - const res2 = await orm.em.find(Book2, { author: { favouriteBook: { author: { name: 'Jon Snow' } } } }, { strategy: LoadStrategy.JOINED, populate: { perex: true, author: { favouriteBook: { author: true } } } }); + const res2 = await orm.em.find(Book2, { author: { favouriteBook: { author: { name: 'Jon Snow' } } } }, { populate: { perex: true, author: { favouriteBook: { author: true } } } }); expect(res2).toHaveLength(3); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."uuid_pk", "e0"."created_at", "e0"."title", "e0"."perex", "e0"."price", "e0".price * 1.19 as "price_taxed", "e0"."double", "e0"."meta", "e0"."author_id", "e0"."publisher_id", ' + @@ -464,21 +464,21 @@ describe('Joined loading strategy', () => { 'left join "author2" as "a1" on "e0"."author_id" = "a1"."id" ' + 'left join "book2" as "f2" on "a1"."favourite_book_uuid_pk" = "f2"."uuid_pk" ' + 'left join "author2" as "a3" on "f2"."author_id" = "a3"."id" ' + - 'where "a3"."name" = $1'); + 'where "e0"."author_id" is not null and "a3"."name" = $1'); orm.em.clear(); mock.mock.calls.length = 0; - const res3 = await orm.em.find(Book2, { author: { favouriteBook: book3 } }, { populate: ['perex'], strategy: LoadStrategy.JOINED }); + const res3 = await orm.em.find(Book2, { author: { favouriteBook: book3 } }, { populate: ['perex'] }); expect(res3).toHaveLength(3); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0".*, "e0".price * 1.19 as "price_taxed" ' + 'from "book2" as "e0" ' + 'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' + - 'where "e1"."favourite_book_uuid_pk" = $1'); + 'where "e0"."author_id" is not null and "e1"."favourite_book_uuid_pk" = $1'); orm.em.clear(); mock.mock.calls.length = 0; - const res4 = await orm.em.find(Book2, { author: { favouriteBook: { $or: [{ author: { name: 'Jon Snow' } }] } } }, { populate: ['perex', 'author.favouriteBook.author'], strategy: LoadStrategy.JOINED }); + const res4 = await orm.em.find(Book2, { author: { favouriteBook: { $or: [{ author: { name: 'Jon Snow' } }] } } }, { populate: ['perex', 'author.favouriteBook.author'] }); expect(res4).toHaveLength(3); expect(mock.mock.calls.length).toBe(1); expect(mock.mock.calls[0][0]).toMatch('select "e0"."uuid_pk", "e0"."created_at", "e0"."title", "e0"."perex", "e0"."price", "e0".price * 1.19 as "price_taxed", "e0"."double", "e0"."meta", "e0"."author_id", "e0"."publisher_id", ' + @@ -487,7 +487,7 @@ describe('Joined loading strategy', () => { 'left join "author2" as "a1" on "e0"."author_id" = "a1"."id" ' + 'left join "book2" as "e2" on "a1"."favourite_book_uuid_pk" = "e2"."uuid_pk" ' + 'left join "author2" as "e3" on "e2"."author_id" = "e3"."id" ' + - 'where "e3"."name" = $1'); + 'where "e0"."author_id" is not null and "e3"."name" = $1'); }); }); diff --git a/yarn.lock b/yarn.lock index e2b44ad9e441..1bc82235d76f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3502,10 +3502,10 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== -escaya@^0.0.42: - version "0.0.42" - resolved "https://registry.yarnpkg.com/escaya/-/escaya-0.0.42.tgz#259d1991cb262cde12769a3b33a97047f7277d05" - integrity sha512-hiiqQo63tb/DM7FJ2DvgH/eWjeCxbcQefSjtZfv8EFTTNXwrBup5W1r0g/lboligQFjFSOTXfx5La7qJ0HXbew== +escaya@^0.0.45: + version "0.0.45" + resolved "https://registry.yarnpkg.com/escaya/-/escaya-0.0.45.tgz#1f57113d0870de9434c10c1b40c86b9f3ae4a1fa" + integrity sha512-IYm/JX/ezJMdlBGatWCWOkL1e/dsv80iiehVwxXJRyPIZsJd20T2C1Iw0aeiENjlQeuBPakItLOEHTykKsUBtg== escodegen@^1.14.1: version "1.14.3"