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 { ConsoleLogger } from '@nestjs/common';
import { ClassConstructor, plainToClass } from 'class-transformer'; import { ClassConstructor } from 'class-transformer';
import { import {
DeleteResult, DeleteResult,
FindConditions, FindConditions,
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
Repository, Repository,
SelectQueryBuilder, SelectQueryBuilder,
UpdateResult, UpdateResult,
DeepPartial,
} from 'typeorm'; } from 'typeorm';
import { import {
BlankReturnMessageDto, BlankReturnMessageDto,
...@@ -16,23 +15,36 @@ import { ...@@ -16,23 +15,36 @@ import {
} from '../dto/ReturnMessage.dto'; } from '../dto/ReturnMessage.dto';
import { QueryWise } from '../entities/interfaces/QueryWise'; import { QueryWise } from '../entities/interfaces/QueryWise';
import { camelCase } from 'typeorm/util/StringUtils'; 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 { 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 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< export class CrudBase<
T extends Record<string, any> & { T extends Record<string, any> & {
id: string | number; id: string | number;
} & QueryWise<T> & } & QueryWise<T> &
DeletionWise & DeletionWise &
PageSettingsFactory ImportWise &
PageSettingsFactory,
> extends ConsoleLogger { > extends ConsoleLogger {
protected readonly entityName: string; protected readonly entityName: string;
constructor( constructor(
protected entityClass: ClassConstructor<T>, protected entityClass: ClassConstructor<T>,
protected repo: Repository<T>, protected repo: Repository<T>,
protected entityRelations: string[] = [], protected entityRelations: (string | RelationDef)[] = [],
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
protected extraGetQuery: (qb: SelectQueryBuilder<T>) => void = (qb) => {}, protected extraGetQuery: (qb: SelectQueryBuilder<T>) => void = (qb) => {},
) { ) {
...@@ -40,9 +52,14 @@ export class CrudBase< ...@@ -40,9 +52,14 @@ export class CrudBase<
this.entityName = entityClass.name; 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 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); const repo = mdb.getRepository(this.entityClass);
if (entsWithId.length) { if (entsWithId.length) {
...@@ -55,7 +72,11 @@ export class CrudBase< ...@@ -55,7 +72,11 @@ export class CrudBase<
const existingEntsWithoutDeleteTime = existingEnts.filter( const existingEntsWithoutDeleteTime = existingEnts.filter(
(ent) => ent.deleteTime == null, (ent) => ent.deleteTime == null,
); );
const existingEntsWithDeleteTime = existingEnts.filter(
(ent) => ent.deleteTime != null,
);
if (existingEntsWithoutDeleteTime.length) { if (existingEntsWithoutDeleteTime.length) {
if (!skipErrors) {
throw new BlankReturnMessageDto( throw new BlankReturnMessageDto(
404, 404,
`${this.entityName} ID ${existingEntsWithoutDeleteTime.join( `${this.entityName} ID ${existingEntsWithoutDeleteTime.join(
...@@ -63,14 +84,32 @@ export class CrudBase< ...@@ -63,14 +84,32 @@ export class CrudBase<
)} already exists`, )} already exists`,
).toException(); ).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) { if (beforeCreate) {
await beforeCreate(repo); await beforeCreate(repo);
} }
try { try {
return await repo.save(ents as DeepPartial<T>[]); const results = await repo.save(ents);
return {
results,
skipped,
};
} catch (e) { } catch (e) {
this.error( this.error(
`Failed to create entity ${JSON.stringify(ents)}: ${e.toString()}`, `Failed to create entity ${JSON.stringify(ents)}: ${e.toString()}`,
...@@ -78,7 +117,7 @@ export class CrudBase< ...@@ -78,7 +117,7 @@ export class CrudBase<
throw new BlankReturnMessageDto(500, 'internal error').toException(); 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) { async create(ent: T, beforeCreate?: (repo: Repository<T>) => void) {
...@@ -105,7 +144,7 @@ export class CrudBase< ...@@ -105,7 +144,7 @@ export class CrudBase<
await beforeCreate(repo); await beforeCreate(repo);
} }
try { try {
return await repo.save(ent as DeepPartial<T>); return await repo.save(ent);
} catch (e) { } catch (e) {
this.error( this.error(
`Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`, `Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`,
...@@ -120,22 +159,33 @@ export class CrudBase< ...@@ -120,22 +159,33 @@ export class CrudBase<
return camelCase(this.entityName); return camelCase(this.entityName);
} }
protected applyRelationToQuery(qb: SelectQueryBuilder<T>, relation: string) { protected applyRelationToQuery(
const relationUnit = relation.split('.'); qb: SelectQueryBuilder<T>,
relation: RelationDef,
) {
const { name } = relation;
const relationUnit = name.split('.');
const base = const base =
relationUnit.length === 1 relationUnit.length === 1
? this.entityAliasName ? this.entityAliasName
: relationUnit.slice(0, relationUnit.length - 1).join('_'); : relationUnit.slice(0, relationUnit.length - 1).join('_');
const property = relationUnit[relationUnit.length - 1]; const property = relationUnit[relationUnit.length - 1];
const properyAlias = relationUnit.join('_'); 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>) { protected applyRelationsToQuery(qb: SelectQueryBuilder<T>) {
for (const relation of this.entityRelations) { for (const relation of this.entityRelations) {
if (typeof relation === 'string') {
this.applyRelationToQuery(qb, { name: relation });
} else {
this.applyRelationToQuery(qb, relation); this.applyRelationToQuery(qb, relation);
} }
} }
}
protected queryBuilder() { protected queryBuilder() {
return this.repo.createQueryBuilder(this.entityAliasName); return this.repo.createQueryBuilder(this.entityAliasName);
...@@ -204,17 +254,6 @@ export class CrudBase< ...@@ -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( async update(
id: EntityId<T>, id: EntityId<T>,
entPart: Partial<T>, entPart: Partial<T>,
...@@ -272,4 +311,65 @@ export class CrudBase< ...@@ -272,4 +311,65 @@ export class CrudBase<
} }
return new BlankReturnMessageDto(204, 'success'); 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 { ...@@ -6,7 +6,16 @@ export interface DeletionWise {
deleteTime?: Date; 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 }) @CreateDateColumn({ select: false })
@NotColumn() @NotColumn()
createTime: Date; createTime: Date;
...@@ -22,6 +31,14 @@ export class TimeBase extends PageSettingsDto implements DeletionWise { ...@@ -22,6 +31,14 @@ export class TimeBase extends PageSettingsDto implements DeletionWise {
toObject() { toObject() {
return JSON.parse(JSON.stringify(this)); return JSON.parse(JSON.stringify(this));
} }
isValidInCreation(): string | undefined {
return;
}
async prepareForSaving(): Promise<void> {}
afterSaving() {}
} }
export const TimeBaseFields: (keyof TimeBase)[] = [ 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