| 1 | /** |
| 2 | * Defines endpoint declarations used inside an HTTP API group. |
| 3 | * |
| 4 | * An endpoint records a stable identifier, HTTP method, router path, request schemas, |
| 5 | * response schemas, declared errors, middleware, and annotations. Endpoint |
| 6 | * values are declarations, not handlers: builders use them to decode requests, |
| 7 | * type handler input, encode responses, generate OpenAPI metadata, and derive |
| 8 | * generated-client call signatures. This module also includes HTTP method |
| 9 | * constructors, payload and response schema helpers, and type utilities used by |
| 10 | * builders and generated clients. |
| 11 | * |
| 12 | * @stability unstable |
| 13 | * @since 4.0.0 |
| 14 | */ |
| 15 | import * as Arr from "../Array.ts" |
| 16 | import type { Brand } from "../Brand.ts" |
| 17 | import * as Context from "../Context.ts" |
| 18 | import type { Effect } from "../Effect.ts" |
| 19 | import type * as Sse from "../encoding/Sse.ts" |
| 20 | import { identity, memoize } from "../Function.ts" |
| 21 | import type { HttpMethod } from "../http/HttpMethod.ts" |
| 22 | import * as HttpRouter from "../http/HttpRouter.ts" |
| 23 | import type { HttpServerRequest } from "../http/HttpServerRequest.ts" |
| 24 | import type { HttpServerResponse } from "../http/HttpServerResponse.ts" |
| 25 | import type * as Multipart from "../http/Multipart.ts" |
| 26 | import { type Pipeable, pipeArguments } from "../Pipeable.ts" |
| 27 | import * as Predicate from "../Predicate.ts" |
| 28 | import * as Schema from "../Schema.ts" |
| 29 | import * as AST from "../SchemaAST.ts" |
| 30 | import type * as Stream from "../Stream.ts" |
| 31 | import type { Simplify } from "../Struct.ts" |
| 32 | import type * as Types from "../Types.ts" |
| 33 | import type * as HttpApiGroup from "./HttpApiGroup.ts" |
| 34 | import type * as HttpApiMiddleware from "./HttpApiMiddleware.ts" |
| 35 | import * as HttpApiSchema from "./HttpApiSchema.ts" |
| 36 | import * as MediaType from "./internal/mediaType.ts" |
| 37 | |
| 38 | const TypeId = "~effect/http-api/HttpApiEndpoint" |
| 39 | |
| 40 | /** |
| 41 | * Returns `true` when a value is an `HttpApiEndpoint`, narrowing the value to the |
| 42 | * endpoint interface. |
| 43 | * |
| 44 | * @stability unstable |
| 45 | * @category guards |
| 46 | * @since 4.0.0 |
| 47 | */ |
| 48 | export const isHttpApiEndpoint = (u: unknown): u is Top => Predicate.hasProperty(u, TypeId) |
| 49 | |
| 50 | type SuccessType<S> = S extends HttpApiSchema.WithHeaders< |
| 51 | infer _Inner, |
| 52 | infer _Headers |
| 53 | > ? HttpApiSchema.withHeaders<SuccessType<_Inner>, _Headers["Type"]> |
| 54 | : S extends HttpApiSchema.StreamSse< |
| 55 | infer _Events, |
| 56 | infer _Error, |
| 57 | infer _Value |
| 58 | > ? Stream.Stream<_Value, _Error["Type"], never> |
| 59 | : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream<Uint8Array, unknown, never> |
| 60 | : S extends Schema.Constraint ? S["Type"] |
| 61 | : never |
| 62 | |
| 63 | type SuccessEncodingServices<S> = S extends HttpApiSchema.StreamSse< |
| 64 | infer _Events, |
| 65 | infer _Error, |
| 66 | infer _Value |
| 67 | > ? _Events["EncodingServices"] | _Error["EncodingServices"] |
| 68 | : S extends HttpApiSchema.StreamUint8Array ? never |
| 69 | : S extends Schema.Constraint ? S["EncodingServices"] |
| 70 | : never |
| 71 | |
| 72 | type SuccessDecodingServices<S> = S extends HttpApiSchema.StreamSse< |
| 73 | infer _Events, |
| 74 | infer _Error, |
| 75 | infer _Value |
| 76 | > ? _Events["DecodingServices"] | _Error["DecodingServices"] |
| 77 | : S extends HttpApiSchema.StreamUint8Array ? never |
| 78 | : S extends Schema.Constraint ? S["DecodingServices"] |
| 79 | : never |
| 80 | |
| 81 | type UnwrapReadonlyArray<S> = S extends ReadonlyArray<infer A> ? A : S |
| 82 | |
| 83 | type ExtractBufferedSuccess<S extends SuccessConstraint> = Exclude< |
| 84 | Extract<UnwrapReadonlyArray<S>, Schema.Top>, |
| 85 | HttpApiSchema.StreamSchema | HttpApiSchema.WithHeaders<Schema.Top, Schema.Top> |
| 86 | > |
| 87 | |
| 88 | type ExtractStreamSuccess<S extends SuccessConstraint> = UnwrapReadonlyArray<S> extends infer Success ? |
| 89 | Success extends HttpApiSchema.StreamSchema ? Success : never |
| 90 | : never |
| 91 | |
| 92 | type ExtractWithHeadersSuccess<S extends SuccessConstraint> = UnwrapReadonlyArray<S> extends infer Success ? |
| 93 | Success extends HttpApiSchema.WithHeaders<infer _Inner, infer _Headers> ? HttpApiSchema.WithHeaders< |
| 94 | _Inner extends HttpApiSchema.StreamSchema ? _Inner : Schema.toCodecJson<_Inner>, |
| 95 | Schema.toCodecStringTree<_Headers> |
| 96 | > : |
| 97 | never |
| 98 | : never |
| 99 | |
| 100 | type ToSuccessCodec<S extends SuccessConstraint> = [ExtractBufferedSuccess<S>] extends [never] ? |
| 101 | ExtractStreamSuccess<S> | ExtractWithHeadersSuccess<S> |
| 102 | : Schema.toCodecJson<ExtractBufferedSuccess<S>> | ExtractStreamSuccess<S> | ExtractWithHeadersSuccess<S> |
| 103 | |
| 104 | type ToJsonCodec<S> = [S] extends [never] ? never |
| 105 | : [S] extends [Schema.Constraint] ? Schema.toCodecJson<S> |
| 106 | : never |
| 107 | |
| 108 | type ToStringTreeCodec<S> = [S] extends [never] ? never |
| 109 | : [S] extends [Schema.Struct.Fields] ? Schema.toCodecStringTree<Schema.Struct<S>> |
| 110 | : [S] extends [Schema.Constraint] ? Schema.toCodecStringTree<S> |
| 111 | : never |
| 112 | |
| 113 | type RequestFromParts<Endpoint, ParamsType, QueryType, PayloadType, HeadersType> = |
| 114 | & ([ParamsType] extends [never] ? {} : { readonly params: Simplify<ParamsType> }) |
| 115 | & ([QueryType] extends [never] ? {} : { readonly query: Simplify<QueryType> }) |
| 116 | & ([PayloadType] extends [never] ? {} |
| 117 | : PayloadType extends Brand<HttpApiSchema.MultipartStreamTypeId> ? |
| 118 | { readonly payload: Stream.Stream<Multipart.Part, Multipart.MultipartError> } |
| 119 | : { readonly payload: Simplify<PayloadType> }) |
| 120 | & ([HeadersType] extends [never] ? {} : { readonly headers: Simplify<HeadersType> }) |
| 121 | & { |
| 122 | readonly request: HttpServerRequest |
| 123 | readonly endpoint: Endpoint |
| 124 | readonly group: HttpApiGroup.Top |
| 125 | } |
| 126 | |
| 127 | type RequestRawFromParts<Endpoint, ParamsType, QueryType, HeadersType> = |
| 128 | & ([ParamsType] extends [never] ? {} : { readonly params: Simplify<ParamsType> }) |
| 129 | & ([QueryType] extends [never] ? {} : { readonly query: Simplify<QueryType> }) |
| 130 | & ([HeadersType] extends [never] ? {} : { readonly headers: Simplify<HeadersType> }) |
| 131 | & { |
| 132 | readonly request: HttpServerRequest |
| 133 | readonly endpoint: Endpoint |
| 134 | readonly group: HttpApiGroup.Top |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Maps normalized media types to a payload encoding strategy and one or more |
| 139 | * schemas. Each schema retains its declared content type in its encoding annotation. |
| 140 | * |
| 141 | * @stability unstable |
| 142 | * @category models |
| 143 | * @since 4.0.0 |
| 144 | */ |
| 145 | export type PayloadMap = ReadonlyMap<string, { |
| 146 | readonly encoding: HttpApiSchema.PayloadEncoding |
| 147 | readonly schemas: readonly [Schema.Top, ...Array<Schema.Top>] |
| 148 | }> |
| 149 | |
| 150 | /** |
| 151 | * Represents an API endpoint. An API endpoint is mapped to a single route on |
| 152 | * the underlying `HttpRouter`. |
| 153 | * |
| 154 | * @stability unstable |
| 155 | * @category models |
| 156 | * @since 4.0.0 |
| 157 | */ |
| 158 | export interface HttpApiEndpoint< |
| 159 | out Identifier extends string, |
| 160 | out Method extends HttpMethod, |
| 161 | out Path extends string, |
| 162 | out Params extends Schema.Top = never, |
| 163 | out Query extends Schema.Top = never, |
| 164 | out Payload extends Schema.Top = never, |
| 165 | out Headers extends Schema.Top = never, |
| 166 | out Success extends Schema.Top = typeof HttpApiSchema.NoContent, |
| 167 | out Error extends Schema.Top = never, |
| 168 | in out Middleware = never, |
| 169 | out MiddlewareServices = never |
| 170 | > extends Pipeable { |
| 171 | new(_: never): {} |
| 172 | readonly [TypeId]: typeof TypeId |
| 173 | readonly "~Params": Params |
| 174 | readonly "~Query": Query |
| 175 | readonly "~Headers": Headers |
| 176 | readonly "~Payload": Payload |
| 177 | readonly "~Success": Success |
| 178 | readonly "~Error": Error |
| 179 | readonly "~Middleware": Middleware |
| 180 | readonly "~MiddlewareServices": MiddlewareServices |
| 181 | readonly "~Request": RequestFromParts<this, Params["Type"], Query["Type"], Payload["Type"], Headers["Type"]> |
| 182 | readonly "~RequestRaw": RequestRawFromParts<this, Params["Type"], Query["Type"], Headers["Type"]> |
| 183 | |
| 184 | readonly identifier: Identifier |
| 185 | readonly path: Path |
| 186 | readonly method: Method |
| 187 | readonly params: Schema.Top | undefined |
| 188 | readonly query: Schema.Top | undefined |
| 189 | readonly headers: Schema.Top | undefined |
| 190 | readonly payload: PayloadMap |
| 191 | readonly success: ReadonlySet<Schema.Top> |
| 192 | readonly error: ReadonlySet<Schema.Top> |
| 193 | readonly annotations: Context.Context<never> |
| 194 | readonly middlewares: ReadonlySet<Context.Key<Middleware, any>> |
| 195 | readonly disableCodecs: boolean |
| 196 | |
| 197 | /** |
| 198 | * Add a prefix to the path of the endpoint. |
| 199 | */ |
| 200 | prefix<const Prefix extends HttpRouter.PathInput>( |
| 201 | prefix: Prefix |
| 202 | ): HttpApiEndpoint< |
| 203 | Identifier, |
| 204 | Method, |
| 205 | `${Prefix}${Path}`, |
| 206 | Params, |
| 207 | Query, |
| 208 | Payload, |
| 209 | Headers, |
| 210 | Success, |
| 211 | Error, |
| 212 | Middleware, |
| 213 | MiddlewareServices |
| 214 | > |
| 215 | |
| 216 | /** |
| 217 | * Add an `HttpApiMiddleware` to the endpoint. |
| 218 | */ |
| 219 | middleware<I extends HttpApiMiddleware.AnyId, S>(middleware: Context.Key<I, S>): HttpApiEndpoint< |
| 220 | Identifier, |
| 221 | Method, |
| 222 | Path, |
| 223 | Params, |
| 224 | Query, |
| 225 | Payload, |
| 226 | Headers, |
| 227 | Success, |
| 228 | Error, |
| 229 | Middleware | I, |
| 230 | HttpApiMiddleware.ApplyServices<I, MiddlewareServices> |
| 231 | > |
| 232 | |
| 233 | /** |
| 234 | * Add an annotation on the endpoint. |
| 235 | */ |
| 236 | annotate<I, S>( |
| 237 | key: Context.Key<I, S>, |
| 238 | value: Types.NoInfer<S> |
| 239 | ): HttpApiEndpoint< |
| 240 | Identifier, |
| 241 | Method, |
| 242 | Path, |
| 243 | Params, |
| 244 | Query, |
| 245 | Payload, |
| 246 | Headers, |
| 247 | Success, |
| 248 | Error, |
| 249 | Middleware, |
| 250 | MiddlewareServices |
| 251 | > |
| 252 | |
| 253 | /** |
| 254 | * Merge the annotations of the endpoint with the provided context. |
| 255 | */ |
| 256 | annotateMerge<I>( |
| 257 | annotations: Context.Context<I> |
| 258 | ): HttpApiEndpoint< |
| 259 | Identifier, |
| 260 | Method, |
| 261 | Path, |
| 262 | Params, |
| 263 | Query, |
| 264 | Payload, |
| 265 | Headers, |
| 266 | Success, |
| 267 | Error, |
| 268 | Middleware, |
| 269 | MiddlewareServices |
| 270 | > |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * @internal |
| 275 | */ |
| 276 | export function getPayloadSchemas(endpoint: Top): Array<Schema.Top> { |
| 277 | const result: Array<Schema.Top> = [] |
| 278 | for (const { schemas } of endpoint.payload.values()) { |
| 279 | result.push(...schemas) |
| 280 | } |
| 281 | return result |
| 282 | } |
| 283 | |
| 284 | /** |
| 285 | * @internal |
| 286 | */ |
| 287 | export function getSuccessSchemas(endpoint: Top): [Schema.Top, ...Array<Schema.Top>] { |
| 288 | const schemas = Array.from(endpoint.success) |
| 289 | return Arr.isArrayNonEmpty(schemas) ? schemas : [HttpApiSchema.NoContent] |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * @internal |
| 294 | */ |
| 295 | export function getErrorSchemas(endpoint: Top): Array<Schema.Top> { |
| 296 | const schemas = new Set<Schema.Top>(endpoint.error) |
| 297 | const transform = endpoint.disableCodecs ? identity : transformResponseSchema |
| 298 | for (const middleware of endpoint.middlewares) { |
| 299 | const key = middleware as any as HttpApiMiddleware.AnyService |
| 300 | for (const schema of key.error) { |
| 301 | schemas.add(transform(schema)) |
| 302 | } |
| 303 | } |
| 304 | return Array.from(schemas) |
| 305 | } |
| 306 | |
| 307 | /** |
| 308 | * A widened `HttpApiEndpoint` type used when the concrete method, path, schemas, |
| 309 | * and middleware types are not needed. |
| 310 | * |
| 311 | * @stability unstable |
| 312 | * @category models |
| 313 | * @since 4.0.0 |
| 314 | */ |
| 315 | export interface Constraint { |
| 316 | readonly [TypeId]: typeof TypeId |
| 317 | readonly identifier: string |
| 318 | readonly ["~Success"]: Schema.Constraint |
| 319 | readonly ["~Error"]: Schema.Constraint |
| 320 | readonly ["~Request"]: unknown |
| 321 | readonly ["~RequestRaw"]: unknown |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * A widened endpoint type that preserves request and middleware pipeline phantom fields. |
| 326 | * |
| 327 | * @stability unstable |
| 328 | * @category models |
| 329 | * @since 4.0.0 |
| 330 | */ |
| 331 | export interface ConstraintRequest extends Constraint { |
| 332 | readonly ["~Params"]: Schema.Constraint |
| 333 | readonly ["~Query"]: Schema.Constraint |
| 334 | readonly ["~Payload"]: Schema.Constraint |
| 335 | readonly ["~Headers"]: Schema.Constraint |
| 336 | readonly ["~Middleware"]: unknown |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * A widened endpoint type that preserves concrete runtime properties such as |
| 341 | * method, path, schemas, annotations, and middleware sets. |
| 342 | * |
| 343 | * @stability unstable |
| 344 | * @category models |
| 345 | * @since 4.0.0 |
| 346 | */ |
| 347 | export interface Top extends |
| 348 | HttpApiEndpoint< |
| 349 | string, |
| 350 | HttpMethod, |
| 351 | string, |
| 352 | Schema.Top, |
| 353 | Schema.Top, |
| 354 | Schema.Top, |
| 355 | Schema.Top, |
| 356 | Schema.Top, |
| 357 | Schema.Top, |
| 358 | any, |
| 359 | unknown |
| 360 | > |
| 361 | {} |
| 362 | |
| 363 | /** |
| 364 | * Extracts the endpoint identifier literal from an `HttpApiEndpoint`. |
| 365 | * |
| 366 | * @stability unstable |
| 367 | * @category utility types |
| 368 | * @since 4.0.0 |
| 369 | */ |
| 370 | export type Identifier<Endpoint> = Endpoint extends Constraint ? Endpoint["identifier"] : never |
| 371 | |
| 372 | /** |
| 373 | * Extracts the success schema associated with an endpoint. |
| 374 | * |
| 375 | * @stability unstable |
| 376 | * @category utility types |
| 377 | * @since 4.0.0 |
| 378 | */ |
| 379 | export type Success<Endpoint> = Endpoint extends Constraint ? Endpoint["~Success"] : never |
| 380 | |
| 381 | /** |
| 382 | * Extracts the error schema associated with an endpoint. |
| 383 | * |
| 384 | * @stability unstable |
| 385 | * @category utility types |
| 386 | * @since 4.0.0 |
| 387 | */ |
| 388 | export type Error<Endpoint> = Endpoint extends Constraint ? Endpoint["~Error"] : never |
| 389 | |
| 390 | /** |
| 391 | * Extracts the schema used for an endpoint's path parameters. |
| 392 | * |
| 393 | * @stability unstable |
| 394 | * @category utility types |
| 395 | * @since 4.0.0 |
| 396 | */ |
| 397 | export type Params<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~Params"] |
| 398 | : never |
| 399 | |
| 400 | /** |
| 401 | * Extracts the schema used for an endpoint's query parameters. |
| 402 | * |
| 403 | * @stability unstable |
| 404 | * @category utility types |
| 405 | * @since 4.0.0 |
| 406 | */ |
| 407 | export type Query<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~Query"] |
| 408 | : never |
| 409 | |
| 410 | /** |
| 411 | * Extracts the schema used for an endpoint's request payload. |
| 412 | * |
| 413 | * @stability unstable |
| 414 | * @category utility types |
| 415 | * @since 4.0.0 |
| 416 | */ |
| 417 | export type Payload<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~Payload"] |
| 418 | : never |
| 419 | |
| 420 | /** |
| 421 | * Extracts the schema used for an endpoint's request headers. |
| 422 | * |
| 423 | * @stability unstable |
| 424 | * @category utility types |
| 425 | * @since 4.0.0 |
| 426 | */ |
| 427 | export type Headers<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~Headers"] |
| 428 | : never |
| 429 | |
| 430 | /** |
| 431 | * Extracts the middleware identifiers attached to an endpoint. |
| 432 | * |
| 433 | * @stability unstable |
| 434 | * @category utility types |
| 435 | * @since 4.0.0 |
| 436 | */ |
| 437 | export type Middleware<Endpoint> = Endpoint extends { readonly "~Middleware": infer M } ? M |
| 438 | : never |
| 439 | |
| 440 | /** |
| 441 | * Computes the services provided by the middleware attached to an endpoint. |
| 442 | * |
| 443 | * @stability unstable |
| 444 | * @category utility types |
| 445 | * @since 4.0.0 |
| 446 | */ |
| 447 | export type MiddlewareProvides<Endpoint> = HttpApiMiddleware.Provides<Middleware<Endpoint>> |
| 448 | |
| 449 | /** |
| 450 | * Computes the client-side middleware services required by an endpoint. |
| 451 | * |
| 452 | * @stability unstable |
| 453 | * @category utility types |
| 454 | * @since 4.0.0 |
| 455 | */ |
| 456 | export type MiddlewareClient<Endpoint> = HttpApiMiddleware.MiddlewareClient<Middleware<Endpoint>> |
| 457 | |
| 458 | /** |
| 459 | * Computes the error types that can be produced by the middleware attached to an |
| 460 | * endpoint. |
| 461 | * |
| 462 | * @stability unstable |
| 463 | * @category utility types |
| 464 | * @since 4.0.0 |
| 465 | */ |
| 466 | export type MiddlewareError<Endpoint> = HttpApiMiddleware.Error<Middleware<Endpoint>> |
| 467 | |
| 468 | /** |
| 469 | * Computes the full error value union for an endpoint, including the endpoint |
| 470 | * error schema's type and errors introduced by middleware. |
| 471 | * |
| 472 | * @stability unstable |
| 473 | * @category utility types |
| 474 | * @since 4.0.0 |
| 475 | */ |
| 476 | export type Errors<Endpoint> = Endpoint extends ConstraintRequest ? |
| 477 | Endpoint["~Error"]["Type"] | HttpApiMiddleware.Error<Endpoint["~Middleware"]> |
| 478 | : never |
| 479 | |
| 480 | /** |
| 481 | * Computes the services required to encode an endpoint's error responses, |
| 482 | * including services required by middleware error encoders. |
| 483 | * |
| 484 | * @stability unstable |
| 485 | * @category utility types |
| 486 | * @since 4.0.0 |
| 487 | */ |
| 488 | export type ErrorServicesEncode<Endpoint> = Endpoint extends ConstraintRequest ? |
| 489 | | Endpoint["~Error"]["EncodingServices"] |
| 490 | | HttpApiMiddleware.ErrorServicesEncode<Endpoint["~Middleware"]> |
| 491 | : never |
| 492 | |
| 493 | /** |
| 494 | * Builds the decoded request shape passed to a normal endpoint handler, including |
| 495 | * available params, query, payload, headers, the raw request, endpoint, and group. |
| 496 | * Multipart stream payloads are exposed as streams of parts. |
| 497 | * |
| 498 | * @stability unstable |
| 499 | * @category utility types |
| 500 | * @since 4.0.0 |
| 501 | */ |
| 502 | export type Request<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~Request"] |
| 503 | : {} |
| 504 | |
| 505 | /** |
| 506 | * Builds the request shape passed to a raw endpoint handler, including decoded |
| 507 | * params, query, and headers plus the raw request, endpoint, and group, while |
| 508 | * leaving payload handling to the raw request. |
| 509 | * |
| 510 | * @stability unstable |
| 511 | * @category utility types |
| 512 | * @since 4.0.0 |
| 513 | */ |
| 514 | export type RequestRaw<Endpoint> = Endpoint extends ConstraintRequest ? Endpoint["~RequestRaw"] |
| 515 | : {} |
| 516 | |
| 517 | /** |
| 518 | * Builds the request object accepted by a generated client method, including the |
| 519 | * params, query, headers, and payload required by the endpoint, plus optional |
| 520 | * response mode and SSE decoding options. Multipart payloads are supplied as |
| 521 | * `FormData`. |
| 522 | * |
| 523 | * @stability unstable |
| 524 | * @category utility types |
| 525 | * @since 4.0.0 |
| 526 | */ |
| 527 | export type ClientRequest< |
| 528 | Params extends Schema.Constraint, |
| 529 | Query extends Schema.Constraint, |
| 530 | Payload extends Schema.Constraint, |
| 531 | Headers extends Schema.Constraint, |
| 532 | ResponseMode extends ClientResponseMode |
| 533 | > = ( |
| 534 | & ([Params["Type"]] extends [never] ? {} : { readonly params: Params["Type"] }) |
| 535 | & ([Query["Type"]] extends [never] ? {} : { readonly query: Query["Type"] }) |
| 536 | & ([Headers["Type"]] extends [never] ? {} : { readonly headers: Headers["Type"] }) |
| 537 | & ([Payload["Type"]] extends [never] ? {} |
| 538 | : Payload["Type"] extends infer P ? |
| 539 | P extends Brand<HttpApiSchema.MultipartTypeId> | Brand<HttpApiSchema.MultipartStreamTypeId> |
| 540 | ? { readonly payload: FormData } |
| 541 | : { readonly payload: Payload["Type"] } |
| 542 | : { readonly payload: Payload["Type"] }) |
| 543 | ) extends infer Req ? keyof Req extends never ? (void | { |
| 544 | readonly responseMode?: ResponseMode |
| 545 | readonly sseOptions?: Sse.DecodeOptions | undefined |
| 546 | }) : |
| 547 | Req & { |
| 548 | readonly responseMode?: ResponseMode |
| 549 | readonly sseOptions?: Sse.DecodeOptions | undefined |
| 550 | } : |
| 551 | void |
| 552 | |
| 553 | /** |
| 554 | * Controls what a generated client method returns: the decoded success value, |
| 555 | * the decoded value paired with the raw response, or only the raw response. |
| 556 | * |
| 557 | * @stability unstable |
| 558 | * @category models |
| 559 | * @since 4.0.0 |
| 560 | */ |
| 561 | export type ClientResponseMode = "decoded-only" | "decoded-and-response" | "response-only" |
| 562 | |
| 563 | /** |
| 564 | * Computes the services required on the server to decode endpoint inputs and |
| 565 | * encode endpoint success, error, and middleware error responses. |
| 566 | * |
| 567 | * @stability unstable |
| 568 | * @category utility types |
| 569 | * @since 4.0.0 |
| 570 | */ |
| 571 | export type ServerServices<Endpoint> = Endpoint extends ConstraintRequest ? |
| 572 | | Endpoint["~Params"]["DecodingServices"] |
| 573 | | Endpoint["~Query"]["DecodingServices"] |
| 574 | | Endpoint["~Payload"]["DecodingServices"] |
| 575 | | Endpoint["~Headers"]["DecodingServices"] |
| 576 | | SuccessEncodingServices<Endpoint["~Success"]> |
| 577 | | Endpoint["~Error"]["EncodingServices"] |
| 578 | | HttpApiMiddleware.ErrorServicesEncode<Endpoint["~Middleware"]> |
| 579 | : never |
| 580 | |
| 581 | /** |
| 582 | * Computes the services required on the client to encode endpoint requests and |
| 583 | * decode endpoint success or error responses. |
| 584 | * |
| 585 | * @stability unstable |
| 586 | * @category utility types |
| 587 | * @since 4.0.0 |
| 588 | */ |
| 589 | export type ClientServices<Endpoint> = Endpoint extends ConstraintRequest ? |
| 590 | | Endpoint["~Params"]["EncodingServices"] |
| 591 | | Endpoint["~Query"]["EncodingServices"] |
| 592 | | Endpoint["~Payload"]["EncodingServices"] |
| 593 | | Endpoint["~Headers"]["EncodingServices"] |
| 594 | | SuccessDecodingServices<Endpoint["~Success"]> |
| 595 | | Endpoint["~Error"]["DecodingServices"] |
| 596 | : never |
| 597 | |
| 598 | /** |
| 599 | * Extracts the additional services required by middleware applied to an endpoint. |
| 600 | * |
| 601 | * @stability unstable |
| 602 | * @category utility types |
| 603 | * @since 4.0.0 |
| 604 | */ |
| 605 | export type MiddlewareServices<Endpoint> = Endpoint extends { readonly "~MiddlewareServices": infer R } ? R |
| 606 | : never |
| 607 | |
| 608 | /** |
| 609 | * Computes the services required to decode an endpoint's error responses, |
| 610 | * including services required by middleware error decoders. |
| 611 | * |
| 612 | * @stability unstable |
| 613 | * @category utility types |
| 614 | * @since 4.0.0 |
| 615 | */ |
| 616 | export type ErrorServicesDecode<Endpoint> = Endpoint extends ConstraintRequest ? |
| 617 | | Endpoint["~Error"]["DecodingServices"] |
| 618 | | HttpApiMiddleware.ErrorServicesDecode<Endpoint["~Middleware"]> |
| 619 | : never |
| 620 | |
| 621 | /** |
| 622 | * The normal server handler for an endpoint, accepting the decoded request shape |
| 623 | * and returning either the endpoint success value or a custom `HttpServerResponse`. |
| 624 | * |
| 625 | * @stability unstable |
| 626 | * @category utility types |
| 627 | * @since 4.0.0 |
| 628 | */ |
| 629 | export type Handler<Endpoint extends Constraint, E, R> = ( |
| 630 | request: Simplify<Endpoint["~Request"]> |
| 631 | ) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R> |
| 632 | |
| 633 | /** |
| 634 | * The raw server handler for an endpoint, receiving a request shape without a |
| 635 | * decoded payload so the handler can read the raw `HttpServerRequest` directly. |
| 636 | * |
| 637 | * @stability unstable |
| 638 | * @category utility types |
| 639 | * @since 4.0.0 |
| 640 | */ |
| 641 | export type HandlerRaw<Endpoint extends Constraint, E, R> = ( |
| 642 | request: Simplify<Endpoint["~RequestRaw"]> |
| 643 | ) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R> |
| 644 | |
| 645 | /** |
| 646 | * Selects the endpoint with the specified identifier from a union of endpoints. |
| 647 | * |
| 648 | * @stability unstable |
| 649 | * @category utility types |
| 650 | * @since 4.0.0 |
| 651 | */ |
| 652 | export type WithIdentifier<Endpoints, Identifier extends string> = Extract< |
| 653 | Endpoints, |
| 654 | { readonly identifier: Identifier } |
| 655 | > |
| 656 | |
| 657 | /** |
| 658 | * Removes endpoints with the specified identifier from a union of endpoints. |
| 659 | * |
| 660 | * @stability unstable |
| 661 | * @category utility types |
| 662 | * @since 4.0.0 |
| 663 | */ |
| 664 | export type ExcludeIdentifier<Endpoints, Identifier extends string> = Exclude< |
| 665 | Endpoints, |
| 666 | { readonly identifier: Identifier } |
| 667 | > |
| 668 | |
| 669 | /** |
| 670 | * Derives the normal handler type for the endpoint with the specified identifier |
| 671 | * in an endpoint union. |
| 672 | * |
| 673 | * @stability unstable |
| 674 | * @category utility types |
| 675 | * @since 4.0.0 |
| 676 | */ |
| 677 | export type HandlerWithIdentifier<Endpoints extends Constraint, Identifier extends string, E, R> = Handler< |
| 678 | WithIdentifier<Endpoints, Identifier>, |
| 679 | E, |
| 680 | R |
| 681 | > |
| 682 | |
| 683 | /** |
| 684 | * Derives the raw handler type for the endpoint with the specified identifier in |
| 685 | * an endpoint union. |
| 686 | * |
| 687 | * @stability unstable |
| 688 | * @category utility types |
| 689 | * @since 4.0.0 |
| 690 | */ |
| 691 | export type HandlerRawWithIdentifier<Endpoints extends Constraint, Identifier extends string, E, R> = HandlerRaw< |
| 692 | WithIdentifier<Endpoints, Identifier>, |
| 693 | E, |
| 694 | R |
| 695 | > |
| 696 | |
| 697 | /** |
| 698 | * Extracts the decoded success value type for the endpoint with the specified |
| 699 | * identifier in an endpoint union. |
| 700 | * |
| 701 | * @stability unstable |
| 702 | * @category utility types |
| 703 | * @since 4.0.0 |
| 704 | */ |
| 705 | export type SuccessWithIdentifier<Endpoints extends Constraint, Identifier extends string> = Success< |
| 706 | WithIdentifier<Endpoints, Identifier> |
| 707 | > extends infer S ? SuccessType<S> : never |
| 708 | |
| 709 | /** |
| 710 | * Computes the full error value union for the endpoint with the specified |
| 711 | * identifier in an endpoint union. |
| 712 | * |
| 713 | * @stability unstable |
| 714 | * @category utility types |
| 715 | * @since 4.0.0 |
| 716 | */ |
| 717 | export type ErrorsWithIdentifier<Endpoints extends Constraint, Identifier extends string> = Errors< |
| 718 | WithIdentifier<Endpoints, Identifier> |
| 719 | > |
| 720 | |
| 721 | /** |
| 722 | * Computes the server-side service requirements for the endpoint with the |
| 723 | * specified identifier in an endpoint union. |
| 724 | * |
| 725 | * @stability unstable |
| 726 | * @category utility types |
| 727 | * @since 4.0.0 |
| 728 | */ |
| 729 | export type ServerServicesWithIdentifier<Endpoints extends Constraint, Identifier extends string> = ServerServices< |
| 730 | WithIdentifier<Endpoints, Identifier> |
| 731 | > |
| 732 | |
| 733 | /** |
| 734 | * Extracts the middleware identifiers for the endpoint with the specified |
| 735 | * identifier in an endpoint union. |
| 736 | * |
| 737 | * @stability unstable |
| 738 | * @category utility types |
| 739 | * @since 4.0.0 |
| 740 | */ |
| 741 | export type MiddlewareWithIdentifier<Endpoints extends Constraint, Identifier extends string> = Middleware< |
| 742 | WithIdentifier<Endpoints, Identifier> |
| 743 | > |
| 744 | |
| 745 | /** |
| 746 | * Extracts the middleware service requirements for the endpoint with the |
| 747 | * specified identifier in an endpoint union. |
| 748 | * |
| 749 | * @stability unstable |
| 750 | * @category utility types |
| 751 | * @since 4.0.0 |
| 752 | */ |
| 753 | export type MiddlewareServicesWithIdentifier<Endpoints extends Constraint, Identifier extends string> = |
| 754 | MiddlewareServices<WithIdentifier<Endpoints, Identifier>> |
| 755 | |
| 756 | /** |
| 757 | * Removes services provided by the HTTP router and the selected endpoint's |
| 758 | * middleware from a service requirement union. |
| 759 | * |
| 760 | * @stability unstable |
| 761 | * @category utility types |
| 762 | * @since 4.0.0 |
| 763 | */ |
| 764 | export type ExcludeProvidedWithIdentifier<Endpoints extends Constraint, Identifier extends string, R> = ExcludeProvided< |
| 765 | WithIdentifier<Endpoints, Identifier>, |
| 766 | R |
| 767 | > |
| 768 | |
| 769 | /** |
| 770 | * Removes services provided by the HTTP router and endpoint middleware from a |
| 771 | * service requirement union. |
| 772 | * |
| 773 | * @stability unstable |
| 774 | * @category utility types |
| 775 | * @since 4.0.0 |
| 776 | */ |
| 777 | export type ExcludeProvided<Endpoint extends Constraint, R> = Exclude< |
| 778 | R, |
| 779 | | HttpRouter.Provided |
| 780 | | HttpApiMiddleware.Provides<Middleware<Endpoint>> |
| 781 | > |
| 782 | |
| 783 | /** |
| 784 | * Returns an endpoint type with the supplied path prefix prepended while |
| 785 | * preserving the endpoint's schemas, method, errors, and middleware. |
| 786 | * |
| 787 | * @stability unstable |
| 788 | * @category utility types |
| 789 | * @since 4.0.0 |
| 790 | */ |
| 791 | export type AddPrefix<Endpoint, Prefix extends HttpRouter.PathInput> = Endpoint extends HttpApiEndpoint< |
| 792 | infer _Identifier, |
| 793 | infer _Method, |
| 794 | infer _Path, |
| 795 | infer _Params, |
| 796 | infer _Query, |
| 797 | infer _Payload, |
| 798 | infer _Headers, |
| 799 | infer _Success, |
| 800 | infer _Error, |
| 801 | infer _M, |
| 802 | infer _MR |
| 803 | > ? HttpApiEndpoint< |
| 804 | _Identifier, |
| 805 | _Method, |
| 806 | `${Prefix}${_Path}`, |
| 807 | _Params, |
| 808 | _Query, |
| 809 | _Payload, |
| 810 | _Headers, |
| 811 | _Success, |
| 812 | _Error, |
| 813 | _M, |
| 814 | _MR |
| 815 | > : |
| 816 | never |
| 817 | |
| 818 | /** |
| 819 | * Returns an endpoint type with additional middleware applied and the endpoint's |
| 820 | * middleware service requirements updated accordingly. |
| 821 | * |
| 822 | * @stability unstable |
| 823 | * @category utility types |
| 824 | * @since 4.0.0 |
| 825 | */ |
| 826 | export type AddMiddleware<Endpoint, M extends HttpApiMiddleware.AnyId> = Endpoint extends HttpApiEndpoint< |
| 827 | infer _Identifier, |
| 828 | infer _Method, |
| 829 | infer _Path, |
| 830 | infer _Params, |
| 831 | infer _Query, |
| 832 | infer _Payload, |
| 833 | infer _Headers, |
| 834 | infer _Success, |
| 835 | infer _Error, |
| 836 | infer _M, |
| 837 | infer _MR |
| 838 | > ? HttpApiEndpoint< |
| 839 | _Identifier, |
| 840 | _Method, |
| 841 | _Path, |
| 842 | _Params, |
| 843 | _Query, |
| 844 | _Payload, |
| 845 | _Headers, |
| 846 | _Success, |
| 847 | _Error, |
| 848 | _M | M, |
| 849 | HttpApiMiddleware.ApplyServices<M, _MR> |
| 850 | > : |
| 851 | never |
| 852 | |
| 853 | const Proto = { |
| 854 | [TypeId]: TypeId, |
| 855 | pipe() { |
| 856 | return pipeArguments(this, arguments) |
| 857 | }, |
| 858 | prefix(this: Top, prefix: HttpRouter.PathInput) { |
| 859 | return makeProto({ |
| 860 | ...optionsFromEndpoint(this), |
| 861 | path: HttpRouter.prefixPath(this.path, prefix) |
| 862 | }) |
| 863 | }, |
| 864 | middleware(this: Top, middleware: HttpApiMiddleware.AnyService) { |
| 865 | return makeProto({ |
| 866 | ...optionsFromEndpoint(this), |
| 867 | middlewares: new Set([...this.middlewares, middleware as any]) |
| 868 | }) |
| 869 | }, |
| 870 | annotate(this: Top, key: Context.Key<any, any>, value: any) { |
| 871 | return makeProto({ |
| 872 | ...optionsFromEndpoint(this), |
| 873 | annotations: Context.add(this.annotations, key, value) |
| 874 | }) |
| 875 | }, |
| 876 | annotateMerge(this: Top, annotations: Context.Context<any>) { |
| 877 | return makeProto({ |
| 878 | ...optionsFromEndpoint(this), |
| 879 | annotations: Context.merge(this.annotations, annotations) |
| 880 | }) |
| 881 | } |
| 882 | } |
| 883 | |
| 884 | const optionsFromEndpoint = (endpoint: Top) => ({ |
| 885 | identifier: endpoint.identifier, |
| 886 | path: endpoint.path, |
| 887 | method: endpoint.method, |
| 888 | params: endpoint.params, |
| 889 | query: endpoint.query, |
| 890 | headers: endpoint.headers, |
| 891 | payload: endpoint.payload, |
| 892 | success: endpoint.success, |
| 893 | error: endpoint.error, |
| 894 | annotations: endpoint.annotations, |
| 895 | middlewares: endpoint.middlewares, |
| 896 | disableCodecs: endpoint.disableCodecs |
| 897 | }) |
| 898 | |
| 899 | function makeProto< |
| 900 | Identifier extends string, |
| 901 | Method extends HttpMethod, |
| 902 | const Path extends string, |
| 903 | Params extends Schema.Top, |
| 904 | Query extends Schema.Top, |
| 905 | Payload extends Schema.Top, |
| 906 | Headers extends Schema.Top, |
| 907 | Success extends Schema.Top, |
| 908 | Error extends Schema.Top, |
| 909 | Middleware, |
| 910 | MiddlewareR |
| 911 | >(options: { |
| 912 | readonly identifier: Identifier |
| 913 | readonly path: Path |
| 914 | readonly method: Method |
| 915 | readonly params: Schema.Top | undefined |
| 916 | readonly query: Schema.Top | undefined |
| 917 | readonly headers: Schema.Top | undefined |
| 918 | readonly payload: PayloadMap |
| 919 | readonly success: ReadonlySet<Schema.Top> |
| 920 | readonly error: ReadonlySet<Schema.Top> |
| 921 | readonly annotations: Context.Context<never> |
| 922 | readonly middlewares: ReadonlySet<Context.Key<Middleware, any>> |
| 923 | readonly disableCodecs: boolean |
| 924 | }): HttpApiEndpoint< |
| 925 | Identifier, |
| 926 | Method, |
| 927 | Path, |
| 928 | Params, |
| 929 | Query, |
| 930 | Payload, |
| 931 | Headers, |
| 932 | Success, |
| 933 | Error, |
| 934 | Middleware, |
| 935 | MiddlewareR |
| 936 | > { |
| 937 | function HttpApiEndpoint() {} |
| 938 | Object.setPrototypeOf(HttpApiEndpoint, Proto) |
| 939 | return Object.assign(HttpApiEndpoint, options) as any |
| 940 | } |
| 941 | |
| 942 | /** |
| 943 | * Constraint for path parameter schemas: each parameter must encode to |
| 944 | * `string | undefined`, or the schema must encode to a record of those values. |
| 945 | * |
| 946 | * @stability unstable |
| 947 | * @category utility types |
| 948 | * @since 4.0.0 |
| 949 | */ |
| 950 | export type ParamsConstraint = |
| 951 | | Record<string, Schema.Encoder<string | undefined, unknown>> |
| 952 | | Schema.Encoder<Record<string, string | undefined>, unknown> |
| 953 | |
| 954 | /** |
| 955 | * Constraint for header schemas: each header must encode to `string | undefined`, |
| 956 | * or the schema must encode to a record of those values. |
| 957 | * |
| 958 | * @stability unstable |
| 959 | * @category utility types |
| 960 | * @since 4.0.0 |
| 961 | */ |
| 962 | export type HeadersConstraint = |
| 963 | | Record<string, Schema.Encoder<string | undefined, unknown>> |
| 964 | | Schema.Encoder<Record<string, string | undefined>, unknown> |
| 965 | |
| 966 | /** |
| 967 | * Constraint for query schemas: each field must encode to `string`, an array of |
| 968 | * strings, or `undefined`, or the schema must encode to a record of those values. |
| 969 | * |
| 970 | * @stability unstable |
| 971 | * @category utility types |
| 972 | * @since 4.0.0 |
| 973 | */ |
| 974 | export type QueryConstraint = |
| 975 | | Record<string, Schema.Encoder<string | ReadonlyArray<string> | undefined, unknown>> |
| 976 | | Schema.Encoder<string | ReadonlyArray<string> | undefined, unknown> |
| 977 | |
| 978 | /** |
| 979 | * Payload schema depends on the HTTP method: |
| 980 | * - for no-body methods, payload is modeled as query params, so each field must |
| 981 | * encode to `string | ReadonlyArray<string> | undefined` and OpenAPI can expand |
| 982 | * it into `in: query` parameters |
| 983 | * - for body methods, payload may be any `Schema.Top` (or content-type keyed |
| 984 | * schemas) and OpenAPI uses `requestBody` instead of `parameters` |
| 985 | * |
| 986 | * @stability unstable |
| 987 | * @category utility types |
| 988 | * @since 4.0.0 |
| 989 | */ |
| 990 | export type PayloadConstraint<Method extends HttpMethod> = Method extends HttpMethod.NoBody ? Record< |
| 991 | string, |
| 992 | Schema.Encoder<string | ReadonlyArray<string> | undefined, unknown> |
| 993 | > : |
| 994 | Schema.Top | ReadonlyArray<Schema.Top> |
| 995 | |
| 996 | /** |
| 997 | * Payload constraint used when automatic codecs are enabled: no-body methods |
| 998 | * accept field records for query-style encoding, while body methods accept one or |
| 999 | * more schemas. |
| 1000 | * |
| 1001 | * @stability unstable |
| 1002 | * @category utility types |
| 1003 | * @since 4.0.0 |
| 1004 | */ |
| 1005 | export type PayloadConstraintCodecs<Method extends HttpMethod> = Method extends HttpMethod.NoBody ? |
| 1006 | Record<string, Schema.Top> : |
| 1007 | Schema.Top | ReadonlyArray<Schema.Top> |
| 1008 | |
| 1009 | /** |
| 1010 | * Constraint for success response schemas, allowing either a single schema or a |
| 1011 | * readonly array of schemas. |
| 1012 | * |
| 1013 | * @stability unstable |
| 1014 | * @category utility types |
| 1015 | * @since 4.0.0 |
| 1016 | */ |
| 1017 | export type SuccessConstraint = Schema.Top | ReadonlyArray<Schema.Top> |
| 1018 | |
| 1019 | /** |
| 1020 | * Constraint for error response schemas, allowing either a single schema or a |
| 1021 | * readonly array of schemas. |
| 1022 | * |
| 1023 | * @stability unstable |
| 1024 | * @category utility types |
| 1025 | * @since 4.0.0 |
| 1026 | */ |
| 1027 | export type ErrorConstraint = Schema.Top | ReadonlyArray<Schema.Top> |
| 1028 | |
| 1029 | type ErrorSchema<S> = S extends HttpApiSchema.WithHeaders<infer Inner, Schema.Top> ? Inner : S |
| 1030 | |
| 1031 | type ErrorNoStream<S extends ErrorConstraint> = [ |
| 1032 | Extract< |
| 1033 | ErrorSchema<S extends ReadonlyArray<Schema.Constraint> ? S[number] : S>, |
| 1034 | HttpApiSchema.StreamSchema |
| 1035 | > |
| 1036 | ] extends [never] ? S : never |
| 1037 | |
| 1038 | /** |
| 1039 | * Creates endpoint constructors for a specific HTTP method. The resulting |
| 1040 | * constructor builds an `HttpApiEndpoint` from an identifier, path, and optional request |
| 1041 | * and response schemas, applying automatic JSON or string-tree codecs unless |
| 1042 | * `disableCodecs` is enabled. |
| 1043 | * |
| 1044 | * @stability unstable |
| 1045 | * @category constructors |
| 1046 | * @since 4.0.0 |
| 1047 | */ |
| 1048 | export const make = <Method extends HttpMethod>(method: Method): { |
| 1049 | < |
| 1050 | const Identifier extends string, |
| 1051 | const Path extends HttpRouter.PathInput, |
| 1052 | Params extends Schema.Top | Schema.Struct.Fields = never, |
| 1053 | Query extends Schema.Top | Schema.Struct.Fields = never, |
| 1054 | Payload extends PayloadConstraintCodecs<Method> = never, |
| 1055 | Headers extends Schema.Top | Schema.Struct.Fields = never, |
| 1056 | const Success extends SuccessConstraint = HttpApiSchema.NoContent, |
| 1057 | const Error extends Schema.Top | ReadonlyArray<Schema.Top> = never |
| 1058 | >( |
| 1059 | identifier: Identifier, |
| 1060 | path: Path, |
| 1061 | options?: { |
| 1062 | readonly disableCodecs?: false | undefined |
| 1063 | readonly params?: Params | undefined |
| 1064 | readonly query?: Query | undefined |
| 1065 | readonly headers?: Headers | undefined |
| 1066 | readonly payload?: Payload | undefined |
| 1067 | readonly success?: Success | undefined |
| 1068 | readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined |
| 1069 | } |
| 1070 | ): HttpApiEndpoint< |
| 1071 | Identifier, |
| 1072 | Method, |
| 1073 | Path, |
| 1074 | ToStringTreeCodec<Params>, |
| 1075 | ToStringTreeCodec<Query>, |
| 1076 | Method extends HttpMethod.WithBody ? ToJsonCodec<ToSchema<Payload>> |
| 1077 | : ToStringTreeCodec<ToSchema<Payload>>, |
| 1078 | ToStringTreeCodec<Headers>, |
| 1079 | ToSuccessCodec<Success>, |
| 1080 | ToJsonCodec<Error extends ReadonlyArray<Schema.Constraint> ? Error[number] : Error> |
| 1081 | > |
| 1082 | < |
| 1083 | const Identifier extends string, |
| 1084 | const Path extends HttpRouter.PathInput, |
| 1085 | Params extends ParamsConstraint = never, |
| 1086 | Query extends QueryConstraint = never, |
| 1087 | Payload extends PayloadConstraint<Method> = never, |
| 1088 | Headers extends HeadersConstraint = never, |
| 1089 | const Success extends SuccessConstraint = HttpApiSchema.NoContent, |
| 1090 | const Error extends ErrorConstraint = never |
| 1091 | >( |
| 1092 | identifier: Identifier, |
| 1093 | path: Path, |
| 1094 | options?: { |
| 1095 | readonly disableCodecs: true |
| 1096 | readonly params?: Params | undefined |
| 1097 | readonly query?: Query | undefined |
| 1098 | readonly headers?: Headers | undefined |
| 1099 | readonly payload?: Payload | undefined |
| 1100 | readonly success?: Success | undefined |
| 1101 | readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined |
| 1102 | } |
| 1103 | ): HttpApiEndpoint< |
| 1104 | Identifier, |
| 1105 | Method, |
| 1106 | Path, |
| 1107 | Params extends Schema.Struct.Fields ? Schema.Struct<Params> : Params, |
| 1108 | Query extends Schema.Struct.Fields ? Schema.Struct<Query> : Query, |
| 1109 | ToSchema<Payload>, |
| 1110 | ToSchema<Headers>, |
| 1111 | UnwrapReadonlyArray<Success>, |
| 1112 | Error extends ReadonlyArray<Schema.Constraint> ? Error[number] : Error |
| 1113 | > |
| 1114 | } => |
| 1115 | < |
| 1116 | const Identifier extends string, |
| 1117 | const Path extends HttpRouter.PathInput, |
| 1118 | Params extends ParamsConstraint = never, |
| 1119 | Query extends QueryConstraint = never, |
| 1120 | Payload extends PayloadConstraint<Method> = never, |
| 1121 | Headers extends HeadersConstraint = never, |
| 1122 | const Success extends SuccessConstraint = HttpApiSchema.NoContent, |
| 1123 | const Error extends ErrorConstraint = never |
| 1124 | >( |
| 1125 | identifier: Identifier, |
| 1126 | path: Path, |
| 1127 | options?: { |
| 1128 | readonly disableCodecs?: boolean | undefined |
| 1129 | readonly params?: Params | undefined |
| 1130 | readonly query?: Query | undefined |
| 1131 | readonly headers?: Headers | undefined |
| 1132 | readonly payload?: Payload | undefined |
| 1133 | readonly success?: Success | undefined |
| 1134 | readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined |
| 1135 | } |
| 1136 | ): HttpApiEndpoint< |
| 1137 | Identifier, |
| 1138 | Method, |
| 1139 | Path, |
| 1140 | Params extends Schema.Struct.Fields ? Schema.Struct<Params> : Params, |
| 1141 | Query extends Schema.Struct.Fields ? Schema.Struct<Query> : Query, |
| 1142 | Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> |
| 1143 | : Payload extends ReadonlyArray<Schema.Constraint> ? Payload[number] |
| 1144 | : Payload, |
| 1145 | Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers, |
| 1146 | UnwrapReadonlyArray<Success>, |
| 1147 | Error extends ReadonlyArray<Schema.Constraint> ? Error[number] : Error |
| 1148 | > => { |
| 1149 | const disableCodecs = options?.disableCodecs ?? false |
| 1150 | const transformStringTree = disableCodecs ? identity : Schema.toCodecStringTree |
| 1151 | return makeProto({ |
| 1152 | identifier, |
| 1153 | path, |
| 1154 | method, |
| 1155 | params: ensureStruct(options?.params, transformStringTree), |
| 1156 | query: ensureStruct(options?.query, transformStringTree), |
| 1157 | headers: ensureStruct(options?.headers, transformStringTree), |
| 1158 | payload: getPayload(options?.payload, method, disableCodecs), |
| 1159 | success: getSuccessResponse(options?.success, method, disableCodecs), |
| 1160 | error: getErrorResponse(options?.error, disableCodecs), |
| 1161 | annotations: Context.empty(), |
| 1162 | middlewares: new Set(), |
| 1163 | disableCodecs |
| 1164 | }) |
| 1165 | } |
| 1166 | |
| 1167 | type ToSchema<S extends Schema.Struct.Fields | Schema.Constraint | ReadonlyArray<Schema.Constraint>> = S extends |
| 1168 | Schema.Struct.Fields ? Schema.Struct<S> |
| 1169 | : S extends ReadonlyArray<Schema.Constraint> ? S[number] |
| 1170 | : S |
| 1171 | |
| 1172 | function ensureStruct( |
| 1173 | params: Schema.Struct.Fields | Schema.Top | undefined, |
| 1174 | transform: (schema: Schema.Top) => Schema.Top |
| 1175 | ): Schema.Top | undefined { |
| 1176 | if (params === undefined) return undefined |
| 1177 | if (Schema.isSchema(params)) return transform(params) |
| 1178 | return transform(Schema.Struct(params)) |
| 1179 | } |
| 1180 | |
| 1181 | function getPayload( |
| 1182 | payload: Schema.Constraint | ReadonlyArray<Schema.Constraint> | Schema.Struct.Fields | undefined, |
| 1183 | method: HttpMethod, |
| 1184 | disableCodecs: boolean |
| 1185 | ): PayloadMap { |
| 1186 | const result: Map< |
| 1187 | string, |
| 1188 | { encoding: HttpApiSchema.PayloadEncoding; schemas: [Schema.Top, ...Array<Schema.Top>] } |
| 1189 | > = new Map() |
| 1190 | if (payload === undefined) return result |
| 1191 | const schemas: Array<Schema.Top> = Array.isArray(payload) |
| 1192 | ? payload |
| 1193 | : Schema.isSchema(payload) |
| 1194 | ? [payload] |
| 1195 | : [(Schema.Struct(payload as any)).pipe(HttpApiSchema.asFormUrlEncoded())] |
| 1196 | const transform = disableCodecs ? identity : transformPayload |
| 1197 | |
| 1198 | for (const schema of schemas) { |
| 1199 | const encoding = HttpApiSchema.getPayloadEncoding(schema.ast, method) |
| 1200 | const contentType = MediaType.normalize(encoding.contentType) |
| 1201 | const existing = result.get(contentType) |
| 1202 | if (existing) { |
| 1203 | if (existing.encoding._tag !== encoding._tag) { |
| 1204 | throw new Error(`Multiple payload encodings for content-type: ${encoding.contentType}`) |
| 1205 | } |
| 1206 | if (existing.encoding._tag === "Multipart") { |
| 1207 | throw new Error(`Multiple multipart payloads for content-type: ${encoding.contentType}`) |
| 1208 | } |
| 1209 | existing.schemas.push(transform(schema, method)) |
| 1210 | } else { |
| 1211 | result.set(contentType, { encoding, schemas: [transform(schema, method)] }) |
| 1212 | } |
| 1213 | } |
| 1214 | return result |
| 1215 | } |
| 1216 | |
| 1217 | const reservedStreamFailureEvent = "effect/http-api/stream/failure" |
| 1218 | |
| 1219 | function getSuccessResponse( |
| 1220 | success: Schema.Top | ReadonlyArray<Schema.Top> | undefined, |
| 1221 | method: HttpMethod, |
| 1222 | disableCodecs: boolean |
| 1223 | ): Set<Schema.Top> { |
| 1224 | if (success === undefined) return new Set() |
| 1225 | const schemas = Arr.ensure(success) |
| 1226 | validateSuccessResponse(schemas, method) |
| 1227 | return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) |
| 1228 | } |
| 1229 | |
| 1230 | const transformResponseSchema = memoize((schema: Schema.Top): Schema.Top => { |
| 1231 | if (HttpApiSchema.isStreamSchema(schema)) return schema |
| 1232 | if (HttpApiSchema.isWithHeaders(schema)) { |
| 1233 | const inner = HttpApiSchema.isStreamSchema(schema.schema) |
| 1234 | ? schema.schema |
| 1235 | : applyResponseEncoding(schema.schema, HttpApiSchema.getResponseEncodingSchema(schema)) |
| 1236 | return HttpApiSchema.rebuildWithHeaders(schema, inner, Schema.toCodecStringTree(schema.headers)) |
| 1237 | } |
| 1238 | return transformResponse(schema) |
| 1239 | }) |
| 1240 | |
| 1241 | function getErrorResponse( |
| 1242 | error: Schema.Top | ReadonlyArray<Schema.Top> | undefined, |
| 1243 | disableCodecs: boolean |
| 1244 | ): Set<Schema.Top> { |
| 1245 | if (error === undefined) return new Set() |
| 1246 | const schemas = Arr.ensure(error) |
| 1247 | for (const schema of schemas) { |
| 1248 | const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema |
| 1249 | if (HttpApiSchema.isStreamSchema(body)) { |
| 1250 | throw new Error("Streaming schemas are not supported in error responses") |
| 1251 | } |
| 1252 | } |
| 1253 | validateResponseExclusivity(schemas, HttpApiSchema.getStatusErrorSchema) |
| 1254 | return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) |
| 1255 | } |
| 1256 | |
| 1257 | function validateSuccessResponse(schemas: ReadonlyArray<Schema.Constraint>, method: HttpMethod) { |
| 1258 | let hasStream = false |
| 1259 | const statuses = new Map<number, { |
| 1260 | readonly stream?: HttpApiSchema.StreamSchema | undefined |
| 1261 | bufferedContentTypes: Set<string> |
| 1262 | noContent: boolean |
| 1263 | }>() |
| 1264 | |
| 1265 | for (const schema of schemas) { |
| 1266 | const inner = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema |
| 1267 | const status = HttpApiSchema.getStatusSuccessSchema(schema) |
| 1268 | if (HttpApiSchema.isStreamSchema(inner)) { |
| 1269 | validateStreamSuccess(inner, method) |
| 1270 | if (hasStream) { |
| 1271 | throw new Error("Multiple streaming success responses are not supported") |
| 1272 | } |
| 1273 | hasStream = true |
| 1274 | const entry = getStatusEntry(statuses, status) |
| 1275 | if (entry.noContent) { |
| 1276 | throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`) |
| 1277 | } |
| 1278 | if (entry.bufferedContentTypes.has(MediaType.normalize(inner.contentType))) { |
| 1279 | throw new Error( |
| 1280 | `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${inner.contentType}` |
| 1281 | ) |
| 1282 | } |
| 1283 | statuses.set(status, { ...entry, stream: inner }) |
| 1284 | } else { |
| 1285 | const entry = getStatusEntry(statuses, status) |
| 1286 | const noContent = HttpApiSchema.isNoContent(inner.ast) |
| 1287 | if (entry.stream !== undefined) { |
| 1288 | if (noContent) { |
| 1289 | throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`) |
| 1290 | } |
| 1291 | const encoding = HttpApiSchema.getResponseEncodingSchema(schema) |
| 1292 | if ( |
| 1293 | MediaType.normalize(encoding.contentType) === MediaType.normalize(entry.stream.contentType) |
| 1294 | ) { |
| 1295 | throw new Error( |
| 1296 | `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${encoding.contentType}` |
| 1297 | ) |
| 1298 | } |
| 1299 | } |
| 1300 | if (!noContent) { |
| 1301 | entry.bufferedContentTypes.add( |
| 1302 | MediaType.normalize(HttpApiSchema.getResponseEncodingSchema(schema).contentType) |
| 1303 | ) |
| 1304 | } |
| 1305 | entry.noContent = entry.noContent || noContent |
| 1306 | } |
| 1307 | } |
| 1308 | |
| 1309 | validateResponseExclusivity(schemas, HttpApiSchema.getStatusSuccessSchema) |
| 1310 | } |
| 1311 | |
| 1312 | function getStatusEntry( |
| 1313 | statuses: Map<number, { |
| 1314 | readonly stream?: HttpApiSchema.StreamSchema | undefined |
| 1315 | bufferedContentTypes: Set<string> |
| 1316 | noContent: boolean |
| 1317 | }>, |
| 1318 | status: number |
| 1319 | ) { |
| 1320 | let entry = statuses.get(status) |
| 1321 | if (entry === undefined) { |
| 1322 | entry = { bufferedContentTypes: new Set(), noContent: false } |
| 1323 | statuses.set(status, entry) |
| 1324 | } |
| 1325 | return entry |
| 1326 | } |
| 1327 | |
| 1328 | function validateResponseExclusivity( |
| 1329 | schemas: ReadonlyArray<Schema.Constraint>, |
| 1330 | getStatus: (schema: Schema.Constraint) => number |
| 1331 | ) { |
| 1332 | const statuses = new Map<number, { |
| 1333 | headerContentType: string | undefined |
| 1334 | readonly plainContentTypes: Set<string> |
| 1335 | }>() |
| 1336 | for (const schema of schemas) { |
| 1337 | const status = getStatus(schema) |
| 1338 | const withHeadersAnnotation = HttpApiSchema.getWithHeadersAnnotation(schema.ast) |
| 1339 | const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : withHeadersAnnotation?.body ?? schema |
| 1340 | const contentType = HttpApiSchema.isNoContent(body.ast) |
| 1341 | ? "" |
| 1342 | : MediaType.normalize( |
| 1343 | HttpApiSchema.isStreamSchema(body) |
| 1344 | ? body.contentType |
| 1345 | : HttpApiSchema.getResponseEncodingSchema(schema).contentType |
| 1346 | ) |
| 1347 | let entry = statuses.get(status) |
| 1348 | if (entry === undefined) { |
| 1349 | entry = { headerContentType: undefined, plainContentTypes: new Set() } |
| 1350 | statuses.set(status, entry) |
| 1351 | } |
| 1352 | const combineError = () => |
| 1353 | new Error( |
| 1354 | `Cannot combine a response with headers with another response for status ${status} and content-type: ${ |
| 1355 | contentType || "<no content>" |
| 1356 | }` |
| 1357 | ) |
| 1358 | if (HttpApiSchema.isWithHeaders(schema) || withHeadersAnnotation !== undefined) { |
| 1359 | if (entry.headerContentType !== undefined) { |
| 1360 | throw new Error(`Cannot declare multiple responses with headers for status ${status}`) |
| 1361 | } |
| 1362 | if (entry.plainContentTypes.has(contentType)) throw combineError() |
| 1363 | entry.headerContentType = contentType |
| 1364 | } else { |
| 1365 | if (entry.headerContentType === contentType) throw combineError() |
| 1366 | entry.plainContentTypes.add(contentType) |
| 1367 | } |
| 1368 | } |
| 1369 | } |
| 1370 | |
| 1371 | function validateStreamSuccess(schema: HttpApiSchema.StreamSchema, method: HttpMethod) { |
| 1372 | if (method === "HEAD") { |
| 1373 | throw new Error("HEAD endpoints cannot declare streaming success responses") |
| 1374 | } |
| 1375 | if (HttpApiSchema.isStreamSse(schema) && hasReservedSseEventName(schema.events.ast)) { |
| 1376 | throw new Error(`SSE event name is reserved: ${reservedStreamFailureEvent}`) |
| 1377 | } |
| 1378 | } |
| 1379 | |
| 1380 | function hasReservedSseEventName(ast: AST.AST): boolean { |
| 1381 | return hasReservedEventName(AST.toEncoded(ast), new Set()) |
| 1382 | } |
| 1383 | |
| 1384 | function hasReservedEventName(ast: AST.AST, seen: Set<AST.AST>): boolean { |
| 1385 | if (seen.has(ast)) return false |
| 1386 | seen.add(ast) |
| 1387 | if (AST.isUnion(ast)) { |
| 1388 | return ast.types.some((type) => hasReservedEventName(type, seen)) |
| 1389 | } |
| 1390 | if (AST.isSuspend(ast)) { |
| 1391 | return hasReservedEventName(ast.thunk(), seen) |
| 1392 | } |
| 1393 | if (!AST.isObjects(ast)) return false |
| 1394 | const event = ast.propertySignatures.find((ps) => ps.name === "event") |
| 1395 | return event !== undefined && hasReservedEventLiteral(event.type, seen) |
| 1396 | } |
| 1397 | |
| 1398 | function hasReservedEventLiteral(ast: AST.AST, seen: Set<AST.AST>): boolean { |
| 1399 | if (seen.has(ast)) return false |
| 1400 | seen.add(ast) |
| 1401 | const encoded = AST.toEncoded(ast) |
| 1402 | if (encoded !== ast) { |
| 1403 | return hasReservedEventLiteral(encoded, seen) |
| 1404 | } |
| 1405 | if (AST.isLiteral(ast)) { |
| 1406 | return ast.literal === reservedStreamFailureEvent |
| 1407 | } |
| 1408 | if (AST.isUnion(ast)) { |
| 1409 | return ast.types.some((type) => hasReservedEventLiteral(type, seen)) |
| 1410 | } |
| 1411 | if (AST.isSuspend(ast)) { |
| 1412 | return hasReservedEventLiteral(ast.thunk(), seen) |
| 1413 | } |
| 1414 | return false |
| 1415 | } |
| 1416 | |
| 1417 | function transformResponse(schema: Schema.Top): Schema.Top { |
| 1418 | const encoding = HttpApiSchema.getResponseEncoding(schema.ast) |
| 1419 | const withHeaders = HttpApiSchema.getWithHeadersAnnotation(schema.ast) |
| 1420 | if (withHeaders === undefined) { |
| 1421 | return applyResponseEncoding(schema, encoding) |
| 1422 | } |
| 1423 | return schema.annotate({ |
| 1424 | "~httpApiWithHeaders": { |
| 1425 | ...withHeaders, |
| 1426 | bodyCodec: applyResponseEncoding(withHeaders.body, encoding), |
| 1427 | headersCodec: Schema.toCodecStringTree(withHeaders.headers) |
| 1428 | } |
| 1429 | }) |
| 1430 | } |
| 1431 | |
| 1432 | function applyResponseEncoding(schema: Schema.Top, encoding: HttpApiSchema.ResponseEncoding): Schema.Top { |
| 1433 | switch (encoding._tag) { |
| 1434 | case "Json": |
| 1435 | return Schema.toCodecJson(schema) |
| 1436 | case "FormUrlEncoded": |
| 1437 | return Schema.toCodecStringTree(schema) |
| 1438 | case "Text": |
| 1439 | case "Uint8Array": |
| 1440 | return schema |
| 1441 | } |
| 1442 | } |
| 1443 | |
| 1444 | function transformPayload(schema: Schema.Top, method: HttpMethod): Schema.Top { |
| 1445 | const encoding = HttpApiSchema.getPayloadEncoding(schema.ast, method) |
| 1446 | switch (encoding._tag) { |
| 1447 | case "Json": |
| 1448 | return Schema.toCodecJson(schema) |
| 1449 | case "FormUrlEncoded": |
| 1450 | return Schema.toCodecStringTree(schema) |
| 1451 | case "Text": |
| 1452 | case "Uint8Array": |
| 1453 | case "Multipart": |
| 1454 | return schema |
| 1455 | } |
| 1456 | } |
| 1457 | |
| 1458 | /** |
| 1459 | * Creates a `GET` endpoint declaration. |
| 1460 | * |
| 1461 | * @stability unstable |
| 1462 | * @category constructors |
| 1463 | * @since 4.0.0 |
| 1464 | */ |
| 1465 | export const get = make("GET") |
| 1466 | |
| 1467 | /** |
| 1468 | * Creates a `QUERY` endpoint declaration. |
| 1469 | * |
| 1470 | * @stability unstable |
| 1471 | * @category constructors |
| 1472 | * @since 4.0.0 |
| 1473 | */ |
| 1474 | export const query = make("QUERY") |
| 1475 | |
| 1476 | /** |
| 1477 | * Creates a `POST` endpoint declaration. |
| 1478 | * |
| 1479 | * @stability unstable |
| 1480 | * @category constructors |
| 1481 | * @since 4.0.0 |
| 1482 | */ |
| 1483 | export const post = make("POST") |
| 1484 | |
| 1485 | /** |
| 1486 | * Creates a `PUT` endpoint declaration. |
| 1487 | * |
| 1488 | * @stability unstable |
| 1489 | * @category constructors |
| 1490 | * @since 4.0.0 |
| 1491 | */ |
| 1492 | export const put = make("PUT") |
| 1493 | |
| 1494 | /** |
| 1495 | * Creates a `PATCH` endpoint declaration. |
| 1496 | * |
| 1497 | * @stability unstable |
| 1498 | * @category constructors |
| 1499 | * @since 4.0.0 |
| 1500 | */ |
| 1501 | export const patch = make("PATCH") |
| 1502 | |
| 1503 | const del = make("DELETE") |
| 1504 | |
| 1505 | /** |
| 1506 | * @stability unstable |
| 1507 | */ |
| 1508 | export { |
| 1509 | /** |
| 1510 | * Creates a `DELETE` endpoint declaration. |
| 1511 | * |
| 1512 | * @stability unstable |
| 1513 | * @category constructors |
| 1514 | * @since 4.0.0 |
| 1515 | */ |
| 1516 | del as delete |
| 1517 | } |
| 1518 | |
| 1519 | /** |
| 1520 | * Creates a `HEAD` endpoint declaration. |
| 1521 | * |
| 1522 | * @stability unstable |
| 1523 | * @category constructors |
| 1524 | * @since 4.0.0 |
| 1525 | */ |
| 1526 | export const head = make("HEAD") |
| 1527 | |
| 1528 | /** |
| 1529 | * Creates an `OPTIONS` endpoint declaration. |
| 1530 | * |
| 1531 | * @stability unstable |
| 1532 | * @category constructors |
| 1533 | * @since 4.0.0 |
| 1534 | */ |
| 1535 | export const options = make("OPTIONS") |