Skip to content

Commit

Permalink
feat(core): support globs in entities (#618)
Browse files Browse the repository at this point in the history
`entitiesDirs` and `entitiesDirsTs` were removed in favour of `entities` and `entitiesTs`,
`entities` will be used as a default for `entitiesTs` (that is used when we detect `ts-node`).

`entities` can now contain mixture of paths to directories, globs pointing to entities,
or references to the entities or instances of `EntitySchema`. Negative globs are also
supported.

This basically means that all you need to change is renaming `entitiesDirs` to `entities`.

```typescript
MikroORM.init({
  entities: ['dist/**/entities', 'dist/**/*.entity.js', FooBar, FooBaz],
  entitiesTs: ['src/**/entities', 'src/**/*.entity.ts', FooBar, FooBaz],
});
```

BREAKING CHANGE:
`entitiesDirs` and `entitiesDirsTs` are removed in favour of `entities` and `entitiesTs`.

Closes #605
  • Loading branch information
B4nan committed Aug 9, 2020
1 parent f5c2302 commit ee81b61
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 253 deletions.
17 changes: 17 additions & 0 deletions docs/docs/upgrading-v3-to-v4.md
Expand Up @@ -37,6 +37,23 @@ Previously the name was constructed from 2 entity names as `entity_a_to_entity_b
ignoring the actual property name. In v4 the name will be `entity_a_coll_name` in
case of the collection property on the owning side being named `collName`.

## Changes in folder-based discovery (`entitiesDirs` removed)

`entitiesDirs` and `entitiesDirsTs` were removed in favour of `entities` and `entitiesTs`,
`entities` will be used as a default for `entitiesTs` (that is used when we detect `ts-node`).

`entities` can now contain mixture of paths to directories, globs pointing to entities,
or references to the entities or instances of `EntitySchema`.

This basically means that all you need to change is renaming `entitiesDirs` to `entities`.

```typescript
MikroORM.init({
entities: ['dist/**/entities', 'dist/**/*.entity.js', FooBar, FooBaz],
entitiesTs: ['src/**/entities', 'src/**/*.entity.ts', FooBar, FooBaz],
});
```

## Changes in `wrap()` helper, `WrappedEntity` interface and `Reference` wrapper

Previously all the methods and properties of `WrappedEntity` interface were
Expand Down
19 changes: 11 additions & 8 deletions packages/cli/src/commands/DebugCommand.ts
@@ -1,4 +1,4 @@
import { Arguments, CommandModule } from 'yargs';
import { CommandModule } from 'yargs';
import chalk from 'chalk';

import { CLIHelper } from '../CLIHelper';
Expand All @@ -12,7 +12,7 @@ export class DebugCommand implements CommandModule {
/**
* @inheritdoc
*/
async handler(args: Arguments) {
async handler() {
CLIHelper.dump(`Current ${chalk.cyan('MikroORM')} CLI configuration`);
await CLIHelper.dumpDependencies();
const settings = await ConfigurationLoader.getSettings();
Expand All @@ -28,13 +28,16 @@ export class DebugCommand implements CommandModule {
try {
const config = await CLIHelper.getConfiguration();
CLIHelper.dump(` - configuration ${chalk.green('found')}`);
const length = config.get('entities', []).length;
const entities = config.get('entities', []);

if (length > 0) {
CLIHelper.dump(` - will use \`entities\` array (contains ${length} items)`);
} else if (config.get('entitiesDirs', []).length > 0) {
CLIHelper.dump(' - will use `entitiesDirs` paths:');
await DebugCommand.checkPaths(config.get('entitiesDirs'), 'red', config.get('baseDir'), true);
if (entities.length > 0) {
const refs = entities.filter(p => !Utils.isString(p));
const paths = entities.filter(p => Utils.isString(p));
CLIHelper.dump(` - will use \`entities\` array (contains ${refs.length} references and ${paths.length} paths)`);

if (paths.length > 0) {
await DebugCommand.checkPaths(paths, 'red', config.get('baseDir'), true);
}
}
} catch (e) {
CLIHelper.dump(`- configuration ${chalk.red('not found')} ${chalk.red(`(${e.message})`)}`);
Expand Down
61 changes: 44 additions & 17 deletions packages/core/src/metadata/MetadataDiscovery.ts
Expand Up @@ -62,27 +62,30 @@ export class MetadataDiscovery {
private async findEntities(preferTsNode: boolean): Promise<EntityMetadata[]> {
this.discovered.length = 0;

if (this.config.get('discovery').requireEntitiesArray && this.config.get('entities').length === 0) {
throw new Error(`[requireEntitiesArray] Explicit list of entities is required, please use the 'entities' option.`);
}
const key = (preferTsNode && this.config.get('tsNode', Utils.detectTsNode()) && this.config.get('entitiesTs').length > 0) ? 'entitiesTs' : 'entities';
const paths = this.config.get(key).filter(item => Utils.isString(item)) as string[];
const refs = this.config.get(key).filter(item => !Utils.isString(item)) as Constructor<AnyEntity>[];

if (this.config.get('entities').length > 0) {
await Utils.runSerial(this.config.get('entities'), entity => this.discoverEntity(entity));
} else if (preferTsNode && this.config.get('tsNode', Utils.detectTsNode())) {
await Utils.runSerial(this.config.get('entitiesDirsTs'), dir => this.discoverDirectory(dir));
} else {
await Utils.runSerial(this.config.get('entitiesDirs'), dir => this.discoverDirectory(dir));
if (this.config.get('discovery').requireEntitiesArray && paths.length > 0) {
throw new Error(`[requireEntitiesArray] Explicit list of entities is required, please use the 'entities' option.`);
}

await this.discoverDirectories(paths);
await this.discoverReferences(refs);
this.validator.validateDiscovered(this.discovered, this.config.get('discovery').warnWhenNoEntities!);

return this.discovered;
}

private async discoverDirectory(basePath: string): Promise<void> {
const files = await globby(Utils.normalizePath(basePath, '*'), { cwd: Utils.normalizePath(this.config.get('baseDir')) });
this.logger.log('discovery', `- processing ${chalk.cyan(files.length)} files from directory ${chalk.cyan(basePath)}`);
const found: [ObjectConstructor, string][] = [];
private async discoverDirectories(paths: string[]): Promise<void> {
if (paths.length === 0) {
return;
}

paths = paths.map(path => Utils.normalizePath(path));
const files = await globby(paths, { cwd: Utils.normalizePath(this.config.get('baseDir')) });
this.logger.log('discovery', `- processing ${chalk.cyan(files.length)} files`);
const found: [Constructor<AnyEntity>, string][] = [];

for (const filepath of files) {
const filename = basename(filepath);
Expand All @@ -108,11 +111,32 @@ export class MetadataDiscovery {
}

this.metadata.set(name, Utils.copy(MetadataStorage.getMetadata(name, path)));
found.push([target, path]);

const entity = this.prepare(target) as Constructor<AnyEntity>;
const schema = this.getSchema(entity);
const meta = schema.init().meta;
this.metadata.set(meta.className, meta);

found.push([entity, path]);
}

for (const [entity, path] of found) {
await this.discoverEntity(entity, path);
}
}

private async discoverReferences(refs: Constructor<AnyEntity>[]): Promise<void> {
const found: Constructor<AnyEntity>[] = [];

for (const entity of refs) {
const schema = this.getSchema(entity);
const meta = schema.init().meta;
this.metadata.set(meta.className, meta);
found.push(entity);
}

for (const [target, path] of found) {
await this.discoverEntity(target, path);
for (const entity of found) {
await this.discoverEntity(entity);
}
}

Expand Down Expand Up @@ -145,7 +169,10 @@ export class MetadataDiscovery {
this.metadata.set(entity.name, meta);
}

const schema = EntitySchema.fromMetadata<T>(this.metadata.get<T>(entity.name, true));
const exists = this.metadata.has(entity.name);
const meta = this.metadata.get<T>(entity.name, true);
meta.abstract = meta.abstract ?? !(exists && meta.name);
const schema = EntitySchema.fromMetadata<T>(meta);
schema.setClass(entity);
schema.meta.useCache = this.metadataProvider.useCache();

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/metadata/MetadataValidator.ts
Expand Up @@ -86,7 +86,7 @@ export class MetadataValidator {
}

// has correct `inversedBy` reference type
if (inverse.type !== meta.name) {
if (inverse.type !== meta.className) {
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
}

Expand All @@ -103,7 +103,7 @@ export class MetadataValidator {
}

// has correct `mappedBy` reference type
if (owner.type !== meta.name) {
if (owner.type !== meta.className) {
throw MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner);
}

Expand Down
22 changes: 5 additions & 17 deletions packages/core/src/utils/Configuration.ts
Expand Up @@ -18,8 +18,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
static readonly DEFAULTS = {
pool: {},
entities: [],
entitiesDirs: [],
entitiesDirsTs: [],
entitiesTs: [],
subscribers: [],
discovery: {
warnWhenNoEntities: true,
Expand Down Expand Up @@ -206,10 +205,6 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
this.options.dbName = this.get('dbName', url[1]);
}

if (this.options.entitiesDirsTs.length === 0) {
this.options.entitiesDirsTs = this.options.entitiesDirs;
}

if (!this.options.charset) {
this.options.charset = this.platform.getDefaultCharset();
}
Expand All @@ -227,14 +222,8 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
throw new Error('No database specified, please fill in `dbName` or `clientUrl` option');
}

if (this.options.entities.length === 0 && this.options.entitiesDirs.length === 0 && this.options.discovery.warnWhenNoEntities) {
throw new Error('No entities found, please use `entities` or `entitiesDirs` option');
}

const notDirectory = this.options.entitiesDirs.find(dir => dir.match(/\.[jt]s$/));

if (notDirectory) {
throw new Error(`Please provide path to directory in \`entitiesDirs\`, found: '${notDirectory}'`);
if (this.options.entities.length === 0 && this.options.discovery.warnWhenNoEntities) {
throw new Error('No entities found, please use `entities` option');
}
}

Expand Down Expand Up @@ -310,9 +299,8 @@ export interface PoolConfig {
}

export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver> extends ConnectionOptions {
entities: (EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema<any>)[]; // `any` required here for some TS weirdness
entitiesDirs: string[];
entitiesDirsTs: string[];
entities: (string | EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema<any>)[]; // `any` required here for some TS weirdness
entitiesTs: (string | EntityClass<AnyEntity> | EntityClassGroup<AnyEntity> | EntitySchema<any>)[]; // `any` required here for some TS weirdness
subscribers: EventSubscriber[];
discovery: {
warnWhenNoEntities?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/Utils.ts
Expand Up @@ -522,7 +522,7 @@ export class Utils {
let path = parts.join('/').replace(/\\/g, '/').replace(/\/$/, '');
path = normalize(path).replace(/\\/g, '/');

return path.match(/^[/.]|[a-zA-Z]:/) ? path : './' + path;
return (path.match(/^[/.]|[a-zA-Z]:/) || path.startsWith('!')) ? path : './' + path;
}

static relativePath(path: string, relativeTo: string): string {
Expand Down
49 changes: 14 additions & 35 deletions packages/reflection/src/TsMorphMetadataProvider.ts
@@ -1,4 +1,3 @@
import globby from 'globby';
import { Project, PropertyDeclaration, SourceFile } from 'ts-morph';
import { EntityMetadata, EntityProperty, MetadataProvider, MetadataStorage, Utils } from '@mikro-orm/core';

Expand All @@ -19,14 +18,14 @@ export class TsMorphMetadataProvider extends MetadataProvider {
await this.initProperties(meta);
}

async getExistingSourceFile(meta: EntityMetadata, ext?: string, validate = true): Promise<SourceFile> {
async getExistingSourceFile(path: string, ext?: string, validate = true): Promise<SourceFile> {
if (!ext) {
return await this.getExistingSourceFile(meta, '.d.ts', false) || await this.getExistingSourceFile(meta, '.ts');
return await this.getExistingSourceFile(path, '.d.ts', false) || await this.getExistingSourceFile(path, '.ts');
}

const path = meta.path.match(/\/[^/]+$/)![0].replace(/\.js$/, ext);
const tsPath = path.match(/\/[^/]+$/)![0].replace(/\.js$/, ext);

return (await this.getSourceFile(path, validate))!;
return (await this.getSourceFile(tsPath, path, validate))!;
}

protected async initProperties(meta: EntityMetadata): Promise<void> {
Expand Down Expand Up @@ -67,7 +66,7 @@ export class TsMorphMetadataProvider extends MetadataProvider {
}

private async readTypeFromSource(meta: EntityMetadata, prop: EntityProperty): Promise<{ type: string; optional?: boolean }> {
const source = await this.getExistingSourceFile(meta);
const source = await this.getExistingSourceFile(meta.path);
const cls = source.getClass(meta.className);

/* istanbul ignore next */
Expand All @@ -89,15 +88,15 @@ export class TsMorphMetadataProvider extends MetadataProvider {
return { type, optional };
}

private async getSourceFile(file: string, validate: boolean): Promise<SourceFile | undefined> {
private async getSourceFile(tsPath: string, file: string, validate: boolean): Promise<SourceFile | undefined> {
if (!this.sources) {
await this.initSourceFiles();
}

const source = this.sources.find(s => s.getFilePath().endsWith(file));
const source = this.sources.find(s => s.getFilePath().endsWith(tsPath));

if (!source && validate) {
throw new Error(`Source file for entity ${file} not found, check your 'entitiesDirsTs' option. If you are using webpack, see https://bit.ly/35pPDNn`);
throw new Error(`Source file for entity '${file}' not found, check your 'entitiesTs' option. If you are using webpack, see https://bit.ly/35pPDNn`);
}

return source;
Expand All @@ -118,32 +117,12 @@ export class TsMorphMetadataProvider extends MetadataProvider {
}

private async initSourceFiles(): Promise<void> {
const tsDirs = this.config.get('entitiesDirsTs');

if (tsDirs.length > 0) {
const dirs = await this.validateDirectories(tsDirs);
this.sources = this.project.addSourceFilesAtPaths(dirs);
} else {
const dirs = Object.values(MetadataStorage.getMetadata()).map(m => m.path.replace(/\.js$/, '.d.ts'));
this.sources = this.project.addSourceFilesAtPaths(dirs);
}
}

private async validateDirectories(dirs: string[]): Promise<string[]> {
const ret: string[] = [];

for (const dir of dirs) {
const path = Utils.normalizePath(this.config.get('baseDir'), dir);
const files = await globby(`${path}/*`);

if (files.length === 0) {
throw new Error(`Path ${path} does not exist`);
}

ret.push(Utils.normalizePath(path, '**', '*.ts'));
}

return ret;
// All entity files are first required during the discovery, before we reach here, so it is safe to get the parts from the global
// metadata storage. We know the path thanks the the decorators being executed. In case we are running via ts-node, the extension
// will be already `.ts`, so no change needed. `.js` files will get renamed to `.d.ts` files as they will be used as a source for
// the ts-morph reflection.
const paths = Object.values(MetadataStorage.getMetadata()).map(m => m.path.replace(/\.js$/, '.d.ts'));
this.sources = this.project.addSourceFilesAtPaths(paths);
}

}

0 comments on commit ee81b61

Please sign in to comment.