diff --git a/.env.dist b/.env.dist index 12b43d2..d912ba7 100644 --- a/.env.dist +++ b/.env.dist @@ -1,14 +1,15 @@ -MYSQL_ROOT_PASSWORD=root MYSQL_DATABASE=polymorphic MYSQL_USER=root -MYSQL_PASSWORD=root +MYSQL_PASSWORD= +MYSQL_ROOT_PASSWORD= +MYSQL_ALLOW_EMPTY_PASSWORD=true TYPEORM_CONNECTION=mysql TYPEORM_HOST=localhost TYPEORM_PORT=3306 TYPEORM_DATABASE=polymorphic TYPEORM_USERNAME=root -TYPEORM_PASSWORD=root +TYPEORM_PASSWORD= TYPEORM_ENTITIES=src/__tests__/**/*.entity.ts TYPEORM_SYNCHRONIZE=true TYPEORM_LOGGING=true diff --git a/.travis.yml b/.travis.yml index 6369b54..d88977b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ node_js: services: - mysql before_install: + - cp .env.dist .env - mysql -e 'CREATE DATABASE IF NOT EXISTS polymorphic;' - npm i -g npm@latest - npm i -g yarn diff --git a/README.md b/README.md index 9b5e8dd..f868bef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # typeorm-polymorphic - + An extension package for polymorphic relationship management, declaration and repository queries for [typeorm](https://typeorm.io/) diff --git a/package.json b/package.json index f244994..ebf1be6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "type": "git", "url": "https://github.com/bashleigh/typeorm-polymorphic" }, - "description": "A simple pagination function to build a pagination object with types", + "description": "A repository for building typed polymorphic relationships", "keywords": [ "nestjs", "typeorm", diff --git a/src/__tests__/polymorphic.repository.spec.ts b/src/__tests__/polymorphic.repository.spec.ts index 805c1aa..7342e45 100644 --- a/src/__tests__/polymorphic.repository.spec.ts +++ b/src/__tests__/polymorphic.repository.spec.ts @@ -4,7 +4,7 @@ import { UserEntity } from './entities/user.entity'; import { config } from 'dotenv'; import { resolve } from 'path'; import { AdvertRepository } from './repository/advert.repository'; -import { AbstractPolymorphicRepository } from '../../dist'; +import { AbstractPolymorphicRepository } from '../'; describe('AbstractPolymorphicRepository', () => { let connection: Connection; @@ -41,38 +41,118 @@ describe('AbstractPolymorphicRepository', () => { ]); }); - describe('child', () => { - it('Can create with parent', async () => { - const repository = connection.getCustomRepository(AdvertRepository); + describe('Childen', () => { + describe('create', () => { + it('Can create with parent', async () => { + const repository = connection.getCustomRepository(AdvertRepository); - const user = new UserEntity(); + const user = new UserEntity(); - const result = repository.create({ - owner: user, + const result = repository.create({ + owner: user, + }); + + expect(result).toBeInstanceOf(AdvertEntity); + expect(result.owner).toBeInstanceOf(UserEntity); + }); + }); + + describe('save', () => { + it('Can save cascade parent', async () => { + const repository = connection.getCustomRepository(AdvertRepository); + const userRepository = connection.getRepository(UserEntity); + + const user = await userRepository.save(new UserEntity()); + + const result = await repository.save( + repository.create({ + owner: user, + }), + ); + + expect(result).toBeInstanceOf(AdvertEntity); + expect(result.owner).toBeInstanceOf(UserEntity); + expect(result.id).toBeTruthy(); + expect(result.owner.id).toBeTruthy(); + expect(result.entityType).toBe(UserEntity.name); + expect(result.entityId).toBe(result.owner.id); }); - expect(result).toBeInstanceOf(AdvertEntity); - expect(result.owner).toBeInstanceOf(UserEntity); + it('Can save many with cascade parent', async () => { + const repository = connection.getCustomRepository(AdvertRepository); + const userRepository = connection.getRepository(UserEntity); + + const user = await userRepository.save(new UserEntity()); + + const result = await repository.save([ + repository.create({ + owner: user, + }), + repository.create({ + owner: user, + }), + ]); + + result.forEach((res) => { + expect(res).toBeInstanceOf(AdvertEntity); + expect(res.owner).toBeInstanceOf(UserEntity); + expect(res.id).toBeTruthy(); + expect(res.owner.id).toBeTruthy(); + expect(res.entityType).toBe(UserEntity.name); + expect(res.entityId).toBe(res.owner.id); + }); + }); }); - it('Can save cascade parent', async () => { - const repository = connection.getCustomRepository(AdvertRepository); - const userRepository = connection.getRepository(UserEntity); + describe('findOne', () => { + it('Can find entity with parent', async () => { + const repository = connection.getCustomRepository(AdvertRepository); + const userRepository = connection.getRepository(UserEntity); - const user = await userRepository.save(new UserEntity()); + const user = await userRepository.save(new UserEntity()); - const result = await repository.save( - repository.create({ - owner: user, - }), - ); - - expect(result).toBeInstanceOf(AdvertEntity); - expect(result.owner).toBeInstanceOf(UserEntity); - expect(result.id).toBeTruthy(); - expect(result.owner.id).toBeTruthy(); - expect(result.entityType).toBe(UserEntity.name); - expect(result.entityId).toBe(result.owner.id); + const advert = await repository.save( + repository.create({ + owner: user, + }), + ); + + const result = await repository.findOne(advert.id); + + expect(result).toBeInstanceOf(AdvertEntity); + expect(result.owner).toBeInstanceOf(UserEntity); + expect(result.owner.id).toBe(result.entityId); + expect(result.entityType).toBe(UserEntity.name); + }); + }); + + describe('find', () => { + it('Can find entities with parent', async () => { + const repository = connection.getCustomRepository(AdvertRepository); + const userRepository = connection.getRepository(UserEntity); + + const user = await userRepository.save(new UserEntity()); + + await repository.save([ + repository.create({ + owner: user, + }), + repository.create({ + owner: user, + }), + ]); + + const result = await repository.find(); + + result.forEach((res) => { + expect(res).toBeInstanceOf(AdvertEntity); + expect(res.owner).toBeInstanceOf(UserEntity); + expect(res.id).toBeTruthy(); + expect(res.owner.id).toBeTruthy(); + expect(res.entityType).toBe(UserEntity.name); + expect(res.entityId).toBe(res.owner.id); + }); + }); }); }); }); diff --git a/src/constants.ts b/src/constants.ts index a4ec66c..1505ba7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ export const POLYMORPHIC_OPTIONS = 'POLYMORPHIC_OPTIONS'; +export const POLYMORPHIC_KEY_SEPARATOR = '::'; diff --git a/src/decorators.ts b/src/decorators.ts index 1ef1d92..ec4cd6f 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -1,7 +1,6 @@ -import { POLYMORPHIC_OPTIONS } from './constants'; +import { POLYMORPHIC_KEY_SEPARATOR, POLYMORPHIC_OPTIONS } from './constants'; import { - PolymorphicChildrenDecoratorOptionsInterface, - PolymorphicParentDecoratorOptionsInterface, + PolymorphicDecoratorOptionsInterface, PolymorphicMetadataOptionsInterface, } from './polymorphic.interface'; @@ -10,7 +9,7 @@ const polymorphicPropertyDecorator = ( ): PropertyDecorator => (target: Object, propertyKey: string) => { Reflect.defineMetadata(POLYMORPHIC_OPTIONS, true, target); Reflect.defineMetadata( - `${POLYMORPHIC_OPTIONS}::${propertyKey}`, + `${POLYMORPHIC_OPTIONS}${POLYMORPHIC_KEY_SEPARATOR}${propertyKey}`, { propertyKey, ...options, @@ -21,7 +20,7 @@ const polymorphicPropertyDecorator = ( export const PolymorphicChildren = ( classType: () => Function[] | Function, - options: PolymorphicChildrenDecoratorOptionsInterface = {}, + options: PolymorphicDecoratorOptionsInterface = {}, ): PropertyDecorator => polymorphicPropertyDecorator({ type: 'children', @@ -35,7 +34,7 @@ export const PolymorphicChildren = ( export const PolymorphicParent = ( classType: () => Function[] | Function, - options: PolymorphicParentDecoratorOptionsInterface = {}, + options: PolymorphicDecoratorOptionsInterface = {}, ): PropertyDecorator => polymorphicPropertyDecorator({ type: 'parent', diff --git a/src/polymorphic.interface.ts b/src/polymorphic.interface.ts index df669cb..999fd85 100644 --- a/src/polymorphic.interface.ts +++ b/src/polymorphic.interface.ts @@ -24,17 +24,7 @@ export interface PolymorphicMetadataInterface extends PolymorphicInterface { propertyKey: string; } -export interface PolymorphicChildrenDecoratorOptionsInterface { - primaryColumn?: string; - hasMany?: boolean; - cascade?: boolean; - eager?: boolean; - deleteBeforeUpdate?: boolean; - entityTypeColumn?: string; - entityTypeId?: string; -} - -export interface PolymorphicParentDecoratorOptionsInterface { +export interface PolymorphicDecoratorOptionsInterface { deleteBeforeUpdate?: boolean; primaryColumn?: string; hasMany?: boolean; diff --git a/src/polymorphic.repository.ts b/src/polymorphic.repository.ts index 78c2801..9a40306 100644 --- a/src/polymorphic.repository.ts +++ b/src/polymorphic.repository.ts @@ -9,7 +9,7 @@ import { FindOneOptions, ObjectID, } from 'typeorm'; -import { POLYMORPHIC_OPTIONS } from './constants'; +import { POLYMORPHIC_KEY_SEPARATOR, POLYMORPHIC_OPTIONS } from './constants'; import { PolymorphicChildType, PolymorphicParentType, @@ -36,41 +36,37 @@ const PrimaryColumn = (options: PolymorphicMetadataInterface): string => export abstract class AbstractPolymorphicRepository extends Repository { private getPolymorphicMetadata(): Array { - let keys = Reflect.getMetadataKeys( + const keys = Reflect.getMetadataKeys( (this.metadata.target as Function)['prototype'], ); - if (!Array.isArray(keys)) { - return []; - } - - keys = keys.filter((key: string) => { - const parts = key.split('::'); - return parts[0] === POLYMORPHIC_OPTIONS; - }); - if (!keys) { return []; } - return keys - .map((key: string): PolymorphicMetadataInterface | undefined => { - const data: PolymorphicMetadataOptionsInterface & { - propertyKey: string; - } = Reflect.getMetadata( - key, - (this.metadata.target as Function)['prototype'], - ); - - if (typeof data === 'object') { - const classType = data.classType(); - return { - ...data, - classType, - }; + return keys.reduce>( + (keys: PolymorphicMetadataInterface[], key: string) => { + if (key.split(POLYMORPHIC_KEY_SEPARATOR)[0] === POLYMORPHIC_OPTIONS) { + const data: PolymorphicMetadataOptionsInterface & { + propertyKey: string; + } = Reflect.getMetadata( + key, + (this.metadata.target as Function)['prototype'], + ); + + if (data && typeof data === 'object') { + const classType = data.classType(); + keys.push({ + ...data, + classType, + }); + } } - }) - .filter((val) => typeof val !== 'undefined'); + + return keys; + }, + [], + ); } protected isPolymorph(): boolean { @@ -230,13 +226,9 @@ export abstract class AbstractPolymorphicRepository extends Repository { options?: SaveOptions & { reload: false }, ): Promise<(T & E) | Array | T | Array> { if (!this.isPolymorph()) { - return Array.isArray(entityOrEntities) && options - ? await super.save(entityOrEntities, options) - : Array.isArray(entityOrEntities) - ? await super.save(entityOrEntities) - : options - ? await super.save(entityOrEntities, options) - : await super.save(entityOrEntities); + return Array.isArray(entityOrEntities) + ? super.save(entityOrEntities, options) + : super.save(entityOrEntities, options); } const metadata = this.getPolymorphicMetadata(); @@ -252,6 +244,10 @@ export abstract class AbstractPolymorphicRepository extends Repository { if (!parent || entity[entityIdColumn(options)] !== undefined) { return entity; } + + /** + * Add parent's id and type to child's id and type field + */ entity[entityIdColumn(options)] = parent[PrimaryColumn(options)]; entity[entityTypeColumn(options)] = parent.constructor.name; return entity; @@ -259,32 +255,24 @@ export abstract class AbstractPolymorphicRepository extends Repository { } }); - const savedEntities = - Array.isArray(entityOrEntities) && options - ? await super.save(entityOrEntities, options) - : Array.isArray(entityOrEntities) - ? await super.save(entityOrEntities) - : options - ? await super.save(entityOrEntities, options) - : await super.save(entityOrEntities); - - return savedEntities; - - // return Promise.all( - // (Array.isArray(savedEntities) ? savedEntities : [savedEntities]).map( - // entity => - // new Promise(async resolve => { - // // @ts-ignore - // await this.deletePolymorphs(entity as E, metadata); - // // @ts-ignore - // resolve(await this.savePolymorphs(entity as E, metadata)); - // }), - // ), - // ); + /** + * Check deleteBeforeUpdate + */ + Array.isArray(entityOrEntities) + ? await Promise.all( + (entityOrEntities as Array).map((entity) => + this.deletePolymorphs(entity, metadata), + ), + ) + : await this.deletePolymorphs(entityOrEntities as T, metadata); + + return Array.isArray(entityOrEntities) + ? super.save(entityOrEntities, options) + : super.save(entityOrEntities, options); } private async deletePolymorphs( - entity: E, + entity: DeepPartial, options: PolymorphicMetadataInterface[], ): Promise { await Promise.all( @@ -292,13 +280,14 @@ export abstract class AbstractPolymorphicRepository extends Repository { (option: PolymorphicMetadataInterface) => new Promise((resolve) => { if (!option.deleteBeforeUpdate) { - return Promise.resolve(); + resolve(Promise.resolve()); } const entityTypes = Array.isArray(option.classType) ? option.classType : [option.classType]; + // resolve to singular query? resolve( Promise.all( entityTypes.map((type: () => Function | Function[]) => { diff --git a/src/repository.token.exception.ts b/src/repository.token.exception.ts index abe5503..ab54437 100644 --- a/src/repository.token.exception.ts +++ b/src/repository.token.exception.ts @@ -1,11 +1,3 @@ -export class RepositoryTokenNotFoundException extends Error { - constructor(classType: string) { - super( - `Repository token cannot be found for given classType [${classType}]`, - ); - } -} - export class RepositoryNotFoundException extends Error { constructor(token: Function | string) { super(`Repository cannot be found for given token [${token}]`);