Stage after merging with project files

This commit is contained in:
Stan
2026-04-09 19:27:10 +02:00
parent d9f0b21b10
commit 0c74a75126
25 changed files with 1120 additions and 197 deletions
+31
View File
@@ -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;
+23
View File
@@ -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)
}
};
-32
View File
@@ -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
};
+30
View File
@@ -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();
}
+15
View File
@@ -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.'
});
}
+46
View File
@@ -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;
+21
View File
@@ -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;
+29
View File
@@ -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;
+49
View File
@@ -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
View File
@@ -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");
});
+69
View File
@@ -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)
}));
}
+76
View File
@@ -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];
}
+89
View File
@@ -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;
}
+9
View File
@@ -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);
}
};
}
+15
View File
@@ -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;
}
}