@pattern-stack/codegen 0.2.0 → 0.3.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 (52) hide show
  1. package/README.md +9 -4
  2. package/dist/src/cli/index.js +136 -128
  3. package/dist/src/cli/index.js.map +1 -1
  4. package/dist/src/index.d.ts +16 -0
  5. package/dist/src/index.js +25 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/package.json +10 -1
  8. package/templates/entity/new/backend/application/commands/create.ejs.t +38 -1
  9. package/templates/entity/new/backend/application/commands/delete.ejs.t +41 -1
  10. package/templates/entity/new/backend/application/commands/update.ejs.t +42 -1
  11. package/templates/entity/new/backend/database/repository.ejs.t +33 -3
  12. package/templates/entity/new/backend/domain/repository-interface.ejs.t +6 -3
  13. package/templates/entity/new/backend/modules/core/module.ejs.t +6 -0
  14. package/templates/entity/new/backend/presentation/controller.ejs.t +32 -10
  15. package/templates/entity/new/clean-lite-ps/controller.ejs.t +72 -11
  16. package/templates/entity/new/clean-lite-ps/entity.ejs.t +16 -2
  17. package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -1
  18. package/templates/entity/new/clean-lite-ps/module.ejs.t +45 -2
  19. package/templates/entity/new/clean-lite-ps/prompt-extension.js +459 -98
  20. package/templates/entity/new/clean-lite-ps/repository.ejs.t +57 -4
  21. package/templates/entity/new/clean-lite-ps/search-controller.ejs.t +50 -0
  22. package/templates/entity/new/clean-lite-ps/service.ejs.t +98 -1
  23. package/templates/entity/new/clean-lite-ps/use-cases/create.ejs.t +150 -0
  24. package/templates/entity/new/clean-lite-ps/use-cases/delete.ejs.t +70 -0
  25. package/templates/entity/new/clean-lite-ps/use-cases/find-by-id-with-fields.ejs.t +19 -0
  26. package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +7 -3
  27. package/templates/entity/new/clean-lite-ps/use-cases/list-with-fields.ejs.t +17 -0
  28. package/templates/entity/new/clean-lite-ps/use-cases/search.ejs.t +63 -0
  29. package/templates/entity/new/clean-lite-ps/use-cases/update.ejs.t +153 -0
  30. package/templates/entity/new/prompt.js +284 -41
  31. package/templates/relationship/new/entity.ejs.t +2 -2
  32. package/templates/relationship/new/prompt.js +3 -7
  33. package/templates/relationship/new/service.ejs.t +1 -1
  34. package/templates/subsystem/bridge/generated-keep.ejs.t +4 -0
  35. package/templates/subsystem/bridge/prompt.js +36 -0
  36. package/templates/subsystem/bridge-config/codegen-config-bridge-block.ejs.t +20 -0
  37. package/templates/subsystem/bridge-config/prompt.js +20 -0
  38. package/templates/subsystem/events/domain-events.schema.ejs.t +81 -0
  39. package/templates/subsystem/events/generated-keep.ejs.t +4 -0
  40. package/templates/subsystem/events/prompt.js +39 -0
  41. package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +26 -0
  42. package/templates/subsystem/events-config/prompt.js +20 -0
  43. package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +221 -0
  44. package/templates/subsystem/jobs/main-hook.ejs.t +11 -0
  45. package/templates/subsystem/jobs/prompt.js +40 -0
  46. package/templates/subsystem/jobs/worker.ejs.t +82 -0
  47. package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +55 -0
  48. package/templates/subsystem/jobs-config/prompt.js +20 -0
  49. package/templates/subsystem/sync/prompt.js +43 -0
  50. package/templates/subsystem/sync/sync-audit.schema.ejs.t +195 -0
  51. package/templates/subsystem/sync-config/codegen-config-sync-block.ejs.t +29 -0
  52. package/templates/subsystem/sync-config/prompt.js +22 -0
