UNPKG

@re621/zestyapi

Version:

Comprehensive JS wrapper for e621.net API

236 lines (205 loc) 8.9 kB
import { ResponseCode, ResponseStatusMessage } from "../error/ResponseCode"; import APIResponse from "../responses/APIResponse"; import ZestyAPI from "../ZestyAPI"; import { FormattedResponse, QueueResponse, ResponseStatus } from "./RequestQueue"; import Util from "./Util"; import { PrimitiveMap, PrimitiveType, StringMap } from "./UtilType"; export default class Endpoint<T extends APIResponse> { protected api: ZestyAPI; // Variables used in the inherited `find()` method. protected endpoint = "unknown"; // determines the URL of the endpoint (without .json) protected searchParams: string[] = []; // list of permitted search parameters protected searchParamAliases: { [prop: string]: string } = {}; constructor(api: ZestyAPI) { this.api = api; } public async find(search: SearchParams = {}): Promise<FormattedResponse<T>> { const query = this.splitQueryParams(search); let lookup: PrimitiveMap; try { lookup = this.validateParams(search, query); } catch (e) { return Endpoint.makeMalformedRequestResponse(); } return this.api.makeRequest(this.endpoint + ".json", { query: Endpoint.flattenParams(lookup) }) .then( (response: QueueResponse) => { if (response.data[this.endpoint]) { response.status.code = ResponseCode.NotFound; response.status.message = ResponseStatusMessage.NotFound; response.data = []; } return Endpoint.formatAPIResponse(response.status, response.data); }, (error: QueueResponse) => Endpoint.formatAPIResponse(error.status, []) ); } /** * Parses the SearchParams and finds values that * need to be split off into QueryParams. * @param {SearchParams} search Search params * @returns {QueryParams} Query params */ protected splitQueryParams(search: SearchParams = {}): QueryParams { const result: QueryParams = {}; if (search.limit) result.limit = search.limit; if (search.page) result.page = search.page; return result; } /** * Validates both sets of parameters and returns a prepared * map that can be plugged into `flattenParams()`. * @param {SearchParams} search Search parameters * @param {QueryParams} query Query parameters * @returns Validated results * @throws {MalformedRequestError} If the errors in the parameters are irreconcilable */ protected validateParams(search: SearchParams = {}, query: QueryParams = {}): PrimitiveMap { search = this.validateSearchParams(search); query = this.validateQueryParams(query); const result = Object.keys(query).length ? query : {}; if (Object.keys(search).length) result["search"] = search; return result; } /** * Validates the search parameters for the `find()` methods. * @param {SearchParams} params Search parameters * @returns {SearchParams} Validated parameters */ protected validateSearchParams(params: SearchParams = {}): SearchParams { const results = {}; // Replace param aliases for (const [antecedent, consequent] of Object.entries(this.searchParamAliases)) { if (params[antecedent]) params[consequent] = params[antecedent]; delete params[antecedent]; } // Find defined permitted params for (const one of this.searchParams) if (typeof params[one] !== "undefined") results[one] = params[one]; return results; } /** * Validates the query parameters for the `find()` methods. * @param {QueryParams} params Query parameters * @returns {QueryParams} Validated parameters */ protected validateQueryParams(params: QueryParams = {}): QueryParams { const results: SearchParams = {}; if (!params) return results; // Result limit // - Number between 1 and 320 if (params.limit && typeof params.limit == "number") results.limit = Util.Math.clamp(params.limit, 1, 320); // Page number // - Number between 1 and 750 // - OR number prefixed by `a` (after) or `b` (before) if (params.page) { if (typeof params.page == "number") results.page = Util.Math.clamp(params.page, 1, 750); else if (typeof params.page == "string" && /[ab]\d+/.test(params.page)) results.page = params.page; } return results; } /** * Shortcut method for making a response in case the search parameters are malformed or missing * @param {bool} array True if the output expects an array, false otherwise * @returns API Response */ protected static makeMalformedRequestResponse(): Promise<FormattedResponse<any>> { return Promise.resolve({ status: { code: ResponseCode.MalformedRequest, message: ResponseStatusMessage.MalformedRequest, url: null, }, data: [], }); } /** * Converts a value in a SearchParams format to an object with string values * @param {PrimitiveMap} params Original object * @param {string} separator Array join separator * @param {StringMap} keyReplacement Substitutions for key names * @returns Flattened object */ protected static flattenParams(params: PrimitiveMap, separator = ",", keyReplacement?: StringMap): StringMap { const result: StringMap = {}; for (const [key, value] of Object.entries(params)) { processValue(result, key, value, keyReplacement, separator, []); } return result; function processValue( obj: StringMap, key: string, value: PrimitiveType | PrimitiveType[] | PrimitiveMap, keyReplacement: StringMap = {}, separator = ",", keyStack: string[] = [], ): void { if (value === null || typeof value === "undefined" || value === "") return; if (keyReplacement[key]) key = keyReplacement[key]; // Array if (Array.isArray(value)) { if (value.length == 0) return; value = Util.encodeArray(value); obj[formatKey(key, keyStack)] = value.join(separator); return; } // Primitive type if (typeof value !== "object") { value = Util.encode(value); obj[formatKey(key, keyStack)] = value; return; } // Object (recursive) keyStack.push(key); for (const [key2, value2] of Object.entries(value)) processValue(obj, key2, value2, keyReplacement, separator, keyStack); function formatKey(key: string, keyStack = []) { if (keyStack.length == 0) return key; else { let result = keyStack[0]; for (const parentKey of keyStack.slice(1)) result += "[" + parentKey + "]"; result += "[" + key + "]"; return result; } } } } /** * Validates the raw API response and returns a consistent response * @param {ResponseStatus} status First portion of the API response * @param {T} data Second part of the API response * @returns */ protected static formatAPIResponse<T extends APIResponse>(status: ResponseStatus, data: T[]): FormattedResponse<T> { if (!status.url) status.url = null; if (!data) data = []; return { status: status, data: data, }; } } /** * Search parameters for the `find()` methods. * Empty by default. Extend this interface to add more. */ export interface SearchParams extends PrimitiveMap, QueryParams { } /** * Query parameters for the `find()` methods. * Include the result limit and page number common for all endpoints */ export interface QueryParams extends PrimitiveMap { /** * Number of posts on the page. * Number between 1 and 320, defaults to 75 */ limit?: number | 75 | 320, /** * Page number. Two possible formats: * - Number between 1 and 750, defaults to 1 * - String, prefixed with either `a` (after)` or `b` (before), followed by an ID */ page?: number | string, }