import assert from 'assert'; import emojiRegex from 'emoji-regex'; import { isSafeInteger } from 'lodash'; import moment from 'moment-timezone'; import { ObjectID } from 'mongodb'; import saslprep from 'saslprep'; type InputType = string | number | Record | any[]; export type Converter = (value: any) => T; export type Validator = (value: Loose extends true ? any : InputType) => boolean; export type Type = readonly [Converter, Validator?, (boolean | 'convert')?]; export interface Types { // String outputs Content: Type; Key: Type; /** @deprecated */ Name: Type; Username: Type; Password: Type; UidOrName: Type; Email: Type; Filename: Type; DomainId: Type; ProblemId: Type; Role: Type; Title: Type; Emoji: Type; String: Type; // Number outputs Int: Type; UnsignedInt: Type; PositiveInt: Type; Float: Type; // Other ObjectID: Type; Boolean: Type; Date: Type; Time: Type; Range: (range: Array | Record) => Type; /** @deprecated */ Array: Type; NumericArray: Type; CommaSeperatedArray: Type; Set: Type>; Any: Type; ArrayOf: >(type: T) => (T extends Type ? Type : never); AnyOf: >(...type: T[]) => (T extends Type ? Type : never); } const basicString = (regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [ convert || ((v) => v.toString()), (v) => { const res = v.toString(); if (regex && !regex.test(res)) return false; if (cb && !cb(res)) return false; return !!res.length; }, ] as [(v) => string, (v) => boolean]; const saslprepString = (regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [ convert || ((v) => saslprep(v.toString().trim())), (v) => { try { const res = saslprep(v.toString().trim()); if (regex && !regex.test(res)) return false; if (cb && !cb(res)) return false; return !!res.length; } catch (e) { return false; } }, ] as [(v) => string, (v) => boolean]; export const Types: Types = { Content: [(v) => v.toString().trim(), (v) => v?.toString()?.trim() && v.toString().trim().length < 65536], Key: saslprepString(/^[a-zA-Z0-9-_]+$/), /** @deprecated */ Name: saslprepString(/^.{1,255}$/), Filename: saslprepString(/^[^\\/?#~!|*]{1,255}$/, (i) => !['con', '.', '..'].includes(i)), UidOrName: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2}|-?[0-9]+)$/), Username: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2})$/), Password: basicString(/^.{6,255}$/), ProblemId: saslprepString(/^[a-zA-Z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)), Email: saslprepString(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i), DomainId: saslprepString(/^[a-zA-Z][a-zA-Z0-9_]{3,31}$/), Role: saslprepString(/^[_0-9A-Za-z\u4e00-\u9fa5]{1,31}$/i), Title: basicString(/^.{1,64}$/), String: basicString(), Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && isSafeInteger(+v)], UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && isSafeInteger(+v)], PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && isSafeInteger(+v)], Float: [(v) => +v, (v) => Number.isFinite(+v)], ObjectID: [(v) => new ObjectID(v), ObjectID.isValid], Boolean: [(v) => !!(v && !['false', 'off'].includes(v)), null, 'convert'], Date: [ (v) => { const d = v.split('-'); assert(d.length === 3); return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; }, (v) => { const d = v.toString().split('-'); if (d.length !== 3) return false; const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`; return moment(st).isValid(); }, ], Time: [ (v) => { const t = v.split(':'); assert(t.length === 2); return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`; }, (v) => { const t = v.toString().split(':'); if (t.length !== 2) return false; return moment(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`).isValid(); }, ], Range: (range) => [ (v) => { if (range instanceof Array) { for (const item of range) { if (typeof item === 'number' && item === +v) return +v; if (item === v) return v; } } return v; }, (v) => { if (range instanceof Array) { for (const item of range) { if (typeof item === 'string' && item === v) return true; if (typeof item === 'number' && item === +v) return true; } } else { for (const key in range) { if (key === v) return true; } } return false; }, ], /** @deprecated suggested to use Types.ArrayOf instead. */ Array: [(v) => { if (v instanceof Array) return v; return v ? [v] : []; }, null], NumericArray: [(v) => { if (v instanceof Array) return v.map(Number); return v.split(',').map(Number); }, (v) => { if (v instanceof Array) return v.map(Number).every(Number.isSafeInteger); return v.toString().split(',').map(Number).every(Number.isSafeInteger); }], CommaSeperatedArray: [ (v) => v.toString().replace(/,/g, ',').split(',').map((e) => e.trim()).filter((i) => i), (v) => !!v.toString(), ], Set: [(v) => { if (v instanceof Array) return new Set(v); return v ? new Set([v]) : new Set(); }, null], Emoji: [ (v: string) => v.matchAll(emojiRegex()).next().value[0], (v) => emojiRegex().test(v.toString()), ], Any: [(v) => v, null], ArrayOf: (type) => [ (v) => { const arr = v instanceof Array ? v : [v]; return arr.map((i) => type[0](i)); }, (v) => { if (!type[1]) return true; const arr = v instanceof Array ? v : [v]; return arr.every((i) => type[1](i)); }, ] as any, AnyOf: (...types) => [ (v) => types.find((type) => type[1](v))[0](v), (v) => types.some((type) => type[1](v)), ] as any, };