elysia
Version:
Ergonomic Framework for Human
1,210 lines (1,209 loc) • 35.4 kB
JavaScript
import {
Kind,
TransformKind
} from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import{ t }from"./type-system.mjs";
import{ mapValueError }from"./error.mjs";
const hasHeaderShorthand = "toJSON" in new Headers();
const replaceUrlPath = (url, pathname) => {
const urlObject = new URL(url);
urlObject.pathname = pathname;
return urlObject.toString();
};
const isClass = (v) => typeof v === "function" && /^\s*class\s+/.test(v.toString()) || // Handle Object.create(null)
v.toString && // Handle import * as Sentry from '@sentry/bun'
// This also handle [object Date], [object Array]
// and FFI value like [object Prisma]
v.toString().startsWith("[object ") && v.toString() !== "[object Object]" || // If object prototype is not pure, then probably a class-like object
isNotEmpty(Object.getPrototypeOf(v));
const isObject = (item) => item && typeof item === "object" && !Array.isArray(item);
const mergeDeep = (target, source, {
skipKeys,
override = true
} = {}) => {
if (!isObject(target) || !isObject(source)) return target;
for (const [key, value] of Object.entries(source)) {
if (skipKeys?.includes(key)) continue;
if (!isObject(value) || !(key in target) || isClass(value)) {
if (override || !(key in target))
target[key] = value;
continue;
}
target[key] = mergeDeep(
target[key],
value,
{ skipKeys, override }
);
}
return target;
};
const mergeCookie = (a, b) => {
const v = mergeDeep(Object.assign({}, a), b, {
skipKeys: ["properties"]
});
if ("properties" in v) delete v.properties;
return v;
};
const mergeObjectArray = (a = [], b = []) => {
if (!a) return void 0;
if (!b) return a;
const array = [];
const checksums = [];
if (!Array.isArray(a)) a = [a];
if (!Array.isArray(b)) b = [b];
for (const item of a) {
array.push(item);
if (item.checksum) checksums.push(item.checksum);
}
for (const item of b)
if (!checksums.includes(item.checksum)) array.push(item);
return array;
};
const primitiveHooks = [
"start",
"request",
"parse",
"transform",
"resolve",
"beforeHandle",
"afterHandle",
"mapResponse",
"afterResponse",
"trace",
"error",
"stop",
"body",
"headers",
"params",
"query",
"response",
"type",
"detail"
];
const primitiveHookMap = primitiveHooks.reduce(
(acc, x) => (acc[x] = true, acc),
{}
);
const mergeResponse = (a, b) => {
const isRecordNumber = (x) => typeof x === "object" && Object.keys(x).every(isNumericString);
if (isRecordNumber(a) && isRecordNumber(b)) return Object.assign(a, b);
else if (a && !isRecordNumber(a) && isRecordNumber(b))
return Object.assign({ 200: a }, b);
return b ?? a;
};
const mergeSchemaValidator = (a, b) => {
return {
body: b?.body ?? a?.body,
headers: b?.headers ?? a?.headers,
params: b?.params ?? a?.params,
query: b?.query ?? a?.query,
cookie: b?.cookie ?? a?.cookie,
// @ts-ignore ? This order is correct - SaltyAom
response: mergeResponse(
// @ts-ignore
a?.response,
// @ts-ignore
b?.response
)
};
};
const mergeHook = (a, b) => {
const { resolve: resolveA, ...restA } = a ?? {};
const { resolve: resolveB, ...restB } = b ?? {};
return {
...restA,
...restB,
// Merge local hook first
// @ts-ignore
body: b?.body ?? a?.body,
// @ts-ignore
headers: b?.headers ?? a?.headers,
// @ts-ignore
params: b?.params ?? a?.params,
// @ts-ignore
query: b?.query ?? a?.query,
// @ts-ignore
cookie: b?.cookie ?? a?.cookie,
// ? This order is correct - SaltyAom
response: mergeResponse(
// @ts-ignore
a?.response,
// @ts-ignore
b?.response
),
type: a?.type || b?.type,
detail: mergeDeep(
// @ts-ignore
b?.detail ?? {},
// @ts-ignore
a?.detail ?? {}
),
parse: mergeObjectArray(a?.parse, b?.parse),
transform: mergeObjectArray(a?.transform, b?.transform),
beforeHandle: mergeObjectArray(
mergeObjectArray(
fnToContainer(resolveA, "resolve"),
a?.beforeHandle
),
mergeObjectArray(
fnToContainer(resolveB, "resolve"),
b?.beforeHandle
)
),
afterHandle: mergeObjectArray(a?.afterHandle, b?.afterHandle),
mapResponse: mergeObjectArray(a?.mapResponse, b?.mapResponse),
afterResponse: mergeObjectArray(
a?.afterResponse,
b?.afterResponse
),
trace: mergeObjectArray(a?.trace, b?.trace),
error: mergeObjectArray(a?.error, b?.error)
};
};
const replaceSchemaType = (schema, options, root = true) => {
if (!Array.isArray(options)) {
options.original = schema;
return _replaceSchemaType(schema, options, root);
}
for (const option of options) {
option.original = schema;
schema = _replaceSchemaType(schema, option, root);
}
return schema;
};
const _replaceSchemaType = (schema, options, root = true) => {
if (!schema) return schema;
if (options.untilObjectFound && !root && schema.type === "object")
return schema;
const fromSymbol = options.from[Kind];
if (schema.oneOf) {
for (let i = 0; i < schema.oneOf.length; i++)
schema.oneOf[i] = _replaceSchemaType(schema.oneOf[i], options, root);
return schema;
}
if (schema.anyOf) {
for (let i = 0; i < schema.anyOf.length; i++)
schema.anyOf[i] = _replaceSchemaType(schema.anyOf[i], options, root);
return schema;
}
if (schema.allOf) {
for (let i = 0; i < schema.allOf.length; i++)
schema.allOf[i] = _replaceSchemaType(schema.allOf[i], options, root);
return schema;
}
if (schema.not) return _replaceSchemaType(schema.not, options, root);
const isRoot = root && !!options.excludeRoot;
if (schema[Kind] === fromSymbol) {
const { anyOf, oneOf, allOf, not, properties: properties2, items, ...rest } = schema;
const to = options.to(rest);
if (!to) return schema;
let transform;
const composeProperties = (v) => {
if (properties2 && v.type === "object") {
const newProperties = {};
for (const [key, value2] of Object.entries(properties2))
newProperties[key] = _replaceSchemaType(
value2,
options,
false
);
return {
...rest,
...v,
properties: newProperties
};
}
if (items && v.type === "array")
return {
...rest,
...v,
items: _replaceSchemaType(items, options, false)
};
const value = {
...rest,
...v
};
delete value["required"];
if (properties2 && v.type === "string" && v.format === "ObjectString" && v.default === "{}") {
transform = t.ObjectString(properties2, rest);
value.default = JSON.stringify(
Value.Create(t.Object(properties2))
);
value.properties = properties2;
}
if (items && v.type === "string" && v.format === "ArrayString" && v.default === "[]") {
transform = t.ArrayString(items, rest);
value.default = JSON.stringify(Value.Create(t.Array(items)));
value.items = items;
}
return value;
};
if (isRoot) {
if (properties2) {
const newProperties = {};
for (const [key, value] of Object.entries(properties2))
newProperties[key] = _replaceSchemaType(
value,
options,
false
);
return {
...rest,
properties: newProperties
};
} else if (items?.map)
return {
...rest,
items: items.map(
(v) => _replaceSchemaType(v, options, false)
)
};
return rest;
}
if (to.anyOf)
for (let i = 0; i < to.anyOf.length; i++)
to.anyOf[i] = composeProperties(to.anyOf[i]);
else if (to.oneOf)
for (let i = 0; i < to.oneOf.length; i++)
to.oneOf[i] = composeProperties(to.oneOf[i]);
else if (to.allOf)
for (let i = 0; i < to.allOf.length; i++)
to.allOf[i] = composeProperties(to.allOf[i]);
else if (to.not) to.not = composeProperties(to.not);
if (transform) to[TransformKind] = transform[TransformKind];
if (to.anyOf || to.oneOf || to.allOf || to.not) return to;
if (properties2) {
const newProperties = {};
for (const [key, value] of Object.entries(properties2))
newProperties[key] = _replaceSchemaType(
value,
options,
false
);
return {
...rest,
...to,
properties: newProperties
};
} else if (items?.map)
return {
...rest,
...to,
items: items.map(
(v) => _replaceSchemaType(v, options, false)
)
};
return {
...rest,
...to
};
}
const properties = schema?.properties;
if (properties && root && options.rootOnly !== true)
for (const [key, value] of Object.entries(properties)) {
switch (value[Kind]) {
case fromSymbol:
const { anyOf, oneOf, allOf, not, type, ...rest } = value;
const to = options.to(rest);
if (!to) return schema;
if (to.anyOf)
for (let i = 0; i < to.anyOf.length; i++)
to.anyOf[i] = { ...rest, ...to.anyOf[i] };
else if (to.oneOf)
for (let i = 0; i < to.oneOf.length; i++)
to.oneOf[i] = { ...rest, ...to.oneOf[i] };
else if (to.allOf)
for (let i = 0; i < to.allOf.length; i++)
to.allOf[i] = { ...rest, ...to.allOf[i] };
else if (to.not) to.not = { ...rest, ...to.not };
properties[key] = {
...rest,
..._replaceSchemaType(rest, options, false)
};
break;
case "Object":
case "Union":
properties[key] = _replaceSchemaType(value, options, false);
break;
default:
if (Array.isArray(value.items)) {
for (let i = 0; i < value.items.length; i++) {
value.items[i] = _replaceSchemaType(
value.items[i],
options,
false
);
}
} else if (value.anyOf || value.oneOf || value.allOf || value.not)
properties[key] = _replaceSchemaType(
value,
options,
false
);
else if (value.type === "array") {
value.items = _replaceSchemaType(
value.items,
options,
false
);
}
break;
}
}
return schema;
};
const createCleaner = (schema) => (value) => {
if (typeof value === "object")
try {
return Value.Clean(schema, structuredClone(value));
} catch {
try {
return Value.Clean(schema, value);
} catch {
return value;
}
}
return value;
};
const getSchemaValidator = (s, {
models = {},
dynamic = false,
modules,
normalize = false,
additionalProperties = false,
coerce = false,
additionalCoerce = []
} = {
modules: t.Module({})
}) => {
if (!s) return void 0;
let schema;
if (typeof s !== "string") schema = s;
else {
const isArray = s.endsWith("[]");
const key = isArray ? s.substring(0, s.length - 2) : s;
schema = modules.Import(key) ?? models[key];
if (isArray) schema = t.Array(schema);
}
if (!schema) return void 0;
if (coerce || additionalCoerce) {
if (coerce)
schema = replaceSchemaType(schema, [
{
from: t.Ref(""),
// @ts-expect-error
to: (options) => modules.Import(options["$ref"])
},
{
from: t.Number(),
to: (options) => t.Numeric(options),
untilObjectFound: true
},
{
from: t.Boolean(),
to: (options) => t.BooleanString(options),
untilObjectFound: true
},
...Array.isArray(additionalCoerce) ? additionalCoerce : [additionalCoerce]
]);
else {
schema = replaceSchemaType(schema, [
{
from: t.Ref(""),
// @ts-expect-error
to: (options) => modules.Import(options["$ref"])
},
...Array.isArray(additionalCoerce) ? additionalCoerce : [additionalCoerce]
]);
}
}
if (schema.type === "object" && "additionalProperties" in schema === false)
schema.additionalProperties = additionalProperties;
if (dynamic) {
const validator = {
schema,
references: "",
checkFunc: () => {
},
code: "",
Check: (value) => Value.Check(schema, value),
Errors: (value) => Value.Errors(schema, value),
Code: () => "",
Clean: createCleaner(schema),
Decode: (value) => Value.Decode(schema, value),
Encode: (value) => Value.Encode(schema, value)
};
if (normalize && schema.additionalProperties === false)
validator.Clean = createCleaner(schema);
if (schema.config) {
validator.config = schema.config;
if (validator?.schema?.config)
delete validator.schema.config;
}
validator.parse = (v) => {
try {
return validator.Decode(v);
} catch (error) {
throw [...validator.Errors(v)].map(mapValueError);
}
};
validator.safeParse = (v) => {
try {
return { success: true, data: validator.Decode(v), error: null };
} catch (error) {
const errors = [...compiled.Errors(v)].map(mapValueError);
return {
success: false,
data: null,
error: errors[0]?.summary,
errors
};
}
};
return validator;
}
const compiled = TypeCompiler.Compile(schema, Object.values(models));
compiled.Clean = createCleaner(schema);
if (schema.config) {
compiled.config = schema.config;
if (compiled?.schema?.config)
delete compiled.schema.config;
}
compiled.parse = (v) => {
try {
return compiled.Decode(v);
} catch (error) {
throw [...compiled.Errors(v)].map(mapValueError);
}
};
compiled.safeParse = (v) => {
try {
return { success: true, data: compiled.Decode(v), error: null };
} catch (error) {
const errors = [...compiled.Errors(v)].map(mapValueError);
return {
success: false,
data: null,
error: errors[0]?.summary,
errors
};
}
};
return compiled;
};
const getResponseSchemaValidator = (s, {
models = {},
modules,
dynamic = false,
normalize = false,
additionalProperties = false
}) => {
if (!s) return;
let maybeSchemaOrRecord;
if (typeof s !== "string") maybeSchemaOrRecord = s;
else {
const isArray = s.endsWith("[]");
const key = isArray ? s.substring(0, s.length - 2) : s;
maybeSchemaOrRecord = modules.Import(key) ?? models[key];
if (isArray)
maybeSchemaOrRecord = t.Array(maybeSchemaOrRecord);
}
if (!maybeSchemaOrRecord) return;
const compile = (schema, references) => {
if (dynamic)
return {
schema,
references: "",
checkFunc: () => {
},
code: "",
Check: (value) => Value.Check(schema, value),
Errors: (value) => Value.Errors(schema, value),
Code: () => "",
Clean: createCleaner(schema),
Decode: (value) => Value.Decode(schema, value),
Encode: (value) => Value.Encode(schema, value)
};
const compiledValidator = TypeCompiler.Compile(schema, references);
if (normalize && schema.additionalProperties === false)
compiledValidator.Clean = createCleaner(schema);
return compiledValidator;
};
const modelValues = Object.values(models);
if (Kind in maybeSchemaOrRecord) {
if ("additionalProperties" in maybeSchemaOrRecord === false)
maybeSchemaOrRecord.additionalProperties = additionalProperties;
return {
200: compile(maybeSchemaOrRecord, modelValues)
};
}
const record = {};
Object.keys(maybeSchemaOrRecord).forEach((status) => {
const maybeNameOrSchema = maybeSchemaOrRecord[+status];
if (typeof maybeNameOrSchema === "string") {
if (maybeNameOrSchema in models) {
const schema = models[maybeNameOrSchema];
schema.type === "object" && "additionalProperties" in schema === false;
record[+status] = Kind in schema ? compile(schema, modelValues) : schema;
}
return void 0;
}
if (maybeNameOrSchema.type === "object" && "additionalProperties" in maybeNameOrSchema === false)
maybeNameOrSchema.additionalProperties = additionalProperties;
record[+status] = Kind in maybeNameOrSchema ? compile(maybeNameOrSchema, modelValues) : maybeNameOrSchema;
});
return record;
};
const isBun = typeof Bun !== "undefined";
const hasHash = isBun && typeof Bun.hash === "function";
const checksum = (s) => {
if (hasHash) return Bun.hash(s);
let h = 9;
for (let i = 0; i < s.length; ) h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
return h = h ^ h >>> 9;
};
let _stringToStructureCoercions;
const stringToStructureCoercions = () => {
if (!_stringToStructureCoercions) {
_stringToStructureCoercions = [
{
from: t.Object({}),
to: () => t.ObjectString({}),
excludeRoot: true
},
{
from: t.Array(t.Any()),
to: () => t.ArrayString(t.Any())
}
];
}
return _stringToStructureCoercions;
};
let _coercePrimitiveRoot;
const coercePrimitiveRoot = () => {
if (!_coercePrimitiveRoot)
_coercePrimitiveRoot = [
{
from: t.Number(),
to: (options) => t.Numeric(options),
rootOnly: true
},
{
from: t.Boolean(),
to: (options) => t.BooleanString(options),
rootOnly: true
}
];
return _coercePrimitiveRoot;
};
const getCookieValidator = ({
validator,
modules,
defaultConfig = {},
config,
dynamic,
models
}) => {
let cookieValidator = getSchemaValidator(validator, {
modules,
dynamic,
models,
additionalProperties: true,
coerce: true,
additionalCoerce: stringToStructureCoercions()
});
if (isNotEmpty(defaultConfig)) {
if (cookieValidator) {
cookieValidator.config = mergeCookie(
// @ts-expect-error private
cookieValidator.config,
config
);
} else {
cookieValidator = getSchemaValidator(t.Cookie({}), {
modules,
dynamic,
models,
additionalProperties: true
});
cookieValidator.config = defaultConfig;
}
}
return cookieValidator;
};
const injectChecksum = (checksum2, x) => {
if (!x) return;
if (!Array.isArray(x)) {
const fn = x;
if (checksum2 && !fn.checksum) fn.checksum = checksum2;
if (fn.scope === "scoped") fn.scope = "local";
return fn;
}
const fns = [...x];
for (const fn of fns) {
if (checksum2 && !fn.checksum) fn.checksum = checksum2;
if (fn.scope === "scoped") fn.scope = "local";
}
return fns;
};
const mergeLifeCycle = (a, b, checksum2) => {
return {
start: mergeObjectArray(
a.start,
injectChecksum(checksum2, b?.start)
),
request: mergeObjectArray(
a.request,
injectChecksum(checksum2, b?.request)
),
parse: mergeObjectArray(
a.parse,
injectChecksum(checksum2, b?.parse)
),
transform: mergeObjectArray(
a.transform,
injectChecksum(checksum2, b?.transform)
),
beforeHandle: mergeObjectArray(
mergeObjectArray(
// @ts-ignore
fnToContainer(a.resolve, "resolve"),
a.beforeHandle
),
injectChecksum(
checksum2,
mergeObjectArray(
fnToContainer(b?.resolve, "resolve"),
b?.beforeHandle
)
)
),
afterHandle: mergeObjectArray(
a.afterHandle,
injectChecksum(checksum2, b?.afterHandle)
),
mapResponse: mergeObjectArray(
a.mapResponse,
injectChecksum(checksum2, b?.mapResponse)
),
afterResponse: mergeObjectArray(
a.afterResponse,
injectChecksum(checksum2, b?.afterResponse)
),
// Already merged on Elysia._use, also logic is more complicated, can't directly merge
trace: mergeObjectArray(
a.trace,
injectChecksum(checksum2, b?.trace)
),
error: mergeObjectArray(
a.error,
injectChecksum(checksum2, b?.error)
),
stop: mergeObjectArray(
a.stop,
injectChecksum(checksum2, b?.stop)
)
};
};
const asHookType = (fn, inject, { skipIfHasType = false } = {}) => {
if (!fn) return fn;
if (!Array.isArray(fn)) {
if (skipIfHasType) fn.scope ??= inject;
else fn.scope = inject;
return fn;
}
for (const x of fn)
if (skipIfHasType) x.scope ??= inject;
else x.scope = inject;
return fn;
};
const filterGlobal = (fn) => {
if (!fn) return fn;
if (!Array.isArray(fn))
switch (fn.scope) {
case "global":
case "scoped":
return { ...fn };
default:
return { fn };
}
const array = [];
for (const x of fn)
switch (x.scope) {
case "global":
case "scoped":
array.push({
...x
});
break;
}
return array;
};
const filterGlobalHook = (hook) => {
return {
// rest is validator
...hook,
type: hook?.type,
detail: hook?.detail,
parse: filterGlobal(hook?.parse),
transform: filterGlobal(hook?.transform),
beforeHandle: filterGlobal(hook?.beforeHandle),
afterHandle: filterGlobal(hook?.afterHandle),
mapResponse: filterGlobal(hook?.mapResponse),
afterResponse: filterGlobal(hook?.afterResponse),
error: filterGlobal(hook?.error),
trace: filterGlobal(hook?.trace)
};
};
const StatusMap = {
Continue: 100,
"Switching Protocols": 101,
Processing: 102,
"Early Hints": 103,
OK: 200,
Created: 201,
Accepted: 202,
"Non-Authoritative Information": 203,
"No Content": 204,
"Reset Content": 205,
"Partial Content": 206,
"Multi-Status": 207,
"Already Reported": 208,
"Multiple Choices": 300,
"Moved Permanently": 301,
Found: 302,
"See Other": 303,
"Not Modified": 304,
"Temporary Redirect": 307,
"Permanent Redirect": 308,
"Bad Request": 400,
Unauthorized: 401,
"Payment Required": 402,
Forbidden: 403,
"Not Found": 404,
"Method Not Allowed": 405,
"Not Acceptable": 406,
"Proxy Authentication Required": 407,
"Request Timeout": 408,
Conflict: 409,
Gone: 410,
"Length Required": 411,
"Precondition Failed": 412,
"Payload Too Large": 413,
"URI Too Long": 414,
"Unsupported Media Type": 415,
"Range Not Satisfiable": 416,
"Expectation Failed": 417,
"I'm a teapot": 418,
"Misdirected Request": 421,
"Unprocessable Content": 422,
Locked: 423,
"Failed Dependency": 424,
"Too Early": 425,
"Upgrade Required": 426,
"Precondition Required": 428,
"Too Many Requests": 429,
"Request Header Fields Too Large": 431,
"Unavailable For Legal Reasons": 451,
"Internal Server Error": 500,
"Not Implemented": 501,
"Bad Gateway": 502,
"Service Unavailable": 503,
"Gateway Timeout": 504,
"HTTP Version Not Supported": 505,
"Variant Also Negotiates": 506,
"Insufficient Storage": 507,
"Loop Detected": 508,
"Not Extended": 510,
"Network Authentication Required": 511
};
const InvertedStatusMap = Object.fromEntries(
Object.entries(StatusMap).map(([k, v]) => [v, k])
);
function removeTrailingEquals(digest) {
let trimmedDigest = digest;
while (trimmedDigest.endsWith("=")) {
trimmedDigest = trimmedDigest.slice(0, -1);
}
return trimmedDigest;
}
const encoder = new TextEncoder();
const signCookie = async (val, secret) => {
if (typeof val !== "string")
throw new TypeError("Cookie value must be provided as a string.");
if (secret === null) throw new TypeError("Secret key must be provided.");
const secretKey = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const hmacBuffer = await crypto.subtle.sign(
"HMAC",
secretKey,
encoder.encode(val)
);
return val + "." + removeTrailingEquals(Buffer.from(hmacBuffer).toString("base64"));
};
const unsignCookie = async (input, secret) => {
if (typeof input !== "string")
throw new TypeError("Signed cookie string must be provided.");
if (null === secret) throw new TypeError("Secret key must be provided.");
const tentativeValue = input.slice(0, input.lastIndexOf("."));
const expectedInput = await signCookie(tentativeValue, secret);
return expectedInput === input ? tentativeValue : false;
};
const traceBackMacro = (extension, property, manage) => {
if (!extension || typeof extension !== "object" || !property) return;
for (const [key, value] of Object.entries(property)) {
if (key in primitiveHookMap || !(key in extension)) continue;
const v = extension[key];
if (typeof v === "function") {
const hook = v(value);
if (typeof hook === "object") {
for (const [k, v2] of Object.entries(hook)) {
manage(k)({
fn: v2
});
}
}
}
delete property[key];
}
};
const createMacroManager = ({
globalHook,
localHook
}) => (stackName) => (type, fn) => {
if (typeof type === "function")
type = {
fn: type
};
if (stackName === "resolve") {
type = {
...type,
subType: "resolve"
};
}
if ("fn" in type || Array.isArray(type)) {
if (!localHook[stackName]) localHook[stackName] = [];
if (typeof localHook[stackName] === "function")
localHook[stackName] = [localHook[stackName]];
if (Array.isArray(type))
localHook[stackName] = localHook[stackName].concat(type);
else localHook[stackName].push(type);
return;
}
const { insert = "after", stack = "local" } = type;
if (typeof fn === "function") fn = { fn };
if (stack === "global") {
if (!Array.isArray(fn)) {
if (insert === "before") {
;
globalHook[stackName].unshift(fn);
} else {
;
globalHook[stackName].push(fn);
}
} else {
if (insert === "before") {
globalHook[stackName] = fn.concat(
globalHook[stackName]
);
} else {
globalHook[stackName] = globalHook[stackName].concat(fn);
}
}
} else {
if (!localHook[stackName]) localHook[stackName] = [];
if (typeof localHook[stackName] === "function")
localHook[stackName] = [localHook[stackName]];
if (!Array.isArray(fn)) {
if (insert === "before") {
;
localHook[stackName].unshift(fn);
} else {
;
localHook[stackName].push(fn);
}
} else {
if (insert === "before") {
localHook[stackName] = fn.concat(localHook[stackName]);
} else {
localHook[stackName] = localHook[stackName].concat(fn);
}
}
}
};
const parseNumericString = (message) => {
if (typeof message === "number") return message;
if (message.length < 16) {
if (message.trim().length === 0) return null;
const length = Number(message);
if (Number.isNaN(length)) return null;
return length;
}
if (message.length === 16) {
if (message.trim().length === 0) return null;
const number = Number(message);
if (Number.isNaN(number) || number.toString() !== message) return null;
return number;
}
return null;
};
const isNumericString = (message) => parseNumericString(message) !== null;
class PromiseGroup {
constructor(onError = console.error) {
this.onError = onError;
this.root = null;
this.promises = [];
}
/**
* The number of promises still being awaited.
*/
get size() {
return this.promises.length;
}
/**
* Add a promise to the group.
* @returns The promise that was added.
*/
add(promise) {
this.promises.push(promise);
this.root ||= this.drain();
return promise;
}
async drain() {
while (this.promises.length > 0) {
try {
await this.promises[0];
} catch (error) {
this.onError(error);
}
this.promises.shift();
}
this.root = null;
}
// Allow the group to be awaited.
then(onfulfilled, onrejected) {
return (this.root ?? Promise.resolve()).then(onfulfilled, onrejected);
}
}
const fnToContainer = (fn, subType) => {
if (!fn) return fn;
if (!Array.isArray(fn)) {
if (typeof fn === "function" || typeof fn === "string")
return subType ? { fn, subType } : { fn };
else if ("fn" in fn) return fn;
}
const fns = [];
for (const x of fn) {
if (typeof x === "function" || typeof x === "string")
fns.push(subType ? { fn: x, subType } : { fn: x });
else if ("fn" in x) fns.push(x);
}
return fns;
};
const localHookToLifeCycleStore = (a) => {
return {
...a,
start: fnToContainer(a?.start),
request: fnToContainer(a?.request),
parse: fnToContainer(a?.parse),
transform: fnToContainer(a?.transform),
beforeHandle: fnToContainer(a?.beforeHandle),
afterHandle: fnToContainer(a?.afterHandle),
mapResponse: fnToContainer(a?.mapResponse),
afterResponse: fnToContainer(a?.afterResponse),
trace: fnToContainer(a?.trace),
error: fnToContainer(a?.error),
stop: fnToContainer(a?.stop)
};
};
const lifeCycleToFn = (a) => {
const hook = {};
if (a.start?.map) hook.start = a.start.map((x) => x.fn);
if (a.request?.map) hook.request = a.request.map((x) => x.fn);
if (a.parse?.map) hook.parse = a.parse.map((x) => x.fn);
if (a.transform?.map) hook.transform = a.transform.map((x) => x.fn);
if (a.beforeHandle?.map) hook.beforeHandle = a.beforeHandle.map((x) => x.fn);
if (a.afterHandle?.map) hook.afterHandle = a.afterHandle.map((x) => x.fn);
if (a.mapResponse?.map) hook.mapResponse = a.mapResponse.map((x) => x.fn);
if (a.afterResponse?.map)
hook.afterResponse = a.afterResponse.map((x) => x.fn);
if (a.trace?.map) hook.trace = a.trace.map((x) => x.fn);
if (a.error?.map) hook.error = a.error.map((x) => x.fn);
if (a.stop?.map) hook.stop = a.stop.map((x) => x.fn);
return hook;
};
const cloneInference = (inference) => ({
body: inference.body,
cookie: inference.cookie,
headers: inference.headers,
query: inference.query,
set: inference.set,
server: inference.server,
request: inference.request,
route: inference.route
});
const redirect = (url, status = 302) => Response.redirect(url, status);
const ELYSIA_FORM_DATA = Symbol("ElysiaFormData");
const ELYSIA_REQUEST_ID = Symbol("ElysiaRequestId");
const form = (items) => {
const formData = new FormData();
for (const [key, value] of Object.entries(items)) {
if (Array.isArray(value)) {
for (const v of value) {
if (value instanceof File)
formData.append(key, value, value.name);
formData.append(key, v);
}
continue;
}
if (value instanceof File) formData.append(key, value, value.name);
formData.append(key, value);
}
return formData;
};
const randomId = () => {
const uuid = crypto.randomUUID();
return uuid.slice(0, 8) + uuid.slice(24, 32);
};
const deduplicateChecksum = (array) => {
const hashes = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (item.checksum) {
if (hashes.includes(item.checksum)) {
array.splice(i, 1);
i--;
}
hashes.push(item.checksum);
}
}
return array;
};
const promoteEvent = (events, as = "scoped") => {
if (!events) return;
if (as === "scoped") {
for (const event of events)
if ("scope" in event && event.scope === "local")
event.scope = "scoped";
return;
}
for (const event of events) if ("scope" in event) event.scope = "global";
};
const getLoosePath = (path) => {
if (path.charCodeAt(path.length - 1) === 47)
return path.slice(0, path.length - 1);
return path + "/";
};
const isNotEmpty = (obj) => {
if (!obj) return false;
for (const x in obj) return true;
return false;
};
const isEmptyHookProperty = (prop) => {
if (Array.isArray(prop)) return prop.length === 0;
return !prop;
};
const compressHistoryHook = (hook) => {
const history = { ...hook };
if (isEmptyHookProperty(hook.afterHandle)) delete history.afterHandle;
if (isEmptyHookProperty(hook.afterResponse)) delete history.afterResponse;
if (isEmptyHookProperty(hook.beforeHandle)) delete history.beforeHandle;
if (isEmptyHookProperty(hook.error)) delete history.error;
if (isEmptyHookProperty(hook.mapResponse)) delete history.mapResponse;
if (isEmptyHookProperty(hook.parse)) delete history.parse;
if (isEmptyHookProperty(hook.request)) delete history.request;
if (isEmptyHookProperty(hook.start)) delete history.start;
if (isEmptyHookProperty(hook.stop)) delete history.stop;
if (isEmptyHookProperty(hook.trace)) delete history.trace;
if (isEmptyHookProperty(hook.transform)) delete history.transform;
if (!history.type) delete history.type;
if (history.detail && !Object.keys(history.detail).length)
delete history.detail;
if (!history.body) delete history.body;
if (!history.cookie) delete history.cookie;
if (!history.headers) delete history.headers;
if (!history.query) delete history.query;
if (!history.params) delete history.params;
if (!history.response) delete history.response;
return history;
};
const decompressHistoryHook = (hook) => {
const history = { ...hook };
if (!history.afterHandle) history.afterHandle = [];
if (!history.afterResponse) history.afterResponse = [];
if (!history.beforeHandle) history.beforeHandle = [];
if (!history.error) history.error = [];
if (!history.mapResponse) history.mapResponse = [];
if (!history.parse) history.parse = [];
if (!history.request) history.request = [];
if (!history.start) history.start = [];
if (!history.stop) history.stop = [];
if (!history.trace) history.trace = [];
if (!history.transform) history.transform = [];
if (!history.body) history.body = void 0;
if (!history.cookie) history.cookie = void 0;
if (!history.headers) history.headers = void 0;
if (!history.query) history.query = void 0;
if (!history.params) history.params = void 0;
if (!history.response) history.response = void 0;
return history;
};
const encodePath = (path, { dynamic = false } = {}) => {
let encoded = encodeURIComponent(path).replace(/%2F/g, "/");
if (dynamic) encoded = encoded.replace(/%3A/g, ":").replace(/%3F/g, "?");
return encoded;
};
export {
ELYSIA_FORM_DATA,
ELYSIA_REQUEST_ID,
InvertedStatusMap,
PromiseGroup,
StatusMap,
asHookType,
checksum,
cloneInference,
coercePrimitiveRoot,
compressHistoryHook,
createMacroManager,
decompressHistoryHook,
deduplicateChecksum,
encodePath,
filterGlobalHook,
fnToContainer,
form,
getCookieValidator,
getLoosePath,
getResponseSchemaValidator,
getSchemaValidator,
hasHeaderShorthand,
injectChecksum,
isClass,
isNotEmpty,
isNumericString,
lifeCycleToFn,
localHookToLifeCycleStore,
mergeCookie,
mergeDeep,
mergeHook,
mergeLifeCycle,
mergeObjectArray,
mergeResponse,
mergeSchemaValidator,
primitiveHooks,
promoteEvent,
randomId,
redirect,
replaceSchemaType,
replaceUrlPath,
signCookie,
stringToStructureCoercions,
traceBackMacro,
unsignCookie
};