@kosdev-code/kos-codegen-core 0.1.0-next.854 → 0.1.0-next.860

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.js CHANGED
@@ -514,7 +514,6 @@ function generateInit(codegenFs, options) {
514
514
  modelDirectory: "lib",
515
515
  appDirectory: "app",
516
516
  components: true,
517
- dataServices: true,
518
517
  internal: false,
519
518
  singleton: false,
520
519
  unitTests: true
@@ -1053,6 +1052,93 @@ function ensureModuleConst(sourceFile, spec) {
1053
1052
  });
1054
1053
  return true;
1055
1054
  }
1055
+ function afterImports(sourceFile) {
1056
+ const imports = sourceFile.getImportDeclarations();
1057
+ return imports.length > 0 ? imports[imports.length - 1].getChildIndex() + 1 : 0;
1058
+ }
1059
+ function unquote$1(name) {
1060
+ return name.replace(/^["']|["']$/g, "");
1061
+ }
1062
+ function getConstCatalogLiteral(sourceFile, name) {
1063
+ const declaration = sourceFile.getVariableDeclaration(name);
1064
+ if (!declaration) return null;
1065
+ let initializer = declaration.getInitializer();
1066
+ if (tsMorph.Node.isAsExpression(initializer)) {
1067
+ initializer = initializer.getExpression();
1068
+ }
1069
+ if (!initializer || !tsMorph.Node.isObjectLiteralExpression(initializer)) {
1070
+ throw new Error(
1071
+ `"${name}" exists in ${sourceFile.getBaseName()} but is not an object literal.`
1072
+ );
1073
+ }
1074
+ return initializer;
1075
+ }
1076
+ function ensureConstCatalogEntry(sourceFile, spec) {
1077
+ const matches = spec.equals ?? ((existing, candidate) => existing === candidate);
1078
+ let literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
1079
+ if (!literal) {
1080
+ sourceFile.insertVariableStatement(afterImports(sourceFile), {
1081
+ isExported: true,
1082
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
1083
+ declarations: [{ name: spec.catalogName, initializer: "{} as const" }]
1084
+ });
1085
+ literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
1086
+ if (!literal) {
1087
+ throw new Error(`Failed to create catalog "${spec.catalogName}".`);
1088
+ }
1089
+ literal.addPropertyAssignment({
1090
+ name: spec.key,
1091
+ initializer: spec.initializer
1092
+ });
1093
+ return { key: spec.key, created: true };
1094
+ }
1095
+ for (const property of literal.getProperties().filter(tsMorph.Node.isPropertyAssignment)) {
1096
+ const existingKey = unquote$1(property.getName());
1097
+ const existingInitializer = property.getInitializerOrThrow().getText();
1098
+ if (matches(existingInitializer, spec.initializer)) {
1099
+ return { key: existingKey, created: false };
1100
+ }
1101
+ if (existingKey === spec.key) {
1102
+ throw new Error(
1103
+ `"${spec.catalogName}.${spec.key}" already exists with a different value (${existingInitializer}). Use a different methodName, or reconcile the catalog by hand.`
1104
+ );
1105
+ }
1106
+ }
1107
+ literal.addPropertyAssignment({
1108
+ name: spec.key,
1109
+ initializer: spec.initializer
1110
+ });
1111
+ return { key: spec.key, created: true };
1112
+ }
1113
+ function ensureExportedTypeAlias(sourceFile, spec) {
1114
+ if (sourceFile.getTypeAlias(spec.name)) return false;
1115
+ sourceFile.addTypeAlias({
1116
+ name: spec.name,
1117
+ type: spec.type,
1118
+ isExported: true
1119
+ });
1120
+ return true;
1121
+ }
1122
+ function ensureExportedMapper(sourceFile, spec) {
1123
+ if (sourceFile.getVariableDeclaration(spec.name)) return false;
1124
+ sourceFile.addVariableStatement({
1125
+ isExported: true,
1126
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
1127
+ declarations: [
1128
+ {
1129
+ name: spec.name,
1130
+ initializer: `(raw: ${spec.rawType}): ${spec.dataType} => raw`
1131
+ }
1132
+ ]
1133
+ });
1134
+ return true;
1135
+ }
1136
+ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1137
+ const present = sourceFile.getExportDeclarations().some((d) => d.getModuleSpecifierValue() === moduleSpecifier);
1138
+ if (present) return false;
1139
+ sourceFile.addExportDeclaration({ moduleSpecifier });
1140
+ return true;
1141
+ }
1056
1142
  function addDecoratedMethod(cls, spec) {
1057
1143
  if (cls.getMethod(spec.name)) return false;
1058
1144
  cls.addMethod({
@@ -1460,20 +1546,6 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1460
1546
  path__namespace.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1461
1547
  { ...normalized, internal }
1462
1548
  );
1463
- if (options.dataServices) {
1464
- logger.info(`Generating data services for ${modelNameDashCase}`);
1465
- generateFilesFromTemplates(
1466
- codegenFs,
1467
- path__namespace.join(templateDir, "services"),
1468
- path__namespace.join(
1469
- projectRoot,
1470
- options.modelDirectory,
1471
- modelNameDashCase,
1472
- "services"
1473
- ),
1474
- { ...normalized, internal }
1475
- );
1476
- }
1477
1549
  const modelIndex = path__namespace.join(projectRoot, "index.ts");
1478
1550
  const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1479
1551
  updateModelIndex(codegenFs, modelIndex, modelPath);
@@ -1574,19 +1646,6 @@ function generateModel(params) {
1574
1646
  path__namespace.join(projectRoot, options.modelDirectory, normalized.nameDashCase),
1575
1647
  { ...normalized, internal }
1576
1648
  );
1577
- if (normalized.dataServices) {
1578
- generateFilesFromTemplates(
1579
- codegenFs,
1580
- path__namespace.join(modelTemplateDir, "services"),
1581
- path__namespace.join(
1582
- projectRoot,
1583
- options.modelDirectory,
1584
- normalized.nameDashCase,
1585
- "services"
1586
- ),
1587
- { ...normalized, internal }
1588
- );
1589
- }
1590
1649
  const modelIndex = path__namespace.join(projectRoot, "index.ts");
1591
1650
  const modelPath = options.modelDirectory ? `${options.modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase;
1592
1651
  updateModelIndex(codegenFs, modelIndex, modelPath);
@@ -1617,8 +1676,7 @@ function generateModel(params) {
1617
1676
  ...options,
1618
1677
  name: `${normalized.name}-container`,
1619
1678
  modelName: normalized.name,
1620
- singleton: normalized.isContainerSingleton,
1621
- dataServices: normalized.dataServices
1679
+ singleton: normalized.isContainerSingleton
1622
1680
  },
1623
1681
  cwd,
1624
1682
  projects
@@ -1696,6 +1754,40 @@ Pass modelPath to pick one.`
1696
1754
  }
1697
1755
  return candidates[0];
1698
1756
  }
1757
+ function modelBaseName(modelFilePath, modelName) {
1758
+ const base = path__namespace.basename(modelFilePath);
1759
+ const match = base.match(/^(.*)-model\.ts$/);
1760
+ return match ? match[1] : modelName;
1761
+ }
1762
+ function servicesFilePathFor(modelFilePath, modelBase) {
1763
+ return path__namespace.join(
1764
+ path__namespace.dirname(modelFilePath),
1765
+ "services",
1766
+ `${modelBase}-services.ts`
1767
+ );
1768
+ }
1769
+ function header(modelBase) {
1770
+ return `/**
1771
+ * Service layer for the ${modelBase} model: the endpoints it calls and the
1772
+ * types derived from them. Standalone service functions for callers outside a
1773
+ * model context belong here too.
1774
+ */
1775
+ `;
1776
+ }
1777
+ function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1778
+ const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1779
+ if (!codegenFs.exists(servicesFilePath)) {
1780
+ codegenFs.write(servicesFilePath, header(modelBase));
1781
+ }
1782
+ const barrelPath = path__namespace.join(path__namespace.dirname(servicesFilePath), "index.ts");
1783
+ if (!codegenFs.exists(barrelPath)) {
1784
+ codegenFs.write(barrelPath, "");
1785
+ }
1786
+ transformSourceFile(codegenFs, barrelPath, (sf) => {
1787
+ ensureBarrelExport(sf, `./${modelBase}-services`);
1788
+ });
1789
+ return servicesFilePath;
1790
+ }
1699
1791
  function normalizeAddFutureOptions(codegenFs, options, projects) {
1700
1792
  const projectConfiguration = findProjectByName(
1701
1793
  codegenFs.root,
@@ -1730,8 +1822,7 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
1730
1822
  projects
1731
1823
  );
1732
1824
  const modelDirectory = path__namespace.dirname(modelFilePath);
1733
- const servicesDirectory = path__namespace.join(modelDirectory, "services");
1734
- const servicesFilePath = codegenFs.exists(servicesDirectory) ? path__namespace.join(servicesDirectory, `${nameDashCase}-services.ts`) : void 0;
1825
+ const servicesFilePath = servicesFilePathFor(modelFilePath, nameDashCase);
1735
1826
  const registrationFilePath = path__namespace.join(
1736
1827
  modelDirectory,
1737
1828
  `${nameDashCase}-registration.ts`
@@ -1941,6 +2032,7 @@ class ModelFileTransformer {
1941
2032
  });
1942
2033
  }
