commit d9f0b21b102041e2e7e450e57105de8499b2cf05 Author: Stan Date: Thu Apr 9 19:15:39 2026 +0200 Initial stage diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..e3411b8 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,3 @@ +FROM bitnami/node:latest + +WORKDIR /workspace \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b35f46d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e911b18 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +copy +node_modules +.git +npm-debug.log \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4e6810c --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbc310d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +node_modules +.env.local +npm-debug.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ad6c75 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/copy/.env.example b/copy/.env.example new file mode 100644 index 0000000..58592b2 --- /dev/null +++ b/copy/.env.example @@ -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 diff --git a/copy/.gitignore b/copy/.gitignore new file mode 100644 index 0000000..a884d38 --- /dev/null +++ b/copy/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +dist/ diff --git a/copy/Check_List_Hybrid_Initial_Solution_Project_Document_v1.txt b/copy/Check_List_Hybrid_Initial_Solution_Project_Document_v1.txt new file mode 100644 index 0000000..a146be7 --- /dev/null +++ b/copy/Check_List_Hybrid_Initial_Solution_Project_Document_v1.txt @@ -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: +__.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 +.zip +├── .xlsx +└── images/ + ├── .jpg + ├── .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. diff --git a/copy/Initial_Solution_Proposal.md b/copy/Initial_Solution_Proposal.md new file mode 100644 index 0000000..3f9b794 --- /dev/null +++ b/copy/Initial_Solution_Proposal.md @@ -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. \ No newline at end of file diff --git a/copy/README.md b/copy/README.md new file mode 100644 index 0000000..d3b3e4d --- /dev/null +++ b/copy/README.md @@ -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. \ No newline at end of file diff --git a/copy/package.json b/copy/package.json new file mode 100644 index 0000000..0fb5b51 --- /dev/null +++ b/copy/package.json @@ -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" + } +} \ No newline at end of file diff --git a/copy/sql/schema.sql b/copy/sql/schema.sql new file mode 100644 index 0000000..ca3cd5e --- /dev/null +++ b/copy/sql/schema.sql @@ -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) +); diff --git a/copy/sql/seed.sql b/copy/sql/seed.sql new file mode 100644 index 0000000..984b084 --- /dev/null +++ b/copy/sql/seed.sql @@ -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); diff --git a/copy/src/app.js b/copy/src/app.js new file mode 100644 index 0000000..fbb8395 --- /dev/null +++ b/copy/src/app.js @@ -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; diff --git a/copy/src/config/env.js b/copy/src/config/env.js new file mode 100644 index 0000000..acab882 --- /dev/null +++ b/copy/src/config/env.js @@ -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) + } +}; diff --git a/copy/src/db/pool.js b/copy/src/db/pool.js new file mode 100644 index 0000000..6fe79d1 --- /dev/null +++ b/copy/src/db/pool.js @@ -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(); +} diff --git a/copy/src/middleware/errorHandler.js b/copy/src/middleware/errorHandler.js new file mode 100644 index 0000000..308668f --- /dev/null +++ b/copy/src/middleware/errorHandler.js @@ -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.' + }); +} diff --git a/copy/src/routes/configRoutes.js b/copy/src/routes/configRoutes.js new file mode 100644 index 0000000..60fe7ee --- /dev/null +++ b/copy/src/routes/configRoutes.js @@ -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; diff --git a/copy/src/routes/healthRoutes.js b/copy/src/routes/healthRoutes.js new file mode 100644 index 0000000..a7dfcc0 --- /dev/null +++ b/copy/src/routes/healthRoutes.js @@ -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; diff --git a/copy/src/routes/lookupRoutes.js b/copy/src/routes/lookupRoutes.js new file mode 100644 index 0000000..63b8cf2 --- /dev/null +++ b/copy/src/routes/lookupRoutes.js @@ -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; diff --git a/copy/src/routes/templateRoutes.js b/copy/src/routes/templateRoutes.js new file mode 100644 index 0000000..11bb37c --- /dev/null +++ b/copy/src/routes/templateRoutes.js @@ -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; diff --git a/copy/src/server.js b/copy/src/server.js new file mode 100644 index 0000000..f1b38ba --- /dev/null +++ b/copy/src/server.js @@ -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); +}); diff --git a/copy/src/services/configService.js b/copy/src/services/configService.js new file mode 100644 index 0000000..dc86350 --- /dev/null +++ b/copy/src/services/configService.js @@ -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) + })); +} diff --git a/copy/src/services/lookupService.js b/copy/src/services/lookupService.js new file mode 100644 index 0000000..7c5acf0 --- /dev/null +++ b/copy/src/services/lookupService.js @@ -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]; +} diff --git a/copy/src/services/templateService.js b/copy/src/services/templateService.js new file mode 100644 index 0000000..bbf6adf --- /dev/null +++ b/copy/src/services/templateService.js @@ -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; +} diff --git a/copy/src/utils/asyncHandler.js b/copy/src/utils/asyncHandler.js new file mode 100644 index 0000000..1f4c708 --- /dev/null +++ b/copy/src/utils/asyncHandler.js @@ -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); + } + }; +} diff --git a/copy/src/utils/json.js b/copy/src/utils/json.js new file mode 100644 index 0000000..1700f92 --- /dev/null +++ b/copy/src/utils/json.js @@ -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; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d04b50e --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/docker/mariadb/init/01-bootstrap.sql b/docker/mariadb/init/01-bootstrap.sql new file mode 100644 index 0000000..ca2e648 --- /dev/null +++ b/docker/mariadb/init/01-bootstrap.sql @@ -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); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fda415a --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f60180c --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/scripts/test-environment.js b/scripts/test-environment.js new file mode 100644 index 0000000..e83f643 --- /dev/null +++ b/scripts/test-environment.js @@ -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); +}); \ No newline at end of file diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..17d2b10 --- /dev/null +++ b/src/db.js @@ -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 +}; \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..755dea5 --- /dev/null +++ b/src/server.js @@ -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"); +}); \ No newline at end of file