@pattern-stack/codegen 0.3.2 → 0.4.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 (28) hide show
  1. package/README.md +2 -1
  2. package/dist/runtime/shared/openapi/error-response.dto.d.ts +33 -0
  3. package/dist/runtime/shared/openapi/error-response.dto.js +13 -0
  4. package/dist/runtime/shared/openapi/error-response.dto.js.map +1 -0
  5. package/dist/runtime/shared/openapi/errors.d.ts +30 -0
  6. package/dist/runtime/shared/openapi/errors.js +24 -0
  7. package/dist/runtime/shared/openapi/errors.js.map +1 -0
  8. package/dist/runtime/shared/openapi/index.d.ts +5 -0
  9. package/dist/runtime/shared/openapi/index.js +115 -0
  10. package/dist/runtime/shared/openapi/index.js.map +1 -0
  11. package/dist/runtime/shared/openapi/registry.d.ts +82 -0
  12. package/dist/runtime/shared/openapi/registry.js +107 -0
  13. package/dist/runtime/shared/openapi/registry.js.map +1 -0
  14. package/dist/runtime/shared/openapi/registry.tokens.d.ts +15 -0
  15. package/dist/runtime/shared/openapi/registry.tokens.js +6 -0
  16. package/dist/runtime/shared/openapi/registry.tokens.js.map +1 -0
  17. package/dist/runtime/subsystems/sync/sync-audit.schema.d.ts +2 -2
  18. package/dist/src/cli/index.js +273 -5
  19. package/dist/src/cli/index.js.map +1 -1
  20. package/package.json +9 -1
  21. package/templates/entity/new/backend/application/schemas/dto.ejs.t +31 -0
  22. package/templates/entity/new/backend/modules/core/module.ejs.t +21 -2
  23. package/templates/entity/new/backend/presentation/controller.ejs.t +74 -0
  24. package/templates/entity/new/clean-lite-ps/controller.ejs.t +48 -0
  25. package/templates/entity/new/clean-lite-ps/module.ejs.t +24 -2
  26. package/templates/entity/new/prompt.js +2 -0
  27. package/templates/subsystem/openapi-config/codegen-config-openapi-block.ejs.t +35 -0
  28. package/templates/subsystem/openapi-config/prompt.js +23 -0
@@ -218304,6 +218304,7 @@ async function detectInstalledSubsystems(ctx) {
218304
218304
  if (!fs8.existsSync(root)) continue;
218305
218305
  for (const name of KNOWN_NAMES) {
218306
218306
  if (seen.has(name)) continue;
218307
+ if (name === "openapi-config") continue;
218307
218308
  const dir = path16.join(root, name);
218308
218309
  if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) continue;
218309
218310
  const files = fs8.readdirSync(dir);
@@ -218317,6 +218318,25 @@ async function detectInstalledSubsystems(ctx) {
218317
218318
  });
218318
218319
  }
218319
218320
  }
218321
+ if (!seen.has("openapi-config")) {
218322
+ const configPath = path16.resolve(
218323
+ ctx.cwd,
218324
+ ctx.config ? "codegen.config.yaml" : "codegen.config.yaml"
218325
+ );
218326
+ if (fs8.existsSync(configPath)) {
218327
+ try {
218328
+ const source = fs8.readFileSync(configPath, "utf-8");
218329
+ if (/^openapi\s*:/m.test(source)) {
218330
+ found.push({
218331
+ name: "openapi-config",
218332
+ path: configPath,
218333
+ backend: "config-only"
218334
+ });
218335
+ }
218336
+ } catch {
218337
+ }
218338
+ }
218339
+ }
218320
218340
  return found;
218321
218341
  }
218322
218342
  var SUBSYSTEMS, KNOWN_NAMES;
