@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.
- package/CHANGELOG.md +27 -0
- package/dist/src/cli/index.js +55 -19
- package/dist/src/cli/index.js.map +1 -1
- package/examples/auth-integrations/README.md +125 -0
- package/examples/auth-integrations/definitions/entities/integration.yaml +98 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-grant-sink.adapter.ts +29 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-reader.adapter.ts +52 -0
- package/examples/auth-integrations/runtime/integrations/adapters/integration-token-writer.adapter.ts +43 -0
- package/examples/auth-integrations/runtime/integrations/facade/integrations.service.ts +150 -0
- package/examples/auth-integrations/runtime/integrations/integrations-auth.module.ts +81 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/create-or-update-from-oauth-grant.use-case.ts +75 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/disconnect-integration.use-case.ts +29 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/list-user-integrations.use-case.ts +21 -0
- package/examples/auth-integrations/runtime/integrations/oauth/use-cases/mark-integration-requires-reauth.use-case.ts +21 -0
- package/package.json +2 -1
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +6 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +36 -4
|
@@ -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.
|
|
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 === '
|
|
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
|
-
|
|
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,
|