chore: split api responses from server, allowing reuse, prepare npm package

This commit is contained in:
Vjacheslav Trushkin 2024-01-17 09:45:43 +02:00
parent 2181b8022c
commit ceb3fc4394
16 changed files with 245 additions and 173 deletions

18
.npmignore Normal file
View File

@ -0,0 +1,18 @@
/.idea
/.vscode
.DS_Store
/.env
/.editorconfig
/.prettierrc
*.map
/docker.sh
/Dockerfile
/tsconfig*.*
/vitest.config.*
/.github
/src
/node_modules
/cache
/tmp
/icons
/tests

View File

@ -6,6 +6,12 @@ This repository contains Iconify API script. It is a HTTP server, written in Nod
- Generates SVG, which you can link to in HTML or stylesheet. - Generates SVG, which you can link to in HTML or stylesheet.
- Provides search engine for hosted icons, which can be used by icon pickers. - Provides search engine for hosted icons, which can be used by icon pickers.
## NPM Package
This package is also available at NPM, allowing using API code in custom wrappers.
NPM package contains only compiled files, to build custom Docker image you need to use source files from Git repository, not NPM package.
## Docker ## Docker
To build a Docker image, run `./docker.sh`. To build a Docker image, run `./docker.sh`.

View File

@ -3,8 +3,11 @@
"description": "Iconify API", "description": "Iconify API",
"author": "Vjacheslav Trushkin", "author": "Vjacheslav Trushkin",
"license": "MIT", "license": "MIT",
"private": true, "version": "3.1.0-beta.1",
"version": "3.0.2", "publishConfig": {
"access": "public",
"tag": "next"
},
"type": "module", "type": "module",
"bugs": "https://github.com/iconify/api/issues", "bugs": "https://github.com/iconify/api/issues",
"homepage": "https://github.com/iconify/api", "homepage": "https://github.com/iconify/api",

View File

@ -125,8 +125,9 @@ export function updateIconSets(): number {
/** /**
* Trigger update * Trigger update
*/ */
export function triggerIconSetsUpdate() { export function triggerIconSetsUpdate(done?: (success?: boolean) => void) {
if (!importers) { if (!importers) {
done?.();
return; return;
} }
console.log('Checking for updates...'); console.log('Checking for updates...');
@ -147,6 +148,10 @@ export function triggerIconSetsUpdate() {
.then((updated) => { .then((updated) => {
console.log(updated ? 'Update complete' : 'Nothing to update'); console.log(updated ? 'Update complete' : 'Nothing to update');
updateIconSets(); updateIconSets();
done?.(true);
}) })
.catch(console.error); .catch((err) => {
console.error(err);
done?.(false);
});
} }

View File

@ -0,0 +1,45 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkJSONPQuery, sendJSONResponse } from './json.js';
import { createIconsDataResponse } from '../responses/icons.js';
type CallbackResult = object | number;
/**
* Handle icons data API response
*/
export function handleIconsDataResponse(
prefix: string,
wrapJS: boolean,
query: FastifyRequest['query'],
res: FastifyReply
) {
const q = (query || {}) as Record<string, string>;
// Check for JSONP
const wrap = checkJSONPQuery(q, wrapJS, 'SimpleSVG._loaderCallback');
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Function to send response
const respond = (result: CallbackResult) => {
if (typeof result === 'number') {
res.send(result);
} else {
sendJSONResponse(result, q, wrap, res);
}
};
// Get result
const result = createIconsDataResponse(prefix, q);
if (result instanceof Promise) {
result.then(respond).catch((err) => {
console.error(err);
respond(500);
});
} else {
respond(result);
}
}

43
src/http/helpers/send.ts Normal file
View File

@ -0,0 +1,43 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkJSONPQuery, sendJSONResponse } from './json.js';
type CallbackResult = object | number;
/**
* Handle JSON API response generated by a callback
*/
export function handleJSONResponse(
req: FastifyRequest,
res: FastifyReply,
callback: (query: Record<string, string>) => CallbackResult | Promise<CallbackResult>
) {
const q = (req.query || {}) as Record<string, string>;
// Check for JSONP
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Function to send response
const respond = (result: CallbackResult) => {
if (typeof result === 'number') {
res.send(result);
} else {
sendJSONResponse(result, q, wrap, res);
}
};
// Get result
const result = callback(q);
if (result instanceof Promise) {
result.then(respond).catch((err) => {
console.error(err);
respond(500);
});
} else {
respond(result);
}
}

