Commit 2d216a78 authored by nanahira's avatar nanahira

put new things

parent f90fe6e3
Pipeline #10495 passed with stages
in 1 minute and 18 seconds
import { ConsoleLogger } from '@nestjs/common';
import { ClassConstructor, plainToClass } from 'class-transformer';
import { ClassConstructor } from 'class-transformer';
import {
DeleteResult,
FindConditions,
......@@ -7,7 +7,6 @@ import {
Repository,
SelectQueryBuilder,
UpdateResult,
DeepPartial,
} from 'typeorm';
import {
BlankReturnMessageDto,
......@@ -16,23 +15,36 @@ import {
} from '../dto/ReturnMessage.dto';
import { QueryWise } from '../entities/interfaces/QueryWise';
import { camelCase } from 'typeorm/util/StringUtils';
import { DeletionWise } from '../entities/bases/TimeBase.entity';
import { DeletionWise, ImportWise } from '../entities/bases/TimeBase.entity';
import { PageSettingsFactory } from '../dto/PageSettings.dto';
import { generateEntityFromRaw } from '../utility/generateEntityFromRaw';
import { ImportEntry } from 'src/dto/import-entry.dto';
import _ from 'lodash';
import { CsvParseService } from 'src/csv-parse/csv-parse/csv-parse.service';
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: string | number;
} & QueryWise<T> &
DeletionWise &
PageSettingsFactory
ImportWise &
PageSettingsFactory,
> extends ConsoleLogger {
protected readonly entityName: string;
constructor(
protected entityClass: ClassConstructor<T>,
protected repo: Repository<T>,
protected entityRelations: string[] = [],
protected entityRelations: (string | RelationDef)[] = [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected extraGetQuery: (qb: SelectQueryBuilder<T>) => void = (qb) => {},
) {
......@@ -40,9 +52,14 @@ export class CrudBase<
this.entityName = entityClass.name;
}
async batchCreate(ents: T[], beforeCreate?: (repo: Repository<T>) => void) {
async batchCreate(
ents: T[],
beforeCreate?: (repo: Repository<T>) => void,
skipErrors = false,
) {
const entsWithId = ents.filter((ent) => ent.id != null);
const savedEnt = await this.repo.manager.transaction(async (mdb) => {
const result = await this.repo.manager.transaction(async (mdb) => {
let skipped: { result: string; entry: T }[] = [];
const repo = mdb.getRepository(this.entityClass);
if (entsWithId.length) {
......@@ -55,7 +72,11 @@ export class CrudBase<
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(
......@@ -63,14 +84,32 @@ export class CrudBase<
)} already exists`,
).toException();
}
await repo.delete(existingEnts.map((ent) => ent.id) as any[]);
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 {
return await repo.save(ents as DeepPartial<T>[]);
const results = await repo.save(ents);
return {
results,
skipped,
};
} catch (e) {
this.error(
`Failed to create entity ${JSON.stringify(ents)}: ${e.toString()}`,
......@@ -78,7 +117,7 @@ export class CrudBase<
throw new BlankReturnMessageDto(500, 'internal error').toException();
}
});
return new ReturnMessageDto(201, 'success', savedEnt);
return new ReturnMessageDto(201, 'success', result);
}
async create(ent: T, beforeCreate?: (repo: Repository<T>) => void) {
......@@ -105,7 +144,7 @@ export class CrudBase<
await beforeCreate(repo);
}
try {
return await repo.save(ent as DeepPartial<T>);
return await repo.save(ent);
} catch (e) {
this.error(
`Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`,
......@@ -120,22 +159,33 @@ export class CrudBase<
return camelCase(this.entityName);
}
protected applyRelationToQuery(qb: SelectQueryBuilder<T>, relation: string) {
const relationUnit = relation.split('.');
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('_');
qb.leftJoinAndSelect(`${base}.${property}`, properyAlias);
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);
......@@ -204,17 +254,6 @@ export class CrudBase<
}
}
async findAllPlain(
ent?: Partial<T>,
extraQuery: (qb: SelectQueryBuilder<T>) => void = () => {},
) {
const queryEnt = plainToClass(this.entityClass, ent, {
groups: ['r'],
enableImplicitConversion: true,
});
return this.findAll(queryEnt, extraQuery);
}
async update(
id: EntityId<T>,
entPart: Partial<T>,
......@@ -272,4 +311,65 @@ export class CrudBase<
}
return new BlankReturnMessageDto(204, 'success');
}
async importCsv(
csvService: CsvParseService,
data: Buffer,
encoding = 'utf8',
extraChecking?: (ent: T) => string | Promise<string>,
) {
try {
const raw = await csvService.parseCsv<any>(data, {
targetEncoding: encoding,
columns: true,
cast: false,
});
const ents = raw.map((d) => generateEntityFromRaw(this.entityClass, d));
return this.importEntities(ents, extraChecking);
} catch (e) {
throw new BlankReturnMessageDto(
400,
'文件解析失败,请检查文件格式。',
).toException();
}
}
async importEntities(
ents: T[],
extraChecking?: (ent: T) => string | Promise<string>,
): Promise<ReturnMessageDto<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) => e.afterSaving());
const results = [
...invalidResults,
...data.skipped,
...data.results.map((e) => ({ entry: e, result: 'OK' })),
];
return new ReturnMessageDto(201, 'success', results);
}
async exists(id: EntityId<T>): Promise<boolean> {
const ent = await this.repo.findOne({ where: { id }, select: ['id'] });
return !!ent;
}
}
......@@ -6,7 +6,16 @@ export interface DeletionWise {
deleteTime?: Date;
}
export class TimeBase extends PageSettingsDto implements DeletionWise {
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;
......@@ -22,6 +31,14 @@ export class TimeBase extends PageSettingsDto implements DeletionWise {
toObject() {
return JSON.parse(JSON.stringify(this));
}
isValidInCreation(): string | undefined {
return;
}
async prepareForSaving(): Promise<void> {}
afterSaving() {}
}
export const TimeBaseFields: (keyof TimeBase)[] = [
......
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