From d9134cac49e092fee0879fa8a41803c1bc10a795 Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Sat, 15 Oct 2022 22:41:48 +0300 Subject: [PATCH] feat: support deprecated API v1 list-icons queries --- src/http/index.ts | 13 +++ src/http/responses/collection-v1.ts | 132 ++++++++++++++++++++++++++++ src/http/responses/collection-v2.ts | 1 - src/types/server/v1.ts | 72 +++++++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/http/responses/collection-v1.ts create mode 100644 src/types/server/v1.ts diff --git a/src/http/index.ts b/src/http/index.ts index bf8a986..e147a82 100644 --- a/src/http/index.ts +++ b/src/http/index.ts @@ -2,6 +2,7 @@ import fastify, { FastifyReply } from 'fastify'; import { appConfig } from '../config/app'; import { runWhenLoaded } from '../data/loading'; import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name'; +import { generateAPIv1IconsListResponse } from './responses/collection-v1'; import { generateAPIv2CollectionResponse } from './responses/collection-v2'; import { generateCollectionsListResponse } from './responses/collections'; import { generateIconsDataResponse } from './responses/icons'; @@ -122,6 +123,18 @@ export async function startHTTPServer() { }); }); + // Icons list, API v1 + server.get('/list-icons', (req, res) => { + runWhenLoaded(() => { + generateAPIv1IconsListResponse(req.query, res, false); + }); + }); + server.get('/list-icons-categorized', (req, res) => { + runWhenLoaded(() => { + generateAPIv1IconsListResponse(req.query, res, true); + }); + }); + // Update icon sets server.get('/update', (req, res) => { generateUpdateResponse(req.query, res); diff --git a/src/http/responses/collection-v1.ts b/src/http/responses/collection-v1.ts new file mode 100644 index 0000000..b172f48 --- /dev/null +++ b/src/http/responses/collection-v1.ts @@ -0,0 +1,132 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { getPrefixes, iconSets } from '../../data/icon-sets'; +import type { StoredIconSet } from '../../types/icon-set/storage'; +import type { + APIv1ListIconsBaseResponse, + APIv1ListIconsCategorisedResponse, + APIv1ListIconsResponse, +} from '../../types/server/v1'; +import { checkJSONPQuery, sendJSONResponse } from '../helpers/json'; +import { filterPrefixesByPrefix } from '../helpers/prefixes'; + +/** + * Send API v2 response + * + * This response ignores the following parameters: + * - `aliases` -> always enabled + * - `hidden` -> always enabled + * + * Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled + */ +export function generateAPIv1IconsListResponse( + query: FastifyRequest['query'], + res: FastifyReply, + categorised: boolean +) { + const q = (query || {}) as Record; + + const wrap = checkJSONPQuery(q); + if (!wrap) { + // Invalid JSONP callback + res.send(400); + return; + } + + function parse(prefix: string, iconSet: StoredIconSet): APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse { + const v2Cache = iconSet.apiV2IconsCache; + const rendered = v2Cache.rendered; + + // Generate common data + const base: APIv1ListIconsBaseResponse = { + prefix, + total: rendered.total, + }; + if (rendered.title) { + base.title = rendered.title; + } + if (q.info && rendered.info) { + base.info = rendered.info; + } + if (q.aliases && rendered.aliases) { + base.aliases = rendered.aliases; + } + if (q.chars && v2Cache.chars) { + base.chars = v2Cache.chars; + } + + // Add icons + if (categorised) { + const result = base as APIv1ListIconsCategorisedResponse; + if (rendered.categories) { + result.categories = rendered.categories; + } + if (rendered.uncategorized) { + result.uncategorized = rendered.uncategorized; + } + return result; + } + + const result = base as APIv1ListIconsResponse; + result.icons = Array.from(iconSet.icons.visible); + return result; + } + + if (q.prefix) { + const prefix = q.prefix; + const iconSet = iconSets[prefix]?.item; + if (!iconSet) { + res.send(404); + return; + } + sendJSONResponse(parse(prefix, iconSet), q, wrap, res); + return; + } + + if (q.prefixes) { + const prefixes = filterPrefixesByPrefix( + getPrefixes(), + { + prefixes: q.prefixes, + }, + false + ); + + // Retrieve all items + interface Item { + prefix: string; + iconSet: StoredIconSet; + } + const items: Item[] = []; + for (let i = 0; i < prefixes.length; i++) { + const prefix = prefixes[i]; + const iconSet = iconSets[prefix]?.item; + if (iconSet) { + items.push({ + prefix, + iconSet, + }); + if (items.length > 10) { + break; + } + } + } + + if (!items.length) { + // Empty list + res.send(404); + return; + } + + // Get all items + const result = Object.create(null) as Record>; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + result[item.prefix] = parse(item.prefix, item.iconSet); + } + sendJSONResponse(result, q, wrap, res); + return; + } + + // Invalid + res.send(400); +} diff --git a/src/http/responses/collection-v2.ts b/src/http/responses/collection-v2.ts index 02a1e1a..36f35d0 100644 --- a/src/http/responses/collection-v2.ts +++ b/src/http/responses/collection-v2.ts @@ -1,5 +1,4 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; -import { generateIconSetIconsTree } from '../../data/icon-set/lists/icons'; import { iconSets } from '../../data/icon-sets'; import type { APIv2CollectionResponse } from '../../types/server/v2'; import { checkJSONPQuery, sendJSONResponse } from '../helpers/json'; diff --git a/src/types/server/v1.ts b/src/types/server/v1.ts new file mode 100644 index 0000000..f4c6955 --- /dev/null +++ b/src/types/server/v1.ts @@ -0,0 +1,72 @@ +import type { IconifyInfo, IconifyJSON } from '@iconify/types'; + +/** + * /list-icons + * /list-icons-categorized + * + * Do not use, supported only for legacy software that should no longer be used + */ +export interface APIv1ListIconsParams { + // Icon set prefix + prefix: string; + + // Include info in response + info?: boolean; + + // Include aliases in response + aliases?: boolean; + + // Include characters in response + chars?: boolean; +} + +export interface APIv1ListIconsBaseResponse { + // Icon set prefix + prefix: string; + + // Number of icons (duplicate of info?.total) + total: number; + + // Icon set title, if available (duplicate of info?.name) + title?: string; + + // Icon set info + info?: IconifyInfo; + + // List of aliases, key = alias, value = parent icon + aliases?: Record; + + // Characters, key = character, value = icon name + chars?: Record; +} + +export interface APIv1ListIconsResponse extends APIv1ListIconsBaseResponse { + // Icons + icons: string[]; +} + +export interface APIv1ListIconsCategorisedResponse extends APIv1ListIconsBaseResponse { + // Icons, sorted by category + categories?: Record; + + // Icons, sorted by category + uncategorized?: string[]; + + // Themes + themes?: IconifyJSON['themes']; +} + +/** + * Same as above, but with `prefixes` parameter set + * + * Result is object, where prefix is key, value is icons list + */ +export interface APIv1ListIconsPrefixedParams extends Omit { + // Comma separated list of prefix matches: 'mdi,mdi-' + // If value ends with '-', it is treated as partial prefix + prefixes: string; +} + +export type APIv1ListIconsPrefixedResponse = Record; + +export type APIv1ListIconsCategorisedPrefixedResponse = Record;