View File

@ -3,17 +3,18 @@ import fastifyFormBody from '@fastify/formbody';
import { appConfig, httpHeaders } from '../config/app.js'; import { appConfig, httpHeaders } from '../config/app.js';
import { runWhenLoaded } from '../data/loading.js'; import { runWhenLoaded } from '../data/loading.js';
import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name.js'; import { iconNameRoutePartialRegEx, iconNameRouteRegEx, splitIconName } from '../misc/name.js';
import { generateAPIv1IconsListResponse } from './responses/collection-v1.js'; import { createAPIv1IconsListResponse } from './responses/collection-v1.js';
import { generateAPIv2CollectionResponse } from './responses/collection-v2.js'; import { createAPIv2CollectionResponse } from './responses/collection-v2.js';
import { generateCollectionsListResponse } from './responses/collections.js'; import { createCollectionsListResponse } from './responses/collections.js';
import { generateIconsDataResponse } from './responses/icons.js'; import { handleIconsDataResponse } from './helpers/send-icons.js';
import { generateKeywordsResponse } from './responses/keywords.js'; import { createKeywordsResponse } from './responses/keywords.js';
import { generateLastModifiedResponse } from './responses/modified.js'; import { createLastModifiedResponse } from './responses/modified.js';
import { generateAPIv2SearchResponse } from './responses/search.js'; import { createAPIv2SearchResponse } from './responses/search.js';
import { generateSVGResponse } from './responses/svg.js'; import { generateSVGResponse } from './responses/svg.js';
import { generateUpdateResponse } from './responses/update.js'; import { generateUpdateResponse } from './responses/update.js';
import { initVersionResponse, versionResponse } from './responses/version.js'; import { initVersionResponse, versionResponse } from './responses/version.js';
import { generateIconsStyleResponse } from './responses/css.js'; import { generateIconsStyleResponse } from './responses/css.js';
import { handleJSONResponse } from './helpers/send.js';
/** /**
* Start HTTP server * Start HTTP server
@ -88,12 +89,12 @@ export async function startHTTPServer() {
// Icons data: /prefix/icons.json, /prefix.json // Icons data: /prefix/icons.json, /prefix.json
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.json', (req, res) => { server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.json', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res); handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
}); });
}); });
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').json', (req, res) => { server.get('/:prefix(' + iconNameRoutePartialRegEx + ').json', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res); handleIconsDataResponse((req.params as PrefixParams).prefix, false, req.query, res);
}); });
}); });
@ -107,19 +108,19 @@ export async function startHTTPServer() {
// Icons data: /prefix/icons.js, /prefix.js // Icons data: /prefix/icons.js, /prefix.js
server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.js', (req, res) => { server.get('/:prefix(' + iconNameRoutePartialRegEx + ')/icons.js', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res); handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
}); });
}); });
server.get('/:prefix(' + iconNameRoutePartialRegEx + ').js', (req, res) => { server.get('/:prefix(' + iconNameRoutePartialRegEx + ').js', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res); handleIconsDataResponse((req.params as PrefixParams).prefix, true, req.query, res);
}); });
}); });
// Last modification time // Last modification time
server.get('/last-modified', (req, res) => { server.get('/last-modified', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateLastModifiedResponse(req.query, res); handleJSONResponse(req, res, createLastModifiedResponse);
}); });
}); });
@ -127,26 +128,26 @@ export async function startHTTPServer() {
// Icon sets list // Icon sets list
server.get('/collections', (req, res) => { server.get('/collections', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateCollectionsListResponse(req.query, res); handleJSONResponse(req, res, createCollectionsListResponse);
}); });
}); });
// Icons list, API v2 // Icons list, API v2
server.get('/collection', (req, res) => { server.get('/collection', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateAPIv2CollectionResponse(req.query, res); handleJSONResponse(req, res, createAPIv2CollectionResponse);
}); });
}); });
// Icons list, API v1 // Icons list, API v1
server.get('/list-icons', (req, res) => { server.get('/list-icons', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateAPIv1IconsListResponse(req.query, res, false); handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, false));
}); });
}); });
server.get('/list-icons-categorized', (req, res) => { server.get('/list-icons-categorized', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateAPIv1IconsListResponse(req.query, res, true); handleJSONResponse(req, res, (q) => createAPIv1IconsListResponse(q, true));
}); });
}); });
@ -154,14 +155,14 @@ export async function startHTTPServer() {
// Search, currently version 2 // Search, currently version 2
server.get('/search', (req, res) => { server.get('/search', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateAPIv2SearchResponse(req.query, res); handleJSONResponse(req, res, createAPIv2SearchResponse);
}); });
}); });
// Keywords // Keywords
server.get('/keywords', (req, res) => { server.get('/keywords', (req, res) => {
runWhenLoaded(() => { runWhenLoaded(() => {
generateKeywordsResponse(req.query, res); handleJSONResponse(req, res, createKeywordsResponse);
}); });
}); });
} }

View File

@ -1,4 +1,3 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets.js'; import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { IconSetAPIv2IconsList } from '../../types/icon-set/extra.js'; import type { IconSetAPIv2IconsList } from '../../types/icon-set/extra.js';
import type { StoredIconSet } from '../../types/icon-set/storage.js'; import type { StoredIconSet } from '../../types/icon-set/storage.js';
@ -7,11 +6,13 @@ import type {
APIv1ListIconsCategorisedResponse, APIv1ListIconsCategorisedResponse,
APIv1ListIconsResponse, APIv1ListIconsResponse,
} from '../../types/server/v1.js'; } from '../../types/server/v1.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js'; import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
// Response results, depends on `categorised` option
type PossibleResults = APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse;
/** /**
* Send API v2 response * Create API v1 response
* *
* This response ignores the following parameters: * This response ignores the following parameters:
* - `aliases` -> always enabled * - `aliases` -> always enabled
@ -19,27 +20,15 @@ import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
* *
* Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled * Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled
*/ */
export function generateAPIv1IconsListResponse( export function createAPIv1IconsListResponse(
query: FastifyRequest['query'], query: Record<string, string>,
res: FastifyReply,
categorised: boolean categorised: boolean
) { ): PossibleResults | Record<string, PossibleResults> | number {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
function parse( function parse(
prefix: string, prefix: string,
iconSet: StoredIconSet, iconSet: StoredIconSet,
v2Cache: IconSetAPIv2IconsList v2Cache: IconSetAPIv2IconsList
): APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse { ): APIv1ListIconsResponse | APIv1ListIconsCategorisedResponse {
const icons = iconSet.icons;
// Generate common data // Generate common data
const base: APIv1ListIconsBaseResponse = { const base: APIv1ListIconsBaseResponse = {
prefix, prefix,
@ -48,13 +37,13 @@ export function generateAPIv1IconsListResponse(
if (v2Cache.title) { if (v2Cache.title) {
base.title = v2Cache.title; base.title = v2Cache.title;
} }
if (q.info && v2Cache.info) { if (query.info && v2Cache.info) {
base.info = v2Cache.info; base.info = v2Cache.info;
} }
if (q.aliases && v2Cache.aliases) { if (query.aliases && v2Cache.aliases) {
base.aliases = v2Cache.aliases; base.aliases = v2Cache.aliases;
} }
if (q.chars && v2Cache.chars) { if (query.chars && v2Cache.chars) {
base.chars = v2Cache.chars; base.chars = v2Cache.chars;
} }
@ -81,22 +70,20 @@ export function generateAPIv1IconsListResponse(
return result; return result;
} }
if (q.prefix) { if (query.prefix) {
const prefix = q.prefix; const prefix = query.prefix;
const iconSet = iconSets[prefix]?.item; const iconSet = iconSets[prefix]?.item;
if (!iconSet || !iconSet.apiV2IconsCache) { if (!iconSet || !iconSet.apiV2IconsCache) {
res.send(404); return 404;
return;
} }
sendJSONResponse(parse(prefix, iconSet, iconSet.apiV2IconsCache), q, wrap, res); return parse(prefix, iconSet, iconSet.apiV2IconsCache);
return;
} }
if (q.prefixes) { if (query.prefixes) {
const prefixes = filterPrefixesByPrefix( const prefixes = filterPrefixesByPrefix(
getPrefixes(), getPrefixes(),
{ {
prefixes: q.prefixes, prefixes: query.prefixes,
}, },
false false
); );
@ -125,20 +112,18 @@ export function generateAPIv1IconsListResponse(
if (!items.length) { if (!items.length) {
// Empty list // Empty list
res.send(404); return 404;
return;
} }
// Get all items // Get all items
const result = Object.create(null) as Record<string, ReturnType<typeof parse>>; const result = Object.create(null) as Record<string, PossibleResults>;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
result[item.prefix] = parse(item.prefix, item.iconSet, item.v2Cache); result[item.prefix] = parse(item.prefix, item.iconSet, item.v2Cache);
} }
sendJSONResponse(result, q, wrap, res); return result;
return;
} }
// Invalid // Invalid
res.send(400); return 400;
} }

View File

@ -1,7 +1,5 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { iconSets } from '../../data/icon-sets.js'; import { iconSets } from '../../data/icon-sets.js';
import type { APIv2CollectionResponse } from '../../types/server/v2.js'; import type { APIv2CollectionResponse } from '../../types/server/v2.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
/** /**
* Send API v2 response * Send API v2 response
@ -12,29 +10,18 @@ import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
* *
* Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled * Those parameters are always requested anyway, so does not make sense to re-create data in case they are disabled
*/ */
export function generateAPIv2CollectionResponse(query: FastifyRequest['query'], res: FastifyReply) { export function createAPIv2CollectionResponse(q: Record<string, string>): APIv2CollectionResponse | number {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Get icon set // Get icon set
const prefix = q.prefix; const prefix = q.prefix;
if (!prefix || !iconSets[prefix]) { if (!prefix || !iconSets[prefix]) {
res.send(404); return 404;
return;
} }
const iconSet = iconSets[prefix].item; const iconSet = iconSets[prefix].item;
const apiV2IconsCache = iconSet.apiV2IconsCache; const apiV2IconsCache = iconSet.apiV2IconsCache;
if (!apiV2IconsCache) { if (!apiV2IconsCache) {
// Disabled // Disabled
res.send(404); return 404;
return;
} }
// Generate response // Generate response
@ -52,5 +39,5 @@ export function generateAPIv2CollectionResponse(query: FastifyRequest['query'],
delete response.chars; delete response.chars;
} }
sendJSONResponse(response, q, wrap, res); return response;
} }

View File

@ -1,7 +1,5 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets.js'; import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { APIv2CollectionsResponse } from '../../types/server/v2.js'; import type { APIv2CollectionsResponse } from '../../types/server/v2.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js'; import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
/** /**
@ -12,15 +10,7 @@ import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
* Ignored parameters: * Ignored parameters:
* - hidden (always enabled) * - hidden (always enabled)
*/ */
export function generateCollectionsListResponse(query: FastifyRequest['query'], res: FastifyReply) { export function createCollectionsListResponse(q: Record<string, string>): APIv2CollectionsResponse {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Filter prefixes // Filter prefixes
const prefixes = filterPrefixesByPrefix(getPrefixes('info'), q, false); const prefixes = filterPrefixesByPrefix(getPrefixes('info'), q, false);
const response = Object.create(null) as APIv2CollectionsResponse; const response = Object.create(null) as APIv2CollectionsResponse;
@ -33,5 +23,5 @@ export function generateCollectionsListResponse(query: FastifyRequest['query'],
} }
} }
sendJSONResponse(response, q, wrap, res); return response;
} }

