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}]`);