/** * @since 1.0.0 */ import type { Brand } from "effect/Brand" import * as Context from "effect/Context" import type { Effect } from "effect/Effect" import * as Option from "effect/Option" import { type Pipeable, pipeArguments } from "effect/Pipeable" import * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import type * as Stream from "effect/Stream" import type * as Types from "effect/Types" import type * as HttpApiMiddleware from "./HttpApiMiddleware.js" import * as HttpApiSchema from "./HttpApiSchema.js" import type { HttpMethod } from "./HttpMethod.js" import * as HttpRouter from "./HttpRouter.js" import type { HttpServerRequest } from "./HttpServerRequest.js" import type { HttpServerResponse } from "./HttpServerResponse.js" import type * as Multipart from "./Multipart.js" /** * @since 1.0.0 * @category type ids */ export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpApiEndpoint") /** * @since 1.0.0 * @category type ids */ export type TypeId = typeof TypeId /** * @since 1.0.0 * @category guards */ export const isHttpApiEndpoint = (u: unknown): u is HttpApiEndpoint => Predicate.hasProperty(u, TypeId) /** * Represents a path segment. A path segment is a string that represents a * segment of a URL path. * * @since 1.0.0 * @category models */ export type PathSegment = `/${string}` /** * Represents an API endpoint. An API endpoint is mapped to a single route on * the underlying `HttpRouter`. * * @since 1.0.0 * @category models */ export interface HttpApiEndpoint< out Name extends string, out Method extends HttpMethod, in out Path = never, in out UrlParams = never, in out Payload = never, in out Headers = never, in out Success = void, in out Error = never, out R = never, out RE = never > extends Pipeable { readonly [TypeId]: TypeId readonly name: Name readonly path: PathSegment readonly method: Method readonly pathSchema: Option.Option> readonly urlParamsSchema: Option.Option> readonly payloadSchema: Option.Option> readonly headersSchema: Option.Option> readonly successSchema: Schema.Schema readonly errorSchema: Schema.Schema readonly annotations: Context.Context readonly middlewares: ReadonlySet /** * Add a schema for the success response of the endpoint. The status code * will be inferred from the schema, otherwise it will default to 200. */ addSuccess( schema: S, annotations?: { readonly status?: number | undefined } ): HttpApiEndpoint< Name, Method, Path, UrlParams, Payload, Headers, Exclude | Schema.Schema.Type, Error, R | Schema.Schema.Context, RE > /** * Add an error response schema to the endpoint. The status code * will be inferred from the schema, otherwise it will default to 500. */ addError( schema: E, annotations?: { readonly status?: number | undefined } ): HttpApiEndpoint< Name, Method, Path, UrlParams, Payload, Headers, Success, Error | Schema.Schema.Type, R, RE | Schema.Schema.Context > /** * Set the schema for the request body of the endpoint. The schema will be * used to validate the request body before the handler is called. * * For endpoints with no request body, the payload will use the url search * parameters. * * You can set a multipart schema to handle file uploads by using the * `HttpApiSchema.Multipart` combinator. */ setPayload

( schema: P & HttpApiEndpoint.ValidatePayload ): HttpApiEndpoint< Name, Method, Path, UrlParams, Schema.Schema.Type

, Headers, Success, Error, R | Schema.Schema.Context

