Implement query execution, scoped RPC enrichment, live collections, and scaffold integration
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { decodeQxValue, type QxValueType } from "./bindings.js";
|
||||
import type { QueryResponse } from "./camino/api_pb.js";
|
||||
import type { QxObjectRef } from "./references.js";
|
||||
|
||||
export type QxQueryPartial<T> = T extends QxObjectRef
|
||||
? T
|
||||
: T extends readonly (infer Item)[]
|
||||
? QxQueryPartial<Item>[]
|
||||
: T extends object
|
||||
? { [Key in keyof T]?: QxQueryPartial<T[Key]> }
|
||||
: T;
|
||||
export type QxQuerySnapshot<T> = {
|
||||
runId: string;
|
||||
sequence: bigint;
|
||||
dataVersion: string;
|
||||
bindingDigest: string;
|
||||
consistency: string;
|
||||
fields: { path: readonly (string | number)[]; status: "pending" | "error"; error?: string }[];
|
||||
} & ({ status: "ready"; data: T } | { status: "partial"; data: QxQueryPartial<T> });
|
||||
declare const queryTypes: unique symbol;
|
||||
/** Generated shape evidence, not permission to run a query. */
|
||||
export interface QxQueryDescriptor<Variables, Result, Root extends string = string> {
|
||||
readonly id: string;
|
||||
readonly definitionDigest: string;
|
||||
readonly rootInterfaceRevisionId: Root;
|
||||
readonly variables: QxValueType;
|
||||
readonly output: QxValueType;
|
||||
readonly watch: boolean;
|
||||
readonly [queryTypes]?: (variables: Variables, result: Result) => [Variables, Result];
|
||||
}
|
||||
|
||||
export function decodeQuerySnapshot<T>(
|
||||
response: QueryResponse,
|
||||
output: QxValueType,
|
||||
runId: string,
|
||||
sequence: bigint,
|
||||
): QxQuerySnapshot<T> {
|
||||
if (response.preparationToken || response.residualWindows.length)
|
||||
throw new Error("QUERY_RESULT_UNFINISHED: private preparation is not a query result");
|
||||
const fields = [
|
||||
...response.pending.map((field) => ({ path: field.path, status: "pending" as const })),
|
||||
...response.errors.map((field) => ({ path: field.path, status: "error" as const, error: field.error })),
|
||||
].map((field) => ({
|
||||
...field,
|
||||
path: field.path.map((part) => {
|
||||
if (part.part.case !== "field" && part.part.case !== "index") throw new Error("QUERY_PATCH_INVALID");
|
||||
return part.part.value;
|
||||
}),
|
||||
}));
|
||||
// Pending/error values are absent, not successful nulls of a scalar type.
|
||||
const shape = structuredClone(output);
|
||||
for (const field of fields) {
|
||||
let cursor = shape;
|
||||
for (const [index, part] of field.path.entries()) {
|
||||
while (cursor.kind === "optional") cursor = cursor.value;
|
||||
const last = index === field.path.length - 1;
|
||||
if (typeof part === "string" && cursor.kind === "record" && cursor.fields[part]) {
|
||||
if (last) cursor.fields[part] = { kind: "optional", value: cursor.fields[part]! };
|
||||
else cursor = cursor.fields[part]!;
|
||||
} else if (typeof part === "number" && cursor.kind === "list") cursor = cursor.value;
|
||||
else throw new Error("QUERY_PATCH_INVALID");
|
||||
}
|
||||
}
|
||||
const data = decodeQxValue(shape, response.value, {}) as Record<string, unknown>;
|
||||
for (const field of fields) {
|
||||
let cursor: unknown = data;
|
||||
for (const [index, part] of field.path.entries()) {
|
||||
if (!cursor || typeof cursor !== "object") throw new Error("QUERY_PATCH_INVALID");
|
||||
if (index === field.path.length - 1) delete (cursor as Record<string | number, unknown>)[part];
|
||||
else cursor = (cursor as Record<string | number, unknown>)[part];
|
||||
}
|
||||
}
|
||||
return {
|
||||
runId,
|
||||
sequence,
|
||||
dataVersion: response.dataVersion,
|
||||
bindingDigest: response.bindingDigest,
|
||||
consistency: response.consistency,
|
||||
fields,
|
||||
...(fields.length ? { status: "partial", data } : { status: "ready", data }),
|
||||
} as QxQuerySnapshot<T>;
|
||||
}
|
||||
Reference in New Issue
Block a user