UNPKG

dcl-catalyst-client

Version:

A client to query and perform changes on Decentraland's catalyst servers

274 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryBuilder = exports.sanitizeUrl = exports.mergeRequestOptions = exports.isNode = exports.convertFiltersToQueryParams = exports.splitValuesIntoManyQueries = exports.splitValuesIntoManyQueryBuilders = exports.splitAndFetchPaginated = exports.splitAndFetch = exports.MAX_URL_LENGTH = exports.addModelToFormData = exports.getCurrentVersion = void 0; const fetch_component_1 = require("@well-known-components/fetch-component"); const package_json_1 = require("./../../package.json"); function getCurrentVersion() { return package_json_1.version || package_json_1.commit || 'Unknown'; } exports.getCurrentVersion = getCurrentVersion; function addModelToFormData(model, form, namespace = '') { for (const propertyName in model) { if (!model.hasOwnProperty(propertyName) || model[propertyName] === null || model[propertyName] === undefined) continue; const formKey = namespace ? `${namespace}[${propertyName}]` : propertyName; if (model[propertyName] instanceof Date) { form.append(formKey, model[propertyName].toISOString()); } else if (model[propertyName] instanceof Array) { model[propertyName].forEach((element, index) => { const tempFormKey = `${formKey}[${index}]`; addModelToFormData(element, form, tempFormKey); }); } else if (typeof model[propertyName] === 'object') { addModelToFormData(model[propertyName], form, formKey); } else { form.append(formKey, model[propertyName].toString()); } } return form; } exports.addModelToFormData = addModelToFormData; function removeDuplicates(array) { return Array.from(new Set(array)); } /** * This method performs one or more fetches to the given server, splitting query params into different queries to avoid exceeding the max length of urls */ exports.MAX_URL_LENGTH = 2048; async function splitAndFetch({ baseUrl, path, queryParams, fetcher, uniqueBy, options }) { // Adding default fetcher = fetcher ? fetcher : (0, fetch_component_1.createFetchComponent)(); // Split values into different queries const queries = splitValuesIntoManyQueries({ baseUrl, path, queryParams }); const results = new Map(); for (const query of queries) { // Perform the different queries const elements = (await (await fetcher.fetch(query, { ...options, headers: { 'Content-Type': 'application/json' } })).json()); // Group by unique property (if set), or add all of them to the map elements.forEach((element) => results.set(uniqueBy ? element[uniqueBy] : results.size, element)); } // Return results return Array.from(results.values()); } exports.splitAndFetch = splitAndFetch; const CHARS_LEFT_FOR_OFFSET = 7; /** * This method performs one or more fetches to the given server, splitting query params into different queries to avoid exceeding the max length of urls * This method should be used if the result is paginated, and needs to be queries many times */ async function splitAndFetchPaginated({ baseUrl, path, queryParams, elementsProperty, uniqueBy, options, fetcher }) { // Set default fetcher = fetcher ? fetcher : (0, fetch_component_1.createFetchComponent)(); // Reserve a few chars to send the offset const reservedParams = new Map([['offset', CHARS_LEFT_FOR_OFFSET]]); // Split values into different queries const queries = splitValuesIntoManyQueries({ baseUrl, path, queryParams, reservedParams }); // Perform the different queries const foundElements = new Map(); let exit = false; for (let i = 0; i < queries.length && !exit; i++) { let url = queries[i]; while (url && !exit) { try { const response = (await fetcher.fetch(url, options)).json(); const elements = response[elementsProperty]; elements.forEach((element) => foundElements.set(element[uniqueBy], element)); const nextRelative = response.pagination.next; url = nextRelative ? new URL(nextRelative, url).toString() : undefined; } catch (error) { exit = true; } } } return Array.from(foundElements.values()); } exports.splitAndFetchPaginated = splitAndFetchPaginated; function splitValuesIntoManyQueryBuilders({ queryParams, baseUrl, path, reservedParams }) { const queryParamsMap = 'name' in queryParams ? new Map([[queryParams.name, queryParams.values]]) : queryParams; // Check that it makes sent to apply the algorithm if (queryParamsMap.size === 0) { return [new QueryBuilder(baseUrl + path, queryParamsMap, reservedParams)]; } // Remove duplicates const withoutDuplicates = Array.from(queryParamsMap.entries()).map(([name, values]) => [ name, removeDuplicates(values) ]); // Sort params by amount of values const sortedByValues = withoutDuplicates.sort(([_, values1], [__, values2]) => values1.length - values2.length); // Add all params (except the last one that is the one with the most values) into the url const defaultQueryBuilder = new QueryBuilder(baseUrl + path, new Map(), reservedParams); for (let i = 0; i < sortedByValues.length - 1; i++) { const [paramName, paramValues] = sortedByValues[i]; if (!defaultQueryBuilder.canSetParams(paramName, paramValues)) { throw new Error(`This library can split one query param into many HTTP requests, but it can't split more than one. You will need to do that on the client side.`); } defaultQueryBuilder.setParams(paramName, paramValues); } // Prepare everything let queryBuilder = QueryBuilder.clone(defaultQueryBuilder); const [lastParamName, lastParamValues] = sortedByValues[sortedByValues.length - 1]; const result = []; for (const value of lastParamValues) { // Check url length if (!queryBuilder.canAddParam(lastParamName, value)) { result.push(queryBuilder); queryBuilder = QueryBuilder.clone(defaultQueryBuilder); } queryBuilder.addParam(lastParamName, value); } // Add current builder one last time result.push(queryBuilder); return result; } exports.splitValuesIntoManyQueryBuilders = splitValuesIntoManyQueryBuilders; function splitValuesIntoManyQueries(parameters) { const builders = splitValuesIntoManyQueryBuilders(parameters); return builders.map((builder) => builder.toString()); } exports.splitValuesIntoManyQueries = splitValuesIntoManyQueries; function convertFiltersToQueryParams(filters) { if (!filters) { return new Map(); } const entries = Object.entries(filters) .filter(([_, value]) => !!value) .map(([name, value]) => { const newName = name.endsWith('s') ? name.slice(0, -1) : name; let newValues; // Force coersion of number, boolean, or string into string if (Array.isArray(value)) { newValues = [...value].filter(isValidQueryParamValue).map((_) => `${_}`); } else if (isValidQueryParamValue(value)) { newValues = [`${value}`]; } else { throw new Error('Query params must be either a string, a number, a boolean or an array of the types just mentioned'); } return [newName, newValues]; }) .filter(([_, values]) => values.length > 0); return new Map(entries); } exports.convertFiltersToQueryParams = convertFiltersToQueryParams; function isNode() { return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; } exports.isNode = isNode; function mergeRequestOptions(target, source) { const combinedHeaders = { ...target === null || target === void 0 ? void 0 : target.headers, ...source === null || source === void 0 ? void 0 : source.headers }; return { ...target, ...source, headers: combinedHeaders }; } exports.mergeRequestOptions = mergeRequestOptions; function isValidQueryParamValue(value) { return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'; } /** Remove white spaces and add https if no protocol is specified */ function sanitizeUrl(url) { // Remove empty spaces url = url.trim(); // Add protocol if necessary if (!url.startsWith('https://') && !url.startsWith('http://')) { url = 'https://' + url; } // Remove trailing slash if present if (url.endsWith('/')) { url = url.slice(0, -1); } return url; } exports.sanitizeUrl = sanitizeUrl; class QueryBuilder { constructor(baseUrl, queryParams = new Map(), reservedParams = new Map()) { this.baseUrl = baseUrl; this.queryParams = queryParams; this.reservedParams = reservedParams; this.length = this.calculateUrlLength(queryParams, reservedParams); } canAddParam(paramName, paramValue) { return this.length + paramName.length + paramValue.length + 2 < exports.MAX_URL_LENGTH; } addParam(paramName, paramValue) { if (!this.canAddParam(paramName, paramValue)) { throw new Error(`You can't add this parameter '${paramName}', since it would exceed the max url length`); } const queryParam = this.queryParams.get(paramName); const values = queryParam ? queryParam : []; values.push(paramValue); this.queryParams.set(paramName, values); this.length += this.calculateAddedLength(paramName, [paramValue]); return this; } canSetParams(paramName, paramValues) { const newQueryParams = new Map([...this.queryParams, [paramName, paramValues]]); const newLength = this.calculateUrlLength(newQueryParams, this.reservedParams); return newLength < exports.MAX_URL_LENGTH; } /** This action will override whatever configuration there was previously for the given query parameter */ setParams(paramName, paramValues) { if (!this.canSetParams(paramName, paramValues)) { throw new Error(`You can't add this parameter '${paramName}', since it would exceed the max url length`); } this.queryParams.set(paramName, paramValues.map((value) => `${value}`)); this.length = this.calculateUrlLength(this.queryParams, this.reservedParams); return this; } /** This action will override whatever configuration there was previously for the given query parameter */ setParam(paramName, paramValue) { this.setParams(paramName, [paramValue]); return this; } toString() { let url = this.baseUrl; let addedParamAlready = false; for (const [paramName, paramValues] of this.queryParams) { for (const paramValue of paramValues) { if (addedParamAlready) { url += `&${paramName}=${paramValue}`; } else { url += `?${paramName}=${paramValue}`; addedParamAlready = true; } } } return url; } calculateUrlLength(queryParams, reservedParams) { let length = this.baseUrl.length; for (const [paramName, reserved] of reservedParams) { if (!this.queryParams.has(paramName)) { // We will avoid the reserved parameters that already have a value set length += paramName.length + 2 + reserved; } } for (const [paramName, paramValues] of queryParams) { length += this.calculateAddedLength(paramName, paramValues); } return length; } static clone(queryBuilder) { return new QueryBuilder(queryBuilder.baseUrl, new Map(queryBuilder.queryParams), queryBuilder.reservedParams); } calculateAddedLength(paramName, paramValues) { const valuesLength = this.calculateArrayLength(paramValues); return valuesLength + (paramName.length + 2) * paramValues.length; } calculateArrayLength(array) { return array.map((value) => `${value}`).reduce((accum, curr) => accum + curr.length, 0); } } exports.QueryBuilder = QueryBuilder; //# sourceMappingURL=Helper.js.map