@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.
Files changed (74) hide show
  1. package/.eslintrc.js +63 -0
  2. package/README.md +2 -2
  3. package/build/analyzer/classdeps.js +1 -1
  4. package/build/analyzer/controller.d.ts +4 -4
  5. package/build/analyzer/controller.js +3 -3
  6. package/build/analyzer/controllers.d.ts +1 -1
  7. package/build/analyzer/controllers.js +10 -2
  8. package/build/analyzer/decorator.d.ts +2 -2
  9. package/build/analyzer/decorator.js +19 -19
  10. package/build/analyzer/extractor.d.ts +4 -4
  11. package/build/analyzer/extractor.js +6 -17
  12. package/build/analyzer/index.d.ts +1 -1
  13. package/build/analyzer/index.js +2 -2
  14. package/build/analyzer/methods.d.ts +7 -7
  15. package/build/analyzer/methods.js +6 -9
  16. package/build/analyzer/module.js +2 -2
  17. package/build/analyzer/params.d.ts +59 -17
  18. package/build/analyzer/params.js +150 -129
  19. package/build/analyzer/route.d.ts +5 -2
  20. package/build/analyzer/route.js +13 -7
  21. package/build/analyzer/typedeps.d.ts +6 -6
  22. package/build/analyzer/typedeps.js +3 -3
  23. package/build/bin.js +9 -13
  24. package/build/config.d.ts +29 -19
  25. package/build/config.js +2 -2
  26. package/build/generator/genmodules.d.ts +33 -10
  27. package/build/generator/genmodules.js +165 -113
  28. package/build/generator/gentypes.js +1 -1
  29. package/build/generator/index.d.ts +1 -1
  30. package/build/generator/index.js +23 -22
  31. package/build/generator/prettier.d.ts +1 -1
  32. package/build/generator/prettier.js +2 -2
  33. package/build/generator/sdk-interface.d.ts +1 -1
  34. package/build/generator/sdk-interface.js +5 -5
  35. package/build/logging.js +4 -2
  36. package/build/rtk-query-generator/genmodules.d.ts +54 -0
  37. package/build/rtk-query-generator/genmodules.js +252 -0
  38. package/build/rtk-query-generator/gentypes.d.ts +10 -0
  39. package/build/rtk-query-generator/gentypes.js +62 -0
  40. package/build/rtk-query-generator/index.d.ts +6 -0
  41. package/build/rtk-query-generator/index.js +84 -0
  42. package/build/rtk-query-generator/prettier.d.ts +16 -0
  43. package/build/rtk-query-generator/prettier.js +43 -0
  44. package/build/rtk-query-generator/sdk-interface.d.ts +1 -0
  45. package/build/rtk-query-generator/sdk-interface.js +36 -0
  46. package/build/tsconfig.tsbuildinfo +1 -1
  47. package/build/utils.js +1 -1
  48. package/bun.lockb +0 -0
  49. package/package.json +16 -5
  50. package/src/analyzer/classdeps.ts +3 -0
  51. package/src/analyzer/controller.ts +12 -6
  52. package/src/analyzer/controllers.ts +16 -1
  53. package/src/analyzer/extractor.ts +12 -23
  54. package/src/analyzer/index.ts +5 -2
  55. package/src/analyzer/methods.ts +12 -13
  56. package/src/analyzer/module.ts +4 -10
  57. package/src/analyzer/params.ts +212 -182
  58. package/src/analyzer/route.ts +10 -4
  59. package/src/analyzer/typedeps.ts +13 -11
  60. package/src/bin.ts +11 -13
  61. package/src/config.ts +35 -24
  62. package/src/generator/genmodules.ts +212 -129
  63. package/src/generator/gentypes.ts +1 -0
  64. package/src/generator/index.ts +23 -23
  65. package/src/generator/prettier.ts +4 -2
  66. package/src/logging.ts +4 -1
  67. package/src/rtk-query-generator/genmodules.ts +335 -0
  68. package/src/rtk-query-generator/gentypes.ts +74 -0
  69. package/src/rtk-query-generator/index.ts +92 -0
  70. package/src/rtk-query-generator/prettier.ts +48 -0
  71. package/src/rtk-query-generator/sdk-interface.ts +32 -0
  72. package/src/utils.ts +2 -2
  73. package/tsconfig.tsbuildinfo +1 -0
  74. package/src/analyzer/decorator.ts +0 -29
@@ -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
- import { analyzeParams, SdkMethodParams } from './params'
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 HTTP method (e.g. GET / POST) */
20
- readonly httpMethod: SdkHttpMethod
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
- // We need to put a '@ts-ignore' here because TypeScript doesn't like indexing an enumeration with a string key, although this works fine
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 = analyzeParams(httpMethod, route, method.getParameters(), filePath, absoluteSrcPath)
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...')
@@ -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
  }
@@ -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
- import { debug, format, warn } from '../logging'
7
- import { expectSingleStrLitDecorator } from './decorator'
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 { paramsOfRoute, Route } from './route'
10
+ import { paramsFromRoute, Route } from './route'
10
11
  import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
11
12
 