View File

@ -1,45 +1,59 @@
import type { FastifyReply, FastifyRequest } from 'fastify'; import { IconifyJSON } from '@iconify/types';
import { getStoredIconsData } from '../../data/icon-set/utils/get-icons.js'; import { getStoredIconsData } from '../../data/icon-set/utils/get-icons.js';
import { iconSets } from '../../data/icon-sets.js'; import { iconSets } from '../../data/icon-sets.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
/** /**
* Generate icons data * Generate icons data
*/ */
export function generateIconsDataResponse( export function createIconsDataResponse(
prefix: string, prefix: string,
wrapJS: boolean, q: Record<string, string>
query: FastifyRequest['query'], ): number | IconifyJSON | Promise<IconifyJSON | number> {
res: FastifyReply
) {
const q = (query || {}) as Record<string, string>;
const names = q.icons?.split(','); const names = q.icons?.split(',');
if (!names || !names.length) { if (!names || !names.length) {
// Missing or invalid icons parameter // Missing or invalid icons parameter
res.send(404); return 404;
return;
}
// Check for JSONP
const wrap = checkJSONPQuery(q, wrapJS, 'SimpleSVG._loaderCallback');
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
} }
// Get icon set // Get icon set
const iconSet = iconSets[prefix]; const iconSet = iconSets[prefix];
if (!iconSet) { if (!iconSet) {
// No such icon set // No such icon set
res.send(404); return 404;
return;
} }
// Get icons // Get icons, possibly sync
let syncData: IconifyJSON | undefined;
let resolveData: undefined | ((data: IconifyJSON) => void);
getStoredIconsData(iconSet.item, names, (data) => { getStoredIconsData(iconSet.item, names, (data) => {
// Send data // Send data
sendJSONResponse(data, q, wrap, res); if (resolveData) {
resolveData(data);
} else {
syncData = data;
}
});
if (syncData) {
return syncData;
}
return new Promise((resolve) => {
resolveData = resolve;
});
}
/**
* Awaitable version of createIconsDataResponse()
*/
export function createIconsDataResponseAsync(prefix: string, q: Record<string, string>): Promise<IconifyJSON | number> {
return new Promise((resolve, reject) => {
const result = createIconsDataResponse(prefix, q);
if (result instanceof Promise) {
result.then(resolve).catch(reject);
} else {
resolve(result);
}
}); });
} }

