Files
Genarrative/scripts/check-spacetime-runtime-access.mjs
2026-05-16 22:52:10 +08:00

222 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = process.cwd();
function readUtf8(relativePath) {
const absolute = path.join(repoRoot, relativePath);
if (!fs.existsSync(absolute)) {
failures.push(`${relativePath}: 文件不存在,无法执行 SpacetimeDB runtime access 检查`);
return null;
}
return fs.readFileSync(absolute, 'utf8');
}
const forbiddenSnippets = [
{
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.owner_user_id == input.owner_user_id)',
reason: 'puzzle_work_profile 已有 by_puzzle_work_owner_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.publication_status == PuzzlePublicationStatus::Published)',
reason: 'puzzle_work_profile 已有 by_puzzle_work_publication_status 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/puzzle.rs',
snippet: '.puzzle_leaderboard_entry()\n .iter()\n .filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)',
reason: 'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/match3d/mod.rs',
snippet: '.match3d_work_profile()\n .iter()\n .filter(|row| {',
reason: 'match3d_work_profile 已有 owner/status 索引,列表不应整表过滤',
},
{
file: 'server-rs/crates/spacetime-module/src/visual_novel.rs',
snippet: '.visual_novel_work_profile()\n .iter()\n .filter(|row| {',
reason: 'visual_novel_work_profile 已有 owner/status 索引,列表不应整表过滤',
},
{
file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs',
snippet: '.asset_object()\n .iter()\n .find(|row| row.bucket == input.bucket && row.object_key == input.object_key)',
reason: 'asset_object 已有 by_bucket_object_key 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs',
snippet: '.asset_object()\n .iter()\n .filter(|row| row.asset_kind == asset_kind)',
reason: 'asset_object 已有 asset_kind 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/ai/stages.rs',
snippet: '.ai_task_stage()\n .iter()\n .filter(|row| row.task_id == task_id)',
reason: 'ai_task_stage 已有 by_ai_task_stage_task_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/ai/stages.rs',
snippet: '.ai_text_chunk()\n .iter()\n .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)',
reason: 'ai_text_chunk 已有 by_ai_text_chunk_task_id / by_ai_text_chunk_task_stage_sequence 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs',
snippet: '.ai_task_stage()\n .iter()\n .filter(|stage| stage.task_id == row.task_id)',
reason: 'ai_task_stage 快照组装应使用 by_ai_task_stage_task_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs',
snippet: '.ai_result_reference()\n .iter()\n .filter(|reference| reference.task_id == row.task_id)',
reason: 'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.profile_save_archive()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
reason: 'profile_save_archive 已有 by_profile_save_archive_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.profile_played_world()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
reason: 'profile_played_world 已有 by_profile_played_world_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.profile_wallet_ledger()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)',
reason: 'profile_wallet_ledger 已有 by_profile_wallet_ledger_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.profile_referral_relation()\n .iter()\n .filter(|row| row.inviter_user_id == user_id)',
reason: 'profile_referral_relation 已有 by_profile_referral_inviter_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.profile_recharge_order()\n .iter()\n .filter(|row| row.user_id == user_id)',
reason: 'profile_recharge_order 已有 by_profile_recharge_order_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs',
snippet: '.tracking_daily_stat()\n .iter()\n .filter(|row| {',
reason: 'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引analytics 查询不应整表过滤',
},
{
file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs',
snippet: '.custom_world_profile()\n .iter()\n .find(|row| {',
reason: 'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引',
},
{
file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs',
snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {',
reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引',
},
];
const procedureResultFiles = [
'server-rs/crates/module-puzzle/src/application.rs',
'server-rs/crates/module-big-fish/src/domain.rs',
'server-rs/crates/spacetime-module/src/match3d/types.rs',
'server-rs/crates/spacetime-module/src/square_hole/types.rs',
'server-rs/crates/spacetime-module/src/visual_novel.rs',
'server-rs/crates/spacetime-module/src/bark_battle/types.rs',
];
const mapperCompatibilityFiles = [
'server-rs/crates/spacetime-client/src/mapper.rs',
'server-rs/crates/spacetime-client/src/lib.rs',
];
const bigFishRuntimeFiles = [
'server-rs/crates/module-big-fish/src/commands.rs',
'server-rs/crates/spacetime-module/src/big_fish/runtime.rs',
'server-rs/crates/spacetime-module/src/big_fish/session.rs',
];
const legacyMapperPatterns = [
{
pattern: /\b[A-Za-z0-9_]*JsonRecord\b/u,
reason: 'spacetime-client mapper 不应保留旧 ProcedureResult JSON 兼容 Record',
},
{
pattern: /\bCompatibleBigFish[A-Za-z0-9_]*\b/u,
reason: 'spacetime-client mapper 不应保留 BigFish 旧 JSON 兼容结构',
},
{
pattern: /\bmap_[A-Za-z0-9_]*_json\b/u,
reason: 'spacetime-client mapper 不应再通过 map_*_json 反序列化 procedure payload',
},
{
pattern: /serde_json::from_str::<[A-Za-z0-9_:]*JsonRecord/u,
reason: 'spacetime-client mapper 不应把 procedure result 再反序列化为 JsonRecord',
},
{
pattern: /\b(?:items|run|work|session|event|feedback)_json:\s*Some\(/u,
reason: 'mapper 测试与兼容路径不应再构造旧 procedure JSON 字符串字段',
},
];
const typedProcedurePayloadFieldPattern =
/\b(?:row|session|work|item|items|run|event|feedback)_json:\s*Option<String>/gu;
const failures = [];
for (const rule of forbiddenSnippets) {
const content = readUtf8(rule.file);
if (content === null) {
continue;
}
if (content.includes(rule.snippet)) {
failures.push(`${rule.file}: ${rule.reason}`);
}
}
for (const file of procedureResultFiles) {
const content = readUtf8(file);
if (content === null) {
continue;
}
const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? [];
for (const block of resultBlocks) {
const jsonFields = block.match(typedProcedurePayloadFieldPattern);
if (jsonFields?.length) {
const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`);
}
}
}
for (const file of mapperCompatibilityFiles) {
const content = readUtf8(file);
if (content === null) {
continue;
}
for (const rule of legacyMapperPatterns) {
if (rule.pattern.test(content)) {
failures.push(`${file}: ${rule.reason}`);
}
}
}
for (const file of bigFishRuntimeFiles) {
const content = readUtf8(file);
if (content === null) {
continue;
}
const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? [];
for (const block of resultBlocks) {
const jsonFields = block.match(typedProcedurePayloadFieldPattern);
if (jsonFields?.length) {
const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult';
failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`);
}
}
}
if (failures.length > 0) {
console.error('SpacetimeDB runtime access 检查失败:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('SpacetimeDB runtime access 检查通过。');