@buildvision/nest-sdk-generator 1.1.3

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 (69) hide show
  1. package/.prettierrc +6 -0
  2. package/LICENSE.md +9 -0
  3. package/README.md +353 -0
  4. package/build/analyzer/builtin.d.ts +2 -0
  5. package/build/analyzer/builtin.js +11 -0
  6. package/build/analyzer/classdeps.d.ts +6 -0
  7. package/build/analyzer/classdeps.js +27 -0
  8. package/build/analyzer/controller.d.ts +26 -0
  9. package/build/analyzer/controller.js +103 -0
  10. package/build/analyzer/controllers.d.ts +7 -0
  11. package/build/analyzer/controllers.js +79 -0
  12. package/build/analyzer/decorator.d.ts +10 -0
  13. package/build/analyzer/decorator.js +29 -0
  14. package/build/analyzer/extractor.d.ts +107 -0
  15. package/build/analyzer/extractor.js +349 -0
  16. package/build/analyzer/index.d.ts +16 -0
  17. package/build/analyzer/index.js +101 -0
  18. package/build/analyzer/methods.d.ts +42 -0
  19. package/build/analyzer/methods.js +115 -0
  20. package/build/analyzer/module.d.ts +11 -0
  21. package/build/analyzer/module.js +41 -0
  22. package/build/analyzer/params.d.ts +38 -0
  23. package/build/analyzer/params.js +163 -0
  24. package/build/analyzer/route.d.ts +53 -0
  25. package/build/analyzer/route.js +131 -0
  26. package/build/analyzer/typedeps.d.ts +49 -0
  27. package/build/analyzer/typedeps.js +86 -0
  28. package/build/bin.d.ts +2 -0
  29. package/build/bin.js +39 -0
  30. package/build/config.d.ts +44 -0
  31. package/build/config.js +31 -0
  32. package/build/generator/genmodules.d.ts +24 -0
  33. package/build/generator/genmodules.js +134 -0
  34. package/build/generator/gentypes.d.ts +10 -0
  35. package/build/generator/gentypes.js +62 -0
  36. package/build/generator/index.d.ts +6 -0
  37. package/build/generator/index.js +83 -0
  38. package/build/generator/prettier.d.ts +16 -0
  39. package/build/generator/prettier.js +43 -0
  40. package/build/generator/sdk-interface.d.ts +1 -0
  41. package/build/generator/sdk-interface.js +36 -0
  42. package/build/logging.d.ts +6 -0
  43. package/build/logging.js +37 -0
  44. package/build/tsconfig.tsbuildinfo +1 -0
  45. package/build/utils.d.ts +9 -0
  46. package/build/utils.js +56 -0
  47. package/package.json +34 -0
  48. package/src/analyzer/builtin.ts +9 -0
  49. package/src/analyzer/classdeps.ts +37 -0
  50. package/src/analyzer/controller.ts +135 -0
  51. package/src/analyzer/controllers.ts +117 -0
  52. package/src/analyzer/decorator.ts +29 -0
  53. package/src/analyzer/extractor.ts +491 -0
  54. package/src/analyzer/index.ts +136 -0
  55. package/src/analyzer/methods.ts +166 -0
  56. package/src/analyzer/module.ts +56 -0
  57. package/src/analyzer/params.ts +241 -0
  58. package/src/analyzer/route.ts +156 -0
  59. package/src/analyzer/typedeps.ts +130 -0
  60. package/src/bin.ts +33 -0
  61. package/src/config.ts +89 -0
  62. package/src/generator/genmodules.ts +163 -0
  63. package/src/generator/gentypes.ts +73 -0
  64. package/src/generator/index.ts +92 -0
  65. package/src/generator/prettier.ts +46 -0
  66. package/src/generator/sdk-interface.ts +32 -0
  67. package/src/logging.ts +35 -0
  68. package/src/utils.ts +61 -0
  69. package/tsconfig.json +26 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @file Entrypoint of the source API analyzer, used to generate the final SDK