View File

@ -1,27 +1,16 @@
import { matchIconName } from '@iconify/utils/lib/icon/name'; import { matchIconName } from '@iconify/utils/lib/icon/name';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { searchIndex } from '../../data/search.js'; import { searchIndex } from '../../data/search.js';
import { getPartialKeywords } from '../../data/search/partial.js'; import { getPartialKeywords } from '../../data/search/partial.js';
import type { APIv3KeywordsQuery, APIv3KeywordsResponse } from '../../types/server/keywords.js'; import type { APIv3KeywordsQuery, APIv3KeywordsResponse } from '../../types/server/keywords.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
/** /**
* Generate icons data * Find full keywords for partial keyword
*/ */
export function generateKeywordsResponse(query: FastifyRequest['query'], res: FastifyReply) { export function createKeywordsResponse(q: Record<string, string>): number | APIv3KeywordsResponse {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Check if search data is available // Check if search data is available
const searchIndexData = searchIndex.data; const searchIndexData = searchIndex.data;
if (!searchIndexData) { if (!searchIndexData) {
res.send(404); return 404;
return;
} }
const keywords = searchIndexData.keywords; const keywords = searchIndexData.keywords;
@ -32,15 +21,16 @@ export function generateKeywordsResponse(query: FastifyRequest['query'], res: Fa
let failed = false; let failed = false;
if (typeof q.prefix === 'string') { if (typeof q.prefix === 'string') {
// Keywords should start with prefix
test = q.prefix; test = q.prefix;
suffixes = false; suffixes = false;
} else if (typeof q.keyword === 'string') { } else if (typeof q.keyword === 'string') {
// All keywords that contain keyword
test = q.keyword; test = q.keyword;
suffixes = true; suffixes = true;
} else { } else {
// Invalid query // Invalid query
res.send(400); return 400;
return;
} }
test = test.toLowerCase().trim(); test = test.toLowerCase().trim();
@ -71,5 +61,5 @@ export function generateKeywordsResponse(query: FastifyRequest['query'], res: Fa
matches: failed || invalid ? [] : getPartialKeywords(test, suffixes, searchIndexData)?.slice(0) || [], matches: failed || invalid ? [] : getPartialKeywords(test, suffixes, searchIndexData)?.slice(0) || [],
}; };
sendJSONResponse(response, q, wrap, res); return response;
} }

