Compare commits
2 Commits
c9265e7903
...
exported
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d31ba51c | |||
| 2dffdbcd9f |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
||||||
"sourceCommit": "4c88728f144add3457b4273d4b9a04ee53c53be3",
|
"sourceCommit": "c87835bff85794bc232dbaaddc1eb3b593c2aab8",
|
||||||
"sourcePath": "quixos-protocol",
|
"sourcePath": "quixos-protocol",
|
||||||
"exportName": "quixos-protocol",
|
"exportName": "quixos-protocol",
|
||||||
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
|
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ syntax = "proto3";
|
|||||||
package camino;
|
package camino;
|
||||||
|
|
||||||
import "camino/schema.proto";
|
import "camino/schema.proto";
|
||||||
|
import "quixos/refs.proto";
|
||||||
|
|
||||||
service CaminoService {
|
service CaminoService {
|
||||||
rpc ExecuteQuery(QueryRequest) returns (QueryResponse);
|
rpc ExecuteQuery(QueryRequest) returns (QueryResponse);
|
||||||
@@ -69,6 +70,27 @@ message Value {
|
|||||||
CrdtValue crdt_value = 10;
|
CrdtValue crdt_value = 10;
|
||||||
}
|
}
|
||||||
ValueSource source = 11;
|
ValueSource source = 11;
|
||||||
|
// Trusted query coordinator annotation. Travels with values through residual
|
||||||
|
// row selection, then is removed in favor of final-path hydration metadata.
|
||||||
|
QueryFieldOrigin query_origin = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryFieldOrigin {
|
||||||
|
string selection_id = 1;
|
||||||
|
string atom_id = 2;
|
||||||
|
string interface_revision_id = 3;
|
||||||
|
string member_id = 4;
|
||||||
|
StateValueSource source = 5;
|
||||||
|
}
|
||||||
|
message QueryLiveField {
|
||||||
|
repeated QueryPathPart path = 1;
|
||||||
|
string selection_id = 2;
|
||||||
|
string object_id = 3;
|
||||||
|
quixos.CapabilityRef capability = 4;
|
||||||
|
string watch_operation_id = 5;
|
||||||
|
string setter_operation_id = 6;
|
||||||
|
quixos.FieldEditing editing = 7;
|
||||||
|
StateValueSource source = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InstallPersistencePlanRequest {
|
message InstallPersistencePlanRequest {
|
||||||
@@ -271,6 +293,7 @@ message QueryResponse {
|
|||||||
// Native reads share one database snapshot; package enrichment does not.
|
// Native reads share one database snapshot; package enrichment does not.
|
||||||
string consistency = 9; // native-snapshot | mixed
|
string consistency = 9; // native-snapshot | mixed
|
||||||
repeated QueryRelationalCapture relational_captures = 10;
|
repeated QueryRelationalCapture relational_captures = 10;
|
||||||
|
repeated QueryLiveField live_fields = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private coordinator input, removed before publishing a result. Memberships
|
// Private coordinator input, removed before publishing a result. Memberships
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ message QuerySelection {
|
|||||||
repeated QuerySelection selection = 8;
|
repeated QuerySelection selection = 8;
|
||||||
QueryRelationalPlan relational = 9;
|
QueryRelationalPlan relational = 9;
|
||||||
QueryPredicate predicate = 10;
|
QueryPredicate predicate = 10;
|
||||||
|
// Stable checked selection identity; empty for ordinary value projections.
|
||||||
|
string live_selection_id = 11;
|
||||||
}
|
}
|
||||||
// Resolved IDs, not GraphQL names, determine execution. Response selections
|
// Resolved IDs, not GraphQL names, determine execution. Response selections
|
||||||
// remain separate so aliases and fragments cannot change relational semantics.
|
// remain separate so aliases and fragments cannot change relational semantics.
|
||||||
|
|||||||
@@ -87,20 +87,16 @@ message InvokeCapabilityResponse {
|
|||||||
FieldEditing field_editing = 7;
|
FieldEditing field_editing = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolved from the checked native getter/setter binding, not Value.source.
|
|
||||||
message FieldEditing {
|
|
||||||
string getter_operation_id = 1;
|
|
||||||
string setter_operation_id = 2;
|
|
||||||
string document_type = 3;
|
|
||||||
string binding_digest = 4;
|
|
||||||
}
|
|
||||||
message EditCapabilityFieldRequest {
|
message EditCapabilityFieldRequest {
|
||||||
// The public getter; setter must belong to the same value member.
|
// The public getter; setter must belong to the same value member.
|
||||||
quixos.CapabilityRef capability = 1;
|
quixos.CapabilityRef capability = 1;
|
||||||
string object_id = 2;
|
string object_id = 2;
|
||||||
string setter_operation_id = 3;
|
string setter_operation_id = 3;
|
||||||
string binding_digest = 4;
|
string binding_digest = 4;
|
||||||
|
oneof edit {
|
||||||
camino.CrdtValue update = 5;
|
camino.CrdtValue update = 5;
|
||||||
|
camino.Value replacement = 7;
|
||||||
|
}
|
||||||
string client_mutation_id = 6;
|
string client_mutation_id = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ message CapabilityRef {
|
|||||||
ConformanceWitness conformance = 3;
|
ConformanceWitness conformance = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolved native editing semantics. A source snapshot alone grants no writer.
|
||||||
|
message FieldEditing {
|
||||||
|
string getter_operation_id = 1;
|
||||||
|
string setter_operation_id = 2;
|
||||||
|
string document_type = 3;
|
||||||
|
string binding_digest = 4;
|
||||||
|
enum Mode {
|
||||||
|
UNSPECIFIED = 0;
|
||||||
|
REGISTER = 1;
|
||||||
|
CRDT = 2;
|
||||||
|
}
|
||||||
|
Mode mode = 5;
|
||||||
|
}
|
||||||
|
|
||||||
message ConformanceWitness {
|
message ConformanceWitness {
|
||||||
string object_id = 1;
|
string object_id = 1;
|
||||||
string interface_revision_id = 2;
|
string interface_revision_id = 2;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare module "@quixos/web-studio-react-runtime" {
|
|||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
export function useComponentOverlayContainer(): HTMLElement;
|
export function useComponentOverlayContainer(): HTMLElement;
|
||||||
export function useComponentStyleRoot(): ShadowRoot;
|
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 & {
|
export type ObjectRef<AtomId extends string> = string & {
|
||||||
readonly $quixosAtom: AtomId;
|
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 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 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 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 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 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 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 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 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};
|
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},
|
options?: {clientMutationId?: string; signal?: AbortSignal},
|
||||||
) => Promise<Result>;
|
) => Promise<Result>;
|
||||||
export const h: typeof React.createElement;
|
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 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 function useLiveField<T>(field: WritableField<T>): readonly [T, (value: T) => Promise<void>];
|
export type WritableLiveFieldState<T> = LiveFieldState<T> & {set(value: T): Promise<void>; retry(): Promise<void>};
|
||||||
export function useLiveField<T>(field: ReadableField<T>): readonly [T];
|
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 { BindingSchema } from "./index.js";
|
||||||
import type { ValueType } from "../capability-model/types.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. */
|
/** Browser projection of the SAME checked RPC types, not a second props schema. */
|
||||||
export function generateReactBindings(
|
export function generateReactBindings(
|
||||||
@@ -128,9 +129,25 @@ export function generateReactBindings(
|
|||||||
return type(value);
|
return type(value);
|
||||||
};
|
};
|
||||||
const queries = pkg.checkedQueries ?? [];
|
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 =
|
const queryCode =
|
||||||
`export type QueryVariables = {${queries.map((query) => `${q(query.declaration.displayName)}: ${queryType(query.variables)}`).join(";")}};\n` +
|
`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(
|
`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(
|
Object.fromEntries(
|
||||||
queries.map((query) => [
|
queries.map((query) => [
|
||||||
@@ -141,6 +158,7 @@ export function generateReactBindings(
|
|||||||
rootInterfaceRevisionId: query.declaration.root,
|
rootInterfaceRevisionId: query.declaration.root,
|
||||||
variables: query.variables,
|
variables: query.variables,
|
||||||
output: query.output,
|
output: query.output,
|
||||||
|
liveFields: queryPresentation(query, schema.interfaces),
|
||||||
watch: query.declaration.watch,
|
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
+36
-58
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)
|
// @generated from file quixos/refs.proto (package quixos, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
|
||||||
import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
|
||||||
import type { Message } from "@bufbuild/protobuf";
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the file quixos/refs.proto.
|
* Describes the file quixos/refs.proto.
|
||||||
*/
|
*/
|
||||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLYAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEgoIcXVlcnlfaWQYByABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
|
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3Mi0QEKDEZpZWxkRWRpdGluZxIbChNnZXR0ZXJfb3BlcmF0aW9uX2lkGAEgASgJEhsKE3NldHRlcl9vcGVyYXRpb25faWQYAiABKAkSFQoNZG9jdW1lbnRfdHlwZRgDIAEoCRIWCg5iaW5kaW5nX2RpZ2VzdBgEIAEoCRInCgRtb2RlGAUgASgOMhkucXVpeG9zLkZpZWxkRWRpdGluZy5Nb2RlIi8KBE1vZGUSDwoLVU5TUEVDSUZJRUQQABIMCghSRUdJU1RFUhABEggKBENSRFQQAiKWAQoSQ29uZm9ybWFuY2VXaXRuZXNzEhEKCW9iamVjdF9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSFgoOY29uZm9ybWFuY2VfaWQYAyABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAQgASgJEhcKD3dvcmtzcGFjZV9lcG9jaBgFIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJItgBChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABISCghxdWVyeV9pZBgHIAEoCUgAEhEKCW9iamVjdF9pZBgGIAEoCUIJCgdiaW5kaW5nIj0KDkVkZ2VEZXBlbmRlbmN5EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIVCg1wcm9qZWN0aW9uX2lkGAIgASgJYgZwcm90bzM");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message quixos.CapabilityRef
|
* @generated from message quixos.CapabilityRef
|
||||||
@@ -41,6 +41,71 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
|
|||||||
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
|
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
|
||||||
messageDesc(file_quixos_refs, 0);
|
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
|
* @generated from message quixos.ConformanceWitness
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +141,7 @@ export type ConformanceWitness = Message<"quixos.ConformanceWitness"> & {
|
|||||||
* Use `create(ConformanceWitnessSchema)` to create a new message.
|
* Use `create(ConformanceWitnessSchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
|
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
|
||||||
messageDesc(file_quixos_refs, 1);
|
messageDesc(file_quixos_refs, 2);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message quixos.PackageExportRef
|
* @generated from message quixos.PackageExportRef
|
||||||
@@ -98,7 +163,7 @@ export type PackageExportRef = Message<"quixos.PackageExportRef"> & {
|
|||||||
* Use `create(PackageExportRefSchema)` to create a new message.
|
* Use `create(PackageExportRefSchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
|
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
|
||||||
messageDesc(file_quixos_refs, 2);
|
messageDesc(file_quixos_refs, 3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message quixos.InjectedDependency
|
* @generated from message quixos.InjectedDependency
|
||||||
@@ -158,7 +223,7 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
|||||||
* Use `create(InjectedDependencySchema)` to create a new message.
|
* Use `create(InjectedDependencySchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
||||||
messageDesc(file_quixos_refs, 3);
|
messageDesc(file_quixos_refs, 4);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message quixos.EdgeDependency
|
* @generated from message quixos.EdgeDependency
|
||||||
@@ -180,5 +245,5 @@ export type EdgeDependency = Message<"quixos.EdgeDependency"> & {
|
|||||||
* Use `create(EdgeDependencySchema)` to create a new message.
|
* Use `create(EdgeDependencySchema)` to create a new message.
|
||||||
*/
|
*/
|
||||||
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
|
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
|
||||||
messageDesc(file_quixos_refs, 4);
|
messageDesc(file_quixos_refs, 5);
|
||||||
|
|
||||||
|
|||||||
+37
-4
@@ -101,7 +101,7 @@ export async function compileQuery(
|
|||||||
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
|
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
|
||||||
},
|
},
|
||||||
Directive(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);
|
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
|
||||||
},
|
},
|
||||||
InlineFragment(node) {
|
InlineFragment(node) {
|
||||||
@@ -180,6 +180,15 @@ export async function compileQuery(
|
|||||||
Field(node) {
|
Field(node) {
|
||||||
const parent = info.getParentType();
|
const parent = info.getParentType();
|
||||||
const contract = parent && generated.byName.get(parent.name);
|
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;
|
if (!contract || node.name.value === "_qx") return;
|
||||||
mark(contract.revisionId, node.name.value, "select", node);
|
mark(contract.revisionId, node.name.value, "select", node);
|
||||||
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
const member = contract.members.find((entry) => entry.displayName === node.name.value)!;
|
||||||
@@ -286,9 +295,12 @@ export async function compileQuery(
|
|||||||
field.type,
|
field.type,
|
||||||
selection.selectionSet,
|
selection.selectionSet,
|
||||||
depth + 1,
|
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]!);
|
fields[key] = valueType.optional(fields[key]!);
|
||||||
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
|
||||||
}
|
}
|
||||||
@@ -354,7 +366,9 @@ export async function compileQuery(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const conditions = (directives: readonly DirectiveNode[] = []) =>
|
const conditions = (directives: readonly DirectiveNode[] = []) =>
|
||||||
directives.map((directive) => ({
|
directives
|
||||||
|
.filter((directive) => directive.name.value !== "live")
|
||||||
|
.map((directive) => ({
|
||||||
include: directive.name.value === "include",
|
include: directive.name.value === "include",
|
||||||
value: argument(directive.arguments!.find((entry) => entry.name.value === "if")!.value),
|
value: argument(directive.arguments!.find((entry) => entry.name.value === "if")!.value),
|
||||||
}));
|
}));
|
||||||
@@ -390,6 +404,13 @@ export async function compileQuery(
|
|||||||
{
|
{
|
||||||
name: node.name.value,
|
name: node.name.value,
|
||||||
key: node.alias?.value ?? 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 && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
|
||||||
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
|
||||||
conditions: [...inherited, ...conditions(node.directives)],
|
conditions: [...inherited, ...conditions(node.directives)],
|
||||||
@@ -405,6 +426,18 @@ export async function compileQuery(
|
|||||||
const normalized = print(document),
|
const normalized = print(document),
|
||||||
schema = printSchema(generated.schema);
|
schema = printSchema(generated.schema);
|
||||||
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
|
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")
|
const definitionDigest = createHash("sha256")
|
||||||
.update(
|
.update(
|
||||||
canonicalJson({
|
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,
|
GraphQLList,
|
||||||
GraphQLNonNull,
|
GraphQLNonNull,
|
||||||
GraphQLSchema,
|
GraphQLSchema,
|
||||||
|
GraphQLDirective,
|
||||||
|
DirectiveLocation,
|
||||||
|
specifiedDirectives,
|
||||||
type GraphQLOutputType,
|
type GraphQLOutputType,
|
||||||
type GraphQLInputType,
|
type GraphQLInputType,
|
||||||
type GraphQLFieldConfigMap,
|
type GraphQLFieldConfigMap,
|
||||||
@@ -241,6 +244,7 @@ export function querySchema(
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
const schema = new GraphQLSchema({
|
const schema = new GraphQLSchema({
|
||||||
|
directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })],
|
||||||
query: new GraphQLObjectType({
|
query: new GraphQLObjectType({
|
||||||
name: "QxQuery",
|
name: "QxQuery",
|
||||||
fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } },
|
fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } },
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export type QueryArgument =
|
|||||||
| { kind: "list"; values: QueryArgument[] }
|
| { kind: "list"; values: QueryArgument[] }
|
||||||
| { kind: "object"; fields: Record<string, QueryArgument> };
|
| { kind: "object"; fields: Record<string, QueryArgument> };
|
||||||
export interface QuerySelection {
|
export interface QuerySelection {
|
||||||
|
liveSelectionId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
key: string;
|
key: string;
|
||||||
interfaceRevisionId?: InterfaceRevisionId;
|
interfaceRevisionId?: InterfaceRevisionId;
|
||||||
|
|||||||
Vendored
+13
-4
@@ -8,7 +8,7 @@ export const queryFixtureSource = {
|
|||||||
repository: "https://query-fixture.example.test/source.git",
|
repository: "https://query-fixture.example.test/source.git",
|
||||||
commit: "1".repeat(40),
|
commit: "1".repeat(40),
|
||||||
};
|
};
|
||||||
export async function queryWorkspaceFixture() {
|
export async function queryWorkspaceFixture(options: { live?: boolean } = {}) {
|
||||||
const interfaces = new Map<string, InterfaceRevision>();
|
const interfaces = new Map<string, InterfaceRevision>();
|
||||||
const sources: Record<string, string> = {};
|
const sources: Record<string, string> = {};
|
||||||
function iface(name: string, members: string, parameters = "") {
|
function iface(name: string, members: string, parameters = "") {
|
||||||
@@ -29,7 +29,7 @@ export async function queryWorkspaceFixture() {
|
|||||||
`
|
`
|
||||||
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
|
queryable value title id "title" : string {get id "title:get"; set id "title:set";}
|
||||||
queryable value done id "done" : bool {get id "done:get";}
|
queryable value done id "done" : bool {get id "done:get";}
|
||||||
queryable value rank id "rank" : int64 {get id "rank:get";}
|
queryable value rank id "rank" : int64 {get id "rank:get"; set id "rank:set";}
|
||||||
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
|
queryable relation assignee id "assignee" : optional-one interface PersonFacts {resolve id "assignee:resolve";}
|
||||||
queryable rpc value score id "score" : int32 {get id "score:get";}
|
queryable rpc value score id "score" : int32 {get id "score:get";}
|
||||||
`,
|
`,
|
||||||
@@ -81,7 +81,16 @@ export async function queryWorkspaceFixture() {
|
|||||||
"score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {score}}}}}}}`,
|
"score-totals.graphql": `query ScoreTotals {root {_qx {relations {items {aggregate {count sum {score}}}}}}}`,
|
||||||
};
|
};
|
||||||
pkg.checkedQueries = await Promise.all(
|
pkg.checkedQueries = await Promise.all(
|
||||||
pkg.queries!.map((query) => compileQuery(query, allInterfaces, async (name) => documents[name]!)),
|
pkg.queries!.map((query) =>
|
||||||
|
compileQuery(query, allInterfaces, async (name) =>
|
||||||
|
options.live
|
||||||
|
? documents[name]!.replace(/\btitle\b/g, "title @live").replace(
|
||||||
|
"title @live rank done",
|
||||||
|
"title @live rank @live done @live",
|
||||||
|
)
|
||||||
|
: documents[name]!,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
|
const implementation = (atom: string) => `conform ${atom} as TaskFacts id "${atom}-facts" {
|
||||||
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
|
private state Title${atom} id "${atom}:title" on ${atom} : string policy crdt(string) default "Untitled";
|
||||||
@@ -92,7 +101,7 @@ export async function queryWorkspaceFixture() {
|
|||||||
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
|
interface PersonFacts projection tasks id "${atom}:assignee:inverse" many;
|
||||||
}
|
}
|
||||||
bind title.get to state Title${atom}.read;
|
bind title.get to state Title${atom}.read;
|
||||||
bind title.set to package Queries.titleSet with {title to state Title${atom};};
|
${options.live ? `bind title.set to state Title${atom}.write;` : `bind title.set to package Queries.titleSet with {title to state Title${atom};};`}
|
||||||
bind done to state Done${atom}; bind rank to state Rank${atom};
|
bind done to state Done${atom}; bind rank to state Rank${atom};
|
||||||
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
|
bind assignee.resolve to edge Assignee${atom}.assignee.resolve;
|
||||||
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
|
bind score.get to package Queries.score query-reason "Explicit bounded score computation";
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { compileCapabilityResourceSource } from "../src/capability-language/inde
|
|||||||
import { compileQuery } from "../src/query/compile.js";
|
import { compileQuery } from "../src/query/compile.js";
|
||||||
import { QueryCompileError } from "../src/query/types.js";
|
import { QueryCompileError } from "../src/query/types.js";
|
||||||
import { checkQueryTemplate } from "../src/query/templates.js";
|
import { checkQueryTemplate } from "../src/query/templates.js";
|
||||||
|
import { queryPresentation } from "../src/query/presentation.js";
|
||||||
|
import { querySelectionToWire } from "../src/query/proto.js";
|
||||||
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
import type { InterfaceRevision } from "../src/capability-model/types.js";
|
||||||
|
|
||||||
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
|
||||||
@@ -125,6 +127,48 @@ const document = `query Upcoming($first: Int!, $before: Int64!) {
|
|||||||
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
|
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
|
||||||
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
|
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
|
||||||
|
|
||||||
|
test("live selections preserve aliases, nullable values, fragment conditions and wire identity", async () => {
|
||||||
|
const checked = await compile(
|
||||||
|
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
|
||||||
|
`fragment Row on TaskFacts {label: title @live due @live}`,
|
||||||
|
);
|
||||||
|
const fields = queryPresentation(checked, [collection, facts]);
|
||||||
|
assert.deepEqual(
|
||||||
|
fields.map((f) => f.path),
|
||||||
|
[
|
||||||
|
["root", "items", "entries", "*", "node", "label"],
|
||||||
|
["root", "items", "entries", "*", "node", "due"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(fields[0]!.getOperationId, "title:get");
|
||||||
|
assert.equal(fields[0]!.setOperationId, undefined);
|
||||||
|
assert.equal(fields[1]!.valueType.kind, "optional");
|
||||||
|
assert.equal(fields[0]!.conditions.length, 1);
|
||||||
|
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes(fields[0]!.selectionId));
|
||||||
|
const defaults = await compile(
|
||||||
|
`query Upcoming($show: Boolean! = true) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`,
|
||||||
|
"fragment Row on TaskFacts {title @live}",
|
||||||
|
);
|
||||||
|
assert.equal(queryPresentation(defaults, [collection, facts])[0]!.conditions[0]!.defaultValue, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("live selections reject RPC getters, synthetic selections and conflicting presentations", async () => {
|
||||||
|
for (const [selected, code] of [
|
||||||
|
["score @live", "QUERY_LIVE_UNSUPPORTED"],
|
||||||
|
["_qx @live {ref}", "QUERY_LIVE_UNSUPPORTED"],
|
||||||
|
["title @live title", "QUERY_LIVE_CONFLICT"],
|
||||||
|
["title @live(unchecked: true)", "QUERY_VALIDATION"],
|
||||||
|
])
|
||||||
|
await assert.rejects(
|
||||||
|
compile(
|
||||||
|
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
|
||||||
|
`fragment Row on TaskFacts {${selected}}`,
|
||||||
|
'allow TaskFacts.score select "bounded";',
|
||||||
|
),
|
||||||
|
(error: unknown) => error instanceof QueryCompileError && error.code === code,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
|
||||||
const result = compileCapabilityResourceSource(
|
const result = compileCapabilityResourceSource(
|
||||||
`import interface Tasks;
|
`import interface Tasks;
|
||||||
|
|||||||
+26
-10
@@ -7,13 +7,14 @@ import { spawnSync } from "node:child_process";
|
|||||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||||
import { generateReactBindings } from "../src/bindings/react.js";
|
import { generateReactBindings } from "../src/bindings/react.js";
|
||||||
import { reactPlatformTypes } from "../src/bindings/react-platform.js";
|
import { reactPlatformTypes } from "../src/bindings/react-platform.js";
|
||||||
|
import { compileQuery } from "../src/query/compile.js";
|
||||||
|
|
||||||
const source = { repository: "https://example.test/fields.git", commit: "a".repeat(40) };
|
const source = { repository: "https://example.test/fields.git", commit: "a".repeat(40) };
|
||||||
test("React bindings preserve read-only, writable and nested reference contracts", async (t) => {
|
test("React bindings preserve read-only, writable and nested reference contracts", async (t) => {
|
||||||
const iface = compileCapabilityResourceSource(
|
const iface = compileCapabilityResourceSource(
|
||||||
`interface Fields id "fields" revision "fields@1" {
|
`interface Fields id "fields" revision "fields@1" {
|
||||||
value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
|
queryable value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; }
|
||||||
value summary id "summary" : string { get id "summary:get"; }
|
queryable value summary id "summary" : string { get id "summary:get"; }
|
||||||
}`,
|
}`,
|
||||||
{ source },
|
{ source },
|
||||||
);
|
);
|
||||||
@@ -22,11 +23,19 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
|||||||
const pkg = compileCapabilityResourceSource(
|
const pkg = compileCapabilityResourceSource(
|
||||||
`import interface Fields; package P id "p" revision "p@1" {
|
`import interface Fields; package P id "p" revision "p@1" {
|
||||||
function props id "props" : unit -> record {fields: interface-ref<Fields>; caption: string;};
|
function props id "props" : unit -> record {fields: interface-ref<Fields>; caption: string;};
|
||||||
|
query Editor id "editor" root Fields document "editor.graphql" operation "Editor" {max rows 1; watch;}
|
||||||
}`,
|
}`,
|
||||||
{ source, environment: { interfaces: new Map([["Fields", iface.resource.revision]]) } },
|
{ source, environment: { interfaces: new Map([["Fields", iface.resource.revision]]) } },
|
||||||
);
|
);
|
||||||
assert.ok(pkg.ok && pkg.resource.kind === "package");
|
assert.ok(pkg.ok && pkg.resource.kind === "package");
|
||||||
if (!pkg.ok || pkg.resource.kind !== "package") throw new Error("package failed");
|
if (!pkg.ok || pkg.resource.kind !== "package") throw new Error("package failed");
|
||||||
|
pkg.resource.revision.checkedQueries = [
|
||||||
|
await compileQuery(
|
||||||
|
pkg.resource.revision.queries![0]!,
|
||||||
|
[iface.resource.revision],
|
||||||
|
async () => "query Editor {root {title @live summary @live plain: title}}",
|
||||||
|
),
|
||||||
|
];
|
||||||
const schema = {
|
const schema = {
|
||||||
format: "quixos-bindings",
|
format: "quixos-bindings",
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -68,7 +77,14 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
|||||||
path.join(root, "consumer.ts"),
|
path.join(root, "consumer.ts"),
|
||||||
`import {useLiveField, tryConform, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
`import {useLiveField, tryConform, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
||||||
import {reactInterfaces} from "./react-props.gen.js";
|
import {reactInterfaces} from "./react-props.gen.js";
|
||||||
import type {ReactResults} from "./react-props.gen.js";
|
import type {ReactResults, QueryResults} from "./react-props.gen.js";
|
||||||
|
declare const query: QueryResults["Editor"];
|
||||||
|
useLiveField(query.root.title).set("new");
|
||||||
|
// @ts-expect-error live query fields retain exact setter types
|
||||||
|
useLiveField(query.root.title).set(123);
|
||||||
|
// @ts-expect-error readonly query fields do not acquire a setter
|
||||||
|
useLiveField(query.root.summary).set("no");
|
||||||
|
const plain: string = query.root.plain;
|
||||||
async function lookup() {
|
async function lookup() {
|
||||||
const view = await tryConform("object", reactInterfaces.Fields);
|
const view = await tryConform("object", reactInterfaces.Fields);
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
@@ -79,14 +95,14 @@ async function lookup() {
|
|||||||
await view.call["summary.set"]("no setter");
|
await view.call["summary.set"]("no setter");
|
||||||
}
|
}
|
||||||
declare const props: ReactResults["props"];
|
declare const props: ReactResults["props"];
|
||||||
const [title, setTitle] = useLiveField(props.fields.fields.title);
|
const title = useLiveField(props.fields.fields.title);
|
||||||
setTitle("new");
|
title.set("new");
|
||||||
// @ts-expect-error wrong setter value
|
// @ts-expect-error wrong setter value
|
||||||
setTitle(123);
|
title.set(123);
|
||||||
// @ts-expect-error read-only hook has no setter
|
// @ts-expect-error read-only hook has no setter
|
||||||
const [summary, setSummary] = useLiveField(props.fields.fields.summary);
|
useLiveField(props.fields.fields.summary).set("no");
|
||||||
const [manual, write] = useLiveField(props.fields.fields.summary, {write: async (value: string) => {}});
|
const manual = useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: string) => {}});
|
||||||
write("new");
|
manual.set("new");
|
||||||
const readonly: ReadableField<string> = props.fields.fields.title;
|
const readonly: ReadableField<string> = props.fields.fields.title;
|
||||||
// @ts-expect-error read-only does not satisfy writable
|
// @ts-expect-error read-only does not satisfy writable
|
||||||
const writable: WritableField<string> = props.fields.fields.summary;
|
const writable: WritableField<string> = props.fields.fields.summary;
|
||||||
@@ -94,7 +110,7 @@ declare const narrow: WritableField<"only">;
|
|||||||
// @ts-expect-error writable references are invariant
|
// @ts-expect-error writable references are invariant
|
||||||
const widened: WritableField<string> = narrow;
|
const widened: WritableField<string> = narrow;
|
||||||
// @ts-expect-error callbacks must accept the field's type
|
// @ts-expect-error callbacks must accept the field's type
|
||||||
useLiveField(props.fields.fields.summary, {write: async (value: number) => {}});
|
useLiveField(props.fields.fields.summary, {id: "manual", write: async (value: number) => {}});
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
const result = spawnSync(
|
const result = spawnSync(
|
||||||
|
|||||||
Reference in New Issue
Block a user