feat(api): 开发平台、门店相关代码
This commit is contained in:
@@ -8,6 +8,7 @@ const migrationsDirectory = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'../migrations',
|
||||
);
|
||||
// 迁移只能使用拥有 schema/table 的专用角色,绝不允许复用 API 的最小权限连接。
|
||||
const connectionString = process.env.MIGRATOR_DATABASE_URL;
|
||||
|
||||
if (!connectionString) {
|
||||
@@ -24,6 +25,7 @@ async function migrate() {
|
||||
.sort();
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
// 迁移账本与业务 DDL 同库保存,用于阻止已应用 migration 被悄悄改写。
|
||||
await client.query('CREATE SCHEMA IF NOT EXISTS atlas');
|
||||
await client.query(`CREATE TABLE IF NOT EXISTS atlas.schema_migrations (
|
||||
filename TEXT PRIMARY KEY,
|
||||
@@ -41,6 +43,7 @@ async function migrate() {
|
||||
[filename],
|
||||
);
|
||||
if (applied.rowCount) {
|
||||
// 已应用文件的内容变化意味着历史与当前 schema 可能不一致,必须人工处理。
|
||||
if (applied.rows[0].checksum !== checksum)
|
||||
throw new Error(
|
||||
`Applied migration was modified: ${filename}`,
|
||||
@@ -49,6 +52,7 @@ async function migrate() {
|
||||
}
|
||||
await client.query('BEGIN');
|
||||
try {
|
||||
// 单个文件原子提交;失败时不会留下半份 DDL 或 migration 账本记录。
|
||||
await client.query(sql);
|
||||
await client.query(
|
||||
'INSERT INTO atlas.schema_migrations (filename, checksum) VALUES ($1, $2)',
|
||||
|
||||
@@ -11,7 +11,7 @@ export class Database {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
// atlas_app_login is NOINHERIT by design; application DML always uses the least-privileged group role.
|
||||
// atlas_app_login 是 NOINHERIT 登录角色;应用 DML 必须降为最小权限的 atlas_app。
|
||||
await client.query('SET LOCAL ROLE atlas_app');
|
||||
const result = await work(client);
|
||||
await client.query('COMMIT');
|
||||
@@ -24,13 +24,13 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/** Every tenant-scoped query must run through this method. */
|
||||
/** 每个带工作空间边界的业务读写都必须通过此方法执行。 */
|
||||
public async workspaceTransaction<T>(
|
||||
workspaceId: string,
|
||||
work: (transaction: DatabaseTransaction) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return this.transaction(async (transaction) => {
|
||||
// SET LOCAL is scoped to this transaction and cannot leak a tenant id through the pool.
|
||||
// SET LOCAL 仅作用于当前事务,连接归还池后不会泄漏租户 ID。
|
||||
await transaction.query(
|
||||
"SELECT set_config('app.workspace_id', $1, true)",
|
||||
[workspaceId],
|
||||
@@ -43,7 +43,7 @@ export class Database {
|
||||
text: string,
|
||||
values?: unknown[],
|
||||
) {
|
||||
// Even non-tenant reads use the restricted role; callers cannot accidentally use the login role's privileges.
|
||||
// 即使不带租户上下文的查询也使用受限角色,调用方不会误用登录角色权限。
|
||||
return this.transaction((transaction) =>
|
||||
transaction.query<Row>(text, values),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user