Skip to content

Commit

Permalink
feat(core): introduce ORM extensions (#3773)
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Nov 18, 2022
1 parent ea92693 commit 0f36967
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 17 deletions.
20 changes: 19 additions & 1 deletion docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,25 @@ MikroORM.init({
Read more about this in [Metadata Providers](metadata-providers.md) sections.

### Adjusting the default type mapping
## Extensions

Since v5.6, the ORM extensions like `SchemaGenerator`, `Migrator` or `EntityGenerator` can be registered via the `extensions` config option. This will be the only supported way to have the shortcuts like `orm.migrator` available in v6, so we no longer need to dynamically require those dependencies or specify them as optional peer dependencies (both of those things cause issues with various bundling tools like Webpack, or those used in Remix or Next.js).

```ts
import { defineConfig } from '@mikro-orm/postgres';
import { Migrator } from '@mikro-orm/migrations';
import { EntityGenerator } from '@mikro-orm/entity-generator';
import { SeedManager } from '@mikro-orm/seeder';

export default defineConfig({
dbName: 'test',
extensions: [Migrator, EntityGenerator, SeedManager],
});
```

> The `SchemaGenerator` (as well as `MongoSchemaGenerator`) is registered automatically as it does not require any 3rd party dependencies to be installed.
## Adjusting default type mapping

Since v5.2 we can alter how the ORM picks the default mapped type representation based on the inferred type of a property. One example is a mapping of `foo: string` to `varchar(255)`. If we wanted to change this default to a `text` type in postgres, we can use the `discover.getMappedType` callback:

Expand Down
44 changes: 40 additions & 4 deletions packages/core/src/MikroORM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,20 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
orm.config.set('allowGlobalContext', true);
await orm.discoverEntities();
orm.config.set('allowGlobalContext', allowGlobalContext);
orm.driver.getPlatform().lookupExtensions(orm);

if (connect && orm.config.get('connect')) {
connect &&= orm.config.get('connect');

if (connect) {
await orm.connect();
}

if (orm.config.get('ensureIndexes')) {
await orm.getSchemaGenerator().ensureIndexes();
}
for (const extension of orm.config.get('extensions')) {
extension.register(orm);
}

if (connect && orm.config.get('ensureIndexes')) {
await orm.getSchemaGenerator().ensureIndexes();
}

return orm;
Expand Down Expand Up @@ -156,27 +163,56 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
* Gets the SchemaGenerator.
*/
getSchemaGenerator(): ReturnType<ReturnType<D['getPlatform']>['getSchemaGenerator']> {
const extension = this.config.getExtension<ReturnType<ReturnType<D['getPlatform']>['getSchemaGenerator']>>('@mikro-orm/schema-generator');

if (extension) {
return extension;
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
/* istanbul ignore next */
return this.driver.getPlatform().getSchemaGenerator(this.driver, this.em) as any;
}

/**
* Gets the EntityGenerator.
*/
getEntityGenerator<T extends IEntityGenerator = IEntityGenerator>(): T {
const extension = this.config.getExtension<T>('@mikro-orm/entity-generator');

if (extension) {
return extension;
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
return this.driver.getPlatform().getEntityGenerator(this.em) as T;
}

/**
* Gets the Migrator.
*/
getMigrator<T extends IMigrator = IMigrator>(): T {
const extension = this.config.getExtension<T>('@mikro-orm/migrator');

if (extension) {
return extension;
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
return this.driver.getPlatform().getMigrator(this.em) as T;
}

/**
* Gets the SeedManager
*/
getSeeder<T extends ISeedManager = ISeedManager>(): T {
const extension = this.config.getExtension<T>('@mikro-orm/seeder');

if (extension) {
return extension;
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { SeedManager } = require('@mikro-orm/seeder');
return this.config.getCachedService(SeedManager, this.em);
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/platforms/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../types';
import { Utils } from '../utils/Utils';
import { ReferenceType } from '../enums';
import type { MikroORM } from '../MikroORM';

export const JsonProperty = Symbol('JsonProperty');

Expand Down Expand Up @@ -327,6 +328,13 @@ export abstract class Platform {
return this.exceptionConverter;
}

/**
* Allows to register extensions of the driver automatically (e.g. `SchemaGenerator` extension in SQL drivers).
*/
lookupExtensions(orm: MikroORM): void {
// no extensions by default
}

getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): ISchemaGenerator {
throw new Error(`${driver.constructor.name} does not support SchemaGenerator`);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/utils/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ import { FlushMode, LoadStrategy, PopulateHint } from '../enums';
import { MemoryCacheAdapter } from '../cache/MemoryCacheAdapter';
import { EntityComparator } from './EntityComparator';
import type { Type } from '../types/Type';
import type { MikroORM } from '../MikroORM';

export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {

static readonly DEFAULTS: MikroORMOptions = {
pool: {},
entities: [],
entitiesTs: [],
extensions: [],
subscribers: [],
filters: {},
discovery: {
Expand Down Expand Up @@ -127,6 +129,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
dynamicImportProvider: /* istanbul ignore next */ (id: string) => import(id),
};

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
static readonly PLATFORMS = {
'mongo': { className: 'MongoDriver', module: () => require('@mikro-orm/mongodb') },
'mysql': { className: 'MySqlDriver', module: () => require('@mikro-orm/mysql') },
Expand All @@ -141,6 +144,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
private readonly driver: D;
private readonly platform: Platform;
private readonly cache = new Map<string, any>();
private readonly extensions = new Map<string, unknown>();

constructor(options: Options, validate = true) {
if (options.dynamicImportProvider) {
Expand Down Expand Up @@ -222,6 +226,14 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
return this.driver;
}

registerExtension(name: string, instance: unknown): void {
this.extensions.set(name, instance);
}

getExtension<T>(name: string): T | undefined {
return this.extensions.get(name) as T;
}

/**
* Gets instance of NamingStrategy. (cached)
*/
Expand Down Expand Up @@ -477,6 +489,7 @@ export interface PoolConfig {
export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver> extends ConnectionOptions {
entities: (string | EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema)[]; // `any` required here for some TS weirdness
entitiesTs: (string | EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema)[]; // `any` required here for some TS weirdness
extensions: { register: (orm: MikroORM) => void }[];
subscribers: EventSubscriber[];
filters: Dictionary<{ name?: string } & Omit<FilterDef, 'name'>>;
discovery: {
Expand Down
6 changes: 5 additions & 1 deletion packages/entity-generator/src/EntityGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ensureDir, writeFile } from 'fs-extra';
import type { EntityProperty , EntityMetadata } from '@mikro-orm/core';
import type { EntityProperty, EntityMetadata, MikroORM } from '@mikro-orm/core';
import { ReferenceType, Utils } from '@mikro-orm/core';
import type { EntityManager } from '@mikro-orm/knex';
import { DatabaseSchema } from '@mikro-orm/knex';
Expand All @@ -18,6 +18,10 @@ export class EntityGenerator {

constructor(private readonly em: EntityManager) { }

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/entity-generator', new EntityGenerator(orm.em as EntityManager));
}

async generate(options: { baseDir?: string; save?: boolean; schema?: string } = {}): Promise<string[]> {
const baseDir = Utils.normalizePath(options.baseDir ?? (this.config.get('baseDir') + '/generated-entities'));
const schema = await DatabaseSchema.create(this.connection, this.platform, this.config);
Expand Down
10 changes: 9 additions & 1 deletion packages/knex/src/AbstractSqlPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escape } from 'sqlstring';
import type { Constructor, EntityManager, EntityRepository, IDatabaseDriver } from '@mikro-orm/core';
import type { Constructor, EntityManager, EntityRepository, IDatabaseDriver, MikroORM } from '@mikro-orm/core';
import { JsonProperty, Platform, Utils } from '@mikro-orm/core';
import { SqlEntityRepository } from './SqlEntityRepository';
import type { SchemaHelper } from './schema';
Expand All @@ -25,17 +25,25 @@ export abstract class AbstractSqlPlatform extends Platform {
return this.schemaHelper;
}

/** @inheritDoc */
lookupExtensions(orm: MikroORM): void {
SchemaGenerator.register(orm);
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): SchemaGenerator {
/* istanbul ignore next */
return this.config.getCachedService(SchemaGenerator, em ?? driver as any); // cast as `any` to get around circular dependencies
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
getEntityGenerator(em: EntityManager) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { EntityGenerator } = require('@mikro-orm/entity-generator');
return this.config.getCachedService(EntityGenerator, em);
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
getMigrator(em: EntityManager) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Migrator } = require('@mikro-orm/migrations');
Expand Down
6 changes: 5 additions & 1 deletion packages/knex/src/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Knex } from 'knex';
import type { Dictionary, EntityMetadata } from '@mikro-orm/core';
import type { Dictionary, EntityMetadata, MikroORM } from '@mikro-orm/core';
import { AbstractSchemaGenerator, Utils } from '@mikro-orm/core';
import type { Check, ForeignKey, Index, SchemaDifference, TableDifference } from '../typings';
import { DatabaseSchema } from './DatabaseSchema';
Expand All @@ -16,6 +16,10 @@ export class SchemaGenerator extends AbstractSchemaGenerator<AbstractSqlDriver>
private readonly options = this.config.get('schemaGenerator');
protected lastEnsuredDatabase?: string;

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/schema-generator', new SchemaGenerator(orm.em));
}

/** @deprecated use `dropSchema` and `createSchema` commands respectively */
async generate(): Promise<string> {
const [dropSchema, createSchema] = await Promise.all([
Expand Down
6 changes: 5 additions & 1 deletion packages/migrations-mongodb/src/Migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InputMigrations, MigrateDownOptions, MigrateUpOptions, MigrationPa
import { Umzug } from 'umzug';
import { join } from 'path';
import { ensureDir } from 'fs-extra';
import type { Constructor, IMigrationGenerator, IMigrator, Transaction } from '@mikro-orm/core';
import type { Constructor, IMigrationGenerator, IMigrator, MikroORM, Transaction } from '@mikro-orm/core';
import { Utils } from '@mikro-orm/core';
import type { EntityManager } from '@mikro-orm/mongodb';
import type { Migration } from './Migration';
Expand Down Expand Up @@ -30,6 +30,10 @@ export class Migrator implements IMigrator {
this.createUmzug();
}

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/migrator', new Migrator(orm.em as EntityManager));
}

/**
* @inheritDoc
*/
Expand Down
6 changes: 5 additions & 1 deletion packages/migrations/src/Migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InputMigrations, MigrateDownOptions, MigrateUpOptions, MigrationPa
import { Umzug } from 'umzug';
import { basename, join } from 'path';
import { ensureDir, pathExists, writeJSON } from 'fs-extra';
import type { Constructor, Dictionary, IMigrationGenerator, IMigrator, Transaction } from '@mikro-orm/core';
import type { Constructor, Dictionary, IMigrationGenerator, IMigrator, MikroORM, Transaction } from '@mikro-orm/core';
import { t, Type, UnknownType, Utils } from '@mikro-orm/core';
import type { EntityManager } from '@mikro-orm/knex';
import { DatabaseSchema, DatabaseTable, SchemaGenerator } from '@mikro-orm/knex';
Expand Down Expand Up @@ -40,6 +40,10 @@ export class Migrator implements IMigrator {
this.createUmzug();
}

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/migrator', new Migrator(orm.em as EntityManager));
}

/**
* @inheritDoc
*/
Expand Down
9 changes: 8 additions & 1 deletion packages/mongodb/src/MongoPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ObjectId } from 'bson';
import type {
IPrimaryKey, Primary, NamingStrategy, Constructor, EntityRepository, EntityProperty,
PopulateOptions, EntityMetadata, IDatabaseDriver, EntityManager, Configuration,
PopulateOptions, EntityMetadata, IDatabaseDriver, EntityManager, Configuration, MikroORM,
} from '@mikro-orm/core';
import { Platform, MongoNamingStrategy, Utils, ReferenceType, MetadataError } from '@mikro-orm/core';
import { MongoExceptionConverter } from './MongoExceptionConverter';
Expand All @@ -25,10 +25,17 @@ export class MongoPlatform extends Platform {
return MongoEntityRepository as Constructor<EntityRepository<T>>;
}

/** @inheritDoc */
lookupExtensions(orm: MikroORM): void {
MongoSchemaGenerator.register(orm);
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): MongoSchemaGenerator {
return new MongoSchemaGenerator(em ?? driver as any);
}

// TODO remove in v6 (https://github.com/mikro-orm/mikro-orm/issues/3743)
getMigrator(em: EntityManager) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Migrator } = require('@mikro-orm/migrations-mongodb');
Expand Down
7 changes: 6 additions & 1 deletion packages/mongodb/src/MongoSchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { Dictionary, EntityMetadata, EntityProperty } from '@mikro-orm/core';
import type { Dictionary, EntityMetadata, EntityProperty, MikroORM } from '@mikro-orm/core';
import { AbstractSchemaGenerator, Utils } from '@mikro-orm/core';
import type { MongoDriver } from './MongoDriver';

export class MongoSchemaGenerator extends AbstractSchemaGenerator<MongoDriver> {

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/schema-generator', new MongoSchemaGenerator(orm.em));
}

async createSchema(options: CreateSchemaOptions = {}): Promise<void> {
options.ensureIndexes ??= true;
const existing = await this.connection.listCollections();
const metadata = this.getOrderedMetadata();

/* istanbul ignore next */
const promises = metadata
.filter(meta => !existing.includes(meta.collection))
.map(meta => this.connection.createCollection(meta.collection).catch(err => {
Expand Down
6 changes: 5 additions & 1 deletion packages/seeder/src/SeedManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import globby from 'globby';
import type { Constructor, EntityManager, ISeedManager } from '@mikro-orm/core';
import type { Constructor, EntityManager, ISeedManager , MikroORM } from '@mikro-orm/core';
import { Utils } from '@mikro-orm/core';
import type { Seeder } from './Seeder';
import { ensureDir, writeFile } from 'fs-extra';
Expand All @@ -18,6 +18,10 @@ export class SeedManager implements ISeedManager {
this.absolutePath = Utils.absolutePath(this.options[key]!, this.config.get('baseDir'));
}

static register(orm: MikroORM): void {
orm.config.registerExtension('@mikro-orm/seeder', new SeedManager(orm.em));
}

async seed(...classNames: Constructor<Seeder>[]): Promise<void> {
for (const SeederClass of classNames) {
const seeder = new SeederClass();
Expand Down
8 changes: 4 additions & 4 deletions tests/EntityManager.postgre.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { EventSubscriber, ChangeSet, AnyEntity, FlushEventArgs, FilterQuery
import {
Collection, Configuration, EntityManager, LockMode, MikroORM, QueryFlag, QueryOrder, Reference, ValidationError, ChangeSetType, wrap, expr,
UniqueConstraintViolationException, TableNotFoundException, NotNullConstraintViolationException, TableExistsException, SyntaxErrorException,
NonUniqueFieldNameException, InvalidFieldNameException, LoadStrategy, IsolationLevel, PopulateHint,
NonUniqueFieldNameException, InvalidFieldNameException, LoadStrategy, IsolationLevel, PopulateHint, ref,
} from '@mikro-orm/core';
import { PostgreSqlDriver, PostgreSqlConnection } from '@mikro-orm/postgresql';
import { Address2, Author2, Book2, BookTag2, FooBar2, FooBaz2, Publisher2, PublisherType, PublisherType2, Test2, Label2 } from './entities-sql';
Expand All @@ -21,9 +21,9 @@ describe('EntityManagerPostgre', () => {
const book2 = new Book2('My Life on The Wall, part 2', author);
const book3 = new Book2('My Life on The Wall, part 3', author);
const publisher = new Publisher2();
book1.publisher = wrap(publisher).toReference();
book2.publisher = wrap(publisher).toReference();
book3.publisher = wrap(publisher).toReference();
book1.publisher = ref(publisher);
book2.publisher = ref(publisher);
book3.publisher = ref(publisher);
const tag1 = new BookTag2('silly');
const tag2 = new BookTag2('funny');
const tag3 = new BookTag2('sick');
Expand Down
Loading

0 comments on commit 0f36967

Please sign in to comment.