@pattern-stack/codegen 0.6.7 → 0.6.8

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 CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.8] — 2026-04-28
8
+
9
+ Hotfix for enum codegen in the `clean-lite-ps` template pipeline. Surfaced during integration-patterns Wave 0b: enum-typed YAML fields emitted a Drizzle `text()` column instead of a `pgEnum`, so `InferSelectModel` resolved to `string` instead of the literal-union type and forced hand-casts in consumer code (e.g. `as DecryptedIntegrationRow['status']`).
10
+
11
+ ### Fixed
12
+
13
+ - **`fix(codegen)` — clean-lite-ps enum emission.** `templates/entity/new/clean-lite-ps/prompt-extension.js` now produces `pgEnum` declarations + column references for any field with `choices` (or `type: enum`), matching the backend pipeline at `templates/entity/new/backend/database/schema.ejs.t:66-104`. Generated entity files now contain `export const xEnum = pgEnum('x', [...])` ahead of the `pgTable(...)` block and reference `xEnum('x').notNull()` inside the column map. `pgEnum` is added to the `drizzle-orm/pg-core` import list automatically when any enum field is present. The emitted `InferSelectModel` type now narrows to the literal union — consumers can drop `as Row['status']` casts. New unit coverage: `src/__tests__/clean-lite-ps/entity-enum-template.test.ts`.
14
+
15
+ ### Migration note
16
+
17
+ Not a breaking change for the emitted code shape (the field's TypeScript type narrows — a strict superset of what consumer code can do). Existing Postgres databases generated against 0.6.7 or earlier will need a one-time `CREATE TYPE … AS ENUM (...)` + `ALTER TABLE … ALTER COLUMN x TYPE x_enum USING x::x_enum;` migration, since the column was previously `text`. Greenfield projects, or anyone regenerating before the first migration runs, are unaffected.
18
+
7
19
  ## [0.6.7] — 2026-04-28
8
20
 
9
21
  Hotfix bundle for `cdp subsystem install auth-integrations`. 0.6.5 / 0.6.6 shipped the auth-integrations starter and install template, but every downstream consumer ran into four blockers on a fresh install. None of them surfaced from the source-checkout smoke (the install code resolves examples/ via the package root, which exists in dev) — they only exposed themselves through `npm install + bunx cdp subsystem install auth-integrations` against the published tarball. Bundles a fifth fix that unifies the integrations folder layout. Surfaced by integration-patterns Wave 0b.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,12 @@ import { type InferSelectModel } from 'drizzle-orm';
18
18
  import { <%= rel.relatedTable %> } from '<%= rel.importPath %>';
19
19
  <%_ } _%>
20
20
  <%_ }) _%>
21
+ <%_ if (typeof clpEnumFields !== 'undefined' && clpEnumFields.length > 0) { _%>
22
+
23
+ <%_ clpEnumFields.forEach(ef => { _%>
24
+ export const <%= ef.enumName %> = pgEnum('<%= ef.dbName %>', [<%- ef.choices.map(c => `'${c}'`).join(', ') %>]);
25
+ <%_ }) _%>
26
+ <%_ } _%>
21
27
 
