// Normalize federation subgraph schemas for graphql-codegen. // // Problem: services schema.graphql files are federation subgraphs that use // `extend type Query` (with no body) and multiple `extend type Query { ... }` // blocks. Standard GraphQL parser used by codegen cannot parse these. // // Solution: read all 7 subgraph schema files, merge all `extend type Query` // blocks into a single `type Query { ... }`, output to a combined SDL file. // // Usage: tsx scripts/normalize-schema.ts // Output: src/lib/api/__generated__/combined-schema.graphql // // Related: spec section 2.4 import * as fs from "node:fs"; import * as path from "node:path"; interface SubgraphSchema { name: string; content: string; } const SCHEMA_FILES = [ { name: "iam", path: "../../services/iam/src/graphql/generated/schema.graphql", }, { name: "config-service", path: "../../services/config-service/src/graphql/generated/schema.graphql", }, { name: "core-edu", path: "../../services/core-edu/src/graphql/generated/schema.graphql", }, { name: "content", path: "../../services/content/src/graphql/generated/schema.graphql", }, { name: "msg", path: "../../services/msg/src/graphql/generated/schema.graphql", }, { name: "data-ana", path: "../../services/data-ana/src/graphql/generated/schema.graphql", }, { name: "ai", path: "../../services/ai/src/graphql/generated/schema.graphql", }, ]; function loadSchemas(): SubgraphSchema[] { const baseDir = process.cwd(); return SCHEMA_FILES.map((f) => ({ name: f.name, content: fs.readFileSync(path.resolve(baseDir, f.path), "utf8"), })); } // Strip federation `extend type Query` syntax: // - Remove standalone `extend type Query` (no body) // - Convert `extend type Query { ... }` blocks to `type Query { ... }` // (we collect fields and merge later) function normalizeSchema(content: string): { staticDefs: string; queryFields: string[]; } { const lines = content.split("\n"); const staticDefs: string[] = []; const queryFields: string[] = []; let i = 0; while (i < lines.length) { const line = lines[i]; // Match `extend type Query` (no body, single line) if (/^\s*extend\s+type\s+Query\s*$/.test(line)) { i++; continue; } // Match `extend type Query {` block start const blockStart = line.match(/^\s*extend\s+type\s+Query\s*\{/); if (blockStart) { // Collect fields until matching `}` i++; while (i < lines.length && !/^\s*\}\s*$/.test(lines[i])) { const fieldLine = lines[i]; if (fieldLine.trim()) { queryFields.push(fieldLine); } i++; } // Skip closing `}` i++; continue; } // Match `type Query {` block (already standard, but merge fields) const stdBlockStart = line.match(/^\s*type\s+Query\s*\{/); if (stdBlockStart) { i++; while (i < lines.length && !/^\s*\}\s*$/.test(lines[i])) { const fieldLine = lines[i]; if (fieldLine.trim()) { queryFields.push(fieldLine); } i++; } i++; continue; } staticDefs.push(line); i++; } return { staticDefs: staticDefs.join("\n"), queryFields }; } function main(): void { const schemas = loadSchemas(); const allStaticDefs: string[] = []; const allQueryFields: string[] = []; for (const s of schemas) { const { staticDefs, queryFields } = normalizeSchema(s.content); allStaticDefs.push(`# === ${s.name} subgraph ===`); allStaticDefs.push(staticDefs.trim()); allQueryFields.push(...queryFields); } // Deduplicate query fields (in case multiple subgraphs define same field) const uniqueQueryFields = Array.from( new Set(allQueryFields.map((f) => f.trim())), ); const combined = [ "# Combined normalized schema for graphql-codegen (federation stripped)", "# DO NOT EDIT - generated by scripts/normalize-schema.ts", "", ...allStaticDefs, "", "type Query {", ...uniqueQueryFields.map((f) => ` ${f}`), "}", "", ].join("\n"); const outDir = path.resolve(process.cwd(), "src/lib/api/__generated__"); fs.mkdirSync(outDir, { recursive: true }); const outPath = path.join(outDir, "combined-schema.graphql"); fs.writeFileSync(outPath, combined); console.log(`Combined schema written to ${outPath}`); console.log(` Query fields: ${uniqueQueryFields.length}`); } main();