UNPKG

pailingual-odata

Version:

TypeScript client for OData v4 services

359 lines (312 loc) 14.4 kB
import { startsWith, endsWith } from "./utils"; import { Options } from "./options"; const COLLECTION_TYPE_PREFIX = "Collection("; export enum EdmTypes { Int32 = "Edm.Int32", Int16 = "Edm.Int16", Boolean = "Edm.Boolean", String = "Edm.String", Single = "Edm.Single", Guid = "Edm.Guid", DateTimeOffset = "Edm.DateTimeOffset", Date="Edm.Date", Double = "Edm.Double", TimeOfDay = "Edm.TimeOfDay", Decimal = "Edm.Decimal", Unknown="Unknown" } export class EdmEntityType { namespace?: Namespace; constructor( public name: string, public properties: Record<string, EdmTypeReference>, public navProperties: Record<string, EdmEntityTypeReference> = {}, public keys?: string[], public baseType?: EdmEntityType, public openType?: boolean ) { } getFullName = () => getFullName(this); } export class EdmComplexType extends EdmEntityType { } export class EdmEnumType { namespace?: Namespace; constructor( public name: string, public members: Record<string, string|number> ) { } getFullName = () => getFullName(this); } export class EdmEntityTypeReference { constructor( public type: EdmEntityType, public nullable = true, public collection = false ) { } static fromTypeReference(typeReference: EdmTypeReference) { if (typeReference.type instanceof EdmEntityType) { return typeReference as any as EdmEntityTypeReference; } throw new Error("Instance must be reference to EdmEntityType"); } } export class EdmTypeReference { constructor( public type: EdmTypes | EdmEntityType | EdmEnumType, public nullable = true, public collection = false ) { } }; export class OperationMetadata { namespace?: Namespace; constructor( public name: string, public isAction: boolean, public parameters?: { name: string, type: EdmTypeReference}[], public returnType?: EdmTypeReference, public bindingTo?: EdmEntityTypeReference ) { } getFullName = () => getFullName(this); } function getFullName(obj: { namespace?: Namespace, name: string }): string { if (obj.namespace) return [obj.namespace.name, obj.name].join("."); return obj.name; } export class Namespace { operations: OperationMetadata[] = []; types: Readonly<Record<string, EdmEntityType | EdmEnumType >> = { }; constructor(readonly name: string) {} addTypes(...types: (EdmEntityType | EdmEnumType)[]) { for (let type of types) { type.namespace = this; (this.types as any)[type.name] = type; } } addOperations(...operations: OperationMetadata[]) { for (let operation of operations) { operation.namespace = this; this.operations.push(operation); } } }; type Namespaces = Record<string, Namespace>; var __metadataCache: Record<string, Readonly<ApiMetadata>> = {}; export function loadMetadata(apiRoot: string, options?: Options, cache = true): Promise<ApiMetadata> { if (endsWith(apiRoot, "/")) apiRoot = apiRoot.substr(0, apiRoot.length - 1); const normalizedApiRoot = apiRoot.toLowerCase(), res: ApiMetadata = __metadataCache[normalizedApiRoot]; if (res == null || !cache) { return ApiMetadata.loadAsync(apiRoot, options) .then(md => { if (cache) __metadataCache[normalizedApiRoot] = md; return md; }); } return Promise.resolve(res); } export class ApiMetadata { constructor( readonly apiRoot: string, readonly containerName: string, readonly namespaces: Namespaces = {}, readonly entitySets: Record<string, EdmEntityType> = {}, readonly singletons: Record<string, EdmEntityType> = {} ) { } static loadFromXml(apiRoot:string, metadataXml: string) { const parser = new DOMParser(); const metadataDoc = parser.parseFromString(metadataXml, "text/xml"); const namespaces = ApiMetadata.parseEntityTypes(metadataDoc); const entitySets: Record<string, EdmEntityType> = {}; const singletons: Record<string, EdmEntityType> = {}; const container = metadataDoc.querySelector("Schema>EntityContainer")!; const containerName = container && getRequiredAttributeValue(container, "Name"); if (container) { const list = container.querySelectorAll("EntitySet,Singleton"); for (var i = 0; i < list.length; i++) { const e = list.item(i) const isSingleton = e.tagName.toUpperCase() === "SINGLETON"; let name = getRequiredAttributeValue(e, "Name"); let typeName = getRequiredAttributeValue(e, isSingleton ? "Type" : "EntityType"); const target = isSingleton ? singletons : entitySets; target[name] = ApiMetadata.getEntitySetMetadata(typeName, namespaces); } } const list3 = metadataDoc.querySelectorAll("Schema>Function,Schema>Action"); for (var i = 0; i < list3.length; i++){ const e = list3.item(i); const metadata = ApiMetadata.parseOperationMetadata(e, namespaces); const namespaceName = getRequiredAttributeValue(e.parentElement as Element, "Namespace"); if (!namespaces[namespaceName]) namespaces[namespaceName] = new Namespace(namespaceName); namespaces[namespaceName].addOperations(metadata); } return new ApiMetadata(apiRoot, containerName, namespaces, entitySets, singletons); } static async loadAsync(apiRoot: string, options?: Options) { const uri = apiRoot + "/$metadata"; const opt = options || {}; const fetchApi = opt.fetch || fetch const credentials = opt.credentials; const response = await fetchApi(uri, { credentials }); return this.loadFromXml(apiRoot, await response.text()); } private static parseEntityTypes(metadataDoc: Document) { let namespaces = {} as Namespaces; let entityTypes: Array<{ element: Element, typeMetadata: EdmEntityType }> = []; const list = metadataDoc.querySelectorAll("Schema>ComplexType,Schema>EntityType,Schema>EnumType"); for (var i = 0; i < list.length; i++){ const e = list.item(i); const getOrAddEdmEntityType = function(namespace: string, name: string){ let namespaceMD = namespaces[namespace]; if (!namespaceMD) namespaces[namespace] = namespaceMD = new Namespace(namespace); let typeMetadata = namespaceMD.types[name] as EdmEntityType; if (!typeMetadata) { typeMetadata = e.tagName.toLowerCase() == "complextype" ? new EdmComplexType(name, {}) : new EdmEntityType(name, {}); namespaceMD.addTypes(typeMetadata); } return typeMetadata; } let ns = getRequiredAttributeValue(e.parentElement as Element, "Namespace"); let name = getRequiredAttributeValue(e,"Name"); if (!(ns in namespaces)) namespaces[ns] = new Namespace(ns); if (e.tagName.toLowerCase() === "enumtype") { const enumType = new EdmEnumType(name, ApiMetadata.parseEnumMembers(e)); namespaces[ns].addTypes(enumType); } else { let typeMetadata = getOrAddEdmEntityType(ns, name); typeMetadata.openType = getAttributeBoolValue(e,"OpenType"); const baseType = getAttributeValue(e,"BaseType"); if (baseType) { const baseTypeNS = baseType.substring(0, baseType.lastIndexOf(".")); const baseTypeName = baseType.substr(baseTypeNS.length + 1); let bt = getOrAddEdmEntityType(baseTypeNS, baseTypeName); typeMetadata.baseType = bt; } entityTypes.push({ element: e, typeMetadata }); } } for (let e of entityTypes) { Object.assign(e.typeMetadata, ApiMetadata.getEntityTypeProperties(e.element, namespaces)); e.typeMetadata.keys = this.parseEntityKeys(e.element); } return namespaces; } private static parseEntityKeys(typeElement: Element) { var res = new Array<string>(); var list = typeElement.querySelectorAll("Key>PropertyRef"); for (var i = 0; i < list.length; i++) { res.push(getRequiredAttributeValue(list.item(i), "Name")); } return res; } private static parseEnumMembers(element: Element): Record<string, string | number> { const res: Record<string, string | number> = {}; var list = element.querySelectorAll("Member"); for (var i = 0; i < list.length; i++) { const e = list.item(i); const name = getRequiredAttributeValue(e, "Name"); const rawValue = getRequiredAttributeValue(e, "Value"); res[name] = (rawValue.match(/\d/)) ? parseInt(rawValue) : rawValue; } return res; } private static getEntityTypeProperties(typeElement: Element, namespaces: Namespaces) { let properties: Record<string, EdmTypeReference> = {}; let navProperties: Record<string, EdmEntityTypeReference> = {}; var list = typeElement.querySelectorAll("Property,NavigationProperty") for (var i = 0; i < list.length; i++) { const e = list.item(i); const name = getRequiredAttributeValue(e,"Name"); let metadata = ApiMetadata.parseType(e, namespaces); if(e.tagName.toLowerCase() == "property") properties[name] = metadata; else navProperties[name] = EdmEntityTypeReference.fromTypeReference(metadata); } return { properties, navProperties }; } getEntitySetMetadata(typeName: string) { return ApiMetadata.getEntitySetMetadata(typeName, this.namespaces); } private static getEntitySetMetadata(typeName: string, namespaces: Namespaces) { const res = ApiMetadata.getEdmTypeMetadata(typeName, namespaces); if (res instanceof EdmEntityType) return res; throw new Error("EntitySet item type must be entity"); } getEdmTypeMetadata(typeName: string): EdmEntityType | EdmEnumType { return ApiMetadata.getEdmTypeMetadata(typeName, this.namespaces); } private static getEdmTypeMetadata(typeName: string, namespaces: Namespaces): EdmEntityType | EdmEnumType { if (startsWith(typeName,COLLECTION_TYPE_PREFIX)) typeName = typeName.substring(COLLECTION_TYPE_PREFIX.length, typeName.length - 1) const namespace = typeName.substring(0, typeName.lastIndexOf(".")); const typeNameNoNs = typeName.substr(namespace.length + 1); if (namespace == "Edm") { let t = (EdmTypes as any)[typeNameNoNs]; if (!t) throw new Error("Not registred Edm type: " + typeNameNoNs) return t; } const nsMeta = namespaces[namespace]; if (!nsMeta) throw new Error(`Namespace '${namespace}' not found`); let typeElement = nsMeta.types[typeNameNoNs]; return typeElement; } private static parseOperationMetadata(operationElement: Element, namespaces: Namespaces): OperationMetadata { const isAction = operationElement.tagName.toLowerCase() === "action"; const name = getRequiredAttributeValue(operationElement, "Name"); const returnTypeElement = operationElement.querySelector("ReturnType"); const returnType = returnTypeElement ? ApiMetadata.parseType(returnTypeElement, namespaces) : undefined; const parameters = new Array<{ name: string, type: EdmTypeReference }>(); let bindingTo: EdmEntityTypeReference | undefined; const list = operationElement.querySelectorAll("Parameter"); for (var i = 0; i < list.length; i++){ const e = list.item(i); const type = ApiMetadata.parseType(e, namespaces); const name = getRequiredAttributeValue(e, "Name"); if (getAttributeBoolValue(operationElement, "IsBound") && !bindingTo) bindingTo = EdmEntityTypeReference.fromTypeReference(type); else parameters.push({ name, type }); } return new OperationMetadata(name, isAction, parameters, returnType, bindingTo); } private static parseType(element: Element, namespaces: Namespaces) { let typeName = getRequiredAttributeValue(element, "Type"); let collection = startsWith(typeName, COLLECTION_TYPE_PREFIX); if (collection) typeName = typeName.substring(COLLECTION_TYPE_PREFIX.length, typeName.length - 1); const typeMetadata = ApiMetadata.getEdmTypeMetadata(typeName, namespaces); const res = new EdmTypeReference(typeMetadata, true, collection); res.nullable = getAttributeBoolValue(element, "Nullable") != false; return res; } } function getRequiredAttributeValue(element: Element, attrName: string): string { return getAttributeValue(element, attrName) || (() => { throw new Error(`Metadata: Attribute '${attrName}' in element '${element.tagName}' not found `) })(); } function getAttributeBoolValue(element: Element, attrName: string): boolean | undefined { var r = getAttributeValue(element, attrName); if (r != undefined) return r.toLowerCase() == "true"; } function getAttributeValue(element: Element, attrName: string): string | undefined { var attr = element.attributes.getNamedItem(attrName); if (attr) return attr.value; }