Implement shared live fields and checked query hydration
Add native register acknowledgments and idempotent mutation replay, shared optimistic field controllers, overlapping custom setters, non-suspending hooks and explicit Suspense. Carry checked @live provenance through batched queries and hydrate shared browser fields with coverage leases. Update tracker/scaffolds/guides and verify compiler, PostgreSQL, browser and immutable workspace paths.
This commit is contained in:
@@ -5,6 +5,8 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
import type * as React from "react";
|
||||
export function useComponentOverlayContainer(): HTMLElement;
|
||||
export function useComponentStyleRoot(): ShadowRoot;
|
||||
/** Aggregate transport counters only; contains no object identities or values. */
|
||||
export function getFieldTransportMetrics(): Partial<Record<"get" | "watch" | "register" | "crdt" | "setter", Readonly<{started: number; active: number; completed: number; failed: number; totalMs: number}>>>;
|
||||
export type ObjectRef<AtomId extends string> = string & {
|
||||
readonly $quixosAtom: AtomId;
|
||||
};
|
||||
@@ -22,8 +24,9 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
|
||||
export type QueryValueType = {kind: "builtin" | "scalar"; name: string} | {kind: "record"; fields: Record<string, QueryValueType>} | {kind: "optional" | "list"; value: QueryValueType} | {kind: "object-ref"; expectation: {kind: "atom"; atomId: string} | {kind: "interface"; interfaceRevisionId: string}};
|
||||
export type QueryReference<Contract extends string = string> = {readonly $quixosRef: string; readonly queryContract: Contract};
|
||||
export interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||
export type QueryPartial<T> = T extends QueryReference ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
|
||||
export type QueryLiveProjection = {path: readonly string[]; selectionId: string; interfaceRevisionId: string; getOperationId: string; setOperationId?: string; watchOperationId?: string; inputKind: "fields" | "value"; valueType: QueryValueType; conditions: {include: boolean; defaultValue?: boolean; value: {kind: "variable"; name: string} | {kind: "literal"; value: unknown}}[]};
|
||||
export interface QueryDescriptor<Variables, Result> {readonly id: string; readonly definitionDigest: string; readonly rootInterfaceRevisionId: string; readonly variables: QueryValueType; readonly output: QueryValueType; readonly watch: boolean; readonly liveFields?: readonly QueryLiveProjection[]; readonly $types?: (variables: Variables, result: Result) => [Variables, Result]}
|
||||
export type QueryPartial<T> = T extends {readonly $quixosRef: string} | {readonly capability: {readonly getOperationId: string}} ? T : T extends readonly (infer Item)[] ? QueryPartial<Item>[] : T extends object ? {[Key in keyof T]?: QueryPartial<T[Key]>} : T;
|
||||
export type QueryFieldState = {path: readonly (string | number)[]; status: "pending" | "error"; error?: string};
|
||||
export type QueryState<T> = {refreshing: boolean; fields: QueryFieldState[]; runId?: string; sequence?: bigint; consistency?: string} & ({status: "loading"; data?: undefined; error?: undefined} | {status: "ready"; data: T; error?: undefined} | {status: "partial"; data: QueryPartial<T>; error?: undefined} | {status: "error"; data?: T | QueryPartial<T>; error: Error});
|
||||
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result>, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
|
||||
@@ -55,8 +58,12 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||
) => Promise<Result>;
|
||||
export const h: typeof React.createElement;
|
||||
export function useLiveField<T>(field: ReadableField<T>, options: {write: (value: T) => Promise<void>}): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: WritableField<T>): readonly [T, (value: T) => Promise<void>];
|
||||
export function useLiveField<T>(field: ReadableField<T>): readonly [T];
|
||||
export type LiveFieldState<T> = {refreshing: boolean; stale: boolean; pendingWrites: number; writeError?: Error; refresh(): Promise<void>; discard(): void} & ({status: "loading"; value?: undefined; error?: undefined} | {status: "ready"; value: T; error?: Error} | {status: "error"; value?: undefined; error: Error});
|
||||
export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>};
|
||||
export function useLiveField<T>(field: ReadableField<T>, options: {id: string; write: (value: T) => Promise<void>}): WritableLiveFieldState<T>;
|
||||
export function useLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T>;
|
||||
export function useLiveField<T>(field: ReadableField<T>): LiveFieldState<T>;
|
||||
export function useSuspenseLiveField<T>(field: WritableField<T>): WritableLiveFieldState<T> & {status: "ready"; value: T};
|
||||
export function useSuspenseLiveField<T>(field: ReadableField<T>): LiveFieldState<T> & {status: "ready"; value: T};
|
||||
}
|
||||
`;
|
||||
|
||||
+19
-1
@@ -1,5 +1,6 @@
|
||||
import type { BindingSchema } from "./index.js";
|
||||
import type { ValueType } from "../capability-model/types.js";
|
||||
import { queryPresentation } from "../query/presentation.js";
|
||||
|
||||
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
||||
export function generateReactBindings(
|
||||
@@ -128,9 +129,25 @@ export function generateReactBindings(
|
||||
return type(value);
|
||||
};
|
||||
const queries = pkg.checkedQueries ?? [];
|
||||
const presentedType = (
|
||||
value: ValueType,
|
||||
fields: ReturnType<typeof queryPresentation>,
|
||||
path: string[] = [],
|
||||
): string => {
|
||||
const field = fields.find((field) => JSON.stringify(field.path) === JSON.stringify(path));
|
||||
if (field)
|
||||
return `${field.conditions.length ? "(" : ""}${field.setOperationId ? "WritableField" : "ReadableField"}<${type(field.valueType)}>${field.conditions.length ? " | null)" : ""}`;
|
||||
if (value.kind === "optional") return `(${presentedType(value.value, fields, path)} | null)`;
|
||||
if (value.kind === "list") return `Array<${presentedType(value.value, fields, [...path, "*"])}>`;
|
||||
if (value.kind === "record")
|
||||
return `{${Object.entries(value.fields)
|
||||
.map(([key, child]) => `${q(key)}: ${presentedType(child, fields, [...path, key])}`)
|
||||
.join(";")}}`;
|
||||
return queryType(value);
|
||||
};
|
||||
const queryCode =
|
||||
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
|
||||
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.output)}`).join(";")}};\n` +
|
||||
`export type QueryResults = {${queries.map((query) => `${q(query.declaration.displayName)}: ${presentedType(query.output, queryPresentation(query, schema.interfaces))}`).join(";")}};\n` +
|
||||
`export const queries: {${queries.map((query) => `${q(query.declaration.displayName)}: QueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}]>`).join(";")}} = ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
queries.map((query) => [
|
||||
@@ -141,6 +158,7 @@ export function generateReactBindings(
|
||||
rootInterfaceRevisionId: query.declaration.root,
|
||||
variables: query.variables,
|
||||
output: query.output,
|
||||
liveFields: queryPresentation(query, schema.interfaces),
|
||||
watch: query.declaration.watch,
|
||||
},
|
||||
]),
|
||||
|
||||
+150
-46
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+37
-59
File diff suppressed because one or more lines are too long
@@ -2,15 +2,15 @@
|
||||
// @generated from file quixos/refs.proto (package quixos, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file quixos/refs.proto.
|
||||
*/
|
||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLYAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEgoIcXVlcnlfaWQYByABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3Mi0QEKDEZpZWxkRWRpdGluZxIbChNnZXR0ZXJfb3BlcmF0aW9uX2lkGAEgASgJEhsKE3NldHRlcl9vcGVyYXRpb25faWQYAiABKAkSFQoNZG9jdW1lbnRfdHlwZRgDIAEoCRIWCg5iaW5kaW5nX2RpZ2VzdBgEIAEoCRInCgRtb2RlGAUgASgOMhkucXVpeG9zLkZpZWxkRWRpdGluZy5Nb2RlIi8KBE1vZGUSDwoLVU5TUEVDSUZJRUQQABIMCghSRUdJU1RFUhABEggKBENSRFQQAiKWAQoSQ29uZm9ybWFuY2VXaXRuZXNzEhEKCW9iamVjdF9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSFgoOY29uZm9ybWFuY2VfaWQYAyABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAQgASgJEhcKD3dvcmtzcGFjZV9lcG9jaBgFIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJItgBChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABISCghxdWVyeV9pZBgHIAEoCUgAEhEKCW9iamVjdF9pZBgGIAEoCUIJCgdiaW5kaW5nIj0KDkVkZ2VEZXBlbmRlbmN5EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIVCg1wcm9qZWN0aW9uX2lkGAIgASgJYgZwcm90bzM");
|
||||
|
||||
/**
|
||||
* @generated from message quixos.CapabilityRef
|
||||
@@ -41,6 +41,71 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
|
||||
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 0);
|
||||
|
||||
/**
|
||||
* Resolved native editing semantics. A source snapshot alone grants no writer.
|
||||
*
|
||||
* @generated from message quixos.FieldEditing
|
||||
*/
|
||||
export type FieldEditing = Message<"quixos.FieldEditing"> & {
|
||||
/**
|
||||
* @generated from field: string getter_operation_id = 1;
|
||||
*/
|
||||
getterOperationId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string setter_operation_id = 2;
|
||||
*/
|
||||
setterOperationId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string document_type = 3;
|
||||
*/
|
||||
documentType: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string binding_digest = 4;
|
||||
*/
|
||||
bindingDigest: string;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.FieldEditing.Mode mode = 5;
|
||||
*/
|
||||
mode: FieldEditing_Mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.FieldEditing.
|
||||
* Use `create(FieldEditingSchema)` to create a new message.
|
||||
*/
|
||||
export const FieldEditingSchema: GenMessage<FieldEditing> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 1);
|
||||
|
||||
/**
|
||||
* @generated from enum quixos.FieldEditing.Mode
|
||||
*/
|
||||
export enum FieldEditing_Mode {
|
||||
/**
|
||||
* @generated from enum value: UNSPECIFIED = 0;
|
||||
*/
|
||||
UNSPECIFIED = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: REGISTER = 1;
|
||||
*/
|
||||
REGISTER = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: CRDT = 2;
|
||||
*/
|
||||
CRDT = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum quixos.FieldEditing.Mode.
|
||||
*/
|
||||
export const FieldEditing_ModeSchema: GenEnum<FieldEditing_Mode> = /*@__PURE__*/
|
||||
enumDesc(file_quixos_refs, 1, 0);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.ConformanceWitness
|
||||
*/
|
||||
@@ -76,7 +141,7 @@ export type ConformanceWitness = Message<"quixos.ConformanceWitness"> & {
|
||||
* Use `create(ConformanceWitnessSchema)` to create a new message.
|
||||
*/
|
||||
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 1);
|
||||
messageDesc(file_quixos_refs, 2);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageExportRef
|
||||
@@ -98,7 +163,7 @@ export type PackageExportRef = Message<"quixos.PackageExportRef"> & {
|
||||
* Use `create(PackageExportRefSchema)` to create a new message.
|
||||
*/
|
||||
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 2);
|
||||
messageDesc(file_quixos_refs, 3);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.InjectedDependency
|
||||
@@ -158,7 +223,7 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
||||
* Use `create(InjectedDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 3);
|
||||
messageDesc(file_quixos_refs, 4);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.EdgeDependency
|
||||
@@ -180,5 +245,5 @@ export type EdgeDependency = Message<"quixos.EdgeDependency"> & {
|
||||
* Use `create(EdgeDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 4);
|
||||
messageDesc(file_quixos_refs, 5);
|
||||
|
||||
|
||||
+40
-7
@@ -101,7 +101,7 @@ export async function compileQuery(
|
||||
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
|
||||
},
|
||||
Directive(node) {
|
||||
if (!["include", "skip"].includes(node.name.value))
|
||||
if (!["include", "skip", "live"].includes(node.name.value))
|
||||
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
|
||||
},
|
||||
InlineFragment(node) {
|
||||
@@ -180,6 +180,15 @@ export async function compileQuery(
|
||||
Field(node) {
|
||||
const parent = info.getParentType();
|
||||
const contract = parent && generated.byName.get(parent.name);
|
||||
if (node.directives?.some((directive) => directive.name.value === "live")) {
|
||||
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
|
||||
if (!member || member.kind !== "value" || member.queryRead?.execution !== "native")
|
||||
fail(
|
||||
"QUERY_LIVE_UNSUPPORTED",
|
||||
"@live requires a direct native-queryable value field, not RPC enrichment or a synthetic result",
|
||||
node,
|
||||
);
|
||||
}
|
||||
if (!contract || node.name.value === "_qx") return;
|
||||
mark(contract.revisionId, node.name.value, "select", node);
|
||||
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
||||
@@ -286,9 +295,12 @@ export async function compileQuery(
|
||||
field.type,
|
||||
selection.selectionSet,
|
||||
depth + 1,
|
||||
conditional || !!selection.directives?.length,
|
||||
conditional || !!selection.directives?.some((entry) => entry.name.value !== "live"),
|
||||
);
|
||||
if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional")
|
||||
if (
|
||||
(conditional || selection.directives?.some((entry) => entry.name.value !== "live")) &&
|
||||
fields[key]!.kind !== "optional"
|
||||
)
|
||||
fields[key] = valueType.optional(fields[key]!);
|
||||
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
||||
}
|
||||
@@ -354,10 +366,12 @@ export async function compileQuery(
|
||||
}
|
||||
};
|
||||
const conditions = (directives: readonly DirectiveNode[] = []) =>
|
||||
directives.map((directive) => ({
|
||||
include: directive.name.value === "include",
|
||||
value: argument(directive.arguments!.find((entry) => entry.name.value === "if")!.value),
|
||||
}));
|
||||
directives
|
||||
.filter((directive) => directive.name.value !== "live")
|
||||
.map((directive) => ({
|
||||
include: directive.name.value === "include",
|
||||
value: argument(directive.arguments!.find((entry) => entry.name.value === "if")!.value),
|
||||
}));
|
||||
const selection = (
|
||||
set: SelectionSetNode,
|
||||
parent: import("graphql").GraphQLObjectType,
|
||||
@@ -390,6 +404,13 @@ export async function compileQuery(
|
||||
{
|
||||
name: node.name.value,
|
||||
key: node.alias?.value ?? node.name.value,
|
||||
...(node.directives?.some((directive) => directive.name.value === "live")
|
||||
? {
|
||||
liveSelectionId: createHash("sha256")
|
||||
.update(JSON.stringify([node.loc?.source.name, node.loc?.start, contract?.revisionId, member?.id]))
|
||||
.digest("hex"),
|
||||
}
|
||||
: {}),
|
||||
...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
|
||||
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
||||
conditions: [...inherited, ...conditions(node.directives)],
|
||||
@@ -405,6 +426,18 @@ export async function compileQuery(
|
||||
const normalized = print(document),
|
||||
schema = printSchema(generated.schema);
|
||||
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
||||
const checkPresentation = (entries: QuerySelection[]) => {
|
||||
for (const key of new Set(entries.map((entry) => entry.key))) {
|
||||
const siblings = entries.filter((entry) => entry.key === key);
|
||||
if (siblings.some((entry) => entry.liveSelectionId) && siblings.some((entry) => !entry.liveSelectionId))
|
||||
throw new QueryCompileError(
|
||||
"QUERY_LIVE_CONFLICT",
|
||||
`${key} mixes live and plain selections; use separate aliases`,
|
||||
);
|
||||
checkPresentation(siblings.flatMap((entry) => entry.selection));
|
||||
}
|
||||
};
|
||||
checkPresentation(selections);
|
||||
const definitionDigest = createHash("sha256")
|
||||
.update(
|
||||
canonicalJson({
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { InterfaceRevision, ValueType } from "../capability-model/types.js";
|
||||
import type { CheckedQuery, QuerySelection } from "./types.js";
|
||||
|
||||
/** Browser presentation is separate from the portable query value contract. */
|
||||
export function queryPresentation(query: CheckedQuery, interfaces: readonly InterfaceRevision[]) {
|
||||
const result: {
|
||||
path: string[];
|
||||
selectionId: string;
|
||||
interfaceRevisionId: string;
|
||||
getOperationId: string;
|
||||
setOperationId?: string;
|
||||
watchOperationId?: string;
|
||||
inputKind: "value" | "fields";
|
||||
valueType: ValueType;
|
||||
conditions: (QuerySelection["conditions"][number] & { defaultValue?: boolean })[];
|
||||
}[] = [];
|
||||
const visit = (type: ValueType, selections: QuerySelection[], path: string[]) => {
|
||||
if (type.kind === "optional") return visit(type.value, selections, path);
|
||||
if (type.kind === "list") return visit(type.value, selections, [...path, "*"]);
|
||||
if (type.kind !== "record") return;
|
||||
for (const [name, field] of Object.entries(type.fields)) {
|
||||
const matches = selections.filter((entry) => entry.key === name);
|
||||
if (matches.some((entry) => entry.liveSelectionId) && matches.some((entry) => !entry.liveSelectionId))
|
||||
throw new Error(`QUERY_LIVE_CONFLICT: ${[...path, name].join(".")} mixes live and plain selections`);
|
||||
for (const entry of matches) {
|
||||
if (entry.liveSelectionId) {
|
||||
const iface = interfaces.find((iface) => iface.revisionId === entry.interfaceRevisionId)!;
|
||||
const member = iface.members.find((member) => member.id === entry.memberId)!;
|
||||
if (member.kind !== "value") throw new Error("QUERY_LIVE_UNSUPPORTED");
|
||||
const get = member.operations.find((op) => op.displayName === "get")!;
|
||||
const set = member.operations.find((op) => op.displayName === "set");
|
||||
result.push({
|
||||
path: [...path, name],
|
||||
selectionId: entry.liveSelectionId,
|
||||
interfaceRevisionId: iface.revisionId,
|
||||
getOperationId: get.id,
|
||||
setOperationId: set?.id,
|
||||
watchOperationId: member.operations.find((op) => op.displayName === "watch-start")?.id,
|
||||
inputKind: set?.inputType.kind === "record" ? "fields" : "value",
|
||||
valueType: get.outputType,
|
||||
conditions: entry.conditions.map((condition) => {
|
||||
const fallback =
|
||||
condition.value.kind === "variable" ? query.variableDefaults[condition.value.name] : undefined;
|
||||
return {
|
||||
...condition,
|
||||
...(fallback?.kind === "literal" && typeof fallback.value === "boolean"
|
||||
? { defaultValue: fallback.value }
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
});
|
||||
} else visit(field, entry.selection, [...path, name]);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(query.output, query.selection, []);
|
||||
return result;
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLSchema,
|
||||
GraphQLDirective,
|
||||
DirectiveLocation,
|
||||
specifiedDirectives,
|
||||
type GraphQLOutputType,
|
||||
type GraphQLInputType,
|
||||
type GraphQLFieldConfigMap,
|
||||
@@ -241,6 +244,7 @@ export function querySchema(
|
||||
return result;
|
||||
};
|
||||
const schema = new GraphQLSchema({
|
||||
directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })],
|
||||
query: new GraphQLObjectType({
|
||||
name: "QxQuery",
|
||||
fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } },
|
||||
|
||||
@@ -98,6 +98,7 @@ export type QueryArgument =
|
||||
| { kind: "list"; values: QueryArgument[] }
|
||||
| { kind: "object"; fields: Record<string, QueryArgument> };
|
||||
export interface QuerySelection {
|
||||
liveSelectionId?: string;
|
||||
name: string;
|
||||
key: string;
|
||||
interfaceRevisionId?: InterfaceRevisionId;
|
||||
|
||||
Reference in New Issue
Block a user