@pattern-stack/codegen 0.6.6 → 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.
@@ -0,0 +1,21 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { IntegrationService } from '../../integration.service';
3
+ import type { Integration } from '../../integration.entity';
4
+
5
+ /**
6
+ * Lists a user's integrations newest-first. Used by the settings page
7
+ * (`GET /integrations`) and any "which providers are connected?" UI.
8
+ *
9
+ * Returns rows with ciphertexts intact — callers should NOT pass these
10
+ * to the frontend. Use `IntegrationsService.listByUser` if you need
11
+ * the consumer-facing facade behavior (which strips ciphertexts before
12
+ * returning).
13
+ */
14
+ @Injectable()
15
+ export class ListUserIntegrationsUseCase {
16
+ constructor(private readonly integrations: IntegrationService) {}
17
+
18
+ async execute(userId: string): Promise<Integration[]> {
19
+ return this.integrations.findByUserId(userId);
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { IntegrationService } from '../../integration.service';
3
+ import type { Integration } from '../../integration.entity';
4
+
5
+ /**
6
+ * Flips an integration's status to `requires_reauth`. Called when the
7
+ * refresh path raises `IntegrationBrokenError` (refresh token rejected,
8
+ * scopes revoked, etc.) — see `OAuth2RefreshStrategy` + `withAuthRetry`.
9
+ *
10
+ * Idempotent: calling on an already-broken row is a no-op write.
11
+ */
12
+ @Injectable()
13
+ export class MarkIntegrationRequiresReauthUseCase {
14
+ constructor(private readonly integrations: IntegrationService) {}
15
+
16
+ async execute(integrationId: string): Promise<Integration> {
17
+ return this.integrations.update(integrationId, {
18
+ status: 'requires_reauth',
19
+ });
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.6.6",
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",
@@ -40,6 +40,7 @@
40
40
  "dist",
41
41
  "runtime",
42
42
  "templates",
43
+ "examples/auth-integrations/**",
43
44
  "src/config/*.mjs",
44
45
  "src/schema/naming-config.schema.mjs",
45
46
  "src/patterns/registry.ts",
@@ -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,