@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -1
  3. package/dist/runtime/shared/openapi/error-response.dto.d.ts +33 -0
  4. package/dist/runtime/shared/openapi/error-response.dto.js +13 -0
  5. package/dist/runtime/shared/openapi/error-response.dto.js.map +1 -0
  6. package/dist/runtime/shared/openapi/errors.d.ts +30 -0
  7. package/dist/runtime/shared/openapi/errors.js +24 -0
  8. package/dist/runtime/shared/openapi/errors.js.map +1 -0
  9. package/dist/runtime/shared/openapi/index.d.ts +5 -0
  10. package/dist/runtime/shared/openapi/index.js +115 -0
  11. package/dist/runtime/shared/openapi/index.js.map +1 -0
  12. package/dist/runtime/shared/openapi/registry.d.ts +82 -0
  13. package/dist/runtime/shared/openapi/registry.js +107 -0
  14. package/dist/runtime/shared/openapi/registry.js.map +1 -0
  15. package/dist/runtime/shared/openapi/registry.tokens.d.ts +15 -0
  16. package/dist/runtime/shared/openapi/registry.tokens.js +6 -0
  17. package/dist/runtime/shared/openapi/registry.tokens.js.map +1 -0
  18. package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +2 -2
  19. package/dist/src/cli/index.js +1888 -1074
  20. package/dist/src/cli/index.js.map +1 -1
  21. package/package.json +10 -1
  22. package/templates/entity/new/backend/application/schemas/dto.ejs.t +31 -0
  23. package/templates/entity/new/backend/modules/core/module.ejs.t +21 -2
  24. package/templates/entity/new/backend/presentation/controller.ejs.t +74 -0
  25. package/templates/entity/new/clean-lite-ps/controller.ejs.t +48 -0
  26. package/templates/entity/new/clean-lite-ps/module.ejs.t +24 -2
  27. package/templates/entity/new/prompt.js +2 -0
  28. package/templates/subsystem/openapi-config/codegen-config-openapi-block.ejs.t +35 -0
  29. 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.2",
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
+ };