Implement independent presentation readiness boundaries
This commit is contained in:
@@ -251,7 +251,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
return (
|
return (
|
||||||
interfaces +
|
interfaces +
|
||||||
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PreparedPresentation} from "@quixos/camino-react";\n' +
|
'\nimport {createObjectRenderer,definePackageQuery,definePackageFragment,type ComponentType,type ComponentProps,type PresentationHandle} from "@quixos/camino-react";\n' +
|
||||||
subjectBindings +
|
subjectBindings +
|
||||||
"\n" +
|
"\n" +
|
||||||
queries +
|
queries +
|
||||||
@@ -288,7 +288,7 @@ export function generateComponentClientBindings(plan: ExecutionPlan, packageRevi
|
|||||||
)!;
|
)!;
|
||||||
extra = props(iface.revisionId, member.id);
|
extra = props(iface.revisionId, member.id);
|
||||||
}
|
}
|
||||||
const handle = `PreparedPresentation<${JSON.stringify(child.subject)}, ${extra}>`;
|
const handle = `PresentationHandle<${JSON.stringify(child.subject)}, ${extra}>`;
|
||||||
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
|
return `readonly ${JSON.stringify(child.name)}: ${child.each ? `readonly ${handle}[]` : child.with || child.when ? `${handle} | null` : handle}`;
|
||||||
});
|
});
|
||||||
const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`;
|
const input = `${tsType(composition.output)} & {readonly children: {${children.join(";")}}}`;
|
||||||
|
|||||||
+28
-3
@@ -4,6 +4,8 @@ import {
|
|||||||
validate,
|
validate,
|
||||||
specifiedRules,
|
specifiedRules,
|
||||||
PossibleFragmentSpreadsRule,
|
PossibleFragmentSpreadsRule,
|
||||||
|
NoUnusedVariablesRule,
|
||||||
|
GraphQLError,
|
||||||
print,
|
print,
|
||||||
printSchema,
|
printSchema,
|
||||||
visit,
|
visit,
|
||||||
@@ -201,10 +203,29 @@ export async function compileQuery(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const bindingVariables = new Set(
|
||||||
|
[...fragmentContracts.values()]
|
||||||
|
.filter((entry) => entry.mode === "reference")
|
||||||
|
.flatMap((entry) => Object.values(entry.variables)),
|
||||||
|
);
|
||||||
const errors = validate(
|
const errors = validate(
|
||||||
generated.schema,
|
generated.schema,
|
||||||
document,
|
document,
|
||||||
specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule),
|
[
|
||||||
|
...specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule && rule !== NoUnusedVariablesRule),
|
||||||
|
(context) => ({
|
||||||
|
OperationDefinition: {
|
||||||
|
leave(operation) {
|
||||||
|
const used = new Set(context.getRecursiveVariableUsages(operation).map(({ node }) => node.name.value));
|
||||||
|
for (const variable of operation.variableDefinitions ?? []) {
|
||||||
|
const name = variable.variable.name.value;
|
||||||
|
if (!used.has(name) && !bindingVariables.has(name))
|
||||||
|
context.reportError(new GraphQLError(`Variable "$${name}" is never used.`, { nodes: variable }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
{ maxErrors: 20 },
|
{ maxErrors: 20 },
|
||||||
);
|
);
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
@@ -399,6 +420,7 @@ export async function compileQuery(
|
|||||||
for (const selection of set.selections) {
|
for (const selection of set.selections) {
|
||||||
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
|
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
|
||||||
if (selection.kind === "FragmentSpread") {
|
if (selection.kind === "FragmentSpread") {
|
||||||
|
if (fragmentContracts.get(selection.name.value)?.mode === "reference") continue;
|
||||||
add(
|
add(
|
||||||
fragments.get(selection.name.value)!.selectionSet,
|
fragments.get(selection.name.value)!.selectionSet,
|
||||||
conditional || !!selection.directives?.length,
|
conditional || !!selection.directives?.length,
|
||||||
@@ -530,7 +552,9 @@ export async function compileQuery(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
|
...(contract?.mode === "reference"
|
||||||
|
? []
|
||||||
|
: selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (node.kind !== "Field") return [];
|
if (node.kind !== "Field") return [];
|
||||||
@@ -610,9 +634,10 @@ export async function compileQuery(
|
|||||||
const definitionDigest = createHash("sha256")
|
const definitionDigest = createHash("sha256")
|
||||||
.update(
|
.update(
|
||||||
canonicalJson({
|
canonicalJson({
|
||||||
semantics: 4,
|
semantics: 5,
|
||||||
declaration,
|
declaration,
|
||||||
normalized,
|
normalized,
|
||||||
|
fragmentContracts: Object.fromEntries(fragmentContracts),
|
||||||
// Source provenance belongs to the checked installation. Identical public
|
// Source provenance belongs to the checked installation. Identical public
|
||||||
// contracts keep their query identity when retained in another repository.
|
// contracts keep their query identity when retained in another repository.
|
||||||
// Closed generic identity remains explicit in revisionId/application.
|
// Closed generic identity remains explicit in revisionId/application.
|
||||||
|
|||||||
+16
-2
@@ -11,6 +11,8 @@ export async function fragmentSources(
|
|||||||
) {
|
) {
|
||||||
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
|
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
|
||||||
for (const imported of declaration.importedFragments ?? []) {
|
for (const imported of declaration.importedFragments ?? []) {
|
||||||
|
if (imported.mode !== undefined && imported.mode !== "reference")
|
||||||
|
throw new QueryCompileError("FRAGMENT_IMPORT_MODE", "Unknown fragment import mode");
|
||||||
if (!resolve)
|
if (!resolve)
|
||||||
throw new QueryCompileError(
|
throw new QueryCompileError(
|
||||||
"FRAGMENT_RESOLVER_REQUIRED",
|
"FRAGMENT_RESOLVER_REQUIRED",
|
||||||
@@ -57,7 +59,16 @@ export async function fragmentSources(
|
|||||||
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
|
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
for (const fragment of fragments) {
|
// A reference import binds the checked export, rather than spreading its
|
||||||
|
// fields into this operation. Keep a nominal stub for GraphQL root checking;
|
||||||
|
// the independently checked export supplies the actual projection contract.
|
||||||
|
const referenced = parse("fragment Binding on Placeholder { _camino { ref } }")
|
||||||
|
.definitions[0] as FragmentDefinitionNode;
|
||||||
|
for (const sourceFragment of imported.mode === "reference"
|
||||||
|
? fragments.filter((entry) => entry.name.value === exported.name)
|
||||||
|
: fragments) {
|
||||||
|
const fragment =
|
||||||
|
imported.mode === "reference" ? { ...sourceFragment, selectionSet: referenced.selectionSet } : sourceFragment;
|
||||||
definitions.push(
|
definitions.push(
|
||||||
visit(fragment, {
|
visit(fragment, {
|
||||||
FragmentDefinition(node) {
|
FragmentDefinition(node) {
|
||||||
@@ -73,13 +84,16 @@ export async function fragmentSources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
markers.set(exported.name, {
|
markers.set(exported.name, {
|
||||||
|
...(imported.mode ? { mode: imported.mode } : {}),
|
||||||
package: imported.packageRevisionId,
|
package: imported.packageRevisionId,
|
||||||
fragment: imported.fragmentId,
|
fragment: imported.fragmentId,
|
||||||
digest: checked.definitionDigest,
|
digest: checked.definitionDigest,
|
||||||
root: checked.declaration.root.interface,
|
root: checked.declaration.root.interface,
|
||||||
variables: imported.variables,
|
variables: imported.variables,
|
||||||
});
|
});
|
||||||
for (const [name, contract] of Object.entries(checked.fragmentContracts ?? {})) {
|
for (const [name, contract] of Object.entries(
|
||||||
|
imported.mode === "reference" ? {} : (checked.fragmentContracts ?? {}),
|
||||||
|
)) {
|
||||||
markers.set(privateNames.get(name) ?? name, {
|
markers.set(privateNames.get(name) ?? name, {
|
||||||
...contract,
|
...contract,
|
||||||
variables: Object.fromEntries(
|
variables: Object.fromEntries(
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export interface QueryDeclaration {
|
|||||||
/** Exported local projection; compilation also creates its direct admission. */
|
/** Exported local projection; compilation also creates its direct admission. */
|
||||||
fragment?: { name: string; operation?: string; admission?: boolean };
|
fragment?: { name: string; operation?: string; admission?: boolean };
|
||||||
importedFragments?: {
|
importedFragments?: {
|
||||||
|
/** Bind an independently observed fragment without including its fields in
|
||||||
|
* this projection. Its acquisition is still an explicit replica source. */
|
||||||
|
mode?: "reference";
|
||||||
packageRevisionId: string;
|
packageRevisionId: string;
|
||||||
packageAlias?: string;
|
packageAlias?: string;
|
||||||
fragmentId: string;
|
fragmentId: string;
|
||||||
@@ -113,6 +116,7 @@ export type QueryArgument =
|
|||||||
export interface QuerySelection {
|
export interface QuerySelection {
|
||||||
/** A checked occurrence marker, not an output field or storage capability. */
|
/** A checked occurrence marker, not an output field or storage capability. */
|
||||||
fragmentSpread?: {
|
fragmentSpread?: {
|
||||||
|
mode?: "reference";
|
||||||
package: string;
|
package: string;
|
||||||
fragment: string;
|
fragment: string;
|
||||||
digest: string;
|
digest: string;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface ReactChildDeclaration {
|
|||||||
export interface ReactModulePreparation {
|
export interface ReactModulePreparation {
|
||||||
module: string;
|
module: string;
|
||||||
composition: string;
|
composition: string;
|
||||||
discoveryComposition?: string;
|
|
||||||
children: ReactChildDeclaration[];
|
children: ReactChildDeclaration[];
|
||||||
}
|
}
|
||||||
export interface ReactPreparationArtifact {
|
export interface ReactPreparationArtifact {
|
||||||
|
|||||||
@@ -147,9 +147,6 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
|||||||
for (const child of children)
|
for (const child of children)
|
||||||
for (const branch of child.branches) await expandModule(identity(branch.packageRevision, branch.id));
|
for (const branch of child.branches) await expandModule(identity(branch.packageRevision, branch.id));
|
||||||
const composition = pkg.queryCompositions!.find((entry) => entry.id === preparation.composition)!;
|
const composition = pkg.queryCompositions!.find((entry) => entry.id === preparation.composition)!;
|
||||||
const discovery = { ...composition, id: composition.id + ":discovery" };
|
|
||||||
preparation.discoveryComposition = discovery.id;
|
|
||||||
pkg.queryCompositions!.push(discovery);
|
|
||||||
const steps = structuredClone(composition.steps);
|
const steps = structuredClone(composition.steps);
|
||||||
const exported: { step: string; query: CheckedQuery }[] = [];
|
const exported: { step: string; query: CheckedQuery }[] = [];
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
@@ -187,7 +184,12 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
|||||||
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
|
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
imports.push({ packageRevisionId: branch.packageRevision, fragmentId: declaration.id, variables: {} });
|
imports.push({
|
||||||
|
packageRevisionId: branch.packageRevision,
|
||||||
|
fragmentId: declaration.id,
|
||||||
|
variables: {},
|
||||||
|
mode: "reference",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
selection = insert(selection, path, declaration.fragment!.name);
|
selection = insert(selection, path, declaration.fragment!.name);
|
||||||
const admission = admissionFragments.get(identity(branch.packageRevision, declaration.id))!;
|
const admission = admissionFragments.get(identity(branch.packageRevision, declaration.id))!;
|
||||||
@@ -203,6 +205,18 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
|||||||
variables: {},
|
variables: {},
|
||||||
});
|
});
|
||||||
admissionSelection = insert(admissionSelection, path, admission.declaration.fragment!.name);
|
admissionSelection = insert(admissionSelection, path, admission.declaration.fragment!.name);
|
||||||
|
if (
|
||||||
|
!admissionImports.some(
|
||||||
|
(entry) => entry.packageRevisionId === branch.packageRevision && entry.fragmentId === declaration.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
admissionImports.push({
|
||||||
|
packageRevisionId: branch.packageRevision,
|
||||||
|
fragmentId: declaration.id,
|
||||||
|
variables: {},
|
||||||
|
mode: "reference",
|
||||||
|
});
|
||||||
|
admissionSelection = insert(admissionSelection, path, declaration.fragment!.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,13 +351,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
|||||||
{ id: composition.id, input: composition.input, steps, maxQueries: composition.maxQueries },
|
{ id: composition.id, input: composition.input, steps, maxQueries: composition.maxQueries },
|
||||||
available,
|
available,
|
||||||
);
|
);
|
||||||
const checkedDiscovery = compileQueryComposition(
|
pkg.queryCompositions = pkg.queryCompositions!.map((entry) => (entry.id === composition.id ? checked : entry));
|
||||||
{ id: discovery.id, input: discovery.input, steps: discovery.steps, maxQueries: discovery.maxQueries },
|
|
||||||
available,
|
|
||||||
);
|
|
||||||
pkg.queryCompositions = pkg.queryCompositions!.map((entry) =>
|
|
||||||
entry.id === composition.id ? checked : entry.id === discovery.id ? checkedDiscovery : entry,
|
|
||||||
);
|
|
||||||
eager.set(key, exported);
|
eager.set(key, exported);
|
||||||
done.add(key);
|
done.add(key);
|
||||||
}
|
}
|
||||||
@@ -411,6 +419,7 @@ export async function expandPresentationQueries(workspace: WorkspaceRevision): P
|
|||||||
packageRevisionId: input.pkg.revisionId,
|
packageRevisionId: input.pkg.revisionId,
|
||||||
fragmentId: input.query.declaration.id,
|
fragmentId: input.query.declaration.id,
|
||||||
variables: {},
|
variables: {},
|
||||||
|
...(original.declaration.placement?.kind === "local" ? { mode: "reference" as const } : {}),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
print({ ...document, definitions }),
|
print({ ...document, definitions }),
|
||||||
|
|||||||
@@ -124,19 +124,20 @@ async function fixture(replacement = "First", change?: (artifact: ReactPreparati
|
|||||||
test("workspace expands distinct providers' private inputs without parent field declarations", async () => {
|
test("workspace expands distinct providers' private inputs without parent field declarations", async () => {
|
||||||
const { pkg } = await fixture();
|
const { pkg } = await fixture();
|
||||||
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
||||||
assert.match(parent.document, /title/);
|
const acquisition = pkg.checkedQueries!.find(
|
||||||
assert.match(parent.document, /detail/);
|
(query) =>
|
||||||
|
query.declaration.id ===
|
||||||
|
(parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""),
|
||||||
|
)!;
|
||||||
|
assert.match(acquisition.document, /title/);
|
||||||
|
assert.match(acquisition.document, /detail/);
|
||||||
const plan = pkg.queryCompositions!.find((composition) => composition.id === "react:inputs:library-card")!;
|
const plan = pkg.queryCompositions!.find((composition) => composition.id === "react:inputs:library-card")!;
|
||||||
assert.equal(plan.steps[0]!.query, parent.declaration.id);
|
assert.equal(plan.steps[0]!.query, parent.declaration.id);
|
||||||
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 2);
|
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 2);
|
||||||
const discovery = pkg.queryCompositions!.find(
|
assert(!pkg.queryCompositions!.some((composition) => composition.id.endsWith(":discovery")));
|
||||||
(composition) => composition.id === "react:inputs:library-card:discovery",
|
const original = pkg.checkedQueries!.find((query) => query.declaration.id === "LibraryData")!;
|
||||||
)!;
|
assert.deepEqual(parent.output, original.output, "Acquisition-only child fields are not parent requirements");
|
||||||
assert.equal(
|
assert(Object.values(parent.fragmentContracts!).every((entry) => entry.mode === "reference"));
|
||||||
discovery.steps[0]!.query,
|
|
||||||
"LibraryData",
|
|
||||||
"Cold child data cannot block discovering its selected reference",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("eager cycles and wrongly typed child bindings fail before runtime", async () => {
|
test("eager cycles and wrongly typed child bindings fail before runtime", async () => {
|
||||||
@@ -167,7 +168,16 @@ test("eager cycles and wrongly typed child bindings fail before runtime", async
|
|||||||
test("workspace replacement changes acquisition requirements without changing the parent", async () => {
|
test("workspace replacement changes acquisition requirements without changing the parent", async () => {
|
||||||
const { pkg } = await fixture("Second");
|
const { pkg } = await fixture("Second");
|
||||||
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
const parent = pkg.checkedQueries!.find((query) => query.declaration.id === "react:input:library-card:data")!;
|
||||||
assert.doesNotMatch(parent.document, /\btitle\b/);
|
const acquisition = pkg.checkedQueries!.find(
|
||||||
assert.match(parent.document, /detail/);
|
(query) =>
|
||||||
|
query.declaration.id ===
|
||||||
|
(parent.declaration.placement?.kind === "local" ? parent.declaration.placement.sourceQueryId : ""),
|
||||||
|
)!;
|
||||||
|
assert.doesNotMatch(acquisition.document, /\btitle\b/);
|
||||||
|
assert.match(acquisition.document, /detail/);
|
||||||
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 1);
|
assert.equal(Object.keys(parent.fragmentContracts ?? {}).length, 1);
|
||||||
|
const previous = (await fixture()).pkg.checkedQueries!.find(
|
||||||
|
(query) => query.declaration.id === parent.declaration.id,
|
||||||
|
)!;
|
||||||
|
assert.deepEqual(parent.output, previous.output);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ test("imported fragments retain checked occurrences and require exact roots", as
|
|||||||
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.digest, checked.definitionDigest);
|
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.digest, checked.definitionDigest);
|
||||||
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.root, item.revisionId);
|
assert.equal(result.selection[0]!.selection[0]!.fragmentSpread?.root, item.revisionId);
|
||||||
assert.ok(result.effects.some((e) => e.memberId === "title"));
|
assert.ok(result.effects.some((e) => e.memberId === "title"));
|
||||||
|
const bound = await compileQuery(
|
||||||
|
{
|
||||||
|
...consumer.queries![0]!,
|
||||||
|
importedFragments: consumer.queries![0]!.importedFragments!.map((entry) => ({ ...entry, mode: "reference" })),
|
||||||
|
},
|
||||||
|
[item],
|
||||||
|
async () => "query Screen {root {...Row}}",
|
||||||
|
async () => checked,
|
||||||
|
);
|
||||||
|
assert.equal(bound.selection[0]!.selection.find((entry) => entry.fragmentSpread)?.fragmentSpread?.mode, "reference");
|
||||||
|
assert(
|
||||||
|
!bound.selection[0]!.selection.some((entry) => entry.memberId === "title"),
|
||||||
|
"A fragment binding does not evaluate its fields in the parent",
|
||||||
|
);
|
||||||
|
assert(!JSON.stringify(bound.output).includes('"title"'), "Parent output excludes separately observed fields");
|
||||||
const wrong = {
|
const wrong = {
|
||||||
...checked,
|
...checked,
|
||||||
declaration: {
|
declaration: {
|
||||||
@@ -113,6 +128,39 @@ test("fragment variable mappings are explicit and checked", async () => {
|
|||||||
async () => checked,
|
async () => checked,
|
||||||
);
|
);
|
||||||
assert.deepEqual(result.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" });
|
assert.deepEqual(result.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" });
|
||||||
|
const referenceDeclaration = build("with {show to visible;}");
|
||||||
|
referenceDeclaration.importedFragments = referenceDeclaration.importedFragments!.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
mode: "reference",
|
||||||
|
}));
|
||||||
|
const binding = await compileQuery(
|
||||||
|
referenceDeclaration,
|
||||||
|
[item],
|
||||||
|
async () => document,
|
||||||
|
async () => checked,
|
||||||
|
);
|
||||||
|
assert.deepEqual(binding.selection[0]!.selection[0]!.fragmentSpread?.variables, { show: "visible" });
|
||||||
|
assert.notEqual(binding.definitionDigest, result.definitionDigest);
|
||||||
|
await assert.rejects(
|
||||||
|
compileQuery(
|
||||||
|
referenceDeclaration,
|
||||||
|
[item],
|
||||||
|
async () => document.replace("$visible: Boolean!", "$visible: Boolean!, $unused: Boolean!"),
|
||||||
|
async () => checked,
|
||||||
|
),
|
||||||
|
/never used/,
|
||||||
|
);
|
||||||
|
const changedDigest = await compileQuery(
|
||||||
|
referenceDeclaration,
|
||||||
|
[item],
|
||||||
|
async () => document,
|
||||||
|
async () => ({ ...checked, definitionDigest: "b".repeat(64) }),
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
changedDigest.definitionDigest,
|
||||||
|
binding.definitionDigest,
|
||||||
|
"A bound fragment's identity remains in the parent contract digest",
|
||||||
|
);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
compileQuery(
|
compileQuery(
|
||||||
build("with {show to visible;}"),
|
build("with {show to visible;}"),
|
||||||
|
|||||||
Reference in New Issue
Block a user