| import * as Context from "../../Context.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Encoding from "../../Encoding.js"; | |
| import * as Fiber from "../../Fiber.js"; | |
| import { identity } from "../../Function.js"; | |
| import { stringOrRedacted } from "../../internal/redacted.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import { pipeArguments } from "../../Pipeable.js"; | |
| import * as Redacted from "../../Redacted.js"; | |
| import * as Result from "../../Result.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as SchemaAST from "../../SchemaAST.js"; | |
| import * as SchemaIssue from "../../SchemaIssue.js"; | |
| import * as SchemaTransformation from "../../SchemaTransformation.js"; | |
| import * as Scope from "../../Scope.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as UndefinedOr from "../../UndefinedOr.js"; | |
| import * as Sse from "../encoding/Sse.js"; | |
| import * as HttpEffect from "../http/HttpEffect.js"; | |
| import * as HttpMethod from "../http/HttpMethod.js"; | |
| import * as HttpRouter from "../http/HttpRouter.js"; | |
| import * as Request from "../http/HttpServerRequest.js"; | |
| import { HttpServerRequest } from "../http/HttpServerRequest.js"; | |
| import * as Response from "../http/HttpServerResponse.js"; | |
| import * as Multipart from "../http/Multipart.js"; | |
| import * as UrlParams from "../http/UrlParams.js"; | |
| import * as HttpApiEndpoint from "./HttpApiEndpoint.js"; | |
| import { HttpApiSchemaError } from "./HttpApiError.js"; | |
| import * as HttpApiMiddleware from "./HttpApiMiddleware.js"; | |
| import * as HttpApiSchema from "./HttpApiSchema.js"; | |
| import * as OpenApi from "./OpenApi.js"; | |
| /** | |
| * Registers an `HttpApi` with a `HttpRouter`. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const layer = (api, options) => HttpRouter.use(Effect.fnUntraced(function* (router) { | |
| const services = yield* Effect.context(); | |
| const routes = []; | |
| const availableGroups = Array.from(services.mapUnsafe.keys()).filter(key => key.startsWith("effect/httpapi/HttpApiGroup/")); | |
| for (const group of Object.values(api.groups)) { | |
| const groupRoutes = services.mapUnsafe.get(group.key)?.routes; | |
| if (groupRoutes === undefined) { | |
| const available = availableGroups.length === 0 ? "none" : availableGroups.join(", "); | |
| return yield* Effect.die(`HttpApiGroup "${group.identifier}" not found (key: "${group.key}"). Did you forget to provide HttpApiBuilder.group(api, "${group.identifier}", ...)? Available groups: ${available}`); | |
| } | |
| routes.push(...groupRoutes); | |
| } | |
| yield* router.addAll(routes); | |
| if (options?.openapiPath) { | |
| const spec = OpenApi.fromApi(api); | |
| yield* router.add("GET", options.openapiPath, Effect.succeed(Response.jsonUnsafe(spec))); | |
| } | |
| })); | |
| /** | |
| * Create a `Layer` that implements all endpoints in an `HttpApi` group. | |
| * | |
| * **Details** | |
| * | |
| * The `build` function receives an unimplemented `Handlers` instance that can | |
| * be used to add handlers to the group. Implement endpoints with | |
| * `handlers.handle`. | |
| * | |
| * @category handlers | |
| * @since 4.0.0 | |
| */ | |
| export const group = (api, groupName, build) => Layer.effectContext(Effect.gen(function* () { | |
| const services = (yield* Effect.context()).pipe(Context.omit(Scope.Scope)); | |
| const group = api.groups[groupName]; | |
| const result = build(makeHandlers(group)); | |
| const handlers = Effect.isEffect(result) ? yield* result : result; | |
| const routes = []; | |
| for (const item of handlers.handlers.values()) { | |
| routes.push(handlerToRoute(group, item, services)); | |
| } | |
| return Context.makeUnsafe(new Map([[group.key, { | |
| routes, | |
| handlers: handlers.handlers | |
| }]])); | |
| })); | |
| /** | |
| * Type identifier symbol used to brand `Handlers` values. | |
| * | |
| * @category type IDs | |
| * @since 4.0.0 | |
| */ | |
| export const HandlersTypeId = /*#__PURE__*/Symbol.for("@effect/platform/HttpApiBuilder/Handlers"); | |
| /** | |
| * Builds the server-side HTTP effect for a single endpoint in an API group using | |
| * the endpoint metadata, middleware, codecs, and supplied handler. | |
| * | |
| * @category handlers | |
| * @since 4.0.0 | |
| */ | |
| export const endpoint = (api, groupName, endpointName, handler) => Effect.contextWith(context => { | |
| const group = api.groups[groupName]; | |
| const endpoint = group.endpoints[endpointName]; | |
| return Effect.succeed(handlerToHttpEffect(group, endpoint, Context.omit(Scope.Scope)(context), handler, false)); | |
| }); | |
| /** | |
| * Decodes credentials for an HTTP API security scheme from the current request, | |
| * supporting bearer, API key, and basic authentication inputs. | |
| * | |
| * @category security | |
| * @since 4.0.0 | |
| */ | |
| export const securityDecode = self => { | |
| switch (self._tag) { | |
| case "Http": | |
| { | |
| return Effect.map(HttpServerRequest, | |
| // schemeLength + space | |
| request => Redacted.make((request.headers.authorization ?? "").slice(self.schemeLength + 1))); | |
| } | |
| case "ApiKey": | |
| { | |
| const key = self.in === "header" ? self.key.toLowerCase() : self.key; | |
| const schema = Schema.Struct({ | |
| [key]: Schema.String | |
| }); | |
| const decode = self.in === "query" ? Request.schemaSearchParams(schema) : self.in === "cookie" ? Request.schemaCookies(schema) : Request.schemaHeaders(schema); | |
| return Effect.match(decode, { | |
| onFailure: () => Redacted.make(""), | |
| onSuccess: match => Redacted.make(match[key]) | |
| }); | |
| } | |
| case "Basic": | |
| { | |
| const empty = { | |
| username: "", | |
| password: Redacted.make("") | |
| }; | |
| return HttpServerRequest.pipe(Effect.flatMap(request => Effect.fromResult(Encoding.decodeBase64String((request.headers.authorization ?? "").slice(basicLen)))), Effect.match({ | |
| onFailure: () => empty, | |
| onSuccess: header => { | |
| const parts = header.split(":"); | |
| if (parts.length !== 2) { | |
| return empty; | |
| } | |
| return { | |
| username: parts[0], | |
| password: Redacted.make(parts[1]) | |
| }; | |
| } | |
| })); | |
| } | |
| } | |
| }; | |
| /** | |
| * Registers a pre-response handler that sets an API-key cookie on the outgoing | |
| * response, defaulting the cookie to `secure` and `httpOnly` unless overridden. | |
| * | |
| * @category security | |
| * @since 4.0.0 | |
| */ | |
| export const securitySetCookie = (self, value, options) => HttpEffect.appendPreResponseHandler((_req, response) => Effect.orDie(Response.setCookie(response, self.key, stringOrRedacted(value), { | |
| secure: true, | |
| httpOnly: true, | |
| ...options | |
| }))); | |
| // ----------------------------------------------------------------------------- | |
| // Internal | |
| // ----------------------------------------------------------------------------- | |
| const basicLen = `Basic `.length; | |
| const HandlersProto = { | |
| [HandlersTypeId]: { | |
| _Endpoints: identity | |
| }, | |
| pipe() { | |
| return pipeArguments(this, arguments); | |
| }, | |
| handle(name, handler, options) { | |
| const endpoint = this.group.endpoints[name]; | |
| this.handlers.set(name, { | |
| endpoint, | |
| handler, | |
| isRaw: false, | |
| uninterruptible: options?.uninterruptible ?? false | |
| }); | |
| return this; | |
| }, | |
| handleRaw(name, handler, options) { | |
| const endpoint = this.group.endpoints[name]; | |
| this.handlers.set(name, { | |
| endpoint, | |
| handler, | |
| isRaw: true, | |
| uninterruptible: options?.uninterruptible ?? false | |
| }); | |
| return this; | |
| } | |
| }; | |
| const makeHandlers = group => { | |
| const self = Object.create(HandlersProto); | |
| self.group = group; | |
| self.handlers = new Map(); | |
| return self; | |
| }; | |
| function buildPayloadDecoders(payloadMap) { | |
| const result = new Map(); | |
| payloadMap.forEach(({ | |
| encoding, | |
| schemas | |
| }, contentType) => { | |
| const decode = Schema.decodeUnknownEffect(Schema.Union(schemas)); | |
| if (encoding._tag === "Multipart") { | |
| result.set(contentType, { | |
| _tag: "Multipart", | |
| mode: encoding.mode, | |
| limits: encoding.limits, | |
| decode | |
| }); | |
| } else { | |
| result.set(contentType, { | |
| _tag: encoding._tag, | |
| decode, | |
| nullOnEmpty: schemas.some(s => SchemaAST.isNull(SchemaAST.toEncoded(s.ast))) | |
| }); | |
| } | |
| }); | |
| return result; | |
| } | |
| function decodePayload(payloadBy, httpRequest, query) { | |
| const hasBody = HttpMethod.hasBody(httpRequest.method); | |
| const contentType = hasBody ? getRequestMediaType(httpRequest) : "application/x-www-form-urlencoded"; | |
| const existing = payloadBy.get(contentType); | |
| if (!existing) { | |
| return Response.text(`Unsupported content-type: ${contentType}`, { | |
| status: 415 | |
| }); | |
| } | |
| const { | |
| _tag, | |
| decode | |
| } = existing; | |
| switch (_tag) { | |
| case "Multipart": | |
| { | |
| if (existing.mode === "buffered") { | |
| let eff = Effect.orDie(httpRequest.multipart); | |
| if (existing.limits) { | |
| eff = Effect.provideContext(eff, Multipart.limitsServices(existing.limits)); | |
| } | |
| return Effect.flatMap(eff, decode); | |
| } | |
| return Effect.succeed(existing.limits ? Stream.provideContext(httpRequest.multipartStream, Multipart.limitsServices(existing.limits)) : httpRequest.multipartStream); | |
| } | |
| case "Json": | |
| const json = Effect.orDie(Effect.flatMap(httpRequest.text, text => { | |
| if (text === "") { | |
| return existing.nullOnEmpty ? Effect.succeed(null) : Effect.undefined; | |
| } | |
| return Effect.succeed(JSON.parse(text)); | |
| })); | |
| return Effect.flatMap(json, decode); | |
| case "Text": | |
| return Effect.flatMap(Effect.orDie(httpRequest.text), decode); | |
| case "FormUrlEncoded": | |
| { | |
| const source = hasBody ? Effect.map(Effect.orDie(httpRequest.urlParamsBody), UrlParams.toRecord) : Effect.succeed(query); | |
| return Effect.flatMap(source, decode); | |
| } | |
| case "Uint8Array": | |
| return Effect.flatMap(Effect.map(Effect.orDie(httpRequest.arrayBuffer), buffer => new Uint8Array(buffer)), decode); | |
| } | |
| } | |
| function handlerToHttpEffect(group, endpoint, context, handler, isRaw) { | |
| const encodeSuccess = Schema.encodeUnknownEffect(makeSuccessSchema(endpoint)); | |
| const encodeError = Schema.encodeUnknownEffect(makeErrorSchema(endpoint)); | |
| const decodeParams = UndefinedOr.map(endpoint.params, Schema.decodeUnknownEffect); | |
| const decodeHeaders = UndefinedOr.map(endpoint.headers, Schema.decodeUnknownEffect); | |
| const decodeQuery = UndefinedOr.map(endpoint.query, Schema.decodeUnknownEffect); | |
| const encodeStream = makeStreamEncoder(endpoint); | |
| const shouldParsePayload = endpoint.payload.size > 0 && !isRaw; | |
| const payloadBy = shouldParsePayload ? buildPayloadDecoders(endpoint.payload) : undefined; | |
| return applyMiddleware(group, endpoint, context, Effect.gen(function* () { | |
| const fiber = Fiber.getCurrent(); | |
| const context = fiber.context; | |
| const httpRequest = Context.getUnsafe(context, HttpServerRequest); | |
| const routeContext = Context.getUnsafe(context, HttpRouter.RouteContext); | |
| const query = Context.getUnsafe(context, Request.ParsedSearchParams); | |
| const request = { | |
| request: httpRequest, | |
| endpoint, | |
| group | |
| }; | |
| if (decodeParams) { | |
| request.params = yield* HttpApiSchemaError.wrap("Params", decodeParams(routeContext.params)); | |
| } | |
| if (decodeHeaders) { | |
| request.headers = yield* HttpApiSchemaError.wrap("Headers", decodeHeaders(httpRequest.headers)); | |
| } | |
| if (decodeQuery) { | |
| request.query = yield* HttpApiSchemaError.wrap("Query", decodeQuery(query)); | |
| } | |
| if (payloadBy) { | |
| const result = decodePayload(payloadBy, httpRequest, query); | |
| if (Response.isHttpServerResponse(result)) { | |
| return result; | |
| } | |
| if (result !== undefined) { | |
| request.payload = yield* HttpApiSchemaError.wrap("Payload", result); | |
| } | |
| } | |
| const response = yield* handler(request); | |
| if (Response.isHttpServerResponse(response)) { | |
| return response; | |
| } | |
| const streamResponse = encodeStream?.(response, context); | |
| if (streamResponse !== undefined) { | |
| return yield* HttpApiSchemaError.wrap("Body", streamResponse); | |
| } | |
| return yield* HttpApiSchemaError.wrap("Body", encodeSuccess(response)); | |
| })).pipe(Effect.withErrorReporting, Effect.catch(error => { | |
| if (HttpApiSchemaError.is(error)) return Effect.die(error); | |
| return Effect.orDie(encodeError(error)); | |
| }), Effect.provideContext(context)); | |
| } | |
| /** @internal */ | |
| export function handlerToRoute(group, handler, context) { | |
| const endpoint = handler.endpoint; | |
| return HttpRouter.route(endpoint.method, endpoint.path, handlerToHttpEffect(group, endpoint, context, handler.handler, handler.isRaw), { | |
| uninterruptible: handler.uninterruptible | |
| }); | |
| } | |
| const getRequestContentType = request => request.headers["content-type"] ? request.headers["content-type"].toLowerCase().trim() : "application/json"; | |
| const getRequestMediaType = request => { | |
| const contentType = getRequestContentType(request); | |
| const index = contentType.indexOf(";"); | |
| return index === -1 ? contentType : contentType.slice(0, index).trim(); | |
| }; | |
| const applyMiddleware = (group, endpoint, context, handler) => { | |
| const options = { | |
| group, | |
| endpoint | |
| }; | |
| for (const key_ of endpoint.middlewares) { | |
| const key = key_; | |
| const service = Context.getUnsafe(context, key); | |
| const apply = HttpApiMiddleware.isSecurity(key) ? makeSecurityMiddleware(key, service) : service; | |
| handler = apply(handler, options); | |
| } | |
| return handler; | |
| }; | |
| const securityMiddlewareCache = /*#__PURE__*/new WeakMap(); | |
| const makeSecurityMiddleware = (key, service) => { | |
| const cached = securityMiddlewareCache.get(service); | |
| if (cached !== undefined) { | |
| return cached; | |
| } | |
| const entries = Object.entries(key.security).map(([securityKey, security]) => ({ | |
| decode: securityDecode(security), | |
| middleware: service[securityKey] | |
| })); | |
| if (entries.length === 0) { | |
| return identity; | |
| } | |
| const middleware = Effect.fnUntraced(function* (handler, options) { | |
| let lastResult; | |
| for (let i = 0; i < entries.length; i++) { | |
| const { | |
| decode, | |
| middleware | |
| } = entries[i]; | |
| const result = yield* Effect.result(Effect.flatMap(decode, credential => middleware(handler, { | |
| credential, | |
| endpoint: options.endpoint, | |
| group: options.group | |
| }))); | |
| if (Result.isFailure(result)) { | |
| lastResult = result; | |
| continue; | |
| } | |
| return result.success; | |
| } | |
| return yield* Effect.fromResult(lastResult); | |
| }); | |
| securityMiddlewareCache.set(service, middleware); | |
| return middleware; | |
| }; | |
| const $HttpServerResponse = /*#__PURE__*/Schema.declare(Response.isHttpServerResponse); | |
| function makeStreamEncoder(endpoint) { | |
| const streamSchema = getStreamSuccessSchema(endpoint); | |
| if (streamSchema === undefined) { | |
| return undefined; | |
| } | |
| const hasBuffered = hasBufferedSuccess(endpoint); | |
| const status = HttpApiSchema.getStatusStream(streamSchema); | |
| const contentType = streamSchema.contentType; | |
| if (HttpApiSchema.isStreamUint8Array(streamSchema)) { | |
| return (response, context) => { | |
| if (!Stream.isStream(response)) { | |
| return hasBuffered ? undefined : expectedStreamResponse(response); | |
| } | |
| return Effect.succeed(Response.stream(Stream.provideContext(response, context), { | |
| status, | |
| contentType | |
| })); | |
| }; | |
| } | |
| const sseEncoder = makeSseEncoder(streamSchema); | |
| return (response, context) => { | |
| if (!Stream.isStream(response)) { | |
| return hasBuffered ? undefined : expectedStreamResponse(response); | |
| } | |
| return Effect.succeed(Response.stream(Stream.provideContext(encodeSseStream(response, sseEncoder), context), { | |
| status, | |
| contentType | |
| })); | |
| }; | |
| } | |
| function getStreamSuccessSchema(endpoint) { | |
| for (const schema of endpoint.success) { | |
| if (HttpApiSchema.isStreamSchema(schema)) { | |
| return schema; | |
| } | |
| } | |
| } | |
| function hasBufferedSuccess(endpoint) { | |
| for (const schema of endpoint.success) { | |
| if (Schema.isSchema(schema) && !HttpApiSchema.isStreamSchema(schema)) return true; | |
| } | |
| return endpoint.success.size === 0; | |
| } | |
| function expectedStreamResponse(response) { | |
| return Effect.fail(makeSchemaError(new SchemaIssue.InvalidValue(Option.some(response), { | |
| message: "Expected a streaming response" | |
| }))); | |
| } | |
| function makeSseEncoder(streamSchema) { | |
| const CauseSchema = Schema.toCodecJson(Schema.Cause(streamSchema.error, Schema.Defect())); | |
| return { | |
| sseMode: streamSchema.sseMode, | |
| encodeEvents: Schema.encodeUnknownEffect(Schema.Array(streamSchema.events)), | |
| encodeCause: Schema.encodeUnknownEffect(Schema.fromJsonString(CauseSchema)) | |
| }; | |
| } | |
| function encodeSseStream(stream, encoder) { | |
| return stream.pipe(encoder.sseMode === "data" ? Stream.map(value => ({ | |
| id: undefined, | |
| event: "message", | |
| data: value | |
| })) : identity, Stream.mapArrayEffect(chunk => Effect.orDie(encoder.encodeEvents(chunk))), Stream.catchCause(cause => Stream.fromEffect(encodeFailureEvent(cause, encoder))), Stream.map(renderSseEvent), Stream.encodeText); | |
| } | |
| function encodeFailureEvent(cause, encoder) { | |
| return encoder.encodeCause(cause).pipe(Effect.orDie, Effect.map(encodedCause => ({ | |
| id: undefined, | |
| event: reservedStreamFailureEvent, | |
| data: encodedCause | |
| }))); | |
| } | |
| const reservedStreamFailureEvent = "effect/httpapi/stream/failure"; | |
| function renderSseEvent(event) { | |
| return Sse.encoder.write({ | |
| _tag: "Event", | |
| event: event.event, | |
| id: event.id, | |
| data: event.data | |
| }); | |
| } | |
| function makeSchemaError(issue) { | |
| return new Schema.SchemaError(issue); | |
| } | |
| const toResponseSuccessSchema = /*#__PURE__*/toResponseSchema(HttpApiSchema.getStatusSuccess); | |
| const toResponseErrorSchema = /*#__PURE__*/toResponseSchema(HttpApiSchema.getStatusError); | |
| function makeSuccessSchema(endpoint) { | |
| const schemas = HttpApiEndpoint.getSuccessSchemas(endpoint).map(toResponseSuccessSchema); | |
| return schemas.length === 1 ? schemas[0] : Schema.Union(schemas); | |
| } | |
| function makeErrorSchema(endpoint) { | |
| const schemas = HttpApiEndpoint.getErrorSchemas(endpoint).map(toResponseErrorSchema); | |
| if (schemas.length === 0) return Schema.Never; | |
| return schemas.length === 1 ? schemas[0] : Schema.Union(schemas); | |
| } | |
| function toResponseSchema(getStatus) { | |
| const cache = new WeakMap(); | |
| return schema => { | |
| const cached = cache.get(schema.ast); | |
| if (cached !== undefined) { | |
| return cached; | |
| } | |
| const responseSchema = $HttpServerResponse.pipe(Schema.decodeTo(schema, getResponseTransformation(getStatus, schema))); | |
| cache.set(responseSchema.ast, responseSchema); | |
| return responseSchema; | |
| }; | |
| } | |
| function getResponseTransformation(getStatus, schema) { | |
| const ast = schema.ast; | |
| const encode = getResponseEncode(getStatus(ast), HttpApiSchema.getResponseEncoding(ast), HttpApiSchema.isNoContent(ast)); | |
| return SchemaTransformation.transformOrFail({ | |
| decode: res => Effect.fail(new SchemaIssue.Forbidden(Option.some(res), { | |
| message: "Encode only schema" | |
| })), | |
| encode | |
| }); | |
| } | |
| function getResponseEncode(status, encoding, isNoContent) { | |
| switch (encoding._tag) { | |
| case "Json": | |
| { | |
| return e => { | |
| if (e === undefined || isNoContent) { | |
| return Effect.succeed(Response.empty({ | |
| status | |
| })); | |
| } | |
| try { | |
| const s = JSON.stringify(e); | |
| return Effect.succeed(Response.text(s, { | |
| status, | |
| contentType: encoding.contentType | |
| })); | |
| } catch (error) { | |
| return Effect.fail(new SchemaIssue.InvalidValue(Option.some(e), { | |
| message: globalThis.String(error) | |
| })); | |
| } | |
| }; | |
| } | |
| case "Text": | |
| return e => Effect.succeed(Response.text(e, { | |
| status, | |
| contentType: encoding.contentType | |
| })); | |
| case "Uint8Array": | |
| return e => Effect.succeed(Response.uint8Array(e, { | |
| status, | |
| contentType: encoding.contentType | |
| })); | |
| case "FormUrlEncoded": | |
| return e => Effect.succeed(Response.urlParams(e, { | |
| status | |
| }).pipe(Response.setHeader("content-type", encoding.contentType))); | |
| } | |
| } | |
| //# sourceMappingURL=HttpApiBuilder.js.map |
Xet Storage Details
- Size:
- 19.9 kB
- Xet hash:
- 70bb4fbd9e281fa44d45cb4b6b73ec0f9c036acc6f2ec9263f10ac508e7a8a51
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.