1943
2034
  }
2035
+ const SDK_MODULE = "@kosdev-code/kos-ui-sdk";
1944
2036
  class ServiceFileTransformer {
1945
2037
  constructor(codegenFs, options) {
1946
2038
  this.codegenFs = codegenFs;
@@ -1949,162 +2041,69 @@ class ServiceFileTransformer {
1949
2041
  codegenFs;
1950
2042
  options;
1951
2043
  transform() {
1952
- const { servicesFilePath } = this.options;
1953
- if (!servicesFilePath || !this.codegenFs.exists(servicesFilePath)) {
1954
- this.createServicesFile();
1955
- return;
1956
- }
1957
- let content = this.codegenFs.read(servicesFilePath);
1958
- content = this.addFutureImports(content);
1959
- content = this.addFutureService(content);
1960
- content = this.addProgressTypes(content);
1961
- this.codegenFs.write(servicesFilePath, content);
1962
- }
1963
- createServicesFile() {
1964
- const { servicesFilePath, nameProperCase, nameDashCase, nameLowerCase } = this.options;
1965
- if (!servicesFilePath) {
1966
- return;
1967
- }
1968
- const content = `import {
1969
- KosLog,
1970
- type ClientResponse,
1971
- type DeepRequired,
1972
- type ElementType,
1973
- type ServiceResponse,
1974
- type FutureResponse
1975
- } from '@kosdev-code/kos-ui-sdk';
1976
-
1977
- import API, { type KosApi, type ApiPath } from '../../../utils/service';
1978
-
1979
- const log = KosLog.createLogger({name: "${nameDashCase}-service", group: "Services"});
1980
-
1981
- const SERVICE_PATH: ApiPath = "ENTER_SERVICE_PATH"
1982
- export type ${nameProperCase}ClientResponse = ClientResponse<
1983
- KosApi,
1984
- typeof SERVICE_PATH,
1985
- 'get'
1986
- >;
1987
- export type ${nameProperCase}Response = DeepRequired<${nameProperCase}ClientResponse>;
1988
-
1989
- /**
1990
- * @category Service
1991
- * Retrieves the initial ${nameLowerCase} data.
1992
- */
1993
- export const get${nameProperCase} = async (): Promise<ServiceResponse<${nameProperCase}Response>> => {
1994
- log.debug('sending GET for ${nameLowerCase}');
1995
- return await API.get(SERVICE_PATH);
1996
- };
1997
-
1998
- /**
1999
- * @category Service - Future Operation
2000
- * Placeholder for a long-running operation that returns a Future for progress tracking
2001
- *
2002
- * Replace this with your actual long-running service operation
2003
- */
2004
- export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
2005
- // TODO: Implement your long-running service operation here
2006
- // This should return a Future that can be tracked for progress
2007
-
2008
- log.debug('starting long-running ${nameLowerCase} operation');
2009
-
2010
- // Example pattern:
2011
- // return API.post(OPERATION_SERVICE_PATH, {
2012
- // // operation parameters
2013
- // });
2014
-
2015
- // Placeholder - replace with actual implementation
2016
- throw new Error('perform${nameProperCase}Operation not yet implemented');
2017
- };
2018
-
2019
- // Additional Future-aware service types (add as needed)
2020
- export type ${nameProperCase}OperationProgress = {
2021
- // Define your progress data structure here
2022
- stage: string;
2023
- percentComplete: number;
2024
- currentItem?: string;
2025
- totalItems?: number;
2026
- };
2027
-
2028
- export type ${nameProperCase}OperationResult = {
2029
- // Define your operation result structure here
2030
- success: boolean;
2031
- message?: string;
2032
- data?: any;
2033
- };
2034
- `;
2035
- this.codegenFs.write(servicesFilePath, content);
2036
- }
2037
- addFutureImports(content) {
2038
- if (content.includes("FutureResponse")) {
2039
- return content;
2040
- }
2041
- const importRegex = /import {\s*([^}]*)\s*} from '@kosdev-code\/kos-ui-sdk';/;
2042
- const importMatch = content.match(importRegex);
2043
- if (importMatch) {
2044
- const existingImports = importMatch[1];
2045
- if (existingImports.includes("FutureResponse")) {
2046
- return content;
2047
- }
2048
- const cleanedImports = existingImports.trim().replace(/,\s*$/, "");
2049
- const newImports = cleanedImports ? `${cleanedImports},
2050
- type FutureResponse` : `type FutureResponse`;
2051
- const newImportStatement = `import {
2052
- ${newImports}
2053
- } from '@kosdev-code/kos-ui-sdk';`;
2054
- return content.replace(importMatch[0], newImportStatement);
2055
- }
2056
- return content;
2044
+ const { modelFilePath, nameDashCase } = this.options;
2045
+ if (!modelFilePath) return;
2046
+ const servicesFilePath = ensureServicesModule(
2047
+ this.codegenFs,
2048
+ modelFilePath,
2049
+ nameDashCase
2050
+ );
2051
+ transformSourceFile(this.codegenFs, servicesFilePath, (sf) => {
2052
+ ensureNamedImport(sf, SDK_MODULE, [
2053
+ { name: "KosLog" },
2054
+ { name: "FutureResponse", isTypeOnly: true }
2055
+ ]);
2056
+ ensureModuleConst(sf, {
2057
+ name: "log",
2058
+ initializer: `KosLog.createLogger({ name: "${nameDashCase}-service", group: "Services" })`
2059
+ });
2060
+ this.addFutureOperation(sf);
2061
+ this.addProgressTypes(sf);
2062
+ });
2057
2063
  }
2058
- addFutureService(content) {
2064
+ addFutureOperation(sf) {
2059
2065
  const { nameProperCase, nameLowerCase } = this.options;
2060
- if (content.includes(`perform${nameProperCase}Operation`)) {
2061
- return content;
2062
- }
2063
- const futureService = `
2064
- /**
2065
- * @category Service - Future Operation
2066
- * Placeholder for a long-running operation that returns a Future for progress tracking
2067
- *
2068
- * Replace this with your actual long-running service operation
2069
- */
2070
- export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
2071
- // TODO: Implement your long-running service operation here
2072
- // This should return a Future that can be tracked for progress
2073
-
2074
- log.debug('starting long-running ${nameLowerCase} operation');
2075
-
2076
- // Example pattern:
2077
- // return API.post(OPERATION_SERVICE_PATH, {
2078
- // // operation parameters
2079
- // });
2080
-
2081
- // Placeholder - replace with actual implementation
2082
- throw new Error('perform${nameProperCase}Operation not yet implemented');
2083
- };`;
2084
- return content + "\n" + futureService;
2066
+ const name = `perform${nameProperCase}Operation`;
2067
+ if (sf.getVariableDeclaration(name)) return;
2068
+ sf.addVariableStatement({
2069
+ isExported: true,
2070
+ declarationKind: tsMorph.VariableDeclarationKind.Const,
2071
+ declarations: [
2072
+ {
2073
+ name,
2074
+ initializer: `async (): Promise<FutureResponse> => {
2075
+ log.debug("starting long-running ${nameLowerCase} operation");
2076
+ throw new Error("${name} not yet implemented");
2077
+ }`
2078
+ }
2079
+ ],
2080
+ docs: [
2081
+ {
2082
+ description: `@category Service - Future Operation
2083
+ A long-running operation returning a Future for progress tracking. Replace the body with the real service call.`
2084
+ }
2085
+ ]
2086
+ });
2085
2087
  }
2086
- addProgressTypes(content) {
2088
+ addProgressTypes(sf) {
2087
2089
  const { nameProperCase } = this.options;
2088
- if (content.includes(`${nameProperCase}OperationProgress`)) {
2089
- return content;
2090
- }
2091
- const progressTypes = `
2092
- // Additional Future-aware service types (add as needed)
2093
- export type ${nameProperCase}OperationProgress = {
2094
- // Define your progress data structure here
2090
+ ensureExportedTypeAlias(sf, {
2091
+ name: `${nameProperCase}OperationProgress`,
2092
+ type: `{
2095
2093
  stage: string;
2096
2094
  percentComplete: number;
2097
2095
  currentItem?: string;
2098
2096
  totalItems?: number;
2099
- };
2100
-
2101
- export type ${nameProperCase}OperationResult = {
2102
- // Define your operation result structure here
2097
+ }`
2098
+ });
2099
+ ensureExportedTypeAlias(sf, {
2100
+ name: `${nameProperCase}OperationResult`,
2101
+ type: `{
2103
2102
  success: boolean;
2104
2103
  message?: string;
2105
- data?: any;
2106
- };`;
2107
- return content + "\n" + progressTypes;
2104
+ data?: unknown;
2105
+ }`
2106
+ });
2108
2107
  }
2109
2108
  }
2110
2109
  class RegistrationFileTransformer {
@@ -2547,6 +2546,32 @@ function toImportSpecifier(fromFile, toFileNoExt) {
2547
2546
  if (!rel.startsWith(".")) rel = `./${rel}`;
2548
2547
  return rel;
2549
2548
  }
2549
+ function fromImportSpecifier(fromFile, specifier) {
2550
+ return path__namespace.join(path__namespace.dirname(fromFile), specifier);
2551
+ }
2552
+ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, modelProject) {
2553
+ const file = `${serviceModuleFile}.ts`;
2554
+ if (!codegenFs.exists(file)) return;
2555
+ const typeParams = readSourceFile(codegenFs, file, (sf) => {
2556
+ const alias = sf.getTypeAlias("EndpointCtx");
2557
+ return alias ? alias.getTypeParameters().length : null;
2558
+ });
2559
+ if (typeParams === null || typeParams >= 2) return;
2560
+ throw new Error(
2561
+ `${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.`
2562
+ );
2563
+ }
2564
+ function sameEndpoint(existing, candidate) {
2565
+ const args = (text) => {
2566
+ const match = text.match(
2567
+ /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2568
+ );
2569
+ return match ? [match[2], match[4].toLowerCase()] : null;
2570
+ };
2571
+ const a = args(existing.trim());
2572
+ const b = args(candidate.trim());
2573
+ return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2574
+ }
2550
2575
  function addServiceRequestToModel(codegenFs, options, projects) {
2551
2576
  const logger = getCodegenLogger();
2552
2577
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2557,14 +2582,15 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2557
2582
  if (!codegenFs.exists(modelFilePath)) {
2558
2583
  throw new Error(`Model file not found: ${modelFilePath}`);
2559
2584
  }
2560
- let serviceImport = options.serviceModule;
2561
- if (!serviceImport && options.serviceModulePath) {
2562
- serviceImport = toImportSpecifier(
2585
+ let serviceModuleFile;
2586
+ if (options.serviceModulePath) {
2587
+ serviceModuleFile = options.serviceModulePath.replace(/\.ts$/, "");
2588
+ } else if (options.serviceModule) {
2589
+ serviceModuleFile = fromImportSpecifier(
2563
2590
  modelFilePath,
2564
- options.serviceModulePath.replace(/\.ts$/, "")
2591
+ options.serviceModule
2565
2592
  );
2566
- }
2567
- if (!serviceImport) {
2593
+ } else {
2568
2594
  if (!sourceRoot) {
2569
2595
  throw new Error(
2570
2596
  "Cannot auto-resolve the service module without a project source root. Pass serviceModule."
@@ -2576,42 +2602,91 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2576
2602
  "No generated service module found. Run `kosui api:generate` for this project first."
2577
2603
  );
2578
2604
  }
2579
- const specifiers = modules.map(
2580
- (m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
2581
- );
2582
2605
  if (modules.length > 1) {
2606
+ const specifiers = modules.map(
2607
+ (m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
2608
+ );
2583
2609
  throw new Error(
2584
2610
  `Multiple service modules found — pass serviceModule (one of): ${specifiers.join(
2585
2611
  ", "
2586
2612
  )}`
2587
2613
  );
2588
2614
  }
2589
- serviceImport = specifiers[0];
2615
+ serviceModuleFile = modules[0].replace(/\.ts$/, "");
2590
2616
  }
2591
- const method = options.method || "get";
2617
+ assertServiceModuleAcceptsTransform(
2618
+ codegenFs,
2619
+ serviceModuleFile,
2620
+ options.modelProject
2621
+ );
2622
+ const modelBase = modelBaseName(modelFilePath, options.modelName);
2623
+ const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
2624
+ const serviceImportFromModel = toImportSpecifier(
2625
+ modelFilePath,
2626
+ serviceModuleFile
2627
+ );
2628
+ const serviceImportFromServices = toImportSpecifier(
2629
+ servicesFilePath,
2630
+ serviceModuleFile
2631
+ );
2632
+ const method = (options.method || "get").toLowerCase();
2592
2633
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2593
2634
  const lifecycle = options.lifecycle || "LOAD";
2635
+ const catalogName = `${pascalCase(modelBase)}Endpoints`;
2594
2636
  logger.info(
2595
2637
  `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2596
2638
  );
2639
+ ensureServicesModule(codegenFs, modelFilePath, modelBase);
2640
+ let endpointKey = options.methodName;
2641
+ let endpointCreated = true;
2642
+ let dataAlias = "";
2643
+ let mapperName = "";
2644
+ let ctxAlias = "";
2645
+ transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2646
+ ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
2647
+ const entry = ensureConstCatalogEntry(sf, {
2648
+ catalogName,
2649
+ key: options.methodName,
2650
+ initializer: `endpoint(${JSON.stringify(
2651
+ options.servicePath
2652
+ )}, ${JSON.stringify(method)})`,
2653
+ equals: sameEndpoint
2654
+ });
2655
+ endpointKey = entry.key;
2656
+ endpointCreated = entry.created;
2657
+ dataAlias = `${pascalCase(endpointKey)}Data`;
2658
+ mapperName = `to${pascalCase(endpointKey)}Data`;
2659
+ ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2660
+ const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2661
+ ensureNamedImport(sf, serviceImportFromServices, [
2662
+ { name: "EndpointResponse", isTypeOnly: true }
2663
+ ]);
2664
+ ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2665
+ ensureExportedMapper(sf, {
2666
+ name: mapperName,
2667
+ rawType,
2668
+ dataType: dataAlias
2669
+ });
2670
+ if (mode === "method") {
2671
+ ensureNamedImport(sf, serviceImportFromServices, [
2672
+ { name: "EndpointCtx", isTypeOnly: true }
2673
+ ]);
2674
+ ensureExportedTypeAlias(sf, {
2675
+ name: ctxAlias,
2676
+ type: `EndpointCtx<typeof ${catalogName}.${endpointKey}, ${dataAlias}>`
2677
+ });
2678
+ }
2679
+ });
2597
2680
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2598
- const endpointConst = endpointConstName(options.methodName);
2599
2681
  const className = getModelClass(sf).getName();
2600
2682
  if (!className) throw new Error("Model class has no name.");
2601
2683
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2602
- ensureNamedImport(sf, serviceImport, [
2603
- { name: "endpoint" },
2604
- { name: "serviceRequest" }
2605
- ]);
2606
- ensureModuleConst(sf, {
2607
- name: endpointConst,
2608
- initializer: `endpoint(${JSON.stringify(
2609
- options.servicePath
2610
- )}, ${JSON.stringify(method)})`
2611
- });
2684
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
2612
2685
  if (mode === "lifecycle") {
2613
- ensureNamedImport(sf, serviceImport, [
2614
- { name: "EndpointResponse", isTypeOnly: true }
2686
+ ensureNamedImport(sf, "./services", [
2687
+ { name: catalogName },
2688
+ { name: mapperName },
2689
+ { name: dataAlias, isTypeOnly: true }
2615
2690
  ]);
2616
2691
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2617
2692
  { name: "DependencyLifecycle" }
@@ -2619,25 +2694,24 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2619
2694
  addDecoratedMethod(getModelClass(sf), {
2620
2695
  name: options.methodName,
2621
2696
  decoratorName: "serviceRequest",
2622
- decoratorArgsText: `${endpointConst}, { lifecycle: DependencyLifecycle.${lifecycle} }`,
2697
+ decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
2623
2698
  // The manager invokes phase handlers error-first, passing (null, data)
2624
2699
  // on success. The `error` half is only ever reached because
2625
2700
  // `serviceRequest` supplies an errorHandler — the bare decorator
2626
2701
  // defaults to `throw`, which fails the phase instead of calling back.
2627
2702
  parameters: [
2628
2703
  { name: "error", type: "string | null" },
2629
- {
2630
- name: "data",
2631
- type: `EndpointResponse<typeof ${endpointConst}>`
2632
- }
2704
+ { name: "data", type: dataAlias }
2633
2705
  ],
2634
2706
  returnType: "void",
2635
2707
  statements: "// TODO: apply `data` to the model"
2636
2708
  });
2637
2709
  return;
2638
2710
  }
2639
- ensureNamedImport(sf, serviceImport, [
2640
- { name: "EndpointCtx", isTypeOnly: true }
2711
+ ensureNamedImport(sf, "./services", [
2712
+ { name: catalogName },
2713
+ { name: mapperName },
2714
+ { name: ctxAlias, isTypeOnly: true }
2641
2715
  ]);
2642
2716
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2643
2717
  { name: "executeServiceRequest" },
@@ -2653,15 +2727,9 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2653
2727
  addDecoratedMethod(getModelClass(sf), {
2654
2728
  name: options.methodName,
2655
2729
  decoratorName: "serviceRequest",
2656
- decoratorArgsText: endpointConst,
2730
+ decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
2657
2731
  // Optional: the framework appends the context, callers never pass it.
2658
- parameters: [
2659
- {
2660
- name: "$ctx",
2661
- type: `EndpointCtx<typeof ${endpointConst}>`,
2662
- optional: true
2663
- }
2664
- ],
2732
+ parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2665
2733
  isAsync: true,
2666
2734
  returnType: "Promise<void>",
2667
2735
  statements: [
@@ -2671,11 +2739,14 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2671
2739
  ].join("\n")
2672
2740
  });
2673
2741
  });
2674
- return { modelFilePath, serviceModule: serviceImport, mode };
2675
- }
2676
- function endpointConstName(methodName) {
2677
- const snake = methodName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2678
- return `ENDPOINT_${snake}`;
2742
+ return {
2743
+ modelFilePath,
2744
+ servicesFilePath,
2745
+ serviceModule: serviceImportFromModel,
2746
+ mode,
2747
+ endpointKey,
2748
+ endpointCreated
2749
+ };
2679
2750
  }
2680
2751
  function modelElementType(typeText) {
2681
2752
  let m;