View File

@ -1,21 +1,11 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { getPrefixes, iconSets } from '../../data/icon-sets.js'; import { getPrefixes, iconSets } from '../../data/icon-sets.js';
import type { APIv3LastModifiedResponse } from '../../types/server/modified.js'; import type { APIv3LastModifiedResponse } from '../../types/server/modified.js';
import { checkJSONPQuery, sendJSONResponse } from '../helpers/json.js';
import { filterPrefixesByPrefix } from '../helpers/prefixes.js'; import { filterPrefixesByPrefix } from '../helpers/prefixes.js';
/** /**
* Generate icons data * Get last modified time for all icon sets
*/ */
export function generateLastModifiedResponse(query: FastifyRequest['query'], res: FastifyReply) { export function createLastModifiedResponse(q: Record<string, string>): number | APIv3LastModifiedResponse {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Filter prefixes // Filter prefixes
const prefixes = filterPrefixesByPrefix(getPrefixes(), q, false); const prefixes = filterPrefixesByPrefix(getPrefixes(), q, false);
@ -36,5 +26,5 @@ export function generateLastModifiedResponse(query: FastifyRequest['query'], res
} }
} }
sendJSONResponse(response, q, wrap, res); return response;
} }

View File

@ -14,28 +14,17 @@ const defaultSearchLimit = minSearchLimit * 2;
/** /**
* Send API v2 response * Send API v2 response
*/ */
export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res: FastifyReply) { export function createAPIv2SearchResponse(q: Record<string, string>): number | APIv2SearchResponse {
const q = (query || {}) as Record<string, string>;
const wrap = checkJSONPQuery(q);
if (!wrap) {
// Invalid JSONP callback
res.send(400);
return;
}
// Check if search data is available // Check if search data is available
const searchIndexData = searchIndex.data; const searchIndexData = searchIndex.data;
if (!searchIndexData) { if (!searchIndexData) {
res.send(404); return 404;
return;
} }
// Get query // Get query
const keyword = q.query; const keyword = q.query;
if (!keyword) { if (!keyword) {
res.send(400); return 400;
return;
} }
// Convert to params // Convert to params
@ -49,16 +38,14 @@ export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res:
if (v2Query.limit) { if (v2Query.limit) {
const limit = parseInt(v2Query.limit); const limit = parseInt(v2Query.limit);
if (!limit) { if (!limit) {
res.send(400); return 400;
return;
} }
params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit)); params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit));
} }
if (v2Query.min) { if (v2Query.min) {
const limit = parseInt(v2Query.min); const limit = parseInt(v2Query.min);
if (!limit) { if (!limit) {
res.send(400); return 400;
return;
} }
params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit)); params.limit = Math.max(minSearchLimit, Math.min(limit, maxSearchLimit));
params.softLimit = true; params.softLimit = true;
@ -68,8 +55,7 @@ export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res:
if (v2Query.start) { if (v2Query.start) {
start = parseInt(v2Query.start); start = parseInt(v2Query.start);
if (isNaN(start) || start < 0 || start >= params.limit) { if (isNaN(start) || start < 0 || start >= params.limit) {
res.send(400); return 400;
return;
} }
} }
@ -130,5 +116,5 @@ export function generateAPIv2SearchResponse(query: FastifyRequest['query'], res:
}; };
} }
sendJSONResponse(response, q, wrap, res); return response;
} }

