-first commit

This commit is contained in:
2025-11-11 12:36:06 +07:00
commit b99c214434
5683 changed files with 713336 additions and 0 deletions

19
@template/.env Normal file
View File

@@ -0,0 +1,19 @@
#project
PJ_NAME=exthernal-mobile-api
# database
PG_HOST=localhost
PG_USER=postgres
PG_PASS=1234
PG_DB=postgres
PG_PORT=5432
# JWT-TOKENS
JWT_SECRET=MY_SUPER_SECRET
# DEV_HINT
DEVHINT=true
DEVHINT_LEVEL=3
#PORT
PORT=4000

29
@template/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run API (Nodemon Debug)",
"type": "node",
"request": "launch",
"runtimeExecutable": "nodemon",
"program": "${workspaceFolder}/src/app.js",
"restart": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"envFile": "${workspaceFolder}/.env",
"cwd": "${workspaceFolder}",
"runtimeArgs": ["--inspect=9229"],
"skipFiles": ["<node_internals>/**"],
// "env": {
// "PJ_NAME": "exthernal-mobile-api",
// "PG_HOST": "localhost",
// "PG_USER": "postgres",
// "PG_PASS": "1234",
// "PG_DB": "postgres",
// "PG_PORT": "5432",
// "JWT_SECRET": "MY_SUPER_SECRET",
// "PORT": "4000"
// }
}
]
}

22
@template/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "exthernal-mobile-api",
"version": "1.0.0",
"description": "External Mobile API following Nuttakit Controller Pattern vFinal",
"type": "module",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js"
},
"author": "Nuttakit Pothong",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.19.2",
"pg": "^8.12.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}

32
@template/src/app.js Normal file
View File

