Add checked workspace-interface query roots
Implement scoped interface extents across QX, generated bindings, SDK, Orch, Camino and browser queries. Preserve native projection, aggregation, pagination and watch invalidation without synthetic persisted objects. Validate exact query grants and scope freshness on execution and replay. Add local PostgreSQL, transport, browser and code-generation coverage. C1 implementation slice only: full-text indexes and Studio search adoption (C2/C3) remain tracked in the implementation handoff. No publication or deployment.
This commit is contained in:
@@ -388,7 +388,7 @@ export const generateTypeScriptBindings = (
|
||||
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
|
||||
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
|
||||
`export type QueryVariables = ${queryVariables};\nexport type QueryResults = ${queryResults};\n` +
|
||||
`export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, rootInterfaceRevisionId: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
|
||||
`export const queries: ${queryDescriptors} = ${JSON.stringify(Object.fromEntries((pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, { id: `${pkg.revisionId}:${query.declaration.id}`, definitionDigest: query.definitionDigest, root: query.declaration.root, variables: query.variables, output: query.output, watch: query.declaration.watch }])), null, 2)} as const;\n` +
|
||||
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
|
||||
`export const interfaces = {${contracts
|
||||
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
|
||||
|
||||
@@ -30,11 +30,14 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
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 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 interface QueryDescriptor<Variables, Result, Kind extends "object" | "workspace-interface" = "object" | "workspace-interface"> {readonly id: string; readonly definitionDigest: string; readonly root: {readonly kind: Kind; readonly interface: 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} | ReadableField<unknown> ? 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};
|
||||
export type WorkspaceQueryScope = {readonly $workspaceQueryScope: true};
|
||||
export function getWorkspaceQueryScope<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">): Promise<WorkspaceQueryScope>;
|
||||
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "object">, options: {root: string | {readonly $quixosRef: string}; variables: Variables}): QueryState<Result> & {refresh(): void};
|
||||
export function useQuery<Variables, Result>(descriptor: QueryDescriptor<Variables, Result, "workspace-interface">, options: {scope: WorkspaceQueryScope; variables: Variables}): QueryState<Result> & {refresh(): void};
|
||||
export interface QueryConnection<Row> {entries: {key: string; node: Row; cursor?: string | null}[]; pageInfo: {hasNextPage: boolean; endCursor?: string | null}}
|
||||
export function CollectionView<Result, Row>(props: {result: QueryState<Result> & {refresh(): void}; connection(data: QueryPartial<Result>): QueryPartial<QueryConnection<Row>> | undefined; renderItem(row: QueryPartial<Row>, entryKey: string): React.ReactNode; onCreate?: () => void; onLoadMore?: (cursor: string | null) => void; onReset?: () => void; loadingMore?: boolean; empty?: React.ReactNode; prefetch?: boolean}): React.ReactElement;
|
||||
export type ReactComponentHostProps<Action> = {
|
||||
|
||||
@@ -148,14 +148,14 @@ export function generateReactBindings(
|
||||
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)}: ${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)}], ${q(query.declaration.root.kind)}>`).join(";")}} = ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
queries.map((query) => [
|
||||
query.declaration.displayName,
|
||||
{
|
||||
id: `${pkg.revisionId}:${query.declaration.id}`,
|
||||
definitionDigest: query.definitionDigest,
|
||||
rootInterfaceRevisionId: query.declaration.root,
|
||||
root: query.declaration.root,
|
||||
variables: query.variables,
|
||||
output: query.output,
|
||||
liveFields: queryPresentation(query, schema.interfaces),
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1244,7 +1244,7 @@ const lowerPackage = (
|
||||
const declaration: QueryDeclaration = {
|
||||
id: stringValue(query.stringLiteral(0)),
|
||||
displayName: identifier(query.identifier()),
|
||||
root: close(query.interfaceType()),
|
||||
root: { kind: query.WORKSPACE() ? "workspace-interface" : "object", interface: close(query.interfaceType()) },
|
||||
document: stringValue(query.stringLiteral(1)),
|
||||
operation: stringValue(query.stringLiteral(2)),
|
||||
fragments: [],
|
||||
@@ -1372,7 +1372,7 @@ const lowerPackage = (
|
||||
queryTemplates.push({
|
||||
declaration: common,
|
||||
parameters,
|
||||
root: expressions.get(root)!,
|
||||
root: { kind: root.kind, interface: expressions.get(root.interface)! },
|
||||
views: templateViews,
|
||||
allowances: templateAllowances,
|
||||
});
|
||||
@@ -1579,7 +1579,17 @@ const lowerBoundDependencies = (
|
||||
const query = lowerQueryReference(state, identifier(entry.identifier(1)), identifier(entry.identifier(2)), entry);
|
||||
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
|
||||
return query && (!entry.VIA() || via)
|
||||
? [{ portId, binding: { kind: "query", ...query, ...(via ? { via } : {}) } }]
|
||||
? [
|
||||
{
|
||||
portId,
|
||||
binding: {
|
||||
kind: "query",
|
||||
...query,
|
||||
scope: entry.WORKSPACE() ? "workspace" : "object",
|
||||
...(via ? { via } : {}),
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
if (entry.STATE()) {
|
||||
|
||||
@@ -292,7 +292,13 @@ export interface PackageRevision {
|
||||
}
|
||||
|
||||
export type DependencyBinding =
|
||||
| { kind: "query"; packageRevisionId: PackageRevisionId; queryId: string; via?: EdgeTraversal }
|
||||
| {
|
||||
kind: "query";
|
||||
packageRevisionId: PackageRevisionId;
|
||||
queryId: string;
|
||||
scope: "object" | "workspace";
|
||||
via?: EdgeTraversal;
|
||||
}
|
||||
| { kind: "state"; slotId: SlotId; via?: EdgeTraversal }
|
||||
| {
|
||||
kind: "edge";
|
||||
|
||||
@@ -1173,6 +1173,20 @@ const validateBoundDependencies = (
|
||||
issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export");
|
||||
break;
|
||||
}
|
||||
if (query.root.kind === "workspace-interface") {
|
||||
if (selected.scope !== "workspace" || selected.via)
|
||||
issue(
|
||||
issues,
|
||||
"invalid-dependency-binding",
|
||||
path,
|
||||
"Workspace-interface queries require an explicit workspace scope without an object traversal",
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (selected.scope !== "object") {
|
||||
issue(issues, "invalid-dependency-binding", path, "Object-root queries require an object dependency binding");
|
||||
break;
|
||||
}
|
||||
const traversal = selected.via
|
||||
? validateTraversal(
|
||||
issues,
|
||||
@@ -1191,8 +1205,13 @@ const validateBoundDependencies = (
|
||||
: []
|
||||
: [params.atomId];
|
||||
for (const atom of targets)
|
||||
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root)))
|
||||
issue(issues, "unsatisfied-interface", path, `Query root ${atom} does not conform to ${query.root}`);
|
||||
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root.interface)))
|
||||
issue(
|
||||
issues,
|
||||
"unsatisfied-interface",
|
||||
path,
|
||||
`Query root ${atom} does not conform to ${query.root.interface}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "state": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file quixos/refs.proto.
|
||||
*/
|
||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3Mi0QEKDEZpZWxkRWRpdGluZxIbChNnZXR0ZXJfb3BlcmF0aW9uX2lkGAEgASgJEhsKE3NldHRlcl9vcGVyYXRpb25faWQYAiABKAkSFQoNZG9jdW1lbnRfdHlwZRgDIAEoCRIWCg5iaW5kaW5nX2RpZ2VzdBgEIAEoCRInCgRtb2RlGAUgASgOMhkucXVpeG9zLkZpZWxkRWRpdGluZy5Nb2RlIi8KBE1vZGUSDwoLVU5TUEVDSUZJRUQQABIMCghSRUdJU1RFUhABEggKBENSRFQQAiKWAQoSQ29uZm9ybWFuY2VXaXRuZXNzEhEKCW9iamVjdF9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSFgoOY29uZm9ybWFuY2VfaWQYAyABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAQgASgJEhcKD3dvcmtzcGFjZV9lcG9jaBgFIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJItgBChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABISCghxdWVyeV9pZBgHIAEoCUgAEhEKCW9iamVjdF9pZBgGIAEoCUIJCgdiaW5kaW5nIj0KDkVkZ2VEZXBlbmRlbmN5EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIVCg1wcm9qZWN0aW9uX2lkGAIgASgJYgZwcm90bzM");
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3Mi0QEKDEZpZWxkRWRpdGluZxIbChNnZXR0ZXJfb3BlcmF0aW9uX2lkGAEgASgJEhsKE3NldHRlcl9vcGVyYXRpb25faWQYAiABKAkSFQoNZG9jdW1lbnRfdHlwZRgDIAEoCRIWCg5iaW5kaW5nX2RpZ2VzdBgEIAEoCRInCgRtb2RlGAUgASgOMhkucXVpeG9zLkZpZWxkRWRpdGluZy5Nb2RlIi8KBE1vZGUSDwoLVU5TUEVDSUZJRUQQABIMCghSRUdJU1RFUhABEggKBENSRFQQAiKWAQoSQ29uZm9ybWFuY2VXaXRuZXNzEhEKCW9iamVjdF9pZBgBIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAkSFgoOY29uZm9ybWFuY2VfaWQYAyABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAQgASgJEhcKD3dvcmtzcGFjZV9lcG9jaBgFIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJIpUCChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABISCghxdWVyeV9pZBgHIAEoCUgAEjsKD3dvcmtzcGFjZV9xdWVyeRgIIAEoCzIgLnF1aXhvcy5Xb3Jrc3BhY2VRdWVyeURlcGVuZGVuY3lIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyKaAQoTV29ya3NwYWNlUXVlcnlTY29wZRIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEhcKD3dvcmtzcGFjZV9lcG9jaBgDIAEoCRIdChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAkSFgoOYmluZGluZ19kaWdlc3QYBSABKAkiWAoYV29ya3NwYWNlUXVlcnlEZXBlbmRlbmN5EhAKCHF1ZXJ5X2lkGAEgASgJEioKBXNjb3BlGAIgASgLMhsucXVpeG9zLldvcmtzcGFjZVF1ZXJ5U2NvcGUiPQoORWRnZURlcGVuZGVuY3kSFAoMZWRnZV90eXBlX2lkGAEgASgJEhUKDXByb2plY3Rpb25faWQYAiABKAliBnByb3RvMw");
|
||||
|
||||
/**
|
||||
* @generated from message quixos.CapabilityRef
|
||||
@@ -207,6 +207,12 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
||||
*/
|
||||
value: string;
|
||||
case: "queryId";
|
||||
} | {
|
||||
/**
|
||||
* @generated from field: quixos.WorkspaceQueryDependency workspace_query = 8;
|
||||
*/
|
||||
value: WorkspaceQueryDependency;
|
||||
case: "workspaceQuery";
|
||||
} | { case: undefined; value?: undefined };
|
||||
|
||||
/**
|
||||
@@ -225,6 +231,68 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
||||
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 4);
|
||||
|
||||
/**
|
||||
* Scope evidence is not authority by itself. The caller must also hold the
|
||||
* matching workspace-query grant (or the authenticated workspace user grant).
|
||||
*
|
||||
* @generated from message quixos.WorkspaceQueryScope
|
||||
*/
|
||||
export type WorkspaceQueryScope = Message<"quixos.WorkspaceQueryScope"> & {
|
||||
/**
|
||||
* @generated from field: string workspace_id = 1;
|
||||
*/
|
||||
workspaceId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_revision_id = 2;
|
||||
*/
|
||||
workspaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_epoch = 3;
|
||||
*/
|
||||
workspaceEpoch: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string interface_revision_id = 4;
|
||||
*/
|
||||
interfaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string binding_digest = 5;
|
||||
*/
|
||||
bindingDigest: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.WorkspaceQueryScope.
|
||||
* Use `create(WorkspaceQueryScopeSchema)` to create a new message.
|
||||
*/
|
||||
export const WorkspaceQueryScopeSchema: GenMessage<WorkspaceQueryScope> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 5);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.WorkspaceQueryDependency
|
||||
*/
|
||||
export type WorkspaceQueryDependency = Message<"quixos.WorkspaceQueryDependency"> & {
|
||||
/**
|
||||
* @generated from field: string query_id = 1;
|
||||
*/
|
||||
queryId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: quixos.WorkspaceQueryScope scope = 2;
|
||||
*/
|
||||
scope?: WorkspaceQueryScope | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.WorkspaceQueryDependency.
|
||||
* Use `create(WorkspaceQueryDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const WorkspaceQueryDependencySchema: GenMessage<WorkspaceQueryDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 6);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.EdgeDependency
|
||||
*/
|
||||
@@ -245,5 +313,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, 5);
|
||||
messageDesc(file_quixos_refs, 7);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "../capability-model/types.js";
|
||||
import { querySchema } from "./schema.js";
|
||||
import { objectRow } from "./relational-schema.js";
|
||||
import { workspaceQueryRootId } from "./root.js";
|
||||
import { relationalCompiler } from "./relational-compile.js";
|
||||
import {
|
||||
QueryCompileError,
|
||||
@@ -120,6 +121,9 @@ export async function compileQuery(
|
||||
.map((entry) => [entry.name.value, entry]),
|
||||
);
|
||||
const mark = (id: InterfaceRevisionId, name: string, use: QueryUse, node: ASTNode) => {
|
||||
// The virtual extent has no provider/getter effect. Its membership is
|
||||
// linked separately, including when the query only selects references.
|
||||
if (id === workspaceQueryRootId) return;
|
||||
const contract = generated.contracts.get(id)!;
|
||||
const member = contract.members.find((entry) => entry.displayName === name);
|
||||
if (!member || member.kind === "operation" || !member.queryRead)
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface LinkedQuery {
|
||||
bindingDigest: string;
|
||||
fields: LinkedQueryField[];
|
||||
attachments: PersistentAttachment[];
|
||||
rootAtoms: AtomId[];
|
||||
}
|
||||
|
||||
/** Link every possible implementation, including atoms absent from today's data. */
|
||||
@@ -128,10 +129,17 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
||||
}
|
||||
}
|
||||
const storage = attachments.filter((entry) => needed.has(entry.id));
|
||||
const rootConformances = workspace.conformances.filter(
|
||||
(entry) => entry.interfaceRevisionId === declaration.root.interface,
|
||||
);
|
||||
const rootAtoms = [...new Set(rootConformances.map((entry) => entry.atomId))].sort();
|
||||
const bindingDigest = createHash("sha256")
|
||||
.update(
|
||||
canonicalJson({
|
||||
definition: checked.definitionDigest,
|
||||
rootConformances: rootConformances
|
||||
.map((entry) => ({ atomId: entry.atomId, id: entry.id, interfaceRevisionId: entry.interfaceRevisionId }))
|
||||
.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
||||
fields: [...fields].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
|
||||
storage: [...storage].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
}),
|
||||
@@ -144,6 +152,7 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
||||
bindingDigest,
|
||||
fields,
|
||||
attachments: storage,
|
||||
rootAtoms,
|
||||
});
|
||||
}
|
||||
for (const entry of linked) entry.runtime = queryRuntimePlan(entry, workspace);
|
||||
|
||||
+26
-1
@@ -48,7 +48,32 @@ export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevisi
|
||||
id: linked.id,
|
||||
definitionDigest: linked.checked.definitionDigest,
|
||||
bindingDigest: linked.bindingDigest,
|
||||
rootInterfaceRevisionId: linked.checked.declaration.root,
|
||||
root: {
|
||||
case: linked.checked.declaration.root.kind === "object" ? "objectInterface" : "workspaceInterface",
|
||||
value: linked.checked.declaration.root.interface,
|
||||
},
|
||||
rootAtomIds: linked.rootAtoms,
|
||||
fields: linked.checked.effects.map((effect) => {
|
||||
const member = workspace.interfaceImports
|
||||
.find((entry) => entry.revisionId === effect.interfaceRevisionId)!
|
||||
.members.find((entry) => entry.id === effect.memberId)!;
|
||||
return {
|
||||
interfaceRevisionId: effect.interfaceRevisionId,
|
||||
memberId: effect.memberId,
|
||||
fieldName: member.displayName,
|
||||
valueTypeJson: member.kind === "value" ? JSON.stringify(member.valueType) : "",
|
||||
keyType: member.kind === "relationship" ? member.keyType : undefined,
|
||||
cardinality:
|
||||
member.kind === "relationship"
|
||||
? {
|
||||
"optional-one": Cardinality.OPTIONAL_ONE,
|
||||
"exactly-one": Cardinality.EXACTLY_ONE,
|
||||
many: Cardinality.MANY,
|
||||
"many-unique": Cardinality.MANY_UNIQUE,
|
||||
}[member.cardinality]
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
selection: linked.checked.selection.map(selection),
|
||||
budgets: linked.checked.declaration.budgets,
|
||||
variablesTypeJson: JSON.stringify(linked.checked.variables),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
capabilityId,
|
||||
type InterfaceRevision,
|
||||
type InterfaceRevisionId,
|
||||
type MemberId,
|
||||
} from "../capability-model/types.js";
|
||||
import { QueryCompileError, type QueryRoot } from "./types.js";
|
||||
|
||||
/** Compiler-only row-source schema; never installed as an atom, edge or capability. */
|
||||
export const workspaceQueryRootId: InterfaceRevisionId = capabilityId.interfaceRevision(
|
||||
"qx-internal-workspace-query-root",
|
||||
);
|
||||
export const workspaceObjectsMemberId: MemberId = capabilityId.member("qx-internal-workspace-objects");
|
||||
export function queryRootSchema(root: QueryRoot, interfaces: readonly InterfaceRevision[]) {
|
||||
if (interfaces.some((i) => i.revisionId === workspaceQueryRootId))
|
||||
throw new QueryCompileError("QUERY_RESERVED_ID", "The workspace query root identity is compiler-owned");
|
||||
if (root.kind === "object") return { interface: root.interface, interfaces };
|
||||
const contract = interfaces.find((i) => i.revisionId === root.interface);
|
||||
if (!contract || contract.template)
|
||||
throw new QueryCompileError("QUERY_CONTRACT", `Expected closed interface ${root.interface}`);
|
||||
const synthetic: InterfaceRevision = {
|
||||
interfaceId: capabilityId.interface("qx-internal-workspace-query-root"),
|
||||
revisionId: workspaceQueryRootId,
|
||||
displayName: "WorkspaceQueryRoot",
|
||||
source: contract.source,
|
||||
members: [
|
||||
{
|
||||
kind: "relationship",
|
||||
id: workspaceObjectsMemberId,
|
||||
displayName: "objects",
|
||||
target: { kind: "interface", interfaceRevisionId: root.interface },
|
||||
cardinality: "many-unique",
|
||||
ordered: false,
|
||||
operations: [],
|
||||
queryRead: { execution: "native" },
|
||||
},
|
||||
],
|
||||
};
|
||||
return { interface: synthetic.revisionId, interfaces: [...interfaces, synthetic] };
|
||||
}
|
||||
+6
-4
@@ -28,6 +28,7 @@ import type {
|
||||
} from "../capability-model/types.js";
|
||||
import { QueryCompileError, type QueryDeclaration } from "./types.js";
|
||||
import { relationalSchema, objectRow } from "./relational-schema.js";
|
||||
import { queryRootSchema, workspaceQueryRootId } from "./root.js";
|
||||
|
||||
// Validate losslessly; callers carry decimal strings across JSON transports.
|
||||
function integerParser(name: string, min: bigint, max: bigint, value: unknown): string {
|
||||
@@ -78,7 +79,8 @@ export function querySchema(
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
document?: DocumentNode,
|
||||
) {
|
||||
const contracts = new Map(interfaces.map((entry) => [entry.revisionId, entry]));
|
||||
const root = queryRootSchema(declaration.root, interfaces);
|
||||
const contracts = new Map(root.interfaces.map((entry) => [entry.revisionId, entry]));
|
||||
const objects = new Map<InterfaceRevisionId, GraphQLObjectType>();
|
||||
const orders = new Map<InterfaceRevisionId, GraphQLInputObjectType>();
|
||||
const byName = new Map<string, InterfaceRevision>();
|
||||
@@ -150,7 +152,7 @@ export function querySchema(
|
||||
{ contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo },
|
||||
document,
|
||||
);
|
||||
relational.discover(declaration.root);
|
||||
relational.discover(root.interface);
|
||||
const filter = (id: InterfaceRevisionId) => relational.where(objectRow(id));
|
||||
const order = (id: InterfaceRevisionId) => {
|
||||
let result = orders.get(id);
|
||||
@@ -177,7 +179,7 @@ export function querySchema(
|
||||
const metadata = new GraphQLObjectType({
|
||||
name: `QxMetadata_${name(id)}`,
|
||||
fields: {
|
||||
ref: { type: new GraphQLNonNull(relational.refs(id)) },
|
||||
...(id === workspaceQueryRootId ? {} : { ref: { type: new GraphQLNonNull(relational.refs(id)) } }),
|
||||
...(helpers ? { relations: { type: new GraphQLNonNull(helpers) } } : {}),
|
||||
},
|
||||
});
|
||||
@@ -247,7 +249,7 @@ export function querySchema(
|
||||
directives: [...specifiedDirectives, new GraphQLDirective({ name: "live", locations: [DirectiveLocation.FIELD] })],
|
||||
query: new GraphQLObjectType({
|
||||
name: "QxQuery",
|
||||
fields: { root: { type: new GraphQLNonNull(object(declaration.root)) } },
|
||||
fields: { root: { type: new GraphQLNonNull(object(root.interface)) } },
|
||||
}),
|
||||
});
|
||||
return { schema, byName, target, contracts, relational, scalarTypes };
|
||||
|
||||
@@ -34,7 +34,7 @@ export function specializeQueryTemplate(
|
||||
return {
|
||||
...structuredClone(template.declaration),
|
||||
...identity,
|
||||
root: substitution.application(template.root),
|
||||
root: { kind: template.root.kind, interface: substitution.application(template.root.interface) },
|
||||
views: template.views
|
||||
.map((view) => {
|
||||
const target = substitution.object(view.target);
|
||||
|
||||
+5
-2
@@ -31,10 +31,13 @@ export const defaultQueryBudgets: Readonly<QueryBudgets> = Object.freeze({
|
||||
concurrency: 8,
|
||||
deadlineMs: 10000,
|
||||
});
|
||||
export type QueryRoot<Interface = InterfaceRevisionId> =
|
||||
| { kind: "object"; interface: Interface }
|
||||
| { kind: "workspace-interface"; interface: Interface };
|
||||
export interface QueryDeclaration {
|
||||
id: string;
|
||||
displayName: string;
|
||||
root: InterfaceRevisionId;
|
||||
root: QueryRoot;
|
||||
document: string;
|
||||
operation: string;
|
||||
fragments: string[];
|
||||
@@ -53,7 +56,7 @@ export interface QueryDeclaration {
|
||||
export interface QueryTemplate {
|
||||
declaration: Omit<QueryDeclaration, "root" | "views" | "allowances">;
|
||||
parameters: TypeParameter[];
|
||||
root: InterfaceApplicationExpression;
|
||||
root: QueryRoot<InterfaceApplicationExpression>;
|
||||
views: { target: ObjectTypeExpression; interface: InterfaceApplicationExpression }[];
|
||||
allowances: { interface: InterfaceApplicationExpression; memberName: string; uses: QueryUse[]; reason: string }[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user