Skip to content

Commit

Permalink
water-fountains#150 support forceRefresh byId
Browse files Browse the repository at this point in the history
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
  • Loading branch information
robstoll committed Dec 20, 2021
1 parent 88376dc commit 2d3b922
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 27 deletions.
6 changes: 2 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 6 additions & 6 deletions server/api/controllers/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -223,7 +223,7 @@ function sendJson(resp: Response, obj: Record<string, any> | 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<Fountain | undefined> {
function byId(req: Request, res: Response, forceRefresh: boolean): Promise<Fountain | undefined> {
const city = getSingleStringQueryParam(req, 'city');
const database = getSingleStringQueryParam(req, 'database');
if (!isDatabase(database)) {
Expand All @@ -238,15 +238,15 @@ function byId(req: Request, res: Response): Promise<Fountain | undefined> {

// l.info('controller.js byId in promise: '+cityS+' '+dbg);
const cityPromises: Promise<FountainCollection | void>[] = [];
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);
}
return Promise.all(cityPromises)
.then(
() => {
if (fountainCollection === undefined) {
if (forceRefresh || fountainCollection === undefined) {
fountainCollection = cityCache.get<FountainCollection>(city);
}
if (fountainCollection !== undefined) {
Expand Down
128 changes: 116 additions & 12 deletions server/api/controllers/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
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<T>(
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<T>(
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<T>(
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<T>(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<T>(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;
}
}
8 changes: 4 additions & 4 deletions server/common/build.info.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"declaration": false,

"strict": true,
"noUnusedLocals": true,
// checked by eslint
//"noUnusedLocals": true
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
Expand Down

0 comments on commit 2d3b922

Please sign in to comment.