22
28
  export const <%= entityNamePlural %> = pgTable(
23
29
  '<%= entityNamePlural %>',
@@ -201,7 +201,7 @@ const EXTERNAL_ID_TRACKING_FIELDS = new Set([
201
201
  /**
202
202
  * Build a Drizzle column chain for a field
203
203
  */
204
- function buildDrizzleChain(fieldName, field, drizzleType) {
204
+ function buildDrizzleChain(fieldName, field, drizzleType, enumName) {
205
205
  const nullable = field.nullable ?? false;
206
206
  const required = field.required ?? false;
207
207
  const hasDefault = field.default !== undefined && field.default !== null;
@@ -211,7 +211,11 @@ function buildDrizzleChain(fieldName, field, drizzleType) {
211
211
  // schemas using z.coerce.date() align with the entity type.
212
212
  // `timestamp` already defaults to Date — no mode override needed.
213
213
  let chain;
214
- if (drizzleType === 'date') {
214
+ if (drizzleType === 'enum' && enumName) {
215
+ // Reference the pgEnum declaration emitted at the top of the entity file.
216
+ // The column name argument keeps the snake_case YAML field name.
217
+ chain = `${enumName}('${fieldName}')`;
218
+ } else if (drizzleType === 'date') {
215
219
  chain = `${drizzleType}('${fieldName}', { mode: 'date' })`;
216
220
  } else {
217
221
  chain = `${drizzleType}('${fieldName}')`;
@@ -247,7 +251,15 @@ function processFields(fields) {
247
251
  const choices = field.choices;
248
252
  const hasChoices = Array.isArray(choices) && choices.length > 0;
249
253
 
250
- const drizzleType = DRIZZLE_TYPE_MAP[type] || 'text';
254
+ // Enum-typed fields (or any field with a `choices` list) emit a
255
+ // Postgres-native pgEnum declaration + column reference, so the
256
+ // generated `InferSelectModel` type narrows to the literal union
257
+ // instead of falling back to `string`. Matches the backend pipeline
258
+ // (templates/entity/new/backend/database/schema.ejs.t:66-104).
259
+ const drizzleType = hasChoices
260
+ ? 'enum'
261
+ : (DRIZZLE_TYPE_MAP[type] || 'text');
262
+ const enumName = hasChoices ? camelCase(fieldName) + 'Enum' : null;
251
263
  const tsType = hasChoices
252
264
  ? choices.map((c) => `'${c}'`).join(' | ')
253
265
  : (TS_TYPE_MAP[type] || 'unknown');
@@ -255,7 +267,7 @@ function processFields(fields) {
255
267
  ? `z.enum([${choices.map((c) => `'${c}'`).join(', ')}])`
256
268
  : (ZOD_TYPE_MAP[type] || 'z.unknown()');
257
269
 
258
- const drizzleChain = buildDrizzleChain(fieldName, field, drizzleType);
270
+ const drizzleChain = buildDrizzleChain(fieldName, field, drizzleType, enumName);
259
271
 
260
272
  processed.push({
261
273
  name: fieldName,
@@ -271,6 +283,7 @@ function processFields(fields) {
271
283
  drizzleChain,
272
284
  choices,
273
285
  hasChoices,
286
+ enumName,
274
287
  });
275
288
  }
276
289
 
@@ -355,6 +368,12 @@ function collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSof
355
368
  const imports = new Set(['pgTable', 'uuid']);
356
369
 
357
370
  for (const field of processedFields) {
371
+ if (field.drizzleType === 'enum') {
372
+ // Enum columns reference a `pgEnum` declaration emitted at the top
373
+ // of the entity file; the helper itself comes from drizzle-orm/pg-core.
374
+ imports.add('pgEnum');
375
+ continue;
376
+ }
358
377
  const importName = DRIZZLE_IMPORT_MAP[field.drizzleType];
359
378
  if (importName) imports.add(importName);
360
379
  }
@@ -783,6 +802,18 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
783
802
  const fkFieldNames = new Set(belongsTo.map((r) => r.field));
784
803
  const nonFkFields = processedFields.filter((f) => !fkFieldNames.has(f.name));
785
804
 
805
+ // Enum field declarations — surface a separate collection so the entity
806
+ // template can emit `export const xEnum = pgEnum('x', [...])` ahead of
807
+ // the `pgTable(...)` block. Both FK-filtered and unfiltered processing
808
+ // include the same enum fields; they're never FKs.
809
+ const clpEnumFields = processedFields
810
+ .filter((f) => f.hasChoices && f.enumName)
811
+ .map((f) => ({
812
+ enumName: f.enumName,
813
+ dbName: f.name,
814
+ choices: f.choices,
815
+ }));
816
+
786
817
  // Drizzle imports needed
787
818
  const drizzleEntityImports = collectDrizzleImports(processedFields, belongsTo, hasTimestamps, hasSoftDelete, hasExternalIdTracking);
788
819
  // Whether relations() import is needed
@@ -980,6 +1011,7 @@ export function buildCleanLitePsLocals(definition, baseLocals) {
980
1011
  // Drizzle
981
1012
  clpDrizzleImports: drizzleEntityImports,
982
1013
  clpHasRelationsBlock: hasRelationsBlock,
1014
+ clpEnumFields,
983
1015
 
984
1016
  // Declarative queries
985
1017
  processedQueries,