@odata2ts/odata2ts
Version:
Flexible generator to produce various TypeScript artefacts (from simple model interfaces to complete odata clients) from OData metadata files
278 lines • 12.2 kB
JavaScript
import { __awaiter } from "tslib";
import { StructureKind } from "ts-morph";
import { CoreImports } from "./import/ImportObjects.js";
export const generateModels = (project, dataModel, version, options, namingHelper) => {
const generator = new ModelGenerator(project, dataModel, version, options, namingHelper);
return generator.generate();
};
class ModelGenerator {
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.initModels();
const promises = [
...this.generateEnums(),
...this.generateEntityTypeModels(),
...this.generateComplexTypeModels(),
];
if (!this.options.skipOperations) {
promises.push(this.generateUnboundOperationParams());
}
yield Promise.all(promises);
return this.project.finalizeModels();
});
}
generateEnums() {
return this.dataModel.getEnums().map((et) => {
const file = this.project.createOrGetModelFile(et.folderPath, et.modelName);
const isNumeric = this.options.numericEnums;
file.getFile().addEnum({
name: et.modelName,
isExported: true,
members: et.members.map((mem) => ({
name: mem.name,
initializer: isNumeric ? String(mem.value) : `"${mem.name}"`,
})),
});
return this.project.finalizeFile(file);
});
}
generateEntityTypeModels() {
return this.dataModel.getEntityTypes().map((model) => {
const file = this.project.createOrGetModelFile(model.folderPath, model.modelName, [
model.modelName,
model.id.modelName,
model.editableName,
]);
// query model
this.generateModel(file, model);
// key model
if (!this.options.skipIdModels && model.generateId) {
this.generateIdModel(file, model);
}
// editable model
if (!this.options.skipEditableModels) {
this.generateEditableModel(file, model);
}
// param models for bound operations
if (!this.options.skipOperations) {
[
...this.dataModel.getEntityTypeOperations(model.fqName),
...this.dataModel.getEntitySetOperations(model.fqName),
].forEach((operation) => {
this.generateOperationParams(file, operation);
});
}
return this.project.finalizeFile(file);
});
}
generateComplexTypeModels() {
return this.dataModel.getComplexTypes().map((model) => {
const file = this.project.createOrGetModelFile(model.folderPath, model.modelName, [
model.modelName,
model.editableName,
]);
// query model
this.generateModel(file, model);
// editable model
if (!this.options.skipEditableModels) {
this.generateEditableModel(file, model);
}
return this.project.finalizeFile(file);
});
}
generateModel(file, model) {
const imports = file.getImports();
let extendsClause = undefined;
if (model.finalBaseClass) {
const modelName = imports.addGeneratedModel(model.baseClasses[0], this.namingHelper.getModelName(model.finalBaseClass));
extendsClause = [modelName];
}
file.getFile().addInterface({
name: model.modelName,
isExported: true,
properties: model.props.map((p) => {
const isEntity = p.dataType == "ModelType" /* DataTypes.ModelType */;
return {
name: p.name,
type: this.getPropType(file.getImports(), p),
// props for entities or entity collections are not added in V4 if not explicitly expanded
hasQuestionToken: this.dataModel.isV4() && isEntity,
docs: this.options.skipComments ? undefined : [this.generatePropDoc(p, model)],
};
}),
extends: extendsClause,
});
}
generatePropDoc(prop, model) {
var _a, _b;
const isKeyProp = (_a = model.keyNames) === null || _a === void 0 ? void 0 : _a.includes(prop.odataName);
const baseAttribs = [];
if (isKeyProp) {
baseAttribs.push("**Key Property**: This is a key property used to identify the entity.");
}
if (prop.managed) {
baseAttribs.push("**Managed**: This property is managed on the server side and cannot be edited.");
}
if ((_b = prop.converters) === null || _b === void 0 ? void 0 : _b.length) {
baseAttribs.push(`**Applied Converters**: ${prop.converters.map((c) => c.converterId).join(",")}.`);
}
const attributeTable = [
["Name", prop.odataName],
["Type", prop.odataType],
];
if (prop.required) {
attributeTable.push(["Nullable", "false"]);
}
const description = (baseAttribs ? baseAttribs.join("<br/>") + "\n\n" : "") +
"OData Attributes:\n" +
"|Attribute Name | Attribute Value |\n| --- | ---|\n" +
attributeTable.map((row) => `| ${row[0]} | \`${row[1]}\` |`).join("\n");
return { kind: StructureKind.JSDoc, description };
}
getPropType(imports, prop) {
// V2 entity special: deferred content
let suffix = "";
if (this.dataModel.isV2() && prop.dataType == "ModelType" /* DataTypes.ModelType */) {
const defContent = imports.addCoreLib(this.version, CoreImports.DeferredContent);
suffix = ` | ${defContent}`;
}
let typeName;
if (prop.dataType === "PrimitiveType" /* DataTypes.PrimitiveType */) {
// custom types which require type imports => possible via converters
typeName = prop.typeModule ? imports.addCustomType(prop.typeModule, prop.type, true) : prop.type;
}
else {
typeName = imports.addGeneratedModel(prop.fqType, prop.type);
}
// Collections
if (prop.isCollection) {
const type = `Array<${typeName}>`;
if (this.dataModel.isV2() && this.options.v2ModelsWithExtraResultsWrapping) {
return `{ results: ${type} }` + suffix;
}
else {
return type + suffix;
}
}
// primitive, enum & complex types
return typeName + (prop.required ? "" : " | null") + suffix;
}
generateIdModel(file, model) {
const singleType = model.keys.length === 1 ? `${model.keys[0].type} | ` : "";
const keyTypes = model.keys
.map((keyProp) => `${keyProp.name}: ${this.getPropType(file.getImports(), keyProp)}`)
.join(",");
const type = `${singleType}{${keyTypes}}`;
file.getFile().addTypeAlias({
name: model.id.modelName,
isExported: true,
type,
});
}
generateEditableModel(file, model) {
const entityTypes = ["ModelType" /* DataTypes.ModelType */, "ComplexType" /* DataTypes.ComplexType */];
const allProps = [...model.baseProps, ...model.props].filter((p) => !p.managed);
const requiredProps = allProps
.filter((p) => p.required && !entityTypes.includes(p.dataType))
.map((p) => `"${p.name}"`)
.join(" | ");
const optionalProps = allProps
.filter((p) => !p.required && !entityTypes.includes(p.dataType))
.map((p) => `"${p.name}"`)
.join(" | ");
const complexProps = allProps.filter((p) => p.dataType === "ComplexType" /* DataTypes.ComplexType */);
const extendsClause = [
requiredProps ? `Pick<${model.modelName}, ${requiredProps}>` : null,
optionalProps ? `Partial<Pick<${model.modelName}, ${optionalProps}>>` : null,
].filter((e) => !!e);
file.getFile().addInterface({
name: model.editableName,
isExported: true,
extends: extendsClause,
properties: !complexProps
? undefined
: complexProps.map((p) => {
return {
name: p.name,
type: this.getEditablePropType(file.getImports(), p),
// optional props don't need to be specified in editable model
// also, entities would require deep insert func => we make it optional for now
hasQuestionToken: !p.required || p.dataType === "ModelType" /* DataTypes.ModelType */,
};
}),
});
}
getEditablePropType(imports, prop) {
const isModelType = ["ModelType" /* DataTypes.ModelType */, "ComplexType" /* DataTypes.ComplexType */].includes(prop.dataType);
let editableType = prop.type;
if (isModelType) {
const editName = this.dataModel.getComplexType(prop.fqType).editableName;
editableType = imports.addGeneratedModel(prop.fqType, editName);
}
// Collections
if (prop.isCollection) {
return `Array<${editableType}>`;
}
// primitive, enum & complex types
return editableType + (prop.required ? "" : " | null");
}
generateUnboundOperationParams() {
return __awaiter(this, void 0, void 0, function* () {
const unboundOps = this.dataModel.getUnboundOperationTypes();
const reservedNames = unboundOps.map((op) => op.paramsModelName);
const file = this.project.createOrGetMainModelFile(reservedNames);
unboundOps.forEach((operation) => {
this.generateOperationParams(file, operation);
});
});
}
generateOperationParams(file, operation) {
var _a;
const paramSets = [operation.parameters, ...((_a = operation.overrides) !== null && _a !== void 0 ? _a : [])].filter((pSet) => !!pSet.length);
// standard: one interface for parameters
if (paramSets.length === 1) {
file.getFile().addInterface({
name: operation.paramsModelName,
isExported: true,
properties: paramSets[0].map((p) => {
return {
name: p.name,
type: this.getPropType(file.getImports(), p),
hasQuestionToken: !p.required,
};
}),
});
}
// function overload: one type with intersections of different param models
else if (paramSets.length > 1) {
file.getFile().addTypeAlias({
name: operation.paramsModelName,
isExported: true,
type: (writer) => {
paramSets.forEach((pSet, index) => {
writer.block(() => {
pSet.forEach((param, index) => {
const paramType = this.getPropType(file.getImports(), param);
writer.write(`${param.name}${param.required ? "" : "?"}: ${paramType}`);
if (index < pSet.length - 1) {
writer.write(",");
}
});
});
if (index < paramSets.length - 1) {
writer.write(" | ");
}
});
},
});
}
}
}
//# sourceMappingURL=ModelGenerator.js.map