@beignet/core
Version:
Core framework primitives for Beignet
131 lines (120 loc) • 3.78 kB
text/typescript
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;
}
}