Commit 52014d4ee2a9ef97a04e369a2bf7ffcf5679247e
1 parent
5babb654
plan: consolidate all config into config-vars.yaml (drop .env.local + DB guards)
- config-vars.yaml now holds全部 config incl. secrets (database/admin_init.password/secrets), committed - delete env-local-template; setup-test-db reads config-vars.yaml; gitignore no longer ignores .env.local - docs-07 single-store rule; db-init/db-design-gen read config-vars.yaml database: section - drop test_db_allowed_hosts whitelist + schema-looks-like-test guard (trust config-vars.yaml)
Showing
7 changed files
with
111 additions
and
122 deletions
skills/plan/db-design-gen/SKILL.md
| @@ -47,7 +47,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion | @@ -47,7 +47,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion | ||
| 47 | 47 | ||
| 48 | ### C. 渲染 docs/03 | 48 | ### C. 渲染 docs/03 |
| 49 | 49 | ||
| 50 | -1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `.env.local` 读 `DB_SCHEMA`,无则填 `【人工填写:DB_SCHEMA】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。 | 50 | +1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `config-vars.yaml` 读 `database.schema`,无则填 `【人工填写:database.schema】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。 |
| 51 | 2. 渲染「表清单」:对每张表:读取并填充 `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`,其中标准列区块用步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` 展开(`TENANT_COLS` = 「无」时不输出租户列行)。 | 51 | 2. 渲染「表清单」:对每张表:读取并填充 `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`,其中标准列区块用步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` 展开(`TENANT_COLS` = 「无」时不输出租户列行)。 |
| 52 | 3. 写入 `docs/03-数据库设计文档.md`。 | 52 | 3. 写入 `docs/03-数据库设计文档.md`。 |
| 53 | 53 |
skills/plan/db-init/SKILL.md
| 1 | --- | 1 | --- |
| 2 | name: db-init | 2 | name: db-init |
| 3 | -description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs 复用三层防护并 DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。 | 3 | +description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。 |
| 4 | user-invocable: false | 4 | user-invocable: false |
| 5 | allowed-tools: Read Write Edit Glob Skill Bash(node *) | 5 | allowed-tools: Read Write Edit Glob Skill Bash(node *) |
| 6 | --- | 6 | --- |
| @@ -56,30 +56,39 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs" \ | @@ -56,30 +56,39 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs" \ | ||
| 56 | 56 | ||
| 57 | ### B. 数据库环境检查 | 57 | ### B. 数据库环境检查 |
| 58 | 58 | ||
| 59 | -用 `Glob` 确认 `.env.local` 存在(不存在 → 提示重跑 A2 `skeleton-gen` 并停下)。用 `Read` 逐行解析 `KEY=VALUE`(跳过空行 / `#` 注释,**不做 shell-source / 变量展开**),校验 `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_SCHEMA` 5 项均非空——任一缺失 → 打印缺失字段并停下。 | 59 | +用 `Glob` 确认 `config-vars.yaml` 存在(不存在 → 提示重跑 A1 `scope-lock` 并停下)。用 `Read` 读其 `database:` 段,校验 `host` / `port` / `user` / `password` / `schema` 5 项均非空且非 `【人工填写` 占位——任一缺失 → 打印缺失字段并停下。 |
| 60 | 60 | ||
| 61 | -用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。 | 61 | +用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。下面的 `node -e` 内联了与 `lib/yaml-config.mjs` 同规则的极简 YAML 读取(2 层 map + 标量),仅读 `database:` 段: |
| 62 | 62 | ||
| 63 | ```bash | 63 | ```bash |
| 64 | node -e ' | 64 | node -e ' |
| 65 | const { spawnSync } = require("node:child_process"); | 65 | const { spawnSync } = require("node:child_process"); |
| 66 | const { readFileSync } = require("node:fs"); | 66 | const { readFileSync } = require("node:fs"); |
| 67 | -const env = {}; | ||
| 68 | -for (const raw of readFileSync(".env.local", "utf8").split(/\r?\n/)) { | ||
| 69 | - const line = raw.trim(); | ||
| 70 | - if (!line || line.startsWith("#")) continue; | ||
| 71 | - const eq = line.indexOf("="); | ||
| 72 | - if (eq < 0) continue; | ||
| 73 | - const key = line.slice(0, eq).trim(); | ||
| 74 | - let value = line.slice(eq + 1).trim(); | ||
| 75 | - if (value.length >= 2 && ((value[0] === "\"" && value.at(-1) === "\"") || (value[0] === "'"'"'" && value.at(-1) === "'"'"'"))) value = value.slice(1, -1); | ||
| 76 | - env[key] = value; | 67 | +const SQ = String.fromCharCode(39), DQ = String.fromCharCode(34); |
| 68 | +function parseScalar(raw) { | ||
| 69 | + let s = String(raw).trim(); | ||
| 70 | + if (s === "" || s[0] === "#") return ""; | ||
| 71 | + const q = s[0]; | ||
| 72 | + if (q === SQ || q === DQ) { const e = s.indexOf(q, 1); if (e !== -1) return s.slice(1, e); } | ||
| 73 | + const h = s.indexOf(" #"); if (h !== -1) s = s.slice(0, h).trim(); | ||
| 74 | + return s; | ||
| 77 | } | 75 | } |
| 76 | +const cfg = {}; let sec = null; | ||
| 77 | +for (const raw of readFileSync("config-vars.yaml", "utf8").split(/\r?\n/)) { | ||
| 78 | + const t = raw.trim(); if (!t || t[0] === "#") continue; | ||
| 79 | + const c = raw.indexOf(":"); if (c < 0) continue; | ||
| 80 | + const k = raw.slice(0, c).trim(); if (!k) continue; | ||
| 81 | + const ind = raw.length - raw.replace(/^\s+/, "").length; | ||
| 82 | + const v = parseScalar(raw.slice(c + 1)); | ||
| 83 | + if (ind === 0) { if (v === "") { sec = {}; cfg[k] = sec; } else { cfg[k] = v; sec = null; } } | ||
| 84 | + else if (sec) { sec[k] = v; } | ||
| 85 | +} | ||
| 86 | +const db = cfg.database || {}; | ||
| 78 | const args = [ | 87 | const args = [ |
| 79 | - `--host=${env.DB_HOST}`, | ||
| 80 | - `--port=${env.DB_PORT || "3306"}`, | ||
| 81 | - `--user=${env.DB_USER}`, | ||
| 82 | - `--password=${env.DB_PASSWORD || ""}`, | 88 | + `--host=${db.host}`, |
| 89 | + `--port=${db.port || "3306"}`, | ||
| 90 | + `--user=${db.user}`, | ||
| 91 | + `--password=${db.password || ""}`, | ||
| 83 | "-e", | 92 | "-e", |
| 84 | "SELECT 1;" | 93 | "SELECT 1;" |
| 85 | ]; | 94 | ]; |
| @@ -90,7 +99,7 @@ process.exit(r.status === null ? 1 : r.status); | @@ -90,7 +99,7 @@ process.exit(r.status === null ? 1 : r.status); | ||
| 90 | 99 | ||
| 91 | 成功 → 进入步骤 C;失败 → 打印具体错误(认证 / 主机不可达 / 端口拒接)并停下。 | 100 | 成功 → 进入步骤 C;失败 → 打印具体错误(认证 / 主机不可达 / 端口拒接)并停下。 |
| 92 | 101 | ||
| 93 | -勾选:` - [ ] .env.local 凭据已验证(mysql -e "SELECT 1" OK)` | 102 | +勾选:` - [ ] config-vars.yaml DB 凭据已验证(mysql -e "SELECT 1" OK)` |
| 94 | 103 | ||
| 95 | ### C. 自动导入 MySQL | 104 | ### C. 自动导入 MySQL |
| 96 | 105 | ||
| @@ -102,10 +111,10 @@ node scripts/setup-test-db.mjs | @@ -102,10 +111,10 @@ node scripts/setup-test-db.mjs | ||
| 102 | 111 | ||
| 103 | #### C.2 把 V1 灌入已清空的 schema | 112 | #### C.2 把 V1 灌入已清空的 schema |
| 104 | 113 | ||
| 105 | -调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `.env.local`(**不** shell-source,消除注入),复用 host 白名单 + schema 名安全闸,再经 mysql2 把 DDL 灌入 schema。 | 114 | +调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `config-vars.yaml` 的 `database:` 段(**不** shell-source,消除注入),再经 mysql2 把 DDL 灌入 schema。 |
| 106 | 115 | ||
| 107 | ```bash | 116 | ```bash |
| 108 | -node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__initial_schema.sql | 117 | +node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" config-vars.yaml sql/migrations/V1__initial_schema.sql |
| 109 | ``` | 118 | ``` |
| 110 | 119 | ||
| 111 | 退出码与处理: | 120 | 退出码与处理: |
| @@ -113,7 +122,7 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini | @@ -113,7 +122,7 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini | ||
| 113 | - `1` → 失败:打印 stderr 并停下 | 122 | - `1` → 失败:打印 stderr 并停下 |
| 114 | - `2` → 用法错(路径找不到),打印路径并停下 | 123 | - `2` → 用法错(路径找不到),打印路径并停下 |
| 115 | 124 | ||
| 116 | -勾选:` - [ ] setup-test-db.mjs 防护通过 + DROP+CREATE + apply V1 已执行` | 125 | +勾选:` - [ ] setup-test-db.mjs DROP+CREATE + apply V1 已执行` |
| 117 | 126 | ||
| 118 | ### D. 勾选 docs/08 进度 + 进入 A5 | 127 | ### D. 勾选 docs/08 进度 + 进入 A5 |
| 119 | 128 | ||
| @@ -126,7 +135,8 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini | @@ -126,7 +135,8 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini | ||
| 126 | ## 参考 | 135 | ## 参考 |
| 127 | 136 | ||
| 128 | - `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(A.3 docs/03 ↔ V1.sql 5 维一致性校验,跨平台纯 Node) | 137 | - `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(A.3 docs/03 ↔ V1.sql 5 维一致性校验,跨平台纯 Node) |
| 129 | -- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 .env.local + mysql2 灌入 DDL,不 shell-source) | 138 | +- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 config-vars.yaml 的 database: 段 + mysql2 灌入 DDL,不 shell-source) |
| 139 | +- `${CLAUDE_PLUGIN_ROOT}/lib/yaml-config.mjs`(apply-ddl 依赖的极简 YAML 读取) | ||
| 130 | - `docs/03-数据库设计文档.md`(DDL 翻译输入,SSoT) | 140 | - `docs/03-数据库设计文档.md`(DDL 翻译输入,SSoT) |
| 131 | -- `.env.local`(DB 凭据) | 141 | +- `config-vars.yaml`(DB 凭据,A1 产出) |
| 132 | - 产物:`sql/migrations/V1__initial_schema.sql`(由 Flyway 在 Spring Boot 启动时验证 / apply) | 142 | - 产物:`sql/migrations/V1__initial_schema.sql`(由 Flyway 在 Spring Boot 启动时验证 / apply) |
skills/plan/scope-lock/templates/config-vars-template.yaml
| 1 | -# config-vars.yaml — non-sensitive project config; rules live in docs/07. | ||
| 2 | -# Sensitive values go in .env.local; only list key names under secrets_ref. | 1 | +# config-vars.yaml — 项目全部配置(含敏感凭据)。随项目提交,内部 git 传播。 |
| 2 | +# 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析此文件。 | ||
| 3 | +# 值含 : / # / 空格 / $ / 引号等特殊字符时,用单引号包裹整个值:password: 'p@ss: w0rd#1' | ||
| 3 | 4 | ||
| 4 | backend: | 5 | backend: |
| 5 | - base_package: 【人工填写:后端根包名 / 命名空间,如 com.acme.erp】 | 6 | + base_package: com.xly.erp |
| 6 | http_port: 【人工填写:后端 HTTP 端口,默认 8080】 | 7 | http_port: 【人工填写:后端 HTTP 端口,默认 8080】 |
| 7 | 8 | ||
| 8 | frontend: | 9 | frontend: |
| 9 | - pkg_name: 【人工填写:前端包名,如 acme-erp-web】 | 10 | + pkg_name: xly-erp-web |
| 10 | dev_port: 【人工填写:前端开发服务器端口,默认 5173】 | 11 | dev_port: 【人工填写:前端开发服务器端口,默认 5173】 |
| 11 | 12 | ||
| 13 | +database: | ||
| 14 | + host: 【人工填写:MySQL host,推荐 localhost】 | ||
| 15 | + port: 【人工填写:MySQL port,默认 3306】 | ||
| 16 | + user: 【人工填写:开发账号名】 | ||
| 17 | + password: 【人工填写:对应密码,含特殊字符时用单引号包裹】 | ||
| 18 | + schema: 【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】 | ||
| 19 | + | ||
| 12 | admin_init: | 20 | admin_init: |
| 13 | - username: 【人工填写:超级管理员初始账号,如 admin】 | ||
| 14 | - # 初始密码属敏感 → 见 .env.local 的 ADMIN_INIT_PASSWORD | 21 | + username: admin |
| 22 | + password: 666666 | ||
| 15 | 23 | ||
| 16 | -secrets_ref: | ||
| 17 | - - DB_PASSWORD # 数据库密码 | ||
| 18 | - - JWT_SECRET # JWT / 令牌签名密钥 | ||
| 19 | - # - REDIS_PASSWORD # 缓存 / 会话(用 Redis 时) | ||
| 20 | - # - ADMIN_INIT_PASSWORD # 超级管理员初始密码(有初始账号时) | ||
| 21 | - # - OSS_ACCESS_KEY_SECRET / SMS_API_SECRET ... # 第三方凭证按需添加 | 24 | +secrets: |
| 25 | + jwt_secret: 【人工填写:JWT 签名密钥,256+ bit 随机串】 | ||
| 26 | + # 项目专属凭据按需取消注释 / 追加,直接填真实值: | ||
| 27 | + # redis_password: 【人工填写:Redis 密码(用 Redis 时)】 | ||
| 28 | + # oss_access_key_secret: 【人工填写:对象存储密钥】 | ||
| 29 | + # sms_api_secret: 【人工填写:短信网关密钥】 |
skills/plan/skeleton-gen/templates/docs-07-env-template.md
| @@ -6,14 +6,15 @@ | @@ -6,14 +6,15 @@ | ||
| 6 | 6 | ||
| 7 | ## 三、配置与凭据规则 | 7 | ## 三、配置与凭据规则 |
| 8 | 8 | ||
| 9 | -项目配置分两处存放,**本文档只记规则、不记具体值**: | 9 | +项目**全部配置**(含敏感凭据)统一存放在仓库根 `config-vars.yaml`,结构化 YAML,随项目提交(内部 git 传播)。**本文档只记规则、不记具体值**: |
| 10 | 10 | ||
| 11 | -- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ 仓库根 `config-vars.yaml`,结构化 YAML,随项目提交。 | ||
| 12 | -- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ 仓库根 `.env.local`,入 `.gitignore`,**不提交**;`config-vars.yaml` 末尾 `secrets_ref` 只登记键名作引用。 | 11 | +- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ `config-vars.yaml` 对应段。 |
| 12 | +- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ `config-vars.yaml` 的 `database` / `admin_init.password` / `secrets` 段,直接填真实值。 | ||
| 13 | 13 | ||
| 14 | 规则: | 14 | 规则: |
| 15 | - 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。 | 15 | - 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。 |
| 16 | - 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。 | 16 | - 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。 |
| 17 | -- 任何敏感值不得出现在 `config-vars.yaml`、docs、源码或日志中——只允许出现在 `.env.local`。 | 17 | +- 任何配置值(含敏感值)只允许出现在 `config-vars.yaml`,不得散落在 docs、源码或日志中。 |
| 18 | +- 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析 `config-vars.yaml`;值含特殊字符时用单引号包裹。 | ||
| 18 | 19 | ||
| 19 | ## 四、常用命令 | 20 | ## 四、常用命令 |
skills/plan/skeleton-gen/templates/env-local-template deleted
| 1 | -# Local dev credentials — gitignored. | ||
| 2 | -# Quote values containing $/space/!/backtick with single quotes: DB_PASSWORD='p@ss$w0rd!' | ||
| 3 | - | ||
| 4 | -DB_HOST=【人工填写:MySQL host,推荐 localhost】 | ||
| 5 | -DB_PORT=【人工填写:MySQL port,默认 3306】 | ||
| 6 | -DB_USER=【人工填写:开发账号名】 | ||
| 7 | -DB_PASSWORD=【人工填写:对应密码,含特殊字符时用单引号包裹】 | ||
| 8 | -DB_SCHEMA=【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】 | ||
| 9 | -JWT_SECRET=【人工填写:JWT 签名密钥,256+ bit 随机串】 | ||
| 10 | - | ||
| 11 | -# 可选:额外允许 DROP CREATE 的远程 host(空格或逗号分隔)。 | ||
| 12 | -TEST_DB_ALLOWED_HOSTS= |
skills/plan/skeleton-gen/templates/gitignore-append-template
skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs
| 1 | #!/usr/bin/env node | 1 | #!/usr/bin/env node |
| 2 | -// scripts/setup-test-db.mjs — DROP + CREATE 空测试库。 | 2 | +// scripts/setup-test-db.mjs — DROP + CREATE 测试库。 |
| 3 | // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 | 3 | // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 |
| 4 | -// 只允许本地 host(或 TEST_DB_ALLOWED_HOSTS 白名单内的 host)+ 测试库名(含 test/_dev/_local/_ci)。 | 4 | +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取(host / schema 完全信任该文件,无额外校验)。 |
| 5 | 5 | ||
| 6 | import { spawnSync } from 'node:child_process' | 6 | import { spawnSync } from 'node:child_process' |
| 7 | import { existsSync, readFileSync } from 'node:fs' | 7 | import { existsSync, readFileSync } from 'node:fs' |
| @@ -9,86 +9,70 @@ import { dirname, join } from 'node:path' | @@ -9,86 +9,70 @@ import { dirname, join } from 'node:path' | ||
| 9 | import { fileURLToPath } from 'node:url' | 9 | import { fileURLToPath } from 'node:url' |
| 10 | 10 | ||
| 11 | const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) | 11 | const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) |
| 12 | -const ENV_FILE = join(SCRIPT_DIR, '..', '.env.local') | 12 | +const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml') |
| 13 | 13 | ||
| 14 | -function parseEnv(text) { | ||
| 15 | - const env = {} | ||
| 16 | - for (const rawLine of text.split(/\r?\n/)) { | ||
| 17 | - const line = rawLine.trim() | ||
| 18 | - if (line === '' || line.startsWith('#')) continue | ||
| 19 | - const eq = line.indexOf('=') | ||
| 20 | - if (eq === -1) continue | ||
| 21 | - const key = line.slice(0, eq).trim() | ||
| 22 | - if (!key) continue | ||
| 23 | - let value = line.slice(eq + 1).trim() | ||
| 24 | - if ( | ||
| 25 | - value.length >= 2 && | ||
| 26 | - ((value.startsWith("'") && value.endsWith("'")) || | ||
| 27 | - (value.startsWith('"') && value.endsWith('"'))) | ||
| 28 | - ) { | ||
| 29 | - value = value.slice(1, -1) | 14 | +// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。 |
| 15 | +function parseScalar(raw) { | ||
| 16 | + let s = String(raw).trim() | ||
| 17 | + if (s === '' || s[0] === '#') return '' | ||
| 18 | + const q = s[0] | ||
| 19 | + if (q === '"' || q === "'") { | ||
| 20 | + const end = s.indexOf(q, 1) | ||
| 21 | + if (end !== -1) return s.slice(1, end) | ||
| 22 | + } | ||
| 23 | + const hash = s.indexOf(' #') | ||
| 24 | + if (hash !== -1) s = s.slice(0, hash).trim() | ||
| 25 | + return s | ||
| 26 | +} | ||
| 27 | +function parseYamlConfig(text) { | ||
| 28 | + const root = {} | ||
| 29 | + let section = null | ||
| 30 | + for (const rawLine of text.split('\n')) { | ||
| 31 | + const line = rawLine.replace(/\r$/, '') | ||
| 32 | + const trimmed = line.trim() | ||
| 33 | + if (trimmed === '' || trimmed[0] === '#') continue | ||
| 34 | + const colon = line.indexOf(':') | ||
| 35 | + if (colon === -1) continue | ||
| 36 | + const key = line.slice(0, colon).trim() | ||
| 37 | + if (key === '') continue | ||
| 38 | + const indent = line.length - line.replace(/^\s+/, '').length | ||
| 39 | + const value = parseScalar(line.slice(colon + 1)) | ||
| 40 | + if (indent === 0) { | ||
| 41 | + if (value === '') { | ||
| 42 | + section = {} | ||
| 43 | + root[key] = section | ||
| 44 | + } else { | ||
| 45 | + root[key] = value | ||
| 46 | + section = null | ||
| 47 | + } | ||
| 48 | + } else if (section) { | ||
| 49 | + section[key] = value | ||
| 50 | + } else { | ||
| 51 | + root[key] = value | ||
| 30 | } | 52 | } |
| 31 | - env[key] = value | ||
| 32 | } | 53 | } |
| 33 | - return env | 54 | + return root |
| 34 | } | 55 | } |
| 35 | 56 | ||
| 36 | -if (!existsSync(ENV_FILE)) { | ||
| 37 | - console.error(`[setup-test-db] .env.local 不存在(${ENV_FILE})`) | 57 | +if (!existsSync(CONFIG_FILE)) { |
| 58 | + console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`) | ||
| 38 | process.exit(1) | 59 | process.exit(1) |
| 39 | } | 60 | } |
| 40 | 61 | ||
| 41 | -const env = parseEnv(readFileSync(ENV_FILE, 'utf8')) | 62 | +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} |
| 42 | 63 | ||
| 43 | -const DB_HOST = env.DB_HOST ?? '' | ||
| 44 | -const DB_PORT = env.DB_PORT ?? '3306' | ||
| 45 | -const DB_USER = env.DB_USER ?? '' | ||
| 46 | -const DB_PASSWORD = env.DB_PASSWORD ?? '' | ||
| 47 | -const DB_SCHEMA = env.DB_SCHEMA ?? '' | 64 | +const DB_HOST = db.host ?? '' |
| 65 | +const DB_PORT = db.port ?? '3306' | ||
| 66 | +const DB_USER = db.user ?? '' | ||
| 67 | +const DB_PASSWORD = db.password ?? '' | ||
| 68 | +const DB_SCHEMA = db.schema ?? '' | ||
| 48 | 69 | ||
| 49 | if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { | 70 | if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { |
| 50 | - console.error(`[setup-test-db] DB_PORT 非法: ${DB_PORT}(必须是 1..65535 的整数)`) | ||
| 51 | - process.exit(1) | ||
| 52 | -} | ||
| 53 | - | ||
| 54 | -if (!/^[A-Za-z0-9_]+$/.test(DB_SCHEMA)) { | ||
| 55 | - console.error(`[setup-test-db] DB_SCHEMA 只能包含字母、数字、下划线,当前为: ${DB_SCHEMA}`) | ||
| 56 | - process.exit(1) | ||
| 57 | -} | ||
| 58 | - | ||
| 59 | -// 防护 1:默认只允许本地 host(localhost / 127.0.0.1 / ::1)。 | ||
| 60 | -// 额外允许的远程 host 在 .env.local 的 TEST_DB_ALLOWED_HOSTS 中(空格或逗号分隔)。 | ||
| 61 | -const extraHosts = (env.TEST_DB_ALLOWED_HOSTS ?? '') | ||
| 62 | - .split(/[\s,]+/) | ||
| 63 | - .filter(Boolean) | ||
| 64 | -const allowedHosts = ['localhost', '127.0.0.1', '::1', ...extraHosts] | ||
| 65 | -if (!allowedHosts.includes(DB_HOST)) { | ||
| 66 | - console.error(`[setup-test-db] 拒绝在非白名单 host (${DB_HOST}) 上执行 DROP DATABASE`) | ||
| 67 | - console.error(` 当前白名单:${allowedHosts.join(' ')}`) | ||
| 68 | - console.error(' 加入 host:在 .env.local 追加 TEST_DB_ALLOWED_HOSTS="<host1> <host2>"') | ||
| 69 | - process.exit(1) | ||
| 70 | -} | ||
| 71 | - | ||
| 72 | -// 防护 2:schema 名需像测试/开发库(含 test / _dev / _local / _ci),否则拒绝。 | ||
| 73 | -const schemaLooksLikeTest = | ||
| 74 | - /test/.test(DB_SCHEMA) || /_dev$/.test(DB_SCHEMA) || /_local$/.test(DB_SCHEMA) || /_ci$/.test(DB_SCHEMA) | ||
| 75 | -if (!schemaLooksLikeTest) { | ||
| 76 | - console.error( | ||
| 77 | - `[setup-test-db] schema '${DB_SCHEMA}' 不像测试库(期望命名含 test / _dev / _local / _ci)` | ||
| 78 | - ) | 71 | + console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`) |
| 79 | process.exit(1) | 72 | process.exit(1) |
| 80 | } | 73 | } |
| 81 | 74 | ||
| 82 | console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) | 75 | console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) |
| 83 | -if (!['localhost', '127.0.0.1', '::1'].includes(DB_HOST)) { | ||
| 84 | - console.log( | ||
| 85 | - '[setup-test-db] 目标是 **远程** host(已在 TEST_DB_ALLOWED_HOSTS 白名单中,每次 test.mjs 都会 DROP)' | ||
| 86 | - ) | ||
| 87 | - console.log(`[setup-test-db] 当前白名单: ${allowedHosts.join(' ')}`) | ||
| 88 | - console.log( | ||
| 89 | - '[setup-test-db] 若不希望每次自动 DROP,从 .env.local 的 TEST_DB_ALLOWED_HOSTS 删掉此 host' | ||
| 90 | - ) | ||
| 91 | -} | ||
| 92 | 76 | ||
| 93 | const sql = | 77 | const sql = |
| 94 | `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` + | 78 | `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` + |