UNPKG

elysia

Version:

Ergonomic Framework for Human

510 lines (509 loc) 23 kB
import { TransformDecodeError } from "@sinclair/typebox/value"; import { parseCookie } from "./cookies.mjs"; import { ElysiaCustomStatusResponse, NotFoundError, status, ValidationError } from "./error.mjs"; import { parseQuery } from "./parse-query.mjs"; import { getSchemaProperties } from "./schema.mjs"; import { hasSetImmediate, redirect, StatusMap, signCookie } from "./utils.mjs"; const ARRAY_INDEX_REGEX = /^(.+)\[(\d+)\]$/, DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]), isDangerousKey = (key) => { if (DANGEROUS_KEYS.has(key)) return !0; const match = key.match(ARRAY_INDEX_REGEX); return match ? DANGEROUS_KEYS.has(match[1]) : !1; }, parseArrayKey = (key) => { const match = key.match(ARRAY_INDEX_REGEX); return match ? { name: match[1], index: parseInt(match[2], 10) } : null; }, parseObjectString = (entry) => { if (!(typeof entry != "string" || entry.charCodeAt(0) !== 123)) try { const parsed = JSON.parse(entry); if (parsed && typeof parsed == "object" && !Array.isArray(parsed)) return parsed; } catch { return; } }, setNestedValue = (obj, path, value) => { const keys = path.split("."), lastKey = keys.pop(); if (isDangerousKey(lastKey) || keys.some(isDangerousKey)) return; let current = obj; for (const key of keys) { const arrayInfo2 = parseArrayKey(key); if (arrayInfo2) { Array.isArray(current[arrayInfo2.name]) || (current[arrayInfo2.name] = []); const existing = current[arrayInfo2.name][arrayInfo2.index], isFile = typeof File < "u" && existing instanceof File; (!existing || typeof existing != "object" || Array.isArray(existing) || isFile) && (current[arrayInfo2.name][arrayInfo2.index] = parseObjectString(existing) ?? {}), current = current[arrayInfo2.name][arrayInfo2.index]; } else (!current[key] || typeof current[key] != "object") && (current[key] = {}), current = current[key]; } const arrayInfo = parseArrayKey(lastKey); arrayInfo ? (Array.isArray(current[arrayInfo.name]) || (current[arrayInfo.name] = []), current[arrayInfo.name][arrayInfo.index] = value) : current[lastKey] = value; }, normalizeFormValue = (value) => { if (value.length === 1) { const stringValue2 = value[0]; if (typeof stringValue2 == "string" && (stringValue2.charCodeAt(0) === 123 || stringValue2.charCodeAt(0) === 91)) try { const parsed2 = JSON.parse(stringValue2); if (parsed2 && typeof parsed2 == "object") return parsed2; } catch { } return value[0]; } const stringValue = value.find( (entry) => typeof entry == "string" ); if (!stringValue || typeof File > "u") return value; const files = value.filter((entry) => entry instanceof File); if (!files.length || stringValue.charCodeAt(0) !== 123) return value; let parsed; try { parsed = JSON.parse(stringValue); } catch { return value; } return typeof parsed != "object" || parsed === null ? value : (!("file" in parsed) && files.length === 1 ? parsed.file = files[0] : !("files" in parsed) && files.length > 1 && (parsed.files = files), parsed); }, injectDefaultValues = (typeChecker, obj) => { let schema = typeChecker.schema; if (!schema) return; schema.$defs?.[schema.$ref] && (schema = schema.$defs[schema.$ref]); const properties = getSchemaProperties(schema); if (properties) for (const [key, keySchema] of Object.entries(properties)) obj[key] ??= keySchema.default; }, createDynamicHandler = (app) => { const { mapResponse, mapEarlyResponse } = app["~adapter"].handler, defaultHeader = app.setHeaders; return async (request) => { const url = request.url, s = url.indexOf("/", 11), qi = url.indexOf("?", s + 1), path = qi === -1 ? url.substring(s) : url.substring(s, qi), set = { cookie: {}, status: 200, headers: defaultHeader ? { ...defaultHeader } : {} }, context = Object.assign( {}, // @ts-expect-error app.singleton.decorator, { set, // @ts-expect-error store: app.singleton.store, request, path, qi, error: status, status, redirect } ); let hooks; try { if (app.event.request) for (let i = 0; i < app.event.request.length; i++) { const onRequest = app.event.request[i].fn; let response2 = onRequest(context); if (response2 instanceof Promise && (response2 = await response2), response2 = mapEarlyResponse(response2, set), response2) return context.response = response2; } const methodKey = request.method === "GET" && request.headers.get("upgrade")?.toLowerCase() === "websocket" ? "WS" : request.method, handler = app.router.dynamic.find(request.method, path) ?? app.router.dynamic.find(methodKey, path) ?? app.router.dynamic.find("ALL", path); if (!handler) throw context.query = qi === -1 ? {} : parseQuery(url.substring(qi + 1)), new NotFoundError(); const { handle, validator, content, route } = handler.store; if (hooks = handler.store.hooks, hooks.config?.mount) return await hooks.config.mount(request); let body; if (request.method !== "GET" && request.method !== "HEAD") if (content) switch (content) { case "application/json": body = await request.json(); break; case "text/plain": body = await request.text(); break; case "application/x-www-form-urlencoded": body = parseQuery(await request.text()); break; case "application/octet-stream": body = await request.arrayBuffer(); break; case "multipart/form-data": { body = {}; const form = await request.formData(); for (const key of form.keys()) { if (body[key]) continue; const value = form.getAll(key), finalValue = normalizeFormValue(value); key.includes(".") || key.includes("[") ? setNestedValue(body, key, finalValue) : body[key] = finalValue; } break; } } else { let contentType; if (request.body && (contentType = request.headers.get("content-type")), contentType) { const index = contentType.indexOf(";"); if (index !== -1 && (contentType = contentType.slice(0, index)), context.contentType = contentType, hooks.parse) for (let i = 0; i < hooks.parse.length; i++) { const hook = hooks.parse[i].fn; if (typeof hook == "string") switch (hook) { case "json": case "application/json": body = await request.json(); break; case "text": case "text/plain": body = await request.text(); break; case "urlencoded": case "application/x-www-form-urlencoded": body = parseQuery( await request.text() ); break; case "arrayBuffer": case "application/octet-stream": body = await request.arrayBuffer(); break; case "formdata": case "multipart/form-data": { body = {}; const form = await request.formData(); for (const key of form.keys()) { if (body[key]) continue; const value = form.getAll(key), finalValue = normalizeFormValue(value); key.includes(".") || key.includes("[") ? setNestedValue(body, key, finalValue) : body[key] = finalValue; } break; } default: { const parser = app["~parser"][hook]; if (parser) { let temp = parser( context, contentType ); if (temp instanceof Promise && (temp = await temp), temp) { body = temp; break; } } break; } } else { let temp = hook(context, contentType); if (temp instanceof Promise && (temp = await temp), temp) { body = temp; break; } } } if (delete context.contentType, body === void 0) switch (contentType) { case "application/json": body = await request.json(); break; case "text/plain": body = await request.text(); break; case "application/x-www-form-urlencoded": body = parseQuery(await request.text()); break; case "application/octet-stream": body = await request.arrayBuffer(); break; case "multipart/form-data": { body = {}; const form = await request.formData(); for (const key of form.keys()) { if (body[key]) continue; const value = form.getAll(key), finalValue = normalizeFormValue(value); key.includes(".") || key.includes("[") ? setNestedValue(body, key, finalValue) : body[key] = finalValue; } break; } } } } context.route = route, context.body = body, context.params = handler?.params || void 0, context.query = qi === -1 ? {} : parseQuery(url.substring(qi + 1)), context.headers = {}; for (const [key, value] of request.headers.entries()) context.headers[key] = value; const cookieMeta = { domain: app.config.cookie?.domain ?? // @ts-expect-error validator?.cookie?.config.domain, expires: app.config.cookie?.expires ?? // @ts-expect-error validator?.cookie?.config.expires, httpOnly: app.config.cookie?.httpOnly ?? // @ts-expect-error validator?.cookie?.config.httpOnly, maxAge: app.config.cookie?.maxAge ?? // @ts-expect-error validator?.cookie?.config.maxAge, // @ts-expect-error path: app.config.cookie?.path ?? validator?.cookie?.config.path, priority: app.config.cookie?.priority ?? // @ts-expect-error validator?.cookie?.config.priority, partitioned: app.config.cookie?.partitioned ?? // @ts-expect-error validator?.cookie?.config.partitioned, sameSite: app.config.cookie?.sameSite ?? // @ts-expect-error validator?.cookie?.config.sameSite, secure: app.config.cookie?.secure ?? // @ts-expect-error validator?.cookie?.config.secure, secrets: app.config.cookie?.secrets ?? // @ts-expect-error validator?.cookie?.config.secrets, // @ts-expect-error sign: app.config.cookie?.sign ?? validator?.cookie?.config.sign }, cookieHeaderValue = request.headers.get("cookie"); context.cookie = await parseCookie( context.set, cookieHeaderValue, cookieMeta ); const headerValidator = validator?.createHeaders?.(); headerValidator && injectDefaultValues(headerValidator, context.headers); const paramsValidator = validator?.createParams?.(); paramsValidator && injectDefaultValues(paramsValidator, context.params); const queryValidator = validator?.createQuery?.(); if (queryValidator && injectDefaultValues(queryValidator, context.query), hooks.transform) for (let i = 0; i < hooks.transform.length; i++) { const hook = hooks.transform[i]; let response2 = hook.fn(context); if (response2 instanceof Promise && (response2 = await response2), response2 instanceof ElysiaCustomStatusResponse) { const result = mapEarlyResponse(response2, context.set); if (result) return context.response = result; } hook.subType === "derive" && Object.assign(context, response2); } if (validator) { if (headerValidator) { const _header = structuredClone(context.headers); for (const [key, value] of request.headers) _header[key] = value; if (validator.headers.Check(_header) === !1) throw new ValidationError( "header", validator.headers, _header ); } else validator.headers?.Decode && (context.headers = validator.headers.Decode(context.headers)); if (paramsValidator?.Check(context.params) === !1) throw new ValidationError( "params", validator.params, context.params ); if (validator.params?.Decode && (context.params = validator.params.Decode(context.params)), validator.query?.schema) { let schema = validator.query.schema; schema.$defs?.[schema.$ref] && (schema = schema.$defs[schema.$ref]); const properties = getSchemaProperties(schema); if (properties) for (const property of Object.keys(properties)) { const value = properties[property]; (value.type === "array" || value.items?.type === "string") && typeof context.query[property] == "string" && context.query[property] && (context.query[property] = context.query[property].split(",")); } } if (queryValidator?.Check(context.query) === !1) throw new ValidationError( "query", validator.query, context.query ); if (validator.query?.Decode && (context.query = validator.query.Decode(context.query)), validator.createCookie?.()) { let cookieValue = {}; for (const [key, value] of Object.entries(context.cookie)) cookieValue[key] = value.value; if (validator.cookie.Check(cookieValue) === !1) throw new ValidationError( "cookie", validator.cookie, cookieValue ); validator.cookie?.Decode && (cookieValue = validator.cookie.Decode( cookieValue )); } if (validator.createBody?.()?.Check(body) === !1) throw new ValidationError("body", validator.body, body); if (validator.body?.Decode) { let decoded = validator.body.Decode(body); decoded instanceof Promise && (decoded = await decoded), context.body = decoded?.value ?? decoded; } } if (hooks.beforeHandle) for (let i = 0; i < hooks.beforeHandle.length; i++) { const hook = hooks.beforeHandle[i]; let response2 = hook.fn(context); if (response2 instanceof Promise && (response2 = await response2), response2 instanceof ElysiaCustomStatusResponse) { const result = mapEarlyResponse(response2, context.set); if (result) return context.response = result; } if (hook.subType === "resolve") { Object.assign(context, response2); continue; } if (response2 !== void 0) { if (context.response = response2, hooks.afterHandle) for (let i2 = 0; i2 < hooks.afterHandle.length; i2++) { let newResponse = hooks.afterHandle[i2].fn( context ); newResponse instanceof Promise && (newResponse = await newResponse), newResponse && (response2 = newResponse); } const result = mapEarlyResponse(response2, context.set); if (result) return context.response = result; } } let response = typeof handle == "function" ? handle(context) : handle; if (response instanceof Promise && (response = await response), hooks.afterHandle?.length) { context.response = response; for (let i = 0; i < hooks.afterHandle.length; i++) { let response2 = hooks.afterHandle[i].fn( context ); response2 instanceof Promise && (response2 = await response2); const isCustomStatuResponse = response2 instanceof ElysiaCustomStatusResponse, status2 = isCustomStatuResponse ? response2.code : set.status ? typeof set.status == "string" ? StatusMap[set.status] : set.status : 200; isCustomStatuResponse && (set.status = status2, response2 = response2.response); const responseValidator = validator?.createResponse?.()?.[status2]; if (responseValidator?.Check(response2) === !1) if (responseValidator?.Clean) try { const temp = responseValidator.Clean(response2); if (responseValidator?.Check(temp) === !1) throw new ValidationError( "response", responseValidator, response2 ); response2 = temp; } catch (error) { throw error instanceof ValidationError ? error : new ValidationError( "response", responseValidator, response2 ); } else throw new ValidationError( "response", responseValidator, response2 ); if (responseValidator?.Encode && (context.response = response2 = responseValidator.Encode(response2)), responseValidator?.Clean) try { context.response = response2 = responseValidator.Clean(response2); } catch { } const result = mapEarlyResponse(response2, context.set); if (result !== void 0) return context.response = result; } } else { const isCustomStatuResponse = response instanceof ElysiaCustomStatusResponse, status2 = isCustomStatuResponse ? response.code : set.status ? typeof set.status == "string" ? StatusMap[set.status] : set.status : 200; isCustomStatuResponse && (set.status = status2, response = response.response); const responseValidator = validator?.createResponse?.()?.[status2]; if (responseValidator?.Check(response) === !1) if (responseValidator?.Clean) try { const temp = responseValidator.Clean(response); if (responseValidator?.Check(temp) === !1) throw new ValidationError( "response", responseValidator, response ); response = temp; } catch (error) { throw error instanceof ValidationError ? error : new ValidationError( "response", responseValidator, response ); } else throw new ValidationError( "response", responseValidator, response ); if (responseValidator?.Encode && (response = responseValidator.Encode(response)), responseValidator?.Clean) try { response = responseValidator.Clean(response); } catch { } } if (context.set.cookie && cookieMeta?.sign) { const secret = cookieMeta.secrets ? typeof cookieMeta.secrets == "string" ? cookieMeta.secrets : cookieMeta.secrets[0] : void 0; if (cookieMeta.sign === !0) { if (secret) for (const [key, cookie] of Object.entries( context.set.cookie )) context.set.cookie[key].value = await signCookie( cookie.value, secret ); } else { const properties = getSchemaProperties(validator?.cookie?.schema); if (secret) for (const name of cookieMeta.sign) !properties || !(name in properties) || context.set.cookie[name]?.value && (context.set.cookie[name].value = await signCookie( context.set.cookie[name].value, secret )); } } return mapResponse(context.response = response, context.set); } catch (error) { const reportedError = error instanceof TransformDecodeError && error.error ? error.error : error; return app.handleError(context, reportedError); } finally { const afterResponses = hooks ? hooks.afterResponse : app.event.afterResponse; afterResponses && (hasSetImmediate ? setImmediate(async () => { for (const afterResponse of afterResponses) await afterResponse.fn(context); }) : Promise.resolve().then(async () => { for (const afterResponse of afterResponses) await afterResponse.fn(context); })); } }; }, createDynamicErrorHandler = (app) => { const { mapResponse } = app["~adapter"].handler; return async (context, error) => { const errorContext = Object.assign(context, { error, code: error.code }); if (errorContext.set = context.set, // @ts-expect-error typeof error?.toResponse == "function" && !(error instanceof ValidationError) && !(error instanceof TransformDecodeError)) try { let raw = error.toResponse(); typeof raw?.then == "function" && (raw = await raw), raw instanceof Response && (context.set.status = raw.status), context.response = raw; } catch { } if (!context.response && app.event.error) for (let i = 0; i < app.event.error.length; i++) { let response = app.event.error[i].fn(errorContext); if (response instanceof Promise && (response = await response), response != null) return context.response = mapResponse( response, context.set ); } if (context.response) { if (app.event.mapResponse) for (let i = 0; i < app.event.mapResponse.length; i++) { let response = app.event.mapResponse[i].fn(errorContext); response instanceof Promise && (response = await response), response != null && (context.response = response); } return mapResponse(context.response, context.set); } return context.set.status = error.status ?? 500, mapResponse( typeof error.cause == "string" ? error.cause : error.message, context.set ); }; }; export { createDynamicErrorHandler, createDynamicHandler };