Skip to content

Commit

Permalink
feat(discovery): validate not discovered entities used in relations
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Adamek committed Mar 17, 2020
1 parent 8ee8f37 commit 12338da
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 23 deletions.
7 changes: 7 additions & 0 deletions lib/metadata/MetadataValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export class MetadataValidator {
if (discovered.filter(meta => meta.name).length === 0 && warnWhenNoEntities) {
throw ValidationError.onlyAbstractEntitiesDiscovered();
}

// check for not discovered entities
discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
if (prop.reference !== ReferenceType.SCALAR && !discovered.find(m => m.className === prop.type)) {
throw ValidationError.fromUnknownEntity(prop.type);
}
}));
}

private validateReference(meta: EntityMetadata, prop: EntityProperty, metadata: MetadataStorage): void {
Expand Down
22 changes: 13 additions & 9 deletions lib/utils/ValidationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export class ValidationError<T extends AnyEntity = AnyEntity> extends Error {
}

static fromMissingPrimaryKey(meta: EntityMetadata): ValidationError {
return new ValidationError(`${meta.name} entity is missing @PrimaryKey()`);
return new ValidationError(`${meta.className} entity is missing @PrimaryKey()`);
}

static fromWrongReference(meta: EntityMetadata, prop: EntityProperty, key: keyof EntityProperty, owner?: EntityProperty): ValidationError {
if (owner) {
return ValidationError.fromMessage(meta, prop, `has wrong '${key}' reference type: ${owner.type} instead of ${meta.name}`);
return ValidationError.fromMessage(meta, prop, `has wrong '${key}' reference type: ${owner.type} instead of ${meta.className}`);
}

return ValidationError.fromMessage(meta, prop, `has unknown '${key}' reference: ${prop.type}.${prop[key]}`);
Expand All @@ -58,15 +58,19 @@ export class ValidationError<T extends AnyEntity = AnyEntity> extends Error {
const type = key === 'inversedBy' ? 'owning' : 'inverse';
const other = key === 'inversedBy' ? 'mappedBy' : 'inversedBy';

return new ValidationError(`Both ${meta.name}.${prop.name} and ${prop.type}.${prop[key]} are defined as ${type} sides, use '${other}' on one of them`);
return new ValidationError(`Both ${meta.className}.${prop.name} and ${prop.type}.${prop[key]} are defined as ${type} sides, use '${other}' on one of them`);
}

static fromMergeWithoutPK(meta: EntityMetadata): void {
throw new ValidationError(`You cannot merge entity '${meta.name}' without identifier!`);
throw new ValidationError(`You cannot merge entity '${meta.className}' without identifier!`);
}

static fromUnknownEntity(className: string): ValidationError {
return new ValidationError(`Entity '${className}' entity was not discovered, please make sure to provide it in 'entities' array when initializing the ORM`);
}

static fromUnknownBaseEntity(meta: EntityMetadata): ValidationError {
return new ValidationError(`Entity '${meta.name}' extends unknown base entity '${meta.extends}', please make sure to provide it in 'entities' array when initializing the ORM`);
return new ValidationError(`Entity '${meta.className}' extends unknown base entity '${meta.extends}', please make sure to provide it in 'entities' array when initializing the ORM`);
}

static transactionRequired(): ValidationError {
Expand All @@ -82,16 +86,16 @@ export class ValidationError<T extends AnyEntity = AnyEntity> extends Error {
}

static notVersioned(meta: EntityMetadata): ValidationError {
return new ValidationError(`Cannot obtain optimistic lock on unversioned entity ${meta.name}`);
return new ValidationError(`Cannot obtain optimistic lock on unversioned entity ${meta.className}`);
}

static multipleVersionFields(meta: EntityMetadata, fields: string[]): ValidationError {
return new ValidationError(`Entity ${meta.name} has multiple version properties defined: '${fields.join('\', \'')}'. Only one version property is allowed per entity.`);
return new ValidationError(`Entity ${meta.className} has multiple version properties defined: '${fields.join('\', \'')}'. Only one version property is allowed per entity.`);
}

static invalidVersionFieldType(meta: EntityMetadata): ValidationError {
const prop = meta.properties[meta.versionProperty];
return new ValidationError(`Version property ${meta.name}.${prop.name} has unsupported type '${prop.type}'. Only 'number' and 'Date' are allowed.`);
return new ValidationError(`Version property ${meta.className}.${prop.name} has unsupported type '${prop.type}'. Only 'number' and 'Date' are allowed.`);
}

static lockFailed(entityOrName: AnyEntity | string): ValidationError {
Expand Down Expand Up @@ -160,7 +164,7 @@ export class ValidationError<T extends AnyEntity = AnyEntity> extends Error {
}

private static fromMessage(meta: EntityMetadata, prop: EntityProperty, message: string): ValidationError {
return new ValidationError(`${meta.name}.${prop.name} ${message}`);
return new ValidationError(`${meta.className}.${prop.name} ${message}`);
}

}
14 changes: 7 additions & 7 deletions tests/MetadataValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('MetadataValidator', () => {
const validator = new MetadataValidator();

test('validates entity definition', async () => {
const meta = { Author: { name: 'Author', properties: {} } } as any;
const meta = { Author: { name: 'Author', className: 'Author', properties: {} } } as any;
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author entity is missing @PrimaryKey()');

// many to one
Expand All @@ -21,7 +21,7 @@ describe('MetadataValidator', () => {
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author.test has unknown type: Test');

// one to many
meta.Test = { name: 'Test', properties: {} };
meta.Test = { name: 'Test', className: 'Test', properties: {} };
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`);

Expand All @@ -34,7 +34,7 @@ describe('MetadataValidator', () => {
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author.books has unknown type: Book');

// many to many inversedBy
meta.Book = { name: 'Book', properties: {} };
meta.Book = { name: 'Book', className: 'Book', properties: {} };
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`);

Expand All @@ -47,7 +47,7 @@ describe('MetadataValidator', () => {
meta.Author.properties.books = { name: 'books', reference: ReferenceType.MANY_TO_MANY, type: 'Book', mappedBy: 'bar' };

// many to many mappedBy
meta.Book = { name: 'Book', properties: {} };
meta.Book = { name: 'Book', className: 'Book', properties: {} };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.books has unknown 'mappedBy' reference: Book.bar`);

meta.Author.properties.books.mappedBy = 'authors';
Expand All @@ -59,12 +59,12 @@ describe('MetadataValidator', () => {
meta.Book.properties.authors = { name: 'authors', reference: ReferenceType.MANY_TO_MANY, type: 'Author', inversedBy: 'books' };

// one to one
meta.Foo = { name: 'Foo', properties: {}, primaryKey: '_id' };
meta.Foo = { name: 'Foo', className: 'Foo', properties: {}, primaryKey: '_id' };
meta.Foo.properties.bar = { name: 'bar', reference: ReferenceType.ONE_TO_ONE, type: 'Bar' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError('Foo.bar has unknown type: Bar');

// one to one inversedBy
meta.Bar = { name: 'Bar', properties: {} };
meta.Bar = { name: 'Bar', className: 'Bar', properties: {} };
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`);

Expand All @@ -77,7 +77,7 @@ describe('MetadataValidator', () => {

// one to one mappedBy
meta.Foo.properties.bar = { name: 'bar', reference: ReferenceType.ONE_TO_ONE, type: 'Bar', mappedBy: 'bar' };
meta.Bar = { name: 'Bar', properties: {} };
meta.Bar = { name: 'Bar', className: 'Bar', properties: {} };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Foo.bar has unknown 'mappedBy' reference: Bar.bar`);

meta.Foo.properties.bar.mappedBy = 'foo';
Expand Down
13 changes: 6 additions & 7 deletions tests/MikroORM.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
(global as any).process.env.FORCE_COLOR = 0;

import { MikroORM, EntityManager, Configuration, ReflectMetadataProvider } from '../lib';
import { MetadataStorage } from '../lib/metadata';
import { Author, Test } from './entities';
import { BASE_DIR } from './bootstrap';
import { FooBaz2 } from './entities-sql';
import { Author2, FooBaz2 } from './entities-sql';
import { BaseEntity2 } from './entities-sql/BaseEntity2';

describe('MikroORM', () => {

beforeEach(() => {
const meta = MetadataStorage.getMetadata();
// Object.keys(meta).forEach(k => delete meta[k]);
});

test('should throw when not enough config provided', async () => {
expect(() => new MikroORM({ entitiesDirs: ['entities'], dbName: '' })).toThrowError('No database specified, please fill in `dbName` or `clientUrl` option');
expect(() => new MikroORM({ entities: [], entitiesDirs: [], dbName: 'test' })).toThrowError('No entities found, please use `entities` or `entitiesDirs` option');
Expand Down Expand Up @@ -68,6 +62,11 @@ describe('MikroORM', () => {
await expect(MikroORM.init({ dbName: 'test', baseDir: BASE_DIR, cache: { enabled: false }, entities: [BaseEntity2], entitiesDirsTs: ['entities-sql'] })).rejects.toThrowError(err);
});

test('should throw when a relation is pointing to not discovered entity', async () => {
const err = 'Entity \'Book2\' entity was not discovered, please make sure to provide it in \'entities\' array when initializing the ORM';
await expect(MikroORM.init({ dbName: 'test', cache: { enabled: false }, entities: [Author2, BaseEntity2], metadataProvider: ReflectMetadataProvider })).rejects.toThrowError(err);
});

test('should throw when only multiple property decorators are used', async () => {
const err = `Multiple property decorators used on 'MultiDecorator.name' property`;
await expect(MikroORM.init({ dbName: 'test', baseDir: BASE_DIR, cache: { enabled: false }, entitiesDirs: ['entities-4'] })).rejects.toThrowError(err);
Expand Down

0 comments on commit 12338da

Please sign in to comment.