UNPKG

angular-odata

Version:

Client side OData typescript library for Angular

1,343 lines (1,333 loc) 509 kB
import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpParams, HttpResponse, HttpErrorResponse, HttpEventType, HttpClientModule } from '@angular/common/http'; import { of, throwError, Subject, map as map$1, EMPTY, Observable, forkJoin, NEVER } from 'rxjs'; import { tap, startWith, map, expand, reduce, finalize, defaultIfEmpty, switchMap, catchError } 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$/; class ODataCache { timeout; entries; constructor({ timeout = DEFAULT_TIMEOUT }) { this.timeout = timeout; this.entries = new Map(); } /** * Using the resource on the request build an array of string to identify the scope of the request * @param req The request with the resource to build the scope * @returns Array of string to identify the scope of the request */ scope(req) { const segments = req.resource.cloneSegments(); return segments.segments({ key: true }).reduce((acc, s) => { if (s.name === PathSegment.entitySet) acc = [...acc, s.path()]; return acc; }, ['request']); } /** * Using the odata context on the response build an array of string to identify the tags of the response * @param res The response to build the tags * @returns Array of string to identify the tags of the response */ tags(res) { const tags = []; const context = res.context; if (context.entitySet) { tags.push(context.key ? `${context.entitySet}(${context.key})` : context.entitySet); } if (context.type) tags.push(context.type); return tags; } /** * Build an entry from a payload and some options * @param payload The payload to store in the cache * @param timeout The timeout for the entry * @param tags The tags for the entry * @returns The entry to store in the cache */ buildEntry(payload, { timeout, tags }) { return { payload, lastRead: Date.now(), timeout: timeout || this.timeout, tags: tags || [], }; } /** * Build a key from store an entry in the cache * @param names The names of the entry * @returns The key for the entry */ buildKey(names) { return names.join(CACHE_KEY_SEPARATOR); } /** * Put some payload in the cache * @param name The name for the entry * @param payload The payload to store in the cache * @param timeout The timeout for the entry * @param scope The scope for the entry * @param tags The tags for the entry */ put(name, payload, { timeout, scope, tags, } = {}) { const entry = this.buildEntry(payload, { timeout, tags }); const key = this.buildKey([...(scope || []), name]); this.entries.set(key, entry); this.forget(); } /** * Return the payload from the cache if it exists and is not expired * @param name The name of the entry * @param scope The scope of the entry * @returns The payload of the entry */ get(name, { scope } = {}) { const key = this.buildKey([...(scope || []), name]); const entry = this.entries.get(key); return entry !== undefined && !this.isExpired(entry) ? entry.payload : undefined; } /** * Remove all cache entries that are matching with the given options * @param options The options to forget */ forget({ name, scope = [], tags = [], } = {}) { if (name !== undefined) scope.push(name); const key = scope.length > 0 ? this.buildKey(scope) : undefined; this.entries.forEach((entry, k) => { if (this.isExpired(entry) || // Expired (key !== undefined && k.startsWith(key)) || // Key (tags.length > 0 && tags.some((t) => entry.tags.indexOf(t) !== -1)) // Tags ) { this.entries.delete(k); } }); } /** * Remove all cache entries */ flush() { this.entries = new Map(); } /** * Check if the entry is expired * @param entry The cache entry * @returns Boolean indicating if the entry is expired */ isExpired(entry) { return entry.lastRead < Date.now() - (entry.timeout || this.timeout) * 1000; } /** * Using the request, handle the fetching of the response * @param req The request to fetch * @param res$ Observable of the response * @returns */ handleRequest(req, res$) { return req.isFetch() ? this.handleFetch(req, res$) : req.isMutate() ? this.handleMutate(req, res$) : res$; } handleFetch(req, res$) { const policy = req.fetchPolicy; const cached = this.getResponse(req); if (policy === 'no-cache') { return res$; } if (policy === 'cache-only') { if (cached) { return of(cached); } else { return throwError(() => new Error('No Cached')); } } if (policy === 'cache-first' || policy === 'cache-and-network' || policy === 'network-only') { res$ = res$.pipe(tap((res) => { if (res.options.cacheability !== 'no-store') this.putResponse(req, res); })); } return cached !== undefined && policy !== 'network-only' ? policy === 'cache-and-network' ? res$.pipe(startWith(cached)) : of(cached) : res$; } handleMutate(req, res$) { const requests = req.isBatch() ? req.resource .requests() .filter((r) => r.isMutate()) : [req]; for (var r of requests) { const scope = this.scope(r); this.forget({ scope }); } return res$; } } class ODataInMemoryCache extends ODataCache { constructor({ timeout } = {}) { super({ timeout }); } /** * Store the response in the cache * @param req The request with the resource to store the response * @param res The response to store in the cache */ putResponse(req, res) { let scope = this.scope(req); let tags = this.tags(res); this.put(req.cacheKey, res, { timeout: res.options.maxAge, scope, tags, }); } /** * Restore the response from the cache * @param req The request with the resource to get the response * @returns The response from the cache */ getResponse(req) { let scope = this.scope(req); return this.get(req.cacheKey, { scope }); } } 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+)\)/; var StandardAggregateMethods; (function (StandardAggregateMethods) { StandardAggregateMethods["sum"] = "sum"; StandardAggregateMethods["min"] = "min"; StandardAggregateMethods["max"] = "max"; StandardAggregateMethods["average"] = "average"; StandardAggregateMethods["countdistinct"] = "countdistinct"; })(StandardAggregateMethods || (StandardAggregateMethods = {})); var QueryCustomTypes; (function (QueryCustomTypes) { QueryCustomTypes[QueryCustomTypes["Raw"] = 0] = "Raw"; QueryCustomTypes[QueryCustomTypes["Alias"] = 1] = "Alias"; QueryCustomTypes[QueryCustomTypes["Duration"] = 2] = "Duration"; QueryCustomTypes[QueryCustomTypes["Binary"] = 3] = "Binary"; })(QueryCustomTypes || (QueryCustomTypes = {})); //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) { let clause = ''; const conditionOperator = collectionOperator == 'all' ? 'ne' : 'eq'; clause = `${propName}/${collectionOperator}(${lambdaParameter}: ${lambdaParameter} ${conditionOperator} '${value}')`; return clause; } 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) => { const { aggregate, filter, groupBy, ...rest } = transform; // TODO: support as many of the following: // topcount, topsum, toppercent, // bottomsum, bottomcount, bottompercent, // identity, concat, expand, search, compute, isdefined const unsupportedKeys = Object.keys(rest); if (unsupportedKeys.length) { throw new Error(`Unsupported transform(s): ${unsupportedKeys}`); } if (aggregate) { result.push(`aggregate(${buildAggregate(aggregate)})`); } if (filter) { const builtFilter = buildFilter(filter, { aliases, escape }); if (builtFilter) { result.push(`filter(${buildFilter(builtFilter, { aliases, escape })})`); } } if (groupBy) { result.push(`groupby(${buildGroupBy(groupBy, { aliases, escape })})`); } 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 === 'function'; }, isArray(value) { return Array.isArray(value); }, isMap(value) { return this.rawType(value) === 'Map'; }, isEmpty(value) { return (value === undefined || value === null || (typeof value === 'string' && !value.length) || (value instanceof Date && isNaN(value.valueOf())) || (Types.isMap(value) && !value.size) || (Types.isArray(value) && !value.length) || (Types.isFunction(value.isEmpty) && value.isEmpty()) || (Types.isArray(value) && value.every((v) => Types.isEmpty(v))) || (Types.isPlainObject(value) && !Object.keys(value).filter((k) => value.hasOwnProperty(k)).length)); }, isEqual(value1, value2) { function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); } function areDatesEqual() { return value1.getTime() === value2.getTime(); } function areArraysBufferEqual() { if (value1.byteLength !== value2.byteLength) { return false; } var view1 = new DataView(value1); var view2 = new DataView(value2); var i = value1.byteLength; while (i--) { if (view1.getUint8(i) !== view2.getUint8(i)) { return false; } } return true; } function areArraysEqual() { // Check length if (value1.length !== value2.length) return false; // Check each item in the array for (let i = 0; i < value1.length; i++) { if (!Types.isEqual(value1[i], value2[i])) return false; } // If no errors, return true return true; } function areObjectsEqual() { if (Object.keys(value1).length !== Object.keys(value2).length) return false; // Check each item in the object for (let key in value1) { if (Object.prototype.hasOwnProperty.call(value1, key)) { if (!Types.isEqual(value1[key], value2[key])) return false; } } // If no errors, return true return true; } function areFunctionsEqual() { return value1.toString() === value2.toString(); } function arePrimativesEqual() { return value1 === value2; } // Get the object type let type = getType(value1); // If the two items are not the same type, return false if (type !== getType(value2)) return false; // Compare based on type if (type === 'date') return areDatesEqual(); if (type === 'arraybuffer') return areArraysBufferEqual(); if (type === 'array') return areArraysEqual(); if (type === 'object') return areObjectsEqual(); if (type === 'function') return areFunctionsEqual(); return arePrimativesEqual(); }, clone(target) { const constrFun = target.constructor; switch (this.rawType(target)) { case 'Boolean': case 'Number': case 'String': case 'Error': case 'Date': return new constrFun(target); case 'RegExp': return cloneReg(target); case 'Symbol': return cloneSymbol(target); case 'Function': return target; default: return null; } }, }; const Http = { //Merge Headers mergeHttpHeaders(...values) { let headers = new HttpHeaders(); values.forEach((value) => { if (value instanceof HttpHeaders) { value.keys().forEach((key) => { headers = (value.getAll(key) || []).reduce((acc, v) => acc.append(key, v), headers); }); } else if (Types.isPlainObject(value)) { Object.entries(value).forEach(([key, value]) => { headers = (Array.isArray(value) ? value : [value]).reduce((acc, v) => acc.append(key, v), headers); }); } }); return headers; }, // Merge Params mergeHttpParams(