@kosdev-code/kos-codegen-core 0.1.0-next.855 → 0.1.0-next.861

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.mjs CHANGED
@@ -492,7 +492,6 @@ function generateInit(codegenFs, options) {
492
492
  modelDirectory: "lib",
493
493
  appDirectory: "app",
494
494
  components: true,
495
- dataServices: true,
496
495
  internal: false,
497
496
  singleton: false,
498
497
  unitTests: true
@@ -1031,6 +1030,93 @@ function ensureModuleConst(sourceFile, spec) {
1031
1030
  });
1032
1031
  return true;
1033
1032
  }
1033
+ function afterImports(sourceFile) {
1034
+ const imports = sourceFile.getImportDeclarations();
1035
+ return imports.length > 0 ? imports[imports.length - 1].getChildIndex() + 1 : 0;
1036
+ }
1037
+ function unquote$1(name) {
1038
+ return name.replace(/^["']|["']$/g, "");
1039
+ }
1040
+ function getConstCatalogLiteral(sourceFile, name) {
1041
+ const declaration = sourceFile.getVariableDeclaration(name);
1042
+ if (!declaration) return null;
1043
+ let initializer = declaration.getInitializer();
1044
+ if (Node.isAsExpression(initializer)) {
1045
+ initializer = initializer.getExpression();
1046
+ }
1047
+ if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
1048
+ throw new Error(
1049
+ `"${name}" exists in ${sourceFile.getBaseName()} but is not an object literal.`
1050
+ );
1051
+ }
1052
+ return initializer;
1053
+ }
1054
+ function ensureConstCatalogEntry(sourceFile, spec) {
1055
+ const matches = spec.equals ?? ((existing, candidate) => existing === candidate);
1056
+ let literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
1057
+ if (!literal) {
1058
+ sourceFile.insertVariableStatement(afterImports(sourceFile), {
1059
+ isExported: true,
1060
+ declarationKind: VariableDeclarationKind.Const,
1061
+ declarations: [{ name: spec.catalogName, initializer: "{} as const" }]
1062
+ });
1063
+ literal = getConstCatalogLiteral(sourceFile, spec.catalogName);
1064
+ if (!literal) {
1065
+ throw new Error(`Failed to create catalog "${spec.catalogName}".`);
1066
+ }
1067
+ literal.addPropertyAssignment({
1068
+ name: spec.key,
1069
+ initializer: spec.initializer
1070
+ });
1071
+ return { key: spec.key, created: true };
1072
+ }
1073
+ for (const property of literal.getProperties().filter(Node.isPropertyAssignment)) {
1074
+ const existingKey = unquote$1(property.getName());
1075
+ const existingInitializer = property.getInitializerOrThrow().getText();
1076
+ if (matches(existingInitializer, spec.initializer)) {
1077
+ return { key: existingKey, created: false };
1078
+ }
1079
+ if (existingKey === spec.key) {
1080
+ throw new Error(
1081
+ `"${spec.catalogName}.${spec.key}" already exists with a different value (${existingInitializer}). Use a different methodName, or reconcile the catalog by hand.`
1082
+ );
1083
+ }
1084
+ }
1085
+ literal.addPropertyAssignment({
1086
+ name: spec.key,
1087
+ initializer: spec.initializer
1088
+ });
1089
+ return { key: spec.key, created: true };
1090
+ }
1091
+ function ensureExportedTypeAlias(sourceFile, spec) {
1092
+ if (sourceFile.getTypeAlias(spec.name)) return false;
1093
+ sourceFile.addTypeAlias({
1094
+ name: spec.name,
1095
+ type: spec.type,
1096
+ isExported: true
1097
+ });
1098
+ return true;
1099
+ }
1100
+ function ensureExportedMapper(sourceFile, spec) {
1101
+ if (sourceFile.getVariableDeclaration(spec.name)) return false;
1102
+ sourceFile.addVariableStatement({
1103
+ isExported: true,
1104
+ declarationKind: VariableDeclarationKind.Const,
1105
+ declarations: [
1106
+ {
1107
+ name: spec.name,
1108
+ initializer: `(raw: ${spec.rawType}): ${spec.dataType} => raw`
1109
+ }
1110
+ ]
1111
+ });
1112
+ return true;
1113
+ }
1114
+ function ensureBarrelExport(sourceFile, moduleSpecifier) {
1115
+ const present = sourceFile.getExportDeclarations().some((d) => d.getModuleSpecifierValue() === moduleSpecifier);
1116
+ if (present) return false;
1117
+ sourceFile.addExportDeclaration({ moduleSpecifier });
1118
+ return true;
1119
+ }
1034
1120
  function addDecoratedMethod(cls, spec) {
1035
1121
  if (cls.getMethod(spec.name)) return false;
1036
1122
  cls.addMethod({
@@ -1438,20 +1524,6 @@ function generateContainerModel(codegenFs, templateDir, options, cwd, projects)
1438
1524
  path.join(projectRoot, options.modelDirectory || "", modelNameDashCase),
1439
1525
  { ...normalized, internal }
1440
1526
  );
1441
- if (options.dataServices) {
1442
- logger.info(`Generating data services for ${modelNameDashCase}`);
1443
- generateFilesFromTemplates(
1444
- codegenFs,
1445
- path.join(templateDir, "services"),
1446
- path.join(
1447
- projectRoot,
1448
- options.modelDirectory,
1449
- modelNameDashCase,
1450
- "services"
1451
- ),
1452
- { ...normalized, internal }
1453
- );
1454
- }
1455
1527
  const modelIndex = path.join(projectRoot, "index.ts");
1456
1528
  const modelPath = normalized.modelDirectory ? `${normalized.modelDirectory}/${modelNameDashCase}` : modelNameDashCase;
1457
1529
  updateModelIndex(codegenFs, modelIndex, modelPath);
@@ -1552,19 +1624,6 @@ function generateModel(params) {
1552
1624
  path.join(projectRoot, options.modelDirectory, normalized.nameDashCase),
1553
1625
  { ...normalized, internal }
1554
1626
  );
1555
- if (normalized.dataServices) {
1556
- generateFilesFromTemplates(
1557
- codegenFs,
1558
- path.join(modelTemplateDir, "services"),
1559
- path.join(
1560
- projectRoot,
1561
- options.modelDirectory,
1562
- normalized.nameDashCase,
1563
- "services"
1564
- ),
1565
- { ...normalized, internal }
1566
- );
1567
- }
1568
1627
  const modelIndex = path.join(projectRoot, "index.ts");
1569
1628
  const modelPath = options.modelDirectory ? `${options.modelDirectory}/${normalized.nameDashCase}` : normalized.nameDashCase;
1570
1629
  updateModelIndex(codegenFs, modelIndex, modelPath);
@@ -1595,8 +1654,7 @@ function generateModel(params) {
1595
1654
  ...options,
1596
1655
  name: `${normalized.name}-container`,
1597
1656
  modelName: normalized.name,
1598
- singleton: normalized.isContainerSingleton,
1599
- dataServices: normalized.dataServices
1657
+ singleton: normalized.isContainerSingleton
1600
1658
  },
1601
1659
  cwd,
1602
1660
  projects
@@ -1674,6 +1732,40 @@ Pass modelPath to pick one.`
1674
1732
  }
1675
1733
  return candidates[0];
1676
1734
  }
1735
+ function modelBaseName(modelFilePath, modelName) {
1736
+ const base = path.basename(modelFilePath);
1737
+ const match = base.match(/^(.*)-model\.ts$/);
1738
+ return match ? match[1] : modelName;
1739
+ }
1740
+ function servicesFilePathFor(modelFilePath, modelBase) {
1741
+ return path.join(
1742
+ path.dirname(modelFilePath),
1743
+ "services",
1744
+ `${modelBase}-services.ts`
1745
+ );
1746
+ }
1747
+ function header(modelBase) {
1748
+ return `/**
1749
+ * Service layer for the ${modelBase} model: the endpoints it calls and the
1750
+ * types derived from them. Standalone service functions for callers outside a
1751
+ * model context belong here too.
1752
+ */
1753
+ `;
1754
+ }
1755
+ function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1756
+ const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1757
+ if (!codegenFs.exists(servicesFilePath)) {
1758
+ codegenFs.write(servicesFilePath, header(modelBase));
1759
+ }
1760
+ const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
1761
+ if (!codegenFs.exists(barrelPath)) {
1762
+ codegenFs.write(barrelPath, "");
1763
+ }
1764
+ transformSourceFile(codegenFs, barrelPath, (sf) => {
1765
+ ensureBarrelExport(sf, `./${modelBase}-services`);
1766
+ });
1767
+ return servicesFilePath;
1768
+ }
1677
1769
  function normalizeAddFutureOptions(codegenFs, options, projects) {
1678
1770
  const projectConfiguration = findProjectByName(
1679
1771
  codegenFs.root,
@@ -1708,8 +1800,7 @@ function normalizeAddFutureOptions(codegenFs, options, projects) {
1708
1800
  projects
1709
1801
  );
1710
1802
  const modelDirectory = path.dirname(modelFilePath);
1711
- const servicesDirectory = path.join(modelDirectory, "services");
1712
- const servicesFilePath = codegenFs.exists(servicesDirectory) ? path.join(servicesDirectory, `${nameDashCase}-services.ts`) : void 0;
1803
+ const servicesFilePath = servicesFilePathFor(modelFilePath, nameDashCase);
1713
1804
  const registrationFilePath = path.join(
1714
1805
  modelDirectory,
1715
1806
  `${nameDashCase}-registration.ts`
@@ -1919,6 +2010,7 @@ class ModelFileTransformer {
1919
2010
  });
1920
2011
  }
1921
2012
  }
2013
+ const SDK_MODULE = "@kosdev-code/kos-ui-sdk";
1922
2014
  class ServiceFileTransformer {
1923
2015
  constructor(codegenFs, options) {
1924
2016
  this.codegenFs = codegenFs;
@@ -1927,162 +2019,69 @@ class ServiceFileTransformer {
1927
2019
  codegenFs;
1928
2020
  options;
1929
2021
  transform() {
1930
- const { servicesFilePath } = this.options;
1931
- if (!servicesFilePath || !this.codegenFs.exists(servicesFilePath)) {
1932
- this.createServicesFile();
1933
- return;
1934
- }
1935
- let content = this.codegenFs.read(servicesFilePath);
1936
- content = this.addFutureImports(content);
1937
- content = this.addFutureService(content);
1938
- content = this.addProgressTypes(content);
1939
- this.codegenFs.write(servicesFilePath, content);
1940
- }
1941
- createServicesFile() {
1942
- const { servicesFilePath, nameProperCase, nameDashCase, nameLowerCase } = this.options;
1943
- if (!servicesFilePath) {
1944
- return;
1945
- }
1946
- const content = `import {
1947
- KosLog,
1948
- type ClientResponse,
1949
- type DeepRequired,
1950
- type ElementType,
1951
- type ServiceResponse,
1952
- type FutureResponse
1953
- } from '@kosdev-code/kos-ui-sdk';
1954
-
1955
- import API, { type KosApi, type ApiPath } from '../../../utils/service';
1956
-
1957
- const log = KosLog.createLogger({name: "${nameDashCase}-service", group: "Services"});
1958
-
1959
- const SERVICE_PATH: ApiPath = "ENTER_SERVICE_PATH"
1960
- export type ${nameProperCase}ClientResponse = ClientResponse<
1961
- KosApi,
1962
- typeof SERVICE_PATH,
1963
- 'get'
1964
- >;
1965
- export type ${nameProperCase}Response = DeepRequired<${nameProperCase}ClientResponse>;
1966
-
1967
- /**
1968
- * @category Service
1969
- * Retrieves the initial ${nameLowerCase} data.
1970
- */
1971
- export const get${nameProperCase} = async (): Promise<ServiceResponse<${nameProperCase}Response>> => {
1972
- log.debug('sending GET for ${nameLowerCase}');
1973
- return await API.get(SERVICE_PATH);
1974
- };
1975
-
1976
- /**
1977
- * @category Service - Future Operation
1978
- * Placeholder for a long-running operation that returns a Future for progress tracking
1979
- *
1980
- * Replace this with your actual long-running service operation
1981
- */
1982
- export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
1983
- // TODO: Implement your long-running service operation here
1984
- // This should return a Future that can be tracked for progress
1985
-
1986
- log.debug('starting long-running ${nameLowerCase} operation');
1987
-
1988
- // Example pattern:
1989
- // return API.post(OPERATION_SERVICE_PATH, {
1990
- // // operation parameters
1991
- // });
1992
-
1993
- // Placeholder - replace with actual implementation
1994
- throw new Error('perform${nameProperCase}Operation not yet implemented');
1995
- };
1996
-
1997
- // Additional Future-aware service types (add as needed)
1998
- export type ${nameProperCase}OperationProgress = {
1999
- // Define your progress data structure here
2000
- stage: string;
2001
- percentComplete: number;
2002
- currentItem?: string;
2003
- totalItems?: number;
2004
- };
2005
-
2006
- export type ${nameProperCase}OperationResult = {
2007
- // Define your operation result structure here
2008
- success: boolean;
2009
- message?: string;
2010
- data?: any;
2011
- };
2012
- `;
2013
- this.codegenFs.write(servicesFilePath, content);
2014
- }
2015
- addFutureImports(content) {
2016
- if (content.includes("FutureResponse")) {
2017
- return content;
2018
- }
2019
- const importRegex = /import {\s*([^}]*)\s*} from '@kosdev-code\/kos-ui-sdk';/;
2020
- const importMatch = content.match(importRegex);
2021
- if (importMatch) {
2022
- const existingImports = importMatch[1];
2023
- if (existingImports.includes("FutureResponse")) {
2024
- return content;
2025
- }
2026
- const cleanedImports = existingImports.trim().replace(/,\s*$/, "");
2027
- const newImports = cleanedImports ? `${cleanedImports},
2028
- type FutureResponse` : `type FutureResponse`;
2029
- const newImportStatement = `import {
2030
- ${newImports}
2031
- } from '@kosdev-code/kos-ui-sdk';`;
2032
- return content.replace(importMatch[0], newImportStatement);
2033
- }
2034
- return content;
2022
+ const { modelFilePath, nameDashCase } = this.options;
2023
+ if (!modelFilePath) return;
2024
+ const servicesFilePath = ensureServicesModule(
2025
+ this.codegenFs,
2026
+ modelFilePath,
2027
+ nameDashCase
2028
+ );
2029
+ transformSourceFile(this.codegenFs, servicesFilePath, (sf) => {
2030
+ ensureNamedImport(sf, SDK_MODULE, [
2031
+ { name: "KosLog" },
2032
+ { name: "FutureResponse", isTypeOnly: true }
2033
+ ]);
2034
+ ensureModuleConst(sf, {
2035
+ name: "log",
2036
+ initializer: `KosLog.createLogger({ name: "${nameDashCase}-service", group: "Services" })`
2037
+ });
2038
+ this.addFutureOperation(sf);
2039
+ this.addProgressTypes(sf);
2040
+ });
2035
2041
  }
2036
- addFutureService(content) {
2042
+ addFutureOperation(sf) {
2037
2043
  const { nameProperCase, nameLowerCase } = this.options;
2038
- if (content.includes(`perform${nameProperCase}Operation`)) {
2039
- return content;
2040
- }
2041
- const futureService = `
2042
- /**
2043
- * @category Service - Future Operation
2044
- * Placeholder for a long-running operation that returns a Future for progress tracking
2045
- *
2046
- * Replace this with your actual long-running service operation
2047
- */
2048
- export const perform${nameProperCase}Operation = async (): Promise<FutureResponse> => {
2049
- // TODO: Implement your long-running service operation here
2050
- // This should return a Future that can be tracked for progress
2051
-
2052
- log.debug('starting long-running ${nameLowerCase} operation');
2053
-
2054
- // Example pattern:
2055
- // return API.post(OPERATION_SERVICE_PATH, {
2056
- // // operation parameters
2057
- // });
2058
-
2059
- // Placeholder - replace with actual implementation
2060
- throw new Error('perform${nameProperCase}Operation not yet implemented');
2061
- };`;
2062
- return content + "\n" + futureService;
2044
+ const name = `perform${nameProperCase}Operation`;
2045
+ if (sf.getVariableDeclaration(name)) return;
2046
+ sf.addVariableStatement({
2047
+ isExported: true,
2048
+ declarationKind: VariableDeclarationKind.Const,
2049
+ declarations: [
2050
+ {
2051
+ name,
2052
+ initializer: `async (): Promise<FutureResponse> => {
2053
+ log.debug("starting long-running ${nameLowerCase} operation");
2054
+ throw new Error("${name} not yet implemented");
2055
+ }`
2056
+ }
2057
+ ],
2058
+ docs: [
2059
+ {
2060
+ description: `@category Service - Future Operation
2061
+ A long-running operation returning a Future for progress tracking. Replace the body with the real service call.`
2062
+ }
2063
+ ]
2064
+ });
2063
2065
  }
2064
- addProgressTypes(content) {
2066
+ addProgressTypes(sf) {
2065
2067
  const { nameProperCase } = this.options;
2066
- if (content.includes(`${nameProperCase}OperationProgress`)) {
2067
- return content;
2068
- }
2069
- const progressTypes = `
2070
- // Additional Future-aware service types (add as needed)
2071
- export type ${nameProperCase}OperationProgress = {
2072
- // Define your progress data structure here
2068
+ ensureExportedTypeAlias(sf, {
2069
+ name: `${nameProperCase}OperationProgress`,
2070
+ type: `{
2073
2071
  stage: string;
2074
2072
  percentComplete: number;
2075
2073
  currentItem?: string;
2076
2074
  totalItems?: number;
2077
- };
2078
-
2079
- export type ${nameProperCase}OperationResult = {
2080
- // Define your operation result structure here
2075
+ }`
2076
+ });
2077
+ ensureExportedTypeAlias(sf, {
2078
+ name: `${nameProperCase}OperationResult`,
2079
+ type: `{
2081
2080
  success: boolean;
2082
2081
  message?: string;
2083
- data?: any;
2084
- };`;
2085
- return content + "\n" + progressTypes;
2082
+ data?: unknown;
2083
+ }`
2084
+ });
2086
2085
  }
2087
2086
  }
2088
2087
  class RegistrationFileTransformer {
@@ -2525,6 +2524,32 @@ function toImportSpecifier(fromFile, toFileNoExt) {
2525
2524
  if (!rel.startsWith(".")) rel = `./${rel}`;
2526
2525
  return rel;
2527
2526
  }
2527
+ function fromImportSpecifier(fromFile, specifier) {
2528
+ return path.join(path.dirname(fromFile), specifier);
2529
+ }
2530
+ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, modelProject) {
2531
+ const file = `${serviceModuleFile}.ts`;
2532
+ if (!codegenFs.exists(file)) return;
2533
+ const typeParams = readSourceFile(codegenFs, file, (sf) => {
2534
+ const alias = sf.getTypeAlias("EndpointCtx");
2535
+ return alias ? alias.getTypeParameters().length : null;
2536
+ });
2537
+ if (typeParams === null || typeParams >= 2) return;
2538
+ throw new Error(
2539
+ `${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
+ );
2541
+ }
2542
+ function sameEndpoint(existing, candidate) {
2543
+ const args = (text) => {
2544
+ const match = text.match(
2545
+ /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2546
+ );
2547
+ return match ? [match[2], match[4].toLowerCase()] : null;
2548
+ };
2549
+ const a = args(existing.trim());
2550
+ const b = args(candidate.trim());
2551
+ return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2552
+ }
2528
2553
  function addServiceRequestToModel(codegenFs, options, projects) {
2529
2554
  const logger = getCodegenLogger();
2530
2555
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2535,14 +2560,15 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2535
2560
  if (!codegenFs.exists(modelFilePath)) {
2536
2561
  throw new Error(`Model file not found: ${modelFilePath}`);
2537
2562
  }
2538
- let serviceImport = options.serviceModule;
2539
- if (!serviceImport && options.serviceModulePath) {
2540
- serviceImport = toImportSpecifier(
2563
+ let serviceModuleFile;
2564
+ if (options.serviceModulePath) {
2565
+ serviceModuleFile = options.serviceModulePath.replace(/\.ts$/, "");
2566
+ } else if (options.serviceModule) {
2567
+ serviceModuleFile = fromImportSpecifier(
2541
2568
  modelFilePath,
2542
- options.serviceModulePath.replace(/\.ts$/, "")
2569
+ options.serviceModule
2543
2570
  );
2544
- }
2545
- if (!serviceImport) {
2571
+ } else {
2546
2572
  if (!sourceRoot) {
2547
2573
  throw new Error(
2548
2574
  "Cannot auto-resolve the service module without a project source root. Pass serviceModule."
@@ -2554,42 +2580,91 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2554
2580
  "No generated service module found. Run `kosui api:generate` for this project first."
2555
2581
  );
2556
2582
  }
2557
- const specifiers = modules.map(
2558
- (m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
2559
- );
2560
2583
  if (modules.length > 1) {
2584
+ const specifiers = modules.map(
2585
+ (m) => toImportSpecifier(modelFilePath, m.replace(/\.ts$/, ""))
2586
+ );
2561
2587
  throw new Error(
2562
2588
  `Multiple service modules found — pass serviceModule (one of): ${specifiers.join(
2563
2589
  ", "
2564
2590
  )}`
2565
2591
  );
2566
2592
  }
2567
- serviceImport = specifiers[0];
2593
+ serviceModuleFile = modules[0].replace(/\.ts$/, "");
2568
2594
  }
2569
- const method = options.method || "get";
2595
+ assertServiceModuleAcceptsTransform(
2596
+ codegenFs,
2597
+ serviceModuleFile,
2598
+ options.modelProject
2599
+ );
2600
+ const modelBase = modelBaseName(modelFilePath, options.modelName);
2601
+ const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
2602
+ const serviceImportFromModel = toImportSpecifier(
2603
+ modelFilePath,
2604
+ serviceModuleFile
2605
+ );
2606
+ const serviceImportFromServices = toImportSpecifier(
2607
+ servicesFilePath,
2608
+ serviceModuleFile
2609
+ );
2610
+ const method = (options.method || "get").toLowerCase();
2570
2611
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2571
2612
  const lifecycle = options.lifecycle || "LOAD";
2613
+ const catalogName = `${pascalCase(modelBase)}Endpoints`;
2572
2614
  logger.info(
2573
2615
  `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2574
2616
  );
2617
+ ensureServicesModule(codegenFs, modelFilePath, modelBase);
2618
+ let endpointKey = options.methodName;
2619
+ let endpointCreated = true;
2620
+ let dataAlias = "";
2621
+ let mapperName = "";
2622
+ let ctxAlias = "";
2623
+ transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2624
+ ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
2625
+ const entry = ensureConstCatalogEntry(sf, {
2626
+ catalogName,
2627
+ key: options.methodName,
2628
+ initializer: `endpoint(${JSON.stringify(
2629
+ options.servicePath
2630
+ )}, ${JSON.stringify(method)})`,
2631
+ equals: sameEndpoint
2632
+ });
2633
+ endpointKey = entry.key;
2634
+ endpointCreated = entry.created;
2635
+ dataAlias = `${pascalCase(endpointKey)}Data`;
2636
+ mapperName = `to${pascalCase(endpointKey)}Data`;
2637
+ ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2638
+ const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2639
+ ensureNamedImport(sf, serviceImportFromServices, [
2640
+ { name: "EndpointResponse", isTypeOnly: true }
2641
+ ]);
2642
+ ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2643
+ ensureExportedMapper(sf, {
2644
+ name: mapperName,
2645
+ rawType,
2646
+ dataType: dataAlias
2647
+ });
2648
+ if (mode === "method") {
2649
+ ensureNamedImport(sf, serviceImportFromServices, [
2650
+ { name: "EndpointCtx", isTypeOnly: true }
2651
+ ]);
2652
+ ensureExportedTypeAlias(sf, {
2653
+ name: ctxAlias,
2654
+ type: `EndpointCtx<typeof ${catalogName}.${endpointKey}, ${dataAlias}>`
2655
+ });
2656
+ }
2657
+ });
2575
2658
  transformSourceFile(codegenFs, modelFilePath, (sf) => {
2576
- const endpointConst = endpointConstName(options.methodName);
2577
2659
  const className = getModelClass(sf).getName();
2578
2660
  if (!className) throw new Error("Model class has no name.");
2579
2661
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2580
- ensureNamedImport(sf, serviceImport, [
2581
- { name: "endpoint" },
2582
- { name: "serviceRequest" }
2583
- ]);
2584
- ensureModuleConst(sf, {
2585
- name: endpointConst,
2586
- initializer: `endpoint(${JSON.stringify(
2587
- options.servicePath
2588
- )}, ${JSON.stringify(method)})`
2589
- });
2662
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
2590
2663
  if (mode === "lifecycle") {
2591
- ensureNamedImport(sf, serviceImport, [
2592
- { name: "EndpointResponse", isTypeOnly: true }
2664
+ ensureNamedImport(sf, "./services", [
2665
+ { name: catalogName },
2666
+ { name: mapperName },
2667
+ { name: dataAlias, isTypeOnly: true }
2593
2668
  ]);
2594
2669
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2595
2670
  { name: "DependencyLifecycle" }
@@ -2597,25 +2672,24 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2597
2672
  addDecoratedMethod(getModelClass(sf), {
2598
2673
  name: options.methodName,
2599
2674
  decoratorName: "serviceRequest",
2600
- decoratorArgsText: `${endpointConst}, { lifecycle: DependencyLifecycle.${lifecycle} }`,
2675
+ decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
2601
2676
  // The manager invokes phase handlers error-first, passing (null, data)
2602
2677
  // on success. The `error` half is only ever reached because
2603
2678
  // `serviceRequest` supplies an errorHandler — the bare decorator
2604
2679
  // defaults to `throw`, which fails the phase instead of calling back.
2605
2680
  parameters: [
2606
2681
  { name: "error", type: "string | null" },
2607
- {
2608
- name: "data",
2609
- type: `EndpointResponse<typeof ${endpointConst}>`
2610
- }
2682
+ { name: "data", type: dataAlias }
2611
2683
  ],
2612
2684
  returnType: "void",
2613
2685
  statements: "// TODO: apply `data` to the model"
2614
2686
  });
2615
2687
  return;
2616
2688
  }
2617
- ensureNamedImport(sf, serviceImport, [
2618
- { name: "EndpointCtx", isTypeOnly: true }
2689
+ ensureNamedImport(sf, "./services", [
2690
+ { name: catalogName },
2691
+ { name: mapperName },
2692
+ { name: ctxAlias, isTypeOnly: true }
2619
2693
  ]);
2620
2694
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2621
2695
  { name: "executeServiceRequest" },
@@ -2631,15 +2705,9 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2631
2705
  addDecoratedMethod(getModelClass(sf), {
2632
2706
  name: options.methodName,
2633
2707
  decoratorName: "serviceRequest",
2634
- decoratorArgsText: endpointConst,
2708
+ decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
2635
2709
  // Optional: the framework appends the context, callers never pass it.
2636
- parameters: [
2637
- {
2638
- name: "$ctx",
2639
- type: `EndpointCtx<typeof ${endpointConst}>`,
2640
- optional: true
2641
- }
2642
- ],
2710
+ parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2643
2711
  isAsync: true,
2644
2712
  returnType: "Promise<void>",
2645
2713
  statements: [
@@ -2649,11 +2717,14 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2649
2717
  ].join("\n")
2650
2718
  });
2651
2719
  });
2652
- return { modelFilePath, serviceModule: serviceImport, mode };
2653
- }
2654
- function endpointConstName(methodName) {
2655
- const snake = methodName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2656
- return `ENDPOINT_${snake}`;
2720
+ return {
2721
+ modelFilePath,
2722
+ servicesFilePath,
2723
+ serviceModule: serviceImportFromModel,
2724
+ mode,
2725
+ endpointKey,
2726
+ endpointCreated
2727
+ };
2657
2728
  }
2658
2729
  function modelElementType(typeText) {
2659
2730
  let m;