Skip to content

Commit

Permalink
Merge pull request #399 from jiho-kr/feature/without-name
Browse files Browse the repository at this point in the history
feat: can be used without define entity name
  • Loading branch information
jiho-kr authored Dec 20, 2023
2 parents c21b4fc + ec5b378 commit e5d5ec1
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 3 deletions.
171 changes: 171 additions & 0 deletions spec/entity-inheritance/child-entity-noname.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/* eslint-disable max-classes-per-file */
import { Controller, HttpStatus, INestApplication, Injectable, Module } from '@nestjs/common';
import { TestingModule, Test } from '@nestjs/testing';
import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm';
import { Type } from 'class-transformer';
import { IsNumber, IsOptional, IsString } from 'class-validator';
import request from 'supertest';
import { Entity, TableInheritance, PrimaryGeneratedColumn, Column, ChildEntity, Repository } from 'typeorm';

import { Crud } from '../../src/lib/crud.decorator';
import { CrudService } from '../../src/lib/crud.service';
import { CrudController, GROUP, Method } from '../../src/lib/interface';
import { TestHelper } from '../test.helper';

@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
class ContentEntity {
@PrimaryGeneratedColumn()
@IsNumber({}, { groups: [GROUP.PARAMS] })
@Type(() => Number)
id: number;

@Column()
@IsString({ always: true })
@IsOptional({ always: true })
title: string;

@Column()
@IsString({ always: true })
@IsOptional({ always: true })
description: string;
}

@ChildEntity()
class PhotoEntity extends ContentEntity {
@Column()
@IsString({ always: true })
@IsOptional({ always: true })
size: string;
}

@Injectable()
class PhotoService extends CrudService<PhotoEntity> {
constructor(@InjectRepository(PhotoEntity) repository: Repository<PhotoEntity>) {
super(repository);
}
}

@Crud({
entity: PhotoEntity,
routes: { [Method.DELETE]: { softDelete: false } },
})
@Controller('photo')
class PhotoController implements CrudController<PhotoEntity> {
constructor(public readonly crudService: PhotoService) {}
}

@ChildEntity()
class QuestionEntity extends ContentEntity {
@Column()
@Type(() => Number)
@IsNumber({}, { always: true })
@IsOptional({ always: true })
answersCount: number;
}
@Injectable()
class QuestionService extends CrudService<QuestionEntity> {
constructor(@InjectRepository(QuestionEntity) repository: Repository<QuestionEntity>) {
super(repository);
}
}

@Crud({
entity: QuestionEntity,
routes: { [Method.DELETE]: { softDelete: false } },
})
@Controller('question')
class QuestionController implements CrudController<QuestionEntity> {
constructor(public readonly crudService: QuestionService) {}
}

@Module({
imports: [TypeOrmModule.forFeature([ContentEntity, PhotoEntity, QuestionEntity])],
controllers: [PhotoController, QuestionController],
providers: [PhotoService, QuestionService],
})
class ContentModule {}

describe('Child Entity (no-named)', () => {
let app: INestApplication;
let photoService: PhotoService;
let questionService: QuestionService;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ContentModule, TestHelper.getTypeOrmPgsqlModule([ContentEntity, PhotoEntity, QuestionEntity])],
}).compile();
app = moduleFixture.createNestApplication();

photoService = moduleFixture.get<PhotoService>(PhotoService);
questionService = moduleFixture.get<QuestionService>(QuestionService);
await app.init();
});

afterAll(async () => {
await app?.close();
});

it('should be defined', () => {
expect(app).toBeDefined();
});

it('should be used PhotoEntity', async () => {
await photoService.repository.delete({});
const title = `Tester-${Date.now()}`;
const { body: created } = await request(app.getHttpServer())
.post('/photo')
.send({ title, description: 'Photo Test', size: '100px' })
.expect(HttpStatus.CREATED);
expect(created.title).toEqual(title);
expect(created.id).toBeDefined();
expect(created.size).toEqual('100px');

const { body: readOne } = await request(app.getHttpServer()).get(`/photo/${created.id}`).expect(HttpStatus.OK);
expect(readOne.title).toEqual(title);

const { body: readMany } = await request(app.getHttpServer()).get('/photo').expect(HttpStatus.OK);
expect(readMany.data).toHaveLength(1);
expect(readMany.data[0].title).toEqual(title);
expect(readMany.metadata.total).toEqual(1);

const { body: updated } = await request(app.getHttpServer())
.patch(`/photo/${created.id}`)
.send({ title: 'updated' })
.expect(HttpStatus.OK);
expect(updated.title).toEqual('updated');

await request(app.getHttpServer()).delete(`/photo/${created.id}`).expect(HttpStatus.OK);
await request(app.getHttpServer()).get(`/photo/${created.id}`).expect(HttpStatus.NOT_FOUND);
});

