@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.
- package/CHANGELOG.md +66 -0
- package/README.md +44 -0
- package/dist/runtime/subsystems/index.d.ts +6 -2
- package/dist/runtime/subsystems/index.js +174 -1
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.d.ts +110 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.js +25 -2
- package/dist/runtime/subsystems/integration/detection-config.schema.js.map +1 -1
- package/dist/runtime/subsystems/integration/incremental-read.d.ts +248 -0
- package/dist/runtime/subsystems/integration/incremental-read.js +149 -0
- package/dist/runtime/subsystems/integration/incremental-read.js.map +1 -0
- package/dist/runtime/subsystems/integration/index.d.ts +2 -1
- package/dist/runtime/subsystems/integration/index.js +172 -2
- package/dist/runtime/subsystems/integration/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
- package/dist/src/cli/index.js +642 -35
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +78 -0
- package/dist/src/index.js +11 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/index.ts +35 -0
- package/runtime/subsystems/integration/detection-config.schema.ts +55 -0
- package/runtime/subsystems/integration/incremental-read.ts +379 -0
- package/runtime/subsystems/integration/index.ts +16 -0
package/dist/src/cli/index.js
CHANGED
|
@@ -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
|
|
8301
|
-
function
|
|
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
|
|
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 `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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
|
-
|
|
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((
|
|
13724
|
+
return new Promise((resolve7) => {
|
|
13118
13725
|
rl.question(`${question} [Y/n] `, (answer) => {
|
|
13119
13726
|
rl.close();
|
|
13120
|
-
|
|
13727
|
+
resolve7(answer.trim().toLowerCase() !== "n");
|
|
13121
13728
|
});
|
|
13122
13729
|
});
|
|
13123
13730
|
}
|