UNPKG

@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
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