it('should be used QuestionEntity', async () => {
await questionService.repository.delete({});
const title = `Tester-${Date.now()}`;
const { body: created } = await request(app.getHttpServer())
.post('/question')
.send({ title, description: 'Question Test', answersCount: 10 })
.expect(HttpStatus.CREATED);
expect(created.title).toEqual(title);
expect(created.id).toBeDefined();
expect(created.size).not.toBeDefined();
expect(created.answersCount).toEqual(10);

const { body: readOne } = await request(app.getHttpServer()).get(`/question/${created.id}`).expect(HttpStatus.OK);
expect(readOne.title).toEqual(title);

const { body: readMany } = await request(app.getHttpServer()).get('/question').expect(HttpStatus.OK);
expect(readMany.data).toHaveLength(1);
expect(readMany.data[0].title).toEqual(title);
expect(readMany.metadata.total).toEqual(1);

const { body: updated } = await request(app.getHttpServer())
.patch(`/question/${created.id}`)
.send({ title: 'updated' })
.expect(HttpStatus.OK);
expect(updated.title).toEqual('updated');

await request(app.getHttpServer()).delete(`/question/${created.id}`).expect(HttpStatus.OK);
await request(app.getHttpServer()).get(`/question/${created.id}`).expect(HttpStatus.NOT_FOUND);
});
});
119 changes: 119 additions & 0 deletions spec/general-entity/without-name.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* eslint-disable max-classes-per-file */
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Controller, Injectable, Module } from '@nestjs/common';
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { Test, TestingModule } from '@nestjs/testing';
import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm';
import { Type } from 'class-transformer';
import { IsNumber, IsOptional, IsString } from 'class-validator';
import request from 'supertest';
import { Entity, Repository, Column, DeleteDateColumn, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

import { Crud } from '../../src/lib/crud.decorator';
import { CrudService } from '../../src/lib/crud.service';
import { CrudController, GROUP } from '../../src/lib/interface';
import { TestHelper } from '../test.helper';

@Entity()
class NoNamedEntity {
@PrimaryGeneratedColumn({ type: 'bigint' })
@ApiProperty({ description: 'ID' })
@IsNumber({}, { groups: [GROUP.PARAMS] })
@Type(() => Number)
id: number;

@Column()
@ApiProperty({ description: 'Name' })
@IsString({ always: true })
@IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] })
name: string;

@Column({ nullable: true })
@ApiProperty({ description: 'Description' })
@IsString({ always: true })
@IsOptional({ always: true })
description: string;

@CreateDateColumn()
@ApiProperty({ description: 'Created At' })
createdAt: Date;

@UpdateDateColumn()
@ApiProperty({ description: 'Last Modified At' })
updatedAt: Date;

@DeleteDateColumn()
@ApiHideProperty()
deletedAt?: Date;
}

@Injectable()
class TestService extends CrudService<NoNamedEntity> {
constructor(@InjectRepository(NoNamedEntity) repository: Repository<NoNamedEntity>) {
super(repository);
}
}

@Crud({
entity: NoNamedEntity,
})
@Controller('no-named')
class TestController implements CrudController<NoNamedEntity> {
constructor(public readonly crudService: TestService) {}
}

@Module({
imports: [TypeOrmModule.forFeature([NoNamedEntity])],
controllers: [TestController],
providers: [TestService],
})
class TestModule {}

describe('Should be used even if it does not defined name', () => {
let app: INestApplication;
let service: TestService;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([NoNamedEntity])],
}).compile();
app = moduleFixture.createNestApplication();

service = app.get<TestService>(TestService);
await app.init();
});

afterAll(async () => {
await app?.close();
});

it('should be defined', () => {
expect(app).toBeDefined();
});

it('should be used NoNamedEntity', async () => {
await service.repository.delete({});

const name = `Tester-${Date.now()}`;
const { body: created } = await request(app.getHttpServer()).post('/no-named').send({ name }).expect(HttpStatus.CREATED);
expect(created.name).toEqual(name);
expect(created.id).toBeDefined();

const { body: readOne } = await request(app.getHttpServer()).get(`/no-named/${created.id}`).expect(HttpStatus.OK);
expect(readOne.name).toEqual(name);

const { body: readMany } = await request(app.getHttpServer()).get('/no-named').expect(HttpStatus.OK);
expect(readMany.data).toHaveLength(1);
expect(readMany.data[0].name).toEqual(name);
expect(readMany.metadata.total).toEqual(1);

const { body: updated } = await request(app.getHttpServer())
.patch(`/no-named/${created.id}`)
.send({ name: 'updated' })
.expect(HttpStatus.OK);
expect(updated.name).toEqual('updated');

await request(app.getHttpServer()).delete(`/no-named/${created.id}`).expect(HttpStatus.OK);
await request(app.getHttpServer()).get(`/no-named/${created.id}`).expect(HttpStatus.NOT_FOUND);
});
});
11 changes: 8 additions & 3 deletions src/lib/crud.route.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ROUTE_ARGS_METADATA,
} from '@nestjs/common/constants';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { getMetadataArgsStorage } from 'typeorm';
import { getMetadataArgsStorage, DefaultNamingStrategy } from 'typeorm';
import { MetadataUtils } from 'typeorm/metadata-builder/MetadataUtils';

import { capitalizeFirstLetter } from './capitalize-first-letter';
Expand Down Expand Up @@ -100,14 +100,19 @@ export class CrudRouteFactory {
private entityInformation(entity: EntityType): void {
const tableName = (() => {
const table = getMetadataArgsStorage().tables.find(({ target }) => target === entity);

if (!table) {
throw new Error('Cannot find Table from TypeORM');
}

const namingStrategy = (tableName: string | undefined) => new DefaultNamingStrategy().tableName(entity.name, tableName);

if (!table.name && table.type === 'entity-child') {
const discriminatorValue = getMetadataArgsStorage().discriminatorValues.find(({ target }) => target === entity)?.value;
return typeof discriminatorValue === 'string' ? discriminatorValue : discriminatorValue?.name;
return namingStrategy(typeof discriminatorValue === 'string' ? discriminatorValue : discriminatorValue?.name);
}
return table.name;

return namingStrategy(table?.name);
})();
if (!tableName) {
throw new Error('Cannot find Entity name from TypeORM');
Expand Down

0 comments on commit e5d5ec1

Please sign in to comment.