Add explicit CRDT edit contracts and structured query values
This commit is contained in:
@@ -154,8 +154,19 @@ export function relationalCompiler(
|
||||
): QueryExpression => {
|
||||
requireAvailable(names, available, node);
|
||||
const meta = names.at(-2) === "_qx";
|
||||
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : -1), uses, node);
|
||||
const name = names.at(-1)!;
|
||||
let fieldIndex = names.length - 1;
|
||||
if (!meta) {
|
||||
let cursor = root;
|
||||
for (let index = 0; index < names.length; index++) {
|
||||
if (cursor.kind === "object" && rowFields(cursor).get(names[index]!)?.kind === "leaf") {
|
||||
fieldIndex = index;
|
||||
break;
|
||||
}
|
||||
if (index < names.length - 1) cursor = walk(root, names.slice(0, index + 1), uses, node).row;
|
||||
}
|
||||
}
|
||||
const { row, path, nullable } = walk(root, names.slice(0, meta ? -2 : fieldIndex), uses, node);
|
||||
const name = names[meta ? names.length - 1 : fieldIndex]!;
|
||||
let type: ValueType, leaf: QueryExpression["leaf"];
|
||||
if (meta) {
|
||||
if (name === "ref" && row.kind === "object") {
|
||||
@@ -185,7 +196,24 @@ export function relationalCompiler(
|
||||
for (const use of uses) mark(row.interfaceRevisionId, name, use, node);
|
||||
const member = env.contracts.get(row.interfaceRevisionId)!.members.find((m) => m.displayName === name)!;
|
||||
type = field.type;
|
||||
leaf = { kind: "field", interfaceRevisionId: row.interfaceRevisionId, memberId: member.id };
|
||||
const valuePath = names.slice(fieldIndex + 1);
|
||||
let valueNullable = false;
|
||||
for (const component of valuePath) {
|
||||
if (type.kind === "optional") {
|
||||
valueNullable = true;
|
||||
type = type.value;
|
||||
}
|
||||
if (type.kind !== "record" || !Object.hasOwn(type.fields, component))
|
||||
return fail("QUERY_FIELD_NOT_QUERYABLE", `Unknown scalar value path ${names.join(".")}`, node);
|
||||
type = type.fields[component]!;
|
||||
}
|
||||
if (valueNullable && type.kind !== "optional") type = valueType.optional(type);
|
||||
leaf = {
|
||||
kind: "field",
|
||||
interfaceRevisionId: row.interfaceRevisionId,
|
||||
memberId: member.id,
|
||||
...(valuePath.length ? { valuePath } : {}),
|
||||
};
|
||||
}
|
||||
return { path, leaf, type: nullable && type.kind !== "optional" ? valueType.optional(type) : type };
|
||||
};
|
||||
@@ -356,15 +384,22 @@ export function relationalCompiler(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const expr = expression(row, [...base, name], ["predicate"], field, available);
|
||||
children.push(
|
||||
...(comparisons(field.value, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: expr,
|
||||
operator,
|
||||
value,
|
||||
})) as QueryPredicate[]),
|
||||
);
|
||||
const valuePredicate = (names: string[], value: ValueNode) => {
|
||||
const expr = expression(row, names, ["predicate"], field, available);
|
||||
const type = expr.type.kind === "optional" ? expr.type.value : expr.type;
|
||||
if (type.kind === "record") {
|
||||
for (const child of pairs(value)) valuePredicate([...names, child.name.value], child.value);
|
||||
} else
|
||||
children.push(
|
||||
...(comparisons(value, (operator, value) => ({
|
||||
kind: "compare",
|
||||
expression: expr,
|
||||
operator,
|
||||
value,
|
||||
})) as QueryPredicate[]),
|
||||
);
|
||||
};
|
||||
valuePredicate([...base, name], field.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { comparableValue } from "./structured.js";
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
@@ -45,6 +46,7 @@ export interface RelationalSchemaEnvironment {
|
||||
name(id: InterfaceRevisionId): string;
|
||||
scalar(type: ValueType): GraphQLInputType & GraphQLOutputType;
|
||||
comparison(type: ValueType): GraphQLInputObjectType;
|
||||
valueOrder(type: ValueType): GraphQLInputType;
|
||||
scalarTypes: Map<string, ValueType>;
|
||||
direction: GraphQLEnumType;
|
||||
cursor: GraphQLScalarType;
|
||||
@@ -255,7 +257,8 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result[name] = { type: ordering ? env.direction : compare(type) };
|
||||
if (!comparableValue(type, ordering)) continue;
|
||||
result[name] = { type: ordering ? env.valueOrder(type) : compare(type) };
|
||||
}
|
||||
}
|
||||
const meta: GraphQLInputFieldConfigMap = {};
|
||||
@@ -300,6 +303,7 @@ export function relationalSchema(env: RelationalSchemaEnvironment, document?: Do
|
||||
for (const [name, field] of fields(row)) {
|
||||
if (row.kind === "object" && field.kind === "row") continue;
|
||||
if (name in result) throw new QueryCompileError("QUERY_SCHEMA_NAME", `Reserved predicate name ${name}`);
|
||||
if (field.kind === "leaf" && !comparableValue(field.type)) continue;
|
||||
result[name] = { type: field.kind === "row" ? where(field.row) : compare(field.type) };
|
||||
}
|
||||
result._qx = {
|
||||
|
||||
@@ -49,7 +49,7 @@ export type QueryPathStep =
|
||||
export interface QueryExpression {
|
||||
path: QueryPathStep[];
|
||||
leaf:
|
||||
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId }
|
||||
| { kind: "field"; interfaceRevisionId: InterfaceRevisionId; memberId: MemberId; valuePath?: string[] }
|
||||
| { kind: "ref"; interfaceRevisionId: InterfaceRevisionId }
|
||||
| { kind: "entry" | "mapKey" };
|
||||
type: ValueType;
|
||||
|
||||
+18
-7
@@ -1,3 +1,4 @@
|
||||
import { structuredQueryTypes, comparableValue } from "./structured.js";
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
@@ -132,7 +133,7 @@ export function querySchema(
|
||||
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
|
||||
return queryScalars[type.name];
|
||||
};
|
||||
const comparison = (type: ValueType) => {
|
||||
const scalarComparison = (type: ValueType) => {
|
||||
const base = type.kind === "optional" ? type.value : type;
|
||||
const value = scalar(base);
|
||||
const key = String(value);
|
||||
@@ -149,8 +150,21 @@ export function querySchema(
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const structured = structuredQueryTypes(scalar, scalarComparison, direction);
|
||||
const comparison = (type: ValueType) => structured.input(type) as GraphQLInputObjectType;
|
||||
const relational = relationalSchema(
|
||||
{ contracts, target, name, scalar, comparison, scalarTypes, direction, cursor: cursorScalar, pageInfo },
|
||||
{
|
||||
contracts,
|
||||
target,
|
||||
name,
|
||||
scalar,
|
||||
comparison,
|
||||
valueOrder: (type) => structured.input(type, true),
|
||||
scalarTypes,
|
||||
direction,
|
||||
cursor: cursorScalar,
|
||||
pageInfo,
|
||||
},
|
||||
document,
|
||||
);
|
||||
relational.discover(root.interface);
|
||||
@@ -162,7 +176,7 @@ export function querySchema(
|
||||
for (const member of contract(id).members) {
|
||||
if (member.kind !== "value" || !member.queryRead) continue;
|
||||
const t = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
|
||||
if (t.kind === "scalar" && t.name !== "bytes") fields[member.displayName] = { type: direction };
|
||||
if (comparableValue(t, true)) fields[member.displayName] = { type: structured.input(t, true) };
|
||||
}
|
||||
if (Object.keys(fields).length === 0) return undefined;
|
||||
result = new GraphQLInputObjectType({ name: `${name(id)}Order`, fields });
|
||||
@@ -190,10 +204,7 @@ export function querySchema(
|
||||
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(member.displayName) || member.displayName.startsWith("_"))
|
||||
throw new QueryCompileError("QUERY_SCHEMA_NAME", `Unsupported query field name ${member.displayName}`);
|
||||
if (member.kind === "value") {
|
||||
const type = scalar(member.valueType);
|
||||
fields[member.displayName] = {
|
||||
type: member.valueType.kind === "optional" ? type : new GraphQLNonNull(type),
|
||||
};
|
||||
fields[member.displayName] = { type: structured.output(member.valueType) };
|
||||
} else {
|
||||
const targetId = target(member),
|
||||
node = object(targetId);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
type GraphQLNullableType,
|
||||
type GraphQLOutputType,
|
||||
type GraphQLInputType,
|
||||
type GraphQLEnumType,
|
||||
} from "graphql";
|
||||
import type { ValueType } from "../capability-model/types.js";
|
||||
import { QueryCompileError } from "./types.js";
|
||||
|
||||
export function comparableValue(type: ValueType, ordering = false): boolean {
|
||||
if (type.kind === "optional") return comparableValue(type.value, ordering);
|
||||
if (type.kind === "record") return Object.values(type.fields).some((t) => comparableValue(t, ordering));
|
||||
return type.kind === "scalar" && (!ordering || type.name !== "bytes");
|
||||
}
|
||||
|
||||
/** Structured values retain one field identity; their children are value projections. */
|
||||
export function structuredQueryTypes(
|
||||
scalar: (type: ValueType) => GraphQLInputType & GraphQLOutputType,
|
||||
scalarComparison: (type: ValueType) => GraphQLInputObjectType,
|
||||
direction: GraphQLEnumType,
|
||||
) {
|
||||
const objects = new Map<string, GraphQLObjectType>();
|
||||
const inputs = new Map<string, GraphQLInputObjectType>();
|
||||
const name = (role: string, type: ValueType) =>
|
||||
`QxValue${role}_${createHash("sha256").update(JSON.stringify(type)).digest("hex").slice(0, 16)}`;
|
||||
const output = (type: ValueType, depth = 0): GraphQLOutputType => {
|
||||
if (depth > 128) throw new QueryCompileError("QUERY_TYPE", "Structured value exceeds type depth limit");
|
||||
if (type.kind === "optional") return required(type.value, depth + 1);
|
||||
return new GraphQLNonNull(required(type, depth));
|
||||
};
|
||||
const required = (type: ValueType, depth: number): GraphQLNullableType & GraphQLOutputType => {
|
||||
if (type.kind === "optional") return required(type.value, depth + 1);
|
||||
if (type.kind === "list") return new GraphQLList(output(type.value, depth + 1));
|
||||
if (type.kind !== "record") return scalar(type) as GraphQLNullableType & GraphQLOutputType;
|
||||
const key = name("Record", type);
|
||||
let result = objects.get(key);
|
||||
if (!result) {
|
||||
if (!Object.keys(type.fields).length)
|
||||
throw new QueryCompileError("QUERY_TYPE", "Queryable records must have at least one field");
|
||||
result = new GraphQLObjectType({
|
||||
name: key,
|
||||
fields: () =>
|
||||
Object.fromEntries(
|
||||
Object.entries(type.fields).map(([key, value]) => [key, { type: output(value, depth + 1) }]),
|
||||
),
|
||||
});
|
||||
objects.set(key, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const input = (type: ValueType, ordering = false): GraphQLInputType => {
|
||||
if (type.kind === "optional") return input(type.value, ordering);
|
||||
if (type.kind !== "record") return ordering ? direction : scalarComparison(type);
|
||||
const key = name(ordering ? "Order" : "Compare", type);
|
||||
let result = inputs.get(key);
|
||||
if (!result) {
|
||||
result = new GraphQLInputObjectType({
|
||||
name: key,
|
||||
fields: () =>
|
||||
Object.fromEntries(
|
||||
Object.entries(type.fields)
|
||||
.filter(([, value]) => comparableValue(value, ordering))
|
||||
.map(([key, value]) => [key, { type: input(value, ordering) }]),
|
||||
),
|
||||
});
|
||||
inputs.set(key, result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { output, input };
|
||||
}
|
||||
Reference in New Issue
Block a user