@odata2ts/odata2ts
Version:
Flexible generator to produce various TypeScript artefacts (from simple model interfaces to complete odata clients) from OData metadata files
506 lines • 27.2 kB
JavaScript
import { __awaiter } from "tslib";
import { ODataVersions } from "@odata2ts/odata-core";
import deepmerge from "deepmerge";
import { Scope, } from "ts-morph";
import { upperCaseFirst } from "upper-case-first";
import { firstCharLowerCase } from "xml2js/lib/processors.js";
import { Modes } from "../OptionModel.js";
import { ClientApiImports, CoreImports, QueryObjectImports, ServiceImports } from "./import/ImportObjects.js";
export function generateServices(project, dataModel, version, namingHelper, options) {
return __awaiter(this, void 0, void 0, function* () {
const generator = new ServiceGenerator(project, dataModel, version, namingHelper, options);
return generator.generate();
});
}
class ServiceGenerator {
constructor(project, dataModel, version, namingHelper, options = {}) {
this.project = project;
this.dataModel = dataModel;
this.version = version;
this.namingHelper = namingHelper;
this.options = options;
this.generateQOperationProp = (operation) => {
return {
scope: Scope.Private,
name: this.namingHelper.getPrivatePropName(operation.qName),
type: operation.qName,
hasQuestionToken: true,
};
};
}
isV4BigNumber() {
return this.options.v4BigNumberAsString && this.version === ODataVersions.V4;
}
generate() {
return __awaiter(this, void 0, void 0, function* () {
const mainServiceName = this.namingHelper.getMainServiceName();
this.project.initServices();
yield Promise.all([
this.generateMainService(mainServiceName),
...this.generateEntityTypeServices(),
...this.generateComplexTypeServices(),
]);
return this.project.finalizeServices();
});
}
generateMainService(mainServiceName) {
return __awaiter(this, void 0, void 0, function* () {
const mainServiceFile = this.project.getMainServiceFile();
const importContainer = mainServiceFile.getImports();
const container = this.dataModel.getEntityContainer();
const unboundOperations = [...Object.values(container.functions), ...Object.values(container.actions)];
const httpClient = importContainer.addClientApi(ClientApiImports.ODataHttpClient);
const rootService = importContainer.addServiceObject(this.version, ServiceImports.ODataService);
const { properties, methods } = deepmerge(this.generateMainServiceProperties(container, importContainer), this.generateMainServiceOperations(unboundOperations, importContainer));
mainServiceFile.getFile().addClass({
isExported: true,
name: mainServiceName,
typeParameters: [`in out ClientType extends ${httpClient}`],
extends: `${rootService}<ClientType>`,
ctors: this.isV4BigNumber()
? [
{
parameters: [
{ name: "client", type: "ClientType" },
{ name: "basePath", type: "string" },
{
name: "options",
type: importContainer.addServiceObject(this.version, ServiceImports.ODataServiceOptions),
hasQuestionToken: true,
},
],
statements: [`super(client, basePath, options);`, "this.__base.options.bigNumbersAsString = true;"],
},
]
: [],
properties,
methods,
});
});
}
generateMainServiceProperties(container, importContainer) {
const result = { properties: [], methods: [] };
Object.values(container.entitySets).forEach(({ name, odataName, entityType }) => {
result.methods.push(this.generateRelatedServiceGetter(name, odataName, entityType, importContainer));
});
Object.values(container.singletons).forEach((singleton) => {
result.properties.push(this.generateSingletonProp(importContainer, singleton));
result.methods.push(this.generateSingletonGetter(singleton));
});
return result;
}
generateMainServiceOperations(ops, importContainer) {
const result = { properties: [], methods: [] };
ops.forEach(({ operation, name }) => {
const op = this.dataModel.getUnboundOperationType(operation);
if (!op) {
throw new Error(`Operation "${operation}" not found!`);
}
result.properties.push(this.generateQOperationProp(op));
result.methods.push(this.generateMethod(name, op, importContainer, ""));
});
return result;
}
generateRelatedServiceGetter(propName, odataPropName, entityType, imports) {
const idName = imports.addGeneratedModel(entityType.id.fqName, entityType.id.modelName);
const idFunctionName = imports.addGeneratedQObject(entityType.id.fqName, entityType.id.qName);
const serviceName = imports.addGeneratedService(entityType.fqName, entityType.serviceName);
const collectionName = imports.addGeneratedService(entityType.fqName, entityType.serviceCollectionName);
return {
scope: Scope.Public,
name: this.namingHelper.getRelatedServiceGetter(propName),
parameters: [
{
name: "id",
type: `${idName} | undefined`,
hasQuestionToken: true,
},
],
overloads: [
{
parameters: [],
returnType: `${collectionName}<ClientType>`,
},
{
parameters: [
{
name: "id",
type: idName,
},
],
returnType: `${serviceName}<ClientType>`,
},
],
statements: [
`const fieldName = "${odataPropName}";`,
`const { client, path, options, isUrlNotEncoded } = this.__base;`,
'return typeof id === "undefined" || id === null',
`? new ${collectionName}(client, path, fieldName, options)`,
`: new ${serviceName}(client, path, new ${idFunctionName}(fieldName).buildUrl(id, isUrlNotEncoded()), options);`,
],
};
}
generateSingletonProp(importContainer, singleton) {
const { name, entityType } = singleton;
const type = entityType.serviceName;
return {
scope: Scope.Private,
name: this.namingHelper.getPrivatePropName(name),
type: `${type}<ClientType>`,
hasQuestionToken: true,
};
}
generateSingletonGetter(singleton) {
const { name, odataName, entityType } = singleton;
const propName = "this." + this.namingHelper.getPrivatePropName(name);
const serviceType = entityType.serviceName;
return {
scope: Scope.Public,
name: this.namingHelper.getRelatedServiceGetter(name),
statements: [
`if(!${propName}) {`,
` const { client, path, options } = this.__base;`,
// prettier-ignore
` ${propName} = new ${serviceType}(client, path, "${odataName}", options)`,
"}",
`return ${propName}`,
],
};
}
generateEntityTypeService(file, model) {
const importContainer = file.getImports();
const operations = this.dataModel.getEntityTypeOperations(model.fqName);
const props = [...model.baseProps, ...model.props];
const entityServiceType = importContainer.addServiceObject(this.version, ServiceImports.EntityTypeService);
const httpClient = importContainer.addClientApi(ClientApiImports.ODataHttpClient);
// note: predictable first imports => no need to take renaming into account
const modelName = importContainer.addGeneratedModel(model.fqName, model.modelName);
const editableModelName = importContainer.addGeneratedModel(model.fqName, model.editableName);
const qName = importContainer.addGeneratedQObject(model.fqName, model.qName, true);
const qObjectName = importContainer.addGeneratedQObject(model.fqName, firstCharLowerCase(model.qName));
const serviceOptions = importContainer.addServiceObject(this.version, this.version === ODataVersions.V4
? ServiceImports.ODataServiceOptionsInternal
: ServiceImports.ODataServiceOptions);
const { properties, methods } = deepmerge(deepmerge(this.generateServiceProperties(importContainer, model.serviceName, props), this.generateServiceOperations(importContainer, model, operations)), this.generateCastOperations(importContainer, model, false));
// generate EntityTypeService
file.getFile().addClass({
isExported: true,
name: model.serviceName,
typeParameters: [`in out ClientType extends ${httpClient}`],
extends: entityServiceType + `<ClientType, ${modelName}, ${editableModelName}, ${qName}>`,
ctors: [
{
parameters: [
{ name: "client", type: "ClientType" },
{ name: "basePath", type: "string" },
{ name: "name", type: "string" },
{ name: "options", type: serviceOptions, hasQuestionToken: true },
],
statements: [`super(client, basePath, name, ${qObjectName}, options);`],
},
],
properties,
methods,
});
}
generateServiceProperties(importContainer, serviceName, props) {
const result = { properties: [], methods: [] };
props.forEach((prop) => {
// collection of ComplexTypes, ComplexTypes, or EntityTypes
if ((prop.dataType === "ModelType" /* DataTypes.ModelType */ && !prop.isCollection) || prop.dataType === "ComplexType" /* DataTypes.ComplexType */) {
result.properties.push(this.generateModelProp(importContainer, prop));
result.methods.push(this.generateModelPropGetter(importContainer, prop));
}
else if (prop.isCollection) {
// collection of EntityTypes
if (prop.dataType === "ModelType" /* DataTypes.ModelType */) {
const entityType = this.dataModel.getEntityType(prop.fqType);
if (!entityType) {
throw new Error(`Entity type "${prop.fqType}" specified by property not found!`);
}
result.methods.push(this.generateRelatedServiceGetter(prop.name, prop.odataName, entityType, importContainer));
}
// collection of primitive or enum types
else {
result.properties.push(this.generatePrimitiveCollectionProp(importContainer, prop));
result.methods.push(this.generatePrimitiveCollectionGetter(importContainer, prop));
}
}
// generation of services for each primitive property: turned off by default
else if (this.options.enablePrimitivePropertyServices && prop.dataType === "PrimitiveType" /* DataTypes.PrimitiveType */) {
result.properties.push(this.generatePrimitiveTypeProp(importContainer, prop));
result.methods.push(this.generatePrimitiveTypeGetter(importContainer, prop));
}
});
return result;
}
generateServiceOperations(importContainer, model, operations) {
const result = { properties: [], methods: [] };
operations.forEach((operation) => {
result.properties.push(this.generateQOperationProp(operation));
result.methods.push(this.generateMethod(operation.name, operation, importContainer, model.fqName));
});
return result;
}
generateModelProp(imports, prop) {
const propModel = this.dataModel.getModel(prop.fqType);
let propModelType;
if (prop.isCollection) {
const modelName = imports.addGeneratedModel(propModel.fqName, propModel.modelName);
const editableModelName = imports.addGeneratedModel(propModel.fqName, propModel.editableName);
const qModelName = imports.addGeneratedQObject(propModel.fqName, propModel.qName, true);
const collectionServiceType = imports.addServiceObject(this.version, ServiceImports.CollectionService);
propModelType = `${collectionServiceType}<ClientType, ${modelName}, ${qModelName}, ${editableModelName}>`;
}
else {
const serviceName = imports.addGeneratedService(propModel.fqName, propModel.serviceName);
propModelType = `${serviceName}<ClientType>`;
}
return {
scope: Scope.Private,
name: this.namingHelper.getPrivatePropName(prop.name),
type: propModelType,
hasQuestionToken: true,
};
}
generatePrimitiveCollectionProp(imports, prop) {
if (!prop.qObject) {
throw new Error("Illegal State: [qObject] must be provided for Collection types!");
}
const collectionServiceType = imports.addServiceObject(this.version, ServiceImports.CollectionService);
const isEnum = prop.dataType === "EnumType" /* DataTypes.EnumType */;
const isNumericEnum = this.options.numericEnums;
let qType;
let type;
if (isEnum) {
const propEnum = this.dataModel.getModel(prop.fqType);
const propTypeModel = imports.addGeneratedModel(propEnum.fqName, propEnum.modelName, false);
type = `${imports.addQObjectType(QueryObjectImports.EnumCollection)}<typeof ${propTypeModel}>`;
qType = `${imports.addQObjectType(isNumericEnum ? QueryObjectImports.QNumericEnumCollection : QueryObjectImports.QEnumCollection)}<typeof ${propTypeModel}>`;
}
else {
// TODO refactor string concat
type = imports.addQObjectType(`${upperCaseFirst(prop.type)}Collection`);
qType = imports.addQObjectType(prop.qObject);
}
const collectionType = `${collectionServiceType}<ClientType, ${type}, ${qType}>`;
return {
scope: Scope.Private,
name: this.namingHelper.getPrivatePropName(prop.name),
type: `${collectionType}`,
hasQuestionToken: true,
};
}
generatePrimitiveTypeProp(imports, prop) {
const serviceType = imports.addServiceObject(this.version, ServiceImports.PrimitiveTypeService);
const type = prop.typeModule ? imports.addCustomType(prop.typeModule, prop.type, true) : prop.type;
return {
scope: Scope.Private,
name: this.namingHelper.getPrivatePropName(prop.name),
type: `${serviceType}<ClientType, ${type}>`,
hasQuestionToken: true,
};
}
generateModelPropGetter(imports, prop) {
const model = this.dataModel.getModel(prop.fqType);
const isComplexCollection = prop.isCollection && model.dataType === "ComplexType" /* DataTypes.ComplexType */;
const type = isComplexCollection
? imports.addServiceObject(this.version, ServiceImports.CollectionService)
: prop.isCollection
? model.serviceCollectionName
: model.serviceName;
const typeWithGenerics = isComplexCollection
? `${type}<ClientType, ${imports.addGeneratedModel(model.fqName, model.modelName)}, ${imports.addGeneratedQObject(model.fqName, model.qName, true)}, ${imports.addGeneratedModel(model.fqName, model.editableName)}>`
: `${type}<ClientType>`;
const privateSrvProp = "this." + this.namingHelper.getPrivatePropName(prop.name);
return {
scope: Scope.Public,
name: this.namingHelper.getRelatedServiceGetter(prop.name),
returnType: typeWithGenerics,
statements: [
`if(!${privateSrvProp}) {`,
` const { client, path, options } = this.__base;`,
// prettier-ignore
` ${privateSrvProp} = new ${type}(client, path, "${prop.odataName}"${isComplexCollection ? `, ${imports.addGeneratedQObject(model.fqName, firstCharLowerCase(model.qName))}` : ""}, options)`,
"}",
`return ${privateSrvProp}`,
],
};
}
generatePrimitiveCollectionGetter(imports, prop) {
const collectionServiceType = imports.addServiceObject(this.version, ServiceImports.CollectionService);
const qCollectionName = imports.addQObject(prop.qObject);
const enumName = prop.dataType === "EnumType" /* DataTypes.EnumType */ ? imports.addGeneratedModel(prop.fqType, prop.type) : undefined;
const propName = "this." + this.namingHelper.getPrivatePropName(prop.name);
return {
scope: Scope.Public,
name: this.namingHelper.getRelatedServiceGetter(prop.name),
statements: [
`if(!${propName}) {`,
` const { client, path, options } = this.__base;`,
// prettier-ignore
` ${propName} = new ${collectionServiceType}(client, path, "${prop.odataName}", new ${qCollectionName}(${enumName !== null && enumName !== void 0 ? enumName : ""}), options)`,
"}",
`return ${propName}`,
],
};
}
generatePrimitiveTypeGetter(imports, prop) {
const serviceType = imports.addServiceObject(this.version, ServiceImports.PrimitiveTypeService);
const propName = "this." + this.namingHelper.getPrivatePropName(prop.name);
// for V2: mapped name must be specified
const v2MappedName = this.version === ODataVersions.V4 ? "" : prop.name !== prop.odataName ? `, "${prop.name}"` : ", undefined";
return {
scope: Scope.Public,
name: this.namingHelper.getRelatedServiceGetter(prop.name),
statements: [
`if(!${propName}) {`,
` const { client, path, qModel, options } = this.__base;`,
` ${propName} = new ${serviceType}(client, path, "${prop.odataName}", qModel.${prop.name}.converter${v2MappedName}, options)`,
"}",
`return ${propName}`,
],
};
}
generateEntityCollectionService(file, model) {
const importContainer = file.getImports();
const editableModelName = model.editableName;
const qObjectName = firstCharLowerCase(model.qName);
const entitySetServiceType = importContainer.addServiceObject(this.version, ServiceImports.EntitySetService);
const paramsModelName = importContainer.addGeneratedModel(model.id.fqName, model.id.modelName);
const qIdFunctionName = importContainer.addGeneratedQObject(model.id.fqName, model.id.qName);
const serviceOptions = importContainer.addServiceObject(this.version, this.version === ODataVersions.V4
? ServiceImports.ODataServiceOptionsInternal
: ServiceImports.ODataServiceOptions);
const collectionOperations = this.dataModel.getEntitySetOperations(model.fqName);
const { properties, methods } = deepmerge(this.generateServiceOperations(importContainer, model, collectionOperations), this.generateCastOperations(importContainer, model, true));
file.getFile().addClass({
isExported: true,
name: model.serviceCollectionName,
typeParameters: ["in out ClientType extends ODataHttpClient"],
extends: entitySetServiceType +
`<ClientType, ${model.modelName}, ${editableModelName}, ${model.qName}, ${paramsModelName}>`,
ctors: [
{
parameters: [
{ name: "client", type: "ClientType" },
{ name: "basePath", type: "string" },
{ name: "name", type: "string" },
{ name: "options", type: serviceOptions, hasQuestionToken: true },
],
statements: [`super(client, basePath, name, ${qObjectName}, new ${qIdFunctionName}(name), options);`],
},
],
properties,
methods,
});
}
generateEntityTypeServices() {
// build service file for each entity, consisting of EntityTypeService & EntityCollectionService
return this.dataModel
.getEntityTypes()
.filter((model) => model.genMode === Modes.service || model.genMode === Modes.all)
.map((model) => {
const file = this.project.createOrGetServiceFile(model.folderPath, model.serviceName, [
model.serviceName,
model.serviceCollectionName,
]);
// entity type service
this.generateEntityTypeService(file, model);
// entity collection service if this entity specified keys at all
if (model.keyNames.length) {
this.generateEntityCollectionService(file, model);
}
return this.project.finalizeFile(file);
});
}
generateComplexTypeServices() {
// build service file for complex types
return this.dataModel
.getComplexTypes()
.filter((model) => model.genMode === Modes.service || model.genMode === Modes.all)
.map((model) => {
const file = this.project.createOrGetServiceFile(model.folderPath, model.serviceName, [model.serviceName]);
// entity type service
this.generateEntityTypeService(file, model);
return this.project.finalizeFile(file);
});
}
generateMethod(name, operation, importContainer, baseFqName) {
var _a, _b;
const isFunc = operation.type === "Function" /* OperationTypes.Function */;
const returnType = operation.returnType;
const hasParams = operation.parameters.length > 0 || ((_a = operation.overrides) === null || _a === void 0 ? void 0 : _a.length);
const isParamsOptional = !![operation.parameters, ...((_b = operation.overrides) !== null && _b !== void 0 ? _b : [])].find((pSet) => pSet.length === 0);
// importing dependencies
const httpClientConfigModel = importContainer.addClientApi(ClientApiImports.ODataHttpClientConfig);
const odataResponse = importContainer.addClientApi(ClientApiImports.HttpResponseModel);
const responseStructure = this.importReturnType(importContainer, returnType);
const qOperationName = importContainer.addGeneratedQObject(baseFqName, operation.qName);
const rtType = (returnType === null || returnType === void 0 ? void 0 : returnType.type) && returnType.dataType !== "PrimitiveType" /* DataTypes.PrimitiveType */
? importContainer.addGeneratedModel(returnType.fqType, returnType.type)
: returnType === null || returnType === void 0 ? void 0 : returnType.type;
const paramsModelName = hasParams
? importContainer.addGeneratedModel(baseFqName, operation.paramsModelName)
: undefined;
const requestConfigParam = {
name: "requestConfig",
hasQuestionToken: true,
type: `${httpClientConfigModel}<ClientType>`,
};
const qOpProp = "this." + this.namingHelper.getPrivatePropName(operation.qName);
return {
scope: Scope.Public,
isAsync: true,
name,
parameters: hasParams
? [{ name: "params", type: paramsModelName, hasQuestionToken: isParamsOptional }, requestConfigParam]
: [requestConfigParam],
returnType: `Promise<${odataResponse}<${responseStructure}<${rtType || "void"}>>>`,
statements: [
`if(!${qOpProp}) {`,
` ${qOpProp} = new ${qOperationName}()`,
"}",
`const { addFullPath, client, getDefaultHeaders, isUrlNotEncoded } = this.__base;`,
`const url = addFullPath(${qOpProp}.buildUrl(${isFunc && hasParams ? "params, " : ""}${isFunc ? "isUrlNotEncoded()" : ""}));`,
`${returnType ? "const response = await " : "return"} client.${!isFunc
? // actions: since V4
`post(url, ${hasParams ? `${qOpProp}.convertUserParams(params)` : "{}"}, ${requestConfigParam.name}, getDefaultHeaders())`
: operation.usePost
? // V2 POST => BUT values are still query params, they are not part of the request body
`post(url, undefined, ${requestConfigParam.name}, getDefaultHeaders())`
: // functions: since V2
`get(url, ${requestConfigParam.name}, getDefaultHeaders())`};`,
returnType ? `return ${qOpProp}.convertResponse(response);` : "",
],
};
}
importReturnType(imports, returnType) {
const typeToImport = (returnType === null || returnType === void 0 ? void 0 : returnType.isCollection)
? CoreImports.ODataCollectionResponse
: (returnType === null || returnType === void 0 ? void 0 : returnType.dataType) === "PrimitiveType" /* DataTypes.PrimitiveType */
? CoreImports.ODataValueResponse
: CoreImports.ODataModelResponse;
return imports.addCoreLib(this.version, typeToImport);
}
generateCastOperations(importContainer, model, isCollection) {
const result = { properties: [], methods: [] };
if (this.version === ODataVersions.V4) {
model.subtypes.forEach((subtype) => {
const subClass = this.dataModel.getModel(subtype);
const serviceName = isCollection ? subClass.serviceCollectionName : subClass.serviceName;
const serviceType = importContainer.addGeneratedService(subClass.fqName, serviceName);
result.methods.push({
name: `as${upperCaseFirst(serviceName)}`,
scope: Scope.Public,
statements: [
"const { client, path, options } = this.__base;",
`return new ${serviceType}(client, path, "${subClass.fqName}", { ...options, subtype: true });`,
],
});
});
}
return result;
}
}
//# sourceMappingURL=ServiceGenerator.js.map