Skip to content

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

FieldEnvหมายเหตุ
userDB_USER.env:1
passwordDB_PASSWORD.env:2
serverDB_SERVER.env:3
databaseDB_DATABASE.env:4 — ชื่อ database เดียวกับ AI DB ของ recruitment-api
options.encrypthardcode falsedb.js:11
options.trustServerCertificatetruedb.js:12
javascript
module.exports = { sql, dbConfig };

แหล่ง: ai-recruitment-api/config/db.js (branch: prod):1-19

ผู้ใช้ config

Controllerวิธีเชื่อมencrypt ที่ใช้จริง
generateJobDescriptionControllersql.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-apiDB_AI_NAME
การยืนยันค่าSHA-256 ของค่าทั้งสอง ตรงกัน บนเครื่องที่เขียนเอกสาร (ไม่ใส่ plaintext ในคู่มือ)
HostDB_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

sql
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

sql
SELECT TOP 1 *
FROM [jobReqPosition]
WHERE hdid = @JobID

แหล่ง: generateInterviewQuestionsController.js:79-85
ใช้ฟิลด์ responsibility, qualification จากแถว (:89-90)

dbo.appProfileHd

sql
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 ย่อบรรทัด
appProfileEduSELECT * ... WHERE hdid = @CandidateID:300-303
appProfileProLicenseSELECT * ... WHERE hdid = @CandidateID:311-314
appProfileExpSELECT * ... WHERE hdid = @CandidateID AND isDeleted = 0 ORDER BY startDate ASC:322-325
jobReqDegreeSELECT * ... WHERE hdid = @JobID:333-336
jobReqFieldSELECT * ... WHERE hdid = @JobID:344-347
appProfileCapabilityLangSELECT 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 ที่ไม่จำเป็นต้องมีในตารางหลัก

ข้อสรุปที่ยืนยันได้จากโค้ดฝั่งนี้:

  1. มีตาราง jobReqPosition ใน recruitment_ai
  2. มีคอลัมน์ AI เพิ่มใน SELECT ของ job description
  3. ไม่พบ INSERT/UPDATE/DELETE ใด ๆ ไปยังตารางเหล่านี้ใน ai-recruitment-api — service นี้ อ่านอย่างเดียว จาก MSSQL

ข้อสรุปที่ยัง UNVERIFIED:

  • ว่า sync จาก recruitmentrecruitment_ai ทำที่ไหน (ไม่มีใน repo นี้; ต้องตรวจฝั่ง recruitment-api / ETL ภายนอก)
  • schema drift ระหว่างสอง database

SQL injection posture

ทุก query ที่ตรวจในสอง controllers ใช้ request.input(...) แล้วค่อย query(sqlText) — ไม่พบ string interpolation ของค่าจาก HTTP เข้า SQL ในไฟล์เหล่านี้

ตัวอย่าง:

javascript
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

ControllerI/O หลัก
module2_1_get_background.jsอ่าน array ใน memory
module2_1_get_background_v2.jsเหมือนกัน (orphan)
module2_2_controller.jsOpenAI + DALL-E + remove.bg + allImages
combineJobPostingController.jsQR + Puppeteer
module_2_3_combine_image.jsเหมือน combine (orphan)
summarizeJob*.jsOpenAI อย่างเดียว
removeBgUploadController.jsremove.bg + filesystem
get-post-upload_image.jsสำเนา remove.bg (unused export)
uploadController.jsfilesystem ผ่าน 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-163mkdir ตรง ไม่ผ่าน 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

javascript
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 ภาพ
เพิ่มภาพใหม่ต้อง:

  1. วางไฟล์ใต้ storage-uploads/...
  2. เพิ่มแถวใน allImages.js
  3. commit ทั้งสอง (ปัจจุบันทีมทำแบบนี้)

ไม่มี migration / SQL folder

ไม่พบโฟลเดอร์ sql/, TypeORM migration, หรือ Sequelize migration ใน repo — schema ของ recruitment_ai ถูกจัดการนอก repo นี้ (น่าจะร่วมกับทีม recruitment / DBA)


Checklist ตอน debug ข้อมูล

  1. Job description ว่างทั้งที่มีตำแหน่งในระบบหลัก → ตรวจว่ามีแถวใน recruitment_ai.dbo.jobReqPosition ที่ positionCareerName ตรงกับ Position ที่ส่งมา (คนละ DB กับ recruitment)
  2. Interview ไม่ดึง JD → ตรวจ Job_ID ตรง hdid หรือไม่ (ไม่ใช่ id)
  3. Concern เรื่องการศึกษาไม่ขึ้น → ดูว่า Candidate_ID ตรง appProfileHd.id และ child tables ใช้ hdid
  4. ภาพ character first-time ไม่เจอ → ตรวจ allImages.js ว่ามี position ตรง keyword ที่ OpenAI คืน (เคส Office worker vs Office Worker ถูก normalize ด้วย toLowerCase ใน normalize())
  5. ไฟล์ upload หาไม่เจอใน browser → เทียบ path ที่ API คืนกับ static mount + JWT (static อาจยังต้องการ auth ถ้าไม่ตรงไฟล์ก่อน middleware — ดูลำดับใน 01)

ไป บท 4 Auth + integrations