pailingual-odata
Version:
TypeScript client for OData v4 services
258 lines (240 loc) • 9.62 kB
text/typescript
import { EdmTypes, EdmEntityType, ApiMetadata, EdmEnumType } from "./metadata";
import { Options } from "./options";
import { startsWith, endsWith } from "./utils";
const guidRE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
type Converter = {
fromEdm?: (v: any, options: Options) => any,
toEdm?: (v: any, forUri: boolean, options: Options) => any
};
var converters: Record<string, Converter> = {
"Edm.String": {
toEdm: (v: string, forUri: boolean) => forUri ? ("'" + v.toString().replace("'", "''").replace("/", "%2F") + "'") : v
},
"Edm.Guid": {
toEdm: (v: string) => {
if (v.match(guidRE)) return v;
throw new Error(`Value '${v}' not parsed as Guid`)
}
},
"Edm.DateTimeOffset": {
toEdm: (v: Date) => v.toISOString(),
fromEdm: (v: string) => new Date(v)
},
"Edm.Boolean": {
toEdm: (v: boolean) => v ? "true" : "false"
}
}
type Formatter = {
contentType: string,
serialize: (data: object, metadata: EdmEntityType, options: Options) => string,
deserialize: (data: string, apiMetadata: ApiMetadata, options: Options) => any
};
var formatters: Record<string, Formatter> = {}
export function addFormatter(formatter: Formatter) {
if (!formatter
|| !formatter.contentType
|| !formatter.serialize
|| !formatter.deserialize)
throw new Error("All formatter properties is required");
formatters[formatter.contentType] = formatter;
}
export function getFormatter(contentType: string): Formatter {
const f = formatters[contentType];
if (!f)
throw new Error(`Not supported format: ${contentType}`);
return f;
}
addFormatter({ contentType: "application/json", serialize: jsonSerialize, deserialize: jsonDeserialize });
function enumMemberByValue(type: EdmEnumType, value: number) : string | undefined {
for (let member in type.members)
if (type.members[member] === value)
return member;
}
function jsonSerialize(payload: any, metadata: EdmEntityType, options: Options = {}) {
let metadataMap = new MapObjToEntityType();
metadataMap.set(payload, metadata);
let otMetadata = new EdmEntityType("$$~~openType~~$$", {});
otMetadata.openType = true;
return JSON.stringify(
payload,
function (this: typeof payload, k, v) {
if (!k || Array.isArray(this))
return v;
let currentMetadata = metadataMap.get(this);
if (currentMetadata != null) {
const propMD = currentMetadata.properties[k]
|| currentMetadata.navProperties[k];
const valueType = propMD && propMD.type
|| ((currentMetadata.openType) ? otMetadata : undefined);
if (!valueType) {
throw new Error(`Property '${k}' not found in metadata`);
}
if (valueType instanceof EdmEntityType)
{
if (Array.isArray(v))
for (let item of v)
metadataMap.set(item, valueType);
else if (v != null)
metadataMap.set(v, valueType);
return v;
}
else {
return convertToEdmValue(this[k], propMD.type as EdmTypes, false) || v;
}
}
throw new Error("Metadata for object not found");
}
)
}
export function serializeValue(value: any, type: EdmTypes | EdmEnumType, forUri: boolean, opt?: Options): string | null {
if (value == null)
return forUri ? "null" : null;
return convertToEdmValue(value, type, forUri, opt) || value.toString();
}
function convertToEdmValue(value: any, type: EdmTypes | EdmEnumType, forUri: boolean, opt: Options = {}) {
if (value == null)
return null;
if (type instanceof EdmEnumType) {
let member = enumMemberByValue(type, value);
if (member) {
if (forUri) {
member = "'" + member + "'";
if (!opt.enumPrefixFree)
member = type.getFullName() + member;
}
return member;
}
throw new Error(`Value '${value}' not found in enum '${type.name} '`)
}
else {
const converter = converters[type as string];
if (converter && converter.toEdm)
return converter.toEdm(value, forUri, opt);
return null;
}
}
function convertFromEdmValue(value: any, type: EdmTypes, options: Options) {
const converter = converters[type as string];
if (converter && converter.fromEdm)
return converter.fromEdm(value, options);
return null;
}
const ODATA_CONTEXT = "@odata.context";
const ODATA_COUNT = "@odata.count";
const ODATA_TYPE = "@odata.type";
function jsonDeserialize(response: string, apiMetadata: ApiMetadata, options: Options) {
const rawData = JSON.parse(response);
let context: string = rawData[ODATA_CONTEXT]
if (context) {
context = context.split("#")[1];
let count = rawData[ODATA_COUNT];
let isEntity = endsWith(context, "$entity");
context = context.replace(/\/\$entity$/, "");
let type = getSourceType(context, apiMetadata)
if (!isEntity && Array.isArray(rawData.value)) {
const value = rawData.value.map((v: any) => convertObj(v, type, apiMetadata, options));
if (count != null)
return { count, value };
return value;
}
else
return convertObj(rawData, type, apiMetadata, options)
}
}
function convertObj(obj: any, type: EdmTypes | EdmEntityType | EdmEnumType, apiMetadata: ApiMetadata, options: Options): any {
if (obj != null) {
if (Array.isArray(obj))
return obj.map(v => convertObj(v, type, apiMetadata, options))
if (obj[ODATA_TYPE]) {
const typeName = (obj[ODATA_TYPE] as string).substr(1);
type = apiMetadata.getEdmTypeMetadata(typeName);
if (type == null)
throw new Error(`Metadata for type '${typeName}' not found.`);
}
if (typeof type == "string") {
if (typeof obj == "object")
obj = obj.value;
return convertFromEdmValue(obj, type as EdmTypes, options) || obj;
}
else
{
let res: any
if (type instanceof EdmEnumType) {
const res = type.members[obj];
if (!res)
throw new Error(`Member '${obj}' not found in enum '${type.name}'`);
return res;
}
else {
const entityType = type as EdmEntityType;
res = {};
const edmProps = getEdmProperties(entityType);
for (let propName in obj) {
const edmPropertyType = edmProps[propName];
if (edmPropertyType)
res[propName] = convertObj(obj[propName], edmPropertyType, apiMetadata, options)
else if (!startsWith(propName,"@")
&& entityType.openType == true)
res[propName] = obj[propName];
}
}
return res;
}
}
return null;
}
function getEdmProperties(type: EdmEntityType): Record<string, EdmTypes | EdmEntityType | EdmEnumType> {
const res: Record<string, EdmTypes | EdmEntityType | EdmEnumType>
= type.baseType
? getEdmProperties(type.baseType)
: {};
for (let prop in type.properties) {
res[prop] = type.properties[prop].type!;
}
for (let prop in type.navProperties) {
res[prop] = type.navProperties[prop].type!;
}
return res;
}
function getSourceType(source: string, apiMetadata: ApiMetadata) {
if (startsWith(source,"Collection"))
source = source.substring("Collection".length + 1, source.length - 1);
const pos = source.indexOf("(");
if (pos > -1)
source = source.substring(0, pos);
const parts = source.split("/");
source = parts[parts.length - 1];
const es = apiMetadata.entitySets[source];
if (!es) {
const dotPos = source.lastIndexOf('.');
if (dotPos > -1) {
const ns = source.substr(0, dotPos);
const type = source.substr(dotPos+1);
if (ns == "Edm") //Primitive type
return source as EdmTypes;
const nsObj = apiMetadata.namespaces[ns];
if (nsObj && nsObj.types[type])
return nsObj.types[type];
}
}
return es;
}
class MapObjToEntityType{
private __keys: object[] = [];
private __values: EdmEntityType[] = [];
get(key: object): EdmEntityType | undefined {
if (key) {
const index = this.__keys.indexOf(key);
if (index > -1)
return this.__values[index];
}
}
set(key: object, entityType: EdmEntityType): void {
if (key == null)
throw new Error("Key must be set");
let index = this.__keys.indexOf(key);
if (index == -1)
index = this.__keys.push(key) - 1;
this.__values[index] = entityType;
}
}