Commit ed68d83f authored by nanahira's avatar nanahira

migrate to nicot

parent 9e3f3e6a
Pipeline #14563 failed with stages
in 23 seconds
#!/bin/bash #!/bin/bash
npm install --save typeorm @nestjs/typeorm pg pg-native npm install --save typeorm @nestjs/typeorm pg pg-native nicot
import { ConsoleLogger } from '@nestjs/common';
import { ClassConstructor } from 'class-transformer';
import {
DeepPartial,
DeleteResult,
FindOptionsWhere,
In,
Repository,
SelectQueryBuilder,
UpdateResult,
} from 'typeorm';
import {
BlankReturnMessageDto,
PaginatedReturnMessageDto,
ReturnMessageDto,
} from '../dto/ReturnMessage.dto';
import { QueryWise } from '../entities/interfaces/QueryWise';
import { camelCase } from 'typeorm/util/StringUtils';
import { DeletionWise, ImportWise } from '../entities/bases/TimeBase.entity';
import { PageSettingsFactory } from '../dto/PageSettings.dto';
import { ImportEntry } from 'src/dto/import-entry.dto';
import _ from 'lodash';
export type EntityId<T extends { id: any }> = T['id'];
export interface RelationDef {
name: string;
inner?: boolean;
}
export const Inner = (name: string): RelationDef => {
return { name, inner: true };
};
export class CrudBase<
T extends Record<string, any> & {
id: any;
} & QueryWise<T> &
DeletionWise &
ImportWise &
PageSettingsFactory,
> extends ConsoleLogger {
protected readonly entityName = this.entityClass.name;
private readonly entityReturnMessageDto = ReturnMessageDto(this.entityClass);
private readonly entityPaginatedReturnMessageDto = PaginatedReturnMessageDto(
this.entityClass,
);
constructor(
protected entityClass: ClassConstructor<T>,
protected repo: Repository<T>,
protected entityRelations: (string | RelationDef)[] = [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected extraGetQuery: (qb: SelectQueryBuilder<T>) => void = (qb) => {},
) {
super(`${entityClass.name} Service`);
}
async batchCreate(
ents: T[],
beforeCreate?: (repo: Repository<T>) => Promise<void>,
skipErrors = false,
) {
const entsWithId = ents.filter((ent) => ent.id != null);
const result = await this.repo.manager.transaction(async (mdb) => {
let skipped: { result: string; entry: T }[] = [];
const repo = mdb.getRepository(this.entityClass);
if (entsWithId.length) {
const existingEnts = await repo.find({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
where: { id: In<string | number>(entsWithId.map((ent) => ent.id)) },
select: ['id', 'deleteTime'],
withDeleted: true,
});
if (existingEnts.length) {
const existingEntsWithoutDeleteTime = existingEnts.filter(
(ent) => ent.deleteTime == null,
);
const existingEntsWithDeleteTime = existingEnts.filter(
(ent) => ent.deleteTime != null,
);
if (existingEntsWithoutDeleteTime.length) {
if (!skipErrors) {
throw new BlankReturnMessageDto(
404,
`${this.entityName} ID ${existingEntsWithoutDeleteTime.join(
',',
)} already exists`,
).toException();
}
const skippedEnts = ents.filter((ent) =>
existingEntsWithoutDeleteTime.some((e) => e.id === ent.id),
);
skipped = skippedEnts.map((ent) => ({
result: 'Already exists',
entry: ent,
}));
const skippedEntsSet = new Set(skippedEnts);
ents = ents.filter((ent) => !skippedEntsSet.has(ent));
}
if (existingEntsWithDeleteTime.length) {
await repo.delete(
existingEntsWithDeleteTime.map((ent) => ent.id) as any[],
);
}
}
}
if (beforeCreate) {
await beforeCreate(repo);
}
try {
const results = await repo.save(ents as DeepPartial<T>[]);
return {
results,
skipped,
};
} catch (e) {
this.error(
`Failed to create entity ${JSON.stringify(ents)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
});
return result;
}
async create(ent: T, beforeCreate?: (repo: Repository<T>) => Promise<void>) {
const savedEnt = await this.repo.manager.transaction(async (mdb) => {
const repo = mdb.getRepository(this.entityClass);
if (ent.id != null) {
const existingEnt = await repo.findOne({
where: { id: ent.id },
select: ['id', 'deleteTime'],
withDeleted: true,
});
if (existingEnt) {
if (existingEnt.deleteTime) {
await repo.delete(existingEnt.id);
} else {
throw new BlankReturnMessageDto(
404,
`${this.entityName} ID ${ent.id} already exists`,
).toException();
}
}
}
if (beforeCreate) {
await beforeCreate(repo);
}
try {
return await repo.save(ent as DeepPartial<T>);
} catch (e) {
this.error(
`Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
});
return new this.entityReturnMessageDto(201, 'success', savedEnt);
}
protected get entityAliasName() {
return camelCase(this.entityName);
}
protected applyRelationToQuery(
qb: SelectQueryBuilder<T>,
relation: RelationDef,
) {
const { name } = relation;
const relationUnit = name.split('.');
const base =
relationUnit.length === 1
? this.entityAliasName
: relationUnit.slice(0, relationUnit.length - 1).join('_');
const property = relationUnit[relationUnit.length - 1];
const properyAlias = relationUnit.join('_');
const methodName = relation.inner
? 'innerJoinAndSelect'
: ('leftJoinAndSelect' as const);
qb[methodName](`${base}.${property}`, properyAlias);
}
protected applyRelationsToQuery(qb: SelectQueryBuilder<T>) {
for (const relation of this.entityRelations) {
if (typeof relation === 'string') {
this.applyRelationToQuery(qb, { name: relation });
} else {
this.applyRelationToQuery(qb, relation);
}
}
}
protected queryBuilder() {
return this.repo.createQueryBuilder(this.entityAliasName);
}
async findOne(
id: EntityId<T>,
// eslint-disable-next-line @typescript-eslint/no-empty-function
extraQuery: (qb: SelectQueryBuilder<T>) => void = () => {},
) {
const query = this.queryBuilder()
.where(`${this.entityAliasName}.id = :id`, { id })
.take(1);
this.applyRelationsToQuery(query);
this.extraGetQuery(query);
extraQuery(query);
query.take(1);
let ent: T;
try {
ent = await query.getOne();
} 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();
}
if (!ent) {
throw new BlankReturnMessageDto(
404,
`${this.entityName} ID ${id} not found.`,
).toException();
}
return new this.entityReturnMessageDto(200, 'success', ent);
}
async findAll(
ent?: Partial<T>,
// eslint-disable-next-line @typescript-eslint/no-empty-function
extraQuery: (qb: SelectQueryBuilder<T>) => void = () => {},
) {
const query = this.queryBuilder();
if (ent) {
ent.applyQuery(query, this.entityAliasName);
}
this.applyRelationsToQuery(query);
this.extraGetQuery(query);
extraQuery(query);
try {
const [ents, count] = await query.getManyAndCount();
return new this.entityPaginatedReturnMessageDto(
200,
'success',
ents,
count,
ent.getActualPageSettings(),
);
} 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>,
cond: FindOptionsWhere<T> = {},
) {
let result: UpdateResult;
try {
result = await this.repo.update(
{
id,
...cond,
},
entPart,
);
} catch (e) {
this.error(
`Failed to update entity ID ${id} to ${JSON.stringify(
entPart,
)}: ${e.toString()}`,
);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
if (!result.affected) {
throw new BlankReturnMessageDto(
404,
`${this.entityName} ID ${id} not found.`,
).toException();
}
return new BlankReturnMessageDto(200, 'success');
}
async remove(
id: EntityId<T>,
hardDelete = false,
cond: FindOptionsWhere<T> = {},
) {
let result: UpdateResult | DeleteResult;
const searchCond = {
id,
...cond,
};
try {
result = await (hardDelete
? this.repo.delete(searchCond)
: this.repo.softDelete(searchCond));
} catch (e) {
this.error(`Failed to delete entity ID ${id}: ${e.toString()}`);
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
if (!result.affected) {
throw new BlankReturnMessageDto(
404,
`${this.entityName} ID ${id} not found.`,
).toException();
}
return new BlankReturnMessageDto(204, 'success');
}
async importEntities(
ents: T[],
extraChecking?: (ent: T) => string | Promise<string>,
): Promise<ImportEntry<T>[]> {
const invalidResults = _.compact(
await Promise.all(
ents.map(async (ent) => {
const reason = ent.isValidInCreation();
if (reason) {
return { entry: ent, result: reason };
}
if (extraChecking) {
const reason = await extraChecking(ent);
if (reason) {
return { entry: ent, result: reason };
}
}
}),
),
);
const remainingEnts = ents.filter(
(ent) => !invalidResults.find((result) => result.entry === ent),
);
await Promise.all(remainingEnts.map((ent) => ent.prepareForSaving()));
const data = await this.batchCreate(remainingEnts, undefined, true);
data.results.forEach((e) => {
if (e.afterSaving) {
e.afterSaving();
}
});
const results = [
...invalidResults,
...data.skipped,
...data.results.map((e) => ({ entry: e, result: 'OK' })),
];
return results;
}
async exists(id: EntityId<T>): Promise<boolean> {
const ent = await this.repo.findOne({ where: { id }, select: ['id'] });
return !!ent;
}
}
import {
ApiBody,
ApiCreatedResponse,
ApiNoContentResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
OmitType,
PartialType,
} from '@nestjs/swagger';
import { Body, Delete, Get, Patch, Post, Query, Type } from '@nestjs/common';
import {
BlankReturnMessageDto,
PaginatedReturnMessageDto,
ReturnMessageDto,
} from '../dto/ReturnMessage.dto';
import { TimeBase, TimeBaseFields } from '../entities/bases/TimeBase.entity';
import { ClassGetPipe, CreatePipe, UpdatePipe } from '../utility/pipes';
export function MergeMethodDecorators(
decorators: MethodDecorator[],
): MethodDecorator {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
decorators.forEach((decorator) => {
decorator(target, key, descriptor);
});
};
}
export class CrudFactory<T extends TimeBase> {
readonly createDto: Type<Omit<T, keyof T>>;
readonly updateDto: Type<Partial<Omit<T, keyof T>>>;
readonly entityReturnMessageDto = ReturnMessageDto(this.entityClass);
readonly entityArrayReturnMessageDto = PaginatedReturnMessageDto(
this.entityClass,
);
constructor(
public readonly entityClass: Type<T>,
fieldsToOmit: (keyof T)[] = [],
// eslint-disable-next-line @typescript-eslint/ban-types
public readonly idType: Function = Number,
) {
this.createDto = OmitType(this.entityClass, [
...TimeBaseFields,
...fieldsToOmit,
]);
this.updateDto = PartialType(this.createDto);
}
create(): MethodDecorator {
return MergeMethodDecorators([
Post(),
ApiOperation({ summary: `Create a new ${this.entityClass.name}` }),
ApiBody({ type: this.createDto }),
ApiCreatedResponse({ type: this.entityReturnMessageDto }),
]);
}
createParam() {
return Body(CreatePipe);
}
findOne(): MethodDecorator {
return MergeMethodDecorators([
Get(':id'),
ApiOperation({ summary: `Find a ${this.entityClass.name} by id` }),
ApiParam({ name: 'id', type: this.idType, required: true }),
ApiOkResponse({ type: this.entityReturnMessageDto }),
]);
}
findAll(): MethodDecorator {
return MergeMethodDecorators([
Get(),
ApiOperation({ summary: `Find all ${this.entityClass.name}` }),
ApiOkResponse({ type: this.entityArrayReturnMessageDto }),
]);
}
findAllParam() {
return Query(new ClassGetPipe(this.entityClass));
}
update(): MethodDecorator {
return MergeMethodDecorators([
Patch(':id'),
ApiOperation({ summary: `Update a ${this.entityClass.name} by id` }),
ApiParam({ name: 'id', type: this.idType, required: true }),
ApiBody({ type: this.updateDto }),
ApiOkResponse({ type: BlankReturnMessageDto }),
]);
}
updateParam() {
return Body(UpdatePipe);
}
delete(): MethodDecorator {
return MergeMethodDecorators([
Delete(':id'),
ApiOperation({ summary: `Delete a ${this.entityClass.name} by id` }),
ApiParam({ name: 'id', type: this.idType, required: true }),
ApiNoContentResponse({ type: BlankReturnMessageDto }),
]);
}
}
import { IsInt, IsPositive } from 'class-validator';
import { SelectQueryBuilder } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { NotWritable } from '../entities/decorators/transform';
export interface PageSettingsWise {
pageCount: number;
recordsPerPage: number;
}
export interface PageSettingsFactory {
getActualPageSettings(): PageSettingsWise;
}
export class PageSettingsDto implements PageSettingsWise, PageSettingsFactory {
@NotWritable()
@IsPositive()
@IsInt()
@ApiProperty({
description: 'The nth page, starting with 1.',
required: false,
})
pageCount: number;
@NotWritable()
@IsPositive()
@IsInt()
@ApiProperty({ description: 'Records per page.', required: false })
recordsPerPage: number;
getActualPageSettings(): PageSettingsWise {
return {
pageCount: this.getPageCount(),
recordsPerPage: this.getRecordsPerPage(),
};
}
private getPageCount() {
return this.pageCount || 1;
}
private getRecordsPerPage() {
return this.recordsPerPage || 25;
}
private getStartingFrom() {
return (this.getPageCount() - 1) * this.getRecordsPerPage();
}
applyQuery(qb: SelectQueryBuilder<PageSettingsDto>, entityName: string) {
qb.take(this.getRecordsPerPage()).skip(this.getStartingFrom());
}
}
import { ApiProperty } from '@nestjs/swagger'; // Blank for nicot use.
import { HttpException } from '@nestjs/common';
import { PageSettingsWise } from './PageSettings.dto';
export interface BlankReturnMessage {
statusCode: number;
message: string;
success: boolean;
}
export interface ReturnMessage<T> extends BlankReturnMessage {
data?: T;
}
export class BlankReturnMessageDto implements BlankReturnMessage {
@ApiProperty({ description: 'Return code' })
statusCode: number;
@ApiProperty({ description: 'Return message' })
message: string;
@ApiProperty({ description: 'Whether success.' })
success: boolean;
constructor(statusCode: number, message?: string) {
this.statusCode = statusCode;
this.message = message || 'success';
this.success = statusCode < 400;
}
toException() {
return new HttpException(this, this.statusCode);
}
}
export class BlankPaginatedReturnMessageDto
extends BlankReturnMessageDto
implements PageSettingsWise
{
@ApiProperty({ description: 'Total record count.' })
total: number;
@ApiProperty({ description: 'Total page count.' })
totalPages: number;
@ApiProperty({ description: 'Current page.' })
pageCount: number;
@ApiProperty({ description: 'Records per page.' })
recordsPerPage: number;
constructor(
statusCode: number,
message: string,
total: number,
pageSettings: PageSettingsWise,
) {
super(statusCode, message);
this.total = total;
this.pageCount = pageSettings.pageCount;
this.recordsPerPage = pageSettings.recordsPerPage;
this.totalPages = Math.ceil(total / pageSettings.recordsPerPage);
}
}
type AnyClass = new (...args: any[]) => any;
type ClassOrArray = AnyClass | [AnyClass];
type TypeFromClass<T> = T extends new (...args: any[]) => infer U ? U : never;
export type ParseType<T extends ClassOrArray> = T extends [infer U]
? TypeFromClass<U>[]
: TypeFromClass<T>;
function getClass(o: ClassOrArray) {
return o instanceof Array ? o[0] : o;
}
export function ReturnMessageDto<T extends ClassOrArray>(type: T) {
const cl = class SpecificReturnMessage extends BlankReturnMessageDto {
data?: ParseType<T>;
constructor(statusCode: number, message?: string, data?: ParseType<T>) {
super(statusCode, message);
this.data = data;
}
};
ApiProperty({ description: 'Return data.', type })(cl.prototype, 'data');
Object.defineProperty(cl, 'name', {
value: `${getClass(type).name}ReturnMessageDto`,
});
return cl;
}
export function PaginatedReturnMessageDto<T extends AnyClass>(type: T) {
const cl = class SpecificPaginatedReturnMessageDto
extends BlankPaginatedReturnMessageDto
implements PageSettingsWise
{
data?: TypeFromClass<T>[];
constructor(
statusCode: number,
message: string,
data: TypeFromClass<T>[],
total: number,
pageSettings: PageSettingsWise,
) {
super(statusCode, message, total, pageSettings);
this.data = data;
}
};
ApiProperty({ description: 'Return data.', type: [type] })(
cl.prototype,
'data',
);
Object.defineProperty(cl, 'name', {
value: `${getClass(type).name}PaginatedReturnMessageDto`,
});
return cl;
}
export class StringReturnMessageDto
extends BlankReturnMessageDto
implements ReturnMessage<string>
{
@ApiProperty({ description: 'Return data.' })
data?: string;
constructor(statusCode: number, message?: string, data?: string) {
super(statusCode, message);
this.data = data;
}
}
import { ApiProperty } from '@nestjs/swagger';
export class ImportEntryBaseDto {
@ApiProperty({ description: 'Import result' })
result: string;
}
export interface ImportEntry<T> {
entry: T;
result: string;
}
import { IdNameDescBase } from './IdNameDescBase.entity';
import { StringColumn } from '../decorators/base';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryPropertyLike } from '../utility/query';
export class AvatarBase extends IdNameDescBase {
@StringColumn(128, '图标地址', undefined, false)
avatarUrl: string;
override applyQuery(qb: SelectQueryBuilder<AvatarBase>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryPropertyLike(this, qb, entityName, 'avatarUrl');
}
}
import { Column, Generated, SelectQueryBuilder } from 'typeorm';
import { IdWise } from '../interfaces/wises';
import { ApiProperty } from '@nestjs/swagger';
import { applyQueryProperty } from '../utility/query';
import { NotWritable } from '../decorators/transform';
import { IsInt, IsPositive } from 'class-validator';
import { BigintTransformer } from '../utility/bigint-transform';
import { TimeBase } from './TimeBase.entity';
export class IdBase extends TimeBase implements IdWise {
@Generated('increment')
@Column('bigint', {
primary: true,
unsigned: true,
transformer: new BigintTransformer(),
})
@ApiProperty({ description: '编号', required: false })
@NotWritable()
@IsInt()
@IsPositive()
id: number;
override 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;
override 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';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryPropertySearch } from '../utility/query';
export class IdNameDescBase extends IdNameBase implements IdNameDescWise {
@EntityDescription()
desc: string;
override applyQuery(
qb: SelectQueryBuilder<IdNameDescBase>,
entityName: string,
) {
super.applyQuery(qb, entityName);
applyQueryPropertySearch(this, qb, entityName, 'desc');
}
}
import { StringColumn } from '../decorators/base';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryProperty } from '../utility/query';
import { ManualNameDescBase } from './ManualNameDescBase.entity';
export class AvatarBase extends ManualNameDescBase {
@StringColumn(128, '图标地址', undefined, false)
avatarUrl: string;
override applyQuery(qb: SelectQueryBuilder<AvatarBase>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryProperty(this, qb, entityName, 'avatarUrl');
}
}
import { PrimaryColumn, SelectQueryBuilder } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { StringIdWise } from '../interfaces/wises';
import { applyQueryProperty } from '../utility/query';
import { NotChangeable } from '../decorators/transform';
import { IsNotEmpty, IsString } from 'class-validator';
import { TimeBase } from './TimeBase.entity';
export class ManualIdBase extends TimeBase implements StringIdWise {
@PrimaryColumn('varchar', { length: 32 })
@ApiProperty({ description: '编号' })
@NotChangeable()
@IsString()
@IsNotEmpty()
id: string;
override applyQuery(
qb: SelectQueryBuilder<ManualIdBase>,
entityName: string,
) {
super.applyQuery(qb, entityName);
qb.orderBy(`${entityName}.id`, 'ASC');
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;
override 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';
import { SelectQueryBuilder } from 'typeorm';
import { applyQueryPropertySearch } from '../utility/query';
export class ManualNameDescBase
extends ManualNameBase
implements StringIdNameDescWise
{
@EntityDescription()
desc: string;
override applyQuery(
qb: SelectQueryBuilder<ManualNameDescBase>,
entityName: string,
) {
super.applyQuery(qb, entityName);
applyQueryPropertySearch(this, qb, entityName, 'desc');
}
}
import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm';
import { PageSettingsDto } from '../../dto/PageSettings.dto';
import { NotColumn } from '../decorators/base';
export interface DeletionWise {
deleteTime?: Date;
}
export interface ImportWise {
isValidInCreation(): string | undefined;
prepareForSaving(): Promise<void>;
afterSaving(): void;
}
export class TimeBase
extends PageSettingsDto
implements DeletionWise, ImportWise
{
@CreateDateColumn({ select: false })
@NotColumn()
createTime: Date;
@UpdateDateColumn({ select: false })
@NotColumn()
updateTime: Date;
@DeleteDateColumn({ select: false })
@NotColumn()
deleteTime: Date;
toObject() {
return JSON.parse(JSON.stringify(this));
}
isValidInCreation(): string | undefined {
return;
}
async prepareForSaving(): Promise<void> {}
afterSaving() {}
}
export const TimeBaseFields: (keyof TimeBase)[] = [
'createTime',
'updateTime',
'deleteTime',
];
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 {
IsDate,
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
Min,
} from 'class-validator';
import { ColumnWithWidthOptions } from 'typeorm/decorator/options/ColumnWithWidthOptions';
import { BigintTransformer } from '../utility/bigint-transform';
import { Exclude } from 'class-transformer';
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 == null,
comment: description,
...columnExtras,
}),
ApiProperty({
type: String,
description,
default: defaultValue,
required: required && defaultValue == null,
maxLength: length,
...propertyExtras,
}),
...(required ? [] : [IsOptional()]),
IsString(),
IsNotEmpty(),
MaxLength(length),
]);
export const IntColumn = (
type: 'int' | 'smallint' | 'bigint' | 'tinyint' = 'int',
unsigned = false,
description = 'unknown',
defaultValue?: number,
required = false,
columnExtras: ColumnCommonOptions & ColumnWithWidthOptions = {},
propertyExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
Column(type, {
default: defaultValue,
nullable: !required && defaultValue == null,
unsigned,
comment: description,
...(type === 'bigint' ? { transformer: new BigintTransformer() } : {}),
...columnExtras,
}),
ApiProperty({
type: Number,
description,
default: defaultValue,
required: required && defaultValue == null,
...propertyExtras,
}),
...(required ? [] : [IsOptional()]),
IsInt(),
...(unsigned ? [Min(0)] : []),
]);
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,
comment: description,
...columnExtras,
}),
ApiProperty({
description,
enum: targetEnum,
default: defaultValue,
required,
...swaggerExtras,
}),
...(required ? [] : [IsOptional()]),
IsEnum(targetEnum),
]);
export const DateColumn = <T>(
description = 'unknown',
required = false,
columnExtras: ColumnCommonOptions & ColumnEnumOptions = {},
swaggerExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
IsDate(),
...(required ? [] : [IsOptional()]),
Column('timestamp', {
nullable: !required,
comment: description,
...columnExtras,
}),
ApiProperty({
description,
type: Date,
required,
...swaggerExtras,
}),
]);
export const NotColumn = (
description?: string,
swaggerExtras: ApiPropertyOptions = {},
): PropertyDecorator =>
MergePropertyDecorators([
Exclude(),
ApiProperty({
description,
required: false,
readOnly: true,
...swaggerExtras,
}),
]);
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, ApiPropertyOptions } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsPositive } from 'class-validator';
import { BigintTransformer } from '../utility/bigint-transform';
import { ColumnCommonOptions } from 'typeorm/decorator/options/ColumnCommonOptions';
import { ColumnWithLengthOptions } from 'typeorm/decorator/options/ColumnWithLengthOptions';
import { ColumnWithWidthOptions } from 'typeorm/decorator/options/ColumnWithWidthOptions';
export const RelationColumn = (
description = '对应编号',
notNull = false,
columnExtras: ColumnCommonOptions & ColumnWithWidthOptions = {},
propertyExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
Column('bigint', {
nullable: !notNull,
unsigned: true,
transformer: new BigintTransformer(),
comment: description,
...columnExtras,
}),
ApiProperty({
type: Number,
description,
required: notNull,
...propertyExtras,
}),
...(notNull ? [] : [IsOptional()]),
IsInt(),
IsPositive(),
]);
export const StringRelationColumn = (
description = '对应编号',
notNull = false,
columnExtras: ColumnCommonOptions & ColumnWithLengthOptions = {},
propertyExtras: ApiPropertyOptions = {},
) =>
MergePropertyDecorators([
Column('varchar', {
length: 32,
nullable: !notNull,
comment: description,
...columnExtras,
}),
ApiProperty({
type: String,
required: notNull,
description,
...propertyExtras,
}),
...(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 & string)[]
) {
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 & string)[]
) {
for (const field of fields) {
if (obj[field] == null) {
continue;
}
qb.andWhere(`${entityName}.${field} like (:${field} || '%')`, {
[field]: obj[field],
});
}
}
export function applyQueryPropertySearch<T extends TimeBase>(
obj: T,
qb: SelectQueryBuilder<T>,
entityName: string,
...fields: (keyof T & string)[]
) {
for (const field of fields) {
if (obj[field] == null) {
continue;
}
qb.andWhere(`${entityName}.${field} like ('%' || :${field} || '%')`, {
[field]: obj[field],
});
}
}
import yaml from 'yaml';
import * as fs from 'fs';
export interface Config {
host: string;
port: number;
}
const defaultConfig: Config = {
host: '::',
port: 3000,
};
export async function loadConfig(): Promise<Config> {
let readConfig: Partial<Config> = {};
try {
const configText = await fs.promises.readFile('./config.yaml', 'utf-8');
readConfig = yaml.parse(configText);
} catch (e) {
console.error(`Failed to read config: ${e.toString()}`);
}
return {
...defaultConfig,
...readConfig,
};
}
import { ValidationPipe } from '@nestjs/common';
import { ClassConstructor, plainToInstance } from 'class-transformer';
export const CreatePipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['c'], enableImplicitConversion: true },
});
export const GetPipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['r'], enableImplicitConversion: true },
skipMissingProperties: true,
skipNullProperties: true,
skipUndefinedProperties: true,
});
export class ClassGetPipe<T> extends ValidationPipe {
constructor(private readonly classConstructor: ClassConstructor<T>) {
super({
transform: false,
transformOptions: {
groups: ['r'],
enableImplicitConversion: true,
},
skipMissingProperties: true,
skipNullProperties: true,
skipUndefinedProperties: true,
});
}
override async transform(value: any, metadata: any) {
const obj = await super.transform(value, metadata);
return plainToInstance(this.classConstructor, obj, {
groups: ['r'],
enableImplicitConversion: false,
});
}
}
export const UpdatePipe = new ValidationPipe({
transform: true,
transformOptions: { groups: ['u'], enableImplicitConversion: true },
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