@pattern-stack/codegen 0.22.0 → 0.23.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 +33 -0
- package/dist/{chunk-NR7QQ6ZI.js → chunk-42763UEE.js} +2 -2
- package/dist/{chunk-6ECCJVYW.js → chunk-4M66MQYA.js} +44 -2
- package/dist/chunk-4M66MQYA.js.map +1 -0
- package/dist/{chunk-FNHNSFIJ.js → chunk-6XP2Q5SS.js} +2 -2
- 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-FIUC6QB5.js} +1 -1
- package/dist/chunk-FIUC6QB5.js.map +1 -0
- package/dist/{chunk-DB5UXJC3.js → chunk-PNCOUFFI.js} +4 -2
- package/dist/chunk-PNCOUFFI.js.map +1 -0
- package/dist/{chunk-QXVCRA23.js → chunk-SH76CFAY.js} +9 -4
- package/dist/chunk-SH76CFAY.js.map +1 -0
- package/dist/runtime/base-classes/index.js +17 -17
- package/dist/runtime/subsystems/auth/auth.module.js +3 -3
- package/dist/runtime/subsystems/auth/index.js +7 -7
- package/dist/runtime/subsystems/bridge/bridge.module.js +4 -4
- package/dist/runtime/subsystems/bridge/index.js +4 -4
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +1 -1
- package/dist/runtime/subsystems/events/events.module.js +2 -2
- package/dist/runtime/subsystems/events/index.js +2 -2
- package/dist/runtime/subsystems/index.js +12 -12
- package/dist/runtime/subsystems/jobs/index.js +3 -3
- 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 +3 -3
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +19 -0
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +1 -1
- package/dist/src/cli/index.js +211 -60
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +477 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +23 -7
- 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-QXVCRA23.js.map +0 -1
- package/dist/chunk-VDL5CJ5C.js.map +0 -1
- /package/dist/{chunk-NR7QQ6ZI.js.map → chunk-42763UEE.js.map} +0 -0
- /package/dist/{chunk-FNHNSFIJ.js.map → chunk-6XP2Q5SS.js.map} +0 -0
- /package/dist/{chunk-NXHL5YII.js.map → chunk-7LKAMLV4.js.map} +0 -0
|
@@ -37,6 +37,25 @@ interface DrizzleBackendExtensions {
|
|
|
37
37
|
listenNotify?: boolean;
|
|
38
38
|
/** Polling interval (ms). Default 1000. */
|
|
39
39
|
pollIntervalMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* CLAIM-HB-1 — stale-claim sweep interval (ms). How often each worker scans
|
|
42
|
+
* for `running` rows whose lease has expired. Default 60_000.
|
|
43
|
+
*/
|
|
44
|
+
staleSweeperIntervalMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* CLAIM-HB-1 — stale-claim threshold (ms). A `running` row whose `claimed_at`
|
|
47
|
+
* has not been renewed within this window is presumed stranded by a dead
|
|
48
|
+
* worker and reset to `pending`. A LIVE worker renews the lease every
|
|
49
|
+
* `claimHeartbeatIntervalMs`, so this only catches genuine crashes. Default
|
|
50
|
+
* 300_000 (5 min).
|
|
51
|
+
*/
|
|
52
|
+
staleThresholdMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* CLAIM-HB-1 — claim heartbeat interval (ms). How often a worker bumps
|
|
55
|
+
* `claimed_at` for its in-flight runs to keep them from being swept. Must be
|
|
56
|
+
* comfortably below `staleThresholdMs`. Default `staleThresholdMs / 3`.
|
|
57
|
+
*/
|
|
58
|
+
claimHeartbeatIntervalMs?: number;
|
|
40
59
|
}
|
|
41
60
|
interface JobsDomainModuleOptions {
|
|
42
61
|
backend: 'drizzle' | 'memory' | 'bullmq';
|
package/dist/src/cli/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
validateOrchestrationProject,
|
|
45
45
|
validateProviders,
|
|
46
46
|
writeManifest
|
|
47
|
-
} from "../../chunk-
|
|
47
|
+
} from "../../chunk-4M66MQYA.js";
|
|
48
48
|
import "../../chunk-KVOWSC5S.js";
|
|
49
49
|
import "../../chunk-QFUIE37H.js";
|
|
50
50
|
import "../../chunk-FFUDEIFF.js";
|
|
@@ -2737,6 +2737,12 @@ function drizzleJobsExtensions(backend, cfg) {
|
|
|
2737
2737
|
if (typeof drizzle.listen_notify === "boolean") out.listenNotify = drizzle.listen_notify;
|
|
2738
2738
|
if (typeof drizzle.poll_interval_ms === "number")
|
|
2739
2739
|
out.pollIntervalMs = drizzle.poll_interval_ms;
|
|
2740
|
+
if (typeof drizzle.stale_sweeper_interval_ms === "number")
|
|
2741
|
+
out.staleSweeperIntervalMs = drizzle.stale_sweeper_interval_ms;
|
|
2742
|
+
if (typeof drizzle.stale_threshold_ms === "number")
|
|
2743
|
+
out.staleThresholdMs = drizzle.stale_threshold_ms;
|
|
2744
|
+
if (typeof drizzle.claim_heartbeat_interval_ms === "number")
|
|
2745
|
+
out.claimHeartbeatIntervalMs = drizzle.claim_heartbeat_interval_ms;
|
|
2740
2746
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
2741
2747
|
}
|
|
2742
2748
|
function drizzleExtensionsClause(ext, key) {
|
|
@@ -3691,63 +3697,115 @@ import {
|
|
|
3691
3697
|
import { dirname, join as join8 } from "path";
|
|
3692
3698
|
|
|
3693
3699
|
// src/cli/shared/sink-emission-generator.ts
|
|
3694
|
-
var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
|
|
3695
3700
|
var USER_ID_FIELD = "userId";
|
|
3696
3701
|
function sinkNames(entityClass) {
|
|
3697
3702
|
return {
|
|
3698
3703
|
sinkClass: `${entityClass}Sink`,
|
|
3699
|
-
|
|
3704
|
+
sinkBaseClass: `${entityClass}SinkBase`,
|
|
3700
3705
|
repoClass: `${entityClass}Repository`,
|
|
3701
3706
|
projectionType: `${entityClass}IntegrationProjection`,
|
|
3702
|
-
writeType: `${entityClass}IntegrationWrite
|
|
3707
|
+
writeType: `${entityClass}IntegrationWrite`,
|
|
3708
|
+
defaultBuildWrite: `default${entityClass}BuildWrite`,
|
|
3709
|
+
defaultToCanonicalView: `default${entityClass}ToCanonicalView`
|
|
3703
3710
|
};
|
|
3704
3711
|
}
|
|
3705
|
-
function
|
|
3712
|
+
function assertIntegrated(input) {
|
|
3706
3713
|
if (input.pattern !== "Integrated") {
|
|
3707
3714
|
throw new Error(
|
|
3708
3715
|
`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
3716
|
);
|
|
3710
3717
|
}
|
|
3711
|
-
|
|
3718
|
+
}
|
|
3719
|
+
function buildWriteBodyLines(input, n) {
|
|
3712
3720
|
const hasUserIdField = input.copyThroughFields.some(
|
|
3713
3721
|
(f) => f.camelName === USER_ID_FIELD
|
|
3714
3722
|
);
|
|
3715
3723
|
const copyThroughLines = input.copyThroughFields.filter((f) => f.camelName !== USER_ID_FIELD).map((f) => ` ${f.camelName}: record.${f.camelName},`);
|
|
3716
|
-
const
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3724
|
+
const fkLines = input.fkExternalKeys.flatMap((fk) => [
|
|
3725
|
+
` // SEAM (FK external key \u2014 null until you widen ${n.projectionType} to carry \`${fk.writeKey}\`):`,
|
|
3726
|
+
` // Replace null with record.${fk.writeKey} after widening. Write-safe: repo skips null FKs.`,
|
|
3727
|
+
` ${fk.writeKey}: null,`
|
|
3728
|
+
]);
|
|
3729
|
+
const lines = [
|
|
3720
3730
|
` externalId: record.externalId,`
|
|
3721
3731
|
];
|
|
3722
3732
|
if (copyThroughLines.length > 0) {
|
|
3723
|
-
|
|
3733
|
+
lines.push(
|
|
3724
3734
|
` // copy-through fields (one line per \`fields:\` entry):`,
|
|
3725
3735
|
...copyThroughLines
|
|
3726
3736
|
);
|
|
3727
3737
|
}
|
|
3728
|
-
if (
|
|
3729
|
-
|
|
3730
|
-
` // FK external join-keys
|
|
3731
|
-
...
|
|
3738
|
+
if (fkLines.length > 0) {
|
|
3739
|
+
lines.push(
|
|
3740
|
+
` // FK external join-keys (null until canonical widens to carry them):`,
|
|
3741
|
+
...fkLines
|
|
3732
3742
|
);
|
|
3733
3743
|
}
|
|
3734
3744
|
if (hasUserIdField) {
|
|
3735
|
-
|
|
3745
|
+
lines.push(` userId,`);
|
|
3746
|
+
}
|
|
3747
|
+
return lines;
|
|
3748
|
+
}
|
|
3749
|
+
function buildFindViewLines(input) {
|
|
3750
|
+
const viewFields = input.viewCopyThroughFields ?? input.copyThroughFields;
|
|
3751
|
+
const lines = [
|
|
3752
|
+
` id: row.id,`,
|
|
3753
|
+
` externalId: row.externalId,`
|
|
3754
|
+
];
|
|
3755
|
+
for (const f of viewFields) {
|
|
3756
|
+
const isJson = f.tsType.startsWith("unknown");
|
|
3757
|
+
if (isJson) {
|
|
3758
|
+
lines.push(` // SEAM (typed json \u2014 unknown; narrow on canonical widen): ${f.camelName}`);
|
|
3759
|
+
}
|
|
3760
|
+
lines.push(` ${f.camelName}: row.${f.camelName},`);
|
|
3761
|
+
}
|
|
3762
|
+
for (const localFk of input.localFkColumns ?? []) {
|
|
3763
|
+
lines.push(` ${localFk.camelName}: row.${localFk.camelName},`);
|
|
3736
3764
|
}
|
|
3765
|
+
if (input.hasTimestamps) {
|
|
3766
|
+
lines.push(` createdAt: row.createdAt,`);
|
|
3767
|
+
lines.push(` updatedAt: row.updatedAt,`);
|
|
3768
|
+
}
|
|
3769
|
+
return lines;
|
|
3770
|
+
}
|
|
3771
|
+
function buildDeleteBody(input) {
|
|
3772
|
+
if ((input.deleteMode ?? "delegate") === "noop") {
|
|
3773
|
+
return [
|
|
3774
|
+
`// delete:noop (YAML integration.sink.delete: noop) \u2014 tombstone-preserving:`,
|
|
3775
|
+
`// an upstream delete signal is a no-op here; the repo row is left intact.`,
|
|
3776
|
+
`// Returns null \u2192 the orchestrator records an audit noop. Override this`,
|
|
3777
|
+
`// method in the subclass if you need a log line.`,
|
|
3778
|
+
`return null;`
|
|
3779
|
+
].join("\n ");
|
|
3780
|
+
}
|
|
3781
|
+
return `return this.repo.softDeleteByExternalId(externalId, this.provider);`;
|
|
3782
|
+
}
|
|
3783
|
+
function generateSinkBase(input) {
|
|
3784
|
+
assertIntegrated(input);
|
|
3785
|
+
const n = sinkNames(input.entityClass);
|
|
3786
|
+
const writeBodyLines = buildWriteBodyLines(input, n);
|
|
3737
3787
|
const writeBody = writeBodyLines.join("\n");
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3788
|
+
const findViewLines = buildFindViewLines(input);
|
|
3789
|
+
const findViewBody = findViewLines.map((l) => ` ${l}`).join("\n");
|
|
3790
|
+
const deleteBody = buildDeleteBody(input);
|
|
3791
|
+
const banner = `// @generated by @pattern-stack/codegen from definitions entity '${input.entityName}' (surface: ${input.surface}) \u2014 DO NOT EDIT.
|
|
3792
|
+
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.
|
|
3741
3793
|
//
|
|
3742
|
-
//
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
//
|
|
3746
|
-
//
|
|
3747
|
-
//
|
|
3748
|
-
//
|
|
3749
|
-
//
|
|
3750
|
-
|
|
3794
|
+
// Two-file seam (Shape C, #491, RFC-0002 \xA74):
|
|
3795
|
+
// THIS FILE \u2014 @generated base: two standalone default functions at concrete types
|
|
3796
|
+
// + abstract class ${n.sinkBaseClass}<TCanonical>.
|
|
3797
|
+
// A YAML field change reflows the mapping here on every run.
|
|
3798
|
+
// ${input.entityName}.sink.ts \u2014 emit-once subclass: \`class ${n.sinkClass} extends ${n.sinkBaseClass}\`
|
|
3799
|
+
// with the two one-line seam wirings. Author overrides survive regen.
|
|
3800
|
+
//
|
|
3801
|
+
// SEAM #1 (canonical widening): change \`extends ${n.sinkBaseClass}\` \u2192
|
|
3802
|
+
// \`extends ${n.sinkBaseClass}<YourCanonical>\` in the subclass.
|
|
3803
|
+
// SEAM #2 (FK activation): override \`buildWrite\` in the subclass.
|
|
3804
|
+
// Replace \`${n.defaultBuildWrite}(record)\` with your body that sets
|
|
3805
|
+
// \`<writeKey>: record.<writeKey>\` after widening the canonical to carry it.
|
|
3806
|
+
// SEAM #3/#4 (typed-json narrow / null-coerce on widen): override \`toCanonicalView\`.
|
|
3807
|
+
// The bare passthrough preserves null (diff-soundness); coerce only on canonical widen.`;
|
|
3808
|
+
return `${banner}
|
|
3751
3809
|
import type { IIntegrationSink } from '${subsystemsImport(input.mode ?? "package", "integration")}';
|
|
3752
3810
|
import {
|
|
3753
3811
|
${n.repoClass},
|
|
@@ -3755,49 +3813,105 @@ import {
|
|
|
3755
3813
|
type ${n.writeType},
|
|
3756
3814
|
} from '${input.repoImportSpecifier}';
|
|
3757
3815
|
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3816
|
+
// Standalone default functions at CONCRETE projection/write types \u2014 the literal IS the
|
|
3817
|
+
// return type, so NO cast is needed (this is what dodges the TS2322 of a generic default
|
|
3818
|
+
// body). The regenerated home of the #487 write body and #488 find view.
|
|
3819
|
+
// Override these in the emit-once subclass via buildWrite / toCanonicalView.
|
|
3820
|
+
export function ${n.defaultBuildWrite}(record: ${n.projectionType}): ${n.writeType} {
|
|
3821
|
+
return {
|
|
3822
|
+
${writeBody}
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3762
3825
|
|
|
3763
|
-
|
|
3764
|
-
|
|
3826
|
+
export function ${n.defaultToCanonicalView}(row: ${n.projectionType}): ${n.projectionType} {
|
|
3827
|
+
// BARE passthrough \u2014 preserves null so the orchestrator's DeepEqualDiffer converges
|
|
3828
|
+
// to noop (null \u2260 ''; deep-equal.differ.ts:187-208). The projection-default canonical
|
|
3829
|
+
// is exactly the projection shape, so bare passthrough type-checks without a cast.
|
|
3830
|
+
const view: ${n.projectionType} = {
|
|
3831
|
+
${findViewBody}
|
|
3832
|
+
};
|
|
3833
|
+
return view;
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// Abstract base \u2014 the three IIntegrationSink methods are CONCRETE (machinery: provider
|
|
3837
|
+
// assert, repo delegation, #490 knob-driven delete). The two protected abstract seams are
|
|
3838
|
+
// typed at TCanonical with NO body \u2014 a default body returning TCanonical would be TS2322
|
|
3839
|
+
// (Shape A failure \u2014 see RFC-0002 \xA74 and the \xA73b gate: sink-widened-canonical.gate.test.ts).
|
|
3840
|
+
// The bodies live in the standalone functions above; the emit-once subclass wires them in one
|
|
3841
|
+
// line each. No @Injectable() \u2014 the assembly binds via useFactory (OQ2 CLOSED, #491).
|
|
3842
|
+
export abstract class ${n.sinkBaseClass}<TCanonical = ${n.projectionType}>
|
|
3843
|
+
implements IIntegrationSink<TCanonical> {
|
|
3765
3844
|
constructor(
|
|
3766
|
-
|
|
3767
|
-
|
|
3845
|
+
protected readonly repo: ${n.repoClass},
|
|
3846
|
+
protected readonly provider: string,
|
|
3768
3847
|
) {}
|
|
3769
3848
|
|
|
3770
|
-
async findByExternalId(userId: string, externalId: string): Promise
|
|
3849
|
+
async findByExternalId(userId: string, externalId: string): Promise<TCanonical | null> {
|
|
3771
3850
|
const row = await this.repo.findByExternalIdProjected(externalId, this.provider);
|
|
3851
|
+
if (row === null) return null;
|
|
3772
3852
|
// The repo lookup is (provider, externalId)-scoped. If your external_id is not
|
|
3773
3853
|
// globally unique, enforce ownership here (e.g. row.userId === userId).
|
|
3774
|
-
return row;
|
|
3854
|
+
return this.toCanonicalView(row);
|
|
3775
3855
|
}
|
|
3776
3856
|
|
|
3777
3857
|
async upsertByExternalId(
|
|
3778
3858
|
userId: string,
|
|
3779
|
-
record:
|
|
3859
|
+
record: TCanonical,
|
|
3780
3860
|
provider: string,
|
|
3781
|
-
): Promise<{ id: string; saved:
|
|
3861
|
+
): Promise<{ id: string; saved: TCanonical }> {
|
|
3782
3862
|
if (provider !== this.provider) {
|
|
3783
3863
|
throw new Error(\`${n.sinkClass}: bound provider '\${this.provider}' != run provider '\${provider}'\`);
|
|
3784
3864
|
}
|
|
3785
|
-
const
|
|
3786
|
-
${writeBody}
|
|
3787
|
-
};
|
|
3788
|
-
const proj = await this.repo.integrationUpsertOne(write, this.provider);
|
|
3865
|
+
const proj = await this.repo.integrationUpsertOne(this.buildWrite(record), this.provider);
|
|
3789
3866
|
return { id: proj.id, saved: record };
|
|
3790
3867
|
}
|
|
3791
3868
|
|
|
3792
3869
|
async softDeleteByExternalId(_userId: string, externalId: string): Promise<{ id: string } | null> {
|
|
3793
|
-
|
|
3870
|
+
${deleteBody}
|
|
3794
3871
|
}
|
|
3872
|
+
|
|
3873
|
+
// ABSTRACT seams (NO body \u2014 a default body returning TCanonical is TS2322).
|
|
3874
|
+
// The projection-default subclass wires the default functions; a widened
|
|
3875
|
+
// subclass reimplements them (FK activation / typed-json narrow / null-coerce).
|
|
3876
|
+
protected abstract toCanonicalView(row: ${n.projectionType}): TCanonical;
|
|
3877
|
+
protected abstract buildWrite(record: TCanonical): ${n.writeType};
|
|
3795
3878
|
}
|
|
3796
3879
|
`;
|
|
3797
3880
|
}
|
|
3798
|
-
function
|
|
3799
|
-
|
|
3800
|
-
|
|
3881
|
+
function generateSinkSubclass(input) {
|
|
3882
|
+
assertIntegrated(input);
|
|
3883
|
+
const n = sinkNames(input.entityClass);
|
|
3884
|
+
return `// Emit-once \u2014 author-owned. Regen never overwrites this file.
|
|
3885
|
+
// The mechanical mapping lives in ${input.entityName}.sink.generated.ts and reflows on every
|
|
3886
|
+
// codegen run (a YAML field change reflows into the @generated base + default functions).
|
|
3887
|
+
//
|
|
3888
|
+
// To WIDEN (all four seams \u2014 see ${input.entityName}.sink.generated.ts banner for detail):
|
|
3889
|
+
// 1. Change \`extends ${n.sinkBaseClass}\` \u2192 \`extends ${n.sinkBaseClass}<YourCanonical>\`.
|
|
3890
|
+
// 2. Override \`toCanonicalView\`: reshape projection, narrow typed json, coerce nulls.
|
|
3891
|
+
// 3. Override \`buildWrite\`: activate FK write keys (\`<writeKey>: record.<writeKey>\`).
|
|
3892
|
+
// 4. Optionally override \`softDeleteByExternalId\` if you want a log line.
|
|
3893
|
+
// (The base handles delegate/noop via the #490 YAML knob \u2014 no override needed otherwise.)
|
|
3894
|
+
// Source: definitions entity '${input.entityName}' (surface: ${input.surface}).
|
|
3895
|
+
import {
|
|
3896
|
+
${n.sinkBaseClass},
|
|
3897
|
+
${n.defaultToCanonicalView},
|
|
3898
|
+
${n.defaultBuildWrite},
|
|
3899
|
+
} from './${input.entityName}.sink.generated';
|
|
3900
|
+
import type {
|
|
3901
|
+
${n.projectionType},
|
|
3902
|
+
${n.writeType},
|
|
3903
|
+
} from '${input.repoImportSpecifier}';
|
|
3904
|
+
|
|
3905
|
+
export class ${n.sinkClass} extends ${n.sinkBaseClass} {
|
|
3906
|
+
protected toCanonicalView(row: ${n.projectionType}): ${n.projectionType} {
|
|
3907
|
+
return ${n.defaultToCanonicalView}(row);
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
protected buildWrite(record: ${n.projectionType}): ${n.writeType} {
|
|
3911
|
+
return ${n.defaultBuildWrite}(record);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
`;
|
|
3801
3915
|
}
|
|
3802
3916
|
|
|
3803
3917
|
// src/cli/shared/assembly-emission-generator.ts
|
|
@@ -3993,6 +4107,7 @@ function resolveEntityModuleImports(input) {
|
|
|
3993
4107
|
}
|
|
3994
4108
|
|
|
3995
4109
|
// src/cli/shared/adapter-emission-generator.ts
|
|
4110
|
+
import pluralize2 from "pluralize";
|
|
3996
4111
|
var SURFACE_REGISTRY = {
|
|
3997
4112
|
crm: {
|
|
3998
4113
|
packageName: "@pattern-stack/codegen-crm",
|
|
@@ -4068,7 +4183,7 @@ var SURFACE_REGISTRY = {
|
|
|
4068
4183
|
function isClientlessProvider(surfaces) {
|
|
4069
4184
|
return surfaces.length > 0 && surfaces.every((s) => SURFACE_REGISTRY[s]?.readPrimitive === true);
|
|
4070
4185
|
}
|
|
4071
|
-
var
|
|
4186
|
+
var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
|
|
4072
4187
|
function generatedBanner2(sourceDesc) {
|
|
4073
4188
|
return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
|
|
4074
4189
|
// Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
|
|
@@ -4278,7 +4393,7 @@ ${l2Members}
|
|
|
4278
4393
|
const ctorOpen = rp ? ` constructor(@Inject(${n.strategyToken}) readonly auth: IAuthStrategy) {${changeSourcesAssign}}` : ` constructor(
|
|
4279
4394
|
@Inject(${n.strategyToken}) readonly auth: IAuthStrategy,${ctorClientParam}
|
|
4280
4395
|
) {${changeSourcesAssign}}`;
|
|
4281
|
-
return `${
|
|
4396
|
+
return `${SCAFFOLD_SENTINEL}
|
|
4282
4397
|
// Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running
|
|
4283
4398
|
// codegen detects the sentinel above and SKIPS this file \u2014 your edits are safe.
|
|
4284
4399
|
// Source: definitions/providers/${def.slug}.yaml (surface: ${surface}).
|
|
@@ -4574,14 +4689,18 @@ function emitAdapters(opts) {
|
|
|
4574
4689
|
backendSrcAbs: opts.backendSrcAbs,
|
|
4575
4690
|
aliases
|
|
4576
4691
|
});
|
|
4577
|
-
const
|
|
4578
|
-
|
|
4579
|
-
|
|
4692
|
+
const sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
|
|
4693
|
+
const basePath = join8(sinksDir, `${entityName}.sink.generated.ts`);
|
|
4694
|
+
const baseContent = generateSinkBase({ ...sinkInput, mode });
|
|
4695
|
+
if (!opts.dryRun) writeIfChanged(basePath, baseContent);
|
|
4696
|
+
result.written.push(basePath);
|
|
4697
|
+
const subclassPath = join8(sinksDir, `${entityName}.sink.ts`);
|
|
4698
|
+
if (existsSync6(subclassPath)) {
|
|
4699
|
+
result.scaffoldsSkipped.push(subclassPath);
|
|
4580
4700
|
} else {
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
result.scaffoldsWritten.push(sinkPath);
|
|
4701
|
+
const subclassContent = generateSinkSubclass({ ...sinkInput, mode });
|
|
4702
|
+
if (!opts.dryRun) writeFile(subclassPath, subclassContent);
|
|
4703
|
+
result.scaffoldsWritten.push(subclassPath);
|
|
4585
4704
|
}
|
|
4586
4705
|
for (const slug of slugs) {
|
|
4587
4706
|
const assemblyPath = join8(
|
|
@@ -4630,6 +4749,13 @@ function emitAdapters(opts) {
|
|
|
4630
4749
|
}
|
|
4631
4750
|
return result;
|
|
4632
4751
|
}
|
|
4752
|
+
function fkWriteKey(target, foreignKey, isSelfFk) {
|
|
4753
|
+
if (isSelfFk) {
|
|
4754
|
+
const base = foreignKey.endsWith("_id") ? foreignKey.slice(0, -3) : foreignKey;
|
|
4755
|
+
return snakeToCamel(base) + "ExternalId";
|
|
4756
|
+
}
|
|
4757
|
+
return `${target}ExternalId`;
|
|
4758
|
+
}
|
|
4633
4759
|
function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
4634
4760
|
const fields = def.fields ?? {};
|
|
4635
4761
|
const relationships = def.relationships ?? {};
|
|
@@ -4639,14 +4765,35 @@ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
|
4639
4765
|
fkColumns.add(rel2.foreign_key);
|
|
4640
4766
|
}
|
|
4641
4767
|
}
|
|
4642
|
-
const
|
|
4768
|
+
const excludeSet = new Set(def.integration?.sink?.exclude_fields ?? []);
|
|
4769
|
+
const isCopyThrough = ([name]) => name !== "id" && !fkColumns.has(name);
|
|
4770
|
+
const viewCopyThroughFields = Object.entries(fields).filter(isCopyThrough).map(([name, f]) => ({
|
|
4643
4771
|
camelName: snakeToCamel(name),
|
|
4644
4772
|
tsType: tsTypeFor(f.type, f.nullable)
|
|
4645
4773
|
}));
|
|
4774
|
+
const copyThroughFields = Object.entries(fields).filter((entry) => isCopyThrough(entry) && !excludeSet.has(entry[0])).map(([name, f]) => ({
|
|
4775
|
+
camelName: snakeToCamel(name),
|
|
4776
|
+
tsType: tsTypeFor(f.type, f.nullable)
|
|
4777
|
+
}));
|
|
4778
|
+
const entityNamePlural = def.entity.plural ?? pluralize2.plural(def.entity.name);
|
|
4646
4779
|
const fkExternalKeys = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
|
|
4647
4780
|
const target = rel2.target ?? relName;
|
|
4648
|
-
|
|
4781
|
+
const foreignKey = rel2.foreign_key ?? `${target}_id`;
|
|
4782
|
+
const isSelfFk = pluralize2.plural(target) === entityNamePlural;
|
|
4783
|
+
return { writeKey: fkWriteKey(target, foreignKey, isSelfFk) };
|
|
4649
4784
|
});
|
|
4785
|
+
const localFkColumns = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
|
|
4786
|
+
const foreignKey = rel2.foreign_key ?? `${relName}_id`;
|
|
4787
|
+
return {
|
|
4788
|
+
camelName: snakeToCamel(foreignKey),
|
|
4789
|
+
tsType: rel2.nullable ? "string | null" : "string"
|
|
4790
|
+
};
|
|
4791
|
+
});
|
|
4792
|
+
const hasTimestamps2 = (def.behaviors ?? []).some(
|
|
4793
|
+
(b) => typeof b === "string" ? b === "timestamps" : b.name === "timestamps"
|
|
4794
|
+
);
|
|
4795
|
+
const deleteKnob = def.integration?.sink?.delete;
|
|
4796
|
+
const deleteMode = deleteKnob === "noop" ? "noop" : "delegate";
|
|
4650
4797
|
return {
|
|
4651
4798
|
entityName: def.entity.name,
|
|
4652
4799
|
entityClass: pascalFromSnake(def.entity.name),
|
|
@@ -4654,8 +4801,12 @@ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
|
|
|
4654
4801
|
pattern: "Integrated",
|
|
4655
4802
|
provider,
|
|
4656
4803
|
copyThroughFields,
|
|
4804
|
+
viewCopyThroughFields,
|
|
4657
4805
|
fkExternalKeys,
|
|
4658
|
-
|
|
4806
|
+
localFkColumns,
|
|
4807
|
+
hasTimestamps: hasTimestamps2,
|
|
4808
|
+
repoImportSpecifier,
|
|
4809
|
+
deleteMode
|
|
4659
4810
|
};
|
|
4660
4811
|
}
|
|
4661
4812
|
var TS_TYPE_FOR_SINK = {
|