@buildvision/nest-sdk-generator 1.2.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +63 -0
- package/README.md +2 -2
- package/build/analyzer/classdeps.js +1 -1
- package/build/analyzer/controller.d.ts +4 -4
- package/build/analyzer/controller.js +3 -3
- package/build/analyzer/controllers.d.ts +1 -1
- package/build/analyzer/controllers.js +10 -2
- package/build/analyzer/decorator.d.ts +2 -2
- package/build/analyzer/decorator.js +19 -19
- package/build/analyzer/extractor.d.ts +4 -4
- package/build/analyzer/extractor.js +6 -17
- package/build/analyzer/index.d.ts +1 -1
- package/build/analyzer/index.js +2 -2
- package/build/analyzer/methods.d.ts +7 -7
- package/build/analyzer/methods.js +6 -9
- package/build/analyzer/module.js +2 -2
- package/build/analyzer/params.d.ts +59 -17
- package/build/analyzer/params.js +150 -129
- package/build/analyzer/route.d.ts +5 -2
- package/build/analyzer/route.js +13 -7
- package/build/analyzer/typedeps.d.ts +6 -6
- package/build/analyzer/typedeps.js +3 -3
- package/build/bin.js +9 -13
- package/build/config.d.ts +29 -19
- package/build/config.js +2 -2
- package/build/generator/genmodules.d.ts +33 -10
- package/build/generator/genmodules.js +165 -113
- package/build/generator/gentypes.js +1 -1
- package/build/generator/index.d.ts +1 -1
- package/build/generator/index.js +23 -22
- package/build/generator/prettier.d.ts +1 -1
- package/build/generator/prettier.js +2 -2
- package/build/generator/sdk-interface.d.ts +1 -1
- package/build/generator/sdk-interface.js +5 -5
- package/build/logging.js +4 -2
- package/build/rtk-query-generator/genmodules.d.ts +54 -0
- package/build/rtk-query-generator/genmodules.js +252 -0
- package/build/rtk-query-generator/gentypes.d.ts +10 -0
- package/build/rtk-query-generator/gentypes.js +62 -0
- package/build/rtk-query-generator/index.d.ts +6 -0
- package/build/rtk-query-generator/index.js +84 -0
- package/build/rtk-query-generator/prettier.d.ts +16 -0
- package/build/rtk-query-generator/prettier.js +43 -0
- package/build/rtk-query-generator/sdk-interface.d.ts +1 -0
- package/build/rtk-query-generator/sdk-interface.js +36 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils.js +1 -1
- package/bun.lockb +0 -0
- package/package.json +16 -5
- package/src/analyzer/classdeps.ts +3 -0
- package/src/analyzer/controller.ts +12 -6
- package/src/analyzer/controllers.ts +16 -1
- package/src/analyzer/extractor.ts +12 -23
- package/src/analyzer/index.ts +5 -2
- package/src/analyzer/methods.ts +12 -13
- package/src/analyzer/module.ts +4 -10
- package/src/analyzer/params.ts +212 -182
- package/src/analyzer/route.ts +10 -4
- package/src/analyzer/typedeps.ts +13 -11
- package/src/bin.ts +11 -13
- package/src/config.ts +35 -24
- package/src/generator/genmodules.ts +212 -129
- package/src/generator/gentypes.ts +1 -0
- package/src/generator/index.ts +23 -23
- package/src/generator/prettier.ts +4 -2
- package/src/logging.ts +4 -1
- package/src/rtk-query-generator/genmodules.ts +335 -0
- package/src/rtk-query-generator/gentypes.ts +74 -0
- package/src/rtk-query-generator/index.ts +92 -0
- package/src/rtk-query-generator/prettier.ts +48 -0
- package/src/rtk-query-generator/sdk-interface.ts +32 -0
- package/src/utils.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -0
- package/src/analyzer/decorator.ts +0 -29
package/src/analyzer/methods.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { blue } from 'chalk'
|
|
6
6
|
import { ClassDeclaration, MethodDeclaration, Node } from 'ts-morph'
|
|
7
|
+
|
|
7
8
|
import { debug, format } from '../logging'
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
import { extractParams, SdkMethodParams } from './params'
|
|
9
11
|
import { analyzeUri, debugUri, Route } from './route'
|
|
10
12
|
import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
|
|
11
13
|
|
|
@@ -13,11 +15,14 @@ import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
|
|
|
13
15
|
* SDK interface for a single controller's method
|
|
14
16
|
*/
|
|
15
17
|
export interface SdkMethod {
|
|
18
|
+
/** Method's HTTP method (e.g. GET / POST) */
|
|
19
|
+
readonly httpMethod: SdkHttpMethod
|
|
20
|
+
|
|
16
21
|
/** Method's name */
|
|
17
22
|
readonly name: string
|
|
18
23
|
|
|
19
|
-
/** Method's
|
|
20
|
-
readonly
|
|
24
|
+
/** Method's parameters */
|
|
25
|
+
readonly params: SdkMethodParams
|
|
21
26
|
|
|
22
27
|
/** Method's return type with resolved dependencies */
|
|
23
28
|
readonly returnType: ResolvedTypeDeps
|
|
@@ -27,20 +32,17 @@ export interface SdkMethod {
|
|
|
27
32
|
|
|
28
33
|
/** Method's URI path */
|
|
29
34
|
readonly uriPath: string
|
|
30
|
-
|
|
31
|
-
/** Method's parameters */
|
|
32
|
-
readonly params: SdkMethodParams
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* HTTP method of a controller's method
|
|
37
39
|
*/
|
|
38
40
|
export enum SdkHttpMethod {
|
|
41
|
+
Delete = 'DELETE',
|
|
39
42
|
Get = 'GET',
|
|
43
|
+
Patch = 'PATCH',
|
|
40
44
|
Post = 'POST',
|
|
41
45
|
Put = 'PUT',
|
|
42
|
-
Patch = 'PATCH',
|
|
43
|
-
Delete = 'DELETE',
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -85,8 +87,7 @@ export function analyzeMethods(
|
|
|
85
87
|
// Get the HTTP decorator
|
|
86
88
|
const dec = decorators[0]
|
|
87
89
|
|
|
88
|
-
//
|
|
89
|
-
// @ts-ignore
|
|
90
|
+
// @ts-expect-error TypeScript doesn't like indexing an enumeration with a string key, although this works fine
|
|
90
91
|
const httpMethod = SdkHttpMethod[dec.getName()]
|
|
91
92
|
|
|
92
93
|
debug('├─── Detected HTTP method: {magentaBright}', httpMethod.toLocaleUpperCase())
|
|
@@ -141,9 +142,7 @@ export function analyzeMethods(
|
|
|
141
142
|
|
|
142
143
|
// Analyze the method's arguments
|
|
143
144
|
debug('├─── Analyzing arguments...')
|
|
144
|
-
const params =
|
|
145
|
-
|
|
146
|
-
if (params instanceof Error) return params
|
|
145
|
+
const params = extractParams({ controllerClass, httpMethod, route, method, filePath, absoluteSrcPath })
|
|
147
146
|
|
|
148
147
|
// Get the method's return type
|
|
149
148
|
debug('├─── Resolving return type...')
|
package/src/analyzer/module.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as path from 'path'
|
|
6
|
+
|
|
6
7
|
import { Node, Project } from 'ts-morph'
|
|
8
|
+
|
|
7
9
|
import { format, panic } from '../logging'
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -36,16 +38,8 @@ export function getModuleName(project: Project, modulePath: string, sourcePath:
|
|
|
36
38
|
const moduleDecorator = decorators.find((dec) => dec.getName() === 'Module')
|
|
37
39
|
|
|
38
40
|
if (!moduleDecorator) {
|
|
39
|
-
panic(
|
|
40
|
-
format(
|
|
41
|
-
`Module class {yellow} is missing {yellow} decorator\nModule path is: {yellow}`,
|
|
42
|
-
moduleName,
|
|
43
|
-
'@Module',
|
|
44
|
-
modulePath
|
|
45
|
-
)
|
|
46
|
-
)
|
|
41
|
+
panic(format(`Module class {yellow} is missing {yellow} decorator\nModule path is: {yellow}`, moduleName, '@Module', modulePath))
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
return moduleName.substr(0, 1).toLocaleLowerCase() + moduleName.substr(1)
|
|
44
|
+
return moduleName
|
|
51
45
|
}
|
package/src/analyzer/params.ts
CHANGED
|
@@ -2,31 +2,89 @@
|
|
|
2
2
|
* @file Analyzer for the source API's controllers' methods' parameters
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ParameterDeclaration } from 'ts-morph'
|
|
6
|
-
|
|
7
|
-
import {
|
|
5
|
+
import { ClassDeclaration, Decorator, MethodDeclaration, Node, ParameterDeclaration, Type } from 'ts-morph'
|
|
6
|
+
|
|
7
|
+
import { debug, panic } from '../logging'
|
|
8
|
+
|
|
8
9
|
import { SdkHttpMethod } from './methods'
|
|
9
|
-
import {
|
|
10
|
+
import { paramsFromRoute, Route } from './route'
|
|
10
11
|
import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Type of a parameter that must be passed to an SDK method: Body, Query or Route Param
|
|
15
|
+
* Supports accessing parameters individually by key or as a whole object
|
|
16
|
+
*
|
|
17
|
+
* EXAMPLE
|
|
18
|
+
*
|
|
19
|
+
* Controller Method:
|
|
20
|
+
*
|
|
21
|
+
* getStuff(@Query('email') email: string, @Query('companyId'): CompanyId)
|
|
22
|
+
*
|
|
23
|
+
* SDK Method Param:
|
|
24
|
+
* { email: string, companyId: CompanyId }
|
|
25
|
+
*
|
|
26
|
+
* OR
|
|
27
|
+
*
|
|
28
|
+
* Controller Method:
|
|
29
|
+
* type EmailAndCompanyId = { email: string, companyId: CompanyId }
|
|
30
|
+
* getStuff(@Query(): EmailAndCompanyId)
|
|
31
|
+
*
|
|
32
|
+
* SDK Method Param:
|
|
33
|
+
* EmailAndCompanyId
|
|
14
34
|
*/
|
|
15
|
-
export interface SdkMethodParams {
|
|
16
|
-
/** Route parameters */
|
|
17
|
-
parameters: Map<string, ResolvedTypeDeps> | null
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
export type SdkMethodParam = SdkMethodParamMap | ResolvedTypeDeps | null
|
|
37
|
+
export type SdkMethodParamMap = { [key: string]: ResolvedTypeDeps }
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
export const isParamResolvedType = (param: SdkMethodParam): param is ResolvedTypeDeps => {
|
|
40
|
+
if (param === null) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
return 'rawType' in param && 'resolvedType' in param && 'relativeFilePath' in param && 'dependencies' in param && 'localTypes' in param
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const getParamResolvedTypes = (param: SdkMethodParam): ResolvedTypeDeps[] => {
|
|
47
|
+
if (param === null) {
|
|
48
|
+
return []
|
|
49
|
+
}
|
|
50
|
+
if (isParamResolvedType(param)) {
|
|
51
|
+
return [param]
|
|
52
|
+
}
|
|
53
|
+
return Object.values(param)
|
|
24
54
|
}
|
|
25
55
|
|
|
26
56
|
/**
|
|
27
|
-
*
|
|
57
|
+
* SDK interface for a controller's method's parameters
|
|
28
58
|
*/
|
|
29
|
-
export
|
|
59
|
+
export interface SdkMethodParams {
|
|
60
|
+
bodyParams: SdkMethodParam
|
|
61
|
+
context: MethodContext
|
|
62
|
+
queryParams: SdkMethodParam
|
|
63
|
+
routeParams: SdkMethodParam
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type MethodContext = {
|
|
67
|
+
absoluteSrcPath: string
|
|
68
|
+
controllerClass: ClassDeclaration
|
|
69
|
+
filePath: string
|
|
70
|
+
httpMethod: SdkHttpMethod
|
|
71
|
+
method: MethodDeclaration
|
|
72
|
+
route: Route
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export enum ArgDecorator {
|
|
76
|
+
Body = 'Body',
|
|
77
|
+
Param = 'Param',
|
|
78
|
+
Query = 'Query',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type DecoratedArg = {
|
|
82
|
+
arg: ParameterDeclaration
|
|
83
|
+
argParamKey: string | null
|
|
84
|
+
context: MethodContext
|
|
85
|
+
decorator: Decorator
|
|
86
|
+
decoratorType: ArgDecorator
|
|
87
|
+
}
|
|
30
88
|
|
|
31
89
|
/**
|
|
32
90
|
* Generate a SDK interface for a controller's method's parameters
|
|
@@ -37,205 +95,177 @@ export type SdkMethodBodyParam = { full: true; type: ResolvedTypeDeps } | { full
|
|
|
37
95
|
* @param absoluteSrcPath Absolute path to the source directory
|
|
38
96
|
* @returns A SDK interface for the method's parameters
|
|
39
97
|
*/
|
|
40
|
-
export function
|
|
41
|
-
httpMethod
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const collected: SdkMethodParams = {
|
|
49
|
-
parameters: null,
|
|
50
|
-
query: null,
|
|
51
|
-
body: null,
|
|
98
|
+
export function extractParams(context: MethodContext): SdkMethodParams {
|
|
99
|
+
const { httpMethod, route } = context
|
|
100
|
+
const { Body, Param, Query } = extractDecoratedArgs(context)
|
|
101
|
+
const resolvedParams: SdkMethodParams = {
|
|
102
|
+
routeParams: mergeDecoratedArgs(Param),
|
|
103
|
+
queryParams: mergeDecoratedArgs(Query),
|
|
104
|
+
bodyParams: mergeDecoratedArgs(Body),
|
|
105
|
+
context,
|
|
52
106
|
}
|
|
53
107
|
|
|
54
|
-
|
|
55
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Validate Body Params are not used in GET requests
|
|
110
|
+
*/
|
|
111
|
+
if (httpMethod === SdkHttpMethod.Get && resolvedParams.bodyParams) {
|
|
112
|
+
panicWithContext(`${httpMethod} request should not have @Body params, use @Query params instead`, context)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validate Query Params are not used in non-GET requests
|
|
117
|
+
*/
|
|
118
|
+
if (httpMethod !== SdkHttpMethod.Get && resolvedParams.queryParams) {
|
|
119
|
+
panicWithContext(`${httpMethod} request should not have @Query params, use @Body params instead`, context)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate Route Params exist in route URL
|
|
124
|
+
*/
|
|
125
|
+
const { routeParams } = resolvedParams
|
|
126
|
+
if (routeParams) {
|
|
127
|
+
const allowedRouteParams = new Set(paramsFromRoute(route))
|
|
128
|
+
const usedRouteParams =
|
|
129
|
+
routeParams instanceof Type
|
|
130
|
+
? // If routeParams was resolved to a single type, assume there's only one decorated argument
|
|
131
|
+
Param[0].arg
|
|
132
|
+
.getType()
|
|
133
|
+
.getProperties()
|
|
134
|
+
.map((prop) => prop.getName())
|
|
135
|
+
: // If routeParams was resolved to a map, assume the keys are the route params
|
|
136
|
+
Object.keys(routeParams)
|
|
137
|
+
|
|
138
|
+
for (const usedParam of usedRouteParams) {
|
|
139
|
+
if (!allowedRouteParams.has(usedParam)) {
|
|
140
|
+
panicWithContext(`Route param ${usedParam} does not appear in route URL`, context)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return resolvedParams
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function extractDecoratedArgs(context: MethodContext): Record<ArgDecorator, DecoratedArg[]> {
|
|
149
|
+
const decoratedArgs = {
|
|
150
|
+
[ArgDecorator.Param]: [] as DecoratedArg[],
|
|
151
|
+
[ArgDecorator.Query]: [] as DecoratedArg[],
|
|
152
|
+
[ArgDecorator.Body]: [] as DecoratedArg[],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { controllerClass, method } = context
|
|
156
|
+
const args = method.getParameters()
|
|
56
157
|
|
|
57
|
-
// Treat all arguments (to not confuse with the route's parameters)
|
|
58
158
|
for (const arg of args) {
|
|
59
|
-
const
|
|
159
|
+
const argName = arg.getName()
|
|
60
160
|
|
|
61
|
-
debug('├───── Detected argument: {yellow}',
|
|
161
|
+
debug('├───── Detected argument: {yellow}', argName)
|
|
62
162
|
|
|
63
163
|
// Arguments are collected as soon as they have a decorator like @Query() or @Body()
|
|
64
|
-
const
|
|
164
|
+
const argDecorators = arg.getDecorators().filter((dec) => (Object.values(ArgDecorator) as string[]).includes(dec.getName()))
|
|
65
165
|
|
|
66
|
-
if (
|
|
166
|
+
if (argDecorators.length === 0) {
|
|
67
167
|
// If we have no argument, this is not an argument we are interested in, so we just skip it
|
|
68
168
|
debug('├───── Skipping this argument as it does not have a decorator')
|
|
69
169
|
continue
|
|
70
|
-
} else if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
170
|
+
} else if (argDecorators.length > 1) {
|
|
171
|
+
panic(
|
|
172
|
+
`${controllerClass.getName()} ${method.getName()} has multiple decorators on argument ${argName} ${argDecorators.map((dec) => dec.getName()).join(', ')}`
|
|
173
|
+
)
|
|
74
174
|
}
|
|
75
|
-
|
|
76
175
|
// Get the only decrator
|
|
77
|
-
const
|
|
78
|
-
const
|
|
176
|
+
const decorator = argDecorators[0]
|
|
177
|
+
const decoratorType = decorator.getName() as ArgDecorator
|
|
178
|
+
const argParamKey = extractArgParamKey({ ...context, arg, decorator })
|
|
179
|
+
const argType = arg.getType()
|
|
180
|
+
|
|
181
|
+
if (!argParamKey && !argType.isObject()) {
|
|
182
|
+
panic(
|
|
183
|
+
`${controllerClass.getName()} ${method.getName()} generic controller argument ${decoratorType}() ${argName} must be an object type`
|
|
184
|
+
)
|
|
185
|
+
}
|
|
79
186
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
187
|
+
decoratedArgs[decoratorType].push({
|
|
188
|
+
arg,
|
|
189
|
+
argParamKey,
|
|
190
|
+
decorator,
|
|
191
|
+
decoratorType,
|
|
192
|
+
context,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
83
195
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const paramName = expectSingleStrLitDecorator(dec)
|
|
196
|
+
return decoratedArgs
|
|
197
|
+
}
|
|
87
198
|
|
|
88
|
-
|
|
199
|
+
export function extractArgParamKey(
|
|
200
|
+
context: MethodContext & {
|
|
201
|
+
arg: ParameterDeclaration
|
|
202
|
+
decorator: Decorator
|
|
203
|
+
}
|
|
204
|
+
): string | null {
|
|
205
|
+
const { arg, controllerClass, decorator, method } = context
|
|
206
|
+
const decoratorArgs = decorator.getArguments()
|
|
207
|
+
|
|
208
|
+
if (decoratorArgs.length > 1) {
|
|
209
|
+
panic(
|
|
210
|
+
`${controllerClass.getName()} ${method.getName()} ${decorator.getName()}() ${arg.getName()} argument decorator has multiple parameters`
|
|
211
|
+
)
|
|
212
|
+
} else if (decoratorArgs.length === 0) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
89
215
|
|
|
90
|
-
|
|
91
|
-
// We theorically *could* extract the type informations from this object type, but this would be insanely complex
|
|
92
|
-
// So, we just skip it as it's a lot more simple, and is not commonly used anyway as it has a set of downsides
|
|
93
|
-
if (paramName === null) {
|
|
94
|
-
warn('├───── Skipping this argument as it is a generic parameters receiver, which is currently not supported')
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
216
|
+
const decoratorArg = decoratorArgs[0]
|
|
97
217
|
|
|
98
|
-
|
|
99
|
-
|
|
218
|
+
if (!Node.isStringLiteral(decoratorArg)) {
|
|
219
|
+
panic('The argument provided to the decorator is not a string literal:\n>>> {cyan}', arg.getText())
|
|
220
|
+
}
|
|
100
221
|
|
|
101
|
-
|
|
222
|
+
const paramKey = decoratorArg.getLiteralText()
|
|
223
|
+
if (paramKey in {}) {
|
|
224
|
+
panicWithContext(`${decorator.getName()}('${paramKey}') param name collides with JavaScript native object property`, context)
|
|
225
|
+
}
|
|
102
226
|
|
|
103
|
-
|
|
104
|
-
|
|
227
|
+
return paramKey
|
|
228
|
+
}
|
|
105
229
|
|
|
106
|
-
|
|
230
|
+
export function mergeDecoratedArgs(decoratedArgs: DecoratedArg[]): SdkMethodParam {
|
|
231
|
+
let genericParam: SdkMethodParam | null = null
|
|
232
|
+
const paramMap: SdkMethodParamMap = {}
|
|
107
233
|
|
|
108
|
-
|
|
234
|
+
for (const decoratedArg of decoratedArgs) {
|
|
235
|
+
const { arg, argParamKey, context, decoratorType } = decoratedArg
|
|
236
|
+
const resolvedType = resolveTypeDependencies(arg.getType(), context.filePath, context.absoluteSrcPath)
|
|
109
237
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
238
|
+
if (argParamKey) {
|
|
239
|
+
if (paramMap[argParamKey]) {
|
|
240
|
+
panicWithContext(`${decoratorType}('${argParamKey}') used twice in controller method`, decoratedArg.context)
|
|
114
241
|
}
|
|
115
242
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Treat the @Query() decorator
|
|
121
|
-
else if (decName === 'Query') {
|
|
122
|
-
debug('├───── Detected decorator {blue}', '@Query')
|
|
123
|
-
|
|
124
|
-
// We expect a single string argument for this decorator,
|
|
125
|
-
// which is the query parameter's name
|
|
126
|
-
const queryName = expectSingleStrLitDecorator(dec)
|
|
127
|
-
|
|
128
|
-
if (queryName instanceof Error) return queryName
|
|
129
|
-
|
|
130
|
-
// If there is no argument, this argument is a global receiver which maps the full set of parameters
|
|
131
|
-
// We theorically *could* extract the type informations from this object type, but this would be insanely complex
|
|
132
|
-
// So, we just skip it as it's a lot more simple, and is not commonly used anyway as it has a set of downsides
|
|
133
|
-
if (queryName === null) {
|
|
134
|
-
warn('├───── Skipping this argument as it is a generic query receiver')
|
|
135
|
-
continue
|
|
243
|
+
paramMap[argParamKey] = resolvedType
|
|
244
|
+
} else {
|
|
245
|
+
if (genericParam) {
|
|
246
|
+
panicWithContext(`${decoratorType}() used twice in controller method`, decoratedArg.context)
|
|
136
247
|
}
|
|
137
248
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Get the parameter's type
|
|
141
|
-
const typ = resolveTypeDependencies(arg.getType(), filePath, absoluteSrcPath)
|
|
142
|
-
|
|
143
|
-
debug(`├───── Detected query type: {yellow} ({magentaBright} dependencies)`, typ.resolvedType, typ.dependencies.size)
|
|
144
|
-
|
|
145
|
-
// Update the method's query parameter
|
|
146
|
-
|
|
147
|
-
if (queryName in {}) {
|
|
148
|
-
return new Error(
|
|
149
|
-
format(`Detected @Query() field whose name {yellow} collides with a JavaScript's native object property`, queryName)
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
collected.query ??= new Map()
|
|
154
|
-
collected.query.set(queryName, typ)
|
|
249
|
+
genericParam = resolvedType
|
|
155
250
|
}
|
|
251
|
+
}
|
|
156
252
|
|
|
157
|
-
|
|
158
|
-
else if (decName === 'Body') {
|
|
159
|
-
debug('├───── Detected decorator {blue}', '@Body')
|
|
160
|
-
|
|
161
|
-
// GET requests cannot have a BODY
|
|
162
|
-
if (httpMethod === SdkHttpMethod.Get) {
|
|
163
|
-
return new Error('GET requests cannot have a BODY!')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// We expect a single string argument for this decorator,
|
|
167
|
-
// which is the body field's name
|
|
168
|
-
const fieldName = expectSingleStrLitDecorator(dec)
|
|
169
|
-
|
|
170
|
-
if (fieldName instanceof Error) return fieldName
|
|
171
|
-
|
|
172
|
-
// Get the field's type
|
|
173
|
-
const typ = resolveTypeDependencies(arg.getType(), filePath, absoluteSrcPath)
|
|
253
|
+
const hasArgsByKey = Object.keys(paramMap).length > 0
|
|
174
254
|
|
|
175
|
-
|
|
255
|
+
if (genericParam && hasArgsByKey) {
|
|
256
|
+
panicWithContext(`Cannot mix generic and specific ${decoratedArgs[0].decoratorType}()`, decoratedArgs[0].context)
|
|
257
|
+
}
|
|
176
258
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
depsCount === 0 ? 'no dependency' : depsCount > 1 ? 'dependencies' : 'dependency'
|
|
180
|
-
})`,
|
|
181
|
-
typ.resolvedType,
|
|
182
|
-
depsCount
|
|
183
|
-
)
|
|
259
|
+
return hasArgsByKey ? paramMap : genericParam
|
|
260
|
+
}
|
|
184
261
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// => It's easy to make an error as the @Body() type is often hidden behind a DTO
|
|
193
|
-
// But that's extremely complex to check automatically, so we just display a warning instead
|
|
194
|
-
// Also, that's not the kind of thing we make on purpose very often, so it's more likely it's an error, which makes it even more important
|
|
195
|
-
// to display a warning here.
|
|
196
|
-
if (body?.full === false)
|
|
197
|
-
warn('├───── Detected full @Body() decorator after a single parameter. This is considered a bad practice, avoid it if you can!')
|
|
198
|
-
// Having two generic @Body() decorators is meaningless and will likey lead to errors, so we return a precise error here
|
|
199
|
-
else if (body?.full) {
|
|
200
|
-
return new Error(
|
|
201
|
-
format(
|
|
202
|
-
`Detected two @Body() decorators: found {yellow} previously, while method argument {yellow} indicates type {yellow}`,
|
|
203
|
-
body.type.resolvedType,
|
|
204
|
-
name,
|
|
205
|
-
typ.resolvedType
|
|
206
|
-
)
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
debug("├───── Mapping argument to full request's body")
|
|
211
|
-
|
|
212
|
-
// Update the whole BODY type
|
|
213
|
-
collected.body = { full: true, type: typ }
|
|
214
|
-
} else {
|
|
215
|
-
// Here we have an @Body(<string>) decorator
|
|
216
|
-
|
|
217
|
-
// If we previously had an @Body() decorator, this can lead to several types of errors (see the big comment above for more informations)
|
|
218
|
-
if (collected.body?.full) {
|
|
219
|
-
warn('├───── Detected single @Body() decorator after a full parameter. This is considered a bad practice, avoid it if you can!')
|
|
220
|
-
} else {
|
|
221
|
-
debug('├───── Mapping argument to BODY field: {yellow}', fieldName)
|
|
222
|
-
|
|
223
|
-
// Update the BODY type by adding the current field to it
|
|
224
|
-
|
|
225
|
-
if (fieldName in {}) {
|
|
226
|
-
return new Error(
|
|
227
|
-
format(`Detected @Body() field whose name {yellow} collides with a JavaScript's native object property`, fieldName)
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
collected.body ??= { full: false, fields: new Map() }
|
|
232
|
-
|
|
233
|
-
collected.body.fields.set(fieldName, typ)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
262
|
+
const panicWithContext = (message: string, context: MethodContext) => {
|
|
263
|
+
const { controllerClass, filePath, httpMethod, method } = context
|
|
264
|
+
const ctx = {
|
|
265
|
+
controller: controllerClass.getName(),
|
|
266
|
+
method: method.getName(),
|
|
267
|
+
httpMethod,
|
|
268
|
+
filePath,
|
|
237
269
|
}
|
|
238
|
-
|
|
239
|
-
// Success!
|
|
240
|
-
return collected
|
|
270
|
+
panic(`${message}\n${JSON.stringify(ctx, null, 2)}`)
|
|
241
271
|
}
|
package/src/analyzer/route.ts
CHANGED
|
@@ -51,6 +51,7 @@ export function analyzeUri(uriPath: string): Route | Error {
|
|
|
51
51
|
if (part === '') continue
|
|
52
52
|
|
|
53
53
|
// Ensure there is no generic character in the path as we don't support them
|
|
54
|
+
// eslint-disable-next-line no-useless-escape
|
|
54
55
|
const genericErr = _findInvalid(part, /[\*\+\?]/)
|
|
55
56
|
|
|
56
57
|
if (genericErr) {
|
|
@@ -90,8 +91,12 @@ export function analyzeUri(uriPath: string): Route | Error {
|
|
|
90
91
|
* Get the named parameters of a parsed route
|
|
91
92
|
* @param route
|
|
92
93
|
*/
|
|
93
|
-
export function
|
|
94
|
-
return
|
|
94
|
+
export function routePartIsParam(part: RoutePart): part is { readonly param: string } {
|
|
95
|
+
return 'param' in part
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function paramsFromRoute(route: Route): string[] {
|
|
99
|
+
return route.parts.filter(routePartIsParam).map((part) => part.param)
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
/**
|
|
@@ -115,11 +120,12 @@ export function debugUri(route: Route, color: (str: string) => string): string {
|
|
|
115
120
|
* @param params
|
|
116
121
|
*/
|
|
117
122
|
export function resolveRoute(route: Route, params: { [name: string]: string }): string | Error {
|
|
118
|
-
|
|
123
|
+
const uri: string[] = []
|
|
119
124
|
|
|
120
125
|
for (const part of route.parts) {
|
|
121
126
|
if ('segment' in part) {
|
|
122
127
|
uri.push(part.segment)
|
|
128
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
123
129
|
} else if (!params.hasOwnProperty(part.param)) {
|
|
124
130
|
return new Error('Missing route parameter ' + part.param)
|
|
125
131
|
} else {
|
|
@@ -136,7 +142,7 @@ export function resolveRoute(route: Route, params: { [name: string]: string }):
|
|
|
136
142
|
* @param paramsProvider
|
|
137
143
|
*/
|
|
138
144
|
export function resolveRouteWith(route: Route, paramsProvider: (param: string) => string | null): string | Error {
|
|
139
|
-
|
|
145
|
+
const uri: string[] = []
|
|
140
146
|
|
|
141
147
|
for (const part of route.parts) {
|
|
142
148
|
if ('segment' in part) {
|
package/src/analyzer/typedeps.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as path from 'path'
|
|
6
|
+
|
|
6
7
|
import { ts, Type } from 'ts-morph'
|
|
8
|
+
|
|
7
9
|
import { unreachable } from '../logging'
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -15,15 +17,6 @@ export const IMPORTED_TYPE_REGEX = /\bimport\(['"]([^'"]+)['"]\)\.([a-zA-Z0-9_]+
|
|
|
15
17
|
* Type with resolved level 1 dependencies (does not look for dependencies' own dependencies!)
|
|
16
18
|
*/
|
|
17
19
|
export interface ResolvedTypeDeps {
|
|
18
|
-
/** The original raw type, with import("...") expressions */
|
|
19
|
-
readonly rawType: string
|
|
20
|
-
|
|
21
|
-
/** The resolved type, without imports */
|
|
22
|
-
readonly resolvedType: string
|
|
23
|
-
|
|
24
|
-
/** File from which the type originates */
|
|
25
|
-
readonly relativeFilePath: string
|
|
26
|
-
|
|
27
20
|
/**
|
|
28
21
|
* The type dependencies used by the resolved type
|
|
29
22
|
* A collection of scripts' relative paths mapped with the list of types imported from them
|
|
@@ -35,6 +28,15 @@ export interface ResolvedTypeDeps {
|
|
|
35
28
|
* They may be either local types (declared in the same file than the one analyzed) or globally-defined types
|
|
36
29
|
*/
|
|
37
30
|
readonly localTypes: string[]
|
|
31
|
+
|
|
32
|
+
/** The original raw type, with import("...") expressions */
|
|
33
|
+
readonly rawType: string
|
|
34
|
+
|
|
35
|
+
/** File from which the type originates */
|
|
36
|
+
readonly relativeFilePath: string
|
|
37
|
+
|
|
38
|
+
/** The resolved type, without imports */
|
|
39
|
+
readonly resolvedType: string
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -55,8 +57,8 @@ export function resolveTypeDependencies(type: Type<ts.Type>, relativeFilePath: s
|
|
|
55
57
|
)
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
const dependencies: ResolvedTypeDeps['dependencies'] = new Map()
|
|
61
|
+
const localTypes: ResolvedTypeDeps['localTypes'] = []
|
|
60
62
|
|
|
61
63
|
/** Resolved type (without import statements) */
|
|
62
64
|
const resolvedType: ResolvedTypeDeps['resolvedType'] = rawType.replace(IMPORTED_TYPE_REGEX, (_, matchedFilePath, type) => {
|