refactor(stage7): 后端目录分层、架构守护测试与文档
后端目录 - db/:mysql / hydrogen / heatmap 连接与氢能只读 SQL 守卫收拢到一处。 - middleware/:auth(JWT → 注入 user)与 read-only 归位。 - 相关测试随文件移动(middleware/read-only.test.ts、db/hydrogen-read-only.test.ts)。 import 改写由一次性 codemod 完成,未改变任何逻辑。 架构守护 - 新增 src/architecture.test.ts,断言 6 条分层铁律: server 不依赖 modules、前端不依赖 server、shared 为叶子层、 无 vendor 引用、model.ts 不依赖 react、@ts-nocheck 仅限已登记的原型快照。 豁免清单只减不增。 测试基建 - npm test 的 glob 同时匹配 .test.ts 与 .test.tsx(此前 .tsx 测试会被静默漏掉)。 文档 - 新增根 README.md:入口、快速开始、命令、目录、部署注意事项 (JWT_SECRET 必须注入且 >=32 字符,否则拒绝启动)。 - 新增 docs/ARCHITECTURE.md:依赖方向、6 条硬规则、业务域形状、 中间件顺序、新增模块步骤,以及"已知未尽事项"的诚实清单。 lint / test(134) / build 全绿。
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import dotenv from 'dotenv';
|
||||
import pg from 'pg';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
const heatmapPool = new Pool({
|
||||
host: process.env.HEATMAP_DB_HOST,
|
||||
port: Number(process.env.HEATMAP_DB_PORT) || 5432,
|
||||
user: process.env.HEATMAP_DB_USER,
|
||||
password: process.env.HEATMAP_DB_PASSWORD,
|
||||
database: process.env.HEATMAP_DB_NAME,
|
||||
ssl: process.env.HEATMAP_DB_SSL === 'true'
|
||||
? { rejectUnauthorized: false }
|
||||
: false,
|
||||
max: 5,
|
||||
idleTimeoutMillis: 30_000,
|
||||
connectionTimeoutMillis: 10_000,
|
||||
statement_timeout: 30_000,
|
||||
application_name: 'ln-bi-vehicle-heatmap',
|
||||
options: '-c timezone=Asia/Shanghai -c default_transaction_read_only=on',
|
||||
});
|
||||
|
||||
heatmapPool.on('error', (error) => {
|
||||
console.error('[vehicle-heatmap] idle PostgreSQL client error:', error);
|
||||
});
|
||||
|
||||
export default heatmapPool;
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { assertHydrogenReadOnlySql } from './hydrogen-read-only.js';
|
||||
|
||||
test('只读模式允许查询语句', () => {
|
||||
for (const sql of ['SELECT 1', ' SHOW TABLES', 'WITH rows AS (SELECT 1) SELECT * FROM rows', 'EXPLAIN SELECT 1']) {
|
||||
assert.doesNotThrow(() => assertHydrogenReadOnlySql(sql, true));
|
||||
}
|
||||
});
|
||||
|
||||
test('只读模式拒绝数据库写入语句', () => {
|
||||
for (const sql of ['INSERT INTO ledger VALUES (1)', 'UPDATE ledger SET value = 1', 'DELETE FROM ledger', 'ALTER TABLE ledger ADD value INT']) {
|
||||
assert.throws(
|
||||
() => assertHydrogenReadOnlySql(sql, true),
|
||||
/HYDROGEN_DB_READ_ONLY blocks non-read SQL/,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
const READ_ONLY_SQL = /^\s*(SELECT|SHOW|WITH|EXPLAIN)\b/i;
|
||||
|
||||
export function assertHydrogenReadOnlySql(statement: string, enabled: boolean) {
|
||||
if (enabled && !READ_ONLY_SQL.test(statement)) {
|
||||
throw new Error('HYDROGEN_DB_READ_ONLY blocks non-read SQL');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
import { assertHydrogenReadOnlySql } from './hydrogen-read-only.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const rawHydrogenPool = mysql.createPool({
|
||||
// 氢能账本归属线上业务库。保留专用变量,便于未来拆分独立只读库;
|
||||
// 未配置专用变量时,复用主业务库,避免部署环境误回退到历史数据库地址。
|
||||
host: process.env.HYDROGEN_DB_HOST || process.env.DB_HOST,
|
||||
port: Number(process.env.HYDROGEN_DB_PORT || process.env.DB_PORT) || 3306,
|
||||
user: process.env.HYDROGEN_DB_USER || process.env.DB_USER,
|
||||
password: process.env.HYDROGEN_DB_PASSWORD || process.env.DB_PASSWORD,
|
||||
database: process.env.HYDROGEN_DB_NAME || process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 5,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
const rawQuery = rawHydrogenPool.query.bind(rawHydrogenPool) as typeof rawHydrogenPool.query;
|
||||
const hydrogenPool = {
|
||||
query: ((sql: unknown, values?: unknown) => {
|
||||
const statement = typeof sql === 'string' ? sql : String((sql as { sql?: unknown })?.sql ?? '');
|
||||
assertHydrogenReadOnlySql(statement, process.env.HYDROGEN_DB_READ_ONLY === '1');
|
||||
return rawQuery(sql as never, values as never);
|
||||
}) as typeof rawHydrogenPool.query,
|
||||
};
|
||||
|
||||
export default hydrogenPool;
|
||||
@@ -0,0 +1,17 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
export default pool;
|
||||
Reference in New Issue
Block a user