# ให้หัวหน้าอนุมัติก่อนเปลี่ยนแปลงฐานข้อมูล

> การอ่านทำงานทันที ทุกการเปลี่ยนแปลงจะรอจนหัวหน้าเห็น SQL ที่แน่นอนและจำนวนแถวโดยประมาณบนมือถือแล้วอนุมัติ จากนั้นจึงทำงานครั้งเดียว ฐานข้อมูลไม่ออกจากเซิร์ฟเวอร์ของคุณ

Source: https://hibira.io/th/use-cases/database-write-approvals

การให้พนักงาน สคริปต์ หรือผู้ช่วยค้นข้อมูลจากฐานข้อมูลได้เองช่วยประหยัดเวลาได้หลายชั่วโมง แต่ถ้าให้แก้ข้อมูลได้โดยไม่มีใครดู คำของ่าย ๆ อย่าง "ล้างตะกร้าเก่าทิ้ง" ก็อาจลบคำสั่งซื้อของสัปดาห์ที่แล้วไปได้ Hibira วางหัวหน้าไว้ตรงกลาง การอ่านข้อมูลทำงานทันที ส่วนการเปลี่ยนแปลงแต่ละครั้งต้องรอจนหัวหน้าเห็น SQL ที่แน่นอนและจำนวนแถวที่จะได้รับผลบนมือถือ แล้วกดอนุมัติ ฐานข้อมูลและข้อมูลการเชื่อมต่อไม่ออกจากเซิร์ฟเวอร์ของคุณ

## สิ่งที่ผู้ใช้ได้รับ

- คำถามที่อ่านข้อมูลอย่างเดียวได้คำตอบทันที ไม่ต้องรอใคร
- ทุกการเปลี่ยนแปลงมีหน้าอนุมัติที่หัวหน้าเปิดได้จากการแจ้งเตือนแบบพุชหรือคิวอาร์โค้ด แสดงคำสั่ง จำนวนแถวโดยประมาณ แผนการทำงาน และลายนิ้วมือ พร้อมปุ่มอนุมัติและปฏิเสธ
- ผลลัพธ์ขึ้นบนหน้าจอของผู้ขอทันทีที่หัวหน้าตัดสินใจ ไม่ต้องรีเฟรช ถ้าถูกปฏิเสธก็จะเห็นเหตุผลของหัวหน้า
- รับประกันว่าคำสั่งที่ทำงานคือคำสั่งที่ได้รับอนุมัติทุกตัวอักษร และทำงานเพียงครั้งเดียว

