@pattern-stack/codegen 0.22.0 → 0.24.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/CHANGELOG.md +56 -1
- package/consumer-skills/integration/SKILL.md +11 -3
- package/dist/{chunk-XKWOJZZ4.js → chunk-37PILMIT.js} +4 -4
- package/dist/{chunk-NR7QQ6ZI.js → chunk-6M6LZEP6.js} +3 -3
- package/dist/{chunk-VDL5CJ5C.js → chunk-7B7MMDOJ.js} +54 -1
- package/dist/chunk-7B7MMDOJ.js.map +1 -0
- package/dist/{chunk-NXHL5YII.js → chunk-7LKAMLV4.js} +4 -4
- package/dist/{chunk-6DQEIXYU.js → chunk-CKLM57IE.js} +10 -10
- package/dist/chunk-CKLM57IE.js.map +1 -0
- package/dist/{chunk-QXVCRA23.js → chunk-ENAR3F5S.js} +9 -4
- package/dist/chunk-ENAR3F5S.js.map +1 -0
- package/dist/{chunk-FFUDEIFF.js → chunk-HN5HT5WL.js} +2 -2
- package/dist/{chunk-6ECCJVYW.js → chunk-K4BQQ2NN.js} +46 -2
- package/dist/chunk-K4BQQ2NN.js.map +1 -0
- package/dist/{chunk-QFUIE37H.js → chunk-KFXXOFDC.js} +4 -4
- package/dist/{chunk-O2A6XHGD.js → chunk-LLDJS7PJ.js} +2 -2
- package/dist/{chunk-JOBQ6RUU.js → chunk-LQZESSM3.js} +28 -1
- package/dist/chunk-LQZESSM3.js.map +1 -0
- package/dist/{chunk-JRQO2IOF.js → chunk-MU54DZCC.js} +27 -1
- package/dist/chunk-MU54DZCC.js.map +1 -0
- package/dist/{chunk-INO47JXD.js → chunk-PBENHIN2.js} +3 -3
- package/dist/{chunk-CLWBNXKF.js → chunk-PLUJEQLU.js} +2 -2
- package/dist/{chunk-DB5UXJC3.js → chunk-PNCOUFFI.js} +4 -2
- package/dist/chunk-PNCOUFFI.js.map +1 -0
- package/dist/{chunk-S7C6TIIF.js → chunk-S5G3HO7N.js} +3 -1
- package/dist/chunk-S5G3HO7N.js.map +1 -0
- package/dist/{chunk-FNHNSFIJ.js → chunk-WZOPWQN2.js} +2 -2
- package/dist/{chunk-TDEHU73T.js → chunk-YIVQ7KLS.js} +46 -5
- package/dist/chunk-YIVQ7KLS.js.map +1 -0
- package/dist/runtime/subsystems/auth/auth.module.js +2 -2
- package/dist/runtime/subsystems/auth/index.js +4 -4
- package/dist/runtime/subsystems/bridge/bridge.module.js +7 -7
- package/dist/runtime/subsystems/bridge/index.js +7 -7
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +1 -1
- package/dist/runtime/subsystems/events/events.module.js +5 -5
- package/dist/runtime/subsystems/events/generated/bus.js +3 -3
- package/dist/runtime/subsystems/events/generated/index.d.ts +2 -2
- package/dist/runtime/subsystems/events/generated/index.js +9 -3
- package/dist/runtime/subsystems/events/generated/registry.d.ts +36 -0
- package/dist/runtime/subsystems/events/generated/registry.js +1 -1
- package/dist/runtime/subsystems/events/generated/schemas.d.ts +109 -1
- package/dist/runtime/subsystems/events/generated/schemas.js +7 -1
- package/dist/runtime/subsystems/events/generated/types.d.ts +48 -2
- package/dist/runtime/subsystems/events/index.js +5 -5
- package/dist/runtime/subsystems/index.d.ts +3 -2
- package/dist/runtime/subsystems/index.js +29 -25
- package/dist/runtime/subsystems/integration/execute-integration.use-case.d.ts +11 -1
- package/dist/runtime/subsystems/integration/execute-integration.use-case.js +2 -2
- package/dist/runtime/subsystems/integration/index.d.ts +2 -1
- package/dist/runtime/subsystems/integration/index.js +10 -8
- package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.d.ts +106 -0
- package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.js +1 -0
- package/dist/runtime/subsystems/integration/integration-change-emitter.protocol.js.map +1 -0
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration.module.js +4 -4
- package/dist/runtime/subsystems/integration/integration.tokens.d.ts +11 -1
- package/dist/runtime/subsystems/integration/integration.tokens.js +3 -1
- package/dist/runtime/subsystems/jobs/index.js +12 -12
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +592 -4
- package/dist/runtime/subsystems/jobs/job-worker.js +3 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.js +6 -6
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +19 -0
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +4 -4
- package/dist/runtime/subsystems/observability/index.js +3 -3
- package/dist/runtime/subsystems/observability/observability.module.js +3 -3
- package/dist/runtime/subsystems/observability/observability.service.js +2 -2
- package/dist/src/cli/index.js +413 -85
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +490 -1
- package/dist/src/index.js +7 -7
- package/package.json +1 -1
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +23 -7
- package/runtime/subsystems/events/generated/registry.ts +27 -0
- package/runtime/subsystems/events/generated/schemas.ts +26 -0
- package/runtime/subsystems/events/generated/types.ts +52 -0
- package/runtime/subsystems/index.ts +23 -0
- package/runtime/subsystems/integration/execute-integration.use-case.ts +69 -1
- package/runtime/subsystems/integration/index.ts +6 -0
- package/runtime/subsystems/integration/integration-change-emitter.protocol.ts +107 -0
- package/runtime/subsystems/integration/integration.tokens.ts +11 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +5 -0
- package/runtime/subsystems/jobs/job-worker.ts +126 -12
- package/runtime/subsystems/jobs/jobs-domain.module.ts +19 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +59 -10
- package/templates/subsystem/jobs-config/codegen-config-jobs-block.ejs.t +11 -0
- package/dist/chunk-6DQEIXYU.js.map +0 -1
- package/dist/chunk-6ECCJVYW.js.map +0 -1
- package/dist/chunk-DB5UXJC3.js.map +0 -1
- package/dist/chunk-JOBQ6RUU.js.map +0 -1
- package/dist/chunk-JRQO2IOF.js.map +0 -1
- package/dist/chunk-QXVCRA23.js.map +0 -1
- package/dist/chunk-S7C6TIIF.js.map +0 -1
- package/dist/chunk-TDEHU73T.js.map +0 -1
- package/dist/chunk-VDL5CJ5C.js.map +0 -1
- /package/dist/{chunk-XKWOJZZ4.js.map → chunk-37PILMIT.js.map} +0 -0
- /package/dist/{chunk-NR7QQ6ZI.js.map → chunk-6M6LZEP6.js.map} +0 -0
- /package/dist/{chunk-NXHL5YII.js.map → chunk-7LKAMLV4.js.map} +0 -0
- /package/dist/{chunk-FFUDEIFF.js.map → chunk-HN5HT5WL.js.map} +0 -0
- /package/dist/{chunk-QFUIE37H.js.map → chunk-KFXXOFDC.js.map} +0 -0
- /package/dist/{chunk-O2A6XHGD.js.map → chunk-LLDJS7PJ.js.map} +0 -0
- /package/dist/{chunk-INO47JXD.js.map → chunk-PBENHIN2.js.map} +0 -0
- /package/dist/{chunk-CLWBNXKF.js.map → chunk-PLUJEQLU.js.map} +0 -0
- /package/dist/{chunk-FNHNSFIJ.js.map → chunk-WZOPWQN2.js.map} +0 -0
package/dist/src/cli/index.js
CHANGED
|
@@ -44,13 +44,13 @@ import {
|
|
|
44
44
|
validateOrchestrationProject,
|
|
45
45
|
validateProviders,
|
|
46
46
|
writeManifest
|
|
47
|
-
} from "../../chunk-
|
|
47
|
+
} from "../../chunk-K4BQQ2NN.js";
|
|
48
48
|
import "../../chunk-KVOWSC5S.js";
|
|
49
|
-
import "../../chunk-QFUIE37H.js";
|
|
50
|
-
import "../../chunk-FFUDEIFF.js";
|
|
51
|
-
import "../../chunk-EO2QPOKH.js";
|
|
52
49
|
import "../../chunk-PRWIX6UW.js";
|
|
53
|
-
import "../../chunk-
|
|
50
|
+
import "../../chunk-KFXXOFDC.js";
|
|
51
|
+
import "../../chunk-HN5HT5WL.js";
|
|
52
|
+
import "../../chunk-EO2QPOKH.js";
|
|
53
|
+
import "../../chunk-LLDJS7PJ.js";
|
|
54
54
|
import "../../chunk-HNWZFNKP.js";
|
|
55
55
|
import "../../chunk-AHV4GDYM.js";
|
|
56
56
|
import "../../chunk-SQDOBLBP.js";
|
|
@@ -63,8 +63,8 @@ import {
|
|
|
63
63
|
} from "../../chunk-5TK7MEN4.js";
|
|
64
64
|
import "../../chunk-4KNXX6TI.js";
|
|
65
65
|
import "../../chunk-3CJFPU6Q.js";
|
|
66
|
-
import "../../chunk-
|
|
67
|
-
import "../../chunk-
|
|
66
|
+
import "../../chunk-YIVQ7KLS.js";
|
|
67
|
+
import "../../chunk-S5G3HO7N.js";
|
|
68
68
|
import "../../chunk-MZ6GV4YF.js";
|
|
69
69
|
import "../../chunk-LG57S2SC.js";
|
|
70
70
|
import "../../chunk-U64T4YZE.js";
|
|
@@ -2151,7 +2151,7 @@ function loadEvents(eventsDir, entityNames) {
|
|
|
2151
2151
|
function desugarEntityEvents(entity) {
|
|
2152
2152
|
const entityName = entity.entity.name;
|
|
2153
2153
|
const entityEvents = entity.events ?? [];
|
|
2154
|
-
|
|
2154
|
+
const explicit = entityEvents.map((ev) => {
|
|
2155
2155
|
const payload = {};
|
|
2156
2156
|
for (const [key, typeString] of Object.entries(ev.body)) {
|
|
2157
2157
|
if (!isEventFieldType(typeString)) {
|
|
@@ -2173,6 +2173,56 @@ function desugarEntityEvents(entity) {
|
|
|
2173
2173
|
};
|
|
2174
2174
|
return def;
|
|
2175
2175
|
});
|
|
2176
|
+
const changeTriad = desugarEmitChangeEvents(entity);
|
|
2177
|
+
return [...explicit, ...changeTriad];
|
|
2178
|
+
}
|
|
2179
|
+
var EMIT_CHANGE_SUFFIXES = ["created", "edited", "deleted"];
|
|
2180
|
+
function desugarEmitChangeEvents(entity) {
|
|
2181
|
+
if (entity.integration?.sink?.emit_changes !== true) return [];
|
|
2182
|
+
const entityName = entity.entity.name;
|
|
2183
|
+
const basePayload = {
|
|
2184
|
+
entity_id: {
|
|
2185
|
+
type: "uuid",
|
|
2186
|
+
nullable: false,
|
|
2187
|
+
description: "Local aggregate id the sink wrote/soft-deleted."
|
|
2188
|
+
},
|
|
2189
|
+
external_id: {
|
|
2190
|
+
type: "string",
|
|
2191
|
+
nullable: false,
|
|
2192
|
+
description: "Vendor external id the change keyed on."
|
|
2193
|
+
},
|
|
2194
|
+
provider: {
|
|
2195
|
+
type: "string",
|
|
2196
|
+
nullable: false,
|
|
2197
|
+
description: "Provider label (e.g. 'slack', 'google')."
|
|
2198
|
+
},
|
|
2199
|
+
source: {
|
|
2200
|
+
type: "string",
|
|
2201
|
+
nullable: false,
|
|
2202
|
+
description: "Provenance marker \u2014 always 'integration'. A write-back action reads this to avoid echoing the change back to the vendor."
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
return EMIT_CHANGE_SUFFIXES.map((suffix) => {
|
|
2206
|
+
const payload = { ...basePayload };
|
|
2207
|
+
if (suffix !== "deleted") {
|
|
2208
|
+
payload.changed_fields = {
|
|
2209
|
+
type: "json",
|
|
2210
|
+
nullable: true,
|
|
2211
|
+
description: "Differ's per-field before/after map (same value as integration_run_items.changed_fields)."
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
const def = {
|
|
2215
|
+
type: `${entityName}_${suffix}`,
|
|
2216
|
+
tier: "domain",
|
|
2217
|
+
direction: "change",
|
|
2218
|
+
aggregate: entityName,
|
|
2219
|
+
payload,
|
|
2220
|
+
retry: { attempts: 3, backoff: "exponential" },
|
|
2221
|
+
version: 1,
|
|
2222
|
+
pool: DIRECTION_TO_POOL.change
|
|
2223
|
+
};
|
|
2224
|
+
return def;
|
|
2225
|
+
});
|
|
2176
2226
|
}
|
|
2177
2227
|
function isEventFieldType(s) {
|
|
2178
2228
|
return EVENT_FIELD_TYPES.includes(s);
|
|
@@ -2737,6 +2787,12 @@ function drizzleJobsExtensions(backend, cfg) {
|
|
|
2737
2787
|
if (typeof drizzle.listen_notify === "boolean") out.listenNotify = drizzle.listen_notify;
|
|
2738
2788
|
if (typeof drizzle.poll_interval_ms === "number")
|
|
2739
2789
|
out.pollIntervalMs = drizzle.poll_interval_ms;
|
|
2790
|
+
if (typeof drizzle.stale_sweeper_interval_ms === "number")
|
|
2791
|
+
out.staleSweeperIntervalMs = drizzle.stale_sweeper_interval_ms;
|
|
2792
|
+
if (typeof drizzle.stale_threshold_ms === "number")
|
|
2793
|
+
out.staleThresholdMs = drizzle.stale_threshold_ms;
|
|
2794
|
+
if (typeof drizzle.claim_heartbeat_interval_ms === "number")
|
|
2795
|
+
out.claimHeartbeatIntervalMs = drizzle.claim_heartbeat_interval_ms;
|
|
2740
2796
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
2741
2797
|
}
|
|
2742
2798
|
function drizzleExtensionsClause(ext, key) {
|
|
@@ -3690,64 +3746,207 @@ import {
|
|
|
3690
3746
|
} from "fs";
|
|
3691
3747
|
import { dirname, join as join8 } from "path";
|
|
3692
3748
|
|
|
3749
|
+
// src/cli/shared/change-emitter-emission-generator.ts
|
|
3750
|
+
function generatedBanner(sourceDesc) {
|
|
3751
|
+
return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
|
|
3752
|
+
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
|
|
3753
|
+
}
|
|
3754
|
+
function changeEmitterClass(entityClass) {
|
|
3755
|
+
return `${entityClass}ChangeEmitter`;
|
|
3756
|
+
}
|
|
3757
|
+
function generateChangeEmitter(input) {
|
|
3758
|
+
const mode = input.mode ?? "package";
|
|
3759
|
+
const cls = changeEmitterClass(input.entityClass);
|
|
3760
|
+
const eventsImport = subsystemsImport(mode, "events");
|
|
3761
|
+
const integrationImport = subsystemsImport(mode, "integration");
|
|
3762
|
+
const createdType = `${input.entityName}_created`;
|
|
3763
|
+
const editedType = `${input.entityName}_edited`;
|
|
3764
|
+
const deletedType = `${input.entityName}_deleted`;
|
|
3765
|
+
return `${generatedBanner(input.sourceDesc)}
|
|
3766
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
3767
|
+
import { TYPED_EVENT_BUS } from '${eventsImport}';
|
|
3768
|
+
import type {
|
|
3769
|
+
IIntegrationChangeEmitter,
|
|
3770
|
+
IntegrationChangeNotification,
|
|
3771
|
+
} from '${integrationImport}';
|
|
3772
|
+
|
|
3773
|
+
/**
|
|
3774
|
+
* Minimal publisher shape the emitter calls \u2014 decoupled from BOTH the package's
|
|
3775
|
+
* \`TypedEventBus\` (whose \`EventTypeName\` union is the package's own events) and
|
|
3776
|
+
* the consumer's generated \`TypedEventBus\` (whose union is the consumer's
|
|
3777
|
+
* events). The \`${createdType}\` / \`${editedType}\` / \`${deletedType}\` events
|
|
3778
|
+
* THIS class publishes live in the CONSUMER's registry; the structural \`publish\`
|
|
3779
|
+
* here accepts the string types without depending on either compile-time union.
|
|
3780
|
+
* The TYPED_EVENT_BUS token resolves to the consumer's generated bus at runtime
|
|
3781
|
+
* (bound by EventsModule.forRoot()), which validates the payload against the
|
|
3782
|
+
* generated event schema.
|
|
3783
|
+
*/
|
|
3784
|
+
interface ChangeEventPublisher {
|
|
3785
|
+
publish(
|
|
3786
|
+
type: string,
|
|
3787
|
+
aggregateId: string,
|
|
3788
|
+
payload: Record<string, unknown>,
|
|
3789
|
+
opts?: { tx?: unknown; metadata?: Record<string, unknown> },
|
|
3790
|
+
): Promise<void>;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* ${cls} \u2014 publishes the typed ${input.entityName} data-level change events
|
|
3795
|
+
* (\`${createdType}\` / \`${editedType}\` / \`${deletedType}\`) after the
|
|
3796
|
+
* integration orchestrator's sink write/soft-delete (EMIT-CHANGES seam).
|
|
3797
|
+
*
|
|
3798
|
+
* Bound to INTEGRATION_CHANGE_EMITTER in the ${input.entityName} integration
|
|
3799
|
+
* assembly; the orchestrator calls \`emitChange\` once per real change. The
|
|
3800
|
+
* payload carries \`source: 'integration'\` so a write-back action can detect an
|
|
3801
|
+
* integration-originated change and decline to echo it back to the vendor.
|
|
3802
|
+
*/
|
|
3803
|
+
@Injectable()
|
|
3804
|
+
export class ${cls} implements IIntegrationChangeEmitter {
|
|
3805
|
+
constructor(
|
|
3806
|
+
@Inject(TYPED_EVENT_BUS) private readonly bus: ChangeEventPublisher,
|
|
3807
|
+
) {}
|
|
3808
|
+
|
|
3809
|
+
async emitChange(notification: IntegrationChangeNotification): Promise<void> {
|
|
3810
|
+
const type =
|
|
3811
|
+
notification.action === 'created'
|
|
3812
|
+
? '${createdType}'
|
|
3813
|
+
: notification.action === 'updated'
|
|
3814
|
+
? '${editedType}'
|
|
3815
|
+
: '${deletedType}';
|
|
3816
|
+
|
|
3817
|
+
const payload: Record<string, unknown> = {
|
|
3818
|
+
entityId: notification.entityId,
|
|
3819
|
+
externalId: notification.externalId,
|
|
3820
|
+
provider: notification.provider,
|
|
3821
|
+
source: 'integration',
|
|
3822
|
+
};
|
|
3823
|
+
// changedFields is present on created/edited only (deletes are tombstones).
|
|
3824
|
+
if (notification.changedFields !== undefined) {
|
|
3825
|
+
payload['changedFields'] = notification.changedFields;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
await this.bus.publish(type, notification.entityId, payload, {
|
|
3829
|
+
tx: notification.tx,
|
|
3830
|
+
metadata:
|
|
3831
|
+
notification.tenantId !== undefined && notification.tenantId !== null
|
|
3832
|
+
? { tenantId: notification.tenantId }
|
|
3833
|
+
: undefined,
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
`;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3693
3840
|
// src/cli/shared/sink-emission-generator.ts
|
|
3694
|
-
var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
|
|
3695
3841
|
var USER_ID_FIELD = "userId";
|
|
3696
3842
|
function sinkNames(entityClass) {
|
|
3697
3843
|
return {
|
|
3698
3844
|
sinkClass: `${entityClass}Sink`,
|
|
3699
|
-
|
|
3845
|
+
sinkBaseClass: `${entityClass}SinkBase`,
|
|
3700
3846
|
repoClass: `${entityClass}Repository`,
|
|
3701
3847
|
projectionType: `${entityClass}IntegrationProjection`,
|
|
3702
|
-
writeType: `${entityClass}IntegrationWrite
|
|
3848
|
+
writeType: `${entityClass}IntegrationWrite`,
|
|
3849
|
+
defaultBuildWrite: `default${entityClass}BuildWrite`,
|
|
3850
|
+
defaultToCanonicalView: `default${entityClass}ToCanonicalView`
|
|
3703
3851
|
};
|
|
3704
3852
|
}
|
|
3705
|
-
function
|
|
3853
|
+
function assertIntegrated(input) {
|
|
3706
3854
|
if (input.pattern !== "Integrated") {
|
|
3707
3855
|
throw new Error(
|
|
3708
3856
|
`cannot emit default integration sink for entity '${input.entityName}': it is 'pattern: ${input.pattern}', but the default sink is emittable only for 'pattern: Integrated' entities (the only family with the integrationUpsertOne / findByExternalIdProjected projection path). Add 'pattern: Integrated' to the entity or provide a hand-authored sink.`
|
|
3709
3857
|
);
|
|
3710
3858
|
}
|
|
3711
|
-
|
|
3859
|
+
}
|
|
3860
|
+
function buildWriteBodyLines(input, n) {
|
|
3712
3861
|
const hasUserIdField = input.copyThroughFields.some(
|
|
3713
3862
|
(f) => f.camelName === USER_ID_FIELD
|
|
3714
3863
|
);
|
|
3715
3864
|
const copyThroughLines = input.copyThroughFields.filter((f) => f.camelName !== USER_ID_FIELD).map((f) => ` ${f.camelName}: record.${f.camelName},`);
|
|
3716
|
-
const
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3865
|
+
const fkLines = input.fkExternalKeys.flatMap((fk) => [
|
|
3866
|
+
` // SEAM (FK external key \u2014 null until you widen ${n.projectionType} to carry \`${fk.writeKey}\`):`,
|
|
3867
|
+
` // Replace null with record.${fk.writeKey} after widening. Write-safe: repo skips null FKs.`,
|
|
3868
|
+
` ${fk.writeKey}: null,`
|
|
3869
|
+
]);
|
|
3870
|
+
const lines = [
|
|
3720
3871
|
` externalId: record.externalId,`
|
|
3721
3872
|
];
|
|
3722
3873
|
if (copyThroughLines.length > 0) {
|
|
3723
|
-
|
|
3874
|
+
lines.push(
|
|
3724
3875
|
` // copy-through fields (one line per \`fields:\` entry):`,
|
|
3725
3876
|
...copyThroughLines
|
|
3726
3877
|
);
|
|
3727
3878
|
}
|
|
3728
|
-
if (
|
|
3729
|
-
|
|
3730
|
-
` // FK external join-keys
|
|
3731
|
-
...
|
|
3879
|
+
if (fkLines.length > 0) {
|
|
3880
|
+
lines.push(
|
|
3881
|
+
` // FK external join-keys (null until canonical widens to carry them):`,
|
|
3882
|
+
...fkLines
|
|
3732
3883
|
);
|
|
3733
3884
|
}
|
|
3734
3885
|
if (hasUserIdField) {
|
|
3735
|
-
|
|
3886
|
+
lines.push(` userId,`);
|
|
3887
|
+
}
|
|
3888
|
+
return lines;
|
|
3889
|
+
}
|
|
3890
|
+
function buildFindViewLines(input) {
|
|
3891
|
+
const viewFields = input.viewCopyThroughFields ?? input.copyThroughFields;
|
|
3892
|
+
const lines = [
|
|
3893
|
+
` id: row.id,`,
|
|
3894
|
+
` externalId: row.externalId,`
|
|
3895
|
+
];
|
|
3896
|
+
for (const f of viewFields) {
|
|
3897
|
+
const isJson = f.tsType.startsWith("unknown");
|
|
3898
|
+
if (isJson) {
|
|
3899
|
+
lines.push(` // SEAM (typed json \u2014 unknown; narrow on canonical widen): ${f.camelName}`);
|
|
3900
|
+
}
|
|
3901
|
+
lines.push(` ${f.camelName}: row.${f.camelName},`);
|
|
3902
|
+
}
|
|
3903
|
+
for (const localFk of input.localFkColumns ?? []) {
|
|
3904
|
+
lines.push(` ${localFk.camelName}: row.${localFk.camelName},`);
|
|
3905
|
+
}
|
|
3906
|
+
if (input.hasTimestamps) {
|
|
3907
|
+
lines.push(` createdAt: row.createdAt,`);
|
|
3908
|
+
lines.push(` updatedAt: row.updatedAt,`);
|
|
3909
|
+
}
|
|
3910
|
+
return lines;
|
|
3911
|
+
}
|
|
3912
|
+
function buildDeleteBody(input) {
|
|
3913
|
+
if ((input.deleteMode ?? "delegate") === "noop") {
|
|
3914
|
+
return [
|
|
3915
|
+
`// delete:noop (YAML integration.sink.delete: noop) \u2014 tombstone-preserving:`,
|
|
3916
|
+
`// an upstream delete signal is a no-op here; the repo row is left intact.`,
|
|
3917
|
+
`// Returns null \u2192 the orchestrator records an audit noop. Override this`,
|
|
3918
|
+
`// method in the subclass if you need a log line.`,
|
|
3919
|
+
`return null;`
|
|
3920
|
+
].join("\n ");
|
|
3736
3921
|
}
|
|
3922
|
+
return `return this.repo.softDeleteByExternalId(externalId, this.provider);`;
|
|
3923
|
+
}
|
|
3924
|
+
function generateSinkBase(input) {
|
|
3925
|
+
assertIntegrated(input);
|
|
3926
|
+
const n = sinkNames(input.entityClass);
|
|
3927
|
+
const writeBodyLines = buildWriteBodyLines(input, n);
|
|
3737
3928
|
const writeBody = writeBodyLines.join("\n");
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3929
|
+
const findViewLines = buildFindViewLines(input);
|
|
3930
|
+
const findViewBody = findViewLines.map((l) => ` ${l}`).join("\n");
|
|
3931
|
+
const deleteBody = buildDeleteBody(input);
|
|
3932
|
+
const banner = `// @generated by @pattern-stack/codegen from definitions entity '${input.entityName}' (surface: ${input.surface}) \u2014 DO NOT EDIT.
|
|
3933
|
+
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.
|
|
3741
3934
|
//
|
|
3742
|
-
//
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
//
|
|
3746
|
-
//
|
|
3747
|
-
//
|
|
3748
|
-
//
|
|
3749
|
-
//
|
|
3750
|
-
|
|
3935
|
+
// Two-file seam (Shape C, #491, RFC-0002 \xA74):
|
|
3936
|
+
// THIS FILE \u2014 @generated base: two standalone default functions at concrete types
|
|
3937
|
+
// + abstract class ${n.sinkBaseClass}<TCanonical>.
|
|
3938
|
+
// A YAML field change reflows the mapping here on every run.
|
|
3939
|
+
// ${input.entityName}.sink.ts \u2014 emit-once subclass: \`class ${n.sinkClass} extends ${n.sinkBaseClass}\`
|
|
3940
|
+
// with the two one-line seam wirings. Author overrides survive regen.
|
|
3941
|
+
//
|
|
3942
|
+
// SEAM #1 (canonical widening): change \`extends ${n.sinkBaseClass}\` \u2192
|
|
3943
|
+
// \`extends ${n.sinkBaseClass}<YourCanonical>\` in the subclass.
|
|
3944
|
+
// SEAM #2 (FK activation): override \`buildWrite\` in the subclass.
|
|
3945
|
+
// Replace \`${n.defaultBuildWrite}(record)\` with your body that sets
|
|
3946
|
+
// \`<writeKey>: record.<writeKey>\` after widening the canonical to carry it.
|
|
3947
|
+
// SEAM #3/#4 (typed-json narrow / null-coerce on widen): override \`toCanonicalView\`.
|
|
3948
|
+
// The bare passthrough preserves null (diff-soundness); coerce only on canonical widen.`;
|
|
3949
|
+
return `${banner}
|
|
3751
3950
|
import type { IIntegrationSink } from '${subsystemsImport(input.mode ?? "package", "integration")}';
|
|
3752
3951
|
import {
|
|
3753
3952
|
${n.repoClass},
|
|
@@ -3755,54 +3954,110 @@ import {
|
|
|
3755
3954
|
type ${n.writeType},
|
|
3756
3955
|
} from '${input.repoImportSpecifier}';
|
|
3757
3956
|
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3957
|
+
// Standalone default functions at CONCRETE projection/write types \u2014 the literal IS the
|
|
3958
|
+
// return type, so NO cast is needed (this is what dodges the TS2322 of a generic default
|
|
3959
|
+
// body). The regenerated home of the #487 write body and #488 find view.
|
|
3960
|
+
// Override these in the emit-once subclass via buildWrite / toCanonicalView.
|
|
3961
|
+
export function ${n.defaultBuildWrite}(record: ${n.projectionType}): ${n.writeType} {
|
|
3962
|
+
return {
|
|
3963
|
+
${writeBody}
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3762
3966
|
|
|
3763
|
-
|
|
3764
|
-
|
|
3967
|
+
export function ${n.defaultToCanonicalView}(row: ${n.projectionType}): ${n.projectionType} {
|
|
3968
|
+
// BARE passthrough \u2014 preserves null so the orchestrator's DeepEqualDiffer converges
|
|
3969
|
+
// to noop (null \u2260 ''; deep-equal.differ.ts:187-208). The projection-default canonical
|
|
3970
|
+
// is exactly the projection shape, so bare passthrough type-checks without a cast.
|
|
3971
|
+
const view: ${n.projectionType} = {
|
|
3972
|
+
${findViewBody}
|
|
3973
|
+
};
|
|
3974
|
+
return view;
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
// Abstract base \u2014 the three IIntegrationSink methods are CONCRETE (machinery: provider
|
|
3978
|
+
// assert, repo delegation, #490 knob-driven delete). The two protected abstract seams are
|
|
3979
|
+
// typed at TCanonical with NO body \u2014 a default body returning TCanonical would be TS2322
|
|
3980
|
+
// (Shape A failure \u2014 see RFC-0002 \xA74 and the \xA73b gate: sink-widened-canonical.gate.test.ts).
|
|
3981
|
+
// The bodies live in the standalone functions above; the emit-once subclass wires them in one
|
|
3982
|
+
// line each. No @Injectable() \u2014 the assembly binds via useFactory (OQ2 CLOSED, #491).
|
|
3983
|
+
export abstract class ${n.sinkBaseClass}<TCanonical = ${n.projectionType}>
|
|
3984
|
+
implements IIntegrationSink<TCanonical> {
|
|
3765
3985
|
constructor(
|
|
3766
|
-
|
|
3767
|
-
|
|
3986
|
+
protected readonly repo: ${n.repoClass},
|
|
3987
|
+
protected readonly provider: string,
|
|
3768
3988
|
) {}
|
|
3769
3989
|
|
|
3770
|
-
async findByExternalId(userId: string, externalId: string): Promise
|
|
3990
|
+
async findByExternalId(userId: string, externalId: string): Promise<TCanonical | null> {
|
|
3771
3991
|
const row = await this.repo.findByExternalIdProjected(externalId, this.provider);
|
|
3992
|
+
if (row === null) return null;
|
|
3772
3993
|
// The repo lookup is (provider, externalId)-scoped. If your external_id is not
|
|
3773
3994
|
// globally unique, enforce ownership here (e.g. row.userId === userId).
|
|
3774
|
-
return row;
|
|
3995
|
+
return this.toCanonicalView(row);
|
|
3775
3996
|
}
|
|
3776
3997
|
|
|
3777
3998
|
async upsertByExternalId(
|
|
3778
3999
|
userId: string,
|
|
3779
|
-
record:
|
|
4000
|
+
record: TCanonical,
|
|
3780
4001
|
provider: string,
|
|
3781
|
-
): Promise<{ id: string; saved:
|
|
4002
|
+
): Promise<{ id: string; saved: TCanonical }> {
|
|
3782
4003
|
if (provider !== this.provider) {
|
|
3783
4004
|
throw new Error(\`${n.sinkClass}: bound provider '\${this.provider}' != run provider '\${provider}'\`);
|
|
3784
4005
|
}
|
|
3785
|
-
const
|
|
3786
|
-
${writeBody}
|
|
3787
|
-
};
|
|
3788
|
-
const proj = await this.repo.integrationUpsertOne(write, this.provider);
|
|
4006
|
+
const proj = await this.repo.integrationUpsertOne(this.buildWrite(record), this.provider);
|
|
3789
4007
|
return { id: proj.id, saved: record };
|
|
3790
4008
|
}
|
|
3791
4009
|
|
|
3792
4010
|
async softDeleteByExternalId(_userId: string, externalId: string): Promise<{ id: string } | null> {
|
|
3793
|
-
|
|
4011
|
+
${deleteBody}
|
|
3794
4012
|
}
|
|
4013
|
+
|
|
4014
|
+
// ABSTRACT seams (NO body \u2014 a default body returning TCanonical is TS2322).
|
|
4015
|
+
// The projection-default subclass wires the default functions; a widened
|
|
4016
|
+
// subclass reimplements them (FK activation / typed-json narrow / null-coerce).
|
|
4017
|
+
protected abstract toCanonicalView(row: ${n.projectionType}): TCanonical;
|
|
4018
|
+
protected abstract buildWrite(record: TCanonical): ${n.writeType};
|
|
3795
4019
|
}
|
|
3796
4020
|
`;
|
|
3797
4021
|
}
|
|
3798
|
-
function
|
|
3799
|
-
|
|
3800
|
-
|
|
4022
|
+
function generateSinkSubclass(input) {
|
|
4023
|
+
assertIntegrated(input);
|
|
4024
|
+
const n = sinkNames(input.entityClass);
|
|
4025
|
+
return `// Emit-once \u2014 author-owned. Regen never overwrites this file.
|
|
4026
|
+
// The mechanical mapping lives in ${input.entityName}.sink.generated.ts and reflows on every
|
|
4027
|
+
// codegen run (a YAML field change reflows into the @generated base + default functions).
|
|
4028
|
+
//
|
|
4029
|
+
// To WIDEN (all four seams \u2014 see ${input.entityName}.sink.generated.ts banner for detail):
|
|
4030
|
+
// 1. Change \`extends ${n.sinkBaseClass}\` \u2192 \`extends ${n.sinkBaseClass}<YourCanonical>\`.
|
|
4031
|
+
// 2. Override \`toCanonicalView\`: reshape projection, narrow typed json, coerce nulls.
|
|
4032
|
+
// 3. Override \`buildWrite\`: activate FK write keys (\`<writeKey>: record.<writeKey>\`).
|
|
4033
|
+
// 4. Optionally override \`softDeleteByExternalId\` if you want a log line.
|
|
4034
|
+
// (The base handles delegate/noop via the #490 YAML knob \u2014 no override needed otherwise.)
|
|
4035
|
+
// Source: definitions entity '${input.entityName}' (surface: ${input.surface}).
|
|
4036
|
+
import {
|
|
4037
|
+
${n.sinkBaseClass},
|
|
4038
|
+
${n.defaultToCanonicalView},
|
|
4039
|
+
${n.defaultBuildWrite},
|
|
4040
|
+
} from './${input.entityName}.sink.generated';
|
|
4041
|
+
import type {
|
|
4042
|
+
${n.projectionType},
|
|
4043
|
+
${n.writeType},
|
|
4044
|
+
} from '${input.repoImportSpecifier}';
|
|
4045
|
+
|
|
4046
|
+
export class ${n.sinkClass} extends ${n.sinkBaseClass} {
|
|
4047
|
+
protected toCanonicalView(row: ${n.projectionType}): ${n.projectionType} {
|
|
4048
|
+
return ${n.defaultToCanonicalView}(row);
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
protected buildWrite(record: ${n.projectionType}): ${n.writeType} {
|
|
4052
|
+
return ${n.defaultBuildWrite}(record);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
`;
|
|
3801
4056
|
}
|
|
3802
4057
|
|
|
3803
4058
|
// src/cli/shared/assembly-emission-generator.ts
|
|
3804
4059
|
import { relative, resolve as resolve2, sep } from "path";
|
|
3805
|
-
function
|
|
4060
|
+
function generatedBanner2(sourceDesc) {
|
|
3806
4061
|
return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
|
|
3807
4062
|
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
|
|
3808
4063
|
}
|
|
@@ -3833,19 +4088,30 @@ function generateAssemblyModule(input) {
|
|
|
3833
4088
|
const token = integrationUseCaseToken(input.entityName, input.provider);
|
|
3834
4089
|
const moduleClass = assemblyModuleClass(input.entityName, input.provider);
|
|
3835
4090
|
const tokensImport = `../../${input.surface}-integration.tokens`;
|
|
3836
|
-
|
|
4091
|
+
const emitChanges = input.emitChanges === true;
|
|
4092
|
+
const emitterClass = changeEmitterClass(input.entityClass);
|
|
4093
|
+
const emitterImport = `../../sinks/${input.entityName}.change-emitter`;
|
|
4094
|
+
const integrationTokenImports = emitChanges ? ` INTEGRATION_CHANGE_EMITTER,
|
|
4095
|
+
INTEGRATION_CHANGE_SOURCE,
|
|
4096
|
+
INTEGRATION_SINK,` : ` INTEGRATION_CHANGE_SOURCE,
|
|
4097
|
+
INTEGRATION_SINK,`;
|
|
4098
|
+
const emitterImportLine = emitChanges ? `
|
|
4099
|
+
import { ${emitterClass} } from '${emitterImport}';` : "";
|
|
4100
|
+
const emitterProviderBlock = emitChanges ? `
|
|
4101
|
+
${emitterClass},
|
|
4102
|
+
{ provide: INTEGRATION_CHANGE_EMITTER, useExisting: ${emitterClass} },` : "";
|
|
4103
|
+
return `${generatedBanner2(input.sourceDesc)}
|
|
3837
4104
|
import { Module } from '@nestjs/common';
|
|
3838
4105
|
import {
|
|
3839
4106
|
ExecuteIntegrationUseCase,
|
|
3840
|
-
|
|
3841
|
-
INTEGRATION_SINK,
|
|
4107
|
+
${integrationTokenImports}
|
|
3842
4108
|
} from '${subsystemsImport(input.mode ?? "package", "integration")}';
|
|
3843
4109
|
import { ${adapterClass} } from '${adapterImport}';
|
|
3844
4110
|
import { ${adapterModuleClass} } from '${adapterModuleImport}';
|
|
3845
4111
|
import { ${sinkClass} } from '${sinkImport}';
|
|
3846
4112
|
import { ${input.repoClass} } from '${input.repoImportSpecifier}';
|
|
3847
4113
|
import { ${input.moduleClass} } from '${input.moduleImportSpecifier}';
|
|
3848
|
-
import { ${token} } from '${tokensImport}'
|
|
4114
|
+
import { ${token} } from '${tokensImport}';${emitterImportLine}
|
|
3849
4115
|
|
|
3850
4116
|
/**
|
|
3851
4117
|
* ${moduleClass} \u2014 the ${input.surface}/${input.entityName} \u2190 ${input.provider}
|
|
@@ -3855,7 +4121,12 @@ import { ${token} } from '${tokensImport}';
|
|
|
3855
4121
|
* \`changeSources.${input.entityName}\` and INTEGRATION_SINK from
|
|
3856
4122
|
* ${sinkClass}, provides a local ExecuteIntegrationUseCase, and aliases+exports
|
|
3857
4123
|
* it under ${token} (the bare class token is ambiguous at app root \u2014 every
|
|
3858
|
-
* assembly provides it)
|
|
4124
|
+
* assembly provides it).${emitChanges ? `
|
|
4125
|
+
*
|
|
4126
|
+
* EMIT-CHANGES: binds INTEGRATION_CHANGE_EMITTER to ${emitterClass} so the
|
|
4127
|
+
* orchestrator publishes typed ${input.entityName}_created/_edited/_deleted events
|
|
4128
|
+
* after every sink write/soft-delete (integration.sink.emit_changes).
|
|
4129
|
+
*` : ""} The substrate (cursor store, run recorder, differ,
|
|
3859
4130
|
* multi-tenant flag) comes from the global IntegrationModule.forRoot(...) in
|
|
3860
4131
|
* AppModule, never re-bound here.
|
|
3861
4132
|
*/
|
|
@@ -3871,7 +4142,7 @@ import { ${token} } from '${tokensImport}';
|
|
|
3871
4142
|
provide: INTEGRATION_SINK,
|
|
3872
4143
|
useFactory: (repo: ${input.repoClass}) => new ${sinkClass}(repo, '${input.provider}'),
|
|
3873
4144
|
inject: [${input.repoClass}],
|
|
3874
|
-
}
|
|
4145
|
+
},${emitterProviderBlock}
|
|
3875
4146
|
ExecuteIntegrationUseCase,
|
|
3876
4147
|
{ provide: ${token}, useExisting: ExecuteIntegrationUseCase },
|
|
3877
4148
|
],
|
|
@@ -3891,7 +4162,7 @@ function generateIntegrationTokens(surface, entries) {
|
|
|
3891
4162
|
* (${assemblyModuleClass(e.entityName, e.provider)}). A trigger grabs this to run it. */
|
|
3892
4163
|
export const ${token} = Symbol.for('${key}');`;
|
|
3893
4164
|
}).join("\n\n");
|
|
3894
|
-
return `${
|
|
4165
|
+
return `${generatedBanner2(`surface: ${surface}`)}
|
|
3895
4166
|
/**
|
|
3896
4167
|
* Use-case handles for the \`${surface}\` surface \u2014 one per (entity, provider)
|
|
3897
4168
|
* assembly. Each is a Symbol \`provide:\` token a per-entity
|
|
@@ -3917,7 +4188,7 @@ function generateIntegrationAggregator(surface, entries) {
|
|
|
3917
4188
|
return `import { ${cls} } from '${path36}';`;
|
|
3918
4189
|
}).join("\n");
|
|
3919
4190
|
const membersInline = moduleClasses.join(", ");
|
|
3920
|
-
return `${
|
|
4191
|
+
return `${generatedBanner2(`surface: ${surface}`)}
|
|
3921
4192
|
import { Module } from '@nestjs/common';
|
|
3922
4193
|
${importLines || "// no (entity, provider) integration assemblies on this surface yet"}
|
|
3923
4194
|
|
|
@@ -3993,6 +4264,7 @@ function resolveEntityModuleImports(input) {
|
|
|
3993
4264
|
}
|
|
3994
4265
|
|
|
3995
4266
|
// src/cli/shared/adapter-emission-generator.ts
|
|
4267
|
+
import pluralize2 from "pluralize";
|
|
3996
4268
|
var SURFACE_REGISTRY = {
|
|
3997
4269
|
crm: {
|
|
3998
4270
|
packageName: "@pattern-stack/codegen-crm",
|
|
@@ -4068,8 +4340,8 @@ var SURFACE_REGISTRY = {
|
|
|
4068
4340
|
function isClientlessProvider(surfaces) {
|
|
4069
4341
|
return surfaces.length > 0 && surfaces.every((s) => SURFACE_REGISTRY[s]?.readPrimitive === true);
|
|
4070
4342
|
}
|
|
4071
|
-
var
|
|
4072
|
-
function
|
|
4343
|
+
var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
|
|
4344
|
+
function generatedBanner3(sourceDesc) {
|
|
4073
4345
|
return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
|
|
4074
4346
|
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
|
|
4075
4347
|
}
|
|
@@ -4278,7 +4550,7 @@ ${l2Members}
|
|
|
4278
4550
|
const ctorOpen = rp ? ` constructor(@Inject(${n.strategyToken}) readonly auth: IAuthStrategy) {${changeSourcesAssign}}` : ` constructor(
|
|
4279
4551
|
@Inject(${n.strategyToken}) readonly auth: IAuthStrategy,${ctorClientParam}
|
|
4280
4552
|
) {${changeSourcesAssign}}`;
|
|
4281
|
-
return `${
|
|
4553
|
+
return `${SCAFFOLD_SENTINEL}
|
|
4282
4554
|
// Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running
|
|
4283
4555
|
// codegen detects the sentinel above and SKIPS this file \u2014 your edits are safe.
|
|
4284
4556
|
// Source: definitions/providers/${def.slug}.yaml (surface: ${surface}).
|
|
@@ -4309,7 +4581,7 @@ ${changeSourcesDecl}
|
|
|
4309
4581
|
}
|
|
4310
4582
|
function generateAdapterModule(def, surface) {
|
|
4311
4583
|
const n = names(def.slug, surface);
|
|
4312
|
-
return `${
|
|
4584
|
+
return `${generatedBanner3(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
|
|
4313
4585
|
import { Module } from '@nestjs/common';
|
|
4314
4586
|
import { ${n.providerModuleClass} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
|
|
4315
4587
|
import { ${n.adapterClass} } from './${def.slug}-${surface}.adapter';
|
|
@@ -4327,13 +4599,13 @@ function generateAdaptersBarrel(surface, providerSlugs) {
|
|
|
4327
4599
|
const n = names(slug, surface);
|
|
4328
4600
|
return `export { ${n.adapterModuleClass} } from './${slug}/${slug}-${surface}.adapter.module';`;
|
|
4329
4601
|
}).join("\n");
|
|
4330
|
-
return `${
|
|
4602
|
+
return `${generatedBanner3(`definitions/providers/*.yaml (surface: ${surface})`)}
|
|
4331
4603
|
${lines}
|
|
4332
4604
|
`;
|
|
4333
4605
|
}
|
|
4334
4606
|
function generateSurfaceTokens(surface, mode = "package") {
|
|
4335
4607
|
const n = names("__placeholder__", surface);
|
|
4336
|
-
return `${
|
|
4608
|
+
return `${generatedBanner3(`surface: ${surface}`)}
|
|
4337
4609
|
import type { IChangeSource } from '${subsystemsImport(mode, "integration")}';
|
|
4338
4610
|
|
|
4339
4611
|
/** The assembled list of every ${surface} adapter's contribution. */
|
|
@@ -4372,7 +4644,7 @@ function generateSurfaceAggregator(surface, providerSlugs, mode = "package") {
|
|
|
4372
4644
|
return `${lowerFirst(p.adapterClass)}: ${p.adapterClass}`;
|
|
4373
4645
|
}).join(", ");
|
|
4374
4646
|
const injectTokens = per.map((p) => p.adapterClass).join(", ");
|
|
4375
|
-
return `${
|
|
4647
|
+
return `${generatedBanner3(`surface: ${surface}`)}
|
|
4376
4648
|
import { Module } from '@nestjs/common';
|
|
4377
4649
|
import {
|
|
4378
4650
|
MemoryEntityChangeSourceRegistry,
|
|
@@ -4444,7 +4716,7 @@ function generateTypedView(surface, providerSlugs, entities) {
|
|
|
4444
4716
|
const providerUnion = slugs.length ? slugs.map((s) => `'${s}'`).join(" | ") : "never";
|
|
4445
4717
|
const entityUnion = ents.length ? ents.map((e) => `'${e}'`).join(" | ") : "never";
|
|
4446
4718
|
const mapEntries = slugs.map((s) => ` ${jsKey(s)}: ${surfacePascal}Entity;`).join("\n");
|
|
4447
|
-
return `${
|
|
4719
|
+
return `${generatedBanner3(`surface: ${surface}`)}
|
|
4448
4720
|
/**
|
|
4449
4721
|
* Per-consumer typed view for the \`${surface}\` surface. Surface-scoped unions
|
|
4450
4722
|
* + a (provider, entity) validity map for compile-time-checked consumer
|
|
@@ -4474,6 +4746,7 @@ function emitAdapters(opts) {
|
|
|
4474
4746
|
scaffoldsSkipped: [],
|
|
4475
4747
|
skippedSurfaces: [],
|
|
4476
4748
|
assembliesWritten: [],
|
|
4749
|
+
changeEmittersWritten: [],
|
|
4477
4750
|
tokensWritten: [],
|
|
4478
4751
|
integrationAggregatorsWritten: [],
|
|
4479
4752
|
skippedAssemblies: []
|
|
@@ -4574,14 +4847,31 @@ function emitAdapters(opts) {
|
|
|
4574
4847
|
backendSrcAbs: opts.backendSrcAbs,
|
|
4575
4848
|
aliases
|
|
4576
4849
|
});
|
|
4577
|
-
const
|
|
4578
|
-
|
|
4579
|
-
|
|
4850
|
+
const sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
|
|
4851
|
+
const basePath = join8(sinksDir, `${entityName}.sink.generated.ts`);
|
|
4852
|
+
const baseContent = generateSinkBase({ ...sinkInput, mode });
|
|
4853
|
+
if (!opts.dryRun) writeIfChanged(basePath, baseContent);
|
|
4854
|
+
result.written.push(basePath);
|
|
4855
|
+
const subclassPath = join8(sinksDir, `${entityName}.sink.ts`);
|
|
4856
|
+
if (existsSync6(subclassPath)) {
|
|
4857
|
+
result.scaffoldsSkipped.push(subclassPath);
|
|
4580
4858
|
} else {
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4859
|
+
const subclassContent = generateSinkSubclass({ ...sinkInput, mode });
|
|
4860
|
+
if (!opts.dryRun) writeFile(subclassPath, subclassContent);
|
|
4861
|
+
result.scaffoldsWritten.push(subclassPath);
|
|
4862
|
+
}
|
|
4863
|
+
const emitChanges = def?.integration?.sink?.emit_changes === true;
|
|
4864
|
+
if (emitChanges) {
|
|
4865
|
+
const emitterPath = join8(sinksDir, `${entityName}.change-emitter.ts`);
|
|
4866
|
+
const emitterContent = generateChangeEmitter({
|
|
4867
|
+
entityName,
|
|
4868
|
+
entityClass: loc.entityClass,
|
|
4869
|
+
surface,
|
|
4870
|
+
sourceDesc: `definitions entity '${entityName}' (integration.sink.emit_changes)`,
|
|
4871
|
+
mode
|
|
4872
|
+
});
|
|
4873
|
+
if (!opts.dryRun) writeIfChanged(emitterPath, emitterContent);
|
|
4874
|
+
result.changeEmittersWritten.push(emitterPath);
|
|
4585
4875
|
}
|
|
4586
4876
|
for (const slug of slugs) {
|
|
4587
4877
|
const assemblyPath = join8(
|
|
@@ -4599,7 +4889,8 @@ function emitAdapters(opts) {
|
|
|
4599
4889
|
repoImportSpecifier: loc.repoImportSpecifier,
|
|
4600
4890
|
repoClass: loc.repoClass,
|
|
4601
4891
|
sourceDesc: `definitions/providers/${slug}.yaml`,
|
|
4602
|
-
mode
|
|
4892
|
+
mode,
|
|
4893
|
+
emitChanges
|
|
4603
4894
|
});
|
|
4604
4895
|
if (!opts.dryRun) writeIfChanged(assemblyPath, assemblyContent);
|
|
4605
4896
|
result.assembliesWritten.push(assemblyPath);
|
|
@@ -4630,6 +4921,13 @@ function emitAdapters(opts) {
|
|
|
4630
4921
|
}
|
|
4631
4922
|
return result;
|
|
4632
4923
|
}
|
|
4924
|
+
function fkWriteKey(target, foreignKey, isSelfFk) {
|
|
4925
|
+
if (isSelfFk) {
|
|
4926
|
+
const base = foreignKey.endsWith("_id") ? foreignKey.slice(0, -3) : foreignKey;
|
|
4927
|
+
return snakeToCamel(base) + "ExternalId";
|
|
4928
|
+
}
|
|
4929
|
+
return `${target}ExternalId`;
|
|
4930
|
+
}
|
|
4633
4931
|
function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
4634
4932
|
const fields = def.fields ?? {};
|
|
4635
4933
|
const relationships = def.relationships ?? {};
|
|
@@ -4639,14 +4937,35 @@ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
|
4639
4937
|
fkColumns.add(rel2.foreign_key);
|
|
4640
4938
|
}
|
|
4641
4939
|
}
|
|
4642
|
-
const
|
|
4940
|
+
const excludeSet = new Set(def.integration?.sink?.exclude_fields ?? []);
|
|
4941
|
+
const isCopyThrough = ([name]) => name !== "id" && !fkColumns.has(name);
|
|
4942
|
+
const viewCopyThroughFields = Object.entries(fields).filter(isCopyThrough).map(([name, f]) => ({
|
|
4943
|
+
camelName: snakeToCamel(name),
|
|
4944
|
+
tsType: tsTypeFor(f.type, f.nullable)
|
|
4945
|
+
}));
|
|
4946
|
+
const copyThroughFields = Object.entries(fields).filter((entry) => isCopyThrough(entry) && !excludeSet.has(entry[0])).map(([name, f]) => ({
|
|
4643
4947
|
camelName: snakeToCamel(name),
|
|
4644
4948
|
tsType: tsTypeFor(f.type, f.nullable)
|
|
4645
4949
|
}));
|
|
4950
|
+
const entityNamePlural = def.entity.plural ?? pluralize2.plural(def.entity.name);
|
|
4646
4951
|
const fkExternalKeys = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
|
|
4647
4952
|
const target = rel2.target ?? relName;
|
|
4648
|
-
|
|
4953
|
+
const foreignKey = rel2.foreign_key ?? `${target}_id`;
|
|
4954
|
+
const isSelfFk = pluralize2.plural(target) === entityNamePlural;
|
|
4955
|
+
return { writeKey: fkWriteKey(target, foreignKey, isSelfFk) };
|
|
4649
4956
|
});
|
|
4957
|
+
const localFkColumns = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
|
|
4958
|
+
const foreignKey = rel2.foreign_key ?? `${relName}_id`;
|
|
4959
|
+
return {
|
|
4960
|
+
camelName: snakeToCamel(foreignKey),
|
|
4961
|
+
tsType: rel2.nullable ? "string | null" : "string"
|
|
4962
|
+
};
|
|
4963
|
+
});
|
|
4964
|
+
const hasTimestamps2 = (def.behaviors ?? []).some(
|
|
4965
|
+
(b) => typeof b === "string" ? b === "timestamps" : b.name === "timestamps"
|
|
4966
|
+
);
|
|
4967
|
+
const deleteKnob = def.integration?.sink?.delete;
|
|
4968
|
+
const deleteMode = deleteKnob === "noop" ? "noop" : "delegate";
|
|
4650
4969
|
return {
|
|
4651
4970
|
entityName: def.entity.name,
|
|
4652
4971
|
entityClass: pascalFromSnake(def.entity.name),
|
|
@@ -4654,8 +4973,12 @@ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
|
4654
4973
|
pattern: "Integrated",
|
|
4655
4974
|
provider,
|
|
4656
4975
|
copyThroughFields,
|
|
4976
|
+
viewCopyThroughFields,
|
|
4657
4977
|
fkExternalKeys,
|
|
4658
|
-
|
|
4978
|
+
localFkColumns,
|
|
4979
|
+
hasTimestamps: hasTimestamps2,
|
|
4980
|
+
repoImportSpecifier,
|
|
4981
|
+
deleteMode
|
|
4659
4982
|
};
|
|
4660
4983
|
}
|
|
4661
4984
|
var TS_TYPE_FOR_SINK = {
|
|
@@ -4901,12 +5224,12 @@ function resolveSyncMode(entity, config) {
|
|
|
4901
5224
|
// src/emitters/frontend/emit-utils.ts
|
|
4902
5225
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4903
5226
|
import { dirname as dirname3 } from "path";
|
|
4904
|
-
function
|
|
5227
|
+
function generatedBanner4(sourceDesc) {
|
|
4905
5228
|
return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
|
|
4906
5229
|
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
|
|
4907
5230
|
}
|
|
4908
5231
|
function withBanner(sourceDesc, body) {
|
|
4909
|
-
return `${
|
|
5232
|
+
return `${generatedBanner4(sourceDesc)}
|
|
4910
5233
|
|
|
4911
5234
|
${body}`;
|
|
4912
5235
|
}
|
|
@@ -6742,6 +7065,11 @@ var EntityNewCommand = class extends Command2 {
|
|
|
6742
7065
|
`integration assembly codegen: ${adapterResult.assembliesWritten.length} module(s) + ${adapterResult.tokensWritten.length} tokens file(s) + ${adapterResult.integrationAggregatorsWritten.length} aggregator(s)`
|
|
6743
7066
|
);
|
|
6744
7067
|
}
|
|
7068
|
+
if (adapterResult.changeEmittersWritten.length) {
|
|
7069
|
+
printInfo(
|
|
7070
|
+
`integration change-emitters (emit_changes): ${adapterResult.changeEmittersWritten.length} emitter(s)`
|
|
7071
|
+
);
|
|
7072
|
+
}
|
|
6745
7073
|
for (const s of adapterResult.scaffoldsSkipped) {
|
|
6746
7074
|
printInfo(`skipped scaffold ${s} (author-owned)`);
|
|
6747
7075
|
}
|