NaturalCycles/nodejs-lib

View on GitHub
src/validation/joi/joi.shared.schemas.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
import {
  _numberEnumKeys,
  _numberEnumValues,
  _stringEnumKeys,
  _stringEnumValues,
  BaseDBEntity,
  NumberEnum,
  StringEnum,
} from '@naturalcycles/js-lib'
import { AlternativesSchema, AnySchema, ArraySchema, ObjectSchema } from 'joi'
import { Joi } from './joi.extensions'
import { NumberSchema } from './number.extensions'
import { StringSchema } from './string.extensions'

export const booleanSchema = Joi.boolean()
export const booleanDefaultToFalseSchema = Joi.boolean().default(false)
export const stringSchema = Joi.string()
export const stringSchemaTyped = <T>(): StringSchema<T> => Joi.string<T>()
export const numberSchema = Joi.number()
export const numberSchemaTyped = <T>(): NumberSchema<T> => Joi.number<T>()
export const integerSchema = Joi.number().integer()
export const percentageSchema = Joi.number().integer().min(0).max(100)
export const dateStringSchema = stringSchema.dateString()
export const binarySchema = Joi.binary()
export const dateObjectSchema = Joi.object().instance(Date)

const DATE_INTERVAL_REGEX = /^\d{4}-\d{2}-\d{2}\/\d{4}-\d{2}-\d{2}$/
export const dateIntervalStringSchema = stringSchema.regex(DATE_INTERVAL_REGEX).messages({
  'string.pattern.base': `must be a DateInterval string`,
})

/**
 * Allows all values of a String Enum.
 */
export const stringEnumValueSchema = <ENUM extends StringEnum>(
  en: ENUM,
): StringSchema<ENUM[keyof ENUM]> => Joi.string<ENUM[keyof ENUM]>().valid(..._stringEnumValues(en))

/**
 * Allows all keys of a String Enum.
 */
export const stringEnumKeySchema = <ENUM extends StringEnum>(en: ENUM): StringSchema =>
  Joi.string().valid(..._stringEnumKeys(en))

/**
 * Allows all values of a String Enum.
 */
export const numberEnumValueSchema = <ENUM extends NumberEnum>(
  en: ENUM,
): NumberSchema<ENUM[keyof ENUM]> => Joi.number<ENUM[keyof ENUM]>().valid(..._numberEnumValues(en))

/**
 * Allows all keys of a Number Enum.
 */
export const numberEnumKeySchema = <ENUM extends NumberEnum>(en: ENUM): StringSchema =>
  Joi.string().valid(..._numberEnumKeys(en))

export const urlSchema = (scheme: string | string[] = 'https'): StringSchema =>
  Joi.string().uri({ scheme })

export function arraySchema<T>(items?: AnySchema<T>): ArraySchema<T[]> {
  return items ? Joi.array().items(items) : Joi.array()
}

export function objectSchema<T>(schema?: {
  [key in keyof Partial<T>]: AnySchema<T[key]>
}): ObjectSchema<T> {
  return Joi.object(schema)
}

export function oneOfSchema<T = any>(...schemas: AnySchema[]): AlternativesSchema<T> {
  return Joi.alternatives(schemas)
}

export const anySchema = Joi.any()
export const anyObjectSchema: ObjectSchema = Joi.object().options({ stripUnknown: false })

export const BASE62_REGEX = /^[a-zA-Z0-9]+$/
export const BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/
export const BASE64URL_REGEX = /^[\w-/]+$/
export const base62Schema = stringSchema.regex(BASE62_REGEX)
export const base64Schema = stringSchema.regex(BASE64_REGEX)
export const base64UrlSchema = stringSchema.regex(BASE64URL_REGEX)

export const JWT_REGEX = /^[\w-]+\.[\w-]+\.[\w-]+$/
export const jwtSchema = stringSchema.regex(JWT_REGEX)

// 1g498efj5sder3324zer
/**
 * [a-zA-Z0-9_]*
 * 6-64 length
 */
export const idSchema = stringSchema.regex(/^[a-zA-Z0-9_]{6,64}$/)

export const idBase62Schema = base62Schema.min(8).max(64)
export const idBase64Schema = base64Schema.min(8).max(64)
export const idBase64UrlSchema = base64UrlSchema.min(8).max(64)

/**
 * `_` should NOT be allowed to be able to use slug-ids as part of natural ids with `_` separator.
 */
export const SLUG_REGEX = /^[a-z0-9-]*$/

/**
 * "Slug" - a valid URL, filename, etc.
 */
export const slugSchema = stringSchema.regex(SLUG_REGEX).min(1).max(255)

const TS_2500 = 16725225600 // 2500-01-01
const TS_2000 = 946684800 // 2000-01-01

/**
 * Between years 1970 and 2050
 */
export const unixTimestampSchema = numberSchema.integer().min(0).max(TS_2500)
/**
 * Between years 2000 and 2050
 */
export const unixTimestamp2000Schema = numberSchema.integer().min(TS_2000).max(TS_2500)
/**
 * Between years 1970 and 2050
 */
export const unixTimestampMillisSchema = numberSchema
  .integer()
  .min(0)
  .max(TS_2500 * 1000)
/**
 * Between years 2000 and 2050
 */
export const unixTimestampMillis2000Schema = numberSchema
  .integer()
  .min(TS_2000 * 1000)
  .max(TS_2500 * 1000)

// 2
export const verSchema = numberSchema.optional().integer().min(1).max(100)

/**
 * Be careful, by default emailSchema does TLD validation. To disable it - use `stringSchema.email({tld: false}).lowercase()`
 */
export const emailSchema = stringSchema.email().lowercase()

/**
 * Pattern is simplified for our use, it's not a canonical SemVer.
 */
export const SEM_VER_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/
export const semVerSchema = stringSchema.regex(SEM_VER_REGEX)
// todo: .error(() => 'should be SemVer')

export const userAgentSchema = stringSchema
  .min(5) // I've seen UA of `Android` (7 characters)
  .max(400)

export const utcOffsetSchema = numberSchema
  .min(-14 * 60)
  .max(14 * 60)
  .dividable(15)

export const ipAddressSchema = stringSchema.ip()

export const baseDBEntitySchema: ObjectSchema<BaseDBEntity> = objectSchema<BaseDBEntity>({
  id: stringSchema.optional(),
  created: unixTimestamp2000Schema.optional(),
  updated: unixTimestamp2000Schema.optional(),
})