import url from 'url'; import to from 'await-to-js'; import axios, { AxiosResponse, AxiosError } from 'axios'; import { ParsedUrlQuery } from 'querystring'; import { ApiError } from './errors'; import { User, UserJSON, Check, Aspect } from './user'; import { Availability, AvailabilityJSON } from './availability'; /** * All the supported filters for the search view. * @property aspect - The currently filtered aspect (i.e. tutors or mentors). * @property langs - The languages that the user can speak; OR category. * @property subjects - The subjects the user can tutor/mentor for; OR category. * @property availability - When the user is available; OR category. * @property checks - The checks the user has passed; AND category. * @property orgs - The organizations that the user belongs to; OR category. */ export interface QueryInterface { aspect: Aspect; langs: Option[]; subjects: Option[]; availability: Availability; checks: Option[]; orgs: Option[]; } export interface QueryJSON { aspect: Aspect; langs: Option[]; subjects: Option[]; availability: AvailabilityJSON; checks: Option[]; orgs: Option[]; } export interface Option { label: string; value: T; } export type Callback = (value: T) => void; // [aspect, subjects, availability, langs] export type QueryDepArray = [ Aspect, Option[], Availability, Option[] ]; export class Query implements QueryInterface { public aspect: Aspect = 'mentoring'; public subjects: Option[] = []; public availability: Availability = new Availability(); public langs: Option[] = []; public checks: Option[] = []; public orgs: Option[] = []; public constructor(query: Partial = {}) { Object.entries(query).forEach(([key, val]: [string, any]) => { /* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */ if (val && key in this) (this as Record)[key] = val; }); } public get url(): string { return this.getURL('/search'); } public get endpoint(): string { return this.getURL('/api/users'); } private getURL(pathname: string): string { return url.format({ pathname, query: { aspect: encodeURIComponent(this.aspect), langs: encodeURIComponent(JSON.stringify(this.langs)), subjects: encodeURIComponent(JSON.stringify(this.subjects)), availability: this.availability.toURLParam(), checks: encodeURIComponent(JSON.stringify(this.checks)), orgs: encodeURIComponent(JSON.stringify(this.orgs)), }, }); } public async search(pathname = '/api/users'): Promise> { const [err, res] = await to< AxiosResponse, AxiosError >(axios.get(this.getURL(pathname))); if (err && err.response) { console.error(`[ERROR] Search API responded: ${err.response.data.msg}`); throw new Error(err.response.data.msg); } else if (err && err.request) { console.error('[ERROR] Search API did not respond.'); throw new Error('Search API did not respond.'); } else if (err) { console.error('[ERROR] While sending request:', err); throw new Error(`While sending request: ${err.message}`); } else { return (res as AxiosResponse).data.map((user: UserJSON) => User.fromJSON(user) ); } } public static fromURLParams(params: ParsedUrlQuery): Query { const orgs: string = params.orgs as string; const checks: string = params.checks as string; const langs: string = params.langs as string; const aspect: string = params.aspect as string; const subjects: string = params.subjects as string; const availability: string = params.availability as string; return new Query({ orgs: orgs ? (JSON.parse(decodeURIComponent(orgs)) as Option[]) : [], checks: checks ? (JSON.parse(decodeURIComponent(checks)) as Option[]) : [], langs: langs ? (JSON.parse(decodeURIComponent(langs)) as Option[]) : [], subjects: subjects ? (JSON.parse(decodeURIComponent(subjects)) as Option[]) : [], availability: availability ? Availability.fromURLParam(availability) : new Availability(), aspect: aspect ? (decodeURIComponent(aspect) as Aspect) : 'mentoring', }); } /** * Turns this `Query` object into an array of "stable" values that can be * passed around and compared to uniquely identify this `Query`. * @example * // You can do this because the `QueryDepArray` will be unique for each * // different query. * const { data: results } = useSWR(['/api/users', ...query.toDepArray]); * @deprecated Currently unused because we haven't implemented SWR in the * search view yet... but it's coming soon. * @see {@link https://github.com/vercel/swr#multiple-arguments} * @see {@link https://overreacted.io/a-complete-guide-to-useeffect/#but-i-cant-put-this-function-inside-an-effect} */ public toDepArray(): QueryDepArray { const { aspect, subjects, availability, langs } = this; return [aspect, subjects, availability, langs]; } public static fromDepArray(arr: QueryDepArray): Query { const [aspect, subjects, availability, langs] = arr; return new Query({ aspect, subjects, availability, langs }); } public static fromJSON(json: QueryJSON): Query { const { availability, ...rest } = json; return new Query({ ...rest, availability: Availability.fromJSON(availability), }); } }