3 · Persistence
ai-recruitment-api ไม่มี ORM และไม่มี entity class — เข้าถึง MSSQL ผ่าน driver mssql ด้วย sql.connect() + parameterized request.input ในเฉพาะ controllers ที่ต้องการข้อมูลงาน/ผู้สมัคร ส่วนใหญ่ของ repo เป็น stateless AI/file I/O
แผนที่: 01 · pipeline ที่แตะ DB: 02
Connection config
ศูนย์กลาง — config/db.js
| Field | Env | หมายเหตุ |
|---|---|---|
user | DB_USER | .env:1 |
password | DB_PASSWORD | .env:2 |
server | DB_SERVER | .env:3 |
database | DB_DATABASE | .env:4 — ชื่อ database เดียวกับ AI DB ของ recruitment-api |
options.encrypt | hardcode false | db.js:11 |
options.trustServerCertificate | true | db.js:12 |
module.exports = { sql, dbConfig };แหล่ง: ai-recruitment-api/config/db.js (branch: prod):1-19
ผู้ใช้ config
| Controller | วิธีเชื่อม | encrypt ที่ใช้จริง |
|---|---|---|
generateJobDescriptionController | sql.connect({ ...dbConfig, options: { encrypt: true, ... }}) | true (override) — :128-135 |
generateInterviewQuestionsController | สร้าง object เองจาก process.env.DB_* | false — :29-42 |
| controllers อื่นทั้งหมด | ไม่เชื่อม DB | — |
Shared database กับ recruitment-api
| หลักฐาน | รายละเอียด |
|---|---|
| Env key ฝั่งนี้ | DB_DATABASE (.env:4) |
| Env key ฝั่ง recruitment-api | DB_AI_NAME |
| การยืนยันค่า | SHA-256 ของค่าทั้งสอง ตรงกัน บนเครื่องที่เขียนเอกสาร (ไม่ใส่ plaintext ในคู่มือ) |
| Host | DB_SERVER ตรง hash กับ DB_AI_HOST ของ recruitment-api |
สรุปสถาปัตยกรรม: recruitment-api มี connection ชื่อ ai ชี้ database เดียวกับที่ service นี้ใช้เป็น primary — shared-database coupling ไม่ผ่าน HTTP API ระหว่างสอง service
ตารางที่ยืนยันจากโค้ด
dbo.jobReqPosition
ใช้สองแบบคนละคีย์ค้นหา:
A) โดยชื่อตำแหน่ง — job description
SELECT TOP (1)
[id], [positionCareerName], [gender], [ageFrom], [ageTo],
[experience], [responsibility], [qualification],
[isAiDescription], [isDescription],
[aiResponsibility], [aiQualification], [aiDescription]
FROM [dbo].[jobReqPosition]
WHERE [positionCareerName] = @Positionแหล่ง: generateJobDescriptionController.js:138-159
Parameter: @Position เป็น sql.NVarChar(255) (:158)
คอลัมน์ AI-specific ที่ SELECT ชัดเจน: isAiDescription, isDescription, aiResponsibility, aiQualification, aiDescription — บ่งชี้ว่าตารางนี้ถูกขยายเพื่องาน AI
B) โดย hdid — interview questions
SELECT TOP 1 *
FROM [jobReqPosition]
WHERE hdid = @JobIDแหล่ง: generateInterviewQuestionsController.js:79-85
ใช้ฟิลด์ responsibility, qualification จากแถว (:89-90)
dbo.appProfileHd
SELECT TOP 1 *
FROM [appProfileHd]
WHERE id = @CandidateIDแหล่ง: generateInterviewQuestionsController.js:56-62
Parameter: @CandidateID เป็น sql.Int
ตารางโปรไฟล์ / job req แบบ fully-qualified
ทุก query ด้านล่างอยู่ใน generateInterviewQuestionsController.js และระบุ catalog [recruitment_ai].[dbo].[...] ตรง ๆ — ยืนยันชื่อ database ใน SQL string ด้วย
| ตาราง | SQL ย่อ | บรรทัด |
|---|---|---|
appProfileEdu | SELECT * ... WHERE hdid = @CandidateID | :300-303 |
appProfileProLicense | SELECT * ... WHERE hdid = @CandidateID | :311-314 |
appProfileExp | SELECT * ... WHERE hdid = @CandidateID AND isDeleted = 0 ORDER BY startDate ASC | :322-325 |
jobReqDegree | SELECT * ... WHERE hdid = @JobID | :333-336 |
jobReqField | SELECT * ... WHERE hdid = @JobID | :344-347 |
appProfileCapabilityLang | SELECT languageName, speakingName, listeningName, readingName ... WHERE hdid = @ProfileID AND isDeleted = 0 | :356-362 |
Comment ในโค้ดอ้าง View_Job_Breakdown (:369) แต่บล็อกถัดไปว่าง — ไม่พบ query จริงไปยัง view นี้ ณ เวอร์ชันที่อ่าน
สิ่งที่ตารางเหล่านี้บอกเรื่อง dual-schema
recruitment-api มี entity JobReqPosition ใน database หลัก recruitment (ดู volume ของ recruitment-api) ในขณะที่ service นี้ทำงานบน recruitment_ai และ SELECT คอลัมน์ AI ที่ไม่จำเป็นต้องมีในตารางหลัก
ข้อสรุปที่ยืนยันได้จากโค้ดฝั่งนี้:
- มีตาราง
jobReqPositionในrecruitment_ai - มีคอลัมน์ AI เพิ่มใน SELECT ของ job description
- ไม่พบ INSERT/UPDATE/DELETE ใด ๆ ไปยังตารางเหล่านี้ใน
ai-recruitment-api— service นี้ อ่านอย่างเดียว จาก MSSQL
ข้อสรุปที่ยัง UNVERIFIED:
- ว่า sync จาก
recruitment→recruitment_aiทำที่ไหน (ไม่มีใน repo นี้; ต้องตรวจฝั่ง recruitment-api / ETL ภายนอก) - schema drift ระหว่างสอง database
SQL injection posture
ทุก query ที่ตรวจในสอง controllers ใช้ request.input(...) แล้วค่อย query(sqlText) — ไม่พบ string interpolation ของค่าจาก HTTP เข้า SQL ในไฟล์เหล่านี้
ตัวอย่าง:
request.input('Position', sql.NVarChar(255), PositionSafe);
const queryResult = await request.query(queryText);แหล่ง: generateJobDescriptionController.js:157-159
ต่างจากบางจุดใน benefit-api / 2way-api ที่ยังมี raw interpolation — มิติ persistence ของ repo นี้ได้คะแนนดีใน scorecard ด้วยเหตุนี้
Controllers ที่ไม่แตะ database
| Controller | I/O หลัก |
|---|---|
module2_1_get_background.js | อ่าน array ใน memory |
module2_1_get_background_v2.js | เหมือนกัน (orphan) |
module2_2_controller.js | OpenAI + DALL-E + remove.bg + allImages |
combineJobPostingController.js | QR + Puppeteer |
module_2_3_combine_image.js | เหมือน combine (orphan) |
summarizeJob*.js | OpenAI อย่างเดียว |
removeBgUploadController.js | remove.bg + filesystem |
get-post-upload_image.js | สำเนา remove.bg (unused export) |
uploadController.js | filesystem ผ่าน file-util |
File system persistence
Runtime write roots
| Path | ผู้เขียน | หมายเหตุ |
|---|---|---|
storage/{YYYY-MM-DD}/{folder}/ | utils/file-util.js writeFileAsync / makeDir | ใช้โดย removeBgService, uploadController |
storage/{date}/Module2_3/ | removeBgUploadController.js:155-163 | mkdir ตรง ไม่ผ่าน file-util |
public/{IMAGE_OUT_DIR}/ (default generated) | config/dalle.js:57-75 | เมื่อ Images API คืน b64_json |
storage-uploads/ | mkdir ตอน boot ถ้าไม่มี (server.js:12-15) | คลังภาพ commit ไว้แล้ว; static serve |
Static HTTP mapping
app.use("/ai-recruitment-api/api", express.static(..., "storage"));
app.use("/ai-recruitment-api/api", express.static(..., "storage-uploads"));แหล่ง: server.js:18-19
ดังนั้น URL ตัวอย่าง:
- ภาพคลัง:
/ai-recruitment-api/api/2025-02-06/uploads/c1.png - พื้นหลัง:
/ai-recruitment-api/api/Background/b1.png - ผล remove.bg upload:
/ai-recruitment-api/api/storage/...หรือ path ที่ controller คืนเป็น/storage/...— ต้องเทียบว่า client ต่อ prefix อย่างไร (ค่าที่ controller คืนคือ/storage/...ที่removeBgUploadController.js:165)
Gitignore vs tracked assets
| Path | .gitignore | สถานะใน git (prod) |
|---|---|---|
storage/ | ละเว้น (.gitignore มี storage/) | runtime writes ไม่ควรเข้า git |
storage-uploads/ | ไม่ ละเว้น | ภาพสำเร็จรูปถูก track (หลายร้อยไฟล์) |
logs/ | ละเว้น | logger เขียนที่นี่ |
.env | ละเว้นใน gitignore แต่ blob ยังถูก track | ดู 04 / 06 |
data/allImages.js ในฐานะ "catalog persistence"
ไม่ใช่ DB แต่เป็น source of truth ของ mapping ตำแหน่งงาน → path ภาพ
เพิ่มภาพใหม่ต้อง:
- วางไฟล์ใต้
storage-uploads/... - เพิ่มแถวใน
allImages.js - commit ทั้งสอง (ปัจจุบันทีมทำแบบนี้)
ไม่มี migration / SQL folder
ไม่พบโฟลเดอร์ sql/, TypeORM migration, หรือ Sequelize migration ใน repo — schema ของ recruitment_ai ถูกจัดการนอก repo นี้ (น่าจะร่วมกับทีม recruitment / DBA)
Checklist ตอน debug ข้อมูล
- Job description ว่างทั้งที่มีตำแหน่งในระบบหลัก → ตรวจว่ามีแถวใน
recruitment_ai.dbo.jobReqPositionที่positionCareerNameตรงกับPositionที่ส่งมา (คนละ DB กับrecruitment) - Interview ไม่ดึง JD → ตรวจ
Job_IDตรงhdidหรือไม่ (ไม่ใช่id) - Concern เรื่องการศึกษาไม่ขึ้น → ดูว่า
Candidate_IDตรงappProfileHd.idและ child tables ใช้hdid - ภาพ character first-time ไม่เจอ → ตรวจ
allImages.jsว่ามีpositionตรง keyword ที่ OpenAI คืน (เคสOffice workervsOffice Workerถูก normalize ด้วยtoLowerCaseในnormalize()) - ไฟล์ upload หาไม่เจอใน browser → เทียบ path ที่ API คืนกับ static mount + JWT (static อาจยังต้องการ auth ถ้าไม่ตรงไฟล์ก่อน middleware — ดูลำดับใน 01)