Scaffold explicit query placement and authored GraphQL documents
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parse } from "graphql";
|
||||
import type { ScaffoldRecipe } from "./scaffold-recipes.js";
|
||||
import type { StructuralRequest } from "./structural-plan.js";
|
||||
import { parseQx, walkSyntax } from "./source.js";
|
||||
|
||||
/** Insert the authored query and its document together; placement never changes implicitly. */
|
||||
export async function scaffoldQuery(root: string, prefix: string, spec: ScaffoldRecipe): Promise<StructuralRequest> {
|
||||
if (
|
||||
!spec.name ||
|
||||
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) ||
|
||||
!spec.id ||
|
||||
!spec.declaration ||
|
||||
typeof spec.queryDocument !== "string"
|
||||
)
|
||||
throw new Error("Query scaffold requires name, id, declaration and queryDocument");
|
||||
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
|
||||
const syntax = parseQx(source);
|
||||
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
|
||||
const queries = [...walkSyntax(syntax.root)].filter((node) => node.kind === "packageQuery");
|
||||
const query = queries[0];
|
||||
if (
|
||||
syntax.diagnostics.length ||
|
||||
syntax.root.children.length !== 1 ||
|
||||
!pkg ||
|
||||
queries.length !== 1 ||
|
||||
!query ||
|
||||
pkg.children.filter((node) => node.kind === "packageExport").length !== 1 ||
|
||||
pkg.children.some((node) => !["identifier", "stringLiteral", "packageExport"].includes(node.kind))
|
||||
)
|
||||
throw new Error("Expected one QX query declaration");
|
||||
const identifier = query.children.find((node) => node.kind === "identifier")!;
|
||||
const literals = query.children
|
||||
.filter((node) => node.kind === "stringLiteral")
|
||||
.map((node) => JSON.parse(source.slice(node.start, node.end)) as string);
|
||||
const [id, document, operation] = literals;
|
||||
if (source.slice(identifier.start, identifier.end) !== spec.name || id !== spec.id)
|
||||
throw new Error("Query declaration name/ID must match its registration");
|
||||
if (!document || !/^queries\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+\.graphql$/.test(document))
|
||||
throw new Error("Query document must be a contained queries/*.graphql path");
|
||||
const graphql = parse(spec.queryDocument);
|
||||
const operations = graphql.definitions.filter((definition) => definition.kind === "OperationDefinition");
|
||||
if (operations.length !== 1 || operations[0]!.operation !== "query" || operations[0]!.name?.value !== operation)
|
||||
throw new Error("GraphQL document must contain the declared named query operation");
|
||||
const authored = await fs.readFile(path.join(root, prefix, "package.qx"), "utf8");
|
||||
const existing = parseQx(authored);
|
||||
const parent = existing.root.children.find((node) => node.kind === "packageResourceDecl");
|
||||
if (existing.diagnostics.length || !parent) throw new Error("Fix package.qx before adding a query");
|
||||
for (const node of parent.children.filter((node) => node.kind === "packageExport").flatMap((node) => node.children)) {
|
||||
const name = node.children.find((child) => child.kind === "identifier");
|
||||
const literal = node.children.find((child) => child.kind === "stringLiteral");
|
||||
if (
|
||||
(name && authored.slice(name.start, name.end) === spec.name) ||
|
||||
(literal && JSON.parse(authored.slice(literal.start, literal.end)) === spec.id)
|
||||
)
|
||||
throw new Error("New query requires a unique name and ID");
|
||||
}
|
||||
const parentID = parent.children.find((node) => node.kind === "stringLiteral")!;
|
||||
return {
|
||||
kind: "package",
|
||||
source: spec.source,
|
||||
resourceRoot: spec.directory,
|
||||
validation: "syntax",
|
||||
files: [
|
||||
{
|
||||
file: prefix + "package.qx",
|
||||
edits: [
|
||||
{
|
||||
operation: "append",
|
||||
parent: { kind: "packageResourceDecl", id: JSON.parse(authored.slice(parentID.start, parentID.end)) },
|
||||
source: spec.declaration,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ file: prefix + document, create: spec.queryDocument },
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user