Stage after merging with project files
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler.js';
|
||||
import configRoutes from './routes/configRoutes.js';
|
||||
import healthRoutes from './routes/healthRoutes.js';
|
||||
import lookupRoutes from './routes/lookupRoutes.js';
|
||||
import templateRoutes from './routes/templateRoutes.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
res.json({
|
||||
service: 'check-list-poc-api',
|
||||
version: '0.1.0',
|
||||
description: 'PoC API for template and configuration delivery.'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/health', healthRoutes);
|
||||
app.use('/api/templates', templateRoutes);
|
||||
app.use('/api/lookups', lookupRoutes);
|
||||
app.use('/api/config', configRoutes);
|
||||
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,23 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const requiredKeys = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
|
||||
|
||||
for (const key of requiredKeys) {
|
||||
if (!process.env[key]) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const env = {
|
||||
port: Number(process.env.PORT || 3000),
|
||||
db: {
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
connectionLimit: Number(process.env.DB_CONNECTION_LIMIT || 5)
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
const mariadb = require("mariadb");
|
||||
|
||||
const pool = mariadb.createPool({
|
||||
host: process.env.DB_HOST || "db",
|
||||
port: Number(process.env.DB_PORT || 3306),
|
||||
user: process.env.DB_USER || "app_user",
|
||||
password: process.env.DB_PASSWORD || "app_password",
|
||||
database: process.env.DB_NAME || "app_db",
|
||||
connectionLimit: 5
|
||||
});
|
||||
|
||||
async function executeQuery(sql, params = []) {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
connection = await pool.getConnection();
|
||||
return await connection.query(sql, params);
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function closePool() {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
closePool,
|
||||
executeQuery
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as mariadb from 'mariadb';
|
||||
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
const pool = mariadb.createPool({
|
||||
host: env.db.host,
|
||||
port: env.db.port,
|
||||
database: env.db.database,
|
||||
user: env.db.user,
|
||||
password: env.db.password,
|
||||
connectionLimit: env.db.connectionLimit,
|
||||
bigIntAsNumber: true
|
||||
});
|
||||
|
||||
export async function query(sql, params = []) {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
connection = await pool.getConnection();
|
||||
return await connection.query(sql, params);
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function closePool() {
|
||||
await pool.end();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function notFoundHandler(_req, res) {
|
||||
res.status(404).json({ message: 'Route not found.' });
|
||||
}
|
||||
|
||||
export function errorHandler(error, _req, res, _next) {
|
||||
const statusCode = error.statusCode || 500;
|
||||
|
||||
if (statusCode >= 500) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
message: error.message || 'Unexpected server error.'
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import {
|
||||
getAppConfig,
|
||||
getExportProfile,
|
||||
getImageRules
|
||||
} from '../services/configService.js';
|
||||
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/image-rules',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const imageRules = await getImageRules();
|
||||
|
||||
if (!imageRules) {
|
||||
return res.status(404).json({ message: 'Image rules not found.' });
|
||||
}
|
||||
|
||||
res.json(imageRules);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/export',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const exportProfile = await getExportProfile();
|
||||
|
||||
if (!exportProfile) {
|
||||
return res.status(404).json({ message: 'Export profile not found.' });
|
||||
}
|
||||
|
||||
res.json(exportProfile);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/app-config',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const config = await getAppConfig();
|
||||
res.json({ items: config });
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import { query } from '../db/pool.js';
|
||||
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (_req, res) => {
|
||||
await query('SELECT 1 AS ok');
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'check-list-poc-api',
|
||||
database: 'connected'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import { getLookup, listLookups } from '../services/lookupService.js';
|
||||
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const lookups = await listLookups();
|
||||
res.json({ items: lookups });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:lookupCode',
|
||||
asyncHandler(async (req, res) => {
|
||||
const lookup = await getLookup(req.params.lookupCode);
|
||||
|
||||
if (!lookup) {
|
||||
return res.status(404).json({ message: 'Lookup not found.' });
|
||||
}
|
||||
|
||||
return res.json(lookup);
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import {
|
||||
getActiveTemplate,
|
||||
getTemplateVersion,
|
||||
listTemplates
|
||||
} from '../services/templateService.js';
|
||||
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const templates = await listTemplates();
|
||||
res.json({ items: templates });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:templateCode',
|
||||
asyncHandler(async (req, res) => {
|
||||
const template = await getActiveTemplate(req.params.templateCode);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ message: 'Template not found.' });
|
||||
}
|
||||
|
||||
return res.json(template);
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:templateCode/versions/:versionNumber',
|
||||
asyncHandler(async (req, res) => {
|
||||
const template = await getTemplateVersion(
|
||||
req.params.templateCode,
|
||||
req.params.versionNumber
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ message: 'Template version not found.' });
|
||||
}
|
||||
|
||||
return res.json(template);
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
+19
-63
@@ -1,73 +1,29 @@
|
||||
const express = require("express");
|
||||
const { closePool, executeQuery } = require("./db");
|
||||
import app from './app.js';
|
||||
import { env } from './config/env.js';
|
||||
import { closePool, query } from './db/pool.js';
|
||||
|
||||
const app = express();
|
||||
const port = Number(process.env.APP_PORT || 3000);
|
||||
async function startServer() {
|
||||
await query('SELECT 1 AS ok');
|
||||
|
||||
app.get("/", (_request, response) => {
|
||||
response.json({
|
||||
service: "clproject-env-test",
|
||||
status: "running",
|
||||
endpoints: ["/health", "/db/check"]
|
||||
const server = app.listen(env.port, () => {
|
||||
console.log(`Check List PoC API listening on port ${env.port}`);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/health", async (_request, response) => {
|
||||
try {
|
||||
const rows = await executeQuery("SELECT 1 AS connection_ok");
|
||||
|
||||
response.json({
|
||||
status: "ok",
|
||||
app: "reachable",
|
||||
database: "reachable",
|
||||
probe: rows[0].connection_ok,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
response.status(503).json({
|
||||
status: "degraded",
|
||||
app: "reachable",
|
||||
database: "unreachable",
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
async function shutdown(signal) {
|
||||
console.log(`Received ${signal}, shutting down...`);
|
||||
server.close(async () => {
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/db/check", async (_request, response) => {
|
||||
try {
|
||||
const rows = await executeQuery(
|
||||
"SELECT id, name, created_at FROM environment_checks ORDER BY id"
|
||||
);
|
||||
|
||||
response.json({
|
||||
status: "ok",
|
||||
records: rows
|
||||
});
|
||||
} catch (error) {
|
||||
response.status(503).json({
|
||||
status: "error",
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
});
|
||||
|
||||
async function shutdown(signal) {
|
||||
console.log(`Received ${signal}, shutting down`);
|
||||
server.close(async () => {
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGINT', () => void shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
shutdown("SIGINT");
|
||||
startServer().catch(async (error) => {
|
||||
console.error('Failed to start server');
|
||||
console.error(error);
|
||||
await closePool();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
shutdown("SIGTERM");
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { query } from '../db/pool.js';
|
||||
import { parseJsonColumn } from '../utils/json.js';
|
||||
|
||||
export async function getImageRules() {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
code,
|
||||
name,
|
||||
allowed_mime_types_json AS allowedMimeTypes,
|
||||
max_file_size_bytes AS maxFileSizeBytes,
|
||||
max_width_px AS maxWidthPx,
|
||||
max_height_px AS maxHeightPx,
|
||||
jpeg_quality AS jpegQuality,
|
||||
oversize_behavior AS oversizeBehavior,
|
||||
max_attachments_per_field AS maxAttachmentsPerField
|
||||
FROM image_rules
|
||||
WHERE is_active = 1
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
);
|
||||
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...rows[0],
|
||||
allowedMimeTypes: parseJsonColumn(rows[0].allowedMimeTypes, [])
|
||||
};
|
||||
}
|
||||
|
||||
export async function getExportProfile() {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
code,
|
||||
name,
|
||||
zip_image_dir AS zipImageDir,
|
||||
excel_sheet_name AS excelSheetName,
|
||||
include_template_version AS includeTemplateVersion,
|
||||
include_export_timestamp AS includeExportTimestamp
|
||||
FROM export_profiles
|
||||
WHERE is_active = 1
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
);
|
||||
|
||||
return rows.length ? rows[0] : null;
|
||||
}
|
||||
|
||||
export async function getAppConfig() {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
config_key AS configKey,
|
||||
config_value_json AS configValue
|
||||
FROM app_config
|
||||
ORDER BY config_key ASC
|
||||
`
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
key: row.configKey,
|
||||
value: parseJsonColumn(row.configValue)
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { query } from '../db/pool.js';
|
||||
|
||||
function groupLookups(rows) {
|
||||
const lookups = new Map();
|
||||
|
||||
for (const row of rows) {
|
||||
if (!lookups.has(row.lookupCode)) {
|
||||
lookups.set(row.lookupCode, {
|
||||
code: row.lookupCode,
|
||||
name: row.lookupName,
|
||||
values: []
|
||||
});
|
||||
}
|
||||
|
||||
if (row.value !== null) {
|
||||
lookups.get(row.lookupCode).values.push({
|
||||
value: row.value,
|
||||
label: row.label,
|
||||
sortOrder: row.sortOrder,
|
||||
isDefault: Boolean(row.isDefault)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(lookups.values());
|
||||
}
|
||||
|
||||
export async function listLookups() {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
ls.code AS lookupCode,
|
||||
ls.name AS lookupName,
|
||||
lv.value,
|
||||
lv.label,
|
||||
lv.sort_order AS sortOrder,
|
||||
lv.is_default AS isDefault
|
||||
FROM lookup_sets ls
|
||||
LEFT JOIN lookup_values lv
|
||||
ON lv.lookup_set_id = ls.id
|
||||
AND lv.is_active = 1
|
||||
WHERE ls.is_active = 1
|
||||
ORDER BY ls.code ASC, lv.sort_order ASC, lv.id ASC
|
||||
`
|
||||
);
|
||||
|
||||
return groupLookups(rows);
|
||||
}
|
||||
|
||||
export async function getLookup(lookupCode) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
ls.code AS lookupCode,
|
||||
ls.name AS lookupName,
|
||||
lv.value,
|
||||
lv.label,
|
||||
lv.sort_order AS sortOrder,
|
||||
lv.is_default AS isDefault
|
||||
FROM lookup_sets ls
|
||||
LEFT JOIN lookup_values lv
|
||||
ON lv.lookup_set_id = ls.id
|
||||
AND lv.is_active = 1
|
||||
WHERE ls.code = ?
|
||||
AND ls.is_active = 1
|
||||
ORDER BY lv.sort_order ASC, lv.id ASC
|
||||
`,
|
||||
[lookupCode]
|
||||
);
|
||||
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return groupLookups(rows)[0];
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { query } from '../db/pool.js';
|
||||
import { parseJsonColumn } from '../utils/json.js';
|
||||
|
||||
function mapTemplateRow(row) {
|
||||
return {
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
version: row.versionNumber,
|
||||
status: row.status,
|
||||
publishedAt: row.publishedAt,
|
||||
definition: parseJsonColumn(row.definitionJson)
|
||||
};
|
||||
}
|
||||
|
||||
export async function listTemplates() {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
t.code,
|
||||
t.name,
|
||||
t.description,
|
||||
tv.version_number AS versionNumber,
|
||||
tv.status,
|
||||
tv.published_at AS publishedAt
|
||||
FROM templates t
|
||||
INNER JOIN template_versions tv
|
||||
ON tv.template_id = t.id
|
||||
AND tv.status = 'active'
|
||||
ORDER BY t.name ASC
|
||||
`
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
activeVersion: row.versionNumber,
|
||||
publishedAt: row.publishedAt
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getActiveTemplate(templateCode) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
t.code,
|
||||
t.name,
|
||||
t.description,
|
||||
tv.version_number AS versionNumber,
|
||||
tv.status,
|
||||
tv.published_at AS publishedAt,
|
||||
tv.definition_json AS definitionJson
|
||||
FROM templates t
|
||||
INNER JOIN template_versions tv
|
||||
ON tv.template_id = t.id
|
||||
AND tv.status = 'active'
|
||||
WHERE t.code = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[templateCode]
|
||||
);
|
||||
|
||||
return rows.length ? mapTemplateRow(rows[0]) : null;
|
||||
}
|
||||
|
||||
export async function getTemplateVersion(templateCode, versionNumber) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT
|
||||
t.code,
|
||||
t.name,
|
||||
t.description,
|
||||
tv.version_number AS versionNumber,
|
||||
tv.status,
|
||||
tv.published_at AS publishedAt,
|
||||
tv.definition_json AS definitionJson
|
||||
FROM templates t
|
||||
INNER JOIN template_versions tv
|
||||
ON tv.template_id = t.id
|
||||
WHERE t.code = ?
|
||||
AND tv.version_number = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
[templateCode, Number(versionNumber)]
|
||||
);
|
||||
|
||||
return rows.length ? mapTemplateRow(rows[0]) : null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function asyncHandler(handler) {
|
||||
return async function wrappedHandler(req, res, next) {
|
||||
try {
|
||||
await handler(req, res, next);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function parseJsonColumn(value, fallback = null) {
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user