@pattern-stack/codegen 0.12.2 → 0.13.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.
@@ -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,28 @@ 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
8298
8626
  }
8299
8627
  };
8300
- var SCAFFOLD_SENTINEL = "// <CODEGEN-SCAFFOLD-V1>";
8301
- function generatedBanner(sourceDesc) {
8628
+ var SCAFFOLD_SENTINEL2 = "// <CODEGEN-SCAFFOLD-V1>";
8629
+ function generatedBanner2(sourceDesc) {
8302
8630
  return `// @generated by @pattern-stack/codegen from ${sourceDesc} \u2014 DO NOT EDIT.
8303
8631
  // Hand edits are overwritten on re-emit. Regenerate with \`bun run codegen\`.`;
8304
8632
  }
@@ -8336,17 +8664,139 @@ function names(providerSlug, surface) {
8336
8664
  entitySourcesToken: `${surfaceConst}_ENTITY_SOURCES`
8337
8665
  };
8338
8666
  }
8339
- function generateAdapterScaffold(def, surface, entities) {
8667
+ function entityPascalCase2(name) {
8668
+ return name.split(/[-_]/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
8669
+ }
8670
+ function entityConstCase(name) {
8671
+ return name.replace(/-/g, "_").toUpperCase();
8672
+ }
8673
+ function serializeFilterArray(filters) {
8674
+ if (filters.length === 0) return "[]";
8675
+ const items = filters.map(
8676
+ (f) => ` { field: ${JSON.stringify(f.field)}, op: ${JSON.stringify(f.op)}, value: ${JSON.stringify(f.value)} },`
8677
+ );
8678
+ return `[
8679
+ ${items.join("\n")}
8680
+ ]`;
8681
+ }
8682
+ function buildReadPrimitiveEmission(providerSlug, providerPascal, surface, entities, entityDetection, clientExportName) {
8683
+ const canonicalTypes = [];
8684
+ const blocks = [];
8685
+ const entries = [];
8686
+ for (const entity of entities) {
8687
+ const pascal = entityPascalCase2(entity);
8688
+ const canonical = `Canonical${pascal}`;
8689
+ const className = `${providerPascal}${pascal}IncrementalRead`;
8690
+ const constName = `${entityConstCase(entity)}_DETECTION_FILTERS`;
8691
+ canonicalTypes.push(canonical);
8692
+ const det = entityDetection?.get(entity);
8693
+ const filters = det?.filters ?? [];
8694
+ const cursorKind = det && det.mode === "poll" ? det.poll.cursor.kind : void 0;
8695
+ const atomic = cursorKind !== void 0 && !isDivisibleCursor(cursorKind);
8696
+ const cursorOverride = atomic ? `
8697
+ // \`${cursorKind}\` is an ATOMIC cursor (RFC-0003 \xA73): its next value only exists
8698
+ // at end-of-walk, so per-ref cursors are withheld and only the final record
8699
+ // carries the token \u2014 a mid-walk crash never persists an unresumable value.
8700
+ protected override readonly cursorDivisible = false;` : "";
8701
+ blocks.push(`/**
8702
+ * \`detection.filters\` for \`${entity}\`, emitted from YAML as a static
8703
+ * \`ResolvedFilter[]\` (RFC-0003 \xA74 fork #2); \`filterFor()\` returns it.
8704
+ */
8705
+ const ${constName}: ResolvedFilter[] = ${serializeFilterArray(filters)};
8706
+
8707
+ // Emit-once read primitive (author-owned). Fill the three vendor methods below.
8708
+ export class ${className} extends IncrementalReadBase<${canonical}, ResolvedFilter[]> {
8709
+ readonly label = '${providerSlug}-${surface}-${entity}';
8710
+ // Flip to \`true\` if your \`enumerate\` pushes the request filter to the vendor
8711
+ // (e.g. Gmail \`q=\`); leave \`false\` to filter post-hydrate via \`matchesRecord\`.
8712
+ protected override readonly filterPushdown = false;${cursorOverride}
8713
+
8714
+ constructor(
8715
+ private readonly auth: IAuthStrategy,
8716
+ private readonly client: ${clientExportName},
8717
+ ) {
8718
+ super();
8719
+ }
8720
+
8721
+ /** TODO: walk the vendor list endpoint \u2192 pages of \`Ref\` (id + cursor + meta). */
8722
+ protected async *enumerate(
8723
+ _mode: ReadMode,
8724
+ _filter?: ResolvedFilter[],
8725
+ _pageSize?: number,
8726
+ ): AsyncIterable<Ref[]> {
8727
+ throw new Error('not implemented: ${className}.enumerate');
8728
+ }
8729
+
8730
+ /** TODO: batched fetch-by-id \u2192 \`Map<id, raw>\` (\`mapConcurrent\`, or a vendor /batch). */
8731
+ protected async hydrate(_ids: string[]): Promise<Map<string, unknown>> {
8732
+ throw new Error('not implemented: ${className}.hydrate');
8733
+ }
8734
+
8735
+ /** TODO: vendor payload \u2192 \`${canonical}\` (return \`null\` to drop). */
8736
+ protected toCanonical(_raw: unknown): ${canonical} | null {
8737
+ throw new Error('not implemented: ${className}.toCanonical');
8738
+ }
8739
+
8740
+ protected override filterFor(
8741
+ _subscription: IntegrationSubscriptionView,
8742
+ ): ResolvedFilter[] {
8743
+ return ${constName};
8744
+ }
8745
+ }`);
8746
+ entries.push(` ${entity}: new ${className}(this.auth, this.client),`);
8747
+ }
8748
+ return { canonicalTypes, preamble: blocks.join("\n\n"), changeSourceEntries: entries };
8749
+ }
8750
+ function generateAdapterScaffold(def, surface, entities, entityDetection) {
8340
8751
  const spec = SURFACE_REGISTRY[surface];
8341
8752
  if (!spec) throw new Error(`no surface package for '${surface}'`);
8342
8753
  const n = names(def.slug, surface);
8343
8754
  const client = parseImportRef(def.client.class);
8344
8755
  const entitiesLiteral = entities.length ? `[${entities.map((e) => `'${e}'`).join(", ")}]` : "[]";
8756
+ const readPrimitive = !!spec.readPrimitive && entities.length > 0;
8757
+ const rp = readPrimitive ? buildReadPrimitiveEmission(
8758
+ def.slug,
8759
+ n.providerPascal,
8760
+ surface,
8761
+ entities,
8762
+ entityDetection,
8763
+ client.exportName
8764
+ ) : null;
8345
8765
  const surfaceTypeImports = [
8346
8766
  spec.portType,
8347
8767
  ...spec.l2Ports.map((p) => p.type),
8348
- spec.capabilitiesType
8768
+ spec.capabilitiesType,
8769
+ ...rp ? rp.canonicalTypes : []
8770
+ ].map((t) => ` ${t},`).join("\n");
8771
+ const subsystemValueImport = rp ? `import { IncrementalReadBase } from '@pattern-stack/codegen/subsystems';
8772
+ ` : "";
8773
+ const subsystemTypeImports = [
8774
+ "IAuthStrategy",
8775
+ "IChangeSource",
8776
+ ...rp ? ["IntegrationSubscriptionView", "ReadMode", "Ref", "ResolvedFilter"] : []
8349
8777
  ].map((t) => ` ${t},`).join("\n");
8778
+ const changeSourcesAssign = rp ? `
8779
+ this.changeSources = {
8780
+ ${rp.changeSourceEntries.join("\n")}
8781
+ };
8782
+ ` : "";
8783
+ const changeSourcesDecl = rp ? ` /**
8784
+ * Per-entity change sources contributed to the ${surface} registry, keyed by
8785
+ * entity name. The surface aggregator folds these into the
8786
+ * \`IEntityChangeSourceRegistry\` bound under \`${n.entitySourcesToken}\`.
8787
+ * Emit-once: edit the \`IncrementalReadBase\` subclasses above, not this map.
8788
+ */
8789
+ readonly changeSources: Record<string, IChangeSource<unknown>>;` : ` /**
8790
+ * Per-entity change sources this adapter contributes to the ${surface}
8791
+ * registry (ADR-033 \`buildChangeSource\`), keyed by entity name. The
8792
+ * surface aggregator folds these into the \`IEntityChangeSourceRegistry\`
8793
+ * bound under \`${n.entitySourcesToken}\`. Author-owned \u2014 populate one entry
8794
+ * per entity in \`capabilities.entities\`.
8795
+ */
8796
+ readonly changeSources: Record<string, IChangeSource<unknown>> = {};`;
8797
+ const preambleSection = rp ? `
8798
+ ${rp.preamble}
8799
+ ` : "";
8350
8800
  const capabilityBody = [
8351
8801
  ` ...${spec.noCapsConst},`,
8352
8802
  ...spec.l2Ports.map((p) => ` ${p.capFlag}: true,`),
@@ -8363,7 +8813,7 @@ function generateAdapterScaffold(def, surface, entities) {
8363
8813
  const l2Section = l2Members ? `
8364
8814
  ${l2Members}
8365
8815
  ` : "";
8366
- return `${SCAFFOLD_SENTINEL}
8816
+ return `${SCAFFOLD_SENTINEL2}
8367
8817
  // Scaffolded once by @pattern-stack/codegen, then author-owned. Re-running
8368
8818
  // codegen detects the sentinel above and SKIPS this file \u2014 your edits are safe.
8369
8819
  // Source: definitions/providers/${def.slug}.yaml (surface: ${surface}).
@@ -8372,15 +8822,12 @@ import type {
8372
8822
  ${surfaceTypeImports}
8373
8823
  } from '${spec.packageName}';
8374
8824
  import { ${spec.noCapsConst} } from '${spec.packageName}';
8375
- import type {
8376
- IAuthStrategy,
8377
- IChangeSource,
8378
- IEntityChangeSourceRegistry,
8825
+ ${subsystemValueImport}import type {
8826
+ ${subsystemTypeImports}
8379
8827
  } from '@pattern-stack/codegen/subsystems';
8380
8828
  import type { ${client.exportName} } from '${client.path}';
8381
8829
  import { ${n.strategyToken}, ${n.clientToken} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8382
- import { ${n.entitySourcesToken} } from '../../${surface}-adapters.tokens';
8383
-
8830
+ ${preambleSection}
8384
8831
  @Injectable()
8385
8832
  export class ${n.adapterClass} implements ${spec.portType} {
8386
8833
  /** Declared capabilities. \`entities\` derives from \`surface: ${surface}\` entity YAML. */
@@ -8391,17 +8838,9 @@ ${capabilityBody}
8391
8838
  constructor(
8392
8839
  @Inject(${n.strategyToken}) readonly auth: IAuthStrategy,
8393
8840
  @Inject(${n.clientToken}) private readonly client: ${client.exportName},
8394
- @Inject(${n.entitySourcesToken}) readonly sources: IEntityChangeSourceRegistry,
8395
- ) {}
8841
+ ) {${changeSourcesAssign}}
8396
8842
  ${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>> = {};
8843
+ ${changeSourcesDecl}
8405
8844
 
8406
8845
  // surface-only methods (optional on ${spec.portType}): add here
8407
8846
  }
@@ -8409,7 +8848,7 @@ ${l2Section}
8409
8848
  }
8410
8849
  function generateAdapterModule(def, surface) {
8411
8850
  const n = names(def.slug, surface);
8412
- return `${generatedBanner(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8851
+ return `${generatedBanner2(`definitions/providers/${def.slug}.yaml (surface: ${surface})`)}
8413
8852
  import { Module } from '@nestjs/common';
8414
8853
  import { ${n.providerModuleClass} } from '../../../providers/${def.slug}/${def.slug}.provider.module';
8415
8854
  import { ${n.adapterClass} } from './${def.slug}-${surface}.adapter';
@@ -8427,13 +8866,13 @@ function generateAdaptersBarrel(surface, providerSlugs) {
8427
8866
  const n = names(slug, surface);
8428
8867
  return `export { ${n.adapterModuleClass} } from './${slug}/${slug}-${surface}.adapter.module';`;
8429
8868
  }).join("\n");
8430
- return `${generatedBanner(`definitions/providers/*.yaml (surface: ${surface})`)}
8869
+ return `${generatedBanner2(`definitions/providers/*.yaml (surface: ${surface})`)}
8431
8870
  ${lines}
8432
8871
  `;
8433
8872
  }
8434
8873
  function generateSurfaceTokens(surface) {
8435
8874
  const n = names("__placeholder__", surface);
8436
- return `${generatedBanner(`surface: ${surface}`)}
8875
+ return `${generatedBanner2(`surface: ${surface}`)}
8437
8876
  import type { IChangeSource } from '@pattern-stack/codegen/subsystems';
8438
8877
 
8439
8878
  /** The assembled list of every ${surface} adapter's contribution. */
@@ -8472,7 +8911,7 @@ function generateSurfaceAggregator(surface, providerSlugs) {
8472
8911
  return `${lowerFirst(p.adapterClass)}: ${p.adapterClass}`;
8473
8912
  }).join(", ");
8474
8913
  const injectTokens = per.map((p) => p.adapterClass).join(", ");
8475
- return `${generatedBanner(`surface: ${surface}`)}
8914
+ return `${generatedBanner2(`surface: ${surface}`)}
8476
8915
  import { Module } from '@nestjs/common';
8477
8916
  import {
8478
8917
  MemoryEntityChangeSourceRegistry,
@@ -8544,7 +8983,7 @@ function generateTypedView(surface, providerSlugs, entities) {
8544
8983
  const providerUnion = slugs.length ? slugs.map((s) => `'${s}'`).join(" | ") : "never";
8545
8984
  const entityUnion = ents.length ? ents.map((e) => `'${e}'`).join(" | ") : "never";
8546
8985
  const mapEntries = slugs.map((s) => ` ${jsKey(s)}: ${surfacePascal}Entity;`).join("\n");
8547
- return `${generatedBanner(`surface: ${surface}`)}
8986
+ return `${generatedBanner2(`surface: ${surface}`)}
8548
8987
  /**
8549
8988
  * Per-consumer typed view for the \`${surface}\` surface. Surface-scoped unions
8550
8989
  * + a (provider, entity) validity map for compile-time-checked consumer
@@ -8571,9 +9010,14 @@ function emitAdapters(opts) {
8571
9010
  written: [],
8572
9011
  scaffoldsWritten: [],
8573
9012
  scaffoldsSkipped: [],
8574
- skippedSurfaces: []
9013
+ skippedSurfaces: [],
9014
+ assembliesWritten: [],
9015
+ tokensWritten: [],
9016
+ integrationAggregatorsWritten: [],
9017
+ skippedAssemblies: []
8575
9018
  };
8576
9019
  const entitiesBySurface = collectEntitiesBySurface(opts.entities);
9020
+ const entityByName = new Map(opts.entities.map((e) => [e.entity.name, e]));
8577
9021
  const bySurface = /* @__PURE__ */ new Map();
8578
9022
  for (const { definition } of opts.providers) {
8579
9023
  for (const surface of definition.surfaces) {
@@ -8602,10 +9046,17 @@ function emitAdapters(opts) {
8602
9046
  if (existsSync10(scaffoldPath)) {
8603
9047
  result.scaffoldsSkipped.push(scaffoldPath);
8604
9048
  } else {
9049
+ const surfaceEntityNames = entitiesBySurface.get(surface) ?? [];
9050
+ const entityDetection = /* @__PURE__ */ new Map();
9051
+ for (const name of surfaceEntityNames) {
9052
+ const det = entityByName.get(name)?.entity.detection?.[slug];
9053
+ if (det) entityDetection.set(name, det);
9054
+ }
8605
9055
  const content = generateAdapterScaffold(
8606
9056
  def,
8607
9057
  surface,
8608
- entitiesBySurface.get(surface) ?? []
9058
+ surfaceEntityNames,
9059
+ entityDetection
8609
9060
  );
8610
9061
  if (!opts.dryRun) writeFile(scaffoldPath, content);
8611
9062
  result.scaffoldsWritten.push(scaffoldPath);
@@ -8628,9 +9079,141 @@ function emitAdapters(opts) {
8628
9079
  if (!opts.dryRun) writeIfChanged2(path34, content);
8629
9080
  result.written.push(path34);
8630
9081
  }
9082
+ if (opts.backendSrcAbs) {
9083
+ const aliases = opts.aliases ?? {};
9084
+ const surfaceEntities = entitiesBySurface.get(surface) ?? [];
9085
+ const tokenEntries = [];
9086
+ const assemblyEntries = [];
9087
+ const sinksDir = join12(surfaceDir, "sinks");
9088
+ const modulesDir = join12(surfaceDir, "modules");
9089
+ for (const entityName of surfaceEntities) {
9090
+ const def = entityByName.get(entityName);
9091
+ const pattern = def?.entity.pattern ?? (Array.isArray(def?.entity.patterns) ? def?.entity.patterns?.[0] : void 0);
9092
+ if (pattern !== "Integrated") {
9093
+ result.skippedAssemblies.push({
9094
+ surface,
9095
+ entity: entityName,
9096
+ 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).`
9097
+ });
9098
+ continue;
9099
+ }
9100
+ const plural = def?.entity.plural ?? `${entityName}s`;
9101
+ const context = def?.entity.context ?? null;
9102
+ const loc = resolveEntityModuleImports({
9103
+ entityName,
9104
+ entityPlural: plural,
9105
+ context,
9106
+ surface,
9107
+ // The sink+repo import is provider-agnostic; pick any provider's
9108
+ // module dir for the relative-path base (all share the same parent).
9109
+ provider: slugs[0],
9110
+ backendSrcAbs: opts.backendSrcAbs,
9111
+ aliases
9112
+ });
9113
+ const sinkPath = join12(sinksDir, `${entityName}.sink.ts`);
9114
+ if (existsSync10(sinkPath)) {
9115
+ result.scaffoldsSkipped.push(sinkPath);
9116
+ } else {
9117
+ const sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
9118
+ const sinkContent = generateDefaultSink(sinkInput);
9119
+ if (!opts.dryRun) writeFile(sinkPath, sinkContent);
9120
+ result.scaffoldsWritten.push(sinkPath);
9121
+ }
9122
+ for (const slug of slugs) {
9123
+ const assemblyPath = join12(
9124
+ modulesDir,
9125
+ slug,
9126
+ `${entityName}-integration.module.ts`
9127
+ );
9128
+ const assemblyContent = generateAssemblyModule({
9129
+ surface,
9130
+ provider: slug,
9131
+ entityName,
9132
+ entityClass: loc.entityClass,
9133
+ moduleImportSpecifier: loc.moduleImportSpecifier,
9134
+ moduleClass: loc.moduleClass,
9135
+ repoImportSpecifier: loc.repoImportSpecifier,
9136
+ repoClass: loc.repoClass,
9137
+ sourceDesc: `definitions/providers/${slug}.yaml`
9138
+ });
9139
+ if (!opts.dryRun) writeIfChanged2(assemblyPath, assemblyContent);
9140
+ result.assembliesWritten.push(assemblyPath);
9141
+ tokenEntries.push({ entityName, entityClass: loc.entityClass, provider: slug });
9142
+ assemblyEntries.push({ entityName, provider: slug });
9143
+ }
9144
+ }
9145
+ const integrationTokensPath = join12(
9146
+ surfaceDir,
9147
+ `${surface}-integration.tokens.ts`
9148
+ );
9149
+ const tokensContent = generateIntegrationTokens(surface, tokenEntries);
9150
+ if (!opts.dryRun) writeIfChanged2(integrationTokensPath, tokensContent);
9151
+ result.tokensWritten.push(integrationTokensPath);
9152
+ if (assemblyEntries.length > 0) {
9153
+ const integrationAggregatorPath = join12(
9154
+ surfaceDir,
9155
+ `${surface}-integration.module.ts`
9156
+ );
9157
+ const aggregatorContent = generateIntegrationAggregator(
9158
+ surface,
9159
+ assemblyEntries
9160
+ );
9161
+ if (!opts.dryRun) writeIfChanged2(integrationAggregatorPath, aggregatorContent);
9162
+ result.integrationAggregatorsWritten.push(integrationAggregatorPath);
9163
+ }
9164
+ }
8631
9165
  }
8632
9166
  return result;
8633
9167
  }
9168
+ function buildSinkInput(def, surface, provider, repoImportSpecifier) {
9169
+ const fields = def.fields ?? {};
9170
+ const relationships = def.relationships ?? {};
9171
+ const fkColumns = /* @__PURE__ */ new Set();
9172
+ for (const rel2 of Object.values(relationships)) {
9173
+ if (rel2.type === "belongs_to" && typeof rel2.foreign_key === "string") {
9174
+ fkColumns.add(rel2.foreign_key);
9175
+ }
9176
+ }
9177
+ const copyThroughFields = Object.entries(fields).filter(([name]) => name !== "id" && !fkColumns.has(name)).map(([name, f]) => ({
9178
+ camelName: snakeToCamel(name),
9179
+ tsType: tsTypeFor(f.type, f.nullable)
9180
+ }));
9181
+ const fkExternalKeys = Object.entries(relationships).filter(([, rel2]) => rel2.type === "belongs_to").map(([relName, rel2]) => {
9182
+ const target = rel2.target ?? relName;
9183
+ return { writeKey: `${snakeToCamel(target)}ExternalId` };
9184
+ });
9185
+ return {
9186
+ entityName: def.entity.name,
9187
+ entityClass: pascalFromSnake(def.entity.name),
9188
+ surface,
9189
+ pattern: "Integrated",
9190
+ provider,
9191
+ copyThroughFields,
9192
+ fkExternalKeys,
9193
+ repoImportSpecifier
9194
+ };
9195
+ }
9196
+ var TS_TYPE_FOR_SINK = {
9197
+ string: "string",
9198
+ integer: "number",
9199
+ decimal: "string",
9200
+ boolean: "boolean",
9201
+ uuid: "string",
9202
+ date: "Date",
9203
+ datetime: "Date",
9204
+ json: "unknown"
9205
+ };
9206
+ function tsTypeFor(type, nullable) {
9207
+ const base = TS_TYPE_FOR_SINK[type ?? "string"] ?? "unknown";
9208
+ return nullable ? `${base} | null` : base;
9209
+ }
9210
+ function snakeToCamel(s) {
9211
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
9212
+ }
9213
+ function pascalFromSnake(s) {
9214
+ const camel = snakeToCamel(s);
9215
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
9216
+ }
8634
9217
  function writeFile(outPath, content) {
8635
9218
  mkdirSync3(dirname2(outPath), { recursive: true });
8636
9219
  writeFileSync3(outPath, content);
@@ -9245,10 +9828,13 @@ var EntityNewCommand = class extends Command2 {
9245
9828
  definition: s.definition,
9246
9829
  filePath: s.filePath
9247
9830
  }));
9831
+ const assemblyTsAliases = resolveTsconfigAliases(ctx.cwd);
9248
9832
  const adapterResult = emitAdapters({
9249
9833
  providers: loadedProviders,
9250
9834
  entities: entityDefs,
9251
- outputRoot: adapterOutputRoot
9835
+ outputRoot: adapterOutputRoot,
9836
+ backendSrcAbs: path13.resolve(ctx.cwd, backendSrcForHandlers),
9837
+ aliases: assemblyTsAliases?.aliases ?? {}
9252
9838
  });
9253
9839
  if (!isJsonMode()) {
9254
9840
  if (adapterResult.written.length || adapterResult.scaffoldsWritten.length) {
@@ -9256,12 +9842,20 @@ var EntityNewCommand = class extends Command2 {
9256
9842
  `adapter codegen: ${adapterResult.scaffoldsWritten.length} scaffold(s) + ${adapterResult.written.length} @generated \u2192 ${adapterOutputRoot}`
9257
9843
  );
9258
9844
  }
9845
+ if (adapterResult.assembliesWritten.length || adapterResult.tokensWritten.length) {
9846
+ printInfo(
9847
+ `integration assembly codegen: ${adapterResult.assembliesWritten.length} module(s) + ${adapterResult.tokensWritten.length} tokens file(s) + ${adapterResult.integrationAggregatorsWritten.length} aggregator(s)`
9848
+ );
9849
+ }
9259
9850
  for (const s of adapterResult.scaffoldsSkipped) {
9260
9851
  printInfo(`skipped scaffold ${s} (author-owned)`);
9261
9852
  }
9262
9853
  for (const s of adapterResult.skippedSurfaces) {
9263
9854
  printWarning(`adapter codegen: ${s.reason} (provider ${s.provider})`);
9264
9855
  }
9856
+ for (const s of adapterResult.skippedAssemblies) {
9857
+ printWarning(`integration assembly: ${s.reason}`);
9858
+ }
9265
9859
  }
9266
9860
  }
9267
9861
  } catch (err) {
@@ -13114,10 +13708,10 @@ var ProjectInitCommand = class extends Command7 {
13114
13708
  };
13115
13709
  function askConfirm(question) {
13116
13710
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13117
- return new Promise((resolve6) => {
13711
+ return new Promise((resolve7) => {
13118
13712
  rl.question(`${question} [Y/n] `, (answer) => {
13119
13713
  rl.close();
13120
- resolve6(answer.trim().toLowerCase() !== "n");
13714
+ resolve7(answer.trim().toLowerCase() !== "n");
13121
13715
  });
13122
13716
  });
13123
13717
  }