Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
N
nicot
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
3rdeye
nicot
Commits
bf7e9005
Commit
bf7e9005
authored
Apr 23, 2025
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add relations to RestfulFactory
parent
a34f5c63
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
105 additions
and
21 deletions
+105
-21
README.md
README.md
+11
-8
src/decorators/property.ts
src/decorators/property.ts
+21
-5
src/decorators/restful.ts
src/decorators/restful.ts
+52
-7
src/utility/get-typeorm-relations.ts
src/utility/get-typeorm-relations.ts
+19
-1
src/utility/metadata.ts
src/utility/metadata.ts
+2
-0
No files found.
README.md
View file @
bf7e9005
...
...
@@ -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: [] }`
以裁剪所有关系字段。
---
...
...
src/decorators/property.ts
View file @
bf7e9005
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
<
1
e12
;
return
timestampToDate
(
value
,
isSeconds
);
}
if
(
typeof
value
===
'
string
'
&&
/^
\d
+
(\.\d
+
)?
$/
.
test
(
value
))
{
const
isSeconds
=
value
.
includes
(
'
.
'
)
||
parseFloat
(
value
)
<
1
e12
;
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
);
};
src/decorators/restful.ts
View file @
bf7e9005
...
...
@@ -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
,
...
...
src/utility/get-typeorm-relations.ts
View file @
bf7e9005
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
>
)
{
...
...
src/utility/metadata.ts
View file @
bf7e9005
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
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment