Commit f8a20598 authored by nanahira's avatar nanahira

add @NotInResult()

parent 0d6dac65
import { NotWritable } from '../decorators';
import { NotInResult, NotWritable } from '../decorators';
import { SelectQueryBuilder } from 'typeorm';
import { IsInt, IsPositive } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
......@@ -32,6 +32,7 @@ export class PageSettingsDto
type: Number,
minimum: 1,
})
@NotInResult()
pageCount: number;
@NotWritable()
......@@ -43,6 +44,7 @@ export class PageSettingsDto
type: Number,
minimum: 1,
})
@NotInResult()
recordsPerPage: number;
getActualPageSettings(): PageSettingsWise {
......
......@@ -4,7 +4,7 @@ import {
SelectQueryBuilder,
UpdateDateColumn,
} from 'typeorm';
import { NotColumn } from '../decorators';
import { NotColumn, NotInResult } from '../decorators';
import { PageSettingsDto } from './page-settings';
export interface DeletionWise {
......@@ -27,14 +27,17 @@ export class TimeBase
{
@CreateDateColumn({ select: false })
@NotColumn()
@NotInResult({ entityVersioningDate: true })
createTime: Date;
@UpdateDateColumn({ select: false })
@NotColumn()
@NotInResult({ entityVersioningDate: true })
updateTime: Date;
@DeleteDateColumn({ select: false })
@NotColumn()
@NotInResult({ entityVersioningDate: true })
deleteTime: Date;
isValidInCreate(): string | undefined {
......
......@@ -17,12 +17,13 @@ import {
import { ConsoleLogger } from '@nestjs/common';
import { camelCase } from 'typeorm/util/StringUtils';
import _ from 'lodash';
import { ClassType } from './utility/insert-field';
import {
BlankReturnMessageDto,
ClassType,
PaginatedReturnMessageDto,
ReturnMessageDto,
} from 'nesties';
import { getNotInResultFields } from './utility/metadata';
export type EntityId<T extends { id: any }> = T['id'];
export interface RelationDef {
......@@ -56,6 +57,7 @@ export interface CrudOptions<T extends ValidCrudEntity<T>> {
extraGetQuery?: (qb: SelectQueryBuilder<T>) => void;
hardDelete?: boolean;
createOrUpdate?: boolean;
keepEntityVersioningDates?: boolean;
}
export class CrudBase<T extends ValidCrudEntity<T>> {
......@@ -80,6 +82,26 @@ export class CrudBase<T extends ValidCrudEntity<T>> {
this.extraGetQuery = crudOptions.extraGetQuery || ((qb) => {});
}
_cleanEntityNotInResultFields(ent: T): T {
const fields = getNotInResultFields(
this.entityClass,
this.crudOptions.keepEntityVersioningDates,
);
for (const field of fields) {
delete (ent as any)[field];
}
return ent;
}
cleanEntityNotInResultFields<E extends T | T[]>(ents: E): E {
if (Array.isArray(ents)) {
return ents.map((ent) => this._cleanEntityNotInResultFields(ent)) as E;
} else {
return this._cleanEntityNotInResultFields(ents as T) as E;
}
}
async _batchCreate(
ents: T[],
beforeCreate?: (repo: Repository<T>) => Promise<void>,
......@@ -181,8 +203,11 @@ export class CrudBase<T extends ValidCrudEntity<T>> {
results = results.concat(savedChunk);
}
return {
results,
skipped,
results: this.cleanEntityNotInResultFields(results),
skipped: skipped.map((e) => ({
...e,
entry: this.cleanEntityNotInResultFields(e.entry),
})),
};
} catch (e) {
this.log.error(
......@@ -237,7 +262,7 @@ export class CrudBase<T extends ValidCrudEntity<T>> {
try {
const savedEnt = await repo.save(ent as DeepPartial<T>);
await savedEnt.afterCreate();
return savedEnt;
return this.cleanEntityNotInResultFields(savedEnt);
} catch (e) {
this.log.error(
`Failed to create entity ${JSON.stringify(ent)}: ${e.toString()}`,
......@@ -318,7 +343,11 @@ export class CrudBase<T extends ValidCrudEntity<T>> {
).toException();
}
await ent.afterGet();
return new this.entityReturnMessageDto(200, 'success', ent);
return new this.entityReturnMessageDto(
200,
'success',
this.cleanEntityNotInResultFields(ent),
);
}
async findAll(
......@@ -342,7 +371,7 @@ export class CrudBase<T extends ValidCrudEntity<T>> {
return new this.entityPaginatedReturnMessageDto(
200,
'success',
ents,
this.cleanEntityNotInResultFields(ents),
count,
newEnt.getActualPageSettings(),
);
......
......@@ -18,3 +18,8 @@ export const NotChangeable = () =>
export const NotQueryable = () =>
Metadata.set('notQueryable', true, 'notQueryableFields');
export const NotInResult = (options: { entityVersioningDate?: boolean } = {}) =>
options.entityVersioningDate
? Metadata.set('entityVersioningDate', true, 'entityVersioningDateFields')
: Metadata.set('notInResult', true, 'notInResultFields');
......@@ -23,11 +23,7 @@ import { ColumnNumericOptions } from 'typeorm/decorator/options/ColumnNumericOpt
import { Exclude, Transform, Type } from 'class-transformer';
import { BigintTransformer } from '../utility/bigint';
import { Metadata } from '../utility/metadata';
import {
ClassOrArray,
getClassFromClassOrArray,
ParseType,
} from '../utility/insert-field';
import { ClassOrArray, getClassFromClassOrArray, ParseType } from 'nesties';
import { TypeTransformer } from '../utility/type-transformer';
import { NotQueryable } from './access';
......
......@@ -11,12 +11,14 @@ import {
} from '@nestjs/common';
import { ImportDataDto, ImportEntryDto } from '../dto';
import {
AnyClass,
BlankReturnMessageDto,
InsertField,
MergeMethodDecorators,
PaginatedReturnMessageDto,
ReturnMessageDto,
ClassType,
} from 'nesties';
import { ClassType } from '../utility/insert-field';
import {
ApiBadRequestResponse,
ApiBody,
......@@ -31,33 +33,39 @@ import {
import { CreatePipe, GetPipe, UpdatePipe } from './pipes';
import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import _ from 'lodash';
import { getSpecificFields } from '../utility/metadata';
import { getNotInResultFields, getSpecificFields } from '../utility/metadata';
import { RenameClass } from '../utility/rename-class';
import { getMetadataArgsStorage } from 'typeorm';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
export interface RestfulFactoryOptions<T> {
fieldsToOmit?: (keyof T)[];
prefix?: string;
keepEntityVersioningDates?: boolean;
outputFieldsToOmit?: (keyof T)[];
entityClassName?: string;
}
export class RestfulFactory<T> {
readonly entityReturnMessageDto = ReturnMessageDto(this.entityClass);
readonly entityArrayReturnMessageDto = PaginatedReturnMessageDto(
this.entityClass,
);
readonly importReturnMessageDto = ReturnMessageDto([
ImportEntryDto(this.entityClass),
]);
private getEntityClassName() {
return this.options.entityClassName || this.entityClass.name;
}
readonly fieldsToOmit = _.uniq([
...(getSpecificFields(this.entityClass, 'notColumn') as (keyof T)[]),
...(this.options.fieldsToOmit || []),
...getMetadataArgsStorage()
.relations.filter((r) => r.target === this.entityClass)
.map((r) => r.propertyName as keyof T),
]);
private readonly basicDto = OmitType(
private readonly basicInputDto = OmitType(
this.entityClass,
this.fieldsToOmit,
) as ClassType<T>;
readonly createDto = RenameClass(
OmitType(
this.basicDto,
this.basicInputDto,
getSpecificFields(this.entityClass, 'notWritable') as (keyof T)[],
),
`Create${this.entityClass.name}Dto`,
......@@ -66,7 +74,7 @@ export class RestfulFactory<T> {
readonly findAllDto = RenameClass(
PartialType(
OmitType(
this.basicDto,
this.basicInputDto,
getSpecificFields(this.entityClass, 'notQueryable') as (keyof T)[],
),
),
......@@ -81,6 +89,77 @@ export class RestfulFactory<T> {
),
`Update${this.entityClass.name}Dto`,
) as ClassType<T>;
private resolveEntityResultDto() {
const outputFieldsToOmit = new Set([
...(getNotInResultFields(
this.entityClass,
this.options.keepEntityVersioningDates,
) as (keyof T)[]),
...(this.options.outputFieldsToOmit || []),
]);
let resultDto = OmitType(this.entityClass, [...outputFieldsToOmit]);
const { relations } = getMetadataArgsStorage();
for (const relation of relations) {
if (
outputFieldsToOmit.has(relation.propertyName as keyof T) ||
relation.target !== this.entityClass
)
continue;
const relationClassFactory = relation.type;
// check if it's a callable function
if (typeof relationClassFactory !== 'function') continue;
const relationClass = (relationClassFactory as () => AnyClass)();
if (typeof relationClass !== 'function') continue;
const oldApiProperty =
Reflect.getMetadata(
DECORATORS.API_MODEL_PROPERTIES,
this.entityClass.prototype,
relation.propertyName,
) || {};
const replace = (useClass) => {
resultDto = InsertField(resultDto, {
[relation.propertyName]: {
required: false,
...oldApiProperty,
type: relation.relationType.endsWith('-many')
? [useClass]
: useClass,
},
});
};
const existing = this.__resolveVisited.get(relationClass);
if (existing) {
replace(existing);
} else {
if (!this.__resolveVisited.has(this.entityClass)) {
this.__resolveVisited.set(this.entityClass, Object);
}
const relationFactory = new RestfulFactory(
relationClass,
{},
this.__resolveVisited,
);
const relationResultDto = relationFactory.resolveEntityResultDto();
replace(relationResultDto);
this.__resolveVisited.set(relationClass, relationResultDto);
}
}
return RenameClass(
resultDto,
`${this.getEntityClassName()}ResultDto`,
) as ClassType<T>;
}
readonly entityResultDto = this.resolveEntityResultDto();
readonly entityReturnMessageDto = ReturnMessageDto(this.entityResultDto);
readonly entityArrayReturnMessageDto = PaginatedReturnMessageDto(
this.entityResultDto,
);
readonly importReturnMessageDto = ReturnMessageDto([
ImportEntryDto(this.entityResultDto),
]);
// eslint-disable-next-line @typescript-eslint/ban-types
readonly idType: Function = Reflect.getMetadata(
'design:type',
......@@ -91,6 +170,7 @@ export class RestfulFactory<T> {
constructor(
public readonly entityClass: ClassType<T>,
private options: RestfulFactoryOptions<T> = {},
private __resolveVisited = new Map<AnyClass, AnyClass>(),
) {}
private usePrefix(
......@@ -117,14 +197,14 @@ export class RestfulFactory<T> {
this.usePrefix(Post),
HttpCode(200),
ApiOperation({
summary: `Create a new ${this.entityClass.name}`,
summary: `Create a new ${this.getEntityClassName()}`,
...extras,
}),
ApiBody({ type: this.createDto }),
ApiOkResponse({ type: this.entityReturnMessageDto }),
ApiBadRequestResponse({
type: BlankReturnMessageDto,
description: `The ${this.entityClass.name} is not valid`,
description: `The ${this.getEntityClassName()} is not valid`,
}),
]);
}
......@@ -137,14 +217,14 @@ export class RestfulFactory<T> {
return MergeMethodDecorators([
this.usePrefix(Get, ':id'),
ApiOperation({
summary: `Find a ${this.entityClass.name} by id`,
summary: `Find a ${this.getEntityClassName()} by id`,
...extras,
}),
ApiParam({ name: 'id', type: this.idType, required: true }),
ApiOkResponse({ type: this.entityReturnMessageDto }),
ApiNotFoundResponse({
type: BlankReturnMessageDto,
description: `The ${this.entityClass.name} with the given id was not found`,
description: `The ${this.getEntityClassName()} with the given id was not found`,
}),
]);
}
......@@ -160,7 +240,10 @@ export class RestfulFactory<T> {
findAll(extras: Partial<OperationObject> = {}): MethodDecorator {
return MergeMethodDecorators([
this.usePrefix(Get),
ApiOperation({ summary: `Find all ${this.entityClass.name}`, ...extras }),
ApiOperation({
summary: `Find all ${this.getEntityClassName()}`,
...extras,
}),
ApiOkResponse({ type: this.entityArrayReturnMessageDto }),
]);
}
......@@ -174,7 +257,7 @@ export class RestfulFactory<T> {
this.usePrefix(Patch, ':id'),
HttpCode(200),
ApiOperation({
summary: `Update a ${this.entityClass.name} by id`,
summary: `Update a ${this.getEntityClassName()} by id`,
...extras,
}),
ApiParam({ name: 'id', type: this.idType, required: true }),
......@@ -182,11 +265,11 @@ export class RestfulFactory<T> {
ApiOkResponse({ type: BlankReturnMessageDto }),
ApiNotFoundResponse({
type: BlankReturnMessageDto,
description: `The ${this.entityClass.name} with the given id was not found`,
description: `The ${this.getEntityClassName()} with the given id was not found`,
}),
ApiBadRequestResponse({
type: BlankReturnMessageDto,
description: `The ${this.entityClass.name} is not valid`,
description: `The ${this.getEntityClassName()} is not valid`,
}),
ApiInternalServerErrorResponse({
type: BlankReturnMessageDto,
......@@ -204,14 +287,14 @@ export class RestfulFactory<T> {
this.usePrefix(Delete, ':id'),
HttpCode(200),
ApiOperation({
summary: `Delete a ${this.entityClass.name} by id`,
summary: `Delete a ${this.getEntityClassName()} by id`,
...extras,
}),
ApiParam({ name: 'id', type: this.idType, required: true }),
ApiOkResponse({ type: BlankReturnMessageDto }),
ApiNotFoundResponse({
type: BlankReturnMessageDto,
description: `The ${this.entityClass.name} with the given id was not found`,
description: `The ${this.getEntityClassName()} with the given id was not found`,
}),
ApiInternalServerErrorResponse({
type: BlankReturnMessageDto,
......@@ -224,7 +307,7 @@ export class RestfulFactory<T> {
return MergeMethodDecorators([
Post('import'),
ApiOperation({
summary: `Import ${this.entityClass.name}`,
summary: `Import ${this.getEntityClassName()}`,
...extras,
}),
ApiBody({ type: ImportDataDto(this.createDto) }),
......
......@@ -6,7 +6,7 @@ import {
ClassOrArray,
getClassFromClassOrArray,
InsertField,
} from '../utility/insert-field';
} from 'nesties';
export class ImportEntryBaseDto {
@ApiProperty({ description: 'Import result', type: String })
......
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
export type AnyClass = new (...args: any[]) => any;
export type ClassOrArray = AnyClass | [AnyClass];
export type ClassType<T> = new (...args: any[]) => T;
export type TypeFromClass<T> = T extends new (...args: any[]) => infer U
? U
: never;
export type ParamsFromClass<T> = T extends new (...args: infer U) => any
? U
: never;
export type ParseType<IC extends ClassOrArray> = IC extends [infer U]
? TypeFromClass<U>[]
: TypeFromClass<IC>;
export function getClassFromClassOrArray(o: ClassOrArray) {
return o instanceof Array ? o[0] : o;
}
export interface InsertOptions<C extends ClassOrArray = ClassOrArray> {
type: C;
options?: ApiPropertyOptions;
}
type TypeFromInsertOptions<O extends InsertOptions> = O extends InsertOptions<
infer C
>
?
| ParseType<C>
| (O extends { options: { required: true } } ? never : undefined)
: never;
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof T
? T[K]
: K extends keyof U
? U[K]
: never;
};
export function InsertField<
C extends AnyClass,
M extends Record<string, InsertOptions>,
>(
cl: C,
map: M,
newName?: string,
): new (...args: ParamsFromClass<C>) => Merge<
{
[F in keyof M]: TypeFromInsertOptions<M[F]>;
},
TypeFromClass<C>
> {
const extendedCl = class extends cl {};
for (const key in map) {
ApiProperty({
type: map[key].type,
...(map[key].options || {}),
})(extendedCl.prototype, key);
}
Object.defineProperty(extendedCl, 'name', {
value: newName || cl.name,
});
return extendedCl;
}
......@@ -6,6 +6,8 @@ interface SpecificFields {
notWritable: boolean;
notChangeable: boolean;
notQueryable: boolean;
notInResult: boolean;
entityVersioningDate: boolean;
}
interface MetadataMap extends SpecificFields {
......@@ -26,3 +28,17 @@ export function getSpecificFields(obj: any, type: keyof SpecificFields) {
.getArray(`${type}Fields`, obj)
.filter((field) => reflector.get(type, obj, field));
}
export function getNotInResultFields(
obj: any,
keepEntityVersioningDates = false,
) {
const notInResultFields = getSpecificFields(obj, 'notInResult');
if (keepEntityVersioningDates) {
return notInResultFields;
}
return [
...notInResultFields,
...getSpecificFields(obj, 'entityVersioningDate'),
];
}
import { ClassOrArray } from './insert-field';
import { ValueTransformer } from 'typeorm';
import { ClassOrArray } from 'nesties';
export class TypeTransformer implements ValueTransformer {
constructor(private definition: ClassOrArray) {}
......
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