UNPKG

angular-odata

Version:

Client side OData typescript library for Angular

1,262 lines (1,254 loc) 511 kB
import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpParams, HttpResponse, HttpErrorResponse, HttpEventType } from '@angular/common/http'; import { Subject, map as map$1, EMPTY, throwError, Observable, forkJoin, of, NEVER } from 'rxjs'; import { map, expand, reduce, finalize, defaultIfEmpty, switchMap, catchError, tap, startWith } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { EventEmitter, Injectable, InjectionToken, makeEnvironmentProviders, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; var PathSegment; (function (PathSegment) { PathSegment["batch"] = "batch"; PathSegment["metadata"] = "metadata"; PathSegment["entitySet"] = "entitySet"; PathSegment["singleton"] = "singleton"; PathSegment["type"] = "type"; PathSegment["property"] = "property"; PathSegment["navigationProperty"] = "navigationProperty"; PathSegment["reference"] = "reference"; PathSegment["value"] = "value"; PathSegment["count"] = "count"; PathSegment["function"] = "function"; PathSegment["action"] = "action"; })(PathSegment || (PathSegment = {})); var QueryOption; (function (QueryOption) { QueryOption["select"] = "select"; QueryOption["expand"] = "expand"; QueryOption["compute"] = "compute"; QueryOption["apply"] = "apply"; QueryOption["filter"] = "filter"; QueryOption["search"] = "search"; QueryOption["transform"] = "transform"; QueryOption["orderBy"] = "orderBy"; QueryOption["top"] = "top"; QueryOption["skip"] = "skip"; QueryOption["skiptoken"] = "skiptoken"; QueryOption["format"] = "format"; QueryOption["levels"] = "levels"; QueryOption["count"] = "count"; })(QueryOption || (QueryOption = {})); var EdmType; (function (EdmType) { //Edm.Guid 16-byte (128-bit) unique identifier EdmType["Guid"] = "Edm.Guid"; //Edm.Int16 Signed 16-bit integer EdmType["Int16"] = "Edm.Int16"; //Edm.String Sequence of UTF-8 characters EdmType["String"] = "Edm.String"; //Edm.Boolean Binary-valued logic EdmType["Boolean"] = "Edm.Boolean"; //Edm.Byte Unsigned 8-bit integer EdmType["Byte"] = "Edm.Byte"; //Edm.SByte Signed 8-bit integer EdmType["SByte"] = "Edm.SByte"; //Edm.Int32 Signed 16-bit integer EdmType["Int32"] = "Edm.Int32"; //Edm.Int64 Signed 16-bit integer EdmType["Int64"] = "Edm.Int64"; //Edm.Date Date without a time-zone offset EdmType["Date"] = "Edm.Date"; //Edm.TimeOfDay Clock time 00:00-23:59:59.999999999999 EdmType["TimeOfDay"] = "Edm.TimeOfDay"; //Edm.DateTimeOffset Date and time with a time-zone offset, no leap seconds EdmType["DateTimeOffset"] = "Edm.DateTimeOffset"; //Edm.Duration Signed duration in days, hours, minutes, and (sub)seconds EdmType["Duration"] = "Edm.Duration"; //Edm.Decimal Numeric values with fixed precision and scale EdmType["Decimal"] = "Edm.Decimal"; //Edm.Double IEEE 754 binary64 floating-point number (15-17 decimal digits) EdmType["Double"] = "Edm.Double"; //Edm.Single IEEE 754 binary32 floating-point number (6-9 decimal digits) EdmType["Single"] = "Edm.Single"; //Edm.Binary Binary data EdmType["Binary"] = "Edm.Binary"; //Edm.Stream Binary data stream EdmType["Stream"] = "Edm.Stream"; //Edm.Geography Abstract base type for all Geography types EdmType["Geography"] = "Edm.Geography"; //Edm.GeographyPoint A point in a round-earth coordinate system EdmType["GeographyPoint"] = "Edm.GeographyPoint"; //Edm.GeographyLineString Line string in a round-earth coordinate system EdmType["GeographyLineString"] = "Edm.GeographyLineString"; //Edm.GeographyPolygon Polygon in a round-earth coordinate system EdmType["GeographyPolygon"] = "Edm.GeographyPolygon"; //Edm.GeographyMultiPoint Collection of points in a round-earth coordinate system EdmType["GeographyMultiPoint"] = "Edm.GeographyMultiPoint"; //Edm.GeographyMultiLineString Collection of line strings in a round-earth coordinate system EdmType["GeographyMultiLineString"] = "Edm.GeographyMultiLineString"; //Edm.GeographyMultiPolygon Collection of polygons in a round-earth coordinate system EdmType["GeographyMultiPolygon"] = "Edm.GeographyMultiPolygon"; //Edm.GeographyCollection Collection of arbitrary Geography values EdmType["GeographyCollection"] = "Edm.GeographyCollection"; //Edm.Geometry Abstract base type for all Geometry types EdmType["Geometry"] = "Edm.Geometry"; //Edm.GeometryPoint Point in a flat-earth coordinate system EdmType["GeometryPoint"] = "Edm.GeometryPoint"; //Edm.GeometryLineString Line string in a flat-earth coordinate system EdmType["GeometryLineString"] = "Edm.GeometryLineString"; //Edm.GeometryPolygon Polygon in a flat-earth coordinate system EdmType["GeometryPolygon"] = "Edm.GeometryPolygon"; //Edm.GeometryMultiPoint Collection of points in a flat-earth coordinate system EdmType["GeometryMultiPoint"] = "Edm.GeometryMultiPoint"; //Edm.GeometryMultiLineString Collection of line strings in a flat-earth coordinate system EdmType["GeometryMultiLineString"] = "Edm.GeometryMultiLineString"; //Edm.GeometryMultiPolygon Collection of polygons in a flat-earth coordinate system EdmType["GeometryMultiPolygon"] = "Edm.GeometryMultiPolygon"; //Edm.GeometryCollection Collection of arbitrary Geometry values EdmType["GeometryCollection"] = "Edm.GeometryCollection"; })(EdmType || (EdmType = {})); var JsonType; (function (JsonType) { JsonType["string"] = "string"; JsonType["number"] = "number"; JsonType["integer"] = "integer"; JsonType["object"] = "object"; JsonType["array"] = "array"; JsonType["boolean"] = "boolean"; JsonType["null"] = "null"; })(JsonType || (JsonType = {})); const NONE_PARSER = { deserialize: (value) => value, serialize: (value) => value, encode: (value) => value, }; //#endregion const $ID = '$id'; const ODATA_ID = '@odata.id'; // SEGMENTS const $METADATA = '$metadata'; const $BATCH = '$batch'; const $REF = '$ref'; const $VALUE = '$value'; const $COUNT = '$count'; const $QUERY = '$query'; const $INLINECOUNT = '$inlinecount'; // HTTP HEADERS const IF_MATCH_HEADER = 'If-Match'; const IF_NONE_MATCH_HEADER = 'If-None-Match'; const CONTENT_TYPE = 'Content-Type'; const HTTP11 = 'HTTP/1.1'; const ACCEPT = 'Accept'; const PREFER = 'Prefer'; const CACHE_CONTROL = 'Cache-Control'; const CACHE_CONTROL_HEADERS = [CACHE_CONTROL, CACHE_CONTROL.toLowerCase()]; const ODATA_VERSION = 'OData-Version'; const ODATA_VERSION_HEADERS = [ ODATA_VERSION, ODATA_VERSION.toLowerCase(), 'dataserviceversion', ]; const LOCATION_HEADER = 'Location'; const LOCATION_HEADERS = [LOCATION_HEADER, LOCATION_HEADER.toLowerCase()]; const ODATA_ENTITYID = 'OData-EntityId'; const ODATA_ENTITYID_HEADERS = [ODATA_ENTITYID, ODATA_ENTITYID.toLowerCase()]; const PREFERENCE_APPLIED = 'Preference-Applied'; const PREFERENCE_APPLIED_HEADERS = [PREFERENCE_APPLIED, PREFERENCE_APPLIED.toLowerCase()]; const ETAG_HEADER = 'ETag'; const ETAG_HEADERS = [ETAG_HEADER, ETAG_HEADER.toLowerCase()]; const RETRY_AFTER = 'Retry-After'; const RETRY_AFTER_HEADERS = [RETRY_AFTER, RETRY_AFTER.toLowerCase()]; // HTTP HEADER VALUES const APPLICATION_JSON = 'application/json'; const APPLICATION_HTTP = 'application/http'; const APPLICATION_XHTML = 'application/xhtml+xml'; const APPLICATION_XML = 'application/xml'; const TEXT_PLAIN = 'text/plain'; const CONTENT_TYPE_ANY = '*/*'; const MULTIPART_MIXED = 'multipart/mixed'; const MULTIPART_MIXED_BOUNDARY = 'multipart/mixed;boundary='; const CONTENT_TRANSFER_ENCODING = 'Content-Transfer-Encoding'; const CONTENT_ID = 'Content-ID'; const MAX_AGE = 'max-age'; // VERSIONS const VERSION_4_0 = '4.0'; const VERSION_3_0 = '3.0'; const VERSION_2_0 = '2.0'; const DEFAULT_VERSION = VERSION_4_0; const BINARY = 'binary'; const BOUNDARY_PREFIX_SUFFIX = '--'; const BATCH_PREFIX = 'batch_'; const CHANGESET_PREFIX = 'changeset_'; const DEFAULT_METADATA = 'minimal'; const DEFAULT_STRIP_METADATA = 'full'; const DEFAULT_FETCH_POLICY = 'network-only'; const DEFAULT_TIMEOUT = 60; // Time in seconds const CALLABLE_BINDING_PARAMETER = 'bindingParameter'; const XSSI_PREFIX = /^\)\]\}',?\n/; // URL PARTS const QUERY_SEPARATOR = '?'; const PARAM_SEPARATOR = '&'; const VALUE_SEPARATOR = '='; const PATH_SEPARATOR = '/'; const ODATA_PARAM_PREFIX = '$'; const ODATA_ALIAS_PREFIX = '@'; const NEWLINE = '\r\n'; const NEWLINE_REGEXP = /\r?\n/; const CACHE_KEY_SEPARATOR = ':'; // Models const CID_FIELD_NAME = '_cid'; const EVENT_SPLITTER = /\s+/; // Standard vocabularies for annotating OData services // https://github.com/oasis-tcs/odata-vocabularies/blob/main/vocabularies/Org.OData.Core.V1.md const COMPUTED = /.*Computed$/; const OPTIMISTIC_CONCURRENCY = /.*OptimisticConcurrency$/; const DESCRIPTION = /.*Description$/; const LONG_DESCRIPTION = /.*LongDescription$/; const OPTIONARL_PARAMETER = /.*OptionalParameter$/; const COLLECTION = /Collection\(([\w\.]+)\)/; const PROPERTY = /([\w\d\-_]+)\(([\'\w\d\-_=]+)\)/; const EXPAND = /([\w\d\-_]+)\(([\w\d\,\(\)]+)\)/; const ODataVersionBaseHelper = { entity(data) { return data; }, entities(data) { return data[this.VALUE]; }, property(data) { return this.VALUE in data ? data[this.VALUE] : data; }, functions(annots) { const funcs = new Map(); [...annots.keys()] .filter((key) => key.startsWith(this.ODATA_FUNCTION_PREFIX)) .forEach((key) => funcs.set(key.substring(this.ODATA_FUNCTION_PREFIX.length), annots.get(key))); return funcs; }, properties(annots) { const props = new Map(); [...annots.keys()] .filter((key) => key.indexOf(this.ODATA_ANNOTATION_PREFIX) > 0) .forEach((key) => { let name = key.substring(0, key.indexOf(this.ODATA_ANNOTATION_PREFIX)); let prop = props.has(name) ? props.get(name) : new Map(); prop.set(key.substring(key.indexOf(this.ODATA_ANNOTATION_PREFIX)), annots.get(key)); props.set(name, prop); }); return props; }, id(annots) { return annots instanceof Map ? annots.get(this.ODATA_ID) : annots[this.ODATA_ID]; }, etag(annots) { return annots instanceof Map ? annots.get(this.ODATA_ETAG) : annots[this.ODATA_ETAG]; }, type(annots) { let type = annots instanceof Map ? annots.get(this.ODATA_TYPE) : annots[this.ODATA_TYPE]; if (!type) return undefined; type = type.substring(1); const matches = COLLECTION.exec(type); if (matches) return matches[1].indexOf('.') === -1 ? `Edm.${matches[1]}` : matches[1]; return type; }, mediaEtag(annots) { return annots.has(this.ODATA_MEDIA_ETAG) ? decodeURIComponent(annots.get(this.ODATA_MEDIA_ETAG)) : undefined; }, metadataEtag(annots) { return annots.has(this.ODATA_METADATA_ETAG) ? decodeURIComponent(annots.get(this.ODATA_METADATA_ETAG)) : undefined; }, count(annots) { return annots.has(this.ODATA_COUNT) ? Number(annots.get(this.ODATA_COUNT)) : undefined; }, annotations(value) { const annots = new Map(); Object.entries(value) .filter(([key]) => key.indexOf(this.ODATA_ANNOTATION_PREFIX) !== -1 || key.startsWith(this.ODATA_FUNCTION_PREFIX)) .forEach(([key, value]) => annots.set(key, value)); return annots; }, attributes(value, metadata) { return Object.entries(value) .filter(([k]) => metadata === 'none' || (metadata === 'minimal' && (k.indexOf(this.ODATA_ANNOTATION_PREFIX) === -1 || k.startsWith(this.ODATA_ANNOTATION_PREFIX)) && !k.startsWith(this.ODATA_FUNCTION_PREFIX)) || (metadata === 'full' && k.indexOf(this.ODATA_ANNOTATION_PREFIX) === -1 && !k.startsWith(this.ODATA_FUNCTION_PREFIX))) .reduce((acc, e) => ({ ...acc, [e[0]]: e[1] }), {}); }, nextLink(annots) { return annots.has(this.ODATA_NEXTLINK) ? decodeURIComponent(annots.get(this.ODATA_NEXTLINK)) : undefined; }, readLink(annots) { return annots.has(this.ODATA_READLINK) ? decodeURIComponent(annots.get(this.ODATA_READLINK)) : undefined; }, mediaReadLink(annots) { return annots.has(this.ODATA_MEDIA_READLINK) ? decodeURIComponent(annots.get(this.ODATA_MEDIA_READLINK)) : undefined; }, editLink(annots) { return annots.has(this.ODATA_EDITLINK) ? decodeURIComponent(annots.get(this.ODATA_EDITLINK)) : undefined; }, mediaEditLink(annots) { return annots.has(this.ODATA_MEDIA_EDITLINK) ? decodeURIComponent(annots.get(this.ODATA_MEDIA_EDITLINK)) : undefined; }, deltaLink(annots) { return annots.has(this.ODATA_DELTALINK) ? decodeURIComponent(annots.get(this.ODATA_DELTALINK)) : undefined; }, mediaContentType(annots) { return annots.has(this.ODATA_MEDIA_CONTENTTYPE) ? decodeURIComponent(annots.get(this.ODATA_MEDIA_CONTENTTYPE)) : undefined; }, }; const ODataHelper = { //#region Version 4.0 [VERSION_4_0]: { ...ODataVersionBaseHelper, VALUE: 'value', ODATA_ANNOTATION_PREFIX: '@odata', ODATA_FUNCTION_PREFIX: '#', //odata.id: the ID of the entity ODATA_ID: '@odata.id', //odata.count: the total count of a collection of entities or collection of entity references, if requested. ODATA_COUNT: '@odata.count', //odata.context: the context URL for a collection, entity, primitive value, or service document. ODATA_CONTEXT: '@odata.context', //odata.etag: the ETag of the entity ODATA_ETAG: '@odata.etag', ODATA_METADATA_ETAG: '@odata.metadataEtag', //odata.type: the type of the containing {[name: string]: any} or targeted property if the type of the {[name: string]: any} or targeted property cannot be heuristically determined ODATA_TYPE: '@odata.type', //odata.delta ODATA_DELTA: '@odata.delta', //odata.remove ODATA_REMOVE: '@odata.remove', //odata.nextLink: the next link of a collection with partial results ODATA_NEXTLINK: '@odata.nextLink', //odata.deltaLink: the delta link for obtaining changes to the result, if requested ODATA_DELTALINK: '@odata.deltaLink', //odata.readLink: the link used to read the entity, if the edit link cannot be used to read the entity ODATA_READLINK: '@odata.readLink', //odata.editLink: the link used to edit/update the entity, if the entity is updatable and the odata.id does not represent a URL that can be used to edit the entity ODATA_EDITLINK: '@odata.editLink', //odata.associationLink: the link used to describe the relationship between this entity and related entities ODATA_ASSOCIATIONLINK: '@odata.associationLink', //odata.navigationLink: the link used to retrieve the values of a navigation property ODATA_NAVIGATIONLINK: '@odata.navigationLink', //Media entities and stream properties may in addition contain the following annotations: //odata.mediaEtag: the ETag of the stream, as appropriate ODATA_MEDIA_ETAG: '@odata.mediaEtag', //odata.mediaContentType: the content type of the stream ODATA_MEDIA_CONTENTTYPE: '@odata.mediaContentType', //odata.mediaReadLink: the link used to read the stream ODATA_MEDIA_READLINK: '@odata.mediaReadLink', //odata.mediaEditLink: the link used to edit/update the stream ODATA_MEDIA_EDITLINK: '@odata.mediaEditLink', //http://nb-mdp-dev01:57970/$metadata#recursos/$entity //http://nb-mdp-dev01:57970/$metadata#categorias //http://nb-mdp-dev01:57970/$metadata#juzgados //http://nb-mdp-dev01:57970/$metadata#Collection(SIU.Recursos.RecursoEntry) //http://nb-mdp-dev01:57970/$metadata#categorias/$entity //http://nb-mdp-dev01:57970/$metadata#categorias(children(children(children(children(children(children(children(children(children(children()))))))))))/$entity //http://nb-mdp-dev01:57970/$metadata#recursos/SIU.Documentos.Documento/$entity //http://nb-mdp-dev01:57970/$metadata#SIU.Api.Infrastructure.Storage.Backend.SiuUrls context(annots) { let ctx = {}; const str = annots instanceof Map ? annots.get(this.ODATA_CONTEXT) : annots[this.ODATA_CONTEXT]; if (typeof str === 'string') { let index = str.indexOf('$metadata'); ctx.serviceRootUrl = str.substring(0, index); index = str.indexOf('#'); ctx.metadataUrl = str.substring(0, index); const parts = str.substring(index + 1).split('/'); const col = COLLECTION.exec(parts[0]); if (col) { ctx.type = col[1]; } else if (parts[0].indexOf('.') !== -1) { ctx.type = parts[0]; } else { const property = parts[0].match(PROPERTY); const expand = parts[0].match(EXPAND); ctx.entity = parts[1] === '$entity'; if (property) { ctx.entitySet = property[1]; ctx.key = property[2]; ctx.property = parts[1]; } else if (expand) { ctx.entitySet = expand[1]; ctx.expand = expand[2]; } else { ctx.entitySet = parts[0]; } } } return ctx; }, countParam() { return { [$COUNT]: 'true' }; }, }, //#endregion //#region Version 3.0 [VERSION_3_0]: { ...ODataVersionBaseHelper, ODATA_ANNOTATION_PREFIX: 'odata.', ODATA_FUNCTION_PREFIX: '', ODATA_ID: 'odata.id', ODATA_DELTA: 'odata.delta', ODATA_REMOVE: 'odata.remove', ODATA_ETAG: 'odata.etag', ODATA_CONTEXT: 'odata.metadata', ODATA_NEXTLINK: 'odata.nextLink', ODATA_TYPE: 'odata.type', ODATA_COUNT: 'odata.count', VALUE: 'value', context(annots) { let ctx = {}; const str = annots instanceof Map ? annots.get(this.ODATA_CONTEXT) : annots[this.ODATA_CONTEXT]; if (typeof str === 'string') { let index = str.indexOf('$metadata'); ctx.serviceRootUrl = str.substring(0, index); index = str.indexOf('#'); ctx.metadataUrl = str.substring(0, index); const parts = str.substring(index + 1).split('/'); ctx.entitySet = parts[0]; } return ctx; }, countParam() { return { [$INLINECOUNT]: 'allpages' }; }, }, //#endregion //#region Version 2.0 [VERSION_2_0]: { ...ODataVersionBaseHelper, ODATA_ID: 'id', ODATA_DELTA: 'delta', ODATA_REMOVE: 'remove', ODATA_ETAG: 'etag', ODATA_ANNOTATION: '__metadata', ODATA_NEXTLINK: '__next', ODATA_COUNT: '__count', ODATA_DEFERRED: '__deferred', ODATA_TYPE: 'type', VALUE: 'results', annotations(value) { const annots = new Map(); if (this.ODATA_ANNOTATION in value) { Object.entries(value[this.ODATA_ANNOTATION]).forEach(([key, value]) => annots.set(key, value)); } return annots; }, context(annots) { let ctx = {}; return ctx; }, attributes(value, metadata) { return value; }, countParam() { return { [$INLINECOUNT]: 'allpages' }; }, }, //#endregion }; const COMPARISON_OPERATORS = ['eq', 'ne', 'gt', 'ge', 'lt', 'le']; const LOGICAL_OPERATORS = ['and', 'or', 'not']; const COLLECTION_OPERATORS = ['any', 'all']; const BOOLEAN_FUNCTIONS = ['startswith', 'endswith', 'contains']; const SUPPORTED_EXPAND_PROPERTIES = [ 'expand', 'levels', 'select', 'top', 'count', 'orderby', 'filter', ]; const FUNCTION_REGEX = /\((.*)\)/; const INDEXOF_REGEX = /(?!indexof)\((\w+)\)/; const StandardAggregateMethods = { sum: 'sum', min: 'min', max: 'max', average: 'average', countdistinct: 'countdistinct' }; const QueryCustomTypes = { Raw: 'Raw', Alias: 'Alias', Duration: 'Duration', Binary: 'Binary' }; //https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_QueryOptions const raw = (value) => ({ type: QueryCustomTypes.Raw, value, }); const alias = (value, name) => ({ type: QueryCustomTypes.Alias, value, name, }); const duration = (value) => ({ type: QueryCustomTypes.Duration, value, }); const binary = (value) => ({ type: QueryCustomTypes.Binary, value, }); const isQueryCustomType = (value) => typeof value === 'object' && 'type' in value && value.type in QueryCustomTypes; const isRawType = (value) => isQueryCustomType(value) && value.type === QueryCustomTypes.Raw; const ITEM_ROOT = ''; function builder ({ select, search, skiptoken, format, top, skip, filter, transform, compute, orderBy, key, count, expand, action, func, aliases, escape, } = {}) { const [path, params] = buildPathAndQuery({ select, search, skiptoken, format, top, skip, filter, transform, compute, orderBy, key, count, expand, action, func, aliases, escape, }); return buildUrl(path, params); } function buildPathAndQuery({ select, search, skiptoken, format, top, skip, filter, apply, transform, compute, orderBy, key, count, expand, action, func, aliases, escape, } = {}) { let path = ''; aliases = aliases || []; const query = {}; // key is not (null, undefined) if (key != undefined) { path += `(${normalizeValue(key, { aliases, escape })})`; } // Select if (select) { query.$select = isRawType(select) ? select.value : Array.isArray(select) ? select.join(',') : select; } // Compute if (compute) { query.$compute = isRawType(compute) ? compute.value : Array.isArray(compute) ? compute.join(',') : compute; } // Search if (search) { query.$search = search; } // Skiptoken if (skiptoken) { query.$skiptoken = skiptoken; } // Format if (format) { query.$format = format; } // Filter if (filter || typeof count === 'object') { query.$filter = buildFilter(typeof count === 'object' ? count : filter, { aliases, escape, }); } // Transform if (transform) { query.$apply = buildTransforms(transform, { aliases, escape }); } // Apply if (apply) { query.$apply = query.$apply ? query.$apply + '/' + buildApply(apply, { aliases, escape }) : buildApply(apply, { aliases, escape }); } // Expand if (expand) { query.$expand = buildExpand(expand, { aliases, escape }); } // OrderBy if (orderBy) { query.$orderby = buildOrderBy(orderBy); } // Count if (isRawType(count)) { query.$count = count.value; } else if (typeof count === 'boolean') { query.$count = true; } else if (count) { path += '/$count'; } // Top if (isRawType(top)) { query.$top = top.value; } else if (typeof top === 'number') { query.$top = top; } // Skip if (isRawType(skip)) { query.$top = skip.value; } else if (typeof skip === 'number') { query.$skip = skip; } if (action) { path += `/${action}`; } if (func) { if (typeof func === 'string') { path += `/${func}()`; } else if (typeof func === 'object') { const [funcName] = Object.keys(func); const funcArgs = normalizeValue(func[funcName], { aliases, escape, }); path += `/${funcName}(${funcArgs})`; } } if (aliases.length > 0) { Object.assign(query, aliases.reduce((acc, alias) => Object.assign(acc, { [`@${alias.name}`]: normalizeValue(alias.value, { escape, }), }), {})); } // Filter empty values const params = Object.entries(query) .filter(([, value]) => value !== undefined && value !== '') .reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), {}); return [path, params]; } function renderPrimitiveValue(key, val, { aliases, escape, }) { return `${key} eq ${normalizeValue(val, { aliases, escape })}`; } function buildFilter(filters = {}, { aliases, propPrefix, escape, }) { return (Array.isArray(filters) ? filters : [filters]).reduce((acc, filter) => { if (filter) { const builtFilter = buildFilterCore(filter, { aliases, propPrefix, escape, }); if (builtFilter) { acc.push(builtFilter); } } return acc; }, []).join(' and '); function buildFilterCore(filter = {}, { aliases, propPrefix, escape, }) { let filterExpr = ''; if (isRawType(filter)) { // Use raw query custom filter string filterExpr = filter.value; } else if (typeof filter === 'string') { // Use raw filter string filterExpr = filter; } else if (filter && typeof filter === 'object') { const filtersArray = Object.keys(filter).reduce((result, filterKey) => { const value = filter[filterKey]; let propName = ''; if (propPrefix) { if (filterKey === ITEM_ROOT) { propName = propPrefix; } else if (INDEXOF_REGEX.test(filterKey)) { propName = filterKey.replace(INDEXOF_REGEX, (_, $1) => $1.trim() === ITEM_ROOT ? `(${propPrefix})` : `(${propPrefix}/${$1.trim()})`); } else if (FUNCTION_REGEX.test(filterKey)) { propName = filterKey.replace(FUNCTION_REGEX, (_, $1) => $1.trim() === ITEM_ROOT ? `(${propPrefix})` : `(${propPrefix}/${$1.trim()})`); } else { propName = `${propPrefix}/${filterKey}`; } } else { propName = filterKey; } if (filterKey === ITEM_ROOT && Array.isArray(value)) { return result.concat(value.map((arrayValue) => renderPrimitiveValue(propName, arrayValue, { escape, aliases }))); } if (['number', 'string', 'boolean'].indexOf(typeof value) !== -1 || value instanceof Date || value === null) { // Simple key/value handled as equals operator result.push(renderPrimitiveValue(propName, value, { aliases, escape })); } else if (Array.isArray(value)) { const op = filterKey; const builtFilters = value .map((v) => buildFilter(v, { aliases, propPrefix, escape })) .filter((f) => f) .map((f) => (LOGICAL_OPERATORS.indexOf(op) !== -1 ? `(${f})` : f)); if (builtFilters.length) { if (LOGICAL_OPERATORS.indexOf(op) !== -1) { if (builtFilters.length) { if (op === 'not') { result.push(parseNot(builtFilters)); } else { result.push(`(${builtFilters.join(` ${op} `)})`); } } } else { result.push(builtFilters.join(` ${op} `)); } } } else if (LOGICAL_OPERATORS.indexOf(propName) !== -1) { const op = propName; const builtFilters = Object.keys(value).map((valueKey) => buildFilterCore({ [valueKey]: value[valueKey] }, { aliases, escape })); if (builtFilters.length) { if (op === 'not') { result.push(parseNot(builtFilters)); } else { result.push(`${builtFilters.join(` ${op} `)}`); } } } else if (typeof value === 'object') { if ('type' in value) { result.push(renderPrimitiveValue(propName, value, { aliases, escape })); } else { const operators = Object.keys(value); operators.forEach((op) => { if (COMPARISON_OPERATORS.indexOf(op) !== -1) { result.push(`${propName} ${op} ${normalizeValue(value[op], { aliases, escape, })}`); } else if (LOGICAL_OPERATORS.indexOf(op) !== -1) { if (Array.isArray(value[op])) { result.push(value[op] .map((v) => '(' + buildFilterCore(v, { aliases, propPrefix: propName, escape, }) + ')') .join(` ${op} `)); } else { result.push('(' + buildFilterCore(value[op], { aliases, propPrefix: propName, escape, }) + ')'); } } else if (COLLECTION_OPERATORS.indexOf(op) !== -1) { const collectionClause = buildCollectionClause(filterKey.toLowerCase(), value[op], op, propName); if (collectionClause) { result.push(collectionClause); } } else if (op === 'has') { result.push(`${propName} ${op} ${normalizeValue(value[op], { aliases, escape, })}`); } else if (op === 'in') { const resultingValues = Array.isArray(value[op]) ? value[op] : value[op].value.map((typedValue) => ({ type: value[op].type, value: typedValue, })); result.push(propName + ' in (' + resultingValues .map((v) => normalizeValue(v, { aliases, escape })) .join(',') + ')'); } else if (BOOLEAN_FUNCTIONS.indexOf(op) !== -1) { // Simple boolean functions (startswith, endswith, contains) result.push(`${op}(${propName},${normalizeValue(value[op], { aliases, escape, })})`); } else { // Nested property const filter = buildFilterCore(value, { aliases, propPrefix: propName, escape, }); if (filter) { result.push(filter); } } }); } } else if (value === undefined) { // Ignore/omit filter if value is `undefined` } else { throw new Error(`Unexpected value type: ${value}`); } return result; }, []); filterExpr = filtersArray.join(' and '); } /* else { throw new Error(`Unexpected filters type: ${filter}`); } */ return filterExpr; } function buildCollectionClause(lambdaParameter, value, op, propName) { let clause = ''; if (typeof value === 'string' || value instanceof String) { clause = getStringCollectionClause(lambdaParameter, value, op, propName); } else if (value) { // normalize {any:[{prop1: 1}, {prop2: 1}]} --> {any:{prop1: 1, prop2: 1}}; same for 'all', // simple values collection: {any:[{'': 'simpleVal1'}, {'': 'simpleVal2'}]} --> {any:{'': ['simpleVal1', 'simpleVal2']}}; same for 'all', const filterValue = Array.isArray(value) ? value.reduce((acc, item) => { if (item.hasOwnProperty(ITEM_ROOT)) { if (!acc.hasOwnProperty(ITEM_ROOT)) { acc[ITEM_ROOT] = []; } acc[ITEM_ROOT].push(item[ITEM_ROOT]); return acc; } return { ...acc, ...item }; }, {}) : value; const filter = buildFilterCore(filterValue, { aliases, propPrefix: lambdaParameter, escape, }); clause = `${propName}/${op}(${filter ? `${lambdaParameter}:${filter}` : ''})`; } return clause; } } function getStringCollectionClause(lambdaParameter, value, collectionOperator, propName) { const conditionOperator = collectionOperator == 'all' ? 'ne' : 'eq'; return `${propName}/${collectionOperator}(${lambdaParameter}: ${lambdaParameter} ${conditionOperator} '${value}')`; } function escapeIllegalChars(string) { string = string.replace(/%/g, '%25'); string = string.replace(/\+/g, '%2B'); string = string.replace(/\//g, '%2F'); string = string.replace(/\?/g, '%3F'); string = string.replace(/#/g, '%23'); string = string.replace(/&/g, '%26'); string = string.replace(/'/g, "''"); return string; } function normalizeValue(value, { aliases, escape = false } = {}) { if (typeof value === 'string') { return escape ? `'${escapeIllegalChars(value)}'` : `'${value}'`; } else if (value instanceof Date) { return value.toISOString(); } else if (typeof value === 'number') { return value; } else if (Array.isArray(value)) { return `[${value.map((d) => normalizeValue(d, { aliases, escape })).join(',')}]`; } else if (value === null) { return value; } else if (typeof value === 'object') { switch (value.type) { case QueryCustomTypes.Raw: return value.value; case QueryCustomTypes.Duration: return `duration'${value.value}'`; case QueryCustomTypes.Binary: return `binary'${value.value}'`; case QueryCustomTypes.Alias: // Store if (Array.isArray(aliases)) { if (value.name === undefined) { value.name = `a${aliases.length + 1}`; } aliases.push(value); } return `@${value.name}`; default: return Object.entries(value) .filter(([, v]) => v !== undefined) .map(([k, v]) => `${k}=${normalizeValue(v, { aliases, escape })}`) .join(','); } } return value; } function buildExpand(expands, { aliases, escape = false }) { if (isRawType(expands)) { return expands.value; } else if (typeof expands === 'number') { return expands; } else if (typeof expands === 'string') { if (expands.indexOf('/') === -1) { return expands; } // Change `Foo/Bar/Baz` to `Foo($expand=Bar($expand=Baz))` return expands .split('/') .reverse() .reduce((results, item, index, arr) => { if (index === 0) { // Inner-most item return `$expand=${item}`; } else if (index === arr.length - 1) { // Outer-most item, don't add `$expand=` prefix (added above) return `${item}(${results})`; } else { // Other items return `$expand=${item}(${results})`; } }, ''); } else if (Array.isArray(expands)) { return `${expands .map((e) => buildExpand(e, { aliases, escape })) .join(',')}`; } else if (typeof expands === 'object') { const expandKeys = Object.keys(expands); if (expandKeys.some((key) => SUPPORTED_EXPAND_PROPERTIES.indexOf(key.toLowerCase()) !== -1)) { return expandKeys .map((key) => { let value; switch (key) { case 'filter': value = buildFilter(expands[key], { aliases, escape, }); break; case 'orderBy': value = buildOrderBy(expands[key]); break; case 'levels': case 'count': case 'top': case 'skip': value = `${expands[key]}`; if (isRawType(value)) value = value.value; break; default: value = buildExpand(expands[key], { aliases, escape, }); } return `$${key.toLowerCase()}=${value}`; }) .join(';'); } else { return expandKeys .map((key) => { const builtExpand = buildExpand(expands[key], { aliases, escape }); return builtExpand ? `${key}(${builtExpand})` : key; }) .join(','); } } return ''; } function buildTransforms(transforms, { aliases, escape = false }) { // Wrap single object an array for simplified processing const transformsArray = Array.isArray(transforms) ? transforms : [transforms]; const transformsResult = transformsArray.reduce((result, transform) => { for (const key of Object.keys(transform)) { switch (key) { // TODO: support as many of the following: // topcount, topsum, toppercent, // bottomsum, bottomcount, bottompercent, // identity, concat, expand, search, compute, isdefined case 'aggregate': { const aggregate = transform[key]; if (aggregate) { result.push(`aggregate(${buildAggregate(aggregate)})`); } break; } case 'filter': { const filter = transform[key]; if (filter) { const builtFilter = buildFilter(filter, { aliases, escape }); if (builtFilter) { result.push(`filter(${builtFilter})`); } } break; } case 'groupBy': { const groupBy = transform[key]; if (groupBy) { result.push(`groupby(${buildGroupBy(groupBy, { aliases, escape })})`); } break; } default: throw new Error(`Unsupported transform(s): ${key}`); } } return result; }, []); return transformsResult.join('/') || undefined; } function buildAggregate(aggregate) { // Wrap single object in an array for simplified processing const aggregateArray = Array.isArray(aggregate) ? aggregate : [aggregate]; return aggregateArray .map((aggregateItem) => { return typeof aggregateItem === 'string' ? aggregateItem : Object.keys(aggregateItem).map((aggregateKey) => { const aggregateValue = aggregateItem[aggregateKey]; // TODO: Are these always required? Can/should we default them if so? if (!aggregateValue.with) { throw new Error(`'with' property required for '${aggregateKey}'`); } if (!aggregateValue.as) { throw new Error(`'as' property required for '${aggregateKey}'`); } return `${aggregateKey} with ${aggregateValue.with} as ${aggregateValue.as}`; }); }) .join(','); } function buildGroupBy(groupBy, { aliases, escape = false }) { if (!groupBy.properties) { throw new Error(`'properties' property required for groupBy`); } let result = `(${groupBy.properties.join(',')})`; if (groupBy.transform) { result += `,${buildTransforms(groupBy.transform, { aliases, escape })}`; } return result; } function buildOrderBy(orderBy, prefix = '') { if (isRawType(orderBy)) { return orderBy.value; } else if (Array.isArray(orderBy)) { return orderBy .map((value) => Array.isArray(value) && value.length === 2 && ['asc', 'desc'].indexOf(value[1]) !== -1 ? value.join(' ') : value) .map((v) => `${prefix}${v}`) .join(','); } else if (typeof orderBy === 'object') { return Object.entries(orderBy) .map(([k, v]) => buildOrderBy(v, `${k}/`)) .map((v) => `${prefix}${v}`) .join(','); } return `${prefix}${orderBy}`; } function buildApply(apply, { aliases, escape = false }) { const applyArray = Array.isArray(apply) ? apply : [apply]; return applyArray.map((v) => normalizeValue(v, { aliases, escape })).join('/'); } function buildUrl(path, params) { // This can be refactored using URL API. But IE does not support it. const queries = Object.entries(params).map(([key, value]) => `${key}=${value}`); return queries.length ? `${path}?${queries.join('&')}` : path; } function parseNot(builtFilters) { return `not (${builtFilters.join(' and ')})`; } const ISO_REGEX = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/; const Dates = { isoStringToDate(value) { if (typeof value === 'string' && value.search(ISO_REGEX) === 0) { return new Date(value); } else if (Array.isArray(value)) { return value.map((v) => this.isoStringToDate(v)); } else if (value && value.constructor === Object) { return Object.keys(value) .map((key) => [key, this.isoStringToDate(value[key])]) .reduce((acc, v) => Object.assign(acc, { [v[0]]: v[1] }), {}); } return value; }, }; const DURATION_REGEX = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; const Durations = { toDuration(v) { const matches = DURATION_REGEX.exec(v); if (!matches || v.length < 3) { throw new TypeError(`duration invalid: "${v}". Must be a ISO 8601 duration. See https://en.wikipedia.org/wiki/ISO_8601#Durations`); } let duration = {}; duration.sign = matches[1] === '-' ? -1 : 1; return ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'].reduce((acc, name, index) => { const v = parseFloat(matches[index + 2]); if (!Number.isNaN(v)) acc[name] = v; return acc; }, duration); }, toString(v) { return [ v.sign === -1 ? '-' : '', 'P', v.years ? v.years + 'Y' : '', v.months ? v.months + 'M' : '', v.weeks ? v.weeks + 'W' : '', v.days ? v.days + 'D' : '', 'T', v.hours ? v.hours + 'H' : '', v.minutes ? v.minutes + 'M' : '', v.seconds ? v.seconds + 'S' : '', ].join(''); }, }; const Enums = { names(enums) { return Object.values(enums).filter((v) => typeof v === 'string'); }, values(enums) { return Object.values(enums).filter((v) => typeof v === 'number'); }, toValue(enums, value) { if (value in enums) return typeof value === 'string' ? enums[value] : value; return undefined; }, toValues(enums, value) { if (typeof value === 'number') { return this.values(enums).filter((v) => (value & v) === v); } if (typeof value === 'string') { value = value.split(',').map((o) => o.trim()); } if (Array.isArray(value) && value.every((v) => v in enums)) { return value.map((o) => this.toValue(enums, o)); } return []; }, toName(enums, value) { if (value in enums) return typeof value === 'number' ? enums[value] : value; return undefined; }, toNames(enums, value) { if (typeof value === 'number') { return this.values(enums) .filter((v) => (value & v) === v) .map((v) => this.toName(enums, v)); } if (typeof value === 'string') { value = value.split(',').map((o) => o.trim()); } if (Array.isArray(value) && value.every((v) => v in enums)) { return value.map((o) => this.toName(enums, o)); } return []; }, toFlags(enums, value) { if (typeof value === 'number') { return this.values(enums) .filter((v) => v !== 0 && (value & v) === v) .map((v) => this.toName(enums, v)); } if (typeof value === 'string') { value = value.split(',').map((o) => o.trim()); } if (Array.isArray(value) && value.every((v) => v in enums)) { return value.filter((v) => enums[v]).map((v) => this.toName(enums, v)); } return []; }, }; function cloneSymbol(targe) { return Object(Symbol.prototype.valueOf.call(targe)); } function cloneReg(targe) { const reFlags = /\w*$/; const result = new targe.constructor(targe.source, reFlags.exec(targe)); result.lastIndex = targe.lastIndex; return result; } const Types = { rawType(value) { return Object.prototype.toString.call(value).slice(8, -1); }, isObject(value) { return typeof value === 'object' && value !== null; }, isPlainObject(value) { if (this.rawType(value) !== 'Object') { return false; } const prototype = Object.getPrototypeOf(value); return prototype === null || prototype === Object.prototype; }, isFunction(value) { return typeof value