Files
Edu/apps/portal-shell/scripts/normalize-schema.ts
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等
- Tailwind v4 + @theme inline,移除 tailwind.config.js
- React 19 use() + Suspense 流式渲染,首屏骨架秒出
- 三级错误边界:Route → Section → Widget 层层兜底
- 错误上报:useErrorReport → sendBeacon → /api/log mock 端点
- 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围
- 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99%
- notify 统一 Toast 封装,禁止业务直接 import sonner
- PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套)

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
2026-07-17 16:10:05 +08:00

198 lines
5.9 KiB
TypeScript

// 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 };
}
// Sanitize invalid input field types.
//
// Problem: services/ai subgraph declares `input ChatRequestInput { messages:
// ChatMessage }` and `input ChatResponseInput { usage: Usage }` where
// ChatMessage/Usage are OUTPUT types. GraphQL spec forbids input fields
// referencing output types; graphql-codegen's typescript plugin rejects this.
//
// Solution: rewrite those offending input field types to `String` in the
// combined schema. This is a codegen-only sanitize; the runtime apollo-router
// uses the original subgraph schemas directly.
//
// Related: spec section 2.4
const SANITIZE_INPUT_FIELD_REPLACEMENTS: Array<{
inputName: string;
fieldName: string;
replacement: string;
}> = [
// services/ai: input ChatRequestInput { messages: ChatMessage }
{
inputName: "ChatRequestInput",
fieldName: "messages",
replacement: "String",
},
// services/ai: input ChatResponseInput { usage: Usage }
{ inputName: "ChatResponseInput", fieldName: "usage", replacement: "String" },
];
function sanitizeInputFields(content: string): string {
let out = content;
for (const r of SANITIZE_INPUT_FIELD_REPLACEMENTS) {
// Match ` fieldName: OriginalType` lines within `input InputName { ... }`
// blocks. We rely on the simple field-line format generated above.
const inputBlockRe = new RegExp(
`(input\\s+${r.inputName}\\s*\\{[^}]*?)` +
`(\\s{2,}${r.fieldName}\\s*:\\s*)[A-Za-z_][A-Za-z0-9_\\[\\]!]*`,
"g",
);
out = out.replace(inputBlockRe, `$1$2${r.replacement}`);
}
return out;
}
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())),
);
let 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");
combined = sanitizeInputFields(combined);
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();