@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.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
|
|
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 {
|
|
1931
|
-
if (!
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
-
|
|
2042
|
+
addFutureOperation(sf) {
|
|
2037
2043
|
const { nameProperCase, nameLowerCase } = this.options;
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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(
|
|
2066
|
+
addProgressTypes(sf) {
|
|
2065
2067
|
const { nameProperCase } = this.options;
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
-
|
|
2080
|
-
|
|
2075
|
+
}`
|
|
2076
|
+
});
|
|
2077
|
+
ensureExportedTypeAlias(sf, {
|
|
2078
|
+
name: `${nameProperCase}OperationResult`,
|
|
2079
|
+
type: `{
|
|
2081
2080
|
success: boolean;
|
|
2082
2081
|
message?: string;
|
|
2083
|
-
data?:
|
|
2084
|
-
}
|
|
2085
|
-
|
|
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
|
|
2539
|
-
if (
|
|
2540
|
-
|
|
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.
|
|
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
|
-
|
|
2593
|
+
serviceModuleFile = modules[0].replace(/\.ts$/, "");
|
|
2568
2594
|
}
|
|
2569
|
-
|
|
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,
|
|
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,
|
|
2592
|
-
{ name:
|
|
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: `${
|
|
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,
|
|
2618
|
-
{ name:
|
|
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:
|
|
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 {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
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;
|