@kosdev-code/kos-codegen-core 3.0.18 → 3.0.20
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 +293 -222
- package/index.js.map +1 -1
- package/index.mjs +293 -222
- package/index.mjs.map +1 -1
- package/lib/generators/add-future-to-model/normalize-options.d.ts.map +1 -1
- package/lib/generators/add-future-to-model/service-transformer.d.ts +1 -3
- package/lib/generators/add-future-to-model/service-transformer.d.ts.map +1 -1
- package/lib/generators/augment/model-services.d.ts +11 -0
- package/lib/generators/augment/model-services.d.ts.map +1 -0
- package/lib/generators/augment/ts-toolkit.d.ts +35 -0
- package/lib/generators/augment/ts-toolkit.d.ts.map +1 -1
- package/lib/generators/generate-container-model.d.ts +0 -1
- package/lib/generators/generate-container-model.d.ts.map +1 -1
- package/lib/generators/generate-init.d.ts.map +1 -1
- package/lib/generators/generate-model.d.ts +0 -1
- package/lib/generators/generate-model.d.ts.map +1 -1
- package/lib/generators/member-mutators/add-service-request.d.ts +9 -2
- package/lib/generators/member-mutators/add-service-request.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/kos-container-model/model/index.ts.template +0 -2
- package/templates/kos-model/model/index.ts__template__ +0 -3
- package/templates/kos-container-model/services/__modelNameDashCase__-services.ts.template +0 -22
- package/templates/kos-container-model/services/index.ts.template +0 -1
- package/templates/kos-model/services/__nameDashCase__-services.ts.template +0 -58
- package/templates/kos-model/services/index.ts.template +0 -1
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
|
|
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 {
|
|
1953
|
-
if (!
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
-
|
|
2064
|
+
addFutureOperation(sf) {
|
|
2059
2065
|
const { nameProperCase, nameLowerCase } = this.options;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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(
|
|
2088
|
+
addProgressTypes(sf) {
|
|
2087
2089
|
const { nameProperCase } = this.options;
|
|
2088
|
-
|
|
2089
|
-
|
|
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
|
-
|
|
2102
|
-
|
|
2097
|
+
}`
|
|
2098
|
+
});
|
|
2099
|
+
ensureExportedTypeAlias(sf, {
|
|
2100
|
+
name: `${nameProperCase}OperationResult`,
|
|
2101
|
+
type: `{
|
|
2103
2102
|
success: boolean;
|
|
2104
2103
|
message?: string;
|
|
2105
|
-
data?:
|
|
2106
|
-
}
|
|
2107
|
-
|
|
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
|
|
2561
|
-
if (
|
|
2562
|
-
|
|
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.
|
|
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
|
-
|
|
2615
|
+
serviceModuleFile = modules[0].replace(/\.ts$/, "");
|
|
2590
2616
|
}
|
|
2591
|
-
|
|
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,
|
|
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,
|
|
2614
|
-
{ name:
|
|
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: `${
|
|
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,
|
|
2640
|
-
{ name:
|
|
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:
|
|
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 {
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
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;
|