Commit 91960695 authored by nanahira's avatar nanahira

update typeorm template

parent 029dfd0f
Pipeline #6879 passed with stages
in 1 minute and 31 seconds
import { ConsoleLogger } from '@nestjs/common';
import { ClassConstructor } from 'class-transformer';
import { Repository, SelectQueryBuilder } from 'typeorm';
import {
BlankReturnMessageDto,
ReturnMessageDto,
} from '../dto/ReturnMessage.dto';
import { DeletionWise } from '../entities/bases/DeletionBase.entity';
import { QueryWise } from '../entities/interfaces/QueryWise';
import { camelCase } from 'typeorm/util/StringUtils';
export type EntityId<T> = T extends { id: string }
? string
: T extends { id: number }
? number
: never;
export class CrudBase<
T extends Record<string, any> & {
id: string | number;
} & QueryWise<T> &
DeletionWise
> extends ConsoleLogger {
protected readonly entityName: string;
constructor(
protected entityClass: ClassConstructor<T>,
protected repo: Repository<T>,
protected entityRelations: string[] = [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected extraGetQuery: (qb: SelectQueryBuilder<T>) => void = (qb) => {},
) {
super(`${entityClass.name} Service`);
this.entityName = entityClass.name;
}
async create(ent: T) {
try {
const savedEntity = await this.repo.save(ent);
return new ReturnMessageDto(201, 'success', savedEntity);
} catch (e) {
this.error(
`Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
}
protected get entityAliasName() {
return `${camelCase(this.entityName)}_0`;
}
protected applyRelationToQuery(qb: SelectQueryBuilder<T>, relation: string) {
const relationUnit = relation.split('.');
const base =
relationUnit.length === 1
? this.entityAliasName
: `${relationUnit[relationUnit.length - 2]}_${relationUnit.length - 1}`;
const property = relationUnit[relationUnit.length - 1];
const properyAlias = `${property}_${relationUnit.length}`;
qb.leftJoinAndSelect(
`${base}.${property}`,
properyAlias,
`${properyAlias}.isDeleted = false`,
);
}
protected applyRelationsToQuery(qb: SelectQueryBuilder<T>) {
for (const relation of this.entityRelations) {
this.applyRelationToQuery(qb, relation);
}
}
private queryBuilder() {
return this.repo.createQueryBuilder(this.entityAliasName);
}
async findOne(id: EntityId<T>) {
const query = this.queryBuilder()
.where(`${this.entityAliasName}.id = :id`, { id })
.andWhere(`${this.entityAliasName}.isDeleted = false`);
this.applyRelationsToQuery(query);
this.extraGetQuery(query);
try {
const ent = await query.getOne();
if (!ent) {
throw new BlankReturnMessageDto(
404,
`ID ${id} not found.`,
).toException();
}
return new ReturnMessageDto(200, 'success', ent);
} catch (e) {
const [sql, params] = query.getQueryAndParameters();
this.error(
`Failed to read entity ID ${id} with SQL ${sql} param ${params.join(
',',
)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
}
async findAll(ent?: T) {
const query = this.queryBuilder();
if (ent) {
ent.applyQuery(query, this.entityAliasName);
}
this.applyRelationsToQuery(query);
this.extraGetQuery(query);
try {
return new ReturnMessageDto(200, 'success', await query.getMany());
} catch (e) {
const [sql, params] = query.getQueryAndParameters();
this.error(
`Failed to read entity cond ${JSON.stringify(
ent,
)} with SQL ${sql} param ${params.join(',')}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
}
async update(id: EntityId<T>, entPart: Partial<T>) {
try {
const result = await this.repo.update({ id, isDeleted: false }, entPart);
if (!result.affected) {
throw new BlankReturnMessageDto(
404,
`ID ${id} not found.`,
).toException();
}
return new BlankReturnMessageDto(200, 'success');
} catch (e) {
this.error(
`Failed to create entity ID ${id} to ${JSON.stringify(
entPart,
)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
}
async remove(id: EntityId<T>) {
try {
const result = await this.repo.update({ id, isDeleted: false }, {
isDeleted: true,
} as Partial<T>);
if (!result.affected) {
throw new BlankReturnMessageDto(
404,
`ID ${id} not found.`,
).toException();
}
return new BlankReturnMessageDto(200, 'success');
} catch (e) {
this.error(`Failed to delete entity ID ${id}: ${e.toString()}`);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
}
}
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { SelectQueryBuilder } from 'typeorm';
export class PageSettingsDto {
@IsOptional()
@IsPositive()
@IsInt()
pageCount: number;
@IsOptional()
@IsPositive()
@IsInt()
recordsPerPage: number;
private getRecordsPerPage() {
return this.recordsPerPage || 25;
}
private getStartingFrom() {
return ((this.pageCount || 1) - 1) * this.getRecordsPerPage();
}
applyQuery(qb: SelectQueryBuilder<PageSettingsDto>, entityName: string) {
qb.take(this.getRecordsPerPage()).skip(this.getStartingFrom());
}
}
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { HttpException } from '@nestjs/common'; import { HttpException } from '@nestjs/common';
import { User } from '../entities/User.entity';
export interface BlankReturnMessage { export interface BlankReturnMessage {
statusCode: number; statusCode: number;
...@@ -39,3 +40,10 @@ export class ReturnMessageDto<T> ...@@ -39,3 +40,10 @@ export class ReturnMessageDto<T>
this.data = data; this.data = data;
} }
} }
export class StringReturnMessageDto
extends BlankReturnMessageDto
implements ReturnMessage<string> {
@ApiProperty({ description: '返回内容' })
data?: string;
}
import { TimeBase } from './TimeBase.entity'; import {
import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
SelectQueryBuilder,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import { ManualNameDescBase } from './bases/ManualNameDescBase.entity';
import { EnumColumn } from './decorators/base';
import { RelationColumn, StringRelationColumn } from './decorators/relation';
import { IsNotEmpty, IsOptional, IsPositive } from 'class-validator';
import { QueryWise } from './interfaces/QueryWise';
import { applyQueryProperty } from './utility/query';
@Entity() export enum UserRole {
export class User extends TimeBase { Admin = 'admin',
@PrimaryColumn('varchar', { length: 32 }) Student = 'student',
id: string; teacher = 'teacher',
}
@Index() @Entity()
@Column('varchar', { length: 32 }) export class User extends ManualNameDescBase implements QueryWise<User> {
name: string; applyQuery(qb: SelectQueryBuilder<User>, entityName: string) {
super.applyQuery(qb, entityName);
}
} }
import { TimeBase } from './TimeBase.entity';
import { Column, SelectQueryBuilder } from 'typeorm';
import { Exclude } from 'class-transformer';
export interface DeletionWise {
isDeleted: boolean;
}
export class DeletionBase extends TimeBase implements DeletionWise {
@Column('boolean', { select: false, default: false })
@Exclude()
isDeleted: boolean;
applyQuery(qb: SelectQueryBuilder<DeletionBase>, entityName: string) {
super.applyQuery(qb, entityName);
qb.where(`${entityName}.isDeleted = false`);
}
}
import { DeletionBase } from './DeletionBase.entity';
import {
Column,
Generated,
PrimaryGeneratedColumn,
SelectQueryBuilder,
} from 'typeorm';
import { IdWise } from '../interfaces/wises';
import { ApiProperty } from '@nestjs/swagger';
import { applyQueryProperty } from '../utility/query';
import { NotChangeable } from '../decorators/transform';
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { BigintTransformer } from '../utility/bigint-transform';
export class IdBase extends DeletionBase implements IdWise {
@Generated('increment')
@Column('int8', { primary: true, transformer: new BigintTransformer() })
@ApiProperty({ description: '编号', required: false })
@NotChangeable()
@IsOptional()
@IsInt()
@IsPositive()
id: number;
applyQuery(qb: SelectQueryBuilder<IdBase>, entityName: string) {
super.applyQuery(qb, entityName);
qb.orderBy(`${entityName}.id`, 'DESC');
applyQueryProperty(this, qb, entityName, 'id');
}
}
import { IdBase } from './IdBase.entity';
import { IdNameWise } from '../interfaces/wises';
import { EntityName } from '../decorators/extended';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryPropertyLike } from '../utility/query';
export class IdNameBase extends IdBase implements IdNameWise {
@EntityName()
name: string;
applyQuery(qb: SelectQueryBuilder<IdNameBase>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryPropertyLike(this, qb, entityName, 'name');
}
}
import { IdNameBase } from './IdNameBase.entity';
import { IdNameDescWise } from '../interfaces/wises';
import { EntityDescription } from '../decorators/extended';
export class IdNameDescBase extends IdNameBase implements IdNameDescWise {
@EntityDescription()
desc: string;
}
import { PrimaryColumn, SelectQueryBuilder } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { DeletionBase } from './DeletionBase.entity';
import { StringIdWise } from '../interfaces/wises';
import { applyQueryProperty } from '../utility/query';
import { NotChangeable } from '../decorators/transform';
import { IsNotEmpty, IsString } from 'class-validator';
export class ManualIdBase extends DeletionBase implements StringIdWise {
@PrimaryColumn('varchar', { length: 32 })
@ApiProperty({ description: '编号' })
@NotChangeable()
@IsString()
@IsNotEmpty()
id: string;
applyQuery(qb: SelectQueryBuilder<ManualIdBase>, entityName: string) {
super.applyQuery(qb, entityName);
qb.orderBy(`${entityName}.id`, 'DESC');
applyQueryProperty(this, qb, entityName, 'id');
}
}
import { ManualIdBase } from './ManualIdBase.entity';
import { StringIdNameWise } from '../interfaces/wises';
import { EntityName } from '../decorators/extended';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryPropertyLike } from '../utility/query';
export class ManualNameBase extends ManualIdBase implements StringIdNameWise {
@EntityName()
name: string;
applyQuery(qb: SelectQueryBuilder<ManualNameBase>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryPropertyLike(this, qb, entityName, 'name');
}
}
import { ManualNameBase } from './ManualNameBase.entity';
import { StringIdNameDescWise } from '../interfaces/wises';
import { EntityDescription } from '../decorators/extended';
export class ManualNameDescBase
extends ManualNameBase
implements StringIdNameDescWise {
@EntityDescription()
desc: string;
}
import { CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Exclude } from 'class-transformer'; import { Exclude } from 'class-transformer';
import { PageSettingsDto } from '../../dto/PageSettings.dto';
export class TimeBase { export class TimeBase extends PageSettingsDto {
@CreateDateColumn({ select: false }) @CreateDateColumn({ select: false })
@Exclude() @Exclude()
createTime: Date; createTime: Date;
......
import { Column, Index } from 'typeorm';
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
import { ColumnWithLengthOptions } from 'typeorm/decorator/options/ColumnWithLengthOptions';
import { ColumnCommonOptions } from 'typeorm/decorator/options/ColumnCommonOptions';
import { ColumnEnumOptions } from 'typeorm/decorator/options/ColumnEnumOptions';
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
ValidateIf,
} from 'class-validator';
export function MergePropertyDecorators(
decs: PropertyDecorator[],
): PropertyDecorator {
return (obj, key) => {
for (const dec of decs) {
dec(obj, key);
}
};
}
export const OptionalValidate = (...conitions: PropertyDecorator[]) =>
MergePropertyDecorators([IsOptional(), ...conitions]);
export const StringColumn = (
length = 32,
description = 'unknown',
defaultValue?: string,
required = false,
columnExtras: ColumnCommonOptions & ColumnWithLengthOptions = {},
propertyExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
Column('varchar', {
length,
default: defaultValue,
nullable: !required && !defaultValue,
...columnExtras,
}),
ApiProperty({
type: String,
description,
default: defaultValue,
required: required && !defaultValue,
...propertyExtras,
}),
...(required ? [] : [IsOptional()]),
IsString(),
IsNotEmpty(),
]);
export const EnumColumn = <T>(
targetEnum: Record<string, T>,
description = 'unknown',
defaultValue?: T,
required = false,
columnExtras: ColumnCommonOptions & ColumnEnumOptions = {},
swaggerExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
Index(),
Column('enum', {
enum: targetEnum,
default: defaultValue,
nullable: !required && !defaultValue,
...columnExtras,
}),
ApiProperty({
description,
enum: targetEnum,
default: defaultValue,
required,
...swaggerExtras,
}),
...(required ? [] : [IsOptional()]),
IsEnum(targetEnum),
]);
import {
MergePropertyDecorators,
StringColumn,
} from './base';
import { Index } from 'typeorm';
export const EntityName = (length = 32, description = '名称') =>
MergePropertyDecorators([
Index(),
StringColumn(length, description, undefined, true),
]);
export const EntityDescription = (length = 5000, description = '描述') =>
StringColumn(length, description, '暂无介绍', false);
import { Column } from 'typeorm';
import { MergePropertyDecorators } from './base';
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsPositive } from 'class-validator';
import { BigintTransformer } from '../utility/bigint-transform';
export const RelationColumn = (description = '对应编号', notNull = false) =>
MergePropertyDecorators([
Column('int8', {
nullable: !notNull,
transformer: new BigintTransformer(),
}),
ApiProperty({ type: Number, description }),
...(notNull ? [] : [IsOptional()]),
IsPositive(),
]);
export const StringRelationColumn = (
description = '对应编号',
notNull = false,
) =>
MergePropertyDecorators([
Column('varchar', { length: 32, nullable: !notNull }),
ApiProperty({ type: String, description }),
...(notNull ? [] : [IsOptional()]),
IsNotEmpty(),
]);
import { Expose } from 'class-transformer';
import { MergePropertyDecorators } from './base';
import { IsOptional } from 'class-validator';
export const NotWritable = () =>
MergePropertyDecorators([Expose({ groups: ['r'] }), IsOptional()]);
export const NotChangeable = () => Expose({ groups: ['r', 'c'] });
import { SelectQueryBuilder } from 'typeorm';
export interface QueryWise<T> {
applyQuery(qb: SelectQueryBuilder<T>, entityName: string): void;
}
export interface IdWise {
id: number;
}
export interface StringIdWise {
id: string;
}
export interface NameWise {
name: string;
}
export interface DescWise {
desc: string;
}
export interface NameDescWise extends NameWise, DescWise {}
export interface IdNameWise extends IdWise, NameWise {}
export interface StringIdNameWise extends StringIdWise, NameWise {}
export interface IdNameDescWise extends IdWise, NameDescWise {}
export interface StringIdNameDescWise extends StringIdNameWise, DescWise {}
import { ValueTransformer } from 'typeorm';
export class BigintTransformer implements ValueTransformer {
from(dbValue) {
if (dbValue == null) {
return dbValue;
}
return parseInt(dbValue);
}
to(entValue): any {
return entValue;
}
}
import { TimeBase } from '../bases/TimeBase.entity';
import { SelectQueryBuilder } from 'typeorm';
export function applyQueryProperty<T extends TimeBase>(
obj: T,
qb: SelectQueryBuilder<T>,
entityName: string,
...fields: (keyof T)[]
) {
for (const field of fields) {
if (obj[field] == null) {
continue;
}
qb.andWhere(`${entityName}.${field} = :${field}`, { [field]: obj[field] });
}
}
export function applyQueryPropertyLike<T extends TimeBase>(
obj: T,
qb: SelectQueryBuilder<T>,
entityName: string,
...fields: (keyof T)[]
) {
for (const field of fields) {
if (obj[field] == null) {
continue;
}
qb.andWhere(`${entityName}.${field} like (:${field} || '%')`, {
[field]: obj[field],
});
}
}
import { ValidationPipe } from '@nestjs/common';
export const CreatePipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['c'] },
});
export const GetPipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['r'] },
skipMissingProperties: true,
skipNullProperties: true,
skipUndefinedProperties: true,
});
export const UpdatePipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['u'] },
skipMissingProperties: true,
skipNullProperties: true,
skipUndefinedProperties: true,
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment