@pattern-stack/codegen 0.3.2 → 0.4.1
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/CHANGELOG.md +14 -0
- package/README.md +2 -1
- package/dist/runtime/shared/openapi/error-response.dto.d.ts +33 -0
- package/dist/runtime/shared/openapi/error-response.dto.js +13 -0
- package/dist/runtime/shared/openapi/error-response.dto.js.map +1 -0
- package/dist/runtime/shared/openapi/errors.d.ts +30 -0
- package/dist/runtime/shared/openapi/errors.js +24 -0
- package/dist/runtime/shared/openapi/errors.js.map +1 -0
- package/dist/runtime/shared/openapi/index.d.ts +5 -0
- package/dist/runtime/shared/openapi/index.js +115 -0
- package/dist/runtime/shared/openapi/index.js.map +1 -0
- package/dist/runtime/shared/openapi/registry.d.ts +82 -0
- package/dist/runtime/shared/openapi/registry.js +107 -0
- package/dist/runtime/shared/openapi/registry.js.map +1 -0
- package/dist/runtime/shared/openapi/registry.tokens.d.ts +15 -0
- package/dist/runtime/shared/openapi/registry.tokens.js +6 -0
- package/dist/runtime/shared/openapi/registry.tokens.js.map +1 -0
- package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +2 -2
- package/dist/src/cli/index.js +1888 -1074
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +10 -1
- package/templates/entity/new/backend/application/schemas/dto.ejs.t +31 -0
- package/templates/entity/new/backend/modules/core/module.ejs.t +21 -2
- package/templates/entity/new/backend/presentation/controller.ejs.t +74 -0
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +48 -0
- package/templates/entity/new/clean-lite-ps/module.ejs.t +24 -2
- package/templates/entity/new/prompt.js +2 -0
- package/templates/subsystem/openapi-config/codegen-config-openapi-block.ejs.t +35 -0
- package/templates/subsystem/openapi-config/prompt.js +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pattern-stack/codegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Entity-driven code generation for full-stack TypeScript applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"glob": "^13.0.6",
|
|
67
67
|
"ora": "^9.3.0",
|
|
68
68
|
"pluralize": "^8.0.0",
|
|
69
|
+
"ts-morph": "^28.0.0",
|
|
69
70
|
"yaml": "^2.8.3",
|
|
70
71
|
"zod": "^3.22.4"
|
|
71
72
|
},
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
"@cubejs-client/core": ">=1.0.0",
|
|
74
75
|
"@nestjs/common": "^10",
|
|
75
76
|
"@nestjs/core": "^10",
|
|
77
|
+
"@nestjs/swagger": "^7.0.0 || ^8.0.0",
|
|
76
78
|
"bullmq": ">=5.0.0",
|
|
77
79
|
"class-transformer": ">=0.5.0",
|
|
78
80
|
"class-validator": ">=0.14.0",
|
|
@@ -82,9 +84,15 @@
|
|
|
82
84
|
"rxjs": "^7.0.0"
|
|
83
85
|
},
|
|
84
86
|
"peerDependenciesMeta": {
|
|
87
|
+
"@anatine/zod-openapi": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
85
90
|
"@cubejs-client/core": {
|
|
86
91
|
"optional": true
|
|
87
92
|
},
|
|
93
|
+
"@nestjs/swagger": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
88
96
|
"bullmq": {
|
|
89
97
|
"optional": true
|
|
90
98
|
},
|
|
@@ -102,6 +110,7 @@
|
|
|
102
110
|
"packages/*"
|
|
103
111
|
],
|
|
104
112
|
"devDependencies": {
|
|
113
|
+
"@anatine/zod-openapi": "^2.2.8",
|
|
105
114
|
"@cubejs-client/core": "^1.0.0",
|
|
106
115
|
"@nestjs/common": "10",
|
|
107
116
|
"@nestjs/core": "10",
|
|
@@ -43,3 +43,34 @@ export const update<%= className %>Schema = z.object({
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
export type Update<%= className %>Dto = z.infer<typeof update<%= className %>Schema>;
|
|
46
|
+
|
|
47
|
+
// OPENAPI-3: response schema — mirrors the select shape of this entity so
|
|
48
|
+
// controller `@ApiResponse({ schema: { $ref: '.../<%= className %>ResponseDto' } })`
|
|
49
|
+
// decorators resolve to a fully-typed document on /docs-json.
|
|
50
|
+
export const <%= camelName %>ResponseSchema = z.object({
|
|
51
|
+
id: z.string().uuid(),
|
|
52
|
+
<% fields.forEach((field) => { -%>
|
|
53
|
+
<% let zodChain = field.zodType; -%>
|
|
54
|
+
<% if (field.type === 'string' && field.maxLength) { zodChain += `.max(${field.maxLength})`; } -%>
|
|
55
|
+
<% if (field.type === 'string' && field.minLength) { zodChain += `.min(${field.minLength})`; } -%>
|
|
56
|
+
<% if (['integer', 'decimal'].includes(field.type) && field.min !== undefined) { zodChain += `.min(${field.min})`; } -%>
|
|
57
|
+
<% if (['integer', 'decimal'].includes(field.type) && field.max !== undefined) { zodChain += `.max(${field.max})`; } -%>
|
|
58
|
+
<% if (field.choices) { zodChain = `z.enum([${field.choices.map(c => `'${c}'`).join(', ')}])`; } -%>
|
|
59
|
+
<% if (field.nullable) { zodChain += '.nullable()'; } -%>
|
|
60
|
+
<%- field.camelName %>: <%- zodChain %>,
|
|
61
|
+
<% }) -%>
|
|
62
|
+
<% if (hasTimestamps) { -%>
|
|
63
|
+
createdAt: z.coerce.date(),
|
|
64
|
+
updatedAt: z.coerce.date(),
|
|
65
|
+
<% } -%>
|
|
66
|
+
<% if (hasSoftDelete) { -%>
|
|
67
|
+
deletedAt: z.coerce.date().nullable(),
|
|
68
|
+
<% } -%>
|
|
69
|
+
<% if (hasTemporalValidity) { -%>
|
|
70
|
+
validFrom: z.coerce.date().nullable(),
|
|
71
|
+
validTo: z.coerce.date().nullable(),
|
|
72
|
+
isActive: z.boolean(),
|
|
73
|
+
<% } -%>
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type <%= className %>ResponseDto = z.infer<typeof <%= camelName %>ResponseSchema>;
|
|
@@ -14,7 +14,8 @@ force: true
|
|
|
14
14
|
<% } -%>
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { Module } from '@nestjs/common';
|
|
17
|
+
import { Inject, Module, type OnModuleInit } from '@nestjs/common';
|
|
18
|
+
import { OPENAPI_REGISTRY, type OpenApiRegistry } from '@shared/openapi';
|
|
18
19
|
import { <%= repositoryToken %> } from '<%= imports.moduleToConstants %>';
|
|
19
20
|
import { <%= getByIdQueryClass %> } from '<%= imports.moduleToGetByIdQuery %>';
|
|
20
21
|
<% if (!exposeElectric) { -%>
|
|
@@ -34,6 +35,9 @@ import { declarativeQueryClasses } from '<%= imports.moduleToDeclarativeQueries
|
|
|
34
35
|
<% if (exposeRest || exposeElectric) { -%>
|
|
35
36
|
import { <%= classNamePlural %>Controller } from '<%= imports.moduleToController %>';
|
|
36
37
|
<% } -%>
|
|
38
|
+
<% if (generate.dtos) { -%>
|
|
39
|
+
import { create<%= className %>Schema, update<%= className %>Schema, <%= camelName %>ResponseSchema } from '<%= imports.moduleToDto %>';
|
|
40
|
+
<% } -%>
|
|
37
41
|
|
|
38
42
|
@Module({
|
|
39
43
|
imports: [DatabaseModule<%= exposeElectric ? ', ElectricModule' : '' %>],
|
|
@@ -70,4 +74,19 @@ import { <%= classNamePlural %>Controller } from '<%= imports.moduleToController
|
|
|
70
74
|
<% } -%>
|
|
71
75
|
],
|
|
72
76
|
})
|
|
73
|
-
export class <%= classNamePlural %>Module {}
|
|
77
|
+
export class <%= classNamePlural %>Module<% if (generate.dtos) { %> implements OnModuleInit<% } %> {
|
|
78
|
+
<% if (generate.dtos) { -%>
|
|
79
|
+
// OPENAPI-2: register this entity's Zod schemas with the shared
|
|
80
|
+
// OpenApiRegistry at module init. OPENAPI-4 awaits `build()` at boot
|
|
81
|
+
// to emit the full /docs-json document.
|
|
82
|
+
constructor(
|
|
83
|
+
@Inject(OPENAPI_REGISTRY) private readonly openApi: OpenApiRegistry,
|
|
84
|
+
) {}
|
|
85
|
+
|
|
86
|
+
onModuleInit(): void {
|
|
87
|
+
this.openApi.registerSchema('Create<%= className %>Dto', create<%= className %>Schema);
|
|
88
|
+
this.openApi.registerSchema('Update<%= className %>Dto', update<%= className %>Schema);
|
|
89
|
+
this.openApi.registerSchema('<%= className %>ResponseDto', <%= camelName %>ResponseSchema);
|
|
90
|
+
}
|
|
91
|
+
<% } -%>
|
|
92
|
+
}
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
<% } -%>
|
|
25
25
|
UsePipes,
|
|
26
26
|
} from '@nestjs/common';
|
|
27
|
+
import {
|
|
28
|
+
ApiBearerAuth,
|
|
29
|
+
ApiBody,
|
|
30
|
+
ApiOperation,
|
|
31
|
+
ApiParam,
|
|
32
|
+
ApiResponse,
|
|
33
|
+
} from '@nestjs/swagger';
|
|
27
34
|
import { <%= getByIdQueryClass %> } from '<%= imports.controllerToGetByIdQuery %>';
|
|
28
35
|
import { <%= listQueryClass %> } from '<%= imports.controllerToListQuery %>';
|
|
29
36
|
import {
|
|
@@ -41,6 +48,13 @@ import { <%= className %> } from '<%= imports.controllerToDomain %>';
|
|
|
41
48
|
import type { <%= className %>With } from '<%= imports.controllerToDomain %>';
|
|
42
49
|
<% } -%>
|
|
43
50
|
|
|
51
|
+
// OPENAPI-3: controller decorators reference schemas by `$ref` rather
|
|
52
|
+
// than `type:` class references because generated DTOs are Zod-derived
|
|
53
|
+
// types (not NestJS classes). Schemas are registered by name in the
|
|
54
|
+
// `OpenApiRegistry` at onModuleInit (OPENAPI-2); these `$ref` URIs point
|
|
55
|
+
// at those registered entries. `ErrorResponseDto` is auto-registered by
|
|
56
|
+
// the shared registry.
|
|
57
|
+
@ApiBearerAuth()
|
|
44
58
|
@Controller('<%= plural %>')
|
|
45
59
|
export class <%= classNamePlural %>Controller {
|
|
46
60
|
constructor(
|
|
@@ -51,6 +65,12 @@ export class <%= classNamePlural %>Controller {
|
|
|
51
65
|
private readonly delete<%= className %>Command: <%= deleteCommandClass %>,
|
|
52
66
|
) {}
|
|
53
67
|
|
|
68
|
+
@ApiOperation({ summary: 'List <%= plural %>', operationId: 'list<%= classNamePlural %>' })
|
|
69
|
+
@ApiResponse({
|
|
70
|
+
status: 200,
|
|
71
|
+
schema: { type: 'array', items: { $ref: '#/components/schemas/<%= className %>ResponseDto' } },
|
|
72
|
+
})
|
|
73
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
54
74
|
@Get()
|
|
55
75
|
async findAll(<%- hasRelationships ? `@Query('include') include?: string` : '' %>): Promise<<%= className %>[]> {
|
|
56
76
|
<% if (hasRelationships) { -%>
|
|
@@ -60,6 +80,11 @@ export class <%= classNamePlural %>Controller {
|
|
|
60
80
|
<% } -%>
|
|
61
81
|
}
|
|
62
82
|
|
|
83
|
+
@ApiOperation({ summary: 'Find <%= name %> by id', operationId: 'find<%= className %>ById' })
|
|
84
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
85
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
86
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
87
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
63
88
|
@Get(':id')
|
|
64
89
|
async findById(
|
|
65
90
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -74,6 +99,11 @@ export class <%= classNamePlural %>Controller {
|
|
|
74
99
|
<% } -%>
|
|
75
100
|
}
|
|
76
101
|
|
|
102
|
+
@ApiOperation({ summary: 'Create <%= name %>', operationId: 'create<%= className %>' })
|
|
103
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/Create<%= className %>Dto' } })
|
|
104
|
+
@ApiResponse({ status: 201, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
105
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
106
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
77
107
|
@Post()
|
|
78
108
|
@UsePipes(new ZodValidationPipe(create<%= className %>Schema))
|
|
79
109
|
async create(
|
|
@@ -84,6 +114,13 @@ export class <%= classNamePlural %>Controller {
|
|
|
84
114
|
return this.create<%= className %>Command.execute(dto, { actor: { tenantId, userId } });
|
|
85
115
|
}
|
|
86
116
|
|
|
117
|
+
@ApiOperation({ summary: 'Update <%= name %>', operationId: 'update<%= className %>' })
|
|
118
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/Update<%= className %>Dto' } })
|
|
119
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
120
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
121
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
122
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
123
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
87
124
|
@Put(':id')
|
|
88
125
|
async update(
|
|
89
126
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -94,6 +131,11 @@ export class <%= classNamePlural %>Controller {
|
|
|
94
131
|
return this.update<%= className %>Command.execute(id, dto, { actor: { tenantId, userId } });
|
|
95
132
|
}
|
|
96
133
|
|
|
134
|
+
@ApiOperation({ summary: 'Delete <%= name %>', operationId: 'delete<%= className %>' })
|
|
135
|
+
@ApiResponse({ status: 204 })
|
|
136
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
137
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
138
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
97
139
|
@Delete(':id')
|
|
98
140
|
async delete(
|
|
99
141
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -141,6 +183,13 @@ import {
|
|
|
141
183
|
UseGuards,
|
|
142
184
|
UsePipes,
|
|
143
185
|
} from '@nestjs/common';
|
|
186
|
+
import {
|
|
187
|
+
ApiBearerAuth,
|
|
188
|
+
ApiBody,
|
|
189
|
+
ApiOperation,
|
|
190
|
+
ApiParam,
|
|
191
|
+
ApiResponse,
|
|
192
|
+
} from '@nestjs/swagger';
|
|
144
193
|
import type { Request, Response } from 'express';
|
|
145
194
|
import { AuthGuard } from '<%= imports.controllerToAuthGuard %>/auth.guard';
|
|
146
195
|
import { CurrentUser } from '<%= imports.controllerToCurrentUser %>/current-user.decorator';
|
|
@@ -160,6 +209,7 @@ import { <%= getByIdQueryClass %> } from '<%= imports.controllerToGetByIdQuery %
|
|
|
160
209
|
import { ZodValidationPipe } from '../../core/pipes/zod-validation.pipe';
|
|
161
210
|
import { <%= className %> } from '<%= imports.controllerToDomain %>';
|
|
162
211
|
|
|
212
|
+
@ApiBearerAuth()
|
|
163
213
|
@Controller('<%= plural %>')
|
|
164
214
|
@UseGuards(AuthGuard)
|
|
165
215
|
export class <%= classNamePlural %>Controller {
|
|
@@ -171,6 +221,8 @@ export class <%= classNamePlural %>Controller {
|
|
|
171
221
|
private readonly delete<%= className %>Command: <%= deleteCommandClass %>,
|
|
172
222
|
) {}
|
|
173
223
|
|
|
224
|
+
@ApiOperation({ summary: 'List <%= plural %> (Electric SQL proxy)', operationId: 'list<%= classNamePlural %>' })
|
|
225
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
174
226
|
@Get()
|
|
175
227
|
async findAll(
|
|
176
228
|
@CurrentUser() user: User,
|
|
@@ -186,11 +238,21 @@ export class <%= classNamePlural %>Controller {
|
|
|
186
238
|
});
|
|
187
239
|
}
|
|
188
240
|
|
|
241
|
+
@ApiOperation({ summary: 'Find <%= name %> by id', operationId: 'find<%= className %>ById' })
|
|
242
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
243
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
244
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
245
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
189
246
|
@Get(':id')
|
|
190
247
|
async findById(@Param('id', ParseUUIDPipe) id: string): Promise<<%= className %>> {
|
|
191
248
|
return this.get<%= className %>ByIdQuery.execute(id);
|
|
192
249
|
}
|
|
193
250
|
|
|
251
|
+
@ApiOperation({ summary: 'Create <%= name %>', operationId: 'create<%= className %>' })
|
|
252
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/Create<%= className %>Dto' } })
|
|
253
|
+
@ApiResponse({ status: 201, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
254
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
255
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
194
256
|
@Post()
|
|
195
257
|
@UsePipes(new ZodValidationPipe(create<%= className %>Schema))
|
|
196
258
|
async create(
|
|
@@ -201,6 +263,13 @@ export class <%= classNamePlural %>Controller {
|
|
|
201
263
|
return this.create<%= className %>Command.execute(dto, { actor: { tenantId, userId } });
|
|
202
264
|
}
|
|
203
265
|
|
|
266
|
+
@ApiOperation({ summary: 'Update <%= name %>', operationId: 'update<%= className %>' })
|
|
267
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/Update<%= className %>Dto' } })
|
|
268
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= className %>ResponseDto' } })
|
|
269
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
270
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
271
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
272
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
204
273
|
@Put(':id')
|
|
205
274
|
async update(
|
|
206
275
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -211,6 +280,11 @@ export class <%= classNamePlural %>Controller {
|
|
|
211
280
|
return this.update<%= className %>Command.execute(id, dto, { actor: { tenantId, userId } });
|
|
212
281
|
}
|
|
213
282
|
|
|
283
|
+
@ApiOperation({ summary: 'Delete <%= name %>', operationId: 'delete<%= className %>' })
|
|
284
|
+
@ApiResponse({ status: 204 })
|
|
285
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
286
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
287
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
214
288
|
@Delete(':id')
|
|
215
289
|
async delete(
|
|
216
290
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -4,6 +4,7 @@ skip_if: "<%= typeof clpOutputPaths === 'undefined' %>"
|
|
|
4
4
|
force: true
|
|
5
5
|
---
|
|
6
6
|
import { Controller, Get<% if (generateWrites) { %>, Post, Patch, Delete, Body, Headers<% } %>, NotFoundException, Param, ParseUUIDPipe } from '@nestjs/common';
|
|
7
|
+
import { ApiBearerAuth, <% if (generateWrites) { %>ApiBody, <% } %>ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
|
7
8
|
import { <%= classNames.findByIdUseCase %> } from './use-cases/find-<%= entityName %>-by-id.use-case';
|
|
8
9
|
import { <%= classNames.listUseCase %> } from './use-cases/list-<%= entityNamePlural %>.use-case';
|
|
9
10
|
<% if (eavEnabled) { -%>
|
|
@@ -22,6 +23,11 @@ import type { <%= classNames.updateDto %> } from './dto/update-<%= entityName %>
|
|
|
22
23
|
<% } -%>
|
|
23
24
|
import type { <%= classNames.entity %> } from './<%= entityName %>.entity';
|
|
24
25
|
|
|
26
|
+
// OPENAPI-3: decorators reference registered schemas by `$ref` because
|
|
27
|
+
// CLP DTOs are Zod-derived types (OPENAPI-2 registers them by name at
|
|
28
|
+
// onModuleInit). `ErrorResponseDto` is auto-registered by the shared
|
|
29
|
+
// registry.
|
|
30
|
+
@ApiBearerAuth()
|
|
25
31
|
@Controller('<%= entityNamePlural %>')
|
|
26
32
|
export class <%= classNames.controller %> {
|
|
27
33
|
constructor(
|
|
@@ -39,22 +45,47 @@ export class <%= classNames.controller %> {
|
|
|
39
45
|
<% } -%>
|
|
40
46
|
) {}
|
|
41
47
|
|
|
48
|
+
@ApiOperation({ summary: 'List <%= entityNamePlural %>', operationId: 'list<%= classNames.entity %>s' })
|
|
49
|
+
@ApiResponse({
|
|
50
|
+
status: 200,
|
|
51
|
+
schema: { type: 'array', items: { $ref: '#/components/schemas/<%= classNames.outputDto %>' } },
|
|
52
|
+
})
|
|
53
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
42
54
|
@Get()
|
|
43
55
|
async getAll(): Promise<<%= classNames.entity %>[]> {
|
|
44
56
|
return this.listUseCase.execute();
|
|
45
57
|
}
|
|
46
58
|
<% if (eavEnabled) { %>
|
|
59
|
+
@ApiOperation({
|
|
60
|
+
summary: 'List <%= entityNamePlural %> with EAV fields',
|
|
61
|
+
operationId: 'list<%= classNames.entity %>sWithFields',
|
|
62
|
+
})
|
|
63
|
+
@ApiResponse({ status: 200 })
|
|
64
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
47
65
|
@Get('with-fields')
|
|
48
66
|
async getAllWithFields(): Promise<Array<<%= classNames.entity %> & { fields: Record<string, unknown> }>> {
|
|
49
67
|
return this.listWithFieldsUseCase.execute();
|
|
50
68
|
}
|
|
51
69
|
<% } %>
|
|
70
|
+
@ApiOperation({ summary: 'Find <%= entityName %> by id', operationId: 'find<%= classNames.entity %>ById' })
|
|
71
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= classNames.outputDto %>' } })
|
|
72
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
73
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
74
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
52
75
|
@Get(':id')
|
|
53
76
|
async getById(@Param('id', ParseUUIDPipe) id: string): Promise<<%= classNames.entity %>> {
|
|
54
77
|
// Use case throws NotFoundException on null/undefined (D2)
|
|
55
78
|
return this.findByIdUseCase.execute(id);
|
|
56
79
|
}
|
|
57
80
|
<% if (eavEnabled) { %>
|
|
81
|
+
@ApiOperation({
|
|
82
|
+
summary: 'Find <%= entityName %> with EAV fields',
|
|
83
|
+
operationId: 'find<%= classNames.entity %>ByIdWithFields',
|
|
84
|
+
})
|
|
85
|
+
@ApiResponse({ status: 200 })
|
|
86
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
87
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
88
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
58
89
|
@Get(':id/with-fields')
|
|
59
90
|
async getByIdWithFields(
|
|
60
91
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -65,6 +96,11 @@ export class <%= classNames.controller %> {
|
|
|
65
96
|
}
|
|
66
97
|
<% } %>
|
|
67
98
|
<% if (generateWrites) { %>
|
|
99
|
+
@ApiOperation({ summary: 'Create <%= entityName %>', operationId: 'create<%= classNames.entity %>' })
|
|
100
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/<%= classNames.createDto %>' } })
|
|
101
|
+
@ApiResponse({ status: 201, schema: { $ref: '#/components/schemas/<%= classNames.outputDto %>' } })
|
|
102
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
103
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
68
104
|
@Post()
|
|
69
105
|
async create(
|
|
70
106
|
@Body(new ZodValidationPipe(<%= classNames.createSchema %>)) dto: <%= classNames.createDto %>,
|
|
@@ -74,6 +110,13 @@ export class <%= classNames.controller %> {
|
|
|
74
110
|
return this.createUseCase.execute(dto, { actor: { tenantId, userId } });
|
|
75
111
|
}
|
|
76
112
|
|
|
113
|
+
@ApiOperation({ summary: 'Update <%= entityName %>', operationId: 'update<%= classNames.entity %>' })
|
|
114
|
+
@ApiBody({ schema: { $ref: '#/components/schemas/<%= classNames.updateDto %>' } })
|
|
115
|
+
@ApiResponse({ status: 200, schema: { $ref: '#/components/schemas/<%= classNames.outputDto %>' } })
|
|
116
|
+
@ApiResponse({ status: 400, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
117
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
118
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
119
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
77
120
|
@Patch(':id')
|
|
78
121
|
async update(
|
|
79
122
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -86,6 +129,11 @@ export class <%= classNames.controller %> {
|
|
|
86
129
|
return entity;
|
|
87
130
|
}
|
|
88
131
|
|
|
132
|
+
@ApiOperation({ summary: 'Delete <%= entityName %>', operationId: 'delete<%= classNames.entity %>' })
|
|
133
|
+
@ApiResponse({ status: 204 })
|
|
134
|
+
@ApiResponse({ status: 401, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
135
|
+
@ApiResponse({ status: 404, schema: { $ref: '#/components/schemas/ErrorResponseDto' } })
|
|
136
|
+
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
|
89
137
|
@Delete(':id')
|
|
90
138
|
async remove(
|
|
91
139
|
@Param('id', ParseUUIDPipe) id: string,
|
|
@@ -10,7 +10,8 @@ force: true
|
|
|
10
10
|
* root AppModule (global) so these tokens resolve at runtime.
|
|
11
11
|
*/
|
|
12
12
|
<% } -%>
|
|
13
|
-
import { Module } from '@nestjs/common';
|
|
13
|
+
import { Inject, Module, type OnModuleInit } from '@nestjs/common';
|
|
14
|
+
import { OPENAPI_REGISTRY, type OpenApiRegistry } from '@shared/openapi';
|
|
14
15
|
import { DatabaseModule } from '@shared/database/database.module';
|
|
15
16
|
<%_ clpBelongsTo.forEach(rel => { _%>
|
|
16
17
|
// import { <%= rel.relatedEntityPascal %>sModule } from '../<%= rel.relatedPlural %>/<%= rel.relatedPlural %>.module';
|
|
@@ -25,6 +26,10 @@ import { <%= eavDefinitionPluralPascal %>Module } from '../<%= eavDefinitionEnti
|
|
|
25
26
|
import { <%= classNames.repository %> } from './<%= entityName %>.repository';
|
|
26
27
|
import { <%= classNames.service %> } from './<%= entityName %>.service';
|
|
27
28
|
import { <%= classNames.controller %> } from './<%= entityName %>.controller';
|
|
29
|
+
// OPENAPI-2: Zod schemas registered with OpenApiRegistry at module init.
|
|
30
|
+
import { <%= classNames.createSchema %> } from './dto/create-<%= entityName %>.dto';
|
|
31
|
+
import { <%= classNames.updateSchema %> } from './dto/update-<%= entityName %>.dto';
|
|
32
|
+
import { <%= classNames.outputSchema %> } from './dto/<%= entityName %>-output.dto';
|
|
28
33
|
import { <%= classNames.findByIdUseCase %> } from './use-cases/find-<%= entityName %>-by-id.use-case';
|
|
29
34
|
import { <%= classNames.listUseCase %> } from './use-cases/list-<%= entityNamePlural %>.use-case';
|
|
30
35
|
<% if (eavEnabled) { -%>
|
|
@@ -83,4 +88,21 @@ import { <%= classNames.searchController %> } from './<%= entityName %>-search.c
|
|
|
83
88
|
],
|
|
84
89
|
exports: [<%= classNames.service %>], // Only service is exported (ADR-002)
|
|
85
90
|
})
|
|
86
|
-
export class <%= classNames.module %> {
|
|
91
|
+
export class <%= classNames.module %> implements OnModuleInit {
|
|
92
|
+
// OPENAPI-2: register this entity's Zod schemas with the shared
|
|
93
|
+
// OpenApiRegistry at module init. OPENAPI-4 awaits `build()` at boot
|
|
94
|
+
// to emit the full /docs-json document.
|
|
95
|
+
constructor(
|
|
96
|
+
@Inject(OPENAPI_REGISTRY) private readonly openApi: OpenApiRegistry,
|
|
97
|
+
) {}
|
|
98
|
+
|
|
99
|
+
onModuleInit(): void {
|
|
100
|
+
this.openApi.registerSchema('<%= classNames.createDto %>', <%= classNames.createSchema %>);
|
|
101
|
+
this.openApi.registerSchema('<%= classNames.updateDto %>', <%= classNames.updateSchema %>);
|
|
102
|
+
// CLP pipeline names the response schema <Entity>OutputDto (matches
|
|
103
|
+
// classNames.outputDto); the OPENAPI-2 spec sketch uses "ResponseDto"
|
|
104
|
+
// but existing CLP code already publishes OutputDto everywhere, so we
|
|
105
|
+
// keep consistency. OPENAPI-3 decorators reference the same name.
|
|
106
|
+
this.openApi.registerSchema('<%= classNames.outputDto %>', <%= classNames.outputSchema %>);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -575,6 +575,8 @@ export default {
|
|
|
575
575
|
moduleToConstants: importHelpers.moduleToConstants(),
|
|
576
576
|
moduleToDatabaseModule: importHelpers.moduleToDatabaseModule(),
|
|
577
577
|
moduleToController: importHelpers.moduleToController(fileNames.controller.replace('.ts', '')),
|
|
578
|
+
// OPENAPI-2: module imports DTO file to register Zod schemas at onModuleInit.
|
|
579
|
+
moduleToDto: importHelpers.moduleToDto(fileNames.dto.replace('.ts', '')),
|
|
578
580
|
// From controller (presentation/rest/) to queries/commands
|
|
579
581
|
controllerToGetByIdQuery: importHelpers.controllerToQuery(name, fileNames.getByIdQuery.replace('.ts', '')),
|
|
580
582
|
controllerToListQuery: importHelpers.controllerToQuery(name, fileNames.listQuery.replace('.ts', '')),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: "<%= configPath %>"
|
|
3
|
+
inject: true
|
|
4
|
+
append: true
|
|
5
|
+
skip_if: "openapi:"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
openapi:
|
|
9
|
+
# ── Master switch (OPENAPI-4) ──
|
|
10
|
+
# When false the `main.ts` bootstrap skips Swagger setup entirely — no
|
|
11
|
+
# `/docs` route, no `/docs-json`, no BearerAuth scheme. Generated
|
|
12
|
+
# controllers still emit `@nestjs/swagger` decorators (see gotchas in
|
|
13
|
+
# CONSUMER-SETUP §OpenAPI) so you must keep the peer dep installed
|
|
14
|
+
# either way.
|
|
15
|
+
enabled: true
|
|
16
|
+
|
|
17
|
+
# ── Mount point for Swagger UI (+ the JSON spec at `<path>-json`) ──
|
|
18
|
+
# '/docs' is the locked default; any path works. Conventional
|
|
19
|
+
# alternatives: '/api-docs', '/openapi', '/swagger'.
|
|
20
|
+
path: /docs
|
|
21
|
+
|
|
22
|
+
# ── Document metadata rendered in Swagger UI ──
|
|
23
|
+
# `title` appears as the page heading; `version` in the header badge.
|
|
24
|
+
# `description` is the paragraph beneath the title.
|
|
25
|
+
title: My App
|
|
26
|
+
version: 0.1.0
|
|
27
|
+
description: Generated by @pattern-stack/codegen
|
|
28
|
+
|
|
29
|
+
# ── Default security scheme ──
|
|
30
|
+
# 'bearer' registers `components.securitySchemes.bearer` as
|
|
31
|
+
# `{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }` and applies
|
|
32
|
+
# it globally via `document.security = [{ bearer: [] }]`. Controllers
|
|
33
|
+
# already emit `@ApiBearerAuth()` at the class level (OPENAPI-3), so
|
|
34
|
+
# Swagger UI's "Authorize" button renders out of the box.
|
|
35
|
+
auth: bearer
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hygen prompt.js — OPENAPI-4 OpenAPI config-block scaffold.
|
|
3
|
+
*
|
|
4
|
+
* The OpenAPI "subsystem" is config-only: the runtime helpers
|
|
5
|
+
* (`OpenApiRegistry`, `OPENAPI_REGISTRY` token, `ErrorResponseDto`) are
|
|
6
|
+
* already vendored into every consumer project by `codegen project init`
|
|
7
|
+
* (see `src/cli/shared/init-scaffold.ts::VENDORED_RUNTIME_FILES`). So
|
|
8
|
+
* there is no `runtime/subsystems/openapi/` directory to copy — this
|
|
9
|
+
* template's sole job is to inject the `openapi:` block into
|
|
10
|
+
* `codegen.config.yaml`.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the bridge-config / events-config / jobs-config / sync-config
|
|
13
|
+
* prompt.js shape. Invoked via:
|
|
14
|
+
* bunx hygen subsystem openapi-config --configPath <abs>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
prompt: async ({ args }) => {
|
|
19
|
+
return {
|
|
20
|
+
configPath: args.configPath ?? "codegen.config.yaml",
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|