Initial stage
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
FROM bitnami/node:latest
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
copy
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
npm-debug.log
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
node_modules
|
||||||
|
.env.local
|
||||||
|
npm-debug.log
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import cors from 'cors';
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { errorHandler, notFoundHandler } from './middleware/errorHandler.js';
|
||||||
|
import configRoutes from './routes/configRoutes.js';
|
||||||
|
import healthRoutes from './routes/healthRoutes.js';
|
||||||
|
import lookupRoutes from './routes/lookupRoutes.js';
|
||||||
|
import templateRoutes from './routes/templateRoutes.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
app.get('/', (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
service: 'check-list-poc-api',
|
||||||
|
version: '0.1.0',
|
||||||
|
description: 'PoC API for template and configuration delivery.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/api/health', healthRoutes);
|
||||||
|
app.use('/api/templates', templateRoutes);
|
||||||
|
app.use('/api/lookups', lookupRoutes);
|
||||||
|
app.use('/api/config', configRoutes);
|
||||||
|
|
||||||
|
app.use(notFoundHandler);
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const requiredKeys = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
|
||||||
|
|
||||||
|
for (const key of requiredKeys) {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
throw new Error(`Missing required environment variable: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const env = {
|
||||||
|
port: Number(process.env.PORT || 3000),
|
||||||
|
db: {
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: Number(process.env.DB_PORT),
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
connectionLimit: Number(process.env.DB_CONNECTION_LIMIT || 5)
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function notFoundHandler(_req, res) {
|
||||||
|
res.status(404).json({ message: 'Route not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorHandler(error, _req, res, _next) {
|
||||||
|
const statusCode = error.statusCode || 500;
|
||||||
|
|
||||||
|
if (statusCode >= 500) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).json({
|
||||||
|
message: error.message || 'Unexpected server error.'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAppConfig,
|
||||||
|
getExportProfile,
|
||||||
|
getImageRules
|
||||||
|
} from '../services/configService.js';
|
||||||
|
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/image-rules',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
const imageRules = await getImageRules();
|
||||||
|
|
||||||
|
if (!imageRules) {
|
||||||
|
return res.status(404).json({ message: 'Image rules not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(imageRules);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/export',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
const exportProfile = await getExportProfile();
|
||||||
|
|
||||||
|
if (!exportProfile) {
|
||||||
|
return res.status(404).json({ message: 'Export profile not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(exportProfile);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/app-config',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
const config = await getAppConfig();
|
||||||
|
res.json({ items: config });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import { query } from '../db/pool.js';
|
||||||
|
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
await query('SELECT 1 AS ok');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'check-list-poc-api',
|
||||||
|
database: 'connected'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import { getLookup, listLookups } from '../services/lookupService.js';
|
||||||
|
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
const lookups = await listLookups();
|
||||||
|
res.json({ items: lookups });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:lookupCode',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const lookup = await getLookup(req.params.lookupCode);
|
||||||
|
|
||||||
|
if (!lookup) {
|
||||||
|
return res.status(404).json({ message: 'Lookup not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(lookup);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getActiveTemplate,
|
||||||
|
getTemplateVersion,
|
||||||
|
listTemplates
|
||||||
|
} from '../services/templateService.js';
|
||||||
|
import { asyncHandler } from '../utils/asyncHandler.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
asyncHandler(async (_req, res) => {
|
||||||
|
const templates = await listTemplates();
|
||||||
|
res.json({ items: templates });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:templateCode',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const template = await getActiveTemplate(req.params.templateCode);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ message: 'Template not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(template);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:templateCode/versions/:versionNumber',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const template = await getTemplateVersion(
|
||||||
|
req.params.templateCode,
|
||||||
|
req.params.versionNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ message: 'Template version not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(template);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { query } from '../db/pool.js';
|
||||||
|
import { parseJsonColumn } from '../utils/json.js';
|
||||||
|
|
||||||
|
export async function getImageRules() {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
allowed_mime_types_json AS allowedMimeTypes,
|
||||||
|
max_file_size_bytes AS maxFileSizeBytes,
|
||||||
|
max_width_px AS maxWidthPx,
|
||||||
|
max_height_px AS maxHeightPx,
|
||||||
|
jpeg_quality AS jpegQuality,
|
||||||
|
oversize_behavior AS oversizeBehavior,
|
||||||
|
max_attachments_per_field AS maxAttachmentsPerField
|
||||||
|
FROM image_rules
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rows[0],
|
||||||
|
allowedMimeTypes: parseJsonColumn(rows[0].allowedMimeTypes, [])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExportProfile() {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
zip_image_dir AS zipImageDir,
|
||||||
|
excel_sheet_name AS excelSheetName,
|
||||||
|
include_template_version AS includeTemplateVersion,
|
||||||
|
include_export_timestamp AS includeExportTimestamp
|
||||||
|
FROM export_profiles
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.length ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppConfig() {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
config_key AS configKey,
|
||||||
|
config_value_json AS configValue
|
||||||
|
FROM app_config
|
||||||
|
ORDER BY config_key ASC
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
key: row.configKey,
|
||||||
|
value: parseJsonColumn(row.configValue)
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { query } from '../db/pool.js';
|
||||||
|
|
||||||
|
function groupLookups(rows) {
|
||||||
|
const lookups = new Map();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!lookups.has(row.lookupCode)) {
|
||||||
|
lookups.set(row.lookupCode, {
|
||||||
|
code: row.lookupCode,
|
||||||
|
name: row.lookupName,
|
||||||
|
values: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.value !== null) {
|
||||||
|
lookups.get(row.lookupCode).values.push({
|
||||||
|
value: row.value,
|
||||||
|
label: row.label,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
isDefault: Boolean(row.isDefault)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(lookups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listLookups() {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
ls.code AS lookupCode,
|
||||||
|
ls.name AS lookupName,
|
||||||
|
lv.value,
|
||||||
|
lv.label,
|
||||||
|
lv.sort_order AS sortOrder,
|
||||||
|
lv.is_default AS isDefault
|
||||||
|
FROM lookup_sets ls
|
||||||
|
LEFT JOIN lookup_values lv
|
||||||
|
ON lv.lookup_set_id = ls.id
|
||||||
|
AND lv.is_active = 1
|
||||||
|
WHERE ls.is_active = 1
|
||||||
|
ORDER BY ls.code ASC, lv.sort_order ASC, lv.id ASC
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return groupLookups(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLookup(lookupCode) {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
ls.code AS lookupCode,
|
||||||
|
ls.name AS lookupName,
|
||||||
|
lv.value,
|
||||||
|
lv.label,
|
||||||
|
lv.sort_order AS sortOrder,
|
||||||
|
lv.is_default AS isDefault
|
||||||
|
FROM lookup_sets ls
|
||||||
|
LEFT JOIN lookup_values lv
|
||||||
|
ON lv.lookup_set_id = ls.id
|
||||||
|
AND lv.is_active = 1
|
||||||
|
WHERE ls.code = ?
|
||||||
|
AND ls.is_active = 1
|
||||||
|
ORDER BY lv.sort_order ASC, lv.id ASC
|
||||||
|
`,
|
||||||
|
[lookupCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupLookups(rows)[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { query } from '../db/pool.js';
|
||||||
|
import { parseJsonColumn } from '../utils/json.js';
|
||||||
|
|
||||||
|
function mapTemplateRow(row) {
|
||||||
|
return {
|
||||||
|
code: row.code,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
version: row.versionNumber,
|
||||||
|
status: row.status,
|
||||||
|
publishedAt: row.publishedAt,
|
||||||
|
definition: parseJsonColumn(row.definitionJson)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTemplates() {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
t.code,
|
||||||
|
t.name,
|
||||||
|
t.description,
|
||||||
|
tv.version_number AS versionNumber,
|
||||||
|
tv.status,
|
||||||
|
tv.published_at AS publishedAt
|
||||||
|
FROM templates t
|
||||||
|
INNER JOIN template_versions tv
|
||||||
|
ON tv.template_id = t.id
|
||||||
|
AND tv.status = 'active'
|
||||||
|
ORDER BY t.name ASC
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
code: row.code,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
activeVersion: row.versionNumber,
|
||||||
|
publishedAt: row.publishedAt
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveTemplate(templateCode) {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
t.code,
|
||||||
|
t.name,
|
||||||
|
t.description,
|
||||||
|
tv.version_number AS versionNumber,
|
||||||
|
tv.status,
|
||||||
|
tv.published_at AS publishedAt,
|
||||||
|
tv.definition_json AS definitionJson
|
||||||
|
FROM templates t
|
||||||
|
INNER JOIN template_versions tv
|
||||||
|
ON tv.template_id = t.id
|
||||||
|
AND tv.status = 'active'
|
||||||
|
WHERE t.code = ?
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[templateCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.length ? mapTemplateRow(rows[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateVersion(templateCode, versionNumber) {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
t.code,
|
||||||
|
t.name,
|
||||||
|
t.description,
|
||||||
|
tv.version_number AS versionNumber,
|
||||||
|
tv.status,
|
||||||
|
tv.published_at AS publishedAt,
|
||||||
|
tv.definition_json AS definitionJson
|
||||||
|
FROM templates t
|
||||||
|
INNER JOIN template_versions tv
|
||||||
|
ON tv.template_id = t.id
|
||||||
|
WHERE t.code = ?
|
||||||
|
AND tv.version_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[templateCode, Number(versionNumber)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.length ? mapTemplateRow(rows[0]) : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function asyncHandler(handler) {
|
||||||
|
return async function wrappedHandler(req, res, next) {
|
||||||
|
try {
|
||||||
|
await handler(req, res, next);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function parseJsonColumn(value, fallback = null) {
|
||||||
|
if (value == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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);
|
||||||
Generated
+909
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user