@@ -0,0 +1,195 @@
1
+ ---
2
+ to: "<%= schemaPath %>"
3
+ force: true
4
+ ---
5
+ /**
6
+ * Drizzle schema for the sync subsystem audit/observability tables (SYNC-1).
7
+ *
8
+ * Three tables model end-to-end sync observability, keyed by the single
9
+ * port every sync adapter implements (`IChangeSource<T>` from SYNC-2):
10
+ *
11
+ * - `sync_subscriptions` — owns the cursor per
12
+ * `(integration_id, adapter, domain, external_ref)` tuple. Addressed
13
+ * by id by `ICursorStore` (SYNC-3/SYNC-4).
14
+ * - `sync_runs` — per-run audit log: start/complete, status,
15
+ * cursor before/after, counts, direction + action.
16
+ * - `sync_run_items` — per-record change log with structured
17
+ * `changed_fields` jsonb (ADR-0003). The `FieldDiff` type alias
18
+ * is owned by the sync subsystem's runtime protocol
19
+ * (`sync-field-diff.protocol.ts` from SYNC-2).
20
+ *
21
+ * ## `tenant_id` columns — scaffold-time conditional
22
+ *
23
+ * When `sync.multi_tenant: true` in `codegen.config.yaml`, this schema
24
+ * emits `tenant_id` as a nullable text column on all three tables. The
25
+ * `SYNC_MULTI_TENANT` DI flag (SYNC-6) enforces non-null at runtime
26
+ * across the orchestrator + Drizzle backends. Enabling post-install
27
+ * requires reinstalling this subsystem (`subsystem install sync --force
28
+ * --force-config`) plus an Atlas migration.
29
+ *
30
+ * See SYNC-1 / SYNC-6 in epic #60 for the decision rationale.
31
+ */
32
+ import {
33
+ pgEnum,
34
+ pgTable,
35
+ uuid,
36
+ text,
37
+ jsonb,
38
+ integer,
39
+ boolean,
40
+ timestamp,
41
+ index,
42
+ uniqueIndex,
43
+ } from 'drizzle-orm/pg-core';
44
+ import type { InferSelectModel } from 'drizzle-orm';
45
+
46
+ // NOTE: the `FieldDiff` type alias is imported from the runtime protocol
47
+ // shipped by `subsystem install sync`. If you moved that file, fix this
48
+ // import to point at the new location.
49
+ import type { FieldDiff } from './sync-field-diff.protocol';
50
+
51
+ // ─── Enums ──────────────────────────────────────────────────────────────────
52
+
53
+ export const syncRunDirectionEnum = pgEnum('sync_run_direction', [
54
+ 'inbound',
55
+ 'outbound',
56
+ ]);
57
+
58
+ export const syncRunActionEnum = pgEnum('sync_run_action', [
59
+ 'poll',
60
+ 'cdc',
61
+ 'webhook',
62
+ 'manual',
63
+ 'writeback',
64
+ ]);
65
+
66
+ export const syncRunStatusEnum = pgEnum('sync_run_status', [
67
+ 'running',
68
+ 'success',
69
+ 'no_changes',
70
+ 'failed',
71
+ ]);
72
+
73
+ export const syncRunItemOperationEnum = pgEnum('sync_run_item_operation', [
74
+ 'created',
75
+ 'updated',
76
+ 'deleted',
77
+ 'noop',
78
+ ]);
79
+
80
+ export const syncRunItemStatusEnum = pgEnum('sync_run_item_status', [
81
+ 'success',
82
+ 'failed',
83
+ 'skipped',
84
+ ]);
85
+
86
+ // ─── sync_subscriptions ─────────────────────────────────────────────────────
87
+
88
+ export const syncSubscriptions = pgTable(
89
+ 'sync_subscriptions',
90
+ {
91
+ id: uuid('id').primaryKey().defaultRandom(),
92
+ integrationId: text('integration_id').notNull(),
93
+ adapter: text('adapter').notNull(),
94
+ domain: text('domain').notNull(),
95
+ externalRef: text('external_ref'),
96
+ enabled: boolean('enabled').notNull().default(true),
97
+ config: jsonb('config').notNull().default({}).$type<Record<string, unknown>>(),
98
+ cursor: jsonb('cursor').$type<unknown>(),
99
+ lastSyncAt: timestamp('last_sync_at', { withTimezone: true }),
100
+ <% if (multiTenant) { -%>
101
+ tenantId: text('tenant_id'), // scaffold-time conditional — see sync.multi_tenant
102
+ <% } -%>
103
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
104
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
105
+ },
106
+ (t) => ({
107
+ uqSyncSubscriptionTuple: uniqueIndex('uq_sync_subscriptions_tuple').on(
108
+ t.integrationId,
109
+ t.adapter,
110
+ t.domain,
111
+ t.externalRef,
112
+ ),
113
+ idxSyncSubscriptionsEnabledLastSync: index(
114
+ 'idx_sync_subscriptions_enabled_last_sync',
115
+ ).on(t.enabled, t.lastSyncAt),
116
+ }),
117
+ );
118
+
119
+ export type SyncSubscriptionRow = InferSelectModel<typeof syncSubscriptions>;
120
+
121
+ // ─── sync_runs ──────────────────────────────────────────────────────────────
122
+
123
+ export const syncRuns = pgTable(
124
+ 'sync_runs',
125
+ {
126
+ id: uuid('id').primaryKey().defaultRandom(),
127
+ subscriptionId: uuid('subscription_id')
128
+ .notNull()
129
+ .references(() => syncSubscriptions.id, { onDelete: 'cascade' }),
130
+ direction: syncRunDirectionEnum('direction').notNull(),
131
+ action: syncRunActionEnum('action').notNull(),
132
+ status: syncRunStatusEnum('status').notNull().default('running'),
133
+ recordsFound: integer('records_found').notNull().default(0),
134
+ recordsProcessed: integer('records_processed').notNull().default(0),
135
+ cursorBefore: jsonb('cursor_before').$type<unknown>(),
136
+ cursorAfter: jsonb('cursor_after').$type<unknown>(),
137
+ durationMs: integer('duration_ms'),
138
+ error: text('error'),
139
+ startedAt: timestamp('started_at', { withTimezone: true })
140
+ .notNull()
141
+ .defaultNow(),
142
+ completedAt: timestamp('completed_at', { withTimezone: true }),
143
+ <% if (multiTenant) { -%>
144
+ tenantId: text('tenant_id'), // scaffold-time conditional — see sync.multi_tenant
145
+ <% } -%>
146
+ },
147
+ (t) => ({
148
+ idxSyncRunsSubscriptionStartedAt: index(
149
+ 'idx_sync_runs_subscription_started_at',
150
+ ).on(t.subscriptionId, t.startedAt),
151
+ idxSyncRunsStatusStartedAt: index('idx_sync_runs_status_started_at').on(
152
+ t.status,
153
+ t.startedAt,
154
+ ),
155
+ }),
156
+ );
157
+
158
+ export type SyncRunRow = InferSelectModel<typeof syncRuns>;
159
+
160
+ // ─── sync_run_items ─────────────────────────────────────────────────────────
161
+
162
+ export const syncRunItems = pgTable(
163
+ 'sync_run_items',
164
+ {
165
+ id: uuid('id').primaryKey().defaultRandom(),
166
+ syncRunId: uuid('sync_run_id')
167
+ .notNull()
168
+ .references(() => syncRuns.id, { onDelete: 'cascade' }),
169
+ entityType: text('entity_type').notNull(),
170
+ externalId: text('external_id').notNull(),
171
+ localId: text('local_id'),
172
+ operation: syncRunItemOperationEnum('operation').notNull(),
173
+ status: syncRunItemStatusEnum('status').notNull(),
174
+ changedFields: jsonb('changed_fields').notNull().default({}).$type<FieldDiff>(),
175
+ title: text('title'),
176
+ error: text('error'),
177
+ createdAt: timestamp('created_at', { withTimezone: true })
178
+ .notNull()
179
+ .defaultNow(),
180
+ <% if (multiTenant) { -%>
181
+ tenantId: text('tenant_id'), // scaffold-time conditional — see sync.multi_tenant
182
+ <% } -%>
183
+ },
184
+ (t) => ({
185
+ idxSyncRunItemsRunCreatedAt: index('idx_sync_run_items_run_created_at').on(
186
+ t.syncRunId,
187
+ t.createdAt,
188
+ ),
189
+ idxSyncRunItemsEntityExternal: index(
190
+ 'idx_sync_run_items_entity_external',
191
+ ).on(t.entityType, t.externalId),
192
+ }),
193
+ );
194
+
195
+ export type SyncRunItemRow = InferSelectModel<typeof syncRunItems>;
@@ -0,0 +1,29 @@
1
+ ---
2
+ to: "<%= configPath %>"
3
+ inject: true
4
+ append: true
5
+ skip_if: "sync:"
6
+ ---
7
+
8
+ sync:
9
+ # ── Backend selection (core/extension model — see CLAUDE.md) ──
10
+ # 'drizzle' is the production backend (Postgres cursor store +
11
+ # sync_runs / sync_run_items audit log). 'memory' is the in-process
12
+ # test backend (MemoryCursorStore + MemoryRunRecorder).
13
+ backend: drizzle
14
+
15
+ # ── Multi-tenancy (SYNC-6 / ADR-008) ──
16
+ # When true:
17
+ # - the generated schema gains `tenant_id` columns on all three
18
+ # sync tables;
19
+ # - `ExecuteSyncUseCase.execute(...)` throws `MissingTenantIdError`
20
+ # when called with a null / missing `tenantId`;
21
+ # - `PostgresCursorStore` + `DrizzleSyncRunRecorder` throw the same
22
+ # error at their write boundary (defense in depth);
23
+ # - `MemoryCursorStore` + `MemoryRunRecorder` accept `tenantId` and
24
+ # record it on their in-memory rows but do not throw — memory
25
+ # state is process-local; cross-tenant isolation there is not
26
+ # meaningful.
27
+ # Enabling post-install requires a reinstall (`subsystem install sync
28
+ # --force --force-config`) plus an Atlas migration.
29
+ multi_tenant: false
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hygen prompt.js — SYNC-7 sync config-block scaffold.
3
+ *
4
+ * Split from `templates/subsystem/sync/` so the CLI can invoke the
5
+ * config-block inject step independently of the rest of the sync scaffold.
6
+ * This lets `subsystem install sync --force` preserve an existing `sync:`
7
+ * block by skipping this action entirely, while `--force-config` opts in
8
+ * to regenerating it.
9
+ *
10
+ * Mirrors `events-config` exactly.
11
+ *
12
+ * Invoked via:
13
+ * bunx hygen subsystem sync-config --configPath <abs>
14
+ */
15
+
16
+ export default {
17
+ prompt: async ({ args }) => {
18
+ return {
19
+ configPath: args.configPath ?? "codegen.config.yaml",
20
+ };
21
+ },
22
+ };