import * as server from './server'; import * as client from './client'; import * as make from './make'; import { fromReflection } from './make'; import * as valueClass from './value-class'; import * as reflection from './reflection-type'; import * as typeTag from './type-tag'; import { ValueClass } from './value-class'; export { redirect } from './redirect'; export { make, fromReflection, server, client, valueClass, reflection }; export const noContentContentType = 'oatsNoContent' as const; export { serialize } from './serialize'; export { getType, withType } from './type-tag'; type Scalar = number | string | boolean; const typeWitnessKey = Symbol(); const tagKey = Symbol(); type ScalarWithoutBrand = V extends ShapedClass ? ScalarWithoutBrand : V; // helper to set necessary metadata for valueclass instances export function instanceAssign( to: ValueClass, value: Record, opts: make.MakeOptions | { unSafeSet: boolean } | undefined, builder: make.Maker ) { Object.assign(to, opts && 'unSafeSet' in opts ? value : builder(value, opts).success()); // when we create an instance of a ValueClass we need to propagate the type information from 'value' to the newly // constructed instance const existingType = typeTag.getType(value); if (existingType) { return typeTag.withType(to, existingType); } return to; } export type ShapeOf = A extends Scalar ? ScalarWithoutBrand : A extends ShapedClass ? S : unknown extends A ? unknown : A extends Array ? Array> : A extends ReadonlyArray ? ReadonlyArray> : { [K in keyof A]: ShapeOf }; class ShapedClass { // @ts-ignore protected [typeWitnessKey]: Shape; } class BrandedClass { // @ts-ignore protected [tagKey]: Tag; } export type Shaped = Type extends Nully ? Type : Type & ShapedClass; type Nully = null | undefined; // note: null and undefined intersected with a {} result in 'never' export type BrandedScalar = Type extends Nully ? Type : Shaped & BrandedClass; export function setHeaders< Status extends number, ConntentType, Value, Headers extends Record >( response: server.Response>, headers: Headers ): server.Response { return { ...response, headers }; } export function noContent( status: Status ): server.Response> { return { status, value: { contentType: noContentContentType, value: null }, headers: {} }; } export function json( status: Status, value: Value ): server.Response> { return { status, value: { contentType: 'application/json', value }, headers: {} }; } export function text( status: Status, value: Value ): server.Response> { return { status, value: { contentType: 'text/plain', value }, headers: {} }; } export function set( to: Cls, set: Cls extends valueClass.ValueClass ? Partial> : never ): make.Make { return (to as any).constructor.make({ ...to, ...set }); } export type ValueType = | valueClass.ValueClass | { [key: string]: any } | readonly any[] | string | boolean | number; export function map( value: A, predicate: (a: any) => a is T, fn: (p: T, traversalPath: string[]) => T ): A { return mapInternal(value, predicate, fn, []); } function mapInternal( value: A, predicate: (a: any) => a is T, fn: (p: T, traversalPath: string[]) => T, traversalPath: string[] ): A { if (predicate(value)) { value = fn(value, traversalPath) as any; } if (Array.isArray(value)) { const arr: any = value.map((item, index) => mapInternal(item, predicate, fn, traversalPath.concat(String(index))) ); return selectArray(value, arr) as any; } if (value && typeof value === 'object') { const record: any = {}; Object.keys(value).map(key => { record[key] = mapInternal((value as any)[key], predicate, fn, traversalPath.concat(key)); }); return selectRecord(value, record); } return value; } export function getAll( value: A, predicate: (a: any) => a is T ): readonly T[] { return getAllInternal(value, predicate); } function getAllInternal( value: A, predicate: (a: any) => a is T ): readonly T[] { const match: T[] = []; if (predicate(value)) { match.push(value); } if (Array.isArray(value)) { return [ ...match, ...value.reduce((acc, item) => acc.concat(getAllInternal(item, predicate)), []) ]; } if (value && typeof value === 'object') { return [ ...match, ...Object.values(value).reduce((acc, item) => acc.concat(getAllInternal(item, predicate)), []) ]; } return match; } export async function pmap( value: A, predicate: (a: any) => a is T, map: (p: T, traversalPath: string[]) => Promise ): Promise { return pmapInternal(value, predicate, map, []); } function isPromise(p: any): p is Promise { return p && typeof p.then === 'function'; } function pmapInternal( value: A, predicate: (a: any) => a is T, map: (p: T, traversalPath: string[]) => Promise, traversalPath: string[] ): Promise | A { if (predicate(value)) { value = map(value, traversalPath) as any; } if (isPromise(value)) { return value.then(n => pmapComposite(n, predicate, map, traversalPath)); } return pmapComposite(value, predicate, map, traversalPath); } function selectArray(original: T[], newArray: T[]): T[] { for (let i = 0; i < original.length; i++) { if (original[i] !== newArray[i]) { return newArray; } } return original.length !== newArray.length ? newArray : original; } function selectRecord(original: T, newRecord: T) { const changed = Object.keys(original).some(key => { return original[key] !== newRecord[key]; }); if (!changed) { return original; } if (original instanceof valueClass.ValueClass) { return set(original, newRecord).success() as T; } return newRecord; } function pmapArray( value: A[], predicate: (v: any) => v is T, map: (v: T, traversalPath: string[]) => Promise, traversalPath: string[] ): Promise | A[] { const mapped = value.map((n, i) => pmapInternal(n, predicate, map, traversalPath.concat(String(i))) ); if (mapped.some(isPromise)) { return Promise.all(mapped).then(newValues => { return selectArray(value, newValues); }); } return selectArray(value, mapped as any); } function pmapObject, T extends ValueType>( value: A, predicate: (v: any) => v is T, map: (v: T, traversalPath: string[]) => Promise, traversalPath: string[] ): Promise | A { const record: any = {}; const promises: Array> = []; Object.keys(value).forEach(key => { const v = pmapInternal((value as any)[key], predicate, map, traversalPath.concat(key)); if (isPromise(v)) { promises.push( v.then(result => { record[key] = result; }) ); } else { record[key] = v; } }); if (promises.length) { return Promise.all(promises).then(() => selectRecord(value, record)); } return selectRecord(value, record); } function pmapComposite( value: A, predicate: (v: any) => v is T, map: (v: T, traversalPath: string[]) => Promise, traversalPath: string[] ): Promise | A { if (Array.isArray(value)) { return pmapArray(value, predicate, map, traversalPath) as any; } if (value && typeof value === 'object') { return pmapObject(value, predicate, map, traversalPath); } return value; }