@pattern-stack/codegen 0.12.2 → 0.13.1

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.
@@ -1120,12 +1120,33 @@ var EventIdCursorSchema = z2.object({
1120
1120
  kind: z2.literal("eventId"),
1121
1121
  field: z2.string().min(1)
1122
1122
  });
1123
+ var HistoryIdCursorSchema = z2.object({
1124
+ kind: z2.literal("historyId"),
1125
+ field: z2.string().min(1)
1126
+ });
1127
+ var SyncTokenCursorSchema = z2.object({
1128
+ kind: z2.literal("syncToken"),
1129
+ field: z2.string().min(1)
1130
+ });
1123
1131
  var CursorStrategySchema = z2.discriminatedUnion("kind", [
1124
1132
  SystemModstampCursorSchema,
1125
1133
  ReplayIdCursorSchema,
1126
1134
  TimestampCursorSchema,
1127
- EventIdCursorSchema
1135
+ EventIdCursorSchema,
1136
+ HistoryIdCursorSchema,
1137
+ SyncTokenCursorSchema
1128
1138
  ]);
1139
+ var CURSOR_DIVISIBILITY = {
1140
+ systemModstamp: true,
1141
+ timestamp: true,
1142
+ replayId: true,
1143
+ eventId: false,
1144
+ historyId: false,
1145
+ syncToken: false
1146
+ };
1147
+ function isDivisibleCursor(kind) {
1148
+ return CURSOR_DIVISIBILITY[kind];
1149
+ }
1129
1150
  var PollDetectionSchema = z2.object({
1130
1151
  cursor: CursorStrategySchema,
1131
1152
  provenance: z2.enum(["poll", "cdc"]).optional()
@@ -8241,6 +8262,310 @@ import {
8241
8262
  writeFileSync as writeFileSync3
8242
8263
  } from "fs";
8243
8264
  import { dirname as dirname2, join as join12 } from "path";
8265
+
8266
+ // src/cli/shared/sink-emission-generator.ts
8267
+ var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
8268
+ var USER_ID_FIELD = "userId";
8269
+ function sinkNames(entityClass) {
8270
+ return {
8271
+ sinkClass: `${entityClass}Sink`,
8272
+ canonicalType: `${entityClass}Canonical`,
8273
+ repoClass: `${entityClass}Repository`,
8274
+ projectionType: `${entityClass}IntegrationProjection`,
8275
+ writeType: `${entityClass}IntegrationWrite`
8276
+ };
8277
+ }
8278
+ function generateDefaultSink(input) {
8279
+ if (input.pattern !== "Integrated") {
8280
+ throw new Error(
8281
+ `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.`
8282
+ );
8283
+ }
8284
+ const n = sinkNames(input.entityClass);
8285
+ const hasUserIdField = input.copyThroughFields.some(
8286
+ (f) => f.camelName === USER_ID_FIELD
8287
+ );
8288
+ const copyThroughLines = input.copyThroughFields.filter((f) => f.camelName !== USER_ID_FIELD).map((f) => ` ${f.camelName}: record.${f.camelName},`);
8289
+ const fkTodoLines = input.fkExternalKeys.map(
8290
+ (fk) => ` // ${fk.writeKey}: /* TODO(author): external id of the related ${relationLabel(fk.writeKey)} */ null,`
8291
+ );
8292
+ const writeBodyLines = [
8293
+ ` externalId: record.externalId,`
8294
+ ];
8295
+ if (copyThroughLines.length > 0) {
8296
+ writeBodyLines.push(
8297
+ ` // copy-through fields (one line per \`fields:\` entry):`,
8298
+ ...copyThroughLines
8299
+ );
8300
+ }
8301
+ if (fkTodoLines.length > 0) {
8302
+ writeBodyLines.push(
8303
+ ` // FK external join-keys \u2014 projection has no external key; supply from your canonical record:`,
8304
+ ...fkTodoLines
8305
+ );
8306
+ }
8307
+ if (hasUserIdField) {
8308
+ writeBodyLines.push(` userId,`);
8309
+ }
8310
+ const writeBody = writeBodyLines.join("\n");
8311
+ return `${SCAFFOLD_SENTINEL}
8312
+ // Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running codegen
8313
+ // detects the sentinel above and SKIPS this file \u2014 your edits are safe.
8314
+ //
8315
+ // Default IIntegrationSink over the generated ${n.repoClass}. The PLUMBING
8316
+ // (constructor, provider-match assert, repo delegation, userId scoping, return
8317
+ // shapes) is generated. The canonical<->local FIELD MAPPING is the author seam:
8318
+ // the canonical type is whatever your adapter's changeSource yields \u2014 the same
8319
+ // seam as the IChangeSource.listChanges fetch body. For FK-free entities the
8320
+ // generated ${n.projectionType} IS the canonical shape (passthrough);
8321
+ // for entities with external FK join-keys, fill the marked TODO(s) below.
8322
+ // Source: definitions entity '${input.entityName}' (surface: ${input.surface}).
8323
+ import { Injectable } from '@nestjs/common';
8324
+ import type { IIntegrationSink } from '@pattern-stack/codegen/subsystems';
8325
+ import {
8326
+ ${n.repoClass},
8327
+ type ${n.projectionType},
8328
+ type ${n.writeType},
8329
+ } from '${input.repoImportSpecifier}';
8330
+
8331
+ /** Canonical type the orchestrator diffs. Defaults to the generated projection;
8332
+ * widen to your adapter's canonical shape if it carries fields the projection
8333
+ * does not (e.g. external FK join-keys). */
8334
+ export type ${n.canonicalType} = ${n.projectionType};
8335
+
8336
+ @Injectable()
8337
+ export class ${n.sinkClass} implements IIntegrationSink<${n.canonicalType}> {
8338
+ constructor(
8339
+ private readonly repo: ${n.repoClass},
8340
+ private readonly provider: string,
8341
+ ) {}
8342
+
8343
+ async findByExternalId(userId: string, externalId: string): Promise<${n.canonicalType} | null> {
8344
+ const row = await this.repo.findByExternalIdProjected(externalId, this.provider);
8345
+ // The repo lookup is (provider, externalId)-scoped. If your external_id is not
8346
+ // globally unique, enforce ownership here (e.g. row.userId === userId).
8347
+ return row;
8348
+ }
8349
+
8350
+ async upsertByExternalId(
8351
+ userId: string,
8352
+ record: ${n.canonicalType},
8353
+ provider: string,
8354
+ ): Promise<{ id: string; saved: ${n.canonicalType} }> {
8355
+ if (provider !== this.provider) {
8356
+ throw new Error(\`${n.sinkClass}: bound provider '\${this.provider}' != run provider '\${provider}'\`);
8357
+ }
8358
+ const write: ${n.writeType} = {
8359
+ ${writeBody}
8360
+ };
8361
+ const proj = await this.repo.integrationUpsertOne(write, this.provider);
8362
+ return { id: proj.id, saved: record };
8363
+ }
8364
+
8365
+ async softDeleteByExternalId(_userId: string, externalId: string): Promise<{ id: string } | null> {
8366
+ return this.repo.softDeleteByExternalId(externalId, this.provider);
8367
+ }
8368
+ }
8369
+ `;
8370
+ }
8371
+ function relationLabel(writeKey) {
8372
+ const stripped = writeKey.replace(/ExternalId$/, "");
8373
+ return stripped || "related entity";
8374
+ }
8375
+
8376
+ // src/cli/shared/assembly-emission-generator.ts
8377
+ import { relative, resolve as resolve6, sep } from "path";
8378
+ function generatedBanner(sourceDesc) {
8379
+ return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
8380
+ // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
8381
+ }
8382
+ function entityPascalCase(name) {
8383
+ return name.split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8384
+ }
8385
+ function entityConstantCase(name) {
8386
+ return name.replace(/-/g, "_").toUpperCase();
8387
+ }
8388
+ function integrationUseCaseToken(entityName, providerSlug) {
8389
+ return `${entityConstantCase(entityName)}_INTEGRATION_USE_CASE__${providerConstantCase(providerSlug)}`;
8390
+ }
8391
+ function assemblyModuleClass(entityName, providerSlug) {
8392
+ return `${entityPascalCase(entityName)}IntegrationModule__${providerPascalCase(providerSlug)}`;
8393
+ }
8394
+ function tokenSymbolKey(surface, entityName, providerSlug) {
8395
+ return `@app/integrations/${surface}.${entityName}-integration-use-case.${providerSlug}`;
8396
+ }
8397
+ function generateAssemblyModule(input) {
8398
+ const providerPascal = providerPascalCase(input.provider);
8399
+ const surfacePascal = providerPascalCase(input.surface);
8400
+ const adapterClass = `${providerPascal}${surfacePascal}Adapter`;
8401
+ const adapterModuleClass = `${adapterClass}Module`;
8402
+ const adapterImport = `../../adapters/${input.provider}/${input.provider}-${input.surface}.adapter`;
8403
+ const adapterModuleImport = `${adapterImport}.module`;
8404
+ const sinkClass = `${input.entityClass}Sink`;
8405
+ const sinkImport = `../../sinks/${input.entityName}.sink`;
8406
+ const token = integrationUseCaseToken(input.entityName, input.provider);
8407
+ const moduleClass = assemblyModuleClass(input.entityName, input.provider);
8408
+ const tokensImport = `../../${input.surface}-integration.tokens`;
8409
+ return `${generatedBanner(input.sourceDesc)}
8410
+ import { Module } from '@nestjs/common';
8411
+ import {
8412
+ ExecuteIntegrationUseCase,
8413
+ INTEGRATION_CHANGE_SOURCE,
8414
+ INTEGRATION_SINK,
8415
+ } from '@pattern-stack/codegen/subsystems';
8416
+ import { ${adapterClass} } from '${adapterImport}';
8417
+ import { ${adapterModuleClass} } from '${adapterModuleImport}';
8418
+ import { ${sinkClass} } from '${sinkImport}';
8419
+ import { ${input.repoClass} } from '${input.repoImportSpecifier}';
8420
+ import { ${input.moduleClass} } from '${input.moduleImportSpecifier}';
8421
+ import { ${token} } from '${tokensImport}';
8422
+
8423
+ /**
8424
+ * ${moduleClass} \u2014 the ${input.surface}/${input.entityName} \u2190 ${input.provider}
8425
+ * inbound-integration assembly (RFC-0002 \xA72, Option A).
8426
+ *
8427
+ * Binds this module's INTEGRATION_CHANGE_SOURCE from the adapter's
8428
+ * \`changeSources['${input.entityName}']\` and INTEGRATION_SINK from
8429
+ * ${sinkClass}, provides a local ExecuteIntegrationUseCase, and aliases+exports
8430
+ * it under ${token} (the bare class token is ambiguous at app root \u2014 every
8431
+ * assembly provides it). The substrate (cursor store, run recorder, differ,
8432
+ * multi-tenant flag) comes from the global IntegrationModule.forRoot(...) in
8433
+ * AppModule, never re-bound here.
8434
+ */
8435
+ @Module({
8436
+ imports: [${input.moduleClass}, ${adapterModuleClass}],
8437
+ providers: [
8438
+ {
8439
+ provide: INTEGRATION_CHANGE_SOURCE,
8440
+ useFactory: (adapter: ${adapterClass}) => adapter.changeSources['${input.entityName}'],
8441
+ inject: [${adapterClass}],
8442
+ },
8443
+ {
8444
+ provide: INTEGRATION_SINK,
8445
+ useFactory: (repo: ${input.repoClass}) => new ${sinkClass}(repo, '${input.provider}'),
8446
+ inject: [${input.repoClass}],
8447
+ },
8448
+ ExecuteIntegrationUseCase,
8449
+ { provide: ${token}, useExisting: ExecuteIntegrationUseCase },
8450
+ ],
8451
+ exports: [${token}],
8452
+ })
8453
+ export class ${moduleClass} {}
8454
+ `;
8455
+ }
8456
+ function generateIntegrationTokens(surface, entries) {
8457
+ const sorted = [...entries].sort(
8458
+ (a, b) => a.entityName === b.entityName ? a.provider < b.provider ? -1 : 1 : a.entityName < b.entityName ? -1 : 1
8459
+ );
8460
+ const tokenLines = sorted.map((e) => {
8461
+ const token = integrationUseCaseToken(e.entityName, e.provider);
8462
+ const key = tokenSymbolKey(surface, e.entityName, e.provider);
8463
+ return `/** Unique handle for the ${e.entityName} \u2190 ${e.provider} integration use-case
8464
+ * (${assemblyModuleClass(e.entityName, e.provider)}). A trigger grabs this to run it. */
8465
+ export const ${token} = Symbol.for('${key}');`;
8466
+ }).join("\n\n");
8467
+ return `${generatedBanner(`surface: ${surface}`)}
8468
+ /**
8469
+ * Use-case handles for the \`${surface}\` surface \u2014 one per (entity, provider)
8470
+ * assembly. Each is a Symbol \`provide:\` token a per-entity
8471
+ * \`ExecuteIntegrationUseCase\` is aliased under (the bare class token is
8472
+ * ambiguous at app root) and exported for the consuming trigger to grab.
8473
+ */
8474
+
8475
+ ${tokenLines || "// no (entity, provider) integration assemblies on this surface yet"}
8476
+ `;
8477
+ }
8478
+ function generateIntegrationAggregator(surface, entries) {
8479
+ const surfacePascal = providerPascalCase(surface);
8480
+ const aggregatorClass = `${surfacePascal}IntegrationModule`;
8481
+ const sorted = [...entries].sort(
8482
+ (a, b) => a.provider === b.provider ? a.entityName < b.entityName ? -1 : 1 : a.provider < b.provider ? -1 : 1
8483
+ );
8484
+ const moduleClasses = sorted.map(
8485
+ (e) => assemblyModuleClass(e.entityName, e.provider)
8486
+ );
8487
+ const importLines = sorted.map((e) => {
8488
+ const cls = assemblyModuleClass(e.entityName, e.provider);
8489
+ const path34 = `./modules/${e.provider}/${e.entityName}-integration.module`;
8490
+ return `import { ${cls} } from '${path34}';`;
8491
+ }).join("\n");
8492
+ const membersInline = moduleClasses.join(", ");
8493
+ return `${generatedBanner(`surface: ${surface}`)}
8494
+ import { Module } from '@nestjs/common';
8495
+ ${importLines || "// no (entity, provider) integration assemblies on this surface yet"}
8496
+
8497
+ /**
8498
+ * ${aggregatorClass} \u2014 the \`${surface}\` integration aggregator. AppModule
8499
+ * imports THIS (not ${surfacePascal}AdaptersModule) to wire every per-entity
8500
+ * integration on the surface. Re-exports each assembly module so their
8501
+ * <ENTITY>_INTEGRATION_USE_CASE__<PROVIDER> tokens are available to the app (the
8502
+ * trigger/consumer grabs them). Per RFC-0002 \xA77 q3, the run path goes through
8503
+ * these assembly modules, keeping the collision-prone ${surfacePascal}AdaptersModule
8504
+ * fold off the graph.
8505
+ */
8506
+ @Module({
8507
+ imports: [${membersInline}],
8508
+ exports: [${membersInline}],
8509
+ })
8510
+ export class ${aggregatorClass} {}
8511
+ `;
8512
+ }
8513
+ function pluralPascalCase(plural) {
8514
+ return plural.split("_").filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
8515
+ }
8516
+ function toImportSpecifier(targetFileAbs, fromDirAbs, aliases) {
8517
+ const noExt = targetFileAbs.replace(/\.ts$/, "");
8518
+ let best = null;
8519
+ for (const [key, dirRaw] of Object.entries(aliases)) {
8520
+ const dir = dirRaw.endsWith(sep) ? dirRaw.slice(0, -1) : dirRaw;
8521
+ const prefix = dir + sep;
8522
+ if (noExt === dir || noExt.startsWith(prefix)) {
8523
+ const rest = noExt === dir ? "" : noExt.slice(prefix.length);
8524
+ if (!best || dir.length > aliases[best.key].length) {
8525
+ best = { key, rest };
8526
+ }
8527
+ }
8528
+ }
8529
+ if (best) {
8530
+ const restPosix = best.rest.split(sep).join("/");
8531
+ return restPosix ? `${best.key}/${restPosix}` : best.key;
8532
+ }
8533
+ let rel2 = relative(fromDirAbs, noExt).split(sep).join("/");
8534
+ if (!rel2.startsWith(".")) rel2 = `./${rel2}`;
8535
+ return rel2;
8536
+ }
8537
+ function resolveEntityModuleImports(input) {
8538
+ const entityClass = entityPascalCase(input.entityName);
8539
+ const repoClass = `${entityClass}Repository`;
8540
+ const moduleClass = `${pluralPascalCase(input.entityPlural)}Module`;
8541
+ const moduleGroupSegs = input.context ? ["modules", input.context, input.entityPlural] : ["modules", input.entityPlural];
8542
+ const repoFileAbs = resolve6(
8543
+ input.backendSrcAbs,
8544
+ ...moduleGroupSegs,
8545
+ `${input.entityName}.repository.ts`
8546
+ );
8547
+ const moduleFileAbs = resolve6(
8548
+ input.backendSrcAbs,
8549
+ ...moduleGroupSegs,
8550
+ `${input.entityPlural}.module.ts`
8551
+ );
8552
+ const assemblyDirAbs = resolve6(
8553
+ input.backendSrcAbs,
8554
+ "integrations",
8555
+ input.surface,
8556
+ "modules",
8557
+ input.provider
8558
+ );
8559
+ return {
8560
+ entityClass,
8561
+ repoClass,
8562
+ moduleClass,
8563
+ repoImportSpecifier: toImportSpecifier(repoFileAbs, assemblyDirAbs, input.aliases),
8564
+ moduleImportSpecifier: toImportSpecifier(moduleFileAbs, assemblyDirAbs, input.aliases)
8565
+ };
8566
+ }
8567
+
8568
+ // src/cli/shared/adapter-emission-generator.ts
8244
8569
  var SURFACE_REGISTRY = {
8245
8570
  crm: {
8246
8571
  packageName: "@pattern-stack/codegen-crm",
@@ -8280,25 +8605,41 @@ var SURFACE_REGISTRY = {
8280
8605
  portType: "CalendarPort",
8281
8606
  capabilitiesType: "CalendarCapabilities",
8282
8607
  noCapsConst: "NO_CALENDAR_CAPABILITIES",
8283
- l2Ports: []
8608
+ l2Ports: [],
8609
+ readPrimitive: true
8284
8610
  },
8285
8611
  mail: {
8286
8612
  packageName: "@pattern-stack/codegen-mail",
8287
8613
  portType: "MailPort",
8288
8614
  capabilitiesType: "MailCapabilities",
8289
8615
  noCapsConst: "NO_MAIL_CAPABILITIES",
8290
- l2Ports: []
8616
+ l2Ports: [],
8617
+ readPrimitive: true
8291
8618
  },
8292
8619
  transcript: {
8293
8620
  packageName: "@pattern-stack/codegen-transcript",
8294
8621
  portType: "TranscriptPort",
8295
8622
  capabilitiesType: "TranscriptCapabilities",
8296
8623
  noCapsConst: "NO_TRANSCRIPT_CAPABILITIES",
8297
- l2Ports: []
8624
+ l2Ports: [],
8625
+ readPrimitive: true
8626
+ },
8627
+ // messaging (swe-brain ADR-0008) — interaction surface like transcript: the
8628
+ // adapter contributes per-entity change sources for `channel` + `message`
8629
+ // (`conversation` is domain-derived by segmentation, not vendor-read). The
8630
+ // capability descriptor adds an optional `canWrite` flag for the bot-user write
8631
+ // path, which ships dark in v1; the scaffold still only constructs `entities`.
8632
+ messaging: {
8633
+ packageName: "@pattern-stack/codegen-messaging",
8634
+ portType: "MessagingPort",
8635
+ capabilitiesType: "MessagingCapabilities",
8636
+ noCapsConst: "NO_MESSAGING_CAPABILITIES",
8637
+ l2Ports: [],
8638
+ readPrimitive: true
8298
8639
  }
8299
8640
  };
8300
- var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
8301
- function generatedBanner(sourceDesc) {
8641
+ var SCAFFOLD_SENTINEL2 = "// <CODEGEN-SCAFFOLD-V1>";
8642
+ function generatedBanner2(sourceDesc) {
8302
8643
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
8303
8644
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
8304
8645
  }
@@ -8336,17 +8677,139 @@ function names(providerSlug, surface) {
8336
8677
  entitySourcesToken: `${surfaceConst}_ENTITY_SOURCES`
8337
8678
  };
8338
8679
  }
8339
- function generateAdapterScaffold(def, surface, entities) {
8680
+ function entityPascalCase2(name) {
8681
+ return name.split(/[-_]/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
8682
+ }
8683
+ function entityConstCase(name) {
8684
+ return name.replace(/-/g, "_").toUpperCase();
8685
+ }
8686
+ function serializeFilterArray(filters) {
8687
+ if (filters.length === 0) return "[]";
8688
+ const items = filters.map(
8689
+ (f) => ` { field: ${JSON.stringify(f.field)}, op: ${JSON.stringify(f.op)}, value: ${JSON.stringify(f.value)} },`
8690
+ );
8691
+ return `[
8692
+ ${items.join("\n")}
8693
+ ]`;
8694
+ }
8695
+ function buildReadPrimitiveEmission(providerSlug, providerPascal, surface, entities, entityDetection, clientExportName) {
8696
+ const canonicalTypes = [];
8697
+ const blocks = [];
8698
+ const entries = [];
8699
+ for (const entity of entities) {
8700
+ const pascal = entityPascalCase2(entity);
8701
+ const canonical = `Canonical${pascal}`;
8702
+ const className = `${providerPascal}${pascal}IncrementalRead`;
8703
+ const constName = `${entityConstCase(entity)}_DETECTION_FILTERS`;
8704
+ canonicalTypes.push(canonical);
8705
+ const det = entityDetection?.get(entity);
8706
+ const filters = det?.filters ?? [];
8707
+ const cursorKind = det && det.mode === "poll" ? det.poll.cursor.kind : void 0;
8708
+ const atomic = cursorKind !== void 0 && !isDivisibleCursor(cursorKind);
8709
+ const cursorOverride = atomic ? `
8710
+ // \`${cursorKind}\` is an ATOMIC cursor (RFC-0003 \xA73): its next value only exists
8711
+ // at end-of-walk, so per-ref cursors are withheld and only the final record
8712
+ // carries the token \u2014 a mid-walk crash never persists an unresumable value.
8713
+ protected override readonly cursorDivisible = false;` : "";
8714
+ blocks.push(`/**
8715
+ * \`detection.filters\` for \`${entity}\`, emitted from YAML as a static
8716
+ * \`ResolvedFilter[]\` (RFC-0003 \xA74 fork #2); \`filterFor()\` returns it.
8717
+ */
8718
+ const ${constName}: ResolvedFilter[] = ${serializeFilterArray(filters)};
8719
+
8720
+ // Emit-once read primitive (author-owned). Fill the three vendor methods below.
8721
+ export class ${className} extends IncrementalReadBase<${canonical}, ResolvedFilter[]> {
8722
+ readonly label = '${providerSlug}-${surface}-${entity}';
8723
+ // Flip to \`true\` if your \`enumerate\` pushes the request filter to the vendor
8724
+ // (e.g. Gmail \`q=\`); leave \`false\` to filter post-hydrate via \`matchesRecord\`.
8725
+ protected override readonly filterPushdown = false;${cursorOverride}
8726
+
8727
+ constructor(
8728
+ private readonly auth: IAuthStrategy,
8729
+ private readonly client: ${clientExportName},
8730
+ ) {
8731
+ super();
8732
+ }
8733
+
8734
+ /** TODO: walk the vendor list endpoint \u2192 pages of \`Ref\` (id + cursor + meta). */
8735
+ protected async *enumerate(
8736
+ _mode: ReadMode,
8737
+ _filter?: ResolvedFilter[],
8738
+ _pageSize?: number,
8739
+ ): AsyncIterable<Ref[]> {
8740
+ throw new Error('not implemented: ${className}.enumerate');
8741
+ }
8742
+
8743
+ /** TODO: batched fetch-by-id \u2192 \`Map<id, raw>\` (\`mapConcurrent\`, or a vendor /batch). */
8744
+ protected async hydrate(_ids: string[]): Promise<Map<string, unknown>> {
8745
+ throw new Error('not implemented: ${className}.hydrate');
8746
+ }
8747
+
8748
+ /** TODO: vendor payload \u2192 \`${canonical}\` (return \`null\` to drop). */
8749
+ protected toCanonical(_raw: unknown): ${canonical} | null {
8750
+ throw new Error('not implemented: ${className}.toCanonical');
8751
+ }
8752
+
8753
+ protected override filterFor(
8754
+ _subscription: IntegrationSubscriptionView,
8755
+ ): ResolvedFilter[] {
8756
+ return ${constName};
8757
+ }
8758
+ }`);
8759
+ entries.push(` ${entity}: new ${className}(this.auth, this.client),`);
8760
+ }
8761
+ return { canonicalTypes, preamble: blocks.join("\n\n"), changeSourceEntries: entries };
8762
+ }
8763
+ function generateAdapterScaffold(def, surface, entities, entityDetection) {
8340
8764
  const spec = SURFACE_REGISTRY[surface];
8341
8765
  if (!spec) throw new Error(`no surface package for '${surface}'`);
8342
8766
  const n = names(def.slug, surface);
8343
8767
  const client = parseImportRef(def.client.class);
8344
8768
  const entitiesLiteral = entities.length ? `[${entities.map((e) => `'${e}'`).join(", ")}]` : "[]";
8769
+ const readPrimitive = !!spec.readPrimitive && entities.length > 0;
8770
+ const rp = readPrimitive ? buildReadPrimitiveEmission(
8771
+ def.slug,
8772
+ n.providerPascal,
8773
+ surface,
8774
+ entities,
8775
+ entityDetection,
8776
+ client.exportName
8777
+ ) : null;
8345
8778
  const surfaceTypeImports = [
8346
8779
  spec.portType,
8347
8780
  ...spec.l2Ports.map((p) => p.type),
8348
- spec.capabilitiesType
8781
+ spec.capabilitiesType,
8782
+ ...rp ? rp.canonicalTypes : []
8783
+ ].map((t) => ` ${t},`).join("\n");
8784
+ const subsystemValueImport = rp ? `import { IncrementalReadBase } from '@pattern-stack/codegen/subsystems';
8785
+ ` : "";
8786
+ const subsystemTypeImports = [
8787
+ "IAuthStrategy",
8788
+ "IChangeSource",
8789
+ ...rp ? ["IntegrationSubscriptionView", "ReadMode", "Ref", "ResolvedFilter"] : []
8349
8790
  ].map((t) => ` ${t},`).join("\n");
8791
+ const changeSourcesAssign = rp ? `
8792
+ this.changeSources = {
8793
+ ${rp.changeSourceEntries.join("\n")}
8794
+ };
8795
+ ` : "";
8796
+ const changeSourcesDecl = rp ? ` /**
8797
+ * Per-entity change sources contributed to the ${surface} registry, keyed by
8798
+ * entity name. The surface aggregator folds these into the
8799
+ * \`IEntityChangeSourceRegistry\` bound under \`${n.entitySourcesToken}\`.
8800
+ * Emit-once: edit the \`IncrementalReadBase\` subclasses above, not this map.
8801
+ */
8802
+ readonly changeSources: Record<string, IChangeSource<unknown>>;` : ` /**
8803
+ * Per-entity change sources this adapter contributes to the ${surface}
8804
+ * registry (ADR-033 \`buildChangeSource\`), keyed by entity name. The
8805
+ * surface aggregator folds these into the \`IEntityChangeSourceRegistry\`
8806
+ * bound under \`${n.entitySourcesToken}\`. Author-owned \u2014 populate one entry
8807
+ * per entity in \`capabilities.entities\`.
8808
+ */
8809
+ readonly changeSources: Record<string, IChangeSource<unknown>> = {};`;
8810
+ const preambleSection = rp ? `
8811
+ ${rp.preamble}
8812
+ ` : "";
8350
8813
  const capabilityBody = [
8351
8814
  ` ...${spec.noCapsConst},`,
8352
8815
  ...spec.l2Ports.map((p) => ` ${p.capFlag}: true,`),
@@ -8363,7 +8826,7 @@ function generateAdapterScaffold(def, surface, entities) {
8363
8826
  const l2Section = l2Members ? `
8364
8827
  ${l2Members}
8365
8828
  ` : "";
8366
- return `${SCAFFOLD_SENTINEL}
8829
+ return `${SCAFFOLD_SENTINEL2}
8367
8830
  // Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running
8368
8831
  // codegen detects the sentinel above and SKIPS this file \u2014 your edits are safe.
8369
8832
  // Source: definitions/providers/${def.slug}.yaml (surface: ${surface}).
@@ -8372,15 +8835,12 @@ import type {
8372
8835
  ${surfaceTypeImports}
8373
8836
  } from '${spec.packageName}';
8374
8837
  import { ${spec.noCapsConst} } from '${spec.packageName}';
8375
- import type {
8376
- IAuthStrategy,
8377
- IChangeSource,
8378
- IEntityChangeSourceRegistry,
8838
+ ${subsystemValueImport}import type {
8839
+ ${subsystemTypeImports}
8379
8840
  } from '@pattern-stack/codegen/subsystems';
8380
8841
  import type { ${client.exportName} } from '${client.path}';
8381
8842
  import { ${n.strategyToken}, ${n.clientToken} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8382
- import { ${n.entitySourcesToken} } from '../../${surface}-adapters.tokens';
8383
-
8843
+ ${preambleSection}
8384
8844
  @Injectable()
8385
8845
  export class ${n.adapterClass} implements ${spec.portType} {
8386
8846
  /** Declared capabilities. \`entities\` derives from \`surface: ${surface}\` entity YAML. */
@@ -8391,17 +8851,9 @@ ${capabilityBody}
8391
8851
  constructor(
8392
8852
  @Inject(${n.strategyToken}) readonly auth: IAuthStrategy,
8393
8853
  @Inject(${n.clientToken}) private readonly client: ${client.exportName},
8394
- @Inject(${n.entitySourcesToken}) readonly sources: IEntityChangeSourceRegistry,
8395
- ) {}
8854
+ ) {${changeSourcesAssign}}
8396
8855
  ${l2Section}
8397
- /**
8398
- * Per-entity change sources this adapter contributes to the ${surface}
8399
- * registry (ADR-033 \`buildChangeSource\`), keyed by entity name. The
8400
- * surface aggregator folds these into the \`IEntityChangeSourceRegistry\`
8401
- * bound under \`${n.entitySourcesToken}\`. Author-owned \u2014 populate one entry
8402
- * per entity in \`capabilities.entities\`.
8403
- */
8404
- readonly changeSources: Record<string, IChangeSource<unknown>> = {};
8856
+ ${changeSourcesDecl}
8405
8857
 
8406
8858
  // surface-only methods (optional on ${spec.portType}): add here
8407
8859
  }
@@ -8409,7 +8861,7 @@ ${l2Section}
8409
8861
  }
8410
8862
  function generateAdapterModule(def, surface) {
8411
8863
  const n = names(def.slug, surface);
8412
- return `${generatedBanner(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8864
+ return `${generatedBanner2(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8413
8865
  import { Module } from '@nestjs/common';
8414
8866
  import { ${n.providerModuleClass} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8415
8867
  import { ${n.adapterClass} } from './${def.slug}-${surface}.adapter';
@@ -8427,13 +8879,13 @@ function generateAdaptersBarrel(surface, providerSlugs) {
8427
8879
  const n = names(slug, surface);
8428
8880
  return `export { ${n.adapterModuleClass} } from './${slug}/${slug}-${surface}.adapter.module';`;
8429
8881
  }).join("\n");
8430
- return `${generatedBanner(`definitions/providers/*.yaml (surface: ${surface})`)}
8882
+ return `${generatedBanner2(`definitions/providers/*.yaml (surface: ${surface})`)}
8431
8883
  ${lines}
8432
8884
  `;
8433
8885
  }
8434
8886
  function generateSurfaceTokens(surface) {
8435
8887
  const n = names("__placeholder__", surface);
8436
- return `${generatedBanner(`surface: ${surface}`)}
8888
+ return `${generatedBanner2(`surface: ${surface}`)}
8437
8889
  import type { IChangeSource } from '@pattern-stack/codegen/subsystems';
8438
8890
 
8439
8891
  /** The assembled list of every ${surface} adapter's contribution. */
@@ -8472,7 +8924,7 @@ function generateSurfaceAggregator(surface, providerSlugs) {
8472
8924
  return `${lowerFirst(p.adapterClass)}: ${p.adapterClass}`;
8473
8925
  }).join(", ");
8474
8926
  const injectTokens = per.map((p) => p.adapterClass).join(", ");
8475
- return `${generatedBanner(`surface: ${surface}`)}
8927
+ return `${generatedBanner2(`surface: ${surface}`)}
8476
8928
  import { Module } from '@nestjs/common';
8477
8929
  import {
8478
8930
  MemoryEntityChangeSourceRegistry,
@@ -8544,7 +8996,7 @@ function generateTypedView(surface, providerSlugs, entities) {
8544
8996
  const providerUnion = slugs.length ? slugs.map((s) => `'${s}'`).join(" | ") : "never";
8545
8997
  const entityUnion = ents.length ? ents.map((e) => `'${e}'`).join(" | ") : "never";
8546
8998
  const mapEntries = slugs.map((s) => ` ${jsKey(s)}: ${surfacePascal}Entity;`).join("\n");
8547
- return `${generatedBanner(`surface: ${surface}`)}
8999
+ return `${generatedBanner2(`surface: ${surface}`)}
8548
9000
  /**
8549
9001
  * Per-consumer typed view for the \`${surface}\` surface. Surface-scoped unions
8550
9002
  * + a (provider, entity) validity map for compile-time-checked consumer
@@ -8571,9 +9023,14 @@ function emitAdapters(opts) {
8571
9023
  written: [],
8572
9024
  scaffoldsWritten: [],
8573
9025
  scaffoldsSkipped: [],
8574
- skippedSurfaces: []
9026
+ skippedSurfaces: [],
9027
+ assembliesWritten: [],
9028
+ tokensWritten: [],
9029
+ integrationAggregatorsWritten: [],
9030
+ skippedAssemblies: []
8575
9031
  };
8576
9032
  const entitiesBySurface = collectEntitiesBySurface(opts.entities);
9033
+ const entityByName = new Map(opts.entities.map((e) => [e.entity.name, e]));
8577
9034
  const bySurface = /* @__PURE__ */ new Map();
8578
9035
  for (const { definition } of opts.providers) {
8579
9036
  for (const surface of definition.surfaces) {
@@ -8602,10 +9059,17 @@ function emitAdapters(opts) {
8602
9059
  if (existsSync10(scaffoldPath)) {
8603
9060
  result.scaffoldsSkipped.push(scaffoldPath);
8604
9061
  } else {
9062
+ const surfaceEntityNames = entitiesBySurface.get(surface) ?? [];
9063
+ const entityDetection = /* @__PURE__ */ new Map();
9064
+ for (const name of surfaceEntityNames) {
9065
+ const det = entityByName.get(name)?.entity.detection?.[slug];
9066
+ if (det) entityDetection.set(name, det);
9067
+ }
8605
9068
  const content = generateAdapterScaffold(
8606
9069
  def,
8607
9070
  surface,
8608
- entitiesBySurface.get(surface) ?? []
9071
+ surfaceEntityNames,
9072
+ entityDetection
8609
9073
  );
8610
9074
  if (!opts.dryRun) writeFile(scaffoldPath, content);
8611
9075
  result.scaffoldsWritten.push(scaffoldPath);
@@ -8628,9 +9092,141 @@ function emitAdapters(opts) {
8628
9092
  if (!opts.dryRun) writeIfChanged2(path34, content);
8629
9093
  result.written.push(path34);
8630
9094
  }
9095
+ if (opts.backendSrcAbs) {
9096
+ const aliases = opts.aliases ?? {};
9097
+ const surfaceEntities = entitiesBySurface.get(surface) ?? [];
9098
+ const tokenEntries = [];
9099
+ const assemblyEntries = [];
9100
+ const sinksDir = join12(surfaceDir, "sinks");
9101
+ const modulesDir = join12(surfaceDir, "modules");
9102
+ for (const entityName of surfaceEntities) {
9103
+ const def = entityByName.get(entityName);
9104
+ const pattern = def?.entity.pattern ?? (Array.isArray(def?.entity.patterns) ? def?.entity.patterns?.[0] : void 0);
9105
+ if (pattern !== "Integrated") {
9106
+ result.skippedAssemblies.push({
9107
+ surface,
9108
+ entity: entityName,
9109
+ reason: `entity '${entityName}' declares surface '${surface}' but is not 'pattern: Integrated'${pattern ? ` (got 'pattern: ${pattern}')` : " (no pattern declared)"} \u2014 the integration assembly + default sink need the Integrated projection/upsert path. Add 'pattern: Integrated' (or provide a hand-authored sink + assembly).`
9110
+ });
9111
+ continue;
9112
+ }
9113
+ const plural = def?.entity.plural ?? `${entityName}s`;
9114
+ const context = def?.entity.context ?? null;
9115
+ const loc = resolveEntityModuleImports({
9116
+ entityName,
9117
+ entityPlural: plural,
9118
+ context,
9119
+ surface,
9120
+ // The sink+repo import is provider-agnostic; pick any provider's
9121
+ // module dir for the relative-path base (all share the same parent).
9122
+ provider: slugs[0],
9123
+ backendSrcAbs: opts.backendSrcAbs,
9124
+ aliases
9125
+ });
9126
+ const sinkPath = join12(sinksDir, `${entityName}.sink.ts`);
9127
+ if (existsSync10(sinkPath)) {
9128
+ result.scaffoldsSkipped.push(sinkPath);
9129
+ } else {
9130
+ const sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
9131
+ const sinkContent = generateDefaultSink(sinkInput);
9132
+ if (!opts.dryRun) writeFile(sinkPath, sinkContent);
9133
+ result.scaffoldsWritten.push(sinkPath);
9134
+ }
9135
+ for (const slug of slugs) {
9136
+ const assemblyPath = join12(
9137
+ modulesDir,
9138
+ slug,
9139
+ `${entityName}-integration.module.ts`
9140
+ );
9141
+ const assemblyContent = generateAssemblyModule({
9142
+ surface,
9143
+ provider: slug,
9144
+ entityName,
9145
+ entityClass: loc.entityClass,
9146
+ moduleImportSpecifier: loc.moduleImportSpecifier,
9147
+ moduleClass: loc.moduleClass,
9148
+ repoImportSpecifier: loc.repoImportSpecifier,
9149
+ repoClass: loc.repoClass,
9150
+ sourceDesc: `definitions/providers/${slug}.yaml`
9151
+ });
9152
+ if (!opts.dryRun) writeIfChanged2(assemblyPath, assemblyContent);
9153
+ result.assembliesWritten.push(assemblyPath);
9154
+ tokenEntries.push({ entityName, entityClass: loc.entityClass, provider: slug });
9155
+ assemblyEntries.push({ entityName, provider: slug });
9156
+ }
9157
+ }
9158
+ const integrationTokensPath = join12(
9159
+ surfaceDir,
9160
+ `${surface}-integration.tokens.ts`
9161
+ );
9162
+ const tokensContent = generateIntegrationTokens(surface, tokenEntries);
9163
+ if (!opts.dryRun) writeIfChanged2(integrationTokensPath, tokensContent);
9164
+ result.tokensWritten.push(integrationTokensPath);
9165
+ if (assemblyEntries.length > 0) {
9166
+ const integrationAggregatorPath = join12(
9167
+ surfaceDir,
9168
+ `${surface}-integration.module.ts`
9169
+ );
9170
+ const aggregatorContent = generateIntegrationAggregator(
9171
+ surface,
9172
+ assemblyEntries
9173
+ );
9174
+ if (!opts.dryRun) writeIfChanged2(integrationAggregatorPath, aggregatorContent);
9175
+ result.integrationAggregatorsWritten.push(integrationAggregatorPath);
9176
+ }
9177
+ }
8631
9178
  }
8632
9179
  return result;
8633
9180
  }
9181
+ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
9182
+ const fields = def.fields ?? {};
9183
+ const relationships = def.relationships ?? {};
9184
+ const fkColumns = /* @__PURE__ */ new Set();
9185
+ for (const rel2 of Object.values(relationships)) {
9186
+ if (rel2.type === "belongs_to" && typeof rel2.foreign_key === "string") {
9187
+ fkColumns.add(rel2.foreign_key);
9188
+ }
9189
+ }
9190
+ const copyThroughFields = Object.entries(fields).filter(([name]) => name !== "id" && !fkColumns.has(name)).map(([name, f]) => ({
9191
+ camelName: snakeToCamel(name),
9192
+ tsType: tsTypeFor(f.type, f.nullable)
9193
+ }));
9194
+ const fkExternalKeys = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
9195
+ const target = rel2.target ?? relName;
9196
+ return { writeKey: `${snakeToCamel(target)}ExternalId` };
9197
+ });
9198
+ return {
9199
+ entityName: def.entity.name,
9200
+ entityClass: pascalFromSnake(def.entity.name),
9201
+ surface,
9202
+ pattern: "Integrated",
9203
+ provider,
9204
+ copyThroughFields,
9205
+ fkExternalKeys,
9206
+ repoImportSpecifier
9207
+ };
9208
+ }
9209
+ var TS_TYPE_FOR_SINK = {
9210
+ string: "string",
9211
+ integer: "number",
9212
+ decimal: "string",
9213
+ boolean: "boolean",
9214
+ uuid: "string",
9215
+ date: "Date",
9216
+ datetime: "Date",
9217
+ json: "unknown"
9218
+ };
9219
+ function tsTypeFor(type, nullable) {
9220
+ const base = TS_TYPE_FOR_SINK[type ?? "string"] ?? "unknown";
9221
+ return nullable ? `${base} | null` : base;
9222
+ }
9223
+ function snakeToCamel(s) {
9224
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
9225
+ }
9226
+ function pascalFromSnake(s) {
9227
+ const camel = snakeToCamel(s);
9228
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
9229
+ }
8634
9230
  function writeFile(outPath, content) {
8635
9231
  mkdirSync3(dirname2(outPath), { recursive: true });
8636
9232
  writeFileSync3(outPath, content);
@@ -9245,10 +9841,13 @@ var EntityNewCommand = class extends Command2 {
9245
9841
  definition: s.definition,
9246
9842
  filePath: s.filePath
9247
9843
  }));
9844
+ const assemblyTsAliases = resolveTsconfigAliases(ctx.cwd);
9248
9845
  const adapterResult = emitAdapters({
9249
9846
  providers: loadedProviders,
9250
9847
  entities: entityDefs,
9251
- outputRoot: adapterOutputRoot
9848
+ outputRoot: adapterOutputRoot,
9849
+ backendSrcAbs: path13.resolve(ctx.cwd, backendSrcForHandlers),
9850
+ aliases: assemblyTsAliases?.aliases ?? {}
9252
9851
  });
9253
9852
  if (!isJsonMode()) {
9254
9853
  if (adapterResult.written.length || adapterResult.scaffoldsWritten.length) {
@@ -9256,12 +9855,20 @@ var EntityNewCommand = class extends Command2 {
9256
9855
  `adapter codegen: ${adapterResult.scaffoldsWritten.length} scaffold(s) + ${adapterResult.written.length} @generated \u2192 ${adapterOutputRoot}`
9257
9856
  );
9258
9857
  }
9858
+ if (adapterResult.assembliesWritten.length || adapterResult.tokensWritten.length) {
9859
+ printInfo(
9860
+ `integration assembly codegen: ${adapterResult.assembliesWritten.length} module(s) + ${adapterResult.tokensWritten.length} tokens file(s) + ${adapterResult.integrationAggregatorsWritten.length} aggregator(s)`
9861
+ );
9862
+ }
9259
9863
  for (const s of adapterResult.scaffoldsSkipped) {
9260
9864
  printInfo(`skipped scaffold ${s} (author-owned)`);
9261
9865
  }
9262
9866
  for (const s of adapterResult.skippedSurfaces) {
9263
9867
  printWarning(`adapter codegen: ${s.reason} (provider ${s.provider})`);
9264
9868
  }
9869
+ for (const s of adapterResult.skippedAssemblies) {
9870
+ printWarning(`integration assembly: ${s.reason}`);
9871
+ }
9265
9872
  }
9266
9873
  }
9267
9874
  } catch (err) {
@@ -13114,10 +13721,10 @@ var ProjectInitCommand = class extends Command7 {
13114
13721
  };
13115
13722
  function askConfirm(question) {
13116
13723
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13117
- return new Promise((resolve6) => {
13724
+ return new Promise((resolve7) => {
13118
13725
  rl.question(`${question} [Y/n] `, (answer) => {
13119
13726
  rl.close();
13120
- resolve6(answer.trim().toLowerCase() !== "n");
13727
+ resolve7(answer.trim().toLowerCase() !== "n");
13121
13728
  });
13122
13729
  });
13123
13730
  }