Add explicit CRDT edit contracts and structured query values
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
|
||||
const source = { repository: "https://example.test/edit.git", commit: "a".repeat(40) };
|
||||
const content = "record { text:string; tags:list<string>; }";
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Comment id "comment" revision "comment@1" {
|
||||
value content id "content" : ${content} {
|
||||
get id "content:get" execution snapshot targets [browser, server];
|
||||
edit id "content:edit" execution transaction targets [browser, server];
|
||||
}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok && iface.resource.kind === "interface", JSON.stringify(iface.diagnostics));
|
||||
const contract = iface.resource.revision;
|
||||
const compile = (policy = `crdt(${content})`, binding = "edit") =>
|
||||
compileCapabilitySource(
|
||||
`
|
||||
workspace Comments id "w" revision "w@1" commit "${"a".repeat(40)}" {
|
||||
import interface Comment;
|
||||
atom Comment id "comment";
|
||||
conform Comment as Comment id "owner:comment" {
|
||||
private state Content id "content" on Comment : ${content} policy ${policy};
|
||||
initialize { content to state Content; };
|
||||
bind content.get to state Content.read;
|
||||
bind content.edit to state Content.${binding};
|
||||
}
|
||||
}`,
|
||||
"comments.qx",
|
||||
{ interfaces: new Map([["Comment", contract]]), interfaceClosure: [contract] },
|
||||
);
|
||||
test("native edit is an explicit typed change capability over structured content", () => {
|
||||
const result = compile();
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const member = contract.members[0]!;
|
||||
assert.equal(member.kind, "value");
|
||||
if (member.kind !== "value") return;
|
||||
assert.deepEqual(member.operations[1]!.inputType, { kind: "change", value: member.valueType });
|
||||
assert.equal(result.workspace.conformances[0]!.operationBindings[1]!.binding.kind, "state");
|
||||
});
|
||||
test("edit cannot target a register, mismatched profile, or ordinary replacement", () => {
|
||||
for (const [policy, binding] of [
|
||||
["register", "edit"],
|
||||
["crdt(bytes)", "edit"],
|
||||
[`crdt(${content})`, "write"],
|
||||
]) {
|
||||
const result = compile(policy, binding);
|
||||
assert.equal(result.ok, false);
|
||||
assert.ok(
|
||||
result.diagnostics.some((d) => d.code === "invalid-state-binding"),
|
||||
JSON.stringify(result.diagnostics),
|
||||
);
|
||||
}
|
||||
});
|
||||
test("authored prepared change transport values fail explicitly until their runtime is implemented", () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
'package P id "p" revision "p@1" { function applyChange id "apply" : change<string> -> unit; }',
|
||||
{ source },
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.ok(
|
||||
result.diagnostics.some((d) => d.code === "unsupported-prepared-change"),
|
||||
JSON.stringify(result.diagnostics),
|
||||
);
|
||||
});
|
||||
|
||||
test("generic edit retains its materialized type through specialization", () => {
|
||||
const generic = compileCapabilityResourceSource(
|
||||
`interface Cell<value T : storable> id "cell" revision "cell@1" {
|
||||
value content id "content" : T { get id "get"; edit id "edit" execution transaction targets [browser, server]; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(generic.ok && generic.resource.kind === "interface", JSON.stringify(generic.diagnostics));
|
||||
const result = compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${"a".repeat(40)}" {
|
||||
import interface Cell;
|
||||
atom Note id "note";
|
||||
conform Note as Cell<${content}> {
|
||||
private state Content id "content" on Note : ${content} policy crdt(${content});
|
||||
bind content to state Content;
|
||||
}
|
||||
}`,
|
||||
"cell.qx",
|
||||
{ interfaces: new Map([["Cell", generic.resource.revision]]), interfaceClosure: [generic.resource.revision] },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
const applied = result.workspace.interfaceImports.find((i) => i.application)!;
|
||||
assert.equal(applied.members[0]!.operations[1]!.inputType.kind, "change");
|
||||
});
|
||||
@@ -455,8 +455,8 @@ test("expanding recursive generic interfaces fail with a bounded diagnostic", ()
|
||||
assert.equal(result.diagnostics[0].code, "recursive-application");
|
||||
});
|
||||
|
||||
test("storable parameters do not imply support for RPC-only records", () => {
|
||||
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), false);
|
||||
test("storable records recursively require storable leaves", () => {
|
||||
assert.equal(isStorableType({ kind: "record", fields: { name: valueType.string } }), true);
|
||||
assert.equal(isStorableType(valueType.list(valueType.optional(valueType.string))), true);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ test("queryable fields need one authoritative getter and supported values", () =
|
||||
assert.equal(compile('queryable value title id "title" : string { get id "title:get"; }').ok, true);
|
||||
for (const member of [
|
||||
'queryable value title id "title" : string { set id "title:set"; }',
|
||||
'queryable value title id "title" : list<string> { get id "title:get"; }',
|
||||
'queryable value title id "title" : list<unit> { get id "title:get"; }',
|
||||
'queryable value title id "title" : string { get id "title:get"; get id "other:get"; }',
|
||||
])
|
||||
assert.equal(compile(member).ok, false, member);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
|
||||
import { compileQuery } from "../src/query/compile.js";
|
||||
import { querySelectionToWire } from "../src/query/proto.js";
|
||||
import { defaultQueryBudgets, type QueryDeclaration } from "../src/query/types.js";
|
||||
const source = { repository: "https://example.test/structured.git", commit: "a".repeat(40) };
|
||||
const result = compileCapabilityResourceSource(
|
||||
`interface Highlight id "highlight" revision "highlight@1" {
|
||||
queryable value range id "range" : record { sourceRevision:string; start:int32; end:int32; } { get id "range:get" execution snapshot targets [browser, server]; }
|
||||
queryable value content id "content" : record { text:string; tags:list<string>; metadata:optional<record { score:int32; }>; } { get id "content:get" execution snapshot targets [browser, server]; }
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
|
||||
const iface = result.resource.revision;
|
||||
const declaration: QueryDeclaration = {
|
||||
id: "rows",
|
||||
displayName: "Rows",
|
||||
root: { kind: "workspace-interface", interface: iface.revisionId },
|
||||
document: "rows.graphql",
|
||||
operation: "Rows",
|
||||
fragments: [],
|
||||
views: [],
|
||||
allowances: [],
|
||||
budgets: { ...defaultQueryBudgets },
|
||||
watch: false,
|
||||
};
|
||||
const compile = (selection: string) =>
|
||||
compileQuery(
|
||||
declaration,
|
||||
[iface],
|
||||
async () =>
|
||||
`query Rows {root {objects(first:10, ${selection}) {entries {node {position:range {begin:start end} content {text tags metadata {score}}}}}}}`,
|
||||
);
|
||||
test("record/list query values preserve nested projections and leaf predicates", async () => {
|
||||
const checked = await compile(
|
||||
"where:{range:{start:{gte:4}},content:{metadata:{score:{gt:2}}}}, orderBy:{range:{start:ASC}}",
|
||||
);
|
||||
const extent = checked.selection[0]!.selection[0]!;
|
||||
assert.equal(extent.predicate?.kind, "and");
|
||||
if (extent.predicate?.kind !== "and") return;
|
||||
const expressions = extent.predicate.children.map((p) => (p.kind === "compare" ? p.expression : undefined));
|
||||
assert.deepEqual(
|
||||
expressions.map((e) => e?.leaf),
|
||||
[
|
||||
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "range", valuePath: ["start"] },
|
||||
{ kind: "field", interfaceRevisionId: "highlight@1", memberId: "content", valuePath: ["metadata", "score"] },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(expressions[1]?.type, { kind: "optional", value: { kind: "scalar", name: "int32" } });
|
||||
const wire = querySelectionToWire(extent);
|
||||
assert.equal(wire.predicate?.predicate.case, "and");
|
||||
if (wire.predicate?.predicate.case === "and") {
|
||||
const child = wire.predicate.predicate.value.children[0]!;
|
||||
assert.equal(child.predicate.case, "compare");
|
||||
if (child.predicate.case === "compare") {
|
||||
const operand = child.predicate.value.operand?.operand;
|
||||
assert.ok(operand?.case === "expression" && operand.value.leaf.case === "field");
|
||||
assert.deepEqual(operand.value.leaf.value.valuePath, ["start"]);
|
||||
}
|
||||
}
|
||||
const raw = JSON.stringify(checked.output);
|
||||
assert.match(raw, /"begin"/);
|
||||
assert.match(raw, /"tags":\{"kind":"list"/);
|
||||
assert.equal(checked.effects.filter((e) => e.memberId === "range").length, 1);
|
||||
});
|
||||
test("structured predicates reject whole-record scalar comparisons and implicit list expansion", async () => {
|
||||
await assert.rejects(compile("where:{range:{eq:4}}"), /not defined/);
|
||||
await assert.rejects(compile('where:{content:{tags:{eq:"x"}}}'), /not defined/);
|
||||
});
|
||||
|
||||
test("explicit budgets can exceed defaults without exceeding wire bounds", () => {
|
||||
const compile = (rows: string) =>
|
||||
compileCapabilityResourceSource(
|
||||
`import interface Highlight;
|
||||
package P id "p" revision "p@1" {
|
||||
query Rows id "rows" root Highlight document "rows.graphql" operation "Rows" { replica; max rows ${rows}; max result-bytes 8388608; }
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Highlight", iface]]), interfaceClosure: [iface] } },
|
||||
);
|
||||
const result = compile("5000");
|
||||
assert.ok(result.ok && result.resource.kind === "package", JSON.stringify(result.diagnostics));
|
||||
assert.equal(result.resource.revision.queries![0]!.budgets.rows, 5000);
|
||||
assert.equal(compile("4294967296").ok, false);
|
||||
});
|
||||
Reference in New Issue
Block a user