3
+ */
4
+
5
+ import * as fs from 'fs'
6
+ import * as path from 'path'
7
+ import { Project } from 'ts-morph'
8
+ import { Config } from '../config'
9
+ import { debug, panic } from '../logging'
10
+ import { builtinMagicTypes } from './builtin'
11
+ import { analyzeControllers, SdkModules } from './controllers'
12
+ import { flattenSdkResolvedTypes, locateTypesFile, TypesExtractor, TypesExtractorContent } from './extractor'
13
+
14
+ export interface SdkContent {
15
+ readonly modules: SdkModules
16
+ readonly types: TypesExtractorContent
17
+ }
18
+
19
+ export interface MagicType {
20
+ readonly package: string
21
+ readonly typeName: string
22
+ readonly content: string
23
+ }
24
+
25
+ export async function analyzerCli(config: Config): Promise<SdkContent> {
26
+ const started = Date.now()
27
+
28
+ const sourcePath = path.resolve(process.cwd(), config.apiInputPath)
29
+
30
+ if (!sourcePath) panic('Please provide a source directory')
31
+
32
+ if (!fs.existsSync(sourcePath)) panic('Provided source path {magentaBright} does not exist', sourcePath)
33
+ if (!fs.lstatSync(sourcePath).isDirectory()) panic('Provided source path is not a directory')
34
+
35
+ debug(`Analyzing from source directory {yellow}`, sourcePath)
36
+
37
+ if (config.jsonOutput) {
38
+ const jsonOutputParentDir = path.dirname(path.resolve(process.cwd(), config.jsonOutput))
39
+
40
+ if (!fs.existsSync(jsonOutputParentDir)) {
41
+ panic("Output file's parent directory {magentaBright} does not exist.", jsonOutputParentDir)
42
+ }
43
+
44
+ debug('Writing output to {yellow}', config.jsonOutput, config.jsonPrettyOutput ? 'beautified' : 'minified')
45
+ }
46
+
47
+ // ====== Find & parse 'tsconfig.json' ====== //
48
+ if (config.tsconfigFile && config.tsconfigFile.includes('/')) {
49
+ panic('Provided tsconfig file name contains slashes')
50
+ }
51
+
52
+ const tsConfigFileName = config.tsconfigFile ?? 'tsconfig.json'
53
+ const tsConfigFilePath = path.join(sourcePath, tsConfigFileName)
54
+
55
+ if (!fs.existsSync(tsConfigFilePath)) {
56
+ panic('No {yellow} file found in provided source path {yellow}', tsConfigFileName, sourcePath)
57
+ }
58
+
59
+ // Create a 'ts-morph' project
60
+ const project = new Project({
61
+ tsConfigFilePath,
62
+ })
63
+
64
+ // Get the list of all TypeScript files in the source directory
65
+ const sourceTSFiles = project.getSourceFiles().map((file) => path.relative(sourcePath, file.getFilePath()))
66
+ debug(`Found {magentaBright} source files.`, sourceTSFiles.length)
67
+
68
+ // Add them
69
+ debug('\nAdding them to the source project...')
70
+
71
+ let progressByTenth = 0
72
+ let strLen = sourceTSFiles.length.toString().length
73
+
74
+ const hasProgress = (filesTreated: number) => filesTreated / sourceTSFiles.length >= (progressByTenth + 1) / 10
75
+
76
+ const controllers = []
77
+
78
+ for (let i = 0; i < sourceTSFiles.length; i++) {
79
+ const file = sourceTSFiles[i]
80
+
81
+ if (file.endsWith('.controller.ts')) {
82
+ controllers.push(file)
83
+ }
84
+
85
+ if (hasProgress(i + 1)) {
86
+ while (hasProgress(i + 1)) progressByTenth++
87
+
88
+ debug(
89
+ '| Progress: {yellow} ({magentaBright} files) - {green} controller{} found',
90
+ (progressByTenth * 10).toString().padStart(3, ' ') + '%',
91
+ `${(i + 1).toString().padStart(strLen, ' ')} / ${sourceTSFiles.length}`,
92
+ controllers.length.toString().padStart(strLen, ''),
93
+ controllers.length > 1 ? 's' : ''
94
+ )
95
+ }
96
+ }
97
+
98
+ debug('All files were added successfully.\n')
99
+
100
+ const modules = analyzeControllers(controllers, sourcePath, project)
101
+
102
+ // Builtin magic types are concatenated **after** the configuration's ones, as this allows users to
103
+ // override the builtin ones if they want. Do **not** change the concatenation order!
104
+ const magicTypes = (config.magicTypes ?? []).concat(builtinMagicTypes)
105
+
106
+ const typesCache = new TypesExtractor(project, sourcePath, magicTypes)
107
+
108
+ const typesToExtract = locateTypesFile(flattenSdkResolvedTypes(modules))
109
+
110
+ debug('\n==== Extracting {} type' + (typesToExtract.length > 1 ? 's' : '') + ' ====\n', typesToExtract.length)
111
+
112
+ for (const loc of typesToExtract) {
113
+ const result = typesCache.extractType(loc)
114
+
115
+ if (result instanceof Error) {
116
+ panic(result.message)
117
+ }
118
+ }
119
+
120
+ const content: SdkContent = {
121
+ modules,
122
+ types: typesCache.extracted,
123
+ }
124
+
125
+ if (config.jsonOutput) {
126
+ fs.writeFileSync(
127
+ config.jsonOutput,
128
+ JSON.stringify(content, (_, v) => (v instanceof Map ? Object.fromEntries(v) : v), config.jsonPrettyOutput ? 4 : 0),
129
+ 'utf8'
130
+ )
131
+ }
132
+
133
+ debug('\n===== Done in {green}! ====', ((Date.now() - started) / 1000).toFixed(2) + 's')
134
+
135
+ return content
136
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @file Analyzer for the source API's methods
3
+ */
4
+
5
+ import { blue } from 'chalk'
6
+ import { ClassDeclaration, MethodDeclaration, Node } from 'ts-morph'
7
+ import { debug, format } from '../logging'
8
+ import { analyzeParams, SdkMethodParams } from './params'
9
+ import { analyzeUri, debugUri, Route } from './route'
10
+ import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
11
+
12
+ /**
13
+ * SDK interface for a single controller's method
14
+ */
15
+ export interface SdkMethod {
16
+ /** Method's name */
17
+ readonly name: string
18
+
19
+ /** Method's HTTP method (e.g. GET / POST) */
20
+ readonly httpMethod: SdkHttpMethod
21
+
22
+ /** Method's return type with resolved dependencies */
23
+ readonly returnType: ResolvedTypeDeps
24
+
25
+ /** Method's parsed route */
26
+ readonly route: Route
27
+
28
+ /** Method's URI path */
29
+ readonly uriPath: string
30
+
31
+ /** Method's parameters */
32
+ readonly params: SdkMethodParams
33
+ }
34
+
35
+ /**
36
+ * HTTP method of a controller's method
37
+ */
38
+ export enum SdkHttpMethod {
39
+ Get = 'GET',
40
+ Post = 'POST',
41
+ Put = 'PUT',
42
+ Patch = 'PATCH',
43
+ Delete = 'DELETE',
44
+ }
45
+
46
+ /**
47
+ * Generate a SDK interface for a controller
48
+ * @param controllerClass The class declaration of the controller
49
+ * @param controllerUriPrefix Optional URI prefix for this controller (e.g. `@Controller("registrationName")`)
50
+ * @param filePath Path to the controller's file
51
+ * @param absoluteSrcPath Absolute path to the source directory
52
+ */
53
+ export function analyzeMethods(
54
+ controllerClass: ClassDeclaration,
55
+ controllerUriPrefix: string | null,
56
+ filePath: string,
57
+ absoluteSrcPath: string
58
+ ): SdkMethod[] | Error {
59
+ // Output variable
60
+ const collected = new Array<SdkMethod>()
61
+
62
+ // Get the list of all methods
63
+ const methods = controllerClass.forEachChildAsArray().filter((node) => node instanceof MethodDeclaration) as MethodDeclaration[]
64
+
65
+ for (const method of methods) {
66
+ const methodName = method.getName()
67
+
68
+ debug('├─ Found method: {yellow}', methodName)
69
+
70
+ // Get the HTTP decorator(s) of the method
71
+ const decorators = method.getDecorators().filter((dec) => Object.keys(SdkHttpMethod).includes(dec.getName()))
72
+
73
+ // We expect to have exactly one HTTP decorator
74
+ if (decorators.length > 1) {
75
+ // If there is more than one decorator, that's invalid, so we can't analyze the method
76
+ return new Error(
77
+ format('├─── Detected multiple HTTP decorators on method: {yellow}' + decorators.map((dec) => dec.getName()).join(','))
78
+ )
79
+ } else if (decorators.length === 0) {
80
+ // If there isn't any HTTP decorator, this is simply not a method available from the outside and so we won't generate an interface for it
81
+ debug('├─── Skipping this method as it does not have an HTTP decorator')
82
+ continue
83
+ }
84
+
85
+ // Get the HTTP decorator
86
+ const dec = decorators[0]
87
+
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
+ const httpMethod = SdkHttpMethod[dec.getName()]
91
+
92
+ debug('├─── Detected HTTP method: {magentaBright}', httpMethod.toLocaleUpperCase())
93
+
94
+ // Get the arguments provided to the HTTP decorator (we expect one, the URI path)
95
+ const decArgs = dec.getArguments()
96
+
97
+ // The method's URI path
98
+ let uriPath: string
99
+
100
+ // We expect the decorator to have exactly one argument
101
+ if (decArgs.length > 1) {
102
+ // If we have more than one argument, that's invalid (or at least not supported here), so we can't analyze the method
103
+ return new Error(`Multiple (${decArgs.length}) arguments were provided to the HTTP decorator`)
104
+ } else if (decArgs.length === 0) {
105
+ // If there is no argument, we take the method's name as the URI path
106
+ debug('├─── No argument found for decorator, using base URI path.')
107
+ uriPath = ''
108
+ } else {
109
+ // If we have exactly one argument, hurray! That's our URI path.
110
+ const uriNameDec = decArgs[0]
111
+
112
+ // Variables are not supported
113
+ if (!Node.isStringLiteral(uriNameDec)) {
114
+ return new Error(
115
+ format('├─── The argument provided to the HTTP decorator is not a string literal:\n>> {cyan}', uriNameDec.getText())
116
+ )
117
+ }
118
+
119
+ // Update the method's URI path
120
+ uriPath = uriNameDec.getLiteralText()
121
+
122
+ debug('├─── Detected argument in HTTP decorator, mapping this method to custom URI name')
123
+ }
124
+
125
+ debug('├─── Detected URI name: {yellow}', uriPath)
126
+
127
+ // Analyze the method's URI
128
+ const route = analyzeUri(controllerUriPrefix ? (uriPath ? `/${controllerUriPrefix}/${uriPath}` : `/` + controllerUriPrefix) : uriPath)
129
+
130
+ if (route instanceof Error) {
131
+ return new Error(
132
+ '├─── Detected unsupported URI format:\n' +
133
+ route.message
134
+ .split('\n')
135
+ .map((line) => '├───── ' + line)
136
+ .join('\n')
137
+ )
138
+ }
139
+
140
+ debug('├─── Parsed URI name to route: {yellow}', debugUri(route, blue))
141
+
142
+ // Analyze the method's arguments
143
+ debug('├─── Analyzing arguments...')
144
+ const params = analyzeParams(httpMethod, route, method.getParameters(), filePath, absoluteSrcPath)
145
+
146
+ if (params instanceof Error) return params
147
+
148
+ // Get the method's return type
149
+ debug('├─── Resolving return type...')
150
+ const returnType = resolveTypeDependencies(method.getReturnType(), filePath, absoluteSrcPath)
151
+
152
+ debug('├─── Detected return type: {cyan}', returnType.resolvedType)
153
+
154
+ // Success!
155
+ collected.push({
156
+ name: methodName,
157
+ httpMethod,
158
+ returnType,
159
+ route,
160
+ uriPath,
161
+ params,
162
+ })
163
+ }
164
+
165
+ return collected
166
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @file Analyzer for the source API's modules
3
+ */
4
+
5
+ import * as path from 'path'
6
+ import { Node, Project } from 'ts-morph'
7
+ import { format, panic } from '../logging'
8
+
9
+ /**
10
+ * Get the name of a module
11
+ * @param project TS-Morph project the module is contained in
12
+ * @param modulePath Path to the module's file
13
+ * @param sourcePath Path to the TypeScript root directory
14
+ */
15
+ export function getModuleName(project: Project, modulePath: string, sourcePath: string): string {
16
+ // Prepare the source file to analyze
17
+ const file = project.getSourceFileOrThrow(path.resolve(sourcePath, modulePath))
18
+
19
+ // Find the module class declaration
20
+ const classDecl = file.forEachChildAsArray().find((node) => Node.isClassDeclaration(node) && node.getDecorators().length > 0)
21
+
22
+ if (!classDecl) {
23
+ panic('No class declaration found in module at {yellow}', modulePath)
24
+ }
25
+
26
+ if (!Node.isClassDeclaration(classDecl))
27
+ panic('Internal error: found class declaration statement which is not an instance of ClassDeclaration')
28
+
29
+ const moduleName = classDecl.getName()
30
+
31
+ if (moduleName === undefined) {
32
+ panic('Internal error: failed to retrieve name of declared class')
33
+ }
34
+
35
+ const decorators = classDecl.getDecorators()
36
+
37
+ if (decorators.length > 1) {
38
+ panic(`Found multiple decorators on module class {yellow} declared at {yellow}`, moduleName, modulePath)
39
+ }
40
+
41
+ const decName = decorators[0].getName()
42
+
43
+ if (decName !== 'Module') {
44
+ panic(
45
+ format(
46
+ `The decorator on module class {yellow} was expected to be a {yellow}, found an {yellow} instead\nModule path is: {yellow}`,
47
+ moduleName,
48
+ '@Module',
49
+ '@' + decName,
50
+ modulePath
51
+ )
52
+ )
53
+ }
54
+
55
+ return moduleName.substr(0, 1).toLocaleLowerCase() + moduleName.substr(1)
56
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * @file Analyzer for the source API's controllers' methods' parameters
3
+ */
4
+
5
+ import { ParameterDeclaration } from 'ts-morph'
6
+ import { debug, format, warn } from '../logging'
7
+ import { expectSingleStrLitDecorator } from './decorator'
8
+ import { SdkHttpMethod } from './methods'
9
+ import { paramsOfRoute, Route } from './route'
10
+ import { ResolvedTypeDeps, resolveTypeDependencies } from './typedeps'
11
+
12
+ /**
13
+ * SDK interface for a controller's method's parameters
14
+ */
15
+ export interface SdkMethodParams {
16
+ /** Route parameters */
17
+ parameters: Map<string, ResolvedTypeDeps> | null
18
+
19
+ /** Query parameters */
20
+ query: Map<string, ResolvedTypeDeps> | null
21
+
22
+ /** Body parameters */
23
+ body: SdkMethodBodyParam | null
24
+ }
25
+
26
+ /**
27
+ * Single body parameter in a SDK's method
28
+ */
29
+ export type SdkMethodBodyParam = { full: true; type: ResolvedTypeDeps } | { full: false; fields: Map<string, ResolvedTypeDeps> }
30
+
31
+ /**
32
+ * Generate a SDK interface for a controller's method's parameters
33
+ * @param httpMethod The method's HTTP method
34
+ * @param route The method's route
35
+ * @param args The method's arguments
36
+ * @param filePath Path to the controller's file
37
+ * @param absoluteSrcPath Absolute path to the source directory
38
+ * @returns A SDK interface for the method's parameters
39
+ */
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,
52
+ }
53
+
54
+ // Get the named parameters of the route
55
+ const routeParams = paramsOfRoute(route)
56
+
57
+ // Treat all arguments (to not confuse with the route's parameters)
58
+ for (const arg of args) {
59
+ const name = arg.getName()
60
+
61
+ debug('├───── Detected argument: {yellow}', name)
62
+
63
+ // Arguments are collected as soon as they have a decorator like @Query() or @Body()
64
+ const decs = arg.getDecorators()
65
+
66
+ if (decs.length === 0) {
67
+ // If we have no argument, this is not an argument we are interested in, so we just skip it
68
+ debug('├───── Skipping this argument as it does not have a decorator')
69
+ 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')
74
+ }
75
+
76
+ // Get the only decrator
77
+ const dec = decs[0]
78
+ const decName = dec.getName()
79
+
80
+ // Treat the @Param() decorator
81
+ if (decName === 'Param') {
82
+ debug('├───── Detected decorator {blue}', '@Param')
83
+
84
+ // We expect a single string argument for this decorator,
85
+ // which is the route parameter's name
86
+ const paramName = expectSingleStrLitDecorator(dec)
87
+
88
+ if (paramName instanceof Error) return paramName
89
+
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
+ }
97
+
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))
100
+
101
+ debug('├───── Mapping argument to parameter: {yellow}', paramName)
102
+
103
+ // Get the route parameter's type
104
+ const typ = resolveTypeDependencies(arg.getType(), filePath, absoluteSrcPath)
105
+
106
+ debug('├───── Detected parameter type: {yellow} ({magentaBright} dependencies)', typ.resolvedType, typ.dependencies.size)
107
+
108
+ // Update the method's route parameters
109
+
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
+ )
114
+ }
115
+
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
136
+ }
137
+
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)
155
+ }
156
+
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)
174
+
175
+ const depsCount = typ.dependencies.size
176
+
177
+ debug(
178
+ `├───── Detected BODY type: {cyan} ({magentaBright} ${
179
+ depsCount === 0 ? 'no dependency' : depsCount > 1 ? 'dependencies' : 'dependency'
180
+ })`,
181
+ typ.resolvedType,
182
+ depsCount
183
+ )
184
+
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
+ }
237
+ }
238
+
239
+ // Success!
240
+ return collected
241
+ }