Implement query execution, scoped RPC enrichment, live collections, and scaffold integration
This commit is contained in:
@@ -13,6 +13,9 @@ export const genericImplementationType = (
|
||||
definition: GenericPackageExport,
|
||||
interfaces: InterfaceRevision[],
|
||||
concrete: (type: ValueType) => string,
|
||||
queryPort: (requirement: Extract<GenericDependencyPort["requirement"], { kind: "query" }>) => string = () => {
|
||||
throw new Error("Query binding schema required");
|
||||
},
|
||||
): string => {
|
||||
const names = new Map(definition.parameters.map((parameter, index) => [parameter.id, `T${index}`]));
|
||||
type Scope = { arguments: Map<string, string>; aliases: ValueAliasDefinition[] };
|
||||
@@ -108,6 +111,8 @@ export const genericImplementationType = (
|
||||
]);
|
||||
return object(methods);
|
||||
}
|
||||
case "query":
|
||||
return queryPort(requirement);
|
||||
case "constructor":
|
||||
return object([
|
||||
[
|
||||
|
||||
+43
-3
@@ -144,6 +144,33 @@ export const generateTypeScriptBindings = (
|
||||
const port = (entry: DependencyPort): { type: string; spec: unknown } => {
|
||||
const requirement = entry.requirement;
|
||||
switch (requirement.kind) {
|
||||
case "query": {
|
||||
const query = schema.packages
|
||||
.find((candidate) => candidate.revisionId === requirement.packageRevisionId)
|
||||
?.checkedQueries?.find((query) => query.declaration.id === requirement.queryId);
|
||||
if (!query) throw new Error(`Missing checked query ${requirement.packageRevisionId}:${requirement.queryId}`);
|
||||
return {
|
||||
type: object([
|
||||
["execute", `(variables: ${type(query.variables)}) => Promise<${type(query.output)}>`],
|
||||
...(query.declaration.watch
|
||||
? ([
|
||||
[
|
||||
"watch",
|
||||
`(variables: ${type(query.variables)}, signal: AbortSignal) => AsyncIterable<QxQuerySnapshot<${type(query.output)}>>`,
|
||||
],
|
||||
] as [string, string][])
|
||||
: []),
|
||||
]),
|
||||
spec: {
|
||||
kind: "query",
|
||||
id: entry.id,
|
||||
definitionDigest: query.definitionDigest,
|
||||
variables: query.variables,
|
||||
output: query.output,
|
||||
watch: query.declaration.watch,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "state": {
|
||||
const methods = requirement.primitives.map((primitive): [string, string] => {
|
||||
if (primitive === "read") return ["get", `() => Promise<${type(requirement.valueType)}>`];
|
||||
@@ -282,7 +309,12 @@ export const generateTypeScriptBindings = (
|
||||
for (const definition of pkg.genericExports ?? []) {
|
||||
handlers.push([
|
||||
definition.displayName,
|
||||
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
||||
genericImplementationType(
|
||||
definition,
|
||||
[...schema.interfaces, ...(schema.interfaceTemplates ?? [])],
|
||||
type,
|
||||
(requirement) => port({ id: capabilityId.dependencyPort("query"), displayName: "query", requirement }).type,
|
||||
),
|
||||
]);
|
||||
}
|
||||
// Unused imported interfaces must not introduce new message-codec obligations.
|
||||
@@ -326,7 +358,13 @@ export const generateTypeScriptBindings = (
|
||||
const queryResults = object(
|
||||
(pkg.checkedQueries ?? []).map((query) => [query.declaration.displayName, type(query.output)]),
|
||||
);
|
||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${contracts.map((entry) => entry.type).join(" ")}`;
|
||||
const queryDescriptors = object(
|
||||
(pkg.checkedQueries ?? []).map((query) => [
|
||||
query.declaration.displayName,
|
||||
`QxQueryDescriptor<QueryVariables[${q(query.declaration.displayName)}], QueryResults[${q(query.declaration.displayName)}], ${q(query.declaration.root)}>`,
|
||||
]),
|
||||
);
|
||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${queryVariables} ${queryResults} ${queryDescriptors} ${contracts.map((entry) => entry.type).join(" ")}`;
|
||||
const typeImports = [
|
||||
"BindingValue",
|
||||
"QxObjectRef",
|
||||
@@ -339,6 +377,8 @@ export const generateTypeScriptBindings = (
|
||||
"QxLiveValue",
|
||||
"RelationshipCollection",
|
||||
"RelationshipEntry",
|
||||
"QxQuerySnapshot",
|
||||
"QxQueryDescriptor",
|
||||
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
|
||||
return (
|
||||
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
|
||||
@@ -348,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 = ${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, rootInterfaceRevisionId: 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)
|
||||
|
||||
@@ -20,6 +20,15 @@ declare module "@quixos/web-studio-react-runtime" {
|
||||
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
||||
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
||||
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 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 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> = {
|
||||
onAction?: (action: Action) => void;
|
||||
fallback?: React.ReactNode;
|
||||
|
||||
+32
-1
@@ -116,8 +116,39 @@ export function generateReactBindings(
|
||||
);
|
||||
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
||||
});
|
||||
const queryType = (value: ValueType): string => {
|
||||
if (value.kind === "object-ref")
|
||||
return `QueryReference<${q(value.expectation.kind === "atom" ? value.expectation.atomId : value.expectation.interfaceRevisionId)}>`;
|
||||
if (value.kind === "optional") return `(${queryType(value.value)} | null)`;
|
||||
if (value.kind === "list") return `Array<${queryType(value.value)}>`;
|
||||
if (value.kind === "record")
|
||||
return `{${Object.entries(value.fields)
|
||||
.map(([name, field]) => `${q(name)}: ${queryType(field)}`)
|
||||
.join(";")}}`;
|
||||
return type(value);
|
||||
};
|
||||
const queries = pkg.checkedQueries ?? [];
|
||||
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 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) => [
|
||||
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,
|
||||
},
|
||||
]),
|
||||
),
|
||||
)};\n`;
|
||||
return (
|
||||
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||||
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField, QueryDescriptor, QueryReference} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||||
queryCode +
|
||||
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
|
||||
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
|
||||
`export const reactInterfaces = {${contracts
|
||||
|
||||
Reference in New Issue
Block a user