From 2d3b9226e0cb9269b4a332e153042377dc3b3d9e Mon Sep 17 00:00:00 2001 From: Robert Stoll Date: Mon, 20 Dec 2021 10:31:35 +0100 Subject: [PATCH] #150 support forceRefresh byId moreover: - fix getSingleBooleanQueryParam: looks like it is not always automatically parsed as boolean - cleanup eslint, no-unused-vars was duplicated resulting in undesired compile error --- .eslintrc.js | 6 +- server/api/controllers/controller.ts | 12 +-- server/api/controllers/utils.ts | 128 ++++++++++++++++++++++++--- server/common/build.info.ts | 8 +- tsconfig.json | 3 +- 5 files changed, 130 insertions(+), 27 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 04c57f10..c9d29d85 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,14 +39,12 @@ module.exports = { ], rules: { - 'no-unused-vars': 'off', // same as "@typescript-eslint/no-unused-vars": "off", + '@typescript-eslint/no-unused-vars': 'off', 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ - 'warn', + 'error', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }, ], - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - '@typescript-eslint/explicit-module-boundary-types': 'error', '@typescript-eslint/array-type': 'error', '@typescript-eslint/ban-tslint-comment': 'error', diff --git a/server/api/controllers/controller.ts b/server/api/controllers/controller.ts index 9352e782..0600c2c9 100644 --- a/server/api/controllers/controller.ts +++ b/server/api/controllers/controller.ts @@ -22,7 +22,7 @@ import { } from '../../common/constants'; import sharedConstants from './../../common/shared-constants'; import { Request, Response } from 'express'; -import { getSingleBooleanQueryParam, getSingleStringQueryParam } from './utils'; +import { getSingleBooleanQueryParam, getSingleNumericQueryParam, getSingleStringQueryParam } from './utils'; import { Fountain, FountainCollection, GalleryValue, isDatabase } from '../../common/typealias'; import { hasWikiCommonsCategories } from '../../common/wikimedia-types'; import { ImageLike } from '../../../config/text2img'; @@ -78,11 +78,11 @@ export class Controller { // When requesting detailed information for a single fountain, there are two types of queries getSingle(req: Request, res: Response): void { const queryType = getSingleStringQueryParam(req, 'queryType'); - const refresh = getSingleBooleanQueryParam(req, 'refresh', /* isOptional = */ true); + const refresh = getSingleBooleanQueryParam(req, 'refresh', /* isOptional = */ true) ?? false; if (queryType === 'byId') { l.info(`controller.js getSingle byId: refresh: ${refresh}`); - byId(req, res); + byId(req, res, refresh); } else { res.status(400).send('only byId supported'); } @@ -223,7 +223,7 @@ function sendJson(resp: Response, obj: Record | undefined, dbg: str /** * Function to respond to request by returning the fountain as defined by the provided identifier */ -function byId(req: Request, res: Response): Promise { +function byId(req: Request, res: Response, forceRefresh: boolean): Promise { const city = getSingleStringQueryParam(req, 'city'); const database = getSingleStringQueryParam(req, 'database'); if (!isDatabase(database)) { @@ -238,7 +238,7 @@ function byId(req: Request, res: Response): Promise { // l.info('controller.js byId in promise: '+cityS+' '+dbg); const cityPromises: Promise[] = []; - if (fountainCollection === undefined) { + if (forceRefresh || fountainCollection === undefined) { l.info('controller.js byId: ' + city + ' not found in cache ' + dbg + ' - start city lazy load'); const genLocPrms = generateLocationDataAndCache(city, cityCache); cityPromises.push(genLocPrms); @@ -246,7 +246,7 @@ function byId(req: Request, res: Response): Promise { return Promise.all(cityPromises) .then( () => { - if (fountainCollection === undefined) { + if (forceRefresh || fountainCollection === undefined) { fountainCollection = cityCache.get(city); } if (fountainCollection !== undefined) { diff --git a/server/api/controllers/utils.ts b/server/api/controllers/utils.ts index bcdd1de9..7ee1648d 100644 --- a/server/api/controllers/utils.ts +++ b/server/api/controllers/utils.ts @@ -1,27 +1,131 @@ import { Request } from 'express'; +import { isArray, isObject } from 'lodash'; export function getSingleStringQueryParam(req: Request, paramName: string): string; export function getSingleStringQueryParam(req: Request, paramName: string, isOptional: true): string | undefined; export function getSingleStringQueryParam(req: Request, paramName: string, isOptional = false): string | undefined { - return getSingleQueryParam(req, paramName, isOptional, 'string'); + return getSingleQueryParamTypeOfCheck(req, paramName, isOptional, 'string'); } -export function getSingleNumberQueryParam(req: Request, paramName: string): number; -export function getSingleNumberQueryParam(req: Request, paramName: string, isOptional: true): number | undefined; -export function getSingleNumberQueryParam(req: Request, paramName: string, isOptional = false): number | undefined { - return getSingleQueryParam(req, paramName, isOptional, 'number'); +export function getSingleNumericQueryParam(req: Request, paramName: string): number; +export function getSingleNumericQueryParam(req: Request, paramName: string, isOptional: true): number | undefined; +export function getSingleNumericQueryParam(req: Request, paramName: string, isOptional = false): number | undefined { + return getSingleQueryParam( + req, + paramName, + isOptional, + 'numeric', + v => isNumeric(v), + v => Number(v) + ); +} + +export function isNumeric(v: string | undefined): boolean { + if (typeof v === 'number') return true; + if (typeof v !== 'string') return false; + return ( + // we also use parseFloat next to Number because Number returns 0 for a blank string and we don't want to accept a blank string + // on the other hand parseFloat accepts things like `10 bananas` which we also don't want, thus the combination + !isNaN(Number(v)) && !isNaN(parseFloat(v)) + ); } export function getSingleBooleanQueryParam(req: Request, paramName: string): boolean; export function getSingleBooleanQueryParam(req: Request, paramName: string, isOptional: true): boolean | undefined; export function getSingleBooleanQueryParam(req: Request, paramName: string, isOptional = false): boolean | undefined { - return getSingleQueryParam(req, paramName, isOptional, 'boolean'); + return getSingleQueryParam( + req, + paramName, + isOptional, + 'boolean', + v => typeof v === 'boolean' || v === 'true' || v === 'false', + v => (typeof v === 'boolean' ? v : v === 'true') + ); +} + +function getSingleQueryParamTypeOfCheck( + req: Request, + paramName: string, + isOptional: boolean, + type: string +): T | undefined { + return getSingleQueryParam( + req, + paramName, + isOptional, + type, + v => typeof v === type, + v => v as unknown as T + ); +} + +interface ParsedQs { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +} + +function getSingleQueryParam( + req: Request, + paramName: string, + isOptional: boolean, + type: string, + typeCheck: (v: string | undefined) => boolean, + typeConversion: (v: string | undefined) => T +): T | undefined { + const param: undefined | string | string[] | ParsedQs[] = throwIfParsedQs(req.query[paramName], paramName); + if (isArray(param)) { + return getSingleQueryParamFromArray(param, paramName, isOptional, type, typeCheck, typeConversion); + } else { + return typeCheckAndConvertParam(param, paramName, isOptional, type, typeCheck, typeConversion); + } +} + +function getSingleQueryParamFromArray( + arr: string[] | ParsedQs[], + paramName: string, + isOptional: boolean, + type: string, + typeCheck: (v: string | undefined) => boolean, + typeConversion: (v: string | undefined) => T +): T | undefined { + if (arr.length === 0 && isOptional) { + return undefined; + } else if (arr.length > 1) { + throw Error(`${paramName} is not a single parameter, was ${JSON.stringify(arr)} with type ${typeof arr}`); + } else { + return typeCheckAndConvertParam( + throwIfParsedQs(arr[0], paramName), + paramName, + isOptional, + type, + typeCheck, + typeConversion + ); + } +} + +function typeCheckAndConvertParam( + param: string | undefined, + paramName: string, + isOptional: boolean, + type: string, + typeCheck: (v: string | undefined) => boolean, + typeConversion: (v: string | undefined) => T +): T | undefined { + if (param === undefined && isOptional) { + return undefined; + } else if (typeCheck(param)) { + return typeConversion(param); + } else { + throw Error(`${paramName} was of a wrong type, expected ${type} was ${JSON.stringify(param)} ${typeof param}`); + } } -function getSingleQueryParam(req: Request, paramName: string, isOptional: boolean, type: string): T | undefined { - const v = req.query[paramName]; - // looks like we sometimes get numbers or booleans and not string even though query[x] does not include it in its type signature - if (typeof v === type) return v as unknown as T; - else if (v === undefined && isOptional) return undefined; - else throw Error(`${paramName} is not a single parameter, was ${JSON.stringify(v)} with type ${typeof v}`); +function throwIfParsedQs(param: T | ParsedQs, paramName: string): T { + if (isObject(param)) { + throw Error( + `${paramName} is not a single parameter, was an object ${JSON.stringify(param)} with type ${typeof param}` + ); + } else { + return param; + } } diff --git a/server/common/build.info.ts b/server/common/build.info.ts index 900d300b..d07f72ba 100644 --- a/server/common/build.info.ts +++ b/server/common/build.info.ts @@ -1,9 +1,9 @@ // this file is automatically generated by git.version.js script const buildInfo = { version: '', - revision: 'de728a0', - branch: '#147-remove-deprecated-byCoords', - commit_time: '2021-12-17 23:49:37 +0100', - build_time: 'Mon Dec 20 2021 08:44:55 GMT+0100 (Central European Standard Time)', + revision: '88376dc', + branch: '#150-forceRefresh-id', + commit_time: '2021-12-20 09:34:27 +0100', + build_time: 'Mon Dec 20 2021 10:30:17 GMT+0100 (Central European Standard Time)', }; export default buildInfo; diff --git a/tsconfig.json b/tsconfig.json index 6548e592..dfb2e863 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "declaration": false, "strict": true, - "noUnusedLocals": true, + // checked by eslint + //"noUnusedLocals": true "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true,