Commit bf7e9005 authored by nanahira's avatar nanahira

add relations to RestfulFactory

parent a34f5c63
...@@ -159,13 +159,14 @@ name: string; ...@@ -159,13 +159,14 @@ name: string;
NICOT 提供以下装饰器用于控制字段在不同接口中的表现: NICOT 提供以下装饰器用于控制字段在不同接口中的表现:
| 装饰器名 | 行为控制说明 | | 装饰器名 | 行为控制说明 |
|-----------------------|--------------------------------------------------------| |----------------------------------------|-------------------------------|
| `@NotWritable()` | 不允许在创建(POST)或修改(PATCH)时传入 | | `@NotWritable()` | 不允许在创建(POST)或修改(PATCH)时传入 |
| `@NotChangeable()` | 不允许在修改(PATCH)时更新(只可创建) | | `@NotChangeable()` | 不允许在修改(PATCH)时更新(只可创建) |
| `@NotQueryable()` | 不允许在 GET 查询参数中使用该字段 | | `@NotQueryable()` | 不允许在 GET 查询参数中使用该字段 |
| `@NotInResult()` | 不会出现在任何返回结果中(如密码字段) | | `@NotInResult()` | 不会出现在任何返回结果中(如密码字段) |
| `@NotColumn()` | 不是数据库字段(仅逻辑字段,如计算用字段) | | `@NotColumn()` | 不是数据库字段(仅逻辑字段,如计算用字段) |
| `@RelationComputed(() => EntityClass)` | 标识该字段依赖关系字段推导而来(通常在 afterGet) |
RestfulFactory 处理 Entity 类的时候,会以这些装饰器为依据,裁剪生成的 DTO 和查询参数。 RestfulFactory 处理 Entity 类的时候,会以这些装饰器为依据,裁剪生成的 DTO 和查询参数。
...@@ -430,7 +431,7 @@ isActive: boolean; ...@@ -430,7 +431,7 @@ isActive: boolean;
### 示例 Controller ### 示例 Controller
```ts ```ts
const factory = new RestfulFactory(User); const factory = new RestfulFactory(User, { relations: ['articles'] });
class CreateUserDto extends factory.createDto {} class CreateUserDto extends factory.createDto {}
class UpdateUserDto extends factory.updateDto {} class UpdateUserDto extends factory.updateDto {}
class FindAllUserDto extends factory.findAllDto {} class FindAllUserDto extends factory.findAllDto {}
...@@ -477,6 +478,8 @@ export class UserController { ...@@ -477,6 +478,8 @@ export class UserController {
- 所有的接口都是返回状态码 200。 - 所有的接口都是返回状态码 200。
- OpenAPI 文档会自动生成,包含所有 DTO 类型与查询参数。 - OpenAPI 文档会自动生成,包含所有 DTO 类型与查询参数。
- Service 需要使用 `CrudService(Entity, options)` 进行标准化实现。 - Service 需要使用 `CrudService(Entity, options)` 进行标准化实现。
- `RestfulFactory` 的选项 `options` 支持传入 `relations`,形式和 `CrudService` 一致,用于自动裁剪结果 DTO 字段。
- 如果本内容的 `CrudService` 不查询任何关系字段,那么请设置 `{ relations: [] }` 以裁剪所有关系字段。
--- ---
......
import { ColumnCommonOptions } from 'typeorm/decorator/options/ColumnCommonOptions'; import { ColumnCommonOptions } from 'typeorm/decorator/options/ColumnCommonOptions';
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
import { ColumnWithLengthOptions } from 'typeorm/decorator/options/ColumnWithLengthOptions'; import { ColumnWithLengthOptions } from 'typeorm/decorator/options/ColumnWithLengthOptions';
import { MergePropertyDecorators } from 'nesties'; import { AnyClass, MergePropertyDecorators } from 'nesties';
import { Column, Index } from 'typeorm'; import { Column, Index } from 'typeorm';
import { import {
IsDate, IsDate,
...@@ -146,20 +146,20 @@ export const DateColumn = ( ...@@ -146,20 +146,20 @@ export const DateColumn = (
(v) => { (v) => {
const value = v.value; const value = v.value;
if (value == null || value instanceof Date) return value; if (value == null || value instanceof Date) return value;
const timestampToDate = (t: number, isSeconds: boolean) => const timestampToDate = (t: number, isSeconds: boolean) =>
new Date(isSeconds ? t * 1000 : t); new Date(isSeconds ? t * 1000 : t);
if (typeof value === 'number') { if (typeof value === 'number') {
const isSeconds = !Number.isInteger(value) || value < 1e12; const isSeconds = !Number.isInteger(value) || value < 1e12;
return timestampToDate(value, isSeconds); return timestampToDate(value, isSeconds);
} }
if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) { if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value)) {
const isSeconds = value.includes('.') || parseFloat(value) < 1e12; const isSeconds = value.includes('.') || parseFloat(value) < 1e12;
return timestampToDate(parseFloat(value), isSeconds); return timestampToDate(parseFloat(value), isSeconds);
} }
return new Date(value); // fallback to native parser return new Date(value); // fallback to native parser
}, },
{ {
...@@ -233,3 +233,19 @@ export const NotColumn = ( ...@@ -233,3 +233,19 @@ export const NotColumn = (
}), }),
Metadata.set('notColumn', true, 'notColumnFields'), Metadata.set('notColumn', true, 'notColumnFields'),
]); ]);
export const RelationComputed =
(type?: () => AnyClass): PropertyDecorator =>
(obj, propertyKey) => {
const fun = () => {
const designType = Reflect.getMetadata('design:type', obj, propertyKey);
const entityClass = type ? type() : designType;
return {
entityClass,
isArray: designType === Array,
};
};
const dec = Metadata.set('relationComputed', fun, 'relationComputedFields');
return dec(obj, propertyKey);
};
...@@ -32,11 +32,12 @@ import { ...@@ -32,11 +32,12 @@ import {
} from '@nestjs/swagger'; } from '@nestjs/swagger';
import { CreatePipe, GetPipe, UpdatePipe } from './pipes'; import { CreatePipe, GetPipe, UpdatePipe } from './pipes';
import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
import _ from 'lodash'; import _, { upperFirst } from 'lodash';
import { getNotInResultFields, getSpecificFields } from '../utility/metadata'; import { getNotInResultFields, getSpecificFields } from '../utility/metadata';
import { RenameClass } from '../utility/rename-class'; import { RenameClass } from '../utility/rename-class';
import { DECORATORS } from '@nestjs/swagger/dist/constants'; import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { getTypeormRelations } from '../utility/get-typeorm-relations'; import { getTypeormRelations } from '../utility/get-typeorm-relations';
import { RelationDef } from '../crud-base';
export interface RestfulFactoryOptions<T> { export interface RestfulFactoryOptions<T> {
fieldsToOmit?: (keyof T)[]; fieldsToOmit?: (keyof T)[];
...@@ -44,8 +45,25 @@ export interface RestfulFactoryOptions<T> { ...@@ -44,8 +45,25 @@ export interface RestfulFactoryOptions<T> {
keepEntityVersioningDates?: boolean; keepEntityVersioningDates?: boolean;
outputFieldsToOmit?: (keyof T)[]; outputFieldsToOmit?: (keyof T)[];
entityClassName?: string; entityClassName?: string;
relations?: (string | RelationDef)[];
} }
const extractRelationName = (relation: string | RelationDef) => {
if (typeof relation === 'string') {
return relation;
} else {
return relation.name;
}
};
const getCurrentLevelRelations = (relations: string[]) =>
relations.filter((r) => !r.includes('.'));
const getNextLevelRelations = (relations: string[], enteringField: string) =>
relations
.filter((r) => r.includes('.') && r.startsWith(`${enteringField}.`))
.map((r) => r.split('.').slice(1).join('.'));
export class RestfulFactory<T> { export class RestfulFactory<T> {
private getEntityClassName() { private getEntityClassName() {
return this.options.entityClassName || this.entityClass.name; return this.options.entityClassName || this.entityClass.name;
...@@ -70,7 +88,7 @@ export class RestfulFactory<T> { ...@@ -70,7 +88,7 @@ export class RestfulFactory<T> {
), ),
`Create${this.entityClass.name}Dto`, `Create${this.entityClass.name}Dto`,
) as ClassType<T>; ) as ClassType<T>;
readonly importDto = ImportDataDto(this.entityClass); readonly importDto = ImportDataDto(this.createDto);
readonly findAllDto = RenameClass( readonly findAllDto = RenameClass(
PartialType( PartialType(
OmitType( OmitType(
...@@ -91,15 +109,27 @@ export class RestfulFactory<T> { ...@@ -91,15 +109,27 @@ export class RestfulFactory<T> {
) as ClassType<T>; ) as ClassType<T>;
private resolveEntityResultDto() { private resolveEntityResultDto() {
const relations = getTypeormRelations(this.entityClass);
const currentLevelRelations =
this.options.relations &&
new Set(
getCurrentLevelRelations(
this.options.relations.map(extractRelationName),
),
);
const outputFieldsToOmit = new Set([ const outputFieldsToOmit = new Set([
...(getNotInResultFields( ...(getNotInResultFields(
this.entityClass, this.entityClass,
this.options.keepEntityVersioningDates, this.options.keepEntityVersioningDates,
) as (keyof T)[]), ) as (keyof T)[]),
...(this.options.outputFieldsToOmit || []), ...(this.options.outputFieldsToOmit || []),
...(this.options.relations
? (relations
.map((r) => r.propertyName)
.filter((r) => !currentLevelRelations.has(r)) as (keyof T)[])
: []),
]); ]);
const resultDto = OmitType(this.entityClass, [...outputFieldsToOmit]); const resultDto = OmitType(this.entityClass, [...outputFieldsToOmit]);
const relations = getTypeormRelations(this.entityClass);
for (const relation of relations) { for (const relation of relations) {
if (outputFieldsToOmit.has(relation.propertyName as keyof T)) continue; if (outputFieldsToOmit.has(relation.propertyName as keyof T)) continue;
const replace = (useClass: [AnyClass]) => { const replace = (useClass: [AnyClass]) => {
...@@ -119,21 +149,36 @@ export class RestfulFactory<T> { ...@@ -119,21 +149,36 @@ export class RestfulFactory<T> {
if (existing) { if (existing) {
replace(existing); replace(existing);
} else { } else {
if (!this.__resolveVisited.has(this.entityClass)) { if (
!this.__resolveVisited.has(this.entityClass) &&
!this.options.relations
) {
this.__resolveVisited.set(this.entityClass, [null]); this.__resolveVisited.set(this.entityClass, [null]);
} }
const relationFactory = new RestfulFactory( const relationFactory = new RestfulFactory(
relation.propertyClass, relation.propertyClass,
{ {
entityClassName: `${this.getEntityClassName()}${ entityClassName: `${this.getEntityClassName()}${
relation.propertyClass.name this.options.relations
? upperFirst(relation.propertyName)
: relation.propertyClass.name
}`, }`,
relations:
this.options.relations &&
getNextLevelRelations(
this.options.relations.map(extractRelationName),
relation.propertyName,
),
}, },
this.__resolveVisited, this.__resolveVisited,
); );
const relationResultDto = relationFactory.entityResultDto; const relationResultDto = relationFactory.entityResultDto;
replace([relationResultDto]); replace([relationResultDto]);
this.__resolveVisited.set(relation.propertyClass, [relationResultDto]); if (!this.options.relations) {
this.__resolveVisited.set(relation.propertyClass, [
relationResultDto,
]);
}
} }
} }
const res = RenameClass( const res = RenameClass(
...@@ -318,7 +363,7 @@ export class RestfulFactory<T> { ...@@ -318,7 +363,7 @@ export class RestfulFactory<T> {
summary: `Import ${this.getEntityClassName()}`, summary: `Import ${this.getEntityClassName()}`,
...extras, ...extras,
}), }),
ApiBody({ type: ImportDataDto(this.createDto) }), ApiBody({ type: this.importDto }),
ApiOkResponse({ type: this.importReturnMessageDto }), ApiOkResponse({ type: this.importReturnMessageDto }),
ApiInternalServerErrorResponse({ ApiInternalServerErrorResponse({
type: BlankReturnMessageDto, type: BlankReturnMessageDto,
......
import { AnyClass, ClassType } from 'nesties'; import { AnyClass, ClassType } from 'nesties';
import { getMetadataArgsStorage } from 'typeorm'; import { getMetadataArgsStorage } from 'typeorm';
import { getSpecificFields, reflector } from './metadata';
import _ from 'lodash';
export function getTypeormRelations<T>(cl: ClassType<T>) { export function getTypeormRelations<T>(cl: ClassType<T>) {
const relations = getMetadataArgsStorage().relations.filter( const relations = getMetadataArgsStorage().relations.filter(
(r) => r.target === cl, (r) => r.target === cl,
); );
return relations.map((relation) => { const typeormRelations = relations.map((relation) => {
const isArray = relation.relationType.endsWith('-many'); const isArray = relation.relationType.endsWith('-many');
const relationClassFactory = relation.type; const relationClassFactory = relation.type;
// check if it's a callable function // check if it's a callable function
...@@ -30,6 +32,22 @@ export function getTypeormRelations<T>(cl: ClassType<T>) { ...@@ -30,6 +32,22 @@ export function getTypeormRelations<T>(cl: ClassType<T>) {
propertyName: relation.propertyName, propertyName: relation.propertyName,
}; };
}); });
const computedRelations = getSpecificFields(cl, 'relationComputed').map(
(field) => {
const meta = reflector.get('relationComputed', cl, field);
const res = meta();
return {
isArray: res.isArray,
propertyClass: res.entityClass,
propertyName: field,
};
},
);
return _.uniqBy(
[...typeormRelations, ...computedRelations], // Merge typeorm relations and computed relations
(r) => r.propertyName,
);
} }
export function getTypeormRelationsMap<T>(cl: ClassType<T>) { export function getTypeormRelationsMap<T>(cl: ClassType<T>) {
......
import { MetadataSetter, Reflector } from 'typed-reflector'; import { MetadataSetter, Reflector } from 'typed-reflector';
import { QueryCond } from '../bases'; import { QueryCond } from '../bases';
import { AnyClass } from 'nesties';
interface SpecificFields { interface SpecificFields {
notColumn: boolean; notColumn: boolean;
...@@ -8,6 +9,7 @@ interface SpecificFields { ...@@ -8,6 +9,7 @@ interface SpecificFields {
notQueryable: boolean; notQueryable: boolean;
notInResult: boolean; notInResult: boolean;
entityVersioningDate: boolean; entityVersioningDate: boolean;
relationComputed: () => { entityClass: AnyClass; isArray: boolean };
} }
interface MetadataMap extends SpecificFields { interface MetadataMap extends SpecificFields {
......
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