@@ -0,0 +1,32 @@
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import router from './routes/route.js'
import { validateJsonFormat } from './middlewares/validate.js'
dotenv.config()
const app = express()
app.use(cors())
// ✅ ตรวจจับ JSON format error ก่อน parser
app.use(express.json({ limit: '10mb' }))
app.use(validateJsonFormat)
app.use('/api', router)
// middleware จัดการ error กลาง
app.use((err, req, res, next) => {
if (err instanceof OftenError) {
res.status(err.statusCode).json({
type: err.type,
messageTh: err.messageTh,
messageEn: err.messageEn
});
} else {
res.status(500).json({ message: "Unexpected error" });
}
});
app.listen(process.env.PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${process.env.PORT}`)
})

View File

@@ -0,0 +1,13 @@
import pkg from 'pg'
import dotenv from 'dotenv'
dotenv.config()
const { Pool } = pkg
export const connection = new Pool({
host: process.env.PG_HOST,
user: process.env.PG_USER,
password: process.env.PG_PASS,
database: process.env.PG_DB,
port: process.env.PG_PORT,
})

View File

@@ -0,0 +1,43 @@
import { userService } from '../services/userservice.js'
import { trim_all_array } from '../utils/trim.js'
import { sendResponse } from '../utils/response.js'
export function userController() {
async function onNavigate(req, res) {
const database = req.body.organization
if (!database) throw new Error('Missing organization')
const prommis = await onUserController(req, res, database)
return prommis
}
async function onUserController(req, res, database) {
let idx = -1
let result
const { usrnam, usreml } = req.body
try {
result = await userService.createUser(database, usrnam, usreml)
}
catch (err) {
idx = 1
}
finally {
if (idx === 1) {
return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error')
}
trim_all_array(result)
const array_diy = {
result,
count: result.length,
timestamp: new Date().toISOString(),
}
return array_diy
}
}
return { onNavigate }
}

View File

@@ -0,0 +1,24 @@
import { sendResponse } from '../utils/response.js'
/**
* ✅ Middleware สำหรับตรวจสอบความถูกต้องของ JSON body
* ป้องกัน body-parser crash (SyntaxError)
*/
export function validateJsonFormat(err, req, res, next) {
if (err instanceof SyntaxError && 'body' in err) {
console.error('[Invalid JSON Format]', err.message)
return sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
}
next()
}
// /**
// * ✅ ตรวจสอบ body/query/params ว่ามีค่า organization หรือไม่
// */
// export function validateRequest(req, res, next) {
// const { organization } = req.body || {}
// if (!organization) {
// return sendResponse(res, 400, 'ไม่พบค่า organization', 'Missing organization')
// }
// next()
// }

View File

@@ -0,0 +1,14 @@
import express from 'express'
const router = express.Router()
// import { userController } from '../controllers/userController.js'
///////////////////////////////////////////////////////////////////////////
// const controller_user_post = userController()
///////////////////////////////////////////////////////////////////////////
// router.post('/user', async (req, res) => { const data = await controller_user_post.onNavigate(req, res); if (data) return sendResponse(res, 200, null, null, data)})
export default router

View File

@@ -0,0 +1,13 @@
import { executeQueryParam } from '../share/generalservice.js'
export const userService = {
async createUser(database, usrnam, usreml) {
const sql = `
SELECT * FROM ${database}.usrmst
WHERE usrnam = $1 OR usreml = $2
`
const params = [usrnam, usreml]
const result = await executeQueryParam(sql, database, params)
return result
},
}

View File

@@ -0,0 +1,179 @@
import { connection } from '../config/db.js'
import dotenv from 'dotenv'
dotenv.config()
// ===================================================
// 🧩 Internal DevHint System
// ===================================================
function devhint(level, location, message, extra = null) {
const isEnabled = process.env.DEVHINT === 'true'
const currentLevel = parseInt(process.env.DEVHINT_LEVEL || '1', 10)
if (!isEnabled || level > currentLevel) return
const timestamp = new Date().toISOString()
const prefix = `🧩 [DEVHINT:${location}]`
const formatted = `${prefix}${message} (${timestamp})`
if (extra) console.log(formatted, '\n', extra)
else console.log(formatted)
}
// ===================================================
// ✅ executeQueryConditions()
// ===================================================
export async function executeQueryConditions(database, baseQuery, conditions = {}) {
devhint(2, 'generalservice.js', 'executeQueryConditions() start')
let whereClauses = []
let params = []
let idx = 1
for (const [key, value] of Object.entries(conditions)) {
if (value === undefined || value === null || value === '') continue
const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i)
if (match) {
const operator = match[1].toUpperCase()
const pattern = match[2].trim()
whereClauses.push(`${key} ${operator} $${idx}`)
params.push(pattern)
} else {
whereClauses.push(`${key} = $${idx}`)
params.push(value)
}
idx++
}
let finalQuery = baseQuery
if (whereClauses.length > 0) {
finalQuery += ' AND ' + whereClauses.join(' AND ')
}
const formattedSQL = finalQuery.replace(/\${database}/g, database)
// 🧩 แสดงเฉพาะเมื่อ DEVHINT_LEVEL >= 2
devhint(2, 'executeQueryConditions', `📤 Executing Query`, {
database,
sql: formattedSQL,
params
})
const result = await connection.query(formattedSQL, params)
devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
}
// ===================================================
// ✅ executeQueryParam()
// ===================================================
export async function executeQueryParam(database, sql, params = []) {
const formattedSQL = sql.replace(/\${database}/g, database)
devhint(2, 'executeQueryParam', `📤 Executing Query`, {
database,
sql: formattedSQL,
params
})
const result = await connection.query(formattedSQL, params)
devhint(2, 'executeQueryParam', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
}
// ===================================================
// Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย
// ===================================================
export { devhint }
// /**
// * ✅ executeQueryParam (ของเดิม)
// * ใช้กับ SQL + database schema + params
// */
// export async function executeQueryParam(sql, database, params = []) {
// try {
// if (!database) throw new Error('Database is not defined')
// const formattedSQL = sql.replace(/\${database}/g, database)
// console.log(`[DB:${database}] → ${formattedSQL}`)
// const result = await connection.query(formattedSQL, params)
// return result.rows
// } catch (err) {
// console.error('[executeQueryParam Error]', err.message)
// throw err
// }
// }
/**
* ✅ executeQueryConditions (ใหม่)
* ใช้สร้าง WHERE อัตโนมัติจาก object เงื่อนไข
* ตัวที่ไม่มีค่า (null, undefined, '') จะไม่ถูกนำมาสร้างใน WHERE
*/
// export async function executeQueryConditions(database, baseQuery, conditions = {}) {
// try {
// if (!database) throw new Error('Database is not defined')
// let whereClauses = []
// let params = []
// let idx = 1
// for (const [key, value] of Object.entries(conditions)) {
// if (value === undefined || value === null || value === '') continue
// // ✅ ตรวจว่า value มีคำว่า LIKE หรือ ILIKE ไหม
// const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i)
// if (match) {
// const operator = match[1].toUpperCase()
// const pattern = match[2].trim()
// whereClauses.push(`${key} ${operator} $${idx}`)
// params.push(pattern)
// } else {
// whereClauses.push(`${key} = $${idx}`)
// params.push(value)
// }
// idx++
// }
// let finalQuery = baseQuery
// if (whereClauses.length > 0) {
// finalQuery += ' AND ' + whereClauses.join(' AND ')
// }
// const formattedSQL = finalQuery.replace(/\${database}/g, database)
// console.log(`[DB:${database}] → ${formattedSQL}`)
// const result = await connection.query(formattedSQL, params)
// return result.rows
// } catch (err) {
// console.error('[executeQueryConditions Error]', err.message)
// throw err
// }
// }
// /**
// * 🧩 devhint — Debug tracer ที่เปิดปิดได้จาก .env
// * @param {string} fileName - ชื่อไฟล์หรือโมดูล (เช่น 'usercontroller.js')
// * @param {string} message - ข้อความหรือจุดใน flow (เช่น 'onNavigate')
// * @param {object|string} [extra] - ข้อมูลเพิ่มเติม (optional)
// */
// export function devhint(fileName, message, extra = null) {
// if (process.env.DEVHINT === 'true') {
// const timestamp = new Date().toISOString()
// const prefix = `🧩 [DEVHINT:${fileName}]`
// const formatted = `${prefix} → ${message} (${timestamp})`
// if (extra) console.log(formatted, '\n', extra)
// else console.log(formatted)
// }
// }

View File

@@ -0,0 +1,40 @@
// utils/errorList.js
export function manualError(key) {
switch (key) {
case "invalid_input":
return {
code: 400,
messageTh: "ข้อมูลที่ส่งมาไม่ถูกต้อง",
messageEn: "Invalid input data"
};
case "not_found":
return {
code: 404,
messageTh: "ไม่พบข้อมูลที่ร้องขอ",
messageEn: "Resource not found"
};
case "unauthorized":
return {
code: 401,
messageTh: "คุณไม่มีสิทธิ์เข้าถึงข้อมูลนี้",
messageEn: "Unauthorized access"
};
case "server_error":
return {
code: 500,
messageTh: "เกิดข้อผิดพลาดภายในระบบ",
messageEn: "Internal server error"
};
default:
return {
code: 500,
messageTh: "ข้อผิดพลาดที่ไม่ทราบสาเหตุ",
messageEn: "Unknown error occurred"
};
}
}

View File

@@ -0,0 +1,41 @@
// utils/oftenError.js
import { manualError } from "./errorList.js";
export class OftenError extends Error {
/**
* ใช้ได้ 2 แบบ:
* 1. throw new OftenError("not_found")
* 2. throw new OftenError(400, "ไทย", "English")
*/
constructor(arg1, arg2, arg3) {
// แบบ lookup จาก key
if (typeof arg1 === "string" && !arg2 && !arg3) {
const found = manualError(arg1);
super(found.messageEn);
this.statusCode = found.code;
this.messageTh = found.messageTh;
this.messageEn = found.messageEn;
this.key = arg1;
}
// แบบ manual
else if (typeof arg1 === "number" && arg2 && arg3) {
super(arg3);
this.statusCode = arg1;
this.messageTh = arg2;
this.messageEn = arg3;
this.key = "manual";
}
// fallback
else {
super("Invalid error format");
this.statusCode = 500;
this.messageTh = "รูปแบบการสร้าง error ไม่ถูกต้อง";
this.messageEn = "Invalid error constructor format";
this.key = "invalid_format";
}
this.name = "OftenError";
}
}

View File

@@ -0,0 +1,29 @@
/**
* sendResponse
* ----------------------------------------------
* ส่ง response แบบมาตรฐาน รองรับข้อความ 2 ภาษา
* ----------------------------------------------
* @param {object} res - Express response object
* @param {number} status - HTTP Status code (200, 400, 500, etc.)
* @param {string} msg_th - ข้อความภาษาไทย
* @param {string} msg_en - ข้อความภาษาอังกฤษ
* @param {any} [data=null] - optional data
*/
export function sendResponse(res, status = 200, msg_th = null, msg_en = null, data = null) {
const isError = status >= 400
// ✅ ถ้าไม่ใช่ error และไม่มีข้อความ → ใช้ข้อความ default
const message_th = msg_th || (isError ? 'เกิดข้อผิดพลาด' : 'สำเร็จ')
const message_en = msg_en || (isError ? 'Error occurred' : 'Succeed')
const response = {
status: isError ? 'error' : 'succeed',
message: {
th: message_th,
en: message_en
},
data
}
res.status(status).json(response)
}

View File

@@ -0,0 +1,11 @@
export function trim_all_array(data) {
if (!Array.isArray(data)) return data
for (let row of data) {
for (let key in row) {
if (typeof row[key] === 'string') {
row[key] = row[key].trim()
}
}
}
return data
}