UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

131 lines (120 loc) 3.78 kB
const QUERY_VALUE_PREFIX = "~beignet-query:"; type EncodedQueryValue = | { type: "null" } | { type: "string"; value: string } | { type: "number"; value: number } | { type: "boolean"; value: boolean } | { type: "date"; value: string } | { type: "array"; value: EncodedQueryValue[] } | { type: "object"; value: Record<string, EncodedQueryValue> }; function encodeStructuredValue( value: unknown, ancestors: Set<object>, ): EncodedQueryValue { if (value === null) return { type: "null" }; if (typeof value === "string") return { type: "string", value }; if (typeof value === "number" && Number.isFinite(value)) { return { type: "number", value }; } if (typeof value === "boolean") return { type: "boolean", value }; if (value instanceof Date && Number.isFinite(value.getTime())) { return { type: "date", value: value.toISOString() }; } if (typeof value !== "object" || value === null) { throw new TypeError("Query value is not serializable."); } if (ancestors.has(value)) { throw new TypeError("Query value contains a circular reference."); } ancestors.add(value); try { if (Array.isArray(value)) { return { type: "array", value: value.map((entry) => encodeStructuredValue(entry, ancestors)), }; } const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) { throw new TypeError("Query value contains a non-plain object."); } return { type: "object", value: Object.fromEntries( Object.entries(value) .filter(([, entry]) => entry !== undefined) .map(([key, entry]) => [ key, encodeStructuredValue(entry, ancestors), ]), ), }; } finally { ancestors.delete(value); } } function decodeStructuredValue(value: unknown): unknown { if (typeof value !== "object" || value === null || !("type" in value)) { throw new TypeError("Invalid encoded query value."); } const encoded = value as { type: unknown; value?: unknown }; switch (encoded.type) { case "null": return null; case "string": if (typeof encoded.value === "string") return encoded.value; break; case "number": if (typeof encoded.value === "number" && Number.isFinite(encoded.value)) { return encoded.value; } break; case "boolean": if (typeof encoded.value === "boolean") return encoded.value; break; case "date": { if (typeof encoded.value !== "string") break; const date = new Date(encoded.value); if (Number.isFinite(date.getTime())) return date; break; } case "array": if (Array.isArray(encoded.value)) { return encoded.value.map(decodeStructuredValue); } break; case "object": if ( typeof encoded.value === "object" && encoded.value !== null && !Array.isArray(encoded.value) ) { return Object.fromEntries( Object.entries(encoded.value).map(([key, entry]) => [ key, decodeStructuredValue(entry), ]), ); } break; } throw new TypeError("Invalid encoded query value."); } export function encodeQueryValue(value: unknown): string { if (typeof value === "string" && !value.startsWith(QUERY_VALUE_PREFIX)) { return value; } return `${QUERY_VALUE_PREFIX}${JSON.stringify( encodeStructuredValue(value, new Set()), )}`; } export function decodeQueryValue(value: string): unknown { if (!value.startsWith(QUERY_VALUE_PREFIX)) return value; try { return decodeStructuredValue( JSON.parse(value.slice(QUERY_VALUE_PREFIX.length)) as unknown, ); } catch { return value; } }