Skip to content

Commit

Permalink
feat(validation): validate one to one relationship metadata
Browse files Browse the repository at this point in the history
Closes #149
  • Loading branch information
B4nan committed Sep 24, 2019
1 parent 1012420 commit ce57a3c
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ checks:
threshold: 5
method-complexity:
config:
threshold: 6
threshold: 7

engines:
duplication:
Expand Down
5 changes: 4 additions & 1 deletion lib/decorators/OneToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { MetadataStorage } from '../metadata';
import { Utils } from '../utils';
import { Cascade, ReferenceType } from '../entity';
import { QueryOrder } from '../query';
import { OneToOneOptions } from './OneToOne';

export function OneToMany<T extends IEntityType<T>>(
entity: OneToManyOptions<T> | string | ((e?: any) => EntityName<T>),
Expand Down Expand Up @@ -46,6 +45,10 @@ export function createOneToDecorator<T extends IEntityType<T>>(
Utils.defaultValue(prop, 'nullable', !prop.cascade.includes(Cascade.REMOVE) && !prop.cascade.includes(Cascade.ALL));
prop.owner = prop.owner || !!prop.inversedBy || !prop.mappedBy;
prop.unique = prop.owner;

if (prop.owner && options.mappedBy) {
Utils.renameKey(prop, 'mappedBy', 'inversedBy');
}
}

meta.properties[propertyName] = prop;
Expand Down
38 changes: 19 additions & 19 deletions lib/metadata/MetadataValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ export class MetadataValidator {
for (const prop of references) {
this.validateReference(meta, prop, metadata);

if (![ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference)) {
this.validateCollection(meta, prop, metadata);
if (prop.reference === ReferenceType.ONE_TO_ONE) {
this.validateBidirectional(meta, prop, metadata);
} else if (prop.reference === ReferenceType.ONE_TO_MANY) {
const owner = metadata.get(prop.type).properties[prop.mappedBy];
this.validateOneToManyInverseSide(meta, prop, owner);
} else if (![ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference)) {
this.validateBidirectional(meta, prop, metadata);
}
}
}
Expand Down Expand Up @@ -54,23 +59,18 @@ export class MetadataValidator {
}
}

private validateCollection(meta: EntityMetadata, prop: EntityProperty, metadata: MetadataStorage): void {
if (prop.reference === ReferenceType.ONE_TO_MANY) {
const owner = metadata.get(prop.type).properties[prop.mappedBy];
return this.validateOneToManyInverseSide(meta, prop, owner);
}

// m:n collection either is owner or has `mappedBy`
private validateBidirectional(meta: EntityMetadata, prop: EntityProperty, metadata: MetadataStorage): void {
// 1:1 reference either is owner or has `mappedBy`
if (!prop.owner && !prop.mappedBy && !prop.inversedBy) {
throw ValidationError.fromMissingOwnership(meta, prop);
}

if (prop.inversedBy) {
const inverse = metadata.get(prop.type).properties[prop.inversedBy];
this.validateManyToManyOwningSide(meta, prop, inverse);
this.validateOwningSide(meta, prop, inverse);
} else if (prop.mappedBy) {
const inverse = metadata.get(prop.type).properties[prop.mappedBy];
this.validateManyToManyInverseSide(meta, prop, inverse);
this.validateInverseSide(meta, prop, inverse);
}
}

Expand All @@ -86,35 +86,35 @@ export class MetadataValidator {
}
}

private validateManyToManyOwningSide(meta: EntityMetadata, prop: EntityProperty, inverse: EntityProperty): void {
// m:n collection has correct `inversedBy` on owning side
private validateOwningSide(meta: EntityMetadata, prop: EntityProperty, inverse: EntityProperty): void {
// has correct `inversedBy` on owning side
if (!inverse) {
throw ValidationError.fromWrongReference(meta, prop, 'inversedBy');
}

// m:n collection has correct `inversedBy` reference type
// has correct `inversedBy` reference type
if (inverse.type !== meta.name) {
throw ValidationError.fromWrongReference(meta, prop, 'inversedBy', inverse);
}

// m:n collection inversed side is not defined as owner
// inversed side is not defined as owner
if (inverse.inversedBy) {
throw ValidationError.fromWrongOwnership(meta, prop, 'inversedBy');
}
}

private validateManyToManyInverseSide(meta: EntityMetadata, prop: EntityProperty, owner: EntityProperty): void {
// m:n collection has correct `mappedBy` on inverse side
private validateInverseSide(meta: EntityMetadata, prop: EntityProperty, owner: EntityProperty): void {
// has correct `mappedBy` on inverse side
if (prop.mappedBy && !owner) {
throw ValidationError.fromWrongReference(meta, prop, 'mappedBy');
}

// m:n collection has correct `mappedBy` reference type
// has correct `mappedBy` reference type
if (owner.type !== meta.name) {
throw ValidationError.fromWrongReference(meta, prop, 'mappedBy', owner);
}

// m:n collection owning side is not defined as inverse
// owning side is not defined as inverse
if (owner.mappedBy) {
throw ValidationError.fromWrongOwnership(meta, prop, 'mappedBy');
}
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/ValidationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class ValidationError 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.name}.${prop.name} and ${prop.type}.${prop[key]} are defined as ${type} sides, use '${other}' on one of them`);
}

static fromMissingOwnership(meta: EntityMetadata, prop: EntityProperty): ValidationError {
Expand Down
42 changes: 38 additions & 4 deletions tests/MetadataValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ describe('MetadataValidator', () => {
const meta = { Author: { name: 'Author', properties: {} } } as any;
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author entity is missing @PrimaryKey()');

// many to one
meta.Author.primaryKey = '_id';
meta.Author.properties.test = { name: 'test', reference: ReferenceType.MANY_TO_ONE };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author.test is missing type definition');

meta.Author.properties.test.type = 'Test';
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.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`);

meta.Test.properties.foo = { name: 'foo', reference: ReferenceType.MANY_TO_ONE, type: 'Wrong', mappedBy: 'foo' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.tests has wrong 'mappedBy' reference type: Wrong instead of Author`);

// many to many
meta.Test.properties.foo.type = 'Author';
meta.Author.properties.books = { name: 'books', reference: ReferenceType.MANY_TO_MANY, type: 'Book' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError('Author.books has unknown type: Book');
Expand All @@ -42,10 +45,10 @@ describe('MetadataValidator', () => {
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.books has wrong 'inversedBy' reference type: Foo instead of Author`);

meta.Book.properties.authors = { name: 'authors', reference: ReferenceType.MANY_TO_MANY, type: 'Author', inversedBy: 'books' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Both Author.books and Book.authors are defined as owning sides, use mappedBy on one of them`);
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Both Author.books and Book.authors are defined as owning sides, use 'mappedBy' on one of them`);
meta.Author.properties.books = { name: 'books', reference: ReferenceType.MANY_TO_MANY, type: 'Book', mappedBy: 'bar' };

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

Expand All @@ -54,10 +57,41 @@ describe('MetadataValidator', () => {
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Author.books has wrong 'mappedBy' reference type: Foo instead of Author`);

meta.Book.properties.authors = { name: 'authors', reference: ReferenceType.MANY_TO_MANY, type: 'Author', mappedBy: 'books' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Both Author.books and Book.authors are defined as inverse sides, use inversedBy on one of them`);
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Both Author.books and Book.authors are defined as inverse sides, use 'inversedBy' on one of them`);
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.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: {} };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Foo.bar needs to have one of 'owner', 'mappedBy' or 'inversedBy' attributes`);

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.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`);

meta.Bar.properties.foo = { name: 'foo', reference: ReferenceType.ONE_TO_ONE, type: 'Foo', inversedBy: 'bar' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Both Foo.bar and Bar.foo are defined as owning sides, use 'mappedBy' on one of them`);

// one to one mappedBy
meta.Foo.properties.bar = { name: 'bar', reference: ReferenceType.ONE_TO_ONE, type: 'Bar', mappedBy: 'bar' };
meta.Bar = { name: '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';
meta.Bar.properties.foo = { name: 'foo', reference: ReferenceType.ONE_TO_ONE, type: 'FooBar', mappedBy: 'bar' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Foo.bar has wrong 'mappedBy' reference type: FooBar instead of Foo`);

meta.Bar.properties.foo = { name: 'foo', reference: ReferenceType.ONE_TO_ONE, type: 'Foo', mappedBy: 'bar' };
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Foo')).toThrowError(`Both Foo.bar and Bar.foo are defined as inverse sides, use 'inversedBy' on one of them`);

// version field
meta.Book.properties.authors = { name: 'authors', reference: ReferenceType.MANY_TO_MANY, type: 'Author', inversedBy: 'books' };
meta.Author.properties.version = { name: 'version', reference: ReferenceType.SCALAR, type: 'Test', version: true };
meta.Author.versionProperty = 'version';
expect(() => validator.validateEntityDefinition(new MetadataStorage(meta as any), 'Author')).toThrowError(`Version property Author.version has unsupported type 'Test'. Only 'number' and 'Date' are allowed.`);
Expand Down

0 comments on commit ce57a3c

Please sign in to comment.