View File

@ -1,30 +1,19 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
import { getImporters } from './config/icon-sets.js';
import { iconSetsStorage } from './data/icon-set/store/storage.js';
import { setImporters, updateIconSets } from './data/icon-sets.js';
import { loaded } from './data/loading.js'; import { loaded } from './data/loading.js';
import { cleanupStorageCache } from './data/storage/startup.js';
import { startHTTPServer } from './http/index.js'; import { startHTTPServer } from './http/index.js';
import { loadEnvConfig } from './misc/load-config.js'; import { loadEnvConfig } from './misc/load-config.js';
import { initAPI } from './init.js';
(async () => { (async () => {
// Configure environment // Configure environment
config(); config();
loadEnvConfig(); loadEnvConfig();
// Reset old cache
await cleanupStorageCache(iconSetsStorage);
// Start HTTP server // Start HTTP server
startHTTPServer(); startHTTPServer();
// Get all importers and load data // Init API
const importers = await getImporters(); await initAPI();
for (let i = 0; i < importers.length; i++) {
await importers[i].init();
}
setImporters(importers);
updateIconSets();
// Loaded // Loaded
loaded(); loaded();

20
src/init.ts Normal file
View File

@ -0,0 +1,20 @@
import { getImporters } from './config/icon-sets.js';
import { iconSetsStorage } from './data/icon-set/store/storage.js';
import { setImporters, updateIconSets } from './data/icon-sets.js';
import { cleanupStorageCache } from './data/storage/startup.js';
/**
* Init API
*/
export async function initAPI() {
// Reset old cache
await cleanupStorageCache(iconSetsStorage);
// Get all importers and load data
const importers = await getImporters();
for (let i = 0; i < importers.length; i++) {
await importers[i].init();
}
setImporters(importers);
updateIconSets();
}