@kosdev-code/kos-codegen-core 0.1.0-next.862 → 0.1.0-next.864
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/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +391 -23
- package/index.js.map +1 -1
- package/index.mjs +391 -23
- package/index.mjs.map +1 -1
- package/lib/generators/augment/model-mocks.d.ts +47 -0
- package/lib/generators/augment/model-mocks.d.ts.map +1 -0
- package/lib/generators/augment/ts-toolkit.d.ts +7 -0
- package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
- package/lib/generators/index.d.ts +2 -2
- package/lib/generators/index.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-service-request.d.ts +71 -0
- package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
- package/lib/generators/member-mutators/index.d.ts +1 -1
- package/lib/generators/member-mutators/index.d.ts.map +1 -1
- package/lib/generators/service-catalog.d.ts +12 -0
- package/lib/generators/service-catalog.d.ts.map +1 -1
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -1066,7 +1066,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1066
1066
|
}
|
|
1067
1067
|
literal.addPropertyAssignment({
|
|
1068
1068
|
name: spec.key,
|
|
1069
|
-
initializer: spec.initializer
|
|
1069
|
+
initializer: spec.initializer,
|
|
1070
|
+
leadingTrivia: spec.leadingTrivia
|
|
1070
1071
|
});
|
|
1071
1072
|
return { key: spec.key, created: true };
|
|
1072
1073
|
}
|
|
@@ -1084,7 +1085,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
|
|
|
1084
1085
|
}
|
|
1085
1086
|
literal.addPropertyAssignment({
|
|
1086
1087
|
name: spec.key,
|
|
1087
|
-
initializer: spec.initializer
|
|
1088
|
+
initializer: spec.initializer,
|
|
1089
|
+
leadingTrivia: spec.leadingTrivia
|
|
1088
1090
|
});
|
|
1089
1091
|
return { key: spec.key, created: true };
|
|
1090
1092
|
}
|
|
@@ -1093,7 +1095,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
|
|
|
1093
1095
|
sourceFile.addTypeAlias({
|
|
1094
1096
|
name: spec.name,
|
|
1095
1097
|
type: spec.type,
|
|
1096
|
-
isExported: true
|
|
1098
|
+
isExported: true,
|
|
1099
|
+
docs: spec.docs ? [spec.docs] : void 0
|
|
1097
1100
|
});
|
|
1098
1101
|
return true;
|
|
1099
1102
|
}
|
|
@@ -1123,6 +1126,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1123
1126
|
name: spec.name,
|
|
1124
1127
|
isAsync: spec.isAsync,
|
|
1125
1128
|
returnType: spec.returnType,
|
|
1129
|
+
docs: spec.docs ? [spec.docs] : void 0,
|
|
1126
1130
|
parameters: spec.parameters?.map((p) => {
|
|
1127
1131
|
return { name: p.name, type: p.type, hasQuestionToken: p.optional };
|
|
1128
1132
|
}),
|
|
@@ -1130,6 +1134,7 @@ function addDecoratedMethod(cls, spec) {
|
|
|
1130
1134
|
decorators: [
|
|
1131
1135
|
{
|
|
1132
1136
|
name: spec.decoratorName,
|
|
1137
|
+
typeArguments: spec.decoratorTypeArgs,
|
|
1133
1138
|
arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
|
|
1134
1139
|
}
|
|
1135
1140
|
]
|
|
@@ -1212,17 +1217,28 @@ function listServiceCatalog(codegenFs, query, projects) {
|
|
|
1212
1217
|
app,
|
|
1213
1218
|
version,
|
|
1214
1219
|
serviceModulePath: posix,
|
|
1215
|
-
operations: readOperations(codegenFs, openapiPath)
|
|
1220
|
+
operations: readOperations(codegenFs, openapiPath) ?? []
|
|
1216
1221
|
});
|
|
1217
1222
|
}
|
|
1218
1223
|
return entries.sort(
|
|
1219
1224
|
(a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
|
|
1220
1225
|
);
|
|
1221
1226
|
}
|
|
1227
|
+
function readServiceModuleOperations(codegenFs, serviceModuleFile) {
|
|
1228
|
+
const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
|
|
1229
|
+
const posix = moduleFile.split(path.sep).join("/");
|
|
1230
|
+
const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
|
|
1231
|
+
for (const candidate of [openapiPath, posix]) {
|
|
1232
|
+
if (!codegenFs.exists(candidate)) continue;
|
|
1233
|
+
const operations = readOperations(codegenFs, candidate);
|
|
1234
|
+
if (operations) return operations;
|
|
1235
|
+
}
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1222
1238
|
function readOperations(codegenFs, openapiPath) {
|
|
1223
1239
|
return readSourceFile(codegenFs, openapiPath, (sf) => {
|
|
1224
1240
|
const paths = sf.getInterface("paths");
|
|
1225
|
-
if (!paths) return
|
|
1241
|
+
if (!paths) return null;
|
|
1226
1242
|
const operations = [];
|
|
1227
1243
|
for (const pathProp of paths.getProperties()) {
|
|
1228
1244
|
const servicePath = unquote(pathProp.getName());
|
|
@@ -1744,7 +1760,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
|
|
|
1744
1760
|
`${modelBase}-services.ts`
|
|
1745
1761
|
);
|
|
1746
1762
|
}
|
|
1747
|
-
function header(modelBase) {
|
|
1763
|
+
function header$1(modelBase) {
|
|
1748
1764
|
return `/**
|
|
1749
1765
|
* Service layer for the ${modelBase} model: the endpoints it calls and the
|
|
1750
1766
|
* types derived from them. Standalone service functions for callers outside a
|
|
@@ -1755,7 +1771,7 @@ function header(modelBase) {
|
|
|
1755
1771
|
function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
|
|
1756
1772
|
const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
|
|
1757
1773
|
if (!codegenFs.exists(servicesFilePath)) {
|
|
1758
|
-
codegenFs.write(servicesFilePath, header(modelBase));
|
|
1774
|
+
codegenFs.write(servicesFilePath, header$1(modelBase));
|
|
1759
1775
|
}
|
|
1760
1776
|
const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
|
|
1761
1777
|
if (!codegenFs.exists(barrelPath)) {
|
|
@@ -2515,6 +2531,154 @@ function addComputedToModel(codegenFs, options, projects) {
|
|
|
2515
2531
|
});
|
|
2516
2532
|
return { modelFilePath };
|
|
2517
2533
|
}
|
|
2534
|
+
function mocksFilePathFor(modelFilePath, modelBase) {
|
|
2535
|
+
return path.join(
|
|
2536
|
+
path.dirname(modelFilePath),
|
|
2537
|
+
"mocks",
|
|
2538
|
+
`${modelBase}-mocks.ts`
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
function mockRegisterFunctionName(modelBase) {
|
|
2542
|
+
return `register${pascalCase(modelBase)}Mocks`;
|
|
2543
|
+
}
|
|
2544
|
+
function toMockRoutePattern(servicePath) {
|
|
2545
|
+
return servicePath.replace(/\{([^}]+)\}/g, ":$1");
|
|
2546
|
+
}
|
|
2547
|
+
function mockRouteCall(method, pattern, body) {
|
|
2548
|
+
const shorthand = {
|
|
2549
|
+
get: "get",
|
|
2550
|
+
post: "post",
|
|
2551
|
+
put: "put",
|
|
2552
|
+
delete: "del"
|
|
2553
|
+
};
|
|
2554
|
+
const fn = shorthand[method];
|
|
2555
|
+
return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
|
|
2556
|
+
pattern
|
|
2557
|
+
)}, ${body});`;
|
|
2558
|
+
}
|
|
2559
|
+
function header(modelBase, registerFn) {
|
|
2560
|
+
return `/**
|
|
2561
|
+
* KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
|
|
2562
|
+
* this project's generated OpenAPI types do not declare.
|
|
2563
|
+
*
|
|
2564
|
+
* NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
|
|
2565
|
+
* gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
|
|
2566
|
+
* dev-only entry module, or behind your own switch). Registering a route enables
|
|
2567
|
+
* KosMock, and a mock that shipped would shadow the real endpoint once it exists.
|
|
2568
|
+
*
|
|
2569
|
+
* Unmatched requests still pass through to the real device (KosMock's hybrid
|
|
2570
|
+
* default), so these routes shadow only the paths named below. Every mocked
|
|
2571
|
+
* response carries the \`kos-mocked: true\` wire header, so mock-fed data is
|
|
2572
|
+
* identifiable in devtools.
|
|
2573
|
+
*
|
|
2574
|
+
* DELETE THIS FILE once every endpoint below is in the generated types.
|
|
2575
|
+
*/
|
|
2576
|
+
`;
|
|
2577
|
+
}
|
|
2578
|
+
function ensureMockRoute(codegenFs, options) {
|
|
2579
|
+
const mocksFilePath = mocksFilePathFor(
|
|
2580
|
+
options.modelFilePath,
|
|
2581
|
+
options.modelBase
|
|
2582
|
+
);
|
|
2583
|
+
const registerFunction = mockRegisterFunctionName(options.modelBase);
|
|
2584
|
+
const routePattern = toMockRoutePattern(options.servicePath);
|
|
2585
|
+
const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
|
|
2586
|
+
const samplePlaceholder = !options.sampleText;
|
|
2587
|
+
if (!codegenFs.exists(mocksFilePath)) {
|
|
2588
|
+
codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
|
|
2589
|
+
}
|
|
2590
|
+
const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
|
|
2591
|
+
let routeCreated = false;
|
|
2592
|
+
transformSourceFile(codegenFs, mocksFilePath, (sf) => {
|
|
2593
|
+
ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
|
|
2594
|
+
ensureNamedImport(sf, "../services", [
|
|
2595
|
+
{ name: options.rawAlias, isTypeOnly: true }
|
|
2596
|
+
]);
|
|
2597
|
+
ensureSample(sf, {
|
|
2598
|
+
name: sampleName,
|
|
2599
|
+
type: options.rawAlias,
|
|
2600
|
+
servicePath: options.servicePath,
|
|
2601
|
+
method: options.method,
|
|
2602
|
+
sampleText: options.sampleText
|
|
2603
|
+
});
|
|
2604
|
+
const statement = mockRouteCall(
|
|
2605
|
+
options.method,
|
|
2606
|
+
routePattern,
|
|
2607
|
+
`{ data: ${sampleName} }`
|
|
2608
|
+
);
|
|
2609
|
+
routeCreated = ensureRegisteredRoute(sf, {
|
|
2610
|
+
registerFunction,
|
|
2611
|
+
modelBase: options.modelBase,
|
|
2612
|
+
statement,
|
|
2613
|
+
// Matching on the pattern literal alone would collide across methods.
|
|
2614
|
+
marker: `${options.method.toUpperCase()} ${routePattern}`,
|
|
2615
|
+
matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
return {
|
|
2619
|
+
mocksFilePath,
|
|
2620
|
+
registerFunction,
|
|
2621
|
+
routePattern,
|
|
2622
|
+
sampleName,
|
|
2623
|
+
routeCreated,
|
|
2624
|
+
samplePlaceholder
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
function shorthandOrMethod(method) {
|
|
2628
|
+
const shorthand = {
|
|
2629
|
+
get: "KosMock.get(",
|
|
2630
|
+
post: "KosMock.post(",
|
|
2631
|
+
put: "KosMock.put(",
|
|
2632
|
+
delete: "KosMock.del("
|
|
2633
|
+
};
|
|
2634
|
+
return shorthand[method] ?? `"${method.toUpperCase()}"`;
|
|
2635
|
+
}
|
|
2636
|
+
function ensureSample(sf, spec) {
|
|
2637
|
+
if (sf.getVariableDeclaration(spec.name)) return;
|
|
2638
|
+
const lines = [
|
|
2639
|
+
`Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
|
|
2640
|
+
"UNWRAPPED `data` payload, i.e. exactly what the transform receives."
|
|
2641
|
+
];
|
|
2642
|
+
if (!spec.sampleText) {
|
|
2643
|
+
lines.push(
|
|
2644
|
+
"",
|
|
2645
|
+
"TODO: replace the placeholder with something the backend would actually",
|
|
2646
|
+
"serve. Until then the route answers with nothing and the transform is",
|
|
2647
|
+
"never exercised."
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const docs = lines.join("\n");
|
|
2651
|
+
sf.addVariableStatement({
|
|
2652
|
+
isExported: true,
|
|
2653
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
2654
|
+
docs: [docs],
|
|
2655
|
+
declarations: [
|
|
2656
|
+
{
|
|
2657
|
+
name: spec.name,
|
|
2658
|
+
type: spec.type,
|
|
2659
|
+
initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
|
|
2660
|
+
}
|
|
2661
|
+
]
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
function ensureRegisteredRoute(sf, spec) {
|
|
2665
|
+
let fn = sf.getFunction(spec.registerFunction);
|
|
2666
|
+
if (!fn) {
|
|
2667
|
+
fn = sf.addFunction({
|
|
2668
|
+
name: spec.registerFunction,
|
|
2669
|
+
isExported: true,
|
|
2670
|
+
returnType: "void",
|
|
2671
|
+
docs: [
|
|
2672
|
+
`Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
|
|
2673
|
+
]
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
|
|
2677
|
+
if (already) return false;
|
|
2678
|
+
fn.addStatements(`// ${spec.marker}
|
|
2679
|
+
${spec.statement}`);
|
|
2680
|
+
return true;
|
|
2681
|
+
}
|
|
2518
2682
|
const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
|
|
2519
2683
|
function findServiceModules(codegenFs, sourceRoot) {
|
|
2520
2684
|
return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path.sep).join("/")));
|
|
@@ -2539,10 +2703,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
|
|
|
2539
2703
|
`${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
2540
2704
|
);
|
|
2541
2705
|
}
|
|
2706
|
+
function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
|
|
2707
|
+
const file = `${serviceModuleFile}.ts`;
|
|
2708
|
+
if (!codegenFs.exists(file)) return;
|
|
2709
|
+
const declared = readSourceFile(
|
|
2710
|
+
codegenFs,
|
|
2711
|
+
file,
|
|
2712
|
+
(sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
|
|
2713
|
+
);
|
|
2714
|
+
if (declared) return;
|
|
2715
|
+
throw new Error(
|
|
2716
|
+
`${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2542
2719
|
function sameEndpoint(existing, candidate) {
|
|
2543
2720
|
const args = (text) => {
|
|
2544
2721
|
const match = text.match(
|
|
2545
|
-
/^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
2722
|
+
/^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
|
|
2546
2723
|
);
|
|
2547
2724
|
return match ? [match[2], match[4].toLowerCase()] : null;
|
|
2548
2725
|
};
|
|
@@ -2550,6 +2727,104 @@ function sameEndpoint(existing, candidate) {
|
|
|
2550
2727
|
const b = args(candidate.trim());
|
|
2551
2728
|
return !!a && !!b && a[0] === b[0] && a[1] === b[1];
|
|
2552
2729
|
}
|
|
2730
|
+
function editDistance(a, b) {
|
|
2731
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2732
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2733
|
+
const current = [i];
|
|
2734
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2735
|
+
current[j] = Math.min(
|
|
2736
|
+
previous[j] + 1,
|
|
2737
|
+
current[j - 1] + 1,
|
|
2738
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
previous = current;
|
|
2742
|
+
}
|
|
2743
|
+
return previous[b.length];
|
|
2744
|
+
}
|
|
2745
|
+
const NEAREST_SHOWN = 5;
|
|
2746
|
+
function provisionalEntryComment(servicePath, method, catalogName) {
|
|
2747
|
+
return `/**
|
|
2748
|
+
* PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
|
|
2749
|
+
* generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
|
|
2750
|
+
* does not exist, so nothing about this endpoint is checked against a spec and
|
|
2751
|
+
* its response cannot be derived from one. Do not read this as a real endpoint.
|
|
2752
|
+
*
|
|
2753
|
+
* WHEN THE ENDPOINT LANDS:
|
|
2754
|
+
* 1. regenerate this project's API types (\`kosui api:generate\`)
|
|
2755
|
+
* 2. delete \` as ApiPath\` from the entry below
|
|
2756
|
+
* 3. point the endpoint's \`…Raw\` alias at
|
|
2757
|
+
* \`EndpointResponse<typeof ${catalogName}.…>\`
|
|
2758
|
+
* 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
|
|
2759
|
+
* 5. delete the model's \`mocks/\` module
|
|
2760
|
+
* 6. KEEP the transform — it is real code and survives all of the above
|
|
2761
|
+
*/
|
|
2762
|
+
`;
|
|
2763
|
+
}
|
|
2764
|
+
function provisionalMethodDocs(servicePath, method) {
|
|
2765
|
+
return [
|
|
2766
|
+
`PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
|
|
2767
|
+
"generated OpenAPI types, so this request is served by a KosMock route, not",
|
|
2768
|
+
"by the device. The feature it backs is NOT complete: report the missing",
|
|
2769
|
+
"endpoint rather than treating this as finished.",
|
|
2770
|
+
"",
|
|
2771
|
+
"See the endpoint's entry in `./services` for what to undo when it lands."
|
|
2772
|
+
].join("\n");
|
|
2773
|
+
}
|
|
2774
|
+
function rawTypeDocs(servicePath, method, rawType) {
|
|
2775
|
+
const shape = rawType ? [
|
|
2776
|
+
"Stated by hand and unverified: there is no spec entry to check it",
|
|
2777
|
+
"against."
|
|
2778
|
+
] : [
|
|
2779
|
+
"TODO: state the shape the backend is expected to serve. `unknown`",
|
|
2780
|
+
"compiles, but leaves the response boundary undescribed and the",
|
|
2781
|
+
"transform with nothing to narrow."
|
|
2782
|
+
];
|
|
2783
|
+
return [
|
|
2784
|
+
`Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
|
|
2785
|
+
"`data` payload, since the client strips the `{status, data}` envelope",
|
|
2786
|
+
"before the transform runs.",
|
|
2787
|
+
"",
|
|
2788
|
+
...shape,
|
|
2789
|
+
"",
|
|
2790
|
+
"Replace with `EndpointResponse<…>` once the endpoint is in the generated",
|
|
2791
|
+
"types."
|
|
2792
|
+
].join("\n");
|
|
2793
|
+
}
|
|
2794
|
+
function removalSteps(spec) {
|
|
2795
|
+
return [
|
|
2796
|
+
`Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
|
|
2797
|
+
`In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
|
|
2798
|
+
`In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
|
|
2799
|
+
`In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
|
|
2800
|
+
spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
|
|
2801
|
+
`KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
|
|
2802
|
+
];
|
|
2803
|
+
}
|
|
2804
|
+
function untypedEndpointError(spec) {
|
|
2805
|
+
const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
|
|
2806
|
+
const pathTypedForOtherMethods = methodsForPath.length > 0;
|
|
2807
|
+
const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
|
|
2808
|
+
(a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
|
|
2809
|
+
).slice(0, NEAREST_SHOWN);
|
|
2810
|
+
const waysForward = [
|
|
2811
|
+
`The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
|
|
2812
|
+
`The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
|
|
2813
|
+
`What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
|
|
2814
|
+
];
|
|
2815
|
+
const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
|
|
2816
|
+
const error = new Error(message);
|
|
2817
|
+
error.details = {
|
|
2818
|
+
servicePath: spec.servicePath,
|
|
2819
|
+
method: spec.method,
|
|
2820
|
+
pathTypedForOtherMethods,
|
|
2821
|
+
methodsForPath,
|
|
2822
|
+
otherEndpointsInThisApi: nearest,
|
|
2823
|
+
warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
|
|
2824
|
+
waysForward
|
|
2825
|
+
};
|
|
2826
|
+
return error;
|
|
2827
|
+
}
|
|
2553
2828
|
function addServiceRequestToModel(codegenFs, options, projects) {
|
|
2554
2829
|
const logger = getCodegenLogger();
|
|
2555
2830
|
const { modelFilePath, sourceRoot } = resolveModelFilePath(
|
|
@@ -2611,34 +2886,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2611
2886
|
const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
|
|
2612
2887
|
const lifecycle = options.lifecycle || "LOAD";
|
|
2613
2888
|
const catalogName = `${pascalCase(modelBase)}Endpoints`;
|
|
2889
|
+
const mockMode = options.mock ?? "auto";
|
|
2890
|
+
const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
|
|
2891
|
+
const pathValidated = operations !== null;
|
|
2892
|
+
const operationTyped = operations === null || operations.some(
|
|
2893
|
+
(op) => op.path === options.servicePath && op.method === method
|
|
2894
|
+
);
|
|
2895
|
+
if (!operationTyped && mockMode === "never") {
|
|
2896
|
+
throw untypedEndpointError({
|
|
2897
|
+
servicePath: options.servicePath,
|
|
2898
|
+
method,
|
|
2899
|
+
modelProject: options.modelProject,
|
|
2900
|
+
operations: operations ?? []
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const provisional = !operationTyped;
|
|
2904
|
+
const writeMock = provisional || mockMode === "always";
|
|
2905
|
+
if (provisional) {
|
|
2906
|
+
assertServiceModuleAcceptsProvisional(
|
|
2907
|
+
codegenFs,
|
|
2908
|
+
serviceModuleFile,
|
|
2909
|
+
options.modelProject
|
|
2910
|
+
);
|
|
2911
|
+
}
|
|
2614
2912
|
logger.info(
|
|
2615
|
-
`Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2913
|
+
`Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
|
|
2616
2914
|
);
|
|
2915
|
+
if (provisional) {
|
|
2916
|
+
logger.warn(
|
|
2917
|
+
`${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2617
2920
|
ensureServicesModule(codegenFs, modelFilePath, modelBase);
|
|
2618
2921
|
let endpointKey = options.methodName;
|
|
2619
2922
|
let endpointCreated = true;
|
|
2620
2923
|
let dataAlias = "";
|
|
2621
2924
|
let mapperName = "";
|
|
2622
2925
|
let ctxAlias = "";
|
|
2926
|
+
let rawAlias = "";
|
|
2623
2927
|
transformSourceFile(codegenFs, servicesFilePath, (sf) => {
|
|
2624
2928
|
ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
|
|
2929
|
+
if (provisional) {
|
|
2930
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
2931
|
+
{ name: "ApiPath", isTypeOnly: true }
|
|
2932
|
+
]);
|
|
2933
|
+
}
|
|
2934
|
+
const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
|
|
2625
2935
|
const entry = ensureConstCatalogEntry(sf, {
|
|
2626
2936
|
catalogName,
|
|
2627
2937
|
key: options.methodName,
|
|
2628
|
-
initializer: `endpoint(${JSON.stringify(
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
equals: sameEndpoint
|
|
2938
|
+
initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
|
|
2939
|
+
equals: sameEndpoint,
|
|
2940
|
+
leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
|
|
2632
2941
|
});
|
|
2633
2942
|
endpointKey = entry.key;
|
|
2634
2943
|
endpointCreated = entry.created;
|
|
2635
2944
|
dataAlias = `${pascalCase(endpointKey)}Data`;
|
|
2636
2945
|
mapperName = `to${pascalCase(endpointKey)}Data`;
|
|
2637
2946
|
ctxAlias = `${pascalCase(endpointKey)}Ctx`;
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2947
|
+
rawAlias = `${pascalCase(endpointKey)}Raw`;
|
|
2948
|
+
let rawType;
|
|
2949
|
+
if (provisional) {
|
|
2950
|
+
ensureExportedTypeAlias(sf, {
|
|
2951
|
+
name: rawAlias,
|
|
2952
|
+
type: options.rawType || "unknown",
|
|
2953
|
+
docs: rawTypeDocs(options.servicePath, method, options.rawType)
|
|
2954
|
+
});
|
|
2955
|
+
rawType = rawAlias;
|
|
2956
|
+
} else {
|
|
2957
|
+
rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
|
|
2958
|
+
ensureNamedImport(sf, serviceImportFromServices, [
|
|
2959
|
+
{ name: "EndpointResponse", isTypeOnly: true }
|
|
2960
|
+
]);
|
|
2961
|
+
if (writeMock) {
|
|
2962
|
+
ensureExportedTypeAlias(sf, {
|
|
2963
|
+
name: rawAlias,
|
|
2964
|
+
type: rawType,
|
|
2965
|
+
docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2642
2969
|
ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
|
|
2643
2970
|
ensureExportedMapper(sf, {
|
|
2644
2971
|
name: mapperName,
|
|
@@ -2659,20 +2986,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2659
2986
|
const className = getModelClass(sf).getName();
|
|
2660
2987
|
if (!className) throw new Error("Model class has no name.");
|
|
2661
2988
|
const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
|
|
2662
|
-
|
|
2989
|
+
const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
|
|
2990
|
+
const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
|
|
2991
|
+
ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
|
|
2663
2992
|
if (mode === "lifecycle") {
|
|
2664
2993
|
ensureNamedImport(sf, "./services", [
|
|
2665
2994
|
{ name: catalogName },
|
|
2666
2995
|
{ name: mapperName },
|
|
2667
|
-
{ name: dataAlias, isTypeOnly: true }
|
|
2996
|
+
{ name: dataAlias, isTypeOnly: true },
|
|
2997
|
+
...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
|
|
2668
2998
|
]);
|
|
2669
2999
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2670
3000
|
{ name: "DependencyLifecycle" }
|
|
2671
3001
|
]);
|
|
2672
3002
|
addDecoratedMethod(getModelClass(sf), {
|
|
2673
3003
|
name: options.methodName,
|
|
2674
|
-
decoratorName
|
|
3004
|
+
decoratorName,
|
|
3005
|
+
decoratorTypeArgs,
|
|
2675
3006
|
decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
|
|
3007
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2676
3008
|
// The manager invokes phase handlers error-first, passing (null, data)
|
|
2677
3009
|
// on success. The `error` half is only ever reached because
|
|
2678
3010
|
// `serviceRequest` supplies an errorHandler — the bare decorator
|
|
@@ -2689,7 +3021,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2689
3021
|
ensureNamedImport(sf, "./services", [
|
|
2690
3022
|
{ name: catalogName },
|
|
2691
3023
|
{ name: mapperName },
|
|
2692
|
-
{ name: ctxAlias, isTypeOnly: true }
|
|
3024
|
+
{ name: ctxAlias, isTypeOnly: true },
|
|
3025
|
+
...provisional ? [
|
|
3026
|
+
{ name: rawAlias, isTypeOnly: true },
|
|
3027
|
+
{ name: dataAlias, isTypeOnly: true }
|
|
3028
|
+
] : []
|
|
2693
3029
|
]);
|
|
2694
3030
|
ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
|
|
2695
3031
|
{ name: "executeServiceRequest" },
|
|
@@ -2704,8 +3040,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2704
3040
|
addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
|
|
2705
3041
|
addDecoratedMethod(getModelClass(sf), {
|
|
2706
3042
|
name: options.methodName,
|
|
2707
|
-
decoratorName
|
|
3043
|
+
decoratorName,
|
|
3044
|
+
decoratorTypeArgs,
|
|
2708
3045
|
decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
|
|
3046
|
+
docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
|
|
2709
3047
|
// Optional: the framework appends the context, callers never pass it.
|
|
2710
3048
|
parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
|
|
2711
3049
|
isAsync: true,
|
|
@@ -2717,13 +3055,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
|
|
|
2717
3055
|
].join("\n")
|
|
2718
3056
|
});
|
|
2719
3057
|
});
|
|
3058
|
+
const mock = writeMock ? ensureMockRoute(codegenFs, {
|
|
3059
|
+
modelFilePath,
|
|
3060
|
+
modelBase,
|
|
3061
|
+
sdkModuleSpecifier: readSourceFile(
|
|
3062
|
+
codegenFs,
|
|
3063
|
+
modelFilePath,
|
|
3064
|
+
resolveSdkModuleSpecifier
|
|
3065
|
+
),
|
|
3066
|
+
rawAlias,
|
|
3067
|
+
servicePath: options.servicePath,
|
|
3068
|
+
method,
|
|
3069
|
+
endpointKey,
|
|
3070
|
+
sampleText: options.sample
|
|
3071
|
+
}) : void 0;
|
|
2720
3072
|
return {
|
|
2721
3073
|
modelFilePath,
|
|
2722
3074
|
servicesFilePath,
|
|
2723
3075
|
serviceModule: serviceImportFromModel,
|
|
2724
3076
|
mode,
|
|
2725
3077
|
endpointKey,
|
|
2726
|
-
endpointCreated
|
|
3078
|
+
endpointCreated,
|
|
3079
|
+
pathValidated,
|
|
3080
|
+
provisional,
|
|
3081
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3082
|
+
mockRegisterFunction: mock?.registerFunction,
|
|
3083
|
+
mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
|
|
3084
|
+
mockSamplePlaceholder: mock?.samplePlaceholder,
|
|
3085
|
+
removalSteps: provisional ? removalSteps({
|
|
3086
|
+
modelProject: options.modelProject,
|
|
3087
|
+
servicePath: options.servicePath,
|
|
3088
|
+
method,
|
|
3089
|
+
catalogName,
|
|
3090
|
+
endpointKey,
|
|
3091
|
+
servicesFilePath,
|
|
3092
|
+
mocksFilePath: mock?.mocksFilePath,
|
|
3093
|
+
mapperName
|
|
3094
|
+
}) : void 0
|
|
2727
3095
|
};
|
|
2728
3096
|
}
|
|
2729
3097
|
function modelElementType(typeText) {
|