@odata2ts/odata2ts
Version:
Flexible generator to produce various TypeScript artefacts (from simple model interfaces to complete odata clients) from OData metadata files
431 lines • 21.7 kB
JavaScript
import { __awaiter } from "tslib";
import { Modes, } from "../OptionModel.js";
import { DataModel, withNamespace } from "./DataModel.js";
import { ServiceConfigHelper } from "./ServiceConfigHelper.js";
import { NameClashValidator } from "./validation/NameClashValidator.js";
import { NoopValidator } from "./validation/NoopValidator.js";
function ifTrue(value) {
return value === "true";
}
function ifFalse(value) {
return value === "false";
}
export class Digester {
constructor(version, schemas, options, namingHelper, converters) {
this.version = version;
this.schemas = schemas;
this.options = options;
this.namingHelper = namingHelper;
/**
* Reverse mapping from fqName to data type: EntityType, ComplexType, EnumType, or Primitive Type.
*/
this.model2Type = new Map();
this.mapProp = (p, entityPropConfig) => {
var _a, _b;
if (!p.$.Type) {
throw new Error(`No type information given for property [${p.$.Name}]!`);
}
const configProp = this.serviceConfigHelper.findPropConfigByName(p.$.Name);
const modelName = this.namingHelper.getModelPropName((entityPropConfig === null || entityPropConfig === void 0 ? void 0 : entityPropConfig.mappedName) || (configProp === null || configProp === void 0 ? void 0 : configProp.mappedName) || p.$.Name);
const isCollection = !!p.$.Type.match(/^Collection\(/);
let odataDataType = p.$.Type.replace(/^Collection\(([^\)]+)\)/, "$1");
// support for primitive type mapping
if (this.namingHelper.includesServicePrefix(odataDataType)) {
const dt = this.dataModel.getPrimitiveType(odataDataType);
if (dt !== undefined) {
odataDataType = dt;
}
}
let result;
// domain object known from service:
// EntityType, ComplexType, EnumType
if (this.namingHelper.includesServicePrefix(odataDataType)) {
const modelType = this.model2Type.get(odataDataType);
const [dataTypeName, dataTypePrefix] = this.namingHelper.getNameAndServicePrefix(odataDataType);
const dataTypeNamespace = [dataTypePrefix];
if (!modelType) {
throw new Error(`Couldn't determine model type (EntityType, ComplexType, etc) for property "${p.$.Name}"! Given data type: "${odataDataType}".`);
}
// special handling for enums
if (modelType === "EnumType" /* DataTypes.EnumType */) {
const enumConfig = this.serviceConfigHelper.findEnumTypeConfig(dataTypeNamespace, dataTypeName);
result = {
dataType: modelType,
type: this.namingHelper.getEnumName((_a = enumConfig === null || enumConfig === void 0 ? void 0 : enumConfig.mappedName) !== null && _a !== void 0 ? _a : odataDataType),
qPath: this.options.numericEnums ? "QNumericEnumPath" : "QEnumPath",
qObject: isCollection
? this.options.numericEnums
? "QNumericEnumCollection"
: "QEnumCollection"
: undefined,
qParam: this.options.numericEnums ? "QNumericEnumParam" : "QEnumParam",
};
}
// handling of complex & entity types
else {
const entityConfig = modelType === "ComplexType" /* DataTypes.ComplexType */
? this.serviceConfigHelper.findComplexTypeConfig(dataTypeNamespace, dataTypeName)
: this.serviceConfigHelper.findEntityTypeConfig(dataTypeNamespace, dataTypeName);
const typeName = (_b = entityConfig === null || entityConfig === void 0 ? void 0 : entityConfig.mappedName) !== null && _b !== void 0 ? _b : odataDataType;
result = {
dataType: modelType,
type: this.namingHelper.getModelName(typeName),
qPath: "QEntityPath",
qObject: this.namingHelper.getQName(typeName),
qParam: "QComplexParam",
};
}
}
// OData built-in data types
else if (odataDataType.startsWith(Digester.EDM_PREFIX)) {
const { outputType, qPath, qParam, qCollection } = this.mapODataType(odataDataType);
const { to, toModule: typeModule, converters } = this.dataModel.getConverter(odataDataType) || {};
const type = !to ? outputType : to.startsWith(Digester.EDM_PREFIX) ? this.mapODataType(to).outputType : to;
result = {
dataType: "PrimitiveType" /* DataTypes.PrimitiveType */,
type,
typeModule,
qPath,
qParam,
qObject: isCollection ? qCollection : undefined,
converters,
};
}
else {
throw new Error(`Unknown type [${odataDataType}]: Not 'Collection(...)', not OData type 'Edm.*', not starting with one of the namespaces!`);
}
return Object.assign({ odataName: p.$.Name, name: modelName, odataType: p.$.Type, fqType: odataDataType, required: ifFalse(p.$.Nullable), isCollection: isCollection, managed: typeof (entityPropConfig === null || entityPropConfig === void 0 ? void 0 : entityPropConfig.managed) !== "undefined" ? entityPropConfig.managed : configProp === null || configProp === void 0 ? void 0 : configProp.managed }, result);
};
const namespaces = schemas.map((s) => [s.$.Namespace, s.$.Alias]);
this.dataModel = new DataModel(namespaces, version, converters);
this.serviceConfigHelper = new ServiceConfigHelper(options);
this.nameValidator = options.bundledFileGeneration ? new NameClashValidator(options) : new NoopValidator();
this.collectModelTypes(schemas);
}
collectModelTypes(schemas) {
schemas.forEach((schema) => {
var _a, _b, _c;
const { Namespace: ns, Alias: alias } = schema.$;
(_a = schema.EnumType) === null || _a === void 0 ? void 0 : _a.forEach((et) => {
this.addModel2Type(ns, alias, et.$.Name, "EnumType" /* DataTypes.EnumType */);
});
(_b = schema.ComplexType) === null || _b === void 0 ? void 0 : _b.forEach((ct) => {
this.addModel2Type(ns, alias, ct.$.Name, "ComplexType" /* DataTypes.ComplexType */);
});
(_c = schema.EntityType) === null || _c === void 0 ? void 0 : _c.forEach((et) => {
this.addModel2Type(ns, alias, et.$.Name, "ModelType" /* DataTypes.ModelType */);
});
});
}
addModel2Type(ns, alias, name, dt) {
this.model2Type.set(withNamespace(ns, name), dt);
if (alias) {
this.model2Type.set(withNamespace(alias, name), dt);
}
}
digest() {
return __awaiter(this, void 0, void 0, function* () {
this.digestEntityTypesAndOperations();
// delegate to version specific entity container digestion
this.schemas.forEach((schema) => this.digestEntityContainer(schema));
this.dataModel.setNameValidation(this.nameValidator.validate());
return this.dataModel;
});
}
digestEntityTypesAndOperations() {
this.schemas.forEach((schema) => {
const ns = [schema.$.Namespace, schema.$.Alias];
// type definitions: alias for primitive types
this.addTypeDefinition(schema.$.Namespace, schema.TypeDefinition);
// enums
this.addEnum(ns, schema.EnumType);
// complex types
this.addComplexType(ns, schema.ComplexType);
// entity types
this.addEntityType(ns, schema.EntityType);
// V4 only: function & action types
this.digestOperations(schema);
});
this.postProcessModel();
this.postProcessKeys();
this.schemas.forEach((schema) => {
var _a;
this.analyzeModelUsage(((_a = schema.EntityContainer) === null || _a === void 0 ? void 0 : _a.length) ? schema.EntityContainer[0] : undefined);
});
}
getBaseModel(entityConfig, model, namespace, name, fqName) {
var _a, _b;
const odataName = model.$.Name;
// map properties respecting the config
const props = [...((_a = model.Property) !== null && _a !== void 0 ? _a : []), ...this.getNavigationProps(model)].map((p) => {
var _a;
const epConfig = (_a = entityConfig === null || entityConfig === void 0 ? void 0 : entityConfig.properties) === null || _a === void 0 ? void 0 : _a.find((ep) => ep.name === p.$.Name);
return this.mapProp(p, epConfig);
});
// support for base types, i.e. extends clause of interfaces
const baseClasses = [];
let finalBaseClass = undefined;
if (model.$.BaseType) {
baseClasses.push(model.$.BaseType);
const [baseName, basePrefix] = this.namingHelper.getNameAndServicePrefix(model.$.BaseType);
const baseConfig = this.serviceConfigHelper.findEntityTypeConfig([basePrefix], baseName) ||
this.serviceConfigHelper.findComplexTypeConfig([basePrefix], baseName);
finalBaseClass = (_b = baseConfig === null || baseConfig === void 0 ? void 0 : baseConfig.mappedName) !== null && _b !== void 0 ? _b : baseName;
}
return {
fqName,
odataName,
name,
modelName: this.namingHelper.getModelName(name),
qName: this.namingHelper.getQName(name),
editableName: this.namingHelper.getEditableModelName(name),
serviceName: this.namingHelper.getServiceName(name),
serviceCollectionName: this.namingHelper.getCollectionServiceName(name),
folderPath: this.namingHelper.getFolderPath(namespace, name),
baseClasses,
finalBaseClass,
props,
baseProps: [],
abstract: ifTrue(model.$.Abstract),
open: ifTrue(model.$.OpenType),
genMode: Modes.qobjects,
subtypes: new Set(),
};
}
addTypeDefinition(ns, types) {
if (!types || !types.length) {
return;
}
for (const t of types) {
this.dataModel.addTypeDefinition(ns, t.$.Name, t.$.UnderlyingType);
}
}
addEnum(namespace, models) {
var _a;
if (!models || !models.length) {
return;
}
for (const et of models) {
const odataName = et.$.Name;
const fqName = withNamespace(namespace[0], odataName);
const config = this.serviceConfigHelper.findEnumTypeConfig(namespace, odataName);
const enumName = this.nameValidator.addEnumType(fqName, (config === null || config === void 0 ? void 0 : config.mappedName) || odataName);
const filePath = this.namingHelper.getFolderPath(namespace[0], enumName);
this.dataModel.addEnum(namespace[0], odataName, {
fqName,
odataName,
name: enumName,
modelName: this.namingHelper.getEnumName(enumName),
folderPath: filePath,
members: ((_a = et.Member) === null || _a === void 0 ? void 0 : _a.length) ? et.Member.map((m) => ({ name: m.$.Name, value: m.$.Value })) : [],
});
}
}
addComplexType(namespace, models) {
if (!models || !models.length) {
return;
}
for (const model of models) {
const config = this.serviceConfigHelper.findComplexTypeConfig(namespace, model.$.Name);
const fqName = withNamespace(namespace[0], model.$.Name);
const name = this.nameValidator.addComplexType(fqName, (config === null || config === void 0 ? void 0 : config.mappedName) || model.$.Name);
const baseModel = this.getBaseModel(config, model, namespace[0], name, fqName);
this.dataModel.addComplexType(namespace[0], baseModel.odataName, baseModel);
}
}
addEntityType(namespace, models) {
var _a;
if (!models || !models.length) {
return;
}
for (const model of models) {
const entityConfig = this.serviceConfigHelper.findEntityTypeConfig(namespace, model.$.Name);
const fqName = withNamespace(namespace[0], model.$.Name);
const name = this.nameValidator.addEntityType(fqName, (entityConfig === null || entityConfig === void 0 ? void 0 : entityConfig.mappedName) || model.$.Name);
const baseModel = this.getBaseModel(entityConfig, model, namespace[0], name, fqName);
// key support: we add keys from this entity,
// but not keys stemming from base classes (postprocess required)
const keyNames = [];
if ((_a = entityConfig === null || entityConfig === void 0 ? void 0 : entityConfig.keys) === null || _a === void 0 ? void 0 : _a.length) {
keyNames.push(...entityConfig.keys);
}
else {
const entity = model;
if (entity.Key && entity.Key.length && entity.Key[0].PropertyRef.length) {
const propNames = entity.Key[0].PropertyRef.map((key) => key.$.Name);
keyNames.push(...propNames);
}
}
this.dataModel.addEntityType(namespace[0], baseModel.odataName, Object.assign(Object.assign({}, baseModel), { id: {
fqName: baseModel.fqName,
modelName: this.namingHelper.getIdModelName(name),
qName: this.namingHelper.getQIdFunctionName(name),
}, generateId: !!keyNames.length, keyNames: keyNames, keys: [], getKeyUnion: () => keyNames.join(" | "), subtypes: new Set() }));
}
}
/**
* Check that models (ComplexType or EntityType) have been referenced in the API
* as entry point or via navProp or by virtue of being a base type or subtype of those.
* For these models one or two services are generated.
*
* In this way unnecessary service generation is prevented. For example, complex types that
* are only referenced as response of an operation do not need a generated service.
*
* @param ec
* @private
*/
analyzeModelUsage(ec) {
var _a, _b;
if ((_a = ec === null || ec === void 0 ? void 0 : ec.EntitySet) === null || _a === void 0 ? void 0 : _a.length) {
ec.EntitySet.forEach((et) => this.analyze(et.$.EntityType));
}
const ec4 = ec;
if ((_b = ec4 === null || ec4 === void 0 ? void 0 : ec4.Singleton) === null || _b === void 0 ? void 0 : _b.length) {
ec4.Singleton.forEach((singleton) => this.analyze(singleton.$.Type));
}
}
/**
* Check usage of model types within API.
*
* @param fqModelName
* @private
*/
analyze(fqModelName) {
var _a;
// to also resolve aliases the data model needs to be used
const model = (_a = this.dataModel.getEntityType(fqModelName)) !== null && _a !== void 0 ? _a : this.dataModel.getComplexType(fqModelName);
if (!(model === null || model === void 0 ? void 0 : model.fqName) || model.genMode === Modes.service) {
return;
}
model.genMode = Modes.service;
if (model) {
// respect base classes
if (model.baseClasses.length) {
this.analyze(model.baseClasses[0]);
}
// include subtypes since each base class can be cast to its subtypes
model.subtypes.forEach((subtype) => {
this.analyze(subtype);
});
model === null || model === void 0 ? void 0 : model.props.forEach((p) => {
if (p.dataType === "ComplexType" /* DataTypes.ComplexType */ || p.dataType === "ModelType" /* DataTypes.ModelType */) {
this.analyze(p.fqType);
}
});
}
}
postProcessModel() {
// complex types
const complexTypes = this.dataModel.getComplexTypes();
complexTypes.forEach((ct) => {
// build up set of subtypes for each complex type
this.addSubtypes(ct);
// get props & keys from base types
const [baseProps, _, baseAttributes] = this.collectBaseClassPropsAndKeys(ct, []);
const { open } = baseAttributes;
ct.baseProps = baseProps.map((bp) => (Object.assign({}, bp)));
if (open) {
ct.open = true;
}
});
// entity types
const entityTypes = this.dataModel.getEntityTypes();
entityTypes.forEach((et) => {
// build up set of subtypes for each entity type
this.addSubtypes(et);
// get props & keys from base types
const [baseProps, baseKeys, baseAttributes] = this.collectBaseClassPropsAndKeys(et, []);
const { fqIdName, idName, qIdName, open } = baseAttributes;
et.baseProps = baseProps.map((bp) => (Object.assign({}, bp)));
if (!et.keyNames.length && idName) {
et.id = {
fqName: fqIdName,
modelName: idName,
qName: qIdName,
};
et.generateId = false;
}
if (open) {
et.open = open;
}
et.keyNames.unshift(...baseKeys.filter((bk) => !et.keyNames.includes(bk)));
});
}
postProcessKeys() {
const entityTypes = this.dataModel.getEntityTypes();
entityTypes.forEach((et) => {
const isSingleKey = et.keyNames.length === 1;
const props = [...et.baseProps, ...et.props];
et.keys = et.keyNames.map((keyName) => {
const prop = props.find((p) => p.odataName === keyName);
if (!prop) {
throw new Error(`Key with name [${keyName}] not found in props!`);
}
// automatically set key prop to managed, if this is the only key of the given entity
if (prop.managed === undefined) {
prop.managed = !this.options.disableAutoManagedKey && isSingleKey;
}
return prop;
});
});
}
collectBaseClassPropsAndKeys(model, visitedModels) {
if (visitedModels.includes(model.fqName)) {
throw new Error(`Cyclic inheritance detected for model ${model.fqName}!`);
}
visitedModels.push(model.fqName);
return model.baseClasses.reduce(([props, keys, attributes], bc) => {
var _a;
const baseModel = this.dataModel.getEntityType(bc) || this.dataModel.getComplexType(bc);
if (!baseModel) {
throw new Error(`BaseModel "${bc}" doesn't exist!`);
}
let { fqIdName, idName, qIdName, open } = attributes;
// recursive
if (baseModel.baseClasses.length) {
const [parentProps, parentKeys, parentAttributes] = this.collectBaseClassPropsAndKeys(baseModel, visitedModels);
props.unshift(...parentProps);
keys.unshift(...parentKeys);
if (parentAttributes === null || parentAttributes === void 0 ? void 0 : parentAttributes.idName) {
fqIdName = parentAttributes.fqIdName;
idName = parentAttributes.idName;
qIdName = parentAttributes.qIdName;
}
if (parentAttributes === null || parentAttributes === void 0 ? void 0 : parentAttributes.open) {
open = true;
}
}
props.push(...baseModel.props);
const entityModel = baseModel;
if ((_a = entityModel.keyNames) === null || _a === void 0 ? void 0 : _a.length) {
keys.push(...entityModel.keyNames.filter((kn) => !keys.includes(kn)));
fqIdName = entityModel.id.fqName;
idName = entityModel.id.modelName;
qIdName = entityModel.id.qName;
}
if (baseModel.open) {
open = true;
}
return [props, keys, { fqIdName, idName, qIdName, open }];
}, [[], [], { fqIdName: "", idName: "", qIdName: "", open: false }]);
}
addSubtypes(model, grandChildren = new Set()) {
if (!model.baseClasses.length) {
return;
}
model.baseClasses.forEach((baseClass) => {
const baseType = this.dataModel.getModel(baseClass);
// add subtypes & base name for q-objects
baseType.subtypes.add(model.fqName);
if (!baseType.qBaseName) {
baseType.qBaseName = this.namingHelper.getQBaseName(baseType.name);
}
grandChildren.forEach((gc) => baseType.subtypes.add(gc));
// recursive
grandChildren.add(model.fqName);
this.addSubtypes(baseType, grandChildren);
});
}
}
Digester.EDM_PREFIX = "Edm.";
//# sourceMappingURL=DataModelDigestion.js.map