@odata2ts/odata2ts
Version:
Flexible generator to produce various TypeScript artefacts (from simple model interfaces to complete odata clients) from OData metadata files
352 lines • 16.9 kB
JavaScript
import { __awaiter } from "tslib";
import { ODataVersions } from "@odata2ts/odata-core";
import { Scope, VariableDeclarationKind, } from "ts-morph";
import { firstCharLowerCase } from "xml2js/lib/processors.js";
import { QueryObjectImports } from "./import/ImportObjects.js";
export const generateQueryObjects = (project, dataModel, version, options, namingHelper) => {
const generator = new QueryObjectGenerator(project, dataModel, version, options, namingHelper);
return generator.generate();
};
class QueryObjectGenerator {
constructor(project, dataModel, version, options, namingHelper) {
this.project = project;
this.dataModel = dataModel;
this.version = version;
this.options = options;
this.namingHelper = namingHelper;
}
generate() {
return __awaiter(this, void 0, void 0, function* () {
this.project.initQObjects();
// process EntityType & ComplexType
const promises = [...this.generateEntityTypes(), ...this.generateComplexTypes()];
if (!this.options.skipOperations) {
// process unbound operations
promises.push(this.generateUnboundOperations());
}
yield Promise.all(promises);
return this.project.finalizeQObjects();
});
}
generateEntityTypes() {
return this.dataModel.getEntityTypes().map((model) => {
const file = this.project.createOrGetQObjectFile(model.folderPath, model.qName, [
model.qName,
firstCharLowerCase(model.qName),
model.id.qName,
]);
// q object
this.generateModel(file, model);
// qId function
if (!this.options.skipIdModels && model.generateId) {
this.generateIdFunction(file, model);
}
// bound q operations
if (!this.options.skipOperations) {
[
...this.dataModel.getEntityTypeOperations(model.fqName),
...this.dataModel.getEntitySetOperations(model.fqName),
].forEach((operation) => {
this.generateOperation(file, operation, model.fqName);
});
}
return this.project.finalizeFile(file);
});
}
generateComplexTypes() {
return this.dataModel.getComplexTypes().map((model) => {
const file = this.project.createOrGetQObjectFile(model.folderPath, model.qName, [
model.qName,
firstCharLowerCase(model.qName),
]);
this.generateModel(file, model);
return this.project.finalizeFile(file);
});
}
generateModel(file, model) {
const imports = file.getImports();
const properties = this.generateQueryObjectProps(file.getImports(), model.props);
let extendsClause;
if (model.baseClasses.length) {
const baseClass = model.baseClasses[0];
const baseModel = this.dataModel.getModel(baseClass);
if (!baseModel) {
throw new Error(`Entity or complex type "${baseClass}" from baseClass attribute not found!`);
}
extendsClause = imports.addGeneratedQObject(baseClass, baseModel.qBaseName);
}
else {
extendsClause = imports.addQObject(QueryObjectImports.QueryObject);
}
if (model.qBaseName) {
// base class which used for extension
file.getFile().addClass({
name: model.qBaseName,
isExported: true,
extends: extendsClause,
properties,
});
const [props, getAccessors, methods, statements] = Array.from(model.subtypes).reduce((collector, subtype) => {
const subClass = this.dataModel.getModel(subtype);
const methodName = `__as${subClass.qName}`;
const enumerablePropDef = imports.addQObject(QueryObjectImports.ENUMERABLE_PROP_DEFINITION);
collector[0].push(`"${subClass.fqName}": "${subClass.qName}"`);
subClass.props.forEach((prop) => {
const propName = this.namingHelper.getQPropName(prop.name);
const fqPropName = `${subClass.qName}_${propName}`;
collector[1].push({
name: fqPropName,
scope: Scope.Public,
statements: `return this.${methodName}().${propName};`,
});
collector[3].push(`${fqPropName}: ${enumerablePropDef}`);
});
collector[2].push({
name: methodName,
scope: Scope.Private,
statements: `return new ${subClass.qName}(this.withPrefix("${subClass.fqName}"))`,
});
return collector;
}, [[], [], [], []]);
file.getFile().addClass({
name: model.qName,
isExported: true,
extends: model.qBaseName,
properties: [
{
name: "__subtypeMapping",
scope: Scope.Protected,
isReadonly: true,
initializer: `{ ${props.join(",")} }`,
},
],
getAccessors,
methods,
});
file.getFile().addStatements(`Object.defineProperties(${model.qName}.prototype, { ${statements.join(",")} });`);
}
else {
file.getFile().addClass({
name: model.qName,
isExported: true,
extends: extendsClause,
properties: properties,
});
}
file.getFile().addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
isExported: true,
declarations: [
{
name: firstCharLowerCase(model.qName),
initializer: `new ${model.qName}()`,
},
],
});
}
generateQueryObjectProps(importContainer, props) {
return props.map((prop) => {
const { odataName } = prop;
const name = this.namingHelper.getQPropName(prop.name);
const isModelType = prop.dataType === "ModelType" /* DataTypes.ModelType */ || prop.dataType === "ComplexType" /* DataTypes.ComplexType */;
const isEnumType = prop.dataType === "EnumType" /* DataTypes.EnumType */;
const isNumericEnum = this.options.numericEnums;
let qPathInit;
// factor in collections
if (prop.isCollection) {
const qPath = importContainer.addQObject(isModelType
? QueryObjectImports.QEntityCollectionPath
: isEnumType
? isNumericEnum
? QueryObjectImports.QNumericEnumCollectionPath
: QueryObjectImports.QEnumCollectionPath
: QueryObjectImports.QCollectionPath);
const qObject = isModelType
? importContainer.addGeneratedQObject(prop.fqType, prop.qObject)
: isEnumType
? importContainer.addGeneratedModel(prop.fqType, prop.type, false)
: importContainer.addQObject(prop.qObject);
qPathInit = `new ${qPath}(this.withPrefix("${odataName}"), ${isEnumType ? qObject : `() => ${qObject}`})`;
}
else {
// add import for data type
const qPath = importContainer.addQObject(prop.qPath);
if (isModelType) {
const qObject = importContainer.addGeneratedQObject(prop.fqType, prop.qObject);
qPathInit = `new ${qPath}(this.withPrefix("${odataName}"), () => ${qObject})`;
}
else if (isEnumType) {
const qObject = importContainer.addGeneratedModel(prop.fqType, prop.type, false);
qPathInit = `new ${qPath}(this.withPrefix("${odataName}"), ${qObject})`;
}
else {
let converterStmt = this.generateConverterStmt(importContainer, prop.converters);
qPathInit = `new ${qPath}(this.withPrefix("${odataName}")${converterStmt ? `, ${converterStmt}` : ""})`;
}
}
return {
name,
scope: Scope.Public,
isReadonly: true,
initializer: qPathInit,
};
});
}
generateConverterStmt(importContainer, converters) {
if (!(converters === null || converters === void 0 ? void 0 : converters.length)) {
return undefined;
}
const converterIds = converters.map((converter) => {
return importContainer.addCustomType(converter.package, converter.converterId);
});
if (converterIds.length === 1) {
return converterIds[0];
}
else {
const createChain = importContainer.addCustomType("@odata2ts/converter-runtime", "createChain");
const [first, second, ...moreConverters] = converterIds;
return moreConverters.reduce((stmt, convId) => `${stmt}.chain(${convId})`, `${createChain}(${first}, ${second})`);
}
}
generateIdFunction(file, model) {
const importContainer = file.getImports();
const qFunc = importContainer.addQObject(QueryObjectImports.QId);
const idModelName = importContainer.addGeneratedModel(model.fqName, model.id.modelName);
file.getFile().addClass({
name: model.id.qName,
isExported: true,
extends: `${qFunc}<${idModelName}>`,
properties: [
{
name: "params",
scope: Scope.Private,
isReadonly: true,
initializer: this.getParamInitString(importContainer, model.keys),
},
],
methods: [
{
name: "getParams",
statements: ["return this.params"],
},
],
});
}
getParamInitString(importContainer, props, overloads) {
const allParams = [props, ...(overloads !== null && overloads !== void 0 ? overloads : [])].map((paramSet) => {
const pString = paramSet
.map((prop) => {
var _a;
let complexQParam = "";
if (prop.dataType === "ModelType" /* DataTypes.ModelType */ || prop.dataType === "ComplexType" /* DataTypes.ComplexType */) {
const importedQObject = importContainer.addGeneratedQObject(prop.fqType, prop.qObject);
complexQParam = `, new ${importedQObject}()`;
}
const qParam = importContainer.addQObject(prop.qParam);
const isMappedNameNecessary = prop.odataName !== prop.name;
const mappedName = isMappedNameNecessary
? `"${prop.name}"`
: ((_a = prop.converters) === null || _a === void 0 ? void 0 : _a.length)
? "undefined"
: undefined;
const converterStmt = this.generateConverterStmt(importContainer, prop.converters);
const mappedNameParam = mappedName ? `, ${mappedName}` : "";
const converterParam = converterStmt ? `, ${converterStmt}` : "";
return `new ${qParam}("${prop.odataName}"${complexQParam}${mappedNameParam}${converterParam})`;
})
.join(",");
return `[${pString}]`;
});
return allParams.length === 1 ? allParams[0] : `[${allParams.join(",")}]`;
}
generateUnboundOperations() {
return __awaiter(this, void 0, void 0, function* () {
const unboundOps = this.dataModel.getUnboundOperationTypes();
const reservedNames = unboundOps.map((op) => op.qName);
const file = this.project.createOrGetMainQObjectFile(reservedNames);
unboundOps.forEach((operation) => {
this.generateOperation(file, operation, "");
});
});
}
generateOperation(file, operation, baseFqName) {
var _a, _b;
const imports = file.getImports();
const isV2 = this.version === ODataVersions.V2;
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);
// imports
const qOp = operation.type === "Action" /* OperationTypes.Action */ ? QueryObjectImports.QAction : QueryObjectImports.QFunction;
const qOperation = imports.addQObject(qOp);
const paramModelName = hasParams ? imports.addGeneratedModel(baseFqName, operation.paramsModelName) : undefined;
let returnTypeOpStmt = "";
if (returnType) {
const collectionSuffix = returnType.isCollection ? "_COLLECTION" : "";
if (returnType.dataType === "ComplexType" /* DataTypes.ComplexType */ || returnType.dataType === "ModelType" /* DataTypes.ModelType */) {
if (returnType.qObject) {
const opRt = imports.addQObject(QueryObjectImports.OperationReturnType);
const rts = imports.addQObject(QueryObjectImports.ReturnTypes);
const qComplexParam = imports.addQObject(QueryObjectImports.QComplexParam);
const returnQName = imports.addGeneratedQObject(returnType.fqType, returnType.qObject);
returnTypeOpStmt = `new ${opRt}(${rts}.COMPLEX${collectionSuffix}, new ${qComplexParam}("NONE", new ${returnQName}))`;
}
}
// currently, it only makes sense to add the OperationReturnType if a converter is present
else if (returnType.converters && returnType.qParam) {
const rtClass = imports.addQObject(QueryObjectImports.OperationReturnType);
const rtTypes = imports.addQObject(QueryObjectImports.ReturnTypes);
const qParam = imports.addQObject(returnType.qParam);
// TODO: some constants with string concat
const rtKind = `${rtTypes}.VALUE${collectionSuffix}`;
const converterParam = returnType.converters
? ", " + this.generateConverterStmt(imports, returnType.converters)
: "";
returnTypeOpStmt = `new ${rtClass}(${rtKind}, new ${qParam}("NONE", undefined${converterParam}))`;
}
}
file.getFile().addClass({
name: operation.qName,
isExported: true,
extends: qOperation + (hasParams ? `<${paramModelName}>` : ""),
properties: [
{
name: "params",
scope: Scope.Private,
isReadonly: true,
type: hasParams ? undefined : "[]",
initializer: this.getParamInitString(imports, operation.parameters, operation.overrides),
},
],
ctors: [
{
statements: [
`super("${operation.odataName}"${returnTypeOpStmt ? ", " + returnTypeOpStmt : isV2 ? ", undefined" : ""}${isV2 ? ", { v2Mode: true }" : ""})`,
],
},
],
methods: [
{
name: "getParams",
statements: ["return this.params"],
},
// functions without params: add an overriding buildUrl() to not force users to have to pass undefined as param
...(operation.type === "Function" /* OperationTypes.Function */ && !hasParams
? [
{
name: "buildUrl",
parameters: [
{
name: "notEncoded",
initializer: "false",
},
],
statements: ["return super.buildUrl(undefined, notEncoded)"],
},
]
: []),
],
});
}
}
//# sourceMappingURL=QueryObjectGenerator.js.map