Commit bf7e9005 authored by nanahira's avatar nanahira

add relations to RestfulFactory

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