@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.
- 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 +171 -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 +221 -0
- package/dist/runtime/subsystems/integration/incremental-read.js +146 -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 +169 -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 +629 -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 +34 -0
- package/runtime/subsystems/integration/detection-config.schema.ts +55 -0
- package/runtime/subsystems/integration/incremental-read.ts +345 -0
- package/runtime/subsystems/integration/index.ts +15 -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,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
|
|
8301
|
-
function
|
|
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
|
|
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 `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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 `${
|
|
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
|
-
|
|
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((
|
|
13711
|
+
return new Promise((resolve7) => {
|
|
13118
13712
|
rl.question(`${question} [Y/n] `, (answer) => {
|
|
13119
13713
|
rl.close();
|
|
13120
|
-
|
|
13714
|
+
resolve7(answer.trim().toLowerCase() !== "n");
|
|
13121
13715
|
});
|
|
13122
13716
|
});
|
|
13123
13717
|
}
|