![คอนโซลเดโม: คำสั่ง DELETE กำลังรอหัวหน้าอนุมัติ พร้อมคิวอาร์โค้ด จำนวนแถวโดยประมาณ และ SHA-256 ของคำสั่ง](https://hibira.io/portal/articles/sql-approvals-console-th-light.webp)

## สร้างด้วย

- **[การอนุมัติ](https://hibira.io/th/guides/approvals)**: `POST /apps/APP_ID/approvals` (หรือ `request_approval` ของ [MCP server](https://hibira.io/th/mcp-server)) สร้างหน้าอนุมัติที่ Hibira โฮสต์ให้ และแจ้งผลการตัดสินใจบนช่อง
- **ช่องส่วนตัว** หนึ่งช่องต่อคอนโซลหรือต่อการทำงาน เช่น `private-sqlgate-<random>` อีเวนต์ `approval` จะมาถึงที่นี่ ไม่ต้องคอยถามซ้ำ
- **การแจ้งเตือนแบบพุช** (ไม่บังคับ) เพื่อส่งลิงก์ไปที่มือถือของหัวหน้า
- **[@hibira-io/approvals/sql](https://hibira.io/th/guides/approvals)**: `classifySql()` `sqlFingerprint()` และ `assertApprovedSql()` สำหรับ Node.js และมีแบบเดียวกันใน Python

```text
sequence: การอ่านทำงานทันที การเปลี่ยนแปลงทำงานหลังหัวหน้าอนุมัติเท่านั้น
ผู้ขอ | เซิร์ฟเวอร์ของคุณ | *Hibira | หัวหน้า
1 > 2: SELECT … | ทำงานแบบอ่านอย่างเดียว ได้ผลทันที
1 > 2: DELETE FROM carts … | ทดลองรัน: 10 แถว
2 > 3: POST /approvals | คำสั่ง จำนวนแถวโดยประมาณ SHA-256
3 > 4: การแจ้งเตือนแบบพุช | ลิงก์ไปหน้าอนุมัติ
4 > 3: อนุมัติ
3 > 1: อีเวนต์ approval | ช่องส่วนตัว ไม่ต้องถามซ้ำ
1 > 2: สั่งให้ทำงาน
2 > 3: GET /approvals/apr_… | ยืนยันคำตอบหนึ่งครั้ง
2 >> 1: ทำงานหนึ่งครั้ง: เปลี่ยน 10 แถว
```

ลองได้ที่ [demos.hibira.io](https://demos.hibira.io/examples/nextjs-sql-approvals/): เลือกการเปลี่ยนแปลง สแกนคิวอาร์โค้ดด้วยมือถือ แล้วกดอนุมัติ

## ขั้นที่ 1: ปล่อยการอ่านผ่าน หยุดทุกอย่างที่เหลือ

`classifySql()` จะถือว่าคำสั่งเป็นการอ่านก็ต่อเมื่อแน่ใจเท่านั้น คือมีคำสั่งเดียว ขึ้นต้นด้วย `SELECT` `WITH` `EXPLAIN` หรือ `SHOW` และไม่มีส่วนที่แก้ข้อมูล หลายคำสั่ง `WITH ... DELETE` `EXPLAIN ANALYZE` `SELECT ... INTO` `FOR UPDATE` และอะไรก็ตามที่แยกวิเคราะห์ได้ไม่แน่ชัด ถือเป็นการเปลี่ยนแปลงทั้งหมด ถึงอย่างนั้นก็ควรรันการอ่านในทรานแซกชันแบบอ่านอย่างเดียว คำสั่งที่ตัดสินผิดจะล้มเหลวแทนที่จะเขียนข้อมูล

```js
import pg from 'pg';
import { classifySql } from '@hibira-io/approvals/sql';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export async function runSql(sql, requestedBy) {
  if (classifySql(sql) === 'read') return { rows: await readOnly(sql) };
  return { approval: await askSupervisor(sql, requestedBy) };
}

async function readOnly(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN READ ONLY');
    const { rows } = await db.query(sql);
    await db.query('COMMIT');
    return rows.slice(0, 200);
  } catch (err) {
    await db.query('ROLLBACK');
    throw err;
  } finally {
    db.release();
  }
}
```

## ขั้นที่ 2: ให้หัวหน้าเห็นว่าการเปลี่ยนแปลงจะทำอะไร

ประมาณจำนวนแถวก่อนขออนุมัติ `EXPLAIN` ไม่รันคำสั่งเลย ส่วนการทดลองรันในทรานแซกชันที่ย้อนกลับให้จำนวนที่แม่นยำ แต่คำสั่งจะรันจริง จึงควรตั้งเวลาหมดอายุ แล้วสร้างลายนิ้วมือจากข้อความคำสั่ง การอนุมัติใช้ได้กับคำสั่งนี้เท่านั้น

```js
import { sqlFingerprint } from '@hibira-io/approvals/sql';

async function preview(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN');
    await db.query("SET LOCAL statement_timeout = '5s'");
    const { rowCount } = await db.query(sql);
    return { rows: rowCount, fingerprint: await sqlFingerprint(sql) };
  } finally {
    await db.query('ROLLBACK');
    db.release();
  }
}
```

## ขั้นที่ 3: ขออนุมัติ

สร้างคำขอโดยใส่คำสั่งและตัวอย่างผลไว้ใน `details` และระบุช่องที่จะแจ้งผลการตัดสินใจ เก็บคำสั่งและลายนิ้วมือไว้ฝั่งคุณโดยผูกกับ ID ของการอนุมัติ สิ่งที่ส่งไป Hibira มีเพียงข้อความคำขอ ไม่มีข้อมูลการเชื่อมต่อและไม่มีแถวข้อมูล

```js
import Pusher from 'pusher';

const hibira = new Pusher({ appId: process.env.HIBIRA_APP_ID, key: process.env.HIBIRA_KEY, secret: process.env.HIBIRA_SECRET, host: 'ws.hibira.io', useTLS: true });

async function askSupervisor(sql, requestedBy) {
  const { rows, fingerprint } = await preview(sql);
  const res = await hibira.post({
    path: '/approvals',
    body: {
      title: 'Run this database change?',
      details: `${sql}\n\nEstimated rows: ${rows}\n\nSHA-256: ${fingerprint}`,
      options: ['Approve', 'Reject'],
      channel: `private-sqlgate-${requestedBy.consoleId}`,
      push_users: ['supervisor-42'],
      expires_in_minutes: 15,
      agent: 'SQL console',
    },
  });
  const approval = await res.json();
  await pool.query('INSERT INTO pending_changes (approval_id, sql, fingerprint) VALUES ($1, $2, $3)', [approval.approval_id, sql, fingerprint]);
  return { id: approval.approval_id, url: approval.url, rows };
}
```

หัวหน้าจะได้รับการแจ้งเตือนแบบพุชพร้อมลิงก์ (หรือสแกนจากหน้าจอของผู้ขอ) แล้วตอบบนหน้าที่แสดงเป็นภาษาของตนเอง

![หน้าอนุมัติบนมือถือของหัวหน้า: คำสั่ง จำนวนแถวโดยประมาณ SHA-256 ปุ่มอนุมัติและปฏิเสธ](https://hibira.io/portal/articles/sql-approvals-page-th.webp)

## ขั้นที่ 4: เมื่อคำตอบมาถึง ให้ทำงานครั้งเดียว

การตัดสินใจมาถึงเป็นอีเวนต์ `approval` บนช่อง ให้มองว่าเป็นกริ่งประตู อ่านคำตอบจาก API หนึ่งครั้ง ตรวจว่าคำสั่งที่เก็บไว้ยังตรงกับลายนิ้วมือ แล้วจึงรัน การจองแถวไว้ก่อนทำให้คำสั่งรันเพียงครั้งเดียว แม้อีเวนต์หรือ worker สองตัวจะเข้ามาพร้อมกัน

```js
import { assertApprovedSql } from '@hibira-io/approvals/sql';

// In the requester's page (pusher-js), or a worker that keeps a connection open:
channel.bind('approval', ({ id, reason }) => {
  if (reason === 'decided') fetch(`/api/changes/${id}/run`, { method: 'POST' });
});

// POST /api/changes/:id/run on your server:
export async function runApproved(id) {
  const res = await hibira.get({ path: `/approvals/${id}` });
  const answer = await res.json();
  if (answer.status !== 'approved') return { status: answer.status, comment: answer.comment };
  const { rows } = await pool.query('UPDATE pending_changes SET ran_at = now() WHERE approval_id = $1 AND ran_at IS NULL RETURNING sql, fingerprint', [id]);
  if (!rows.length) return { status: 'already run' };
  await assertApprovedSql(rows[0].sql, rows[0].fingerprint);
  const result = await pool.query(rows[0].sql);
  return { status: 'ran', rows: result.rowCount };
}
```

![หลังอนุมัติ: คอนโซลแสดงว่าการเปลี่ยนแปลงทำงานหนึ่งครั้ง และเปลี่ยนไปกี่แถว](https://hibira.io/portal/articles/sql-approvals-done-th-light.webp)

## เชื่อมฐานข้อมูลผ่าน MCP server อยู่แล้วใช่ไหม

ถ้าทีมของคุณทำงานกับฐานข้อมูลจาก Claude, Cursor, ChatGPT หรือไคลเอนต์ MCP อื่น ผ่าน server อย่าง DBHub, postgres-mcp หรือ Supabase MCP เพียงวาง [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) ไว้ข้างหน้า server นั้น ไม่ต้องแก้โค้ดเลย การอ่านข้อมูลผ่านได้ทันที ส่วนการเพิ่ม แก้ไข ลบ หรือเปลี่ยนโครงสร้างตารางจะรออยู่ที่หน้าอนุมัติเดียวกัน แล้วรันเพียงครั้งเดียวตามที่ได้รับอนุมัติ

```sh
npx -y @hibira-io/mcp-approval-gate --label shop-production --push-interests db-approvals \
  -- npx -y @bytebase/dbhub --transport stdio --dsn "$DATABASE_URL"
```

![การใช้งานจริง: คำขอเปลี่ยนคำสั่งซื้อ 1042 เป็นคืนเงินแล้วจะรอก่อน หัวหน้าอนุมัติ UPDATE บนมือถือ แล้วการเปลี่ยนแปลงจึงรันครั้งเดียว](https://hibira.io/portal/articles/mcp-approval-gate-th.mp4)

## ด่านเดียวกันในเครื่องมือที่คุณใช้

- **Vercel AI SDK, Mastra, LangGraph (JavaScript และ Python)**: [@hibira-io/approvals](https://hibira.io/th/guides/approvals) มีสูตรสำหรับแต่ละตัว ให้ `needsApproval` หรือ `requireApproval` เป็นฟังก์ชันที่ตัดสินจากคำสั่ง และใช้กล่องอนุมัติในแอปของคุณเองที่แสดงตัวอย่างผลใต้การเรียกแต่ละครั้ง
- **Laravel**: [filament-approvals](https://hibira.io/th/guides/filament-approvals) ขออนุมัติภายในแผง Filament ของคุณ ใน README มีเครื่องมือ `RunSql` สำหรับ Laravel AI SDK
- **[n8n](https://hibira.io/th/guides/n8n), [Make](https://hibira.io/th/guides/make), [Activepieces](https://hibira.io/th/guides/activepieces) และ Dify**: ขั้นโค้ดแยกประเภทคำสั่ง ขั้นอนุมัติของ Hibira ขอหัวหน้าและรอคำตอบ และต่อขั้นฐานข้อมูลไว้เฉพาะทางอนุมัติ README ของแต่ละตัวมีเวิร์กโฟลว์ครบ
- **Claude, Cursor และไคลเอนต์ MCP อื่น ๆ**: `request_approval` ของ [MCP server](https://hibira.io/th/mcp-server) ขออนุมัติแบบเดียวกันได้จากในแชต
- **database MCP server ใดก็ได้**: [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) ครอบ DBHub, postgres-mcp, Supabase MCP และอื่น ๆ ได้โดยไม่ต้องแก้โค้ด
