UNPKG

pailingual-odata

Version:

TypeScript client for OData v4 services

382 lines (341 loc) 13.3 kB
import { EdmEntityType, OperationMetadata, EdmTypes, ApiMetadata, EdmTypeReference } from "./metadata"; import { getFormatter, serializeValue } from "./serialization"; import { Options } from "./options"; import { expandExpressionBuild, startsWith } from "./utils"; export class Query { protected _segments: Segment[] = []; protected params: QueryParams = {}; protected _method: ODataMethods = "get"; protected _payload: any; private constructor( protected readonly _apiMetadata: ApiMetadata, protected _entityMetadata: EdmEntityType, protected readonly _options?: Options ) { } static create(apiMetadata: ApiMetadata, entityMetadata: EdmEntityType, options: Options | undefined): Query { return new Query(apiMetadata, entityMetadata, options)._freeze(); } private _clone(beforeFreeze: (q: Query) => void): Query { let res = new Query(this._apiMetadata, this._entityMetadata, this._options); res._segments = [...this._segments]; let cloneArray: (a: Array<any> | undefined) => any = (a: any) => a ? [...a] : null; for (const name of Object.getOwnPropertyNames(this.params)) { let val = (this.params as any)[name] if (val != undefined) { (res.params as any)[name] = Array.isArray(val) ? cloneArray(val) : val; } } res._method = this._method; res._payload = this._payload; if (beforeFreeze) beforeFreeze(res); return res._freeze(); } private _freeze(): Query{ //freeze segments this._segments = Object.freeze(this._segments) as any; //freeze params Object.getOwnPropertyNames(this.params).forEach(n => { var v = (this.params as any)[n]; if (v !== null && typeof v === "object") Object.freeze(v); }); this.params = Object.freeze(this.params as any); this._payload = Object.freeze(this._payload); //freze query return Object.freeze(this) as any as Query; } private _action(metadata: OperationMetadata, args: any[]): Query { return this._clone( q => { q._segments.push(new ActionSegment(metadata)); q._method = "post"; (q as any)._entityMetadata = metadata.returnType && metadata.returnType.type as EdmEntityType; if (args && args.length>0) { let payload: Record<string, any> = {}; let edmProps: Record<string, EdmTypeReference> = {} if (metadata.parameters) { for (let i = 0; i < args.length; i++ ) { const param = metadata.parameters[i]; payload[param.name] = args[i]; edmProps[param.name] = param.type; } } q._entityMetadata = new EdmEntityType("", edmProps); q._payload = payload; } } ); } private _func(metadata: OperationMetadata, args: any[]): Query { return this._clone( q => { (q as any)._entityMetadata = metadata.returnType && metadata.returnType.type as EdmEntityType; q._segments.push(new FuncSegment(metadata, args)) } ); } byKey(keyExpr: string): Query { return this._clone(q => { let pos = q._segments.length; if (q._segments[pos-1] instanceof CastSegment) pos-- q._segments.splice(pos, 0, new KeySegment(keyExpr)); }); } cast(fullTypeName: string): Query { return this._clone( q => q._segments.push( new CastSegment(fullTypeName)) ); } navigate(property: string, entityMetadata: EdmEntityType): Query { return this._clone( q => { q._entityMetadata = entityMetadata; q._segments.push(new NavigationSegment(property)); } ); } operation(metadata: OperationMetadata, args: any[]): Query { return metadata.isAction ? this._action(metadata, args) : this._func(metadata, args); } count(o={ inline: false}) { return this._clone( q => { if (o.inline) q.params.count = true; else q._segments.push(new CountSegment()); } ); } delete() { return this._clone( q => { q._method = "delete"; } ); } expand(expand: string, expr?: Function) { return this._clone(q => { let expandParam = q.params.expand == null ? q.params.expand = [] : q.params.expand; expandParam.push({ expand, expr }); }); } filter(expr: string) { return this._clone(q => { let filters = (q.params.filter == null) ? q.params.filter = new Array<string>() : q.params.filter; filters.push(expr); }); } insert(payload: any) { return this._clone(q => { q._payload = payload; q._method = "post"; }); } orderBy(expressions: string[]) { return this._clone(q => { if (expressions) { if (!q.params.orderBy) q.params.orderBy = []; q.params.orderBy.push(...expressions); } }); } search(expr: string) { return this._clone(q => { if (!q.params.search) q.params.search = []; q.params.search.push(expr); }); } select(fields: string[]) { return this._clone(q=> q.params.select = fields); } skip(num: number) { return this._clone(q=>q.params.skip = num); } top(num: number) { return this._clone(q=> q.params.top = num); } update(payload: string, put: boolean) { return this._clone(q => { q._payload = payload; q._method = put ? "put" : "patch"; }) } url(queryParams = true, options?: Options) { const apiRoot = (this._apiMetadata && this._apiMetadata.apiRoot) || ""; const params = queryParams ? this.params : undefined; const opt = Object.assign({}, this._options, options); let url = apiRoot + this._segments .map(s => s.toUrlFragment(opt)) .join(""); if (params) { const urlParams = this.buildParams(opt, '&'); if (urlParams) return url + "?" + urlParams } return url; } buildParams(options: Options, separator = "&") { return Object.getOwnPropertyNames(this.params) .map(n => this.params[n] && this.processParameter(n, this.params[n], options)) .filter(v => v) .join(separator); } processParameter(name: string, value: any, options: Options): string | undefined { switch (name) { case "select": return "$select=" + value.join(","); case "expand": return "$expand=" + value.map((e: ExpandExpr) => this.expandToString(e, options)).join(","); case "filter": return "$filter=" + value.join(" and "); case "orderBy": return "$orderby=" + value.join(","); case "skip": return "$skip=" + value; case "top": return "$top=" + value; case "count": return value === true ? "$count=true" : undefined; case "search": return "$search=" + value.join(" AND "); } } private expandToString(e: ExpandExpr, options: Options) { if (e.expr) return expandExpressionBuild(e.expand, e.expr, this._apiMetadata, this._entityMetadata, options); return e.expand; } exec(options: Options | undefined): Promise<any> { var opt = Object.assign({}, this._options, options) as Options; var url = this.url(true,opt); return this._fetchData(url, opt); } private _fetchData(url: string, options: Options) { const fetchApi = (options && options.fetch) || fetch; const inputFormatter = getFormatter(options.format || "application/json"); const body = this._payload? inputFormatter.serialize(this._payload, this._entityMetadata, options):null; return fetchApi( url, { method: this._method, body, headers: { "Content-Type": inputFormatter.contentType, "X-Requested-With":"XMLHttpRequest" }, credentials: options.credentials }) .then(response => new Promise<{ response: Response, body?: string }>( (resolve, reject) => { if (response.body) response.text().then(body => resolve({ response, body }), reject); else resolve({ response }); }) ) .then(data => { const bodyStr = data.body; const response = data.response; let contentType = response.headers.get("Content-Type"); if (response.ok) { if (bodyStr && bodyStr.length>0) { if (!contentType) { if (startsWith(bodyStr, "{")) contentType = "application/json"; } else contentType = contentType.split(";")[0].trim(); const outputFormatter = getFormatter(contentType!); return outputFormatter.deserialize(bodyStr, this._apiMetadata, options); } else return null; } else { try { var odError = bodyStr && JSON.parse(bodyStr); } catch { } let error = (odError && odError.error) || response.statusText; throw { status: response.status, error }; } }); } } type ExpandExpr = { expand: string, expr?: Function } type QueryParams = { [x: string]: any; filter?: string[]; top?: number; skip?: number; orderBy?: string[]; expand?: ExpandExpr[]; select?: string[]; count?: boolean; search?: string[] }; type ODataMethods = "get" | "post" | "put" | "patch" | "delete"; abstract class Segment { abstract toUrlFragment(options: Options): string; } class NavigationSegment extends Segment { constructor(public property: string) { super(); } toUrlFragment(): string { return "/" + this.property; } } class KeySegment extends Segment { constructor(public key: string) { super() } toUrlFragment(): string { return "(" + this.key + ")"; } } class ActionSegment extends Segment { constructor(private readonly _metadata: OperationMetadata) { super(); } toUrlFragment(options: Options): string { const actionName = this._metadata.bindingTo && options.enableUnqualifiedNameCall != true ? this._metadata.getFullName() : this._metadata.name; return "/" + actionName; } } class FuncSegment extends Segment { constructor(private __metadata: OperationMetadata, private __args: any[]) { super(); } toUrlFragment(options: Options): string { const actionName = this.__metadata.bindingTo && options.enableUnqualifiedNameCall != true ? this.__metadata.getFullName() : this.__metadata.name; const serializedArgs = this.__args.map((a, i) => { if (this.__metadata.parameters) { const paramMetadata = this.__metadata.parameters[i]; if (paramMetadata != null) { const v = serializeValue(a, paramMetadata.type.type as EdmTypes, true); return [paramMetadata.name, v].join("="); } } throw new Error(`Parameter '${i}', for function '${actionName}', not defined in metadata`) }) return `/${actionName}(${serializedArgs.join(",")})`; } } class CountSegment extends Segment { toUrlFragment(): string { return "/$count"; } } class CastSegment extends Segment { constructor(private __fullTypeName: string) { super(); } toUrlFragment(): string { return "/" + this.__fullTypeName; } }