edgespec
Version:
Write Winter-CG compatible routes with filesystem routing and tons of features
246 lines (237 loc) • 13.6 kB
TypeScript
import { Primitive, ReadonlyDeep } from 'type-fest';
import { z } from 'zod';
import { SecuritySchemeObject } from 'openapi3-ts/oas31';
/**
* This just pulls the "`I`" from a `Record<I, any>`
*
* Had to create this utility since `keyof Record<I, any>` doesn't seem to give I :/
*/
type InferRecordKey<R extends Record<any, any>> = R extends Record<infer K, any> ? K : never;
/**
* Executes an "array map" function on a tuple through a record
*
* For example:
*
* ```ts
* Map = { [name in "arthur"|"jane"|"john"]: `hi ${name}!` }
* Arr = [ "jane", "arthur" ]
*
* ->
*
* [ "hi jane!", "hi arthur!" ]
* ```
*/
type MapArray<Map extends {
[index: string | number | symbol]: any;
}, Arr extends readonly (keyof Map)[]> = Arr extends readonly [
infer K extends keyof Map,
...infer Remaining extends (keyof Map)[]
] ? readonly [Map[K], ...MapArray<Map, Remaining>] : readonly [];
type ResponseTypeToContext<ResponseType extends SerializableToResponse | Response> = Exclude<ResponseType, Response> extends EdgeSpecJsonResponse<infer T> ? {
json: typeof EdgeSpecResponse.json<T>;
} : Exclude<ResponseType, Response> extends EdgeSpecMultiPartFormDataResponse<infer T> ? {
multipartFormData: typeof EdgeSpecResponse.multipartFormData<T>;
} : Exclude<ResponseType, Response> extends EdgeSpecCustomResponse<infer T, infer C> ? {
custom: typeof EdgeSpecResponse.custom<T, C>;
} : {
json: typeof EdgeSpecResponse.json<unknown>;
multipartFormData: typeof EdgeSpecResponse.multipartFormData<Record<string, string>>;
};
type QueryArrayFormat = "brackets" | "comma" | "repeat";
type QueryArrayFormats = readonly QueryArrayFormat[];
type GlobalSpec = {
authMiddleware: Record<string, Middleware<any, any>>;
beforeAuthMiddleware?: readonly Middleware<any, any, {}>[];
afterAuthMiddleware?: readonly Middleware<any, any>[];
openapi?: {
apiName?: string;
productionServerUrl?: string;
securitySchemas?: Record<string, SecuritySchemeObject>;
readonly globalSchemas?: Record<string, z.ZodTypeAny>;
};
shouldValidateResponses?: boolean;
supportedArrayFormats?: QueryArrayFormats;
/**
* If an endpoint accepts multiple auth methods and they all fail, this hook will be called with the errors thrown by the middlewares.
* You can inspect the errors and throw a more generic error in this hook if you want.
*/
onMultipleAuthMiddlewareFailures?: (errors: unknown[]) => void;
};
type GetAuthMiddlewaresFromGlobalSpec<GS extends GlobalSpec> = InferRecordKey<GS["authMiddleware"]>;
type RouteSpec<AuthMiddlewares extends string> = {
methods: readonly HTTPMethods[];
jsonBody?: z.ZodTypeAny;
multiPartFormData?: z.ZodTypeAny;
queryParams?: z.ZodTypeAny;
commonParams?: z.ZodTypeAny;
urlEncodedFormData?: z.ZodTypeAny;
routeParams?: z.ZodTypeAny;
jsonResponse?: z.ZodTypeAny;
multipartFormDataResponse?: z.ZodTypeAny;
customResponseMap?: Record<string, z.ZodTypeAny>;
auth?: AuthMiddlewares | readonly AuthMiddlewares[] | "none" | null | undefined;
middleware?: MiddlewareChain<any>;
openApiMetadata?: any;
onMultipleAuthMiddlewareFailures?: (errors: unknown[]) => void;
};
type CustomResponseMapToEdgeSpecResponse<M extends Record<string, z.ZodTypeAny>, K extends keyof M = keyof M> = K extends keyof M & string ? EdgeSpecCustomResponse<z.output<M[K]>, K> : never;
type GetRouteSpecResponseType<GS extends GlobalSpec, RS extends RouteSpec<GetAuthMiddlewaresFromGlobalSpec<GS>>> = (RS["jsonResponse"] extends z.ZodTypeAny ? EdgeSpecJsonResponse<z.output<RS["jsonResponse"]>> : never) | (RS["multipartFormDataResponse"] extends z.ZodObject<any> ? EdgeSpecMultiPartFormDataResponse<z.output<RS["multipartFormDataResponse"]>> : never) | (RS["customResponseMap"] extends Record<string, z.ZodTypeAny> ? CustomResponseMapToEdgeSpecResponse<RS["customResponseMap"]> : never) | Response;
/**
* Collects all middleware request options from a Global/Route spec pair
*
* This goes in order of execution:
* 1. Global middlewares (intersection)
* 2. Auth middlewares (union)
* 3. Global middlewares after auth (intersection)
* 4. Route middlewares (intersection)
*/
type GetMiddlewareRequestOptions<GS extends GlobalSpec, RS extends RouteSpec<GetAuthMiddlewaresFromGlobalSpec<GS>>> = (RS["jsonBody"] extends infer ZT extends z.ZodTypeAny ? {
jsonBody: z.output<ZT>;
} : {}) & (RS["multiPartFormData"] extends infer ZT extends z.ZodTypeAny ? {
multiPartFormData: z.output<ZT>;
} : {}) & (RS["queryParams"] extends infer ZT extends z.ZodTypeAny ? {
query: z.output<ZT>;
} : {}) & (RS["commonParams"] extends infer ZT extends z.ZodTypeAny ? {
commonParams: z.output<ZT>;
} : {}) & (RS["urlEncodedFormData"] extends infer ZT extends z.ZodTypeAny ? {
urlEncodedFormData: z.output<ZT>;
} : {}) & (RS["routeParams"] extends infer ZT extends z.ZodTypeAny ? {
routeParams: z.output<ZT>;
} : {
routeParams: EdgeSpecRouteParams;
});
type GetMiddlewareRequestContext<GS extends GlobalSpec, RS extends RouteSpec<GetAuthMiddlewaresFromGlobalSpec<GS>>> = AccumulateMiddlewareChainResultOptions<GS["beforeAuthMiddleware"] extends MiddlewareChain ? GS["beforeAuthMiddleware"] : readonly [], "intersection"> & (RS["auth"] extends "none" ? {} : undefined extends RS["auth"] ? {} : AccumulateMiddlewareChainResultOptions<MapMiddlewares<GS["authMiddleware"], RS["auth"] extends undefined | null ? "none" : Exclude<RS["auth"], undefined | null>>, "union">) & AccumulateMiddlewareChainResultOptions<GS["afterAuthMiddleware"] extends MiddlewareChain ? GS["afterAuthMiddleware"] : readonly [], "intersection"> & AccumulateMiddlewareChainResultOptions<RS["middleware"] extends MiddlewareChain<any> ? RS["middleware"] : readonly [], "intersection">;
type EdgeSpecRouteFnFromSpecs<GS extends GlobalSpec, RS extends RouteSpec<GetAuthMiddlewaresFromGlobalSpec<GS>>> = EdgeSpecRouteFn<GetMiddlewareRequestOptions<GS, RS>, GetRouteSpecResponseType<GS, RS>, GetMiddlewareRequestContext<GS, RS> & ResponseTypeToContext<GetRouteSpecResponseType<GS, RS>>>;
type CreateWithRouteSpecFn<GS extends GlobalSpec> = <const RS extends RouteSpec<GetAuthMiddlewaresFromGlobalSpec<GS>>>(routeSpec: RS) => (route: EdgeSpecRouteFnFromSpecs<GS, RS>) => EdgeSpecRouteFn;
type HTTPMethods = "GET" | "POST" | "DELETE" | "PUT" | "PATCH" | "HEAD" | "OPTIONS";
type EdgeSpecRouteParams = {
[routeParam: string]: string | string[];
};
interface EdgeSpecRequestOptions {
routeParams: EdgeSpecRouteParams;
edgeSpec: EdgeSpecRouteBundle;
}
type EdgeSpecRequest<T = {}> = EdgeSpecRequestOptions & Request & T;
interface SerializableToResponse {
/**
* Serialize the response to a Response object
*
* @throws z.ZodError if the response does not match the schema
* @param schema - the schema to validate the response against
*/
serializeToResponse(schema: z.ZodTypeAny): Response;
statusCode(): number;
}
type ValidFormDataValue = Primitive | Blob;
declare abstract class EdgeSpecResponse implements SerializableToResponse {
protected options: ResponseInit;
abstract serializeToResponse(schema: z.ZodTypeAny): Response;
statusCode(): number;
status(status: number): this;
header(key: string, value: string): this;
headers(headers: HeadersInit): this;
statusText(statusText: string): this;
constructor(options?: ResponseInit);
static json<T>(...args: ConstructorParameters<typeof EdgeSpecJsonResponse<T>>): EdgeSpecJsonResponse<T>;
static multipartFormData<T extends Record<string, ValidFormDataValue>>(...args: ConstructorParameters<typeof EdgeSpecMultiPartFormDataResponse<T>>): EdgeSpecMultiPartFormDataResponse<T>;
static custom<T, const C extends string>(...args: ConstructorParameters<typeof EdgeSpecCustomResponse<T, C>>): EdgeSpecCustomResponse<T, C>;
}
declare class EdgeSpecJsonResponse<T> extends EdgeSpecResponse {
data: T;
constructor(data: T, options?: ResponseInit);
serializeToResponse(schema: z.ZodTypeAny): Response;
}
declare class EdgeSpecCustomResponse<T, const C extends string> extends EdgeSpecResponse {
data: T;
contentType: C;
constructor(data: T, contentType: C, options?: ResponseInit);
serializeToResponse(schema: z.ZodTypeAny): Response;
}
declare class MiddlewareResponseData extends EdgeSpecResponse {
constructor(options?: ResponseInit);
serializeToResponse(): Response;
}
declare class EdgeSpecMultiPartFormDataResponse<T extends Record<string, ValidFormDataValue>> extends EdgeSpecResponse {
data: T;
constructor(data: T, options?: ResponseInit);
serializeToResponse(schema: z.ZodTypeAny): Response;
}
type EdgeSpecRouteFn<RequestOptions = EdgeSpecRequestOptions, ResponseType extends SerializableToResponse | Response = Response, Context = ResponseTypeToContext<ResponseType>> = ((req: EdgeSpecRequest<RequestOptions>, ctx: Context) => ResponseType | Promise<ResponseType>) & {
_globalSpec?: GlobalSpec;
_routeSpec?: RouteSpec<any>;
};
type Middleware<RequiredContext = {}, NewContext = {}, RequestOptions = {}> = (request: EdgeSpecRequest<{
routeParams: Readonly<Record<string, unknown>>;
} & RequestOptions>, ctx: RequiredContext & Partial<NewContext>, next: (request: EdgeSpecRequest, ctx: RequiredContext & Partial<NewContext>) => Promise<Response>) => Response | SerializableToResponse | Promise<Response | SerializableToResponse>;
type MiddlewareChain<RequiredOptions = any> = readonly Middleware<RequiredOptions>[];
/**
* Collect all result options from a middleware chain
*
* For example:
*
* ```ts
* Middleware<{}, { auth: string }>
* Middleware<{}, { user: string }>
*
* ->
*
* { auth: string, user: string }
* ```
*/
type AccumulateMiddlewareChainResultOptions<MiddlewareChain, AccumulationType extends "union" | "intersection"> = MiddlewareChain extends readonly [
Middleware<any, infer ResultOptions>,
...infer Remaining
] ? AccumulationType extends "intersection" ? ResultOptions & AccumulateMiddlewareChainResultOptions<Remaining, AccumulationType> : ResultOptions | AccumulateMiddlewareChainResultOptions<Remaining, AccumulationType> : MiddlewareChain extends readonly any[] ? AccumulationType extends "intersection" ? {} : never : never;
/**
* Picks out a subset of middlewares from a map, maintaining the order given in the array
*
* For example:
*
* ```ts
* MiddlewareMap = {
* "session_token": Middleware<{}, { session_token: string }>,
* "pat": Middleware<{}, { pat: string }>,
* "api_token": Middleware<{}, { api_token: string }>
* }
* Middlewares = ["session_token", "pat"]
*
* ->
*
* [ Middleware<{}, { session_token: string }>, Middleware<{}, { pat: string }> ]
* ```
*
*/
type MapMiddlewares<MiddlewareMap extends {
[mw: string]: Middleware;
}, Middlewares extends readonly (keyof MiddlewareMap)[] | keyof MiddlewareMap | "none"> = Middlewares extends readonly (keyof MiddlewareMap)[] ? MapArray<MiddlewareMap, Middlewares> : Middlewares extends infer K extends keyof MiddlewareMap ? readonly [MiddlewareMap[K]] : readonly [];
type EdgeSpecRouteMatcher = (pathname: string) => {
matchedRoute: string;
routeParams: EdgeSpecRouteParams;
} | undefined | null;
type EdgeSpecRouteMap = Record<string, EdgeSpecRouteFn>;
interface EdgeSpecOptions {
handle404?: EdgeSpecRouteFn;
}
interface MakeRequestOptions {
/**
* Defaults to true. When true, we will attempt to automatically remove any pathname prefix from the request. This is useful when you're hosting an EdgeSpec service on a subpath of your application.
*
* For example, if you're hosting an EdgeSpec service "Foo" at /foo/[...path], then this option will automatically remove the /foo prefix from the request so that the Foo service only sees /[...path].
*
* This currently only works if your parent application is also an EdgeSpec service and it's hosting the child service on a wildcard route (/foo/[...path]).
*/
automaticallyRemovePathnamePrefix?: boolean;
/**
* If you want to manually remove a pathname prefix, you can specify it here. `automaticallyRemovePathnamePrefix` must be false when specifying this option.
*/
removePathnamePrefix?: string;
middleware?: Middleware[];
}
type EdgeSpecRouteBundle = ReadonlyDeep<EdgeSpecOptions & {
routeMatcher: EdgeSpecRouteMatcher;
routeMapWithHandlers: EdgeSpecRouteMap;
makeRequest: (request: Request, options?: MakeRequestOptions) => Promise<Response>;
}>;
type EdgeSpecAdapter<Options extends Array<unknown> = [], ReturnValue = void> = (edgeSpec: EdgeSpecRouteBundle, ...options: Options) => ReturnValue;
declare function makeRequestAgainstEdgeSpec(edgeSpec: EdgeSpecRouteBundle, options?: MakeRequestOptions): (request: Request) => Promise<Response>;
export { type CreateWithRouteSpecFn as C, type EdgeSpecAdapter as E, type GlobalSpec as G, type HTTPMethods as H, type Middleware as M, type QueryArrayFormat as Q, type ResponseTypeToContext as R, type SerializableToResponse as S, type MiddlewareChain as a, type EdgeSpecRouteFn as b, type EdgeSpecRequest as c, type EdgeSpecRouteBundle as d, EdgeSpecJsonResponse as e, EdgeSpecCustomResponse as f, EdgeSpecResponse as g, type EdgeSpecRouteMatcher as h, type EdgeSpecRouteMap as i, type EdgeSpecOptions as j, type EdgeSpecRequestOptions as k, EdgeSpecMultiPartFormDataResponse as l, makeRequestAgainstEdgeSpec as m, MiddlewareResponseData as n, type EdgeSpecRouteParams as o, type GetAuthMiddlewaresFromGlobalSpec as p, type QueryArrayFormats as q, type RouteSpec as r, type EdgeSpecRouteFnFromSpecs as s };