@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.
- package/README.md +9 -4
- package/dist/src/cli/index.js +136 -128
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +10 -1
- package/templates/entity/new/backend/application/commands/create.ejs.t +38 -1
- package/templates/entity/new/backend/application/commands/delete.ejs.t +41 -1
- package/templates/entity/new/backend/application/commands/update.ejs.t +42 -1
- package/templates/entity/new/backend/database/repository.ejs.t +33 -3
- package/templates/entity/new/backend/domain/repository-interface.ejs.t +6 -3
- package/templates/entity/new/backend/modules/core/module.ejs.t +6 -0
- package/templates/entity/new/backend/presentation/controller.ejs.t +32 -10
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +72 -11
- package/templates/entity/new/clean-lite-ps/entity.ejs.t +16 -2
- package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -1
- package/templates/entity/new/clean-lite-ps/module.ejs.t +45 -2
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +459 -98
- package/templates/entity/new/clean-lite-ps/repository.ejs.t +57 -4
- package/templates/entity/new/clean-lite-ps/search-controller.ejs.t +50 -0
- package/templates/entity/new/clean-lite-ps/service.ejs.t +98 -1
- package/templates/entity/new/clean-lite-ps/use-cases/create.ejs.t +150 -0
- package/templates/entity/new/clean-lite-ps/use-cases/delete.ejs.t +70 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id-with-fields.ejs.t +19 -0
- package/templates/entity/new/clean-lite-ps/use-cases/find-by-id.ejs.t +7 -3
- package/templates/entity/new/clean-lite-ps/use-cases/list-with-fields.ejs.t +17 -0
- package/templates/entity/new/clean-lite-ps/use-cases/search.ejs.t +63 -0
- package/templates/entity/new/clean-lite-ps/use-cases/update.ejs.t +153 -0
- package/templates/entity/new/prompt.js +284 -41
- package/templates/relationship/new/entity.ejs.t +2 -2
- package/templates/relationship/new/prompt.js +3 -7
- package/templates/relationship/new/service.ejs.t +1 -1
- package/templates/subsystem/bridge/generated-keep.ejs.t +4 -0
- package/templates/subsystem/bridge/prompt.js +36 -0
- package/templates/subsystem/bridge-config/codegen-config-bridge-block.ejs.t +20 -0
- package/templates/subsystem/bridge-config/prompt.js +20 -0
- package/templates/subsystem/events/domain-events.schema.ejs.t +81 -0
- package/templates/subsystem/events/generated-keep.ejs.t +4 -0
- package/templates/subsystem/events/prompt.js +39 -0
- package/templates/subsystem/events-config/codegen-config-events-block.ejs.t +26 -0
- package/templates/subsystem/events-config/prompt.js +20 -0
- package/templates/subsystem/jobs/job-orchestration.schema.ejs.t +221 -0
- package/templates/subsystem/jobs/main-hook.ejs.t +11 -0
- package/templates/subsystem/jobs/prompt.js +40 -0
- package/templates/subsystem/jobs/worker.ejs.t +82 -0
- package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +55 -0
- package/templates/subsystem/jobs-config/prompt.js +20 -0
- package/templates/subsystem/sync/prompt.js +43 -0
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +195 -0
- package/templates/subsystem/sync-config/codegen-config-sync-block.ejs.t +29 -0
- 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
|
+
};
|