12
13
  /**
13
- * SDK interface for a controller's method's parameters
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
- /** Query parameters */
20
- query: Map<string, ResolvedTypeDeps> | null
36
+ export type SdkMethodParam = SdkMethodParamMap | ResolvedTypeDeps | null
37
+ export type SdkMethodParamMap = { [key: string]: ResolvedTypeDeps }
21
38
 
22
- /** Body parameters */
23
- body: SdkMethodBodyParam | null
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
- * Single body parameter in a SDK's method
57
+ * SDK interface for a controller's method's parameters
28
58
  */
29
- export type SdkMethodBodyParam = { full: true; type: ResolvedTypeDeps } | { full: false; fields: Map<string, ResolvedTypeDeps> }
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 analyzeParams(
41
- httpMethod: SdkHttpMethod,
42
- route: Route,
43
- args: ParameterDeclaration[],
44
- filePath: string,
45
- absoluteSrcPath: string
46
- ): SdkMethodParams | Error {
47
- // The collected informations we will return
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
- // Get the named parameters of the route
55
- const routeParams = paramsOfRoute(route)
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 name = arg.getName()
159
+ const argName = arg.getName()
60
160
 
61
- debug('├───── Detected argument: {yellow}', name)
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 decs = arg.getDecorators()
164
+ const argDecorators = arg.getDecorators().filter((dec) => (Object.values(ArgDecorator) as string[]).includes(dec.getName()))
65
165
 
66
- if (decs.length === 0) {
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 (decs.length > 1) {
71
- // If we have more than one decorator, this could mean we have for instance an @NotEmpty() @Query() or something like this,
72
- // which is currently not supported.
73
- return new Error('Skipping this argument as it has multiple decorators, which is currently not supported')
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 dec = decs[0]
78
- const decName = dec.getName()
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
- // Treat the @Param() decorator
81
- if (decName === 'Param') {
82
- debug('├───── Detected decorator {blue}', '@Param')
187
+ decoratedArgs[decoratorType].push({
188
+ arg,
189
+ argParamKey,
190
+ decorator,
191
+ decoratorType,
192
+ context,
193
+ })
194
+ }
83
195
 
84
- // We expect a single string argument for this decorator,
85
- // which is the route parameter's name
86
- const paramName = expectSingleStrLitDecorator(dec)
196
+ return decoratedArgs
197
+ }
87
198
 
88
- if (paramName instanceof Error) return paramName
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
- // If there is no argument, this argument is a global receiver which maps the full set of parameters
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
- // Ensure the specified parameter appears in the method's route
99
- if (!routeParams.includes(paramName)) return new Error(format('├───── Cannot map unknown parameter {yellow}', paramName))
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
- debug('├───── Mapping argument to parameter: {yellow}', paramName)
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
- // Get the route parameter's type
104
- const typ = resolveTypeDependencies(arg.getType(), filePath, absoluteSrcPath)
227
+ return paramKey
228
+ }
105
229
 
106
- debug('├───── Detected parameter type: {yellow} ({magentaBright} dependencies)', typ.resolvedType, typ.dependencies.size)
230
+ export function mergeDecoratedArgs(decoratedArgs: DecoratedArg[]): SdkMethodParam {
231
+ let genericParam: SdkMethodParam | null = null
232
+ const paramMap: SdkMethodParamMap = {}
107
233
 
108
- // Update the method's route parameters
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
- if (paramName in {}) {
111
- return new Error(
112
- format(`Detected @Param() field whose name {yellow} collides with a JavaScript's native object property`, paramName)
113
- )
238
+ if (argParamKey) {
239
+ if (paramMap[argParamKey]) {
240
+ panicWithContext(`${decoratorType}('${argParamKey}') used twice in controller method`, decoratedArg.context)
114
241
  }
115
242
 
116
- collected.parameters ??= new Map()
117
- collected.parameters.set(paramName, typ)
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
- debug('├───── Mapping argument to query: {yellow}', queryName)
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
- // Treat the @Body() decorator
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
- const depsCount = typ.dependencies.size
255
+ if (genericParam && hasArgsByKey) {
256
+ panicWithContext(`Cannot mix generic and specific ${decoratedArgs[0].decoratorType}()`, decoratedArgs[0].context)
257
+ }
176
258
 
177
- debug(
178
- `├───── Detected BODY type: {cyan} ({magentaBright} ${
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
- // If there no name was provided to the decorator, then the decorator is a generic receiver which means it maps to the full body type
186
- // This also means we can map the BODY type to this argument's type
187
- if (fieldName === null) {
188
- const body = collected.body
189
-
190
- // If we previously had an @Body(<name>) decorator on another argument, we have an important risk of mistyping
191
- // => e.g. `@Body("a") a: string, @Body() body: { a: number }` is invalid as the type for the `a` field mismatches
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
  }
@@ -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 paramsOfRoute(route: Route): string[] {
94
- return route.parts.map((part) => ('param' in part ? part.param : null)).filter((e) => e !== null) as string[]
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
- let uri: string[] = []
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
- let uri: string[] = []
145
+ const uri: string[] = []
140
146
 
141
147
  for (const part of route.parts) {
142
148
  if ('segment' in part) {
@@ -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
- let dependencies: ResolvedTypeDeps['dependencies'] = new Map()
59
- let localTypes: ResolvedTypeDeps['localTypes'] = []
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) => {