Initial stage

This commit is contained in:
Stan
2026-04-09 19:15:39 +02:00
commit d9f0b21b10
35 changed files with 3318 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
FROM bitnami/node:latest
WORKDIR /workspace
+31
View File
@@ -0,0 +1,31 @@
{
"name": "clproject-node-mariadb",
"dockerComposeFile": [
"../docker-compose.yml"
],
"service": "app",
"workspaceFolder": "/workspace",
"shutdownAction": "stopCompose",
"overrideCommand": false,
"remoteUser": "1001",
"updateRemoteUserUID": true,
"forwardPorts": [3000, 8080, 3306],
"portsAttributes": {
"3000": {
"label": "Node.js app"
},
"8080": {
"label": "phpMyAdmin"
},
"3306": {
"label": "MariaDB"
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker"
]
}
}
}
+4
View File
@@ -0,0 +1,4 @@
copy
node_modules
.git
npm-debug.log
+9
View File
@@ -0,0 +1,9 @@
APP_PORT=3000
APP_URL=http://localhost:3000
DB_HOST=db
DB_PORT=3306
DB_NAME=app_db
DB_USER=app_user
DB_PASSWORD=app_password
MARIADB_ROOT_PASSWORD=change_me_for_local_dev
PHPMYADMIN_PORT=8080
+4
View File
@@ -0,0 +1,4 @@
.env
node_modules
.env.local
npm-debug.log
+46
View File
@@ -0,0 +1,46 @@
# CLProject Development Environment
This workspace now contains a containerized local development setup based on Node.js, MariaDB, phpMyAdmin, Docker Compose, and VS Code Dev Containers.
## Services
- `app`: Node.js development container built from `bitnami/node`
- `db`: MariaDB container built from `bitnami/mariadb`
- `phpmyadmin`: phpMyAdmin container for database inspection
## Open In VS Code
1. Open this workspace in VS Code.
2. Run `Dev Containers: Reopen in Container`.
3. Wait for the `app` service to install dependencies and start the server.
The mounted workspace remains editable from VS Code while running inside the container.
## Ports
- App: `http://localhost:3000`
- phpMyAdmin: `http://localhost:8080`
- MariaDB: `localhost:3306`
## Validate The Environment
After the containers are up, run:
```bash
npm run test:environment
```
The test checks that:
- the Node.js app is reachable
- the app can talk to MariaDB
- the seed table was created successfully
## Database Login
- Server: `db`
- Database: `app_db`
- User: `app_user`
- Password: `app_password`
Root password is configured in `.env` for local development.
+7
View File
@@ -0,0 +1,7 @@
PORT=3000
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=check_list
DB_USER=check_list_user
DB_PASSWORD=check_list_password
DB_CONNECTION_LIMIT=5
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.env
dist/
@@ -0,0 +1,724 @@
Project Document
Check List Hybrid Initial Solution for Quality Check Reports
1. Document Information
Project Name: Check List
Document Type: Initial Project Documentation / Concept Specification
Version: 1.0
Date: 09 April 2026
Project Phase: Initial Implementation (Hybrid model between Option 2 and Option 3)
2. Executive Summary
The purpose of the Check List project is to replace the current Excel-based quality check reporting process with a modern digital solution that supports structured report creation, image attachments, offline work, and standardized export.
The proposed initial implementation will follow a hybrid approach between a fully local web solution and a server-connected application.
In this phase:
- Node.js and MariaDB available on the server side will be used to centrally manage:
- checklist templates,
- report structure,
- required fields,
- validation rules,
- lookup values,
- image processing rules,
- template versions.
- The client-side web application will be responsible for:
- loading and caching templates,
- creating reports offline,
- saving reports locally as drafts,
- switching between multiple reports,
- attaching and resizing images directly in the browser,
- renaming images automatically,
- generating the final output as a ZIP file containing:
- an Excel report file,
- a folder with attached images.
At this stage, the final reports will not yet be stored centrally in the database.
Instead, the database will serve as a template and configuration repository, while the final report remains a file-based deliverable.
This approach provides:
- a practical and low-risk first implementation,
- offline usability,
- centralized management of checklist definitions,
- and a clear migration path toward future server-side report synchronization.
3. Project Background
The current reporting process is based on Excel files used to create quality check lists and reports. This creates a number of operational and technical limitations, including:
- manual preparation of reports,
- inconsistent structure between reports,
- difficulty enforcing required fields,
- inefficient handling of images,
- lack of standardized image naming,
- dependency on Excel files and manual file handling,
- limited support for mobile devices,
- no controlled offline workflow,
- difficult future integration with central systems.
The new solution is intended to improve data consistency, user convenience, and long-term maintainability by introducing a controlled digital workflow.
4. Project Objective
The objective of the project is to implement a digital reporting solution that replaces Excel-based checklist reporting with a structured web application that works across platforms, supports offline work, and produces a standardized ZIP-based report package.
The initial version of the solution must:
1. allow users to create quality check reports in a web application,
2. support Windows, Android, and iOS devices,
3. allow use without permanent internet access,
4. provide centrally managed checklist templates from the server,
5. allow users to save reports as drafts and continue them later,
6. allow switching between multiple reports,
7. support image attachment with automatic resizing/compression in the browser,
8. automatically rename images based on report naming rules,
9. generate the final report as a ZIP package with Excel file and images,
10. prepare the solution architecture for future server-side report synchronization.
5. Scope of the Initial Implementation
5.1 In Scope
The following functions are included in the initial project scope:
Server-side scope
- central storage of checklist templates,
- central storage of required field definitions,
- central storage of validation rules,
- central storage of lookup values / dropdown lists,
- central storage of image handling rules,
- template version management,
- API for providing templates and configuration to the web application.
Client-side scope
- responsive web application,
- dynamic rendering of checklists based on templates,
- local storage of report drafts,
- local storage of image attachments,
- report editing in offline mode,
- report auto-save,
- support for multiple local reports,
- switching between reports within the application,
- browser-based image resizing/compression,
- automatic image renaming,
- local ZIP file generation,
- local Excel file generation,
- local export of finished reports.
5.2 Out of Scope (Initial Phase)
The following items are intentionally excluded from the first implementation phase:
- central storage of completed reports in the database,
- full backend synchronization of report data,
- server-side image upload for completed reports,
- conflict handling between multiple users/devices,
- server-side draft storage,
- advanced reporting/analytics dashboards,
- workflow approvals,
- ERP/QMS integrations,
- embedding images directly inside Excel (unless later required),
- native Android/iOS applications.
These items can be considered in later project phases.
6. Proposed Solution Overview
The recommended initial solution is a hybrid offline-first web application.
Core principle:
- Templates and configuration are centrally managed on the server
- Reports are created and stored locally in the browser
- Final output is exported as ZIP
This model combines the most important business and technical benefits:
- centralized control over checklist content,
- simpler initial implementation than full synchronization,
- full support for offline report creation,
- broad device compatibility,
- structured handling of images,
- future readiness for backend expansion.
7. Business Benefits
The proposed solution provides the following business benefits:
7.1 Standardization
- consistent report structure,
- consistent validation rules,
- controlled mandatory fields,
- unified image naming convention.
7.2 Improved usability
- easier report creation compared to manual Excel editing,
- better experience on mobile devices,
- faster attachment handling,
- draft-based workflow.
7.3 Offline capability
- users can continue work even without internet access,
- templates can be loaded earlier and used later offline.
7.4 Central governance
- checklist templates are managed from one place,
- changes in required fields do not require front-end redesign,
- image limitations are centrally configurable.
7.5 Lower implementation risk
- final reports remain file-based in the first phase,
- backend complexity is limited,
- architecture remains ready for later expansion.
8. User Groups
The initial solution is intended for users involved in quality check and inspection reporting, for example:
- quality inspectors,
- production or warehouse staff performing checks,
- incoming inspection personnel,
- mobile users working on shop floor or at supplier/customer location,
- administrators responsible for checklist configuration.
9. Functional Requirements
9.1 Template Management
Checklist templates must be centrally stored on the server and provided to the web application through an API.
Each template should be able to define:
- template identifier,
- template name,
- version,
- sections/groups,
- field labels,
- field types,
- required fields,
- default values,
- validation rules,
- optional comments,
- image requirements,
- export metadata.
The application must support the use of different template versions.
Template versioning rule
- Existing draft reports must remain bound to the template version under which they were created.
- New reports should use the newest active version of the selected template.
9.2 Report Creation
The user must be able to create a new report based on a selected checklist template.
A report should include:
- report header information,
- checklist items,
- comments,
- attachments,
- report status,
- metadata (created date, updated date, template version, etc.).
Possible field types include:
- text,
- number,
- date,
- checkbox / boolean,
- dropdown / lookup list,
- pass/fail choice,
- comments,
- attachment-required items.
9.3 Draft Save and Continue Later
The application must allow the user to save incomplete reports locally as drafts.
The user must be able to:
- save a report as draft,
- leave the report,
- return later,
- continue editing from the last saved state.
Draft saving should occur:
- manually via a save function,
- automatically during editing,
- when switching between reports,
- when adding/removing attachments.
Draft reports must remain accessible from a report list/dashboard.
9.4 Switching Between Reports
The user must be able to work with multiple reports within the same application.
The application must provide a report dashboard or report manager where the user can:
- see all locally stored reports,
- filter reports by status,
- open a selected report,
- continue editing,
- duplicate a report,
- archive or delete a report,
- export a completed report.
When switching from one report to another:
- the current report must be saved automatically,
- the selected report must be loaded from local storage.
9.5 Report Statuses
The system should support at least the following statuses:
- Draft
- In Progress
- Ready for Export
- Exported
- Archived
These statuses should be visible in the report list.
Future phases may introduce additional synchronization-related statuses, but they are not required in the initial version.
9.6 Offline Mode
The application must support offline work after templates and configuration have been loaded.
Offline mode must allow:
- opening cached templates,
- creating new reports,
- editing existing drafts,
- attaching images,
- saving report progress locally,
- generating ZIP output locally.
If internet access is unavailable, the application should continue using the latest cached template data.
9.7 Image Attachment Handling
The system must support attaching images to reports.
Images may be added from:
- file picker,
- mobile device gallery,
- mobile camera (if browser/device supports direct capture).
The application must validate image files against centrally defined rules such as:
- allowed file types,
- maximum file size,
- maximum dimensions,
- output quality settings.
9.8 Browser-Based Image Resizing
Image optimization must be performed directly in the web browser on the client side.
When an image is attached, the application must:
1. read the image locally,
2. validate file type, size, and dimensions,
3. resize/compress the image if it exceeds configured limits,
4. store the optimized version locally,
5. show a preview,
6. associate the optimized image with the current report.
This functionality is required to:
- reduce storage usage,
- standardize attachment size,
- improve performance,
- maintain offline capability,
- prepare for future upload optimization.
The system should support either:
- automatic resizing,
- warning + resizing,
- or blocking oversized files,
depending on centrally configured rules.
For the initial implementation, the recommended behavior is:
- automatic optimization with user notification.
9.9 Automatic Image Renaming
The application must automatically rename attached image files according to a structured naming convention.
The naming convention should be centrally configurable and may include:
- report number,
- section code,
- sequence number,
- date or other metadata if required.
Example naming structure:
<ReportNumber>_<SectionCode>_<Sequence>.jpg
Both the original filename and generated filename should be tracked in metadata.
9.10 Validation Rules
The application must support validation based on centrally managed rules.
Validation may include:
- required fields,
- allowed values,
- numeric ranges,
- image required for selected checklist item,
- minimum/maximum number of attachments,
- checklist completeness.
Validation must distinguish between:
- draft save (allowed even if incomplete),
- ready for export (only allowed if validation passes).
9.11 Export to ZIP
The final report must be generated locally as a ZIP archive.
The ZIP file should contain:
- one Excel file,
- one image folder containing all attached images.
Recommended ZIP structure
<ReportNumber>.zip
├── <ReportNumber>.xlsx
└── images/
├── <image1>.jpg
├── <image2>.jpg
└── ...
The Excel file must include:
- report header,
- checklist answers,
- comments,
- image file references,
- template version,
- export timestamp.
For the first version, it is recommended that:
- images remain as separate files in the ZIP package,
- images are not embedded into the Excel file unless explicitly required later.
10. Non-Functional Requirements
10.1 Platform Compatibility
The application must operate on:
- Windows browsers,
- Android browsers,
- iOS browsers.
A responsive layout is required.
10.2 Performance
The solution should perform adequately on standard office devices and mobile devices.
Special attention should be given to:
- image processing time,
- local storage performance,
- report loading speed,
- ZIP generation performance.
10.3 Usability
The interface should be simple and optimized for operational use.
The system should:
- minimize manual steps,
- provide visible save status,
- provide clear validation messages,
- support touch-based interaction,
- reduce risk of data loss.
10.4 Maintainability
The solution should be modular and designed for future expansion.
The frontend should separate:
- UI logic,
- template rendering,
- local data storage,
- image processing,
- export logic,
- future synchronization logic.
10.5 Data Integrity
The system must:
- auto-save regularly,
- preserve locally stored reports,
- bind reports to template versions,
- avoid corruption caused by later template changes.
10.6 Security
In the initial phase, security requirements may be lighter than in a fully centralized solution, but should still include:
- secure API communication when online,
- controlled access to template management,
- safe handling of local report data,
- user/session identification if introduced.
11. Technical Architecture
11.1 General Architecture
The recommended architecture for the initial implementation is:
Server Side
- Node.js application
- MariaDB database
- REST API for templates and configuration
Client Side
- browser-based web application
- local storage for reports and attachments
- local report generation and export
11.2 Server Responsibilities
The server will manage:
- checklist templates,
- template versions,
- field definitions,
- required field rules,
- lookup data,
- image processing rules,
- configuration data.
At this stage, the server will not store final report data.
11.3 Client Responsibilities
The client application will manage:
- template download and caching,
- form rendering,
- local report editing,
- draft storage,
- image resizing,
- image previews,
- automatic renaming,
- Excel generation,
- ZIP generation.
12. Recommended Technology Stack
12.1 Frontend
Recommended:
- React for the web application,
- responsive UI framework or custom responsive design,
- IndexedDB for local offline storage,
- Excel generation library,
- ZIP generation library,
- browser Canvas API (or equivalent browser-side image processing approach).
12.2 Backend
Recommended:
- Node.js
- REST API architecture
12.3 Database
Recommended:
- MariaDB
13. Data Storage Concept
13.1 Server-side Data
Stored in MariaDB:
- templates,
- template sections,
- template fields,
- validation rules,
- lookups,
- image settings,
- export settings,
- template version data.
13.2 Client-side Data
Stored locally in the browser:
- downloaded templates (cached),
- draft reports,
- report metadata,
- checklist answers,
- attachment metadata,
- optimized image files.
Recommended local storage technology
Use IndexedDB rather than basic browser local storage.
Reason:
- better support for structured data,
- support for binary image data,
- better capacity and performance,
- better fit for multiple drafts and attachments.
14. Suggested Logical Data Model
14.1 Template-related entities (server side)
Possible logical entities include:
- Templates
- Template Sections
- Template Fields
- Lookup Values
- Template Settings
- Template Versions
These should support dynamic form rendering.
14.2 Report-related entities (client side)
Each local report should contain at least:
- local report ID,
- report number or temporary identifier,
- template ID,
- template version,
- report status,
- header data,
- checklist values,
- comments,
- attachment metadata,
- created date,
- last update date.
Each attachment should contain:
- internal attachment ID,
- original filename,
- generated filename,
- MIME type,
- file size,
- dimensions,
- relation to report and field/section.
15. API Scope for Initial Phase
The initial backend API can be intentionally limited.
Suggested API scope:
- get available templates,
- get template details,
- get template version,
- get lookup data,
- get image/configuration settings,
- optional application version/configuration endpoint,
- optional authentication endpoints.
Report upload API is not required in the initial phase.
16. User Workflow
16.1 Online startup scenario
1. User opens the application.
2. Application loads latest templates and configuration from the server.
3. Templates are cached locally.
4. User can start creating reports.
16.2 Offline work scenario
1. User opens the application without internet access.
2. Application uses last cached templates.
3. User creates or continues reports.
4. Images are processed locally.
5. Drafts are saved locally.
6. User exports a ZIP file when the report is finished.
16.3 Report editing scenario
1. User creates or opens a report.
2. User fills checklist fields.
3. User adds comments and images.
4. Application auto-saves progress.
5. User may leave report as draft or continue later.
6. User marks report as ready.
7. Application validates data.
8. User exports ZIP file.
17. Risks and Constraints
17.1 Local Storage Dependency
Because reports are stored locally in the first phase, there is a dependency on browser/device storage.
Risk:
- local drafts may be lost if storage is cleared.
Mitigation:
- auto-save,
- visible draft management,
- encourage timely export/archiving,
- consider backup/import in later phase.
17.2 Browser Storage Limits
Images and multiple drafts can consume significant space.
Mitigation:
- automatic image compression,
- optimized image limits,
- archive/delete completed reports,
- monitor practical storage capacity during pilot.
17.3 Template Change During Active Reports
Changing a template could affect reports already started.
Mitigation:
- enforce template versioning,
- bind each report to the template version it started with.
17.4 No Central Report Repository Yet
The initial phase does not yet provide central report storage or reporting history.
Mitigation:
- keep ZIP export as the official output,
- plan future extension to server-side synchronization.
18. Future Expansion Path
The proposed architecture is intentionally designed to support future transition toward a more advanced server-connected model.
Future phases may include:
- upload of completed reports to backend,
- server-side report repository,
- attachment upload,
- synchronization of offline-created reports,
- user roles and permissions,
- dashboards and analytics,
- integration with other systems,
- central audit trail.
Because templates are already server-managed in the initial phase, future expansion should require fewer architectural changes.
19. Recommended Implementation Phases
Phase 1 Hybrid Initial Solution
- template/configuration management on server,
- local report creation,
- offline-capable report editing,
- draft save and continue later,
- switching between multiple reports,
- browser-side image resizing,
- automatic image renaming,
- ZIP export.
Phase 2 Extended Hybrid
- optional user authentication,
- optional upload of ZIP packages,
- central administration UI for templates,
- export history,
- advanced template/version control.
Phase 3 Full Server-Based Reporting
- full report synchronization,
- central report database,
- attachment upload,
- online/offline sync handling,
- reporting dashboard and analytics.
20. Final Recommendation
The recommended starting point for the Check List project is a hybrid initial solution in which:
- Node.js + MariaDB are used from the beginning for central checklist/template management,
- the web application works across Windows, Android, and iOS,
- the application supports offline work after templates are loaded,
- reports are stored locally as drafts,
- the user can switch between reports and continue later,
- images are processed directly in the browser,
- and the final report is exported as a ZIP package with Excel and images.
This approach provides the best balance between:
- implementation speed,
- operational usefulness,
- centralized control,
- offline support,
- and future scalability.
21. Proposed Next Step
The next recommended activity is to prepare a detailed implementation specification covering:
1. screen-by-screen user flow,
2. template JSON structure,
3. local report data model,
4. API endpoint specification,
5. MariaDB schema proposal,
6. image resizing rules,
7. ZIP/Excel export format,
8. development backlog / user stories.
+274
View File
@@ -0,0 +1,274 @@
# Check List Initial Solution Proposal
## 1. Recommended Starting Point
The strongest initial implementation is an offline-first web application with a thin server-backed configuration layer.
This matches the source document's core constraint set:
- reports must work without permanent internet access,
- templates must be centrally controlled,
- final output remains file-based,
- the architecture must be extendable toward future synchronization.
## 2. Proposed Architecture
### Client application
Build a responsive Progressive Web App using:
- React
- TypeScript
- IndexedDB for offline persistence
- a service worker for application asset caching
- browser-based image processing using Canvas or createImageBitmap
- XLSX generation library
- ZIP generation library
Main client modules:
- template cache module
- dynamic form renderer
- report draft repository
- image processing pipeline
- validation engine
- Excel export mapper
- ZIP packaging module
- sync-ready integration layer placeholder
### Server application
Build a small REST API using:
- Node.js
- Express or Fastify
- MariaDB
Main server responsibilities:
- template CRUD
- template versioning
- lookup management
- validation rule delivery
- image rule delivery
- export configuration delivery
- optional authentication later
The server should not accept completed reports in phase 1.
## 3. Why This Design Fits
This design solves the main business problems without introducing early complexity:
- central governance is handled by the backend,
- offline draft work is handled entirely on the client,
- images are optimized before storage/export,
- ZIP export preserves compatibility with the current file-based process,
- future synchronization can be added without replacing the client-side report model.
## 4. Initial Technical Blueprint
### Frontend structure
Suggested feature-oriented structure:
```text
src/
app/
features/templates/
features/reports/
features/images/
features/export/
features/validation/
features/dashboard/
shared/ui/
shared/lib/
shared/storage/
shared/api/
```
Key implementation decisions:
- use template-driven rendering instead of hardcoded forms,
- keep report state normalized by report ID,
- store attachments separately from answer objects in IndexedDB,
- version templates immutably once published,
- separate draft validation from export validation.
### Backend structure
Suggested backend domains:
- templates
- templateVersions
- fields
- lookups
- validationRules
- imageRules
- exportSettings
Recommended principle:
the API should deliver a fully materialized template definition for a chosen version so the client can render forms without additional server joins during report editing.
## 5. Minimal Phase 1 Data Model
### Server-side entities
Recommended MariaDB tables:
- templates
- template_versions
- template_sections
- template_fields
- field_validation_rules
- lookup_sets
- lookup_values
- image_rules
- export_profiles
Important rules:
- only one active version per template for new report creation,
- old versions remain readable for existing drafts,
- template JSON snapshots can be generated and cached for fast client download.
### Client-side entities
Recommended IndexedDB stores:
- templatesCache
- reports
- reportAnswers
- attachments
- appConfig
Example report aggregate:
```json
{
"id": "local-report-uuid",
"reportNumber": "QC-2026-0001",
"templateId": "incoming-inspection",
"templateVersion": 3,
"status": "in_progress",
"header": {
"supplier": "ACME",
"inspectionDate": "2026-04-09"
},
"createdAt": "2026-04-09T09:00:00Z",
"updatedAt": "2026-04-09T10:15:00Z"
}
```
Example attachment metadata:
```json
{
"id": "attachment-uuid",
"reportId": "local-report-uuid",
"fieldId": "damage-photo",
"originalFilename": "IMG_0412.jpg",
"generatedFilename": "QC-2026-0001_SEC01_001.jpg",
"mimeType": "image/jpeg",
"width": 1600,
"height": 1200,
"sizeBytes": 245312
}
```
## 6. Recommended API Surface
Initial REST endpoints:
- `GET /api/templates`
- `GET /api/templates/:templateId`
- `GET /api/templates/:templateId/versions/:version`
- `GET /api/lookups`
- `GET /api/config/image-rules`
- `GET /api/config/export`
- `GET /api/app-config`
Optional later endpoints:
- `POST /api/auth/login`
- `POST /api/report-uploads`
API response strategy:
- return explicit version metadata with every template,
- include cache timestamps or ETags,
- support incremental refresh later if template volume grows.
## 7. Core User Flow
Phase 1 flow should be:
1. User opens the app online.
2. App downloads active templates and configuration.
3. Data is cached locally.
4. User creates or resumes a report.
5. Report is auto-saved into IndexedDB.
6. Images are validated, optimized, renamed, and stored locally.
7. User marks the report ready for export.
8. Full validation runs.
9. App generates XLSX.
10. App packages XLSX and images into a ZIP and downloads it locally.
## 8. Delivery Plan
### Iteration 1
- define template JSON contract,
- define MariaDB schema,
- implement template read API,
- build application shell and offline cache,
- build report dashboard and draft persistence.
### Iteration 2
- implement dynamic checklist renderer,
- implement draft save and resume,
- implement report switching and status management,
- implement export-readiness validation.
### Iteration 3
- implement image attach, preview, compression, and renaming,
- implement XLSX mapping,
- implement ZIP export,
- run pilot tests on Windows, Android, and iOS.
### Iteration 4
- harden error handling,
- tune storage usage,
- add admin-facing template maintenance workflow,
- prepare phase 2 synchronization extension points.
## 9. Open Gaps That Still Need Definition
The source document is strong at the concept level, but these items still need explicit specification before implementation starts:
- exact template JSON schema,
- exact report numbering strategy,
- exact Excel layout and formatting rules,
- rule language for conditional validation,
- whether PWA installation is required or optional,
- retention and deletion rules for local drafts,
- browser support baseline and test matrix,
- authentication requirements for template administration.
## 10. Main Risks and Mitigations
### Risk: local draft loss
Mitigation: auto-save, visible save state, export reminders, later import/export backup feature.
### Risk: storage pressure from images
Mitigation: enforce compression rules, cap attachment count where needed, show storage usage warnings.
### Risk: template drift
Mitigation: immutable template versions and version-bound drafts.
### Risk: export mismatch with current Excel expectations
Mitigation: validate the XLSX output format early using sample reports before full UI completion.
## 11. Final Recommendation
Proceed with a phase 1 implementation based on:
- React + TypeScript PWA frontend,
- IndexedDB local persistence,
- Node.js REST backend,
- MariaDB template repository,
- local XLSX and ZIP export.
This is the lowest-risk path that still satisfies the documented requirements and preserves a clean path to later server-side synchronization.
## 12. Best Immediate Next Steps
The next concrete deliverables should be:
1. template JSON schema,
2. MariaDB schema draft,
3. API contract draft,
4. report and attachment IndexedDB schema,
5. one sample export template in XLSX format,
6. a first implementation backlog split into frontend and backend work items.
+137
View File
@@ -0,0 +1,137 @@
# Check List Proof of Concept
This repository contains a minimal proof-of-concept backend for the Check List hybrid reporting solution described in the project documentation.
## What is included
- Node.js REST API for template and configuration delivery
- MariaDB schema for phase 1 configuration data
- seed data with one sample inspection checklist template
- lookup values, image policy, and export profile
- setup instructions for local development
## Scope of this PoC
Included:
- template list endpoint
- active template endpoint
- specific template version endpoint
- lookup endpoints
- image rule endpoint
- export profile endpoint
- generic application config endpoint
- MariaDB schema and seed data
Not included:
- report upload
- authentication
- admin UI
- report draft storage backend
- XLSX or ZIP generation
- client-side offline application
The PoC keeps template content inside a JSON column to reduce initial complexity and speed up delivery. This is deliberate for phase 1 proof-of-concept work.
## Project structure
```text
.
├── package.json
├── sql/
│ ├── schema.sql
│ └── seed.sql
└── src/
├── app.js
├── server.js
├── config/
├── db/
├── middleware/
├── routes/
├── services/
└── utils/
```
## Prerequisites
- Node.js 20+
- MariaDB 10.6+
## Setup
1. Copy `.env.example` to `.env` and adjust the database credentials.
2. Create the schema:
```sql
SOURCE sql/schema.sql;
```
3. Seed the sample data:
```sql
SOURCE sql/seed.sql;
```
4. Install dependencies:
```bash
npm install
```
5. Start the API:
```bash
npm start
```
## API endpoints
### Service health
`GET /api/health`
### Templates
- `GET /api/templates`
- `GET /api/templates/incoming-inspection`
- `GET /api/templates/incoming-inspection/versions/1`
### Lookups
- `GET /api/lookups`
- `GET /api/lookups/pass-fail`
### Configuration
- `GET /api/config/image-rules`
- `GET /api/config/export`
- `GET /api/config/app-config`
## Example response
`GET /api/templates/incoming-inspection`
```json
{
"code": "incoming-inspection",
"name": "Incoming Inspection Checklist",
"description": "PoC template for supplier or incoming goods quality inspection.",
"version": 1,
"status": "active",
"publishedAt": "2026-04-09T10:00:00.000Z",
"definition": {
"templateId": "incoming-inspection",
"templateName": "Incoming Inspection Checklist",
"version": 1,
"sections": []
}
}
```
## Recommended next step after this PoC
The next logical implementation layer is the client application that:
- caches templates in IndexedDB,
- renders forms dynamically from `definition`,
- stores local drafts and image metadata,
- applies validation rules before export,
- generates XLSX and ZIP locally.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "check-list-poc-api",
"version": "0.1.0",
"description": "Proof-of-concept backend for the Check List hybrid quality reporting solution.",
"type": "module",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"mariadb": "^3.4.0"
}
}
+97
View File
@@ -0,0 +1,97 @@
CREATE DATABASE IF NOT EXISTS check_list CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE check_list;
CREATE TABLE IF NOT EXISTS templates (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_templates_code (code)
);
CREATE TABLE IF NOT EXISTS template_versions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
template_id BIGINT UNSIGNED NOT NULL,
version_number INT NOT NULL,
status ENUM('draft', 'active', 'retired') NOT NULL DEFAULT 'draft',
definition_json JSON NOT NULL,
published_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_template_version (template_id, version_number),
KEY idx_template_versions_template_status (template_id, status),
CONSTRAINT fk_template_versions_template
FOREIGN KEY (template_id) REFERENCES templates (id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS lookup_sets (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_lookup_sets_code (code)
);
CREATE TABLE IF NOT EXISTS lookup_values (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
lookup_set_id BIGINT UNSIGNED NOT NULL,
value VARCHAR(100) NOT NULL,
label VARCHAR(200) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
is_default TINYINT(1) NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY uq_lookup_value (lookup_set_id, value),
KEY idx_lookup_values_lookup_set (lookup_set_id),
CONSTRAINT fk_lookup_values_lookup_set
FOREIGN KEY (lookup_set_id) REFERENCES lookup_sets (id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS image_rules (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
allowed_mime_types_json JSON NOT NULL,
max_file_size_bytes INT UNSIGNED NOT NULL,
max_width_px INT UNSIGNED NOT NULL,
max_height_px INT UNSIGNED NOT NULL,
jpeg_quality INT UNSIGNED NOT NULL,
oversize_behavior ENUM('auto_optimize', 'warn_then_optimize', 'block') NOT NULL DEFAULT 'auto_optimize',
max_attachments_per_field INT UNSIGNED NOT NULL DEFAULT 5,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_image_rules_code (code)
);
CREATE TABLE IF NOT EXISTS export_profiles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
zip_image_dir VARCHAR(100) NOT NULL DEFAULT 'images',
excel_sheet_name VARCHAR(100) NOT NULL DEFAULT 'Checklist',
include_template_version TINYINT(1) NOT NULL DEFAULT 1,
include_export_timestamp TINYINT(1) NOT NULL DEFAULT 1,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_export_profiles_code (code)
);
CREATE TABLE IF NOT EXISTS app_config (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
config_key VARCHAR(100) NOT NULL,
config_value_json JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_app_config_key (config_key)
);
+220
View File
@@ -0,0 +1,220 @@
USE check_list;
INSERT INTO templates (code, name, description)
VALUES (
'incoming-inspection',
'Incoming Inspection Checklist',
'PoC template for supplier or incoming goods quality inspection.'
)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
description = VALUES(description);
SET @template_id = (SELECT id FROM templates WHERE code = 'incoming-inspection');
INSERT INTO template_versions (
template_id,
version_number,
status,
definition_json,
published_at
)
VALUES (
@template_id,
1,
'active',
'{
"templateId": "incoming-inspection",
"templateName": "Incoming Inspection Checklist",
"version": 1,
"reportNumberPattern": "QC-{yyyy}-{seq:4}",
"exportProfileCode": "default-report-export",
"imageRuleCode": "standard-mobile-images",
"sections": [
{
"id": "header",
"title": "Report Header",
"type": "group",
"fields": [
{
"id": "reportNumber",
"label": "Report Number",
"type": "text",
"required": true,
"readOnly": false
},
{
"id": "inspectionDate",
"label": "Inspection Date",
"type": "date",
"required": true
},
{
"id": "supplierName",
"label": "Supplier",
"type": "text",
"required": true
},
{
"id": "batchNumber",
"label": "Batch Number",
"type": "text",
"required": false
}
]
},
{
"id": "check-items",
"title": "Inspection Items",
"type": "group",
"fields": [
{
"id": "packagingCondition",
"label": "Packaging Condition",
"type": "lookup",
"lookupCode": "pass-fail",
"required": true
},
{
"id": "labelCheck",
"label": "Label Verification",
"type": "lookup",
"lookupCode": "pass-fail",
"required": true
},
{
"id": "quantityVerified",
"label": "Quantity Verified",
"type": "number",
"required": true,
"validation": {
"min": 0
}
},
{
"id": "damageFound",
"label": "Visible Damage Found",
"type": "checkbox",
"required": false,
"defaultValue": false
},
{
"id": "damagePhoto",
"label": "Damage Photo",
"type": "attachment",
"requiredWhen": {
"field": "damageFound",
"equals": true
},
"maxAttachments": 3
},
{
"id": "inspectorComment",
"label": "Inspector Comment",
"type": "comment",
"required": false,
"maxLength": 1000
}
]
}
]
}',
NOW()
)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
definition_json = VALUES(definition_json),
published_at = VALUES(published_at);
INSERT INTO lookup_sets (code, name)
VALUES
('pass-fail', 'Pass/Fail'),
('draft-status', 'Draft Status')
ON DUPLICATE KEY UPDATE
name = VALUES(name);
SET @pass_fail_id = (SELECT id FROM lookup_sets WHERE code = 'pass-fail');
SET @draft_status_id = (SELECT id FROM lookup_sets WHERE code = 'draft-status');
INSERT INTO lookup_values (lookup_set_id, value, label, sort_order, is_default)
VALUES
(@pass_fail_id, 'pass', 'Pass', 1, 1),
(@pass_fail_id, 'fail', 'Fail', 2, 0),
(@draft_status_id, 'draft', 'Draft', 1, 1),
(@draft_status_id, 'in_progress', 'In Progress', 2, 0),
(@draft_status_id, 'ready_for_export', 'Ready for Export', 3, 0),
(@draft_status_id, 'exported', 'Exported', 4, 0),
(@draft_status_id, 'archived', 'Archived', 5, 0)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
sort_order = VALUES(sort_order),
is_default = VALUES(is_default);
INSERT INTO image_rules (
code,
name,
allowed_mime_types_json,
max_file_size_bytes,
max_width_px,
max_height_px,
jpeg_quality,
oversize_behavior,
max_attachments_per_field,
is_active
)
VALUES (
'standard-mobile-images',
'Standard Mobile Image Policy',
'["image/jpeg", "image/png", "image/webp"]',
5242880,
1920,
1920,
82,
'auto_optimize',
5,
1
)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
allowed_mime_types_json = VALUES(allowed_mime_types_json),
max_file_size_bytes = VALUES(max_file_size_bytes),
max_width_px = VALUES(max_width_px),
max_height_px = VALUES(max_height_px),
jpeg_quality = VALUES(jpeg_quality),
oversize_behavior = VALUES(oversize_behavior),
max_attachments_per_field = VALUES(max_attachments_per_field),
is_active = VALUES(is_active);
INSERT INTO export_profiles (
code,
name,
zip_image_dir,
excel_sheet_name,
include_template_version,
include_export_timestamp,
is_active
)
VALUES (
'default-report-export',
'Default Report Export',
'images',
'Checklist',
1,
1,
1
)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
zip_image_dir = VALUES(zip_image_dir),
excel_sheet_name = VALUES(excel_sheet_name),
include_template_version = VALUES(include_template_version),
include_export_timestamp = VALUES(include_export_timestamp),
is_active = VALUES(is_active);
INSERT INTO app_config (config_key, config_value_json)
VALUES
('autosave', '{"enabled": true, "intervalSeconds": 20}'),
('offlineCache', '{"templateTtlHours": 24, "refreshOnStartup": true}'),
('reportStatuses', '["draft", "in_progress", "ready_for_export", "exported", "archived"]')
ON DUPLICATE KEY UPDATE
config_value_json = VALUES(config_value_json);
+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)
}
};
+30
View File
@@ -0,0 +1,30 @@
import 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;
+29
View File
@@ -0,0 +1,29 @@
import app from './app.js';
import { env } from './config/env.js';
import { closePool, query } from './db/pool.js';
async function startServer() {
await query('SELECT 1 AS ok');
const server = app.listen(env.port, () => {
console.log(`Check List PoC API listening on port ${env.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'));
}
startServer().catch(async (error) => {
console.error('Failed to start server');
console.error(error);
await closePool();
process.exit(1);
});
+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;
}
}
+46
View File
@@ -0,0 +1,46 @@
services:
app:
build:
context: .
dockerfile: .devcontainer/Dockerfile
env_file:
- .env
working_dir: /workspace
command: >-
sh -lc "if [ ! -d node_modules ]; then npm install --no-fund --no-audit; fi && npm run dev"
volumes:
- .:/workspace:cached
ports:
- "${APP_PORT:-3000}:${APP_PORT:-3000}"
depends_on:
- db
db:
image: bitnami/mariadb:latest
env_file:
- .env
environment:
MARIADB_DATABASE: ${DB_NAME:-app_db}
MARIADB_USER: ${DB_USER:-app_user}
MARIADB_PASSWORD: ${DB_PASSWORD:-app_password}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root_password}
ports:
- "${DB_PORT:-3306}:3306"
volumes:
- mariadb_data:/bitnami/mariadb
- ./docker/mariadb/init:/docker-entrypoint-initdb.d:ro
phpmyadmin:
image: phpmyadmin:5-apache
depends_on:
- db
environment:
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: ${DB_USER:-app_user}
PMA_PASSWORD: ${DB_PASSWORD:-app_password}
ports:
- "${PHPMYADMIN_PORT:-8080}:80"
volumes:
mariadb_data:
+9
View File
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS environment_checks (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO environment_checks (name)
VALUES ('containers-online')
ON DUPLICATE KEY UPDATE name = VALUES(name);
+909
View File
@@ -0,0 +1,909 @@
{
"name": "clproject-env-test",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clproject-env-test",
"version": "1.0.0",
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mariadb": "^3.4.0"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/mariadb": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.5.2.tgz",
"integrity": "sha512-9rztrI4nouxAY/82a+RlzzZ5ie2vxu2eYclkBvTy1ATXH1B9cnvZ0O71Pzsy/mlfDb5P3HhOg0JzQKkDRhctyA==",
"license": "LGPL-2.1-or-later",
"dependencies": {
"@types/geojson": "^7946.0.16",
"@types/node": ">=18",
"denque": "^2.1.0",
"iconv-lite": "^0.7.2",
"lru-cache": "^10.4.3"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mariadb/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "clproject-env-test",
"version": "1.0.0",
"private": true,
"description": "Containerized Node.js + MariaDB development environment smoke test",
"main": "src/server.js",
"scripts": {
"dev": "node --watch src/server.js",
"start": "node src/server.js",
"test:environment": "node scripts/test-environment.js"
},
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mariadb": "^3.4.0"
}
}
+122
View File
@@ -0,0 +1,122 @@
require("dotenv").config();
const assert = require("node:assert/strict");
const http = require("node:http");
const https = require("node:https");
const mariadb = require("mariadb");
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
function getJson(targetUrl) {
const url = new URL(targetUrl);
const transport = url.protocol === "https:" ? https : http;
return new Promise((resolve, reject) => {
const request = transport.get(url, (response) => {
let body = "";
response.on("data", (chunk) => {
body += chunk;
});
response.on("end", () => {
try {
resolve({
body: JSON.parse(body),
statusCode: response.statusCode
});
} catch (error) {
reject(error);
}
});
});
request.on("error", reject);
});
}
async function testApp() {
const appPort = Number(process.env.APP_PORT || 3000);
const candidates = unique([
process.env.APP_URL,
`http://localhost:${appPort}`,
`http://app:${appPort}`
]);
let lastError;
for (const baseUrl of candidates) {
try {
const response = await getJson(`${baseUrl}/health`);
assert.equal(response.statusCode, 200, `Unexpected status from ${baseUrl}`);
assert.equal(response.body.status, "ok", "Application health endpoint is not healthy");
assert.equal(response.body.database, "reachable", "Application cannot reach MariaDB");
return { baseUrl, payload: response.body };
} catch (error) {
lastError = error;
}
}
throw lastError;
}
async function testDatabase() {
const candidates = unique([
process.env.DB_HOST,
"127.0.0.1",
"localhost",
"db"
]);
let lastError;
for (const host of candidates) {
const pool = mariadb.createPool({
host,
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: 1
});
try {
const connection = await pool.getConnection();
const rows = await connection.query(
"SELECT name FROM environment_checks ORDER BY id"
);
connection.release();
await pool.end();
assert.ok(rows.length >= 1, "Seed table is empty");
return { host, rows };
} catch (error) {
lastError = error;
await pool.end();
}
}
throw lastError;
}
async function main() {
const appResult = await testApp();
const databaseResult = await testDatabase();
console.log("Environment test passed");
console.log(`App endpoint: ${appResult.baseUrl}`);
console.log(`Database host: ${databaseResult.host}`);
console.log(`Seed rows: ${databaseResult.rows.length}`);
}
main().catch((error) => {
console.error("Environment test failed");
console.error(error.message);
process.exit(1);
});
+32
View File
@@ -0,0 +1,32 @@
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
};
+73
View File
@@ -0,0 +1,73 @@
const express = require("express");
const { closePool, executeQuery } = require("./db");
const app = express();
const port = Number(process.env.APP_PORT || 3000);
app.get("/", (_request, response) => {
response.json({
service: "clproject-env-test",
status: "running",
endpoints: ["/health", "/db/check"]
});
});
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()
});
}
});
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", () => {
shutdown("SIGINT");
});
process.on("SIGTERM", () => {
shutdown("SIGTERM");
});