, RE > /** * Set the schema for the path parameters of the endpoint. The schema will be * used to validate the path parameters before the handler is called. */ setPath( schema: Path & HttpApiEndpoint.ValidatePath ): HttpApiEndpoint< Name, Method, Schema.Schema.Type, UrlParams, Payload, Headers, Success, Error, R | Schema.Schema.Context, RE > /** * Set the schema for the url search parameters of the endpoint. */ setUrlParams( schema: UrlParams & HttpApiEndpoint.ValidateUrlParams ): HttpApiEndpoint< Name, Method, Path, Schema.Schema.Type, Payload, Headers, Success, Error, R | Schema.Schema.Context, RE > /** * Set the schema for the headers of the endpoint. The schema will be * used to validate the headers before the handler is called. */ setHeaders( schema: H & HttpApiEndpoint.ValidateHeaders ): HttpApiEndpoint< Name, Method, Path, UrlParams, Payload, Schema.Schema.Type, Success, Error, R | Schema.Schema.Context, RE > /** * Add a prefix to the path of the endpoint. */ prefix( prefix: PathSegment ): HttpApiEndpoint /** * Add an `HttpApiMiddleware` to the endpoint. */ middleware(middleware: Context.Tag): HttpApiEndpoint< Name, Method, Path, UrlParams, Payload, Headers, Success, Error | HttpApiMiddleware.HttpApiMiddleware.Error, R | I, RE | HttpApiMiddleware.HttpApiMiddleware.ErrorContext > /** * Add an annotation on the endpoint. */ annotate( tag: Context.Tag, value: S ): HttpApiEndpoint /** * Merge the annotations of the endpoint with the provided context. */ annotateContext( context: Context.Context ): HttpApiEndpoint } /** * @since 1.0.0 * @category models */ export declare namespace HttpApiEndpoint { /** * @since 1.0.0 * @category models */ export interface Any extends Pipeable { readonly [TypeId]: TypeId readonly name: string } /** * @since 1.0.0 * @category models */ export interface AnyWithProps extends HttpApiEndpoint {} /** * @since 1.0.0 * @category models */ export type Name = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Name : never /** * @since 1.0.0 * @category models */ export type Success = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Success : never /** * @since 1.0.0 * @category models */ export type Error = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Error : never /** * @since 1.0.0 * @category models */ export type PathParsed = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Path : never /** * @since 1.0.0 * @category models */ export type UrlParams = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _UrlParams : never /** * @since 1.0.0 * @category models */ export type Payload = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Payload : never /** * @since 1.0.0 * @category models */ export type Headers = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _Headers : never /** * @since 1.0.0 * @category models */ export type Request = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? & ([_Path] extends [never] ? {} : { readonly path: _Path }) & ([_UrlParams] extends [never] ? {} : { readonly urlParams: _UrlParams }) & ([_Payload] extends [never] ? {} : _Payload extends Brand ? { readonly payload: Stream.Stream } : { readonly payload: _Payload }) & ([_Headers] extends [never] ? {} : { readonly headers: _Headers }) & { readonly request: HttpServerRequest } : {} /** * @since 1.0.0 * @category models */ export type RequestRaw = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? & ([_Path] extends [never] ? {} : { readonly path: _Path }) & ([_UrlParams] extends [never] ? {} : { readonly urlParams: _UrlParams }) & ([_Headers] extends [never] ? {} : { readonly headers: _Headers }) & { readonly request: HttpServerRequest } : {} /** * @since 1.0.0 * @category models */ export type ClientRequest = ( & ([Path] extends [void] ? {} : { readonly path: Path }) & ([UrlParams] extends [never] ? {} : { readonly urlParams: UrlParams }) & ([Headers] extends [never] ? {} : { readonly headers: Headers }) & ([Payload] extends [never] ? {} : Payload extends infer P ? P extends Brand | Brand ? { readonly payload: FormData } : { readonly payload: P } : { readonly payload: Payload }) ) extends infer Req ? keyof Req extends never ? (void | { readonly withResponse?: WithResponse }) : Req & { readonly withResponse?: WithResponse } : void /** * @since 1.0.0 * @category models */ export type Context = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _R : never /** * @since 1.0.0 * @category models */ export type ErrorContext = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? _RE : never /** * @since 1.0.0 * @category models */ export type Handler = ( request: Types.Simplify> ) => Effect | HttpServerResponse, Error | E, R> /** * @since 1.0.0 * @category models */ export type HandlerRaw = ( request: Types.Simplify> ) => Effect | HttpServerResponse, Error | E, R> /** * @since 1.0.0 * @category models */ export type WithName = Extract /** * @since 1.0.0 * @category models */ export type ExcludeName = Exclude /** * @since 1.0.0 * @category models */ export type HandlerWithName = Handler< WithName, E, R > /** * @since 1.0.0 * @category models */ export type HandlerRawWithName = HandlerRaw< WithName, E, R > /** * @since 1.0.0 * @category models */ export type SuccessWithName = Success> /** * @since 1.0.0 * @category models */ export type ErrorWithName = Error> /** * @since 1.0.0 * @category models */ export type ContextWithName = Context> /** * @since 1.0.0 * @category models */ export type ErrorContextWithName = ErrorContext> /** * @since 1.0.0 * @category models */ export type ExcludeProvided = Exclude< R, | HttpRouter.HttpRouter.DefaultServices | HttpRouter.HttpRouter.Provided | HttpApiMiddleware.HttpApiMiddleware.ExtractProvides> > /** * @since 1.0.0 * @category models */ export type ValidatePath = S extends Schema.Schema ? [_I] extends [Readonly>] ? {} : `Path schema must be encodeable to strings` : {} /** * @since 1.0.0 * @category models */ export type ValidateUrlParams = S extends Schema.Schema ? [_I] extends [Readonly | undefined>>] ? {} : `UrlParams schema must be encodeable to strings` : {} /** * @since 1.0.0 * @category models */ export type ValidateHeaders = S extends Schema.Schema ? [_I] extends [Readonly>] ? {} : `Headers schema must be encodeable to strings` : {} /** * @since 1.0.0 * @category models */ export type ValidatePayload = Method extends HttpMethod.NoBody ? P extends Schema.Schema ? [_I] extends [Readonly | undefined>>] ? {} : `'${Method}' payload must be encodeable to strings` : {} : {} /** * @since 1.0.0 * @category models */ export type ValidateParams< Schemas extends ReadonlyArray, Prev extends (Schema.Schema.Any | Schema.PropertySignature.Any) = never > = Schemas extends [ infer Head extends (Schema.Schema.Any | Schema.PropertySignature.Any), ...infer Tail extends ReadonlyArray ] ? [ Head extends HttpApiSchema.Param ? HttpApiSchema.Param<_Name, any> extends Prev ? `Duplicate param: ${_Name}` : [Schema.Schema.Encoded & {}] extends [string] ? Head : `Must be encodeable to string: ${_Name}` : Head, ...ValidateParams ] : Schemas /** * @since 1.0.0 * @category models */ export type AddError = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? HttpApiEndpoint< _Name, _Method, _Path, _UrlParams, _Payload, _Headers, _Success, _Error | E, _R, _RE | R > : never /** * @since 1.0.0 * @category models */ export type AddContext = Endpoint extends HttpApiEndpoint< infer _Name, infer _Method, infer _Path, infer _UrlParams, infer _Payload, infer _Headers, infer _Success, infer _Error, infer _R, infer _RE > ? HttpApiEndpoint< _Name, _Method, _Path, _UrlParams, _Payload, _Headers, _Success, _Error | HttpApiMiddleware.HttpApiMiddleware.Error, _R | R, _RE | HttpApiMiddleware.HttpApiMiddleware.ErrorContext > : never /** * @since 1.0.0 * @category models */ export type PathEntries> = Extract extends infer K ? K extends keyof Schemas ? Schemas[K] extends HttpApiSchema.Param ? [_Name, _S] : Schemas[K] extends (Schema.Schema.Any | Schema.PropertySignature.Any) ? [K, Schemas[K]] : never : never : never type OptionalTypePropertySignature = | Schema.PropertySignature<"?:", any, PropertyKey, Schema.PropertySignature.Token, any, boolean, unknown> | Schema.PropertySignature<"?:", any, PropertyKey, Schema.PropertySignature.Token, never, boolean, unknown> | Schema.PropertySignature<"?:", never, PropertyKey, Schema.PropertySignature.Token, any, boolean, unknown> | Schema.PropertySignature<"?:", never, PropertyKey, Schema.PropertySignature.Token, never, boolean, unknown> /** * @since 1.0.0 * @category models */ export type ExtractPath> = Schema.Simplify< & { readonly [ Entry in Extract, [any, OptionalTypePropertySignature]> as Entry[0] ]?: Schema.Schema.Type } & { readonly [ Entry in Exclude, [any, OptionalTypePropertySignature]> as Entry[0] ]: Schema.Schema.Type } > /** * @since 1.0.0 * @category models */ export type Constructor = < const Schemas extends ReadonlyArray >( segments: TemplateStringsArray, ...schemas: ValidateParams ) => HttpApiEndpoint< Name, Method, Schemas["length"] extends 0 ? never : Types.Simplify>, never, never, never, void, never, Schema.Schema.Context > } const Proto = { [TypeId]: TypeId, pipe() { return pipeArguments(this, arguments) }, addSuccess( this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any, annotations?: { readonly status?: number } ) { schema = annotations?.status ? schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) : schema return makeProto({ ...this, successSchema: this.successSchema === HttpApiSchema.NoContent ? schema : HttpApiSchema.UnionUnify(this.successSchema, schema) }) }, addError(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any, annotations?: { readonly status?: number }) { return makeProto({ ...this, errorSchema: HttpApiSchema.UnionUnify( this.errorSchema, annotations?.status ? schema.annotations(HttpApiSchema.annotations({ status: annotations.status })) : schema ) }) }, setPayload(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { return makeProto({ ...this, payloadSchema: Option.some(schema) }) }, setPath(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { return makeProto({ ...this, pathSchema: Option.some(schema) }) }, setUrlParams(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { return makeProto({ ...this, urlParamsSchema: Option.some(schema) }) }, setHeaders(this: HttpApiEndpoint.AnyWithProps, schema: Schema.Schema.Any) { return makeProto({ ...this, headersSchema: Option.some(schema) }) }, prefix(this: HttpApiEndpoint.AnyWithProps, prefix: PathSegment) { return makeProto({ ...this, path: HttpRouter.prefixPath(this.path, prefix) as PathSegment }) }, middleware(this: HttpApiEndpoint.AnyWithProps, middleware: HttpApiMiddleware.TagClassAny) { return makeProto({ ...this, errorSchema: HttpApiSchema.UnionUnify(this.errorSchema, middleware.failure), middlewares: new Set([...this.middlewares, middleware]) }) }, annotate(this: HttpApiEndpoint.AnyWithProps, tag: Context.Tag, value: any) { return makeProto({ ...this, annotations: Context.add(this.annotations, tag, value) }) }, annotateContext(this: HttpApiEndpoint.AnyWithProps, context: Context.Context) { return makeProto({ ...this, annotations: Context.merge(this.annotations, context) }) } } const makeProto = < Name extends string, Method extends HttpMethod, Path, UrlParams, Payload, Headers, Success, Error, R, RE >(options: { readonly name: Name readonly path: PathSegment readonly method: Method readonly pathSchema: Option.Option> readonly urlParamsSchema: Option.Option> readonly payloadSchema: Option.Option> readonly headersSchema: Option.Option> readonly successSchema: Schema.Schema readonly errorSchema: Schema.Schema readonly annotations: Context.Context readonly middlewares: ReadonlySet }): HttpApiEndpoint => Object.assign(Object.create(Proto), options) /** * @since 1.0.0 * @category constructors */ export const make = (method: Method): { (name: Name): HttpApiEndpoint.Constructor (name: Name, path: PathSegment): HttpApiEndpoint } => ((name: string, ...args: [PathSegment]) => { if (args.length === 1) { return makeProto({ name, path: args[0], method, pathSchema: Option.none(), urlParamsSchema: Option.none(), payloadSchema: Option.none(), headersSchema: Option.none(), successSchema: HttpApiSchema.NoContent as any, errorSchema: Schema.Never as any, annotations: Context.empty(), middlewares: new Set() }) } return ( segments: TemplateStringsArray, ...schemas: ReadonlyArray ) => { let path = segments[0].replace(":", "::") as PathSegment let pathSchema = Option.none() if (schemas.length > 0) { const obj: Record = {} for (let i = 0; i < schemas.length; i++) { const schema = schemas[i] const key = HttpApiSchema.getParam(schema.ast) ?? String(i) const optional = schema.ast._tag === "PropertySignatureTransformation" && schema.ast.from.isOptional || schema.ast._tag === "PropertySignatureDeclaration" && schema.ast.isOptional obj[key] = schema path += `:${key}${optional ? "?" : ""}${segments[i + 1].replace(":", "::")}` } pathSchema = Option.some(Schema.Struct(obj)) } return makeProto({ name, path, method, pathSchema, urlParamsSchema: Option.none(), payloadSchema: Option.none(), headersSchema: Option.none(), successSchema: HttpApiSchema.NoContent as any, errorSchema: Schema.Never as any, annotations: Context.empty(), middlewares: new Set() }) } }) as any /** * @since 1.0.0 * @category constructors */ export const get: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("GET") /** * @since 1.0.0 * @category constructors */ export const post: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("POST") /** * @since 1.0.0 * @category constructors */ export const put: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("PUT") /** * @since 1.0.0 * @category constructors */ export const patch: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("PATCH") /** * @since 1.0.0 * @category constructors */ export const del: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("DELETE") /** * @since 1.0.0 * @category constructors */ export const head: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("HEAD") /** * @since 1.0.0 * @category constructors */ export const options: { /** * @since 1.0.0 * @category constructors */ (name: Name): HttpApiEndpoint.Constructor /** * @since 1.0.0 * @category constructors */ (name: Name, path: PathSegment): HttpApiEndpoint } = make("OPTIONS")