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 { 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 }, ], }; }