@@ -218359,6 +218379,16 @@ var init_subsystem_detect = __esm({
218359
218379
  description: "Event-to-job bridge (durable async fanout via @JobHandler.triggers)",
218360
218380
  backends: ["drizzle", "memory"],
218361
218381
  defaultBackend: "drizzle"
218382
+ },
218383
+ {
218384
+ // OPENAPI-4. "Config-only" pseudo-subsystem — the runtime helpers
218385
+ // (OpenApiRegistry, ErrorResponseDto) are already vendored by
218386
+ // `codegen project init`. Installing this subsystem just injects the
218387
+ // `openapi:` block into codegen.config.yaml.
218388
+ name: "openapi-config",
218389
+ description: "OpenAPI/Swagger config block (registry is vendored at init)",
218390
+ backends: ["config-only"],
218391
+ defaultBackend: "config-only"
218362
218392
  }
218363
218393
  ];
218364
218394
  KNOWN_NAMES = SUBSYSTEMS.map((s) => s.name);
@@ -218830,6 +218860,9 @@ var init_subsystem = __esm({
218830
218860
  );
218831
218861
  return 2;
218832
218862
  }
218863
+ if (desc.name === "openapi-config") {
218864
+ return this.executeOpenApiConfig(ctx);
218865
+ }
218833
218866
  const installed = await detectInstalledSubsystems(ctx);
218834
218867
  const already = installed.find((i) => i.name === desc.name);
218835
218868
  if (already && !this.force) {
@@ -219041,6 +219074,72 @@ var init_subsystem = __esm({
219041
219074
  }
219042
219075
  return 0;
219043
219076
  }
219077
+ /**
219078
+ * OPENAPI-4: install flow for the config-only `openapi-config`
219079
+ * pseudo-subsystem.
219080
+ *
219081
+ * Nothing to copy — `src/shared/openapi/*` was already vendored by
219082
+ * `codegen project init`. This method just invokes the
219083
+ * `subsystem/openapi-config` Hygen action to inject the `openapi:`
219084
+ * block into `codegen.config.yaml`, honoring the same `--force-config`
219085
+ * semantics as jobs/events/sync/bridge.
219086
+ */
219087
+ async executeOpenApiConfig(ctx) {
219088
+ const configPath = path17.join(ctx.cwd, "codegen.config.yaml");
219089
+ const outcome = planConfigBlockAction(configPath, "openapi", this.forceConfig);
219090
+ if (outcome === "parse-error") {
219091
+ printError(
219092
+ "codegen.config.yaml is not valid YAML: refusing to inject openapi config block. Fix the YAML and re-run."
219093
+ );
219094
+ return 1;
219095
+ }
219096
+ if (this.dryRun) {
219097
+ if (isJsonMode()) {
219098
+ printJson({
219099
+ command: "subsystem install",
219100
+ subsystem: "openapi-config",
219101
+ dryRun: true,
219102
+ configBlockOutcome: outcome,
219103
+ planned: [configPath]
219104
+ });
219105
+ } else {
219106
+ printInfo(`Dry run \u2014 openapi config block would be ${outcome}`);
219107
+ console.log(` ${theme.muted(icons.arrow)} ${path17.relative(ctx.cwd, configPath) || configPath}`);
219108
+ }
219109
+ return 0;
219110
+ }
219111
+ const configResult = runConfigBlockAction({
219112
+ cwd: ctx.cwd,
219113
+ actionFolder: "openapi-config",
219114
+ configPath,
219115
+ subsystem: "openapi",
219116
+ outcome,
219117
+ json: isJsonMode()
219118
+ });
219119
+ if (!configResult.ok) {
219120
+ printError(
219121
+ `openapi-config install failed: ${configResult.error ?? "unknown error"}`
219122
+ );
219123
+ return 1;
219124
+ }
219125
+ if (isJsonMode()) {
219126
+ printJson({
219127
+ command: "subsystem install",
219128
+ subsystem: "openapi-config",
219129
+ configBlockOutcome: outcome,
219130
+ configPath
219131
+ });
219132
+ return 0;
219133
+ }
219134
+ printSuccess(`openapi config block ${outcome === "skipped" ? "already present" : "installed"}.`);
219135
+ printInfo(
219136
+ "Install the peer deps: bun add @nestjs/swagger @anatine/zod-openapi"
219137
+ );
219138
+ printInfo(
219139
+ "Swagger UI mounts at /docs once main.ts calls SwaggerModule.setup(...) \u2014 see CONSUMER-SETUP.md \xA7OpenAPI."
219140
+ );
219141
+ return 0;
219142
+ }
219044
219143
  };
219045
219144
  SubsystemListCommand = class extends Command3 {
219046
219145
  static paths = [["subsystem", "list"]];
@@ -219179,22 +219278,158 @@ export class DatabaseModule {}
219179
219278
  `;
219180
219279
  }
219181
219280
  function appModuleContent() {
219182
- return `import { Module } from '@nestjs/common';
219281
+ return `import { Global, Module } from '@nestjs/common';
219183
219282
  import { DatabaseModule } from './shared/database/database.module';
219184
219283
  import { GENERATED_MODULES } from './generated/modules';
219284
+ import { OPENAPI_REGISTRY, OpenApiRegistry } from './shared/openapi';
219185
219285
 
219186
219286
  /**
219187
- * AppModule \u2014 wires DatabaseModule (global) + the GENERATED_MODULES barrel.
219287
+ * OpenApiModule \u2014 @Global() wrapper around the OPENAPI_REGISTRY singleton.
219288
+ *
219289
+ * OPENAPI-4: every generated entity module \`@Inject(OPENAPI_REGISTRY)\` to
219290
+ * register its Zod DTOs at onModuleInit (OPENAPI-2). NestJS's DI scoping
219291
+ * means providers declared in AppModule are NOT automatically visible
219292
+ * inside imported feature modules \u2014 exports from AppModule only flow to
219293
+ * modules that explicitly import AppModule, which feature modules don't.
219294
+ * Making the provider module \`@Global()\` broadcasts the token to every
219295
+ * module in the application graph; each generated module picks it up
219296
+ * without needing to import anything extra.
219297
+ *
219298
+ * \`useValue: new OpenApiRegistry()\` locks the instance to one per
219299
+ * process. Never instantiate \`new OpenApiRegistry()\` elsewhere \u2014 a
219300
+ * forked registry forks the schema table and produces a partial
219301
+ * /docs-json at boot.
219302
+ */
219303
+ @Global()
219304
+ @Module({
219305
+ providers: [{ provide: OPENAPI_REGISTRY, useValue: new OpenApiRegistry() }],
219306
+ exports: [OPENAPI_REGISTRY],
219307
+ })
219308
+ class OpenApiModule {}
219309
+
219310
+ /**
219311
+ * AppModule \u2014 wires DatabaseModule (global) + the OpenApiModule (global)
219312
+ * + the GENERATED_MODULES barrel.
219188
219313
  *
219189
219314
  * DatabaseModule must come first \u2014 it provides the DRIZZLE token that every
219190
- * generated repository depends on.
219315
+ * generated repository depends on. OpenApiModule follows so the registry
219316
+ * is in the global injector before any generated module's onModuleInit
219317
+ * fires. main.ts reads the built registry at boot to produce the Swagger
219318
+ * document (OPENAPI-4).
219191
219319
  */
219192
219320
  @Module({
219193
- imports: [DatabaseModule, ...GENERATED_MODULES],
219321
+ imports: [DatabaseModule, OpenApiModule, ...GENERATED_MODULES],
219194
219322
  })
219195
219323
  export class AppModule {}
219196
219324
  `;
219197
219325
  }
219326
+ function mainTsContent() {
219327
+ return `import 'reflect-metadata';
219328
+ import fs from 'node:fs';
219329
+ import path from 'node:path';
219330
+ import { NestFactory } from '@nestjs/core';
219331
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
219332
+ import { parse as parseYaml } from 'yaml';
219333
+ import { AppModule } from './app.module';
219334
+ import { OPENAPI_REGISTRY, OpenApiRegistry } from './shared/openapi';
219335
+
219336
+ interface OpenApiConfig {
219337
+ enabled?: boolean;
219338
+ path?: string;
219339
+ title?: string;
219340
+ version?: string;
219341
+ description?: string;
219342
+ auth?: 'bearer' | 'none';
219343
+ }
219344
+
219345
+ interface CodegenConfig {
219346
+ openapi?: OpenApiConfig;
219347
+ }
219348
+
219349
+ /**
219350
+ * Load \`codegen.config.yaml\` to pick up the \`openapi:\` block. Missing or
219351
+ * malformed \u2192 no config (Swagger disabled). The registry is the source of
219352
+ * truth for the document content; this just toggles whether Swagger UI
219353
+ * mounts + which metadata the header shows.
219354
+ */
219355
+ function loadConfig(): CodegenConfig {
219356
+ const configPath = path.resolve(process.cwd(), 'codegen.config.yaml');
219357
+ if (!fs.existsSync(configPath)) return {};
219358
+ try {
219359
+ const raw = fs.readFileSync(configPath, 'utf-8');
219360
+ const parsed = parseYaml(raw);
219361
+ return (parsed && typeof parsed === 'object' ? parsed : {}) as CodegenConfig;
219362
+ } catch {
219363
+ return {};
219364
+ }
219365
+ }
219366
+
219367
+ async function bootstrap(): Promise<void> {
219368
+ const config = loadConfig();
219369
+ const app = await NestFactory.create(AppModule);
219370
+ app.enableShutdownHooks();
219371
+
219372
+ if (config.openapi?.enabled) {
219373
+ // OPENAPI-4: build the document in two passes.
219374
+ //
219375
+ // 1. Our vendored \`OpenApiRegistry\` owns the component schemas
219376
+ // (Zod-derived DTOs registered by every generated module at
219377
+ // onModuleInit \u2014 OPENAPI-2).
219378
+ // 2. \`SwaggerModule.createDocument\` scans controller decorators
219379
+ // (@Api* \u2014 OPENAPI-3) and produces the \`paths\` map. Our
219380
+ // schemas then get merged into the \`components.schemas\` it
219381
+ // emits by reference.
219382
+ //
219383
+ // Both passes are needed: Nest's scanner is the source of truth for
219384
+ // paths (it knows the routes); the registry is the source of truth
219385
+ // for schemas (Zod can't be inferred from reflection metadata).
219386
+ const registry = app.get<OpenApiRegistry>(OPENAPI_REGISTRY);
219387
+ const registryDocument = await registry.build({
219388
+ title: config.openapi.title ?? 'API',
219389
+ version: config.openapi.version ?? '0.0.0',
219390
+ description: config.openapi.description,
219391
+ });
219392
+
219393
+ const docBuilder = new DocumentBuilder()
219394
+ .setTitle(config.openapi.title ?? 'API')
219395
+ .setVersion(config.openapi.version ?? '0.0.0');
219396
+ if (config.openapi.description) docBuilder.setDescription(config.openapi.description);
219397
+ if ((config.openapi.auth ?? 'bearer') === 'bearer') docBuilder.addBearerAuth();
219398
+
219399
+ const nestDocument = SwaggerModule.createDocument(app, docBuilder.build());
219400
+
219401
+ // Merge registry-owned component schemas on top of whatever Nest's
219402
+ // decorator scanner produced. Controllers reference schemas by
219403
+ // \`$ref\` (OPENAPI-3), so this merge is what actually resolves the
219404
+ // refs consumers see in /docs-json.
219405
+ // Registry schemas are typed Record<string, unknown> locally
219406
+ // (avoids depending on openapi3-ts); the runtime shape matches Nest's
219407
+ // SchemaObject \u2014 generateSchema(zodRef, false, '3.0') emits valid
219408
+ // OpenAPI 3.0 schema objects. Cast to satisfy Nest's stricter typing.
219409
+ nestDocument.components = {
219410
+ ...nestDocument.components,
219411
+ schemas: {
219412
+ ...(nestDocument.components?.schemas ?? {}),
219413
+ ...registryDocument.components.schemas,
219414
+ } as NonNullable<typeof nestDocument.components>['schemas'],
219415
+ };
219416
+
219417
+ SwaggerModule.setup(config.openapi.path ?? '/docs', app, nestDocument);
219418
+ }
219419
+
219420
+ const port = Number(process.env.PORT ?? 3000);
219421
+ await app.listen(port);
219422
+ // eslint-disable-next-line no-console
219423
+ console.log(\`Application listening on http://localhost:\${port}\`);
219424
+ }
219425
+
219426
+ bootstrap().catch((err) => {
219427
+ // eslint-disable-next-line no-console
219428
+ console.error('Failed to start application:', err);
219429
+ process.exit(1);
219430
+ });
219431
+ `;
219432
+ }
219198
219433
  function rootSchemaContent() {
219199
219434
  return `/**
219200
219435
  * Drizzle schema root.
@@ -219522,6 +219757,24 @@ async function buildInitPlan(ctx, options) {
219522
219757
  });
219523
219758
  }
219524
219759
  }
219760
+ {
219761
+ const mainPath = path18.join(cwd, "src", "main.ts");
219762
+ if (!fs10.existsSync(mainPath)) {
219763
+ entries.push({
219764
+ path: mainPath,
219765
+ relPath: relOf(cwd, mainPath),
219766
+ action: "create",
219767
+ content: mainTsContent()
219768
+ });
219769
+ } else {
219770
+ entries.push({
219771
+ path: mainPath,
219772
+ relPath: relOf(cwd, mainPath),
219773
+ action: "skip",
219774
+ reason: "exists \u2014 copy the OpenAPI bootstrap block from CONSUMER-SETUP \xA7OpenAPI manually"
219775
+ });
219776
+ }
219777
+ }
219525
219778
  {
219526
219779
  const schemaPath = path18.join(cwd, "src", "schema.ts");
219527
219780
  if (!fs10.existsSync(schemaPath)) {
@@ -219639,7 +219892,22 @@ var init_init_scaffold = __esm({
219639
219892
  // @Body() to give runtime Zod validation at the controller boundary.
219640
219893
  { runtime: "pipes/zod-validation.pipe.ts", target: "src/shared/pipes/zod-validation.pipe.ts" },
219641
219894
  // EAV helpers — referenced by generated services on `eav_value_table` entities
219642
- { runtime: "eav-helpers.ts", target: "src/shared/eav-helpers.ts" }
219895
+ { runtime: "eav-helpers.ts", target: "src/shared/eav-helpers.ts" },
219896
+ // OpenAPI registry (OPENAPI-1/2) — generated modules register Zod DTOs
219897
+ // here at onModuleInit; OPENAPI-4 mounts Swagger UI off the same registry.
219898
+ // `@anatine/zod-openapi` is an optional peer dep (lazy-imported on build).
219899
+ { runtime: "shared/openapi/registry.ts", target: "src/shared/openapi/registry.ts" },
219900
+ { runtime: "shared/openapi/registry.tokens.ts", target: "src/shared/openapi/registry.tokens.ts" },
219901
+ { runtime: "shared/openapi/errors.ts", target: "src/shared/openapi/errors.ts" },
219902
+ // OPENAPI-3: shared error response schema referenced by every generated
219903
+ // controller's 4xx `@ApiResponse` `$ref`. Auto-registered in the registry
219904
+ // constructor so consumer projects expose `ErrorResponseDto` on
219905
+ // `/docs-json` without per-entity duplication.
219906
+ {
219907
+ runtime: "shared/openapi/error-response.dto.ts",
219908
+ target: "src/shared/openapi/error-response.dto.ts"
219909
+ },
219910
+ { runtime: "shared/openapi/index.ts", target: "src/shared/openapi/index.ts" }
219643
219911
  ];
219644
219912
  REQUIRED_ALIASES = {
219645
219913
  "@shared/*": ["./src/shared/*"],