digitaltwin-core 0.14.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +494 -359
  3. package/dist/auth/apisix_parser.d.ts +141 -0
  4. package/dist/auth/apisix_parser.d.ts.map +1 -0
  5. package/dist/auth/apisix_parser.js +161 -0
  6. package/dist/auth/apisix_parser.js.map +1 -0
  7. package/dist/auth/auth_config.d.ts +126 -0
  8. package/dist/auth/auth_config.d.ts.map +1 -0
  9. package/dist/auth/auth_config.js +169 -0
  10. package/dist/auth/auth_config.js.map +1 -0
  11. package/dist/auth/auth_provider.d.ts +118 -0
  12. package/dist/auth/auth_provider.d.ts.map +1 -0
  13. package/dist/auth/auth_provider.js +8 -0
  14. package/dist/auth/auth_provider.js.map +1 -0
  15. package/dist/auth/auth_provider_factory.d.ts +91 -0
  16. package/dist/auth/auth_provider_factory.d.ts.map +1 -0
  17. package/dist/auth/auth_provider_factory.js +146 -0
  18. package/dist/auth/auth_provider_factory.js.map +1 -0
  19. package/dist/auth/index.d.ts +8 -0
  20. package/dist/auth/index.d.ts.map +1 -0
  21. package/dist/auth/index.js +7 -0
  22. package/dist/auth/index.js.map +1 -0
  23. package/dist/auth/providers/gateway_auth_provider.d.ts +78 -0
  24. package/dist/auth/providers/gateway_auth_provider.d.ts.map +1 -0
  25. package/dist/auth/providers/gateway_auth_provider.js +109 -0
  26. package/dist/auth/providers/gateway_auth_provider.js.map +1 -0
  27. package/dist/auth/providers/index.d.ts +4 -0
  28. package/dist/auth/providers/index.d.ts.map +1 -0
  29. package/dist/auth/providers/index.js +4 -0
  30. package/dist/auth/providers/index.js.map +1 -0
  31. package/dist/auth/providers/jwt_auth_provider.d.ts +91 -0
  32. package/dist/auth/providers/jwt_auth_provider.d.ts.map +1 -0
  33. package/dist/auth/providers/jwt_auth_provider.js +204 -0
  34. package/dist/auth/providers/jwt_auth_provider.js.map +1 -0
  35. package/dist/auth/providers/no_auth_provider.d.ts +61 -0
  36. package/dist/auth/providers/no_auth_provider.d.ts.map +1 -0
  37. package/dist/auth/providers/no_auth_provider.js +76 -0
  38. package/dist/auth/providers/no_auth_provider.js.map +1 -0
  39. package/dist/auth/types.d.ts +100 -0
  40. package/dist/auth/types.d.ts.map +1 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/auth/types.js.map +1 -0
  43. package/dist/auth/user_service.d.ts +86 -0
  44. package/dist/auth/user_service.d.ts.map +1 -0
  45. package/dist/auth/user_service.js +237 -0
  46. package/dist/auth/user_service.js.map +1 -0
  47. package/dist/components/assets_manager.d.ts +662 -0
  48. package/dist/components/assets_manager.d.ts.map +1 -0
  49. package/dist/components/assets_manager.js +1537 -0
  50. package/dist/components/assets_manager.js.map +1 -0
  51. package/dist/components/async_upload.d.ts +20 -0
  52. package/dist/components/async_upload.d.ts.map +1 -0
  53. package/dist/components/async_upload.js +10 -0
  54. package/dist/components/async_upload.js.map +1 -0
  55. package/dist/components/collector.d.ts +203 -0
  56. package/dist/components/collector.d.ts.map +1 -0
  57. package/dist/components/collector.js +214 -0
  58. package/dist/components/collector.js.map +1 -0
  59. package/dist/components/custom_table_manager.d.ts +503 -0
  60. package/dist/components/custom_table_manager.d.ts.map +1 -0
  61. package/dist/components/custom_table_manager.js +1023 -0
  62. package/dist/components/custom_table_manager.js.map +1 -0
  63. package/dist/components/global_assets_handler.d.ts +63 -0
  64. package/dist/components/global_assets_handler.d.ts.map +1 -0
  65. package/dist/components/global_assets_handler.js +127 -0
  66. package/dist/components/global_assets_handler.js.map +1 -0
  67. package/dist/components/handler.d.ts +104 -0
  68. package/dist/components/handler.d.ts.map +1 -0
  69. package/dist/components/handler.js +110 -0
  70. package/dist/components/handler.js.map +1 -0
  71. package/dist/components/harvester.d.ts +182 -0
  72. package/dist/components/harvester.d.ts.map +1 -0
  73. package/dist/components/harvester.js +406 -0
  74. package/dist/components/harvester.js.map +1 -0
  75. package/dist/components/index.d.ts +11 -0
  76. package/dist/components/index.d.ts.map +1 -0
  77. package/dist/components/index.js +9 -0
  78. package/dist/components/index.js.map +1 -0
  79. package/dist/components/interfaces.d.ts +126 -0
  80. package/dist/components/interfaces.d.ts.map +1 -0
  81. package/dist/components/interfaces.js +8 -0
  82. package/dist/components/interfaces.js.map +1 -0
  83. package/dist/components/map_manager.d.ts +61 -0
  84. package/dist/components/map_manager.d.ts.map +1 -0
  85. package/dist/components/map_manager.js +242 -0
  86. package/dist/components/map_manager.js.map +1 -0
  87. package/dist/components/tileset_manager.d.ts +125 -0
  88. package/dist/components/tileset_manager.d.ts.map +1 -0
  89. package/dist/components/tileset_manager.js +623 -0
  90. package/dist/components/tileset_manager.js.map +1 -0
  91. package/dist/components/types.d.ts +226 -0
  92. package/dist/components/types.d.ts.map +1 -0
  93. package/dist/components/types.js +8 -0
  94. package/dist/components/types.js.map +1 -0
  95. package/dist/database/adapters/knex_database_adapter.d.ts +97 -0
  96. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -0
  97. package/dist/database/adapters/knex_database_adapter.js +729 -0
  98. package/dist/database/adapters/knex_database_adapter.js.map +1 -0
  99. package/dist/database/database_adapter.d.ts +262 -0
  100. package/dist/database/database_adapter.d.ts.map +1 -0
  101. package/dist/database/database_adapter.js +46 -0
  102. package/dist/database/database_adapter.js.map +1 -0
  103. package/dist/engine/digital_twin_engine.d.ts +295 -0
  104. package/dist/engine/digital_twin_engine.d.ts.map +1 -0
  105. package/dist/engine/digital_twin_engine.js +907 -0
  106. package/dist/engine/digital_twin_engine.js.map +1 -0
  107. package/dist/engine/endpoints.d.ts +47 -0
  108. package/dist/engine/endpoints.d.ts.map +1 -0
  109. package/dist/engine/endpoints.js +88 -0
  110. package/dist/engine/endpoints.js.map +1 -0
  111. package/dist/engine/error_handler.d.ts +20 -0
  112. package/dist/engine/error_handler.d.ts.map +1 -0
  113. package/dist/engine/error_handler.js +69 -0
  114. package/dist/engine/error_handler.js.map +1 -0
  115. package/dist/engine/events.d.ts +93 -0
  116. package/dist/engine/events.d.ts.map +1 -0
  117. package/dist/engine/events.js +71 -0
  118. package/dist/engine/events.js.map +1 -0
  119. package/dist/engine/health.d.ts +112 -0
  120. package/dist/engine/health.d.ts.map +1 -0
  121. package/dist/engine/health.js +190 -0
  122. package/dist/engine/health.js.map +1 -0
  123. package/dist/engine/initializer.d.ts +62 -0
  124. package/dist/engine/initializer.d.ts.map +1 -0
  125. package/dist/engine/initializer.js +108 -0
  126. package/dist/engine/initializer.js.map +1 -0
  127. package/dist/engine/queue_manager.d.ts +87 -0
  128. package/dist/engine/queue_manager.d.ts.map +1 -0
  129. package/dist/engine/queue_manager.js +196 -0
  130. package/dist/engine/queue_manager.js.map +1 -0
  131. package/dist/engine/scheduler.d.ts +30 -0
  132. package/dist/engine/scheduler.d.ts.map +1 -0
  133. package/dist/engine/scheduler.js +378 -0
  134. package/dist/engine/scheduler.js.map +1 -0
  135. package/dist/engine/upload_processor.d.ts +36 -0
  136. package/dist/engine/upload_processor.d.ts.map +1 -0
  137. package/dist/engine/upload_processor.js +113 -0
  138. package/dist/engine/upload_processor.js.map +1 -0
  139. package/dist/env/env.d.ts +134 -0
  140. package/dist/env/env.d.ts.map +1 -0
  141. package/dist/env/env.js +177 -0
  142. package/dist/env/env.js.map +1 -0
  143. package/dist/errors/index.d.ts +94 -0
  144. package/dist/errors/index.d.ts.map +1 -0
  145. package/dist/errors/index.js +149 -0
  146. package/dist/errors/index.js.map +1 -0
  147. package/dist/index.d.ts +55 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +65 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/openapi/generator.d.ts +93 -0
  152. package/dist/openapi/generator.d.ts.map +1 -0
  153. package/dist/openapi/generator.js +293 -0
  154. package/dist/openapi/generator.js.map +1 -0
  155. package/dist/openapi/index.d.ts +9 -0
  156. package/dist/openapi/index.d.ts.map +1 -0
  157. package/dist/openapi/index.js +9 -0
  158. package/dist/openapi/index.js.map +1 -0
  159. package/dist/openapi/types.d.ts +182 -0
  160. package/dist/openapi/types.d.ts.map +1 -0
  161. package/dist/openapi/types.js +16 -0
  162. package/dist/openapi/types.js.map +1 -0
  163. package/dist/storage/adapters/local_storage_service.d.ts +57 -0
  164. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -0
  165. package/dist/storage/adapters/local_storage_service.js +132 -0
  166. package/dist/storage/adapters/local_storage_service.js.map +1 -0
  167. package/dist/storage/adapters/ovh_storage_service.d.ts +72 -0
  168. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -0
  169. package/dist/storage/adapters/ovh_storage_service.js +205 -0
  170. package/dist/storage/adapters/ovh_storage_service.js.map +1 -0
  171. package/dist/storage/storage_factory.d.ts +14 -0
  172. package/dist/storage/storage_factory.d.ts.map +1 -0
  173. package/dist/storage/storage_factory.js +43 -0
  174. package/dist/storage/storage_factory.js.map +1 -0
  175. package/dist/storage/storage_service.d.ts +163 -0
  176. package/dist/storage/storage_service.d.ts.map +1 -0
  177. package/dist/storage/storage_service.js +58 -0
  178. package/dist/storage/storage_service.js.map +1 -0
  179. package/dist/types/data_record.d.ts +123 -0
  180. package/dist/types/data_record.d.ts.map +1 -0
  181. package/dist/types/data_record.js +8 -0
  182. package/dist/types/data_record.js.map +1 -0
  183. package/dist/utils/graceful_shutdown.d.ts +44 -0
  184. package/dist/utils/graceful_shutdown.d.ts.map +1 -0
  185. package/dist/utils/graceful_shutdown.js +79 -0
  186. package/dist/utils/graceful_shutdown.js.map +1 -0
  187. package/dist/utils/http_responses.d.ts +175 -0
  188. package/dist/utils/http_responses.d.ts.map +1 -0
  189. package/dist/utils/http_responses.js +216 -0
  190. package/dist/utils/http_responses.js.map +1 -0
  191. package/dist/utils/index.d.ts +8 -0
  192. package/dist/utils/index.d.ts.map +1 -0
  193. package/dist/utils/index.js +6 -0
  194. package/dist/utils/index.js.map +1 -0
  195. package/dist/utils/logger.d.ts +74 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +92 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/map_to_data_record.d.ts +10 -0
  200. package/dist/utils/map_to_data_record.d.ts.map +1 -0
  201. package/dist/utils/map_to_data_record.js +36 -0
  202. package/dist/utils/map_to_data_record.js.map +1 -0
  203. package/dist/utils/safe_async.d.ts +50 -0
  204. package/dist/utils/safe_async.d.ts.map +1 -0
  205. package/dist/utils/safe_async.js +90 -0
  206. package/dist/utils/safe_async.js.map +1 -0
  207. package/dist/utils/servable_endpoint.d.ts +63 -0
  208. package/dist/utils/servable_endpoint.d.ts.map +1 -0
  209. package/dist/utils/servable_endpoint.js +67 -0
  210. package/dist/utils/servable_endpoint.js.map +1 -0
  211. package/dist/utils/zip_utils.d.ts +66 -0
  212. package/dist/utils/zip_utils.d.ts.map +1 -0
  213. package/dist/utils/zip_utils.js +169 -0
  214. package/dist/utils/zip_utils.js.map +1 -0
  215. package/dist/validation/index.d.ts +3 -0
  216. package/dist/validation/index.d.ts.map +1 -0
  217. package/dist/validation/index.js +7 -0
  218. package/dist/validation/index.js.map +1 -0
  219. package/dist/validation/schemas.d.ts +273 -0
  220. package/dist/validation/schemas.d.ts.map +1 -0
  221. package/dist/validation/schemas.js +82 -0
  222. package/dist/validation/schemas.js.map +1 -0
  223. package/dist/validation/validate.d.ts +49 -0
  224. package/dist/validation/validate.d.ts.map +1 -0
  225. package/dist/validation/validate.js +110 -0
  226. package/dist/validation/validate.js.map +1 -0
  227. package/package.json +23 -13
package/README.md CHANGED
@@ -1,360 +1,495 @@
1
- # Digital Twin Core
2
-
3
- Digital Twin Core is a minimalist TypeScript framework used to collect and process data for Digital Twin projects. It provides building blocks to create scheduled collectors, harvesters and HTTP handlers while abstracting storage and database access.
4
-
5
- ## Features
6
-
7
- - **Collectors** - fetch regular data from APIs (typically JSON) based on a Buffer schedule, store it and expose it via GET endpoints.
8
- - **Harvesters** – transform data collected by collectors, store the results and expose them via GET endpoints.
9
- - **Handlers** – expose GET endpoints that directly return the result of the method defined in the decorator.
10
- - **Assets Manager** – upload, store and manage file assets with metadata, providing RESTful endpoints for CRUD operations.
11
- - **Custom Table Manager** – manage structured data in custom database tables with automatic CRUD endpoints and custom business logic endpoints.
12
- - **Storage adapters** – currently local filesystem and OVH Object Storage via S3 API.
13
- - **Database adapter** – implemented with [Knex](https://knexjs.org/) to index metadata.
14
- - **Engine** – orchestrates components, schedules jobs with BullMQ and exposes endpoints via Express.
15
-
16
- ## Installation
17
-
18
- ```bash
19
- pnpm add digitaltwin-core
20
- ```
21
-
22
- The project requires Node.js 20 or later.
23
-
24
- ## Building
25
-
26
- Compile the TypeScript sources to `dist/`:
27
-
28
- ```bash
29
- npm run build
30
- ```
31
-
32
- During development you can use the watcher:
33
-
34
- ```bash
35
- npm run dev
36
- ```
37
-
38
- ## Running tests
39
-
40
- The test suite uses [Japa](https://github.com/japa/runner). Run all tests with:
41
-
42
- ```bash
43
- npm test
44
- ```
45
-
46
- ## Example usage
47
-
48
- Below is a very small example showing how the engine may be instantiated. Storage and database implementations are selected through the provided factories.
49
-
50
- ```ts
51
- import { DigitalTwinEngine } from './src/engine/digital_twin_engine.js';
52
- import { StorageServiceFactory } from './src/storage/storage_factory.js';
53
- import { KnexDatabaseAdapter } from './src/database/adapters/knex_database_adapter.js';
54
- import { Env } from './src/.env/.env.js';
55
-
56
- // Validate environment variables and bootstrap services
57
- const env = Env.validate({
58
- STORAGE_CONFIG: Env.schema.enum(['local', 'ovh'])
59
- });
60
-
61
- const storage = StorageServiceFactory.create();
62
- const database = new KnexDatabaseAdapter({ client: 'sqlite3', connection: ':memory:' }, storage);
63
-
64
- const engine = new DigitalTwinEngine({ storage, database });
65
- engine.start();
66
- ```
67
-
68
- ## Components
69
-
70
- ### Collectors
71
-
72
- Collectors are scheduled components that fetch data from external sources at regular intervals. They implement a `collect()` method that returns a Buffer, which is then stored and exposed via HTTP endpoints.
73
-
74
- **Key features:**
75
- - Cron-based scheduling
76
- - Automatic storage and metadata indexing
77
- - HTTP GET endpoint for retrieving latest data
78
- - Event emission on successful collection
79
-
80
- ### Assets Manager
81
-
82
- The Assets Manager provides a complete solution for file asset management with metadata support. It's an abstract base class that can be extended for specific asset types.
83
-
84
- **Key features:**
85
- - File upload with metadata (description, source URL, owner, filename)
86
- - RESTful CRUD operations via HTTP endpoints
87
- - Content-type aware storage and retrieval
88
- - Separate display and download endpoints
89
- - Source URL validation for data provenance
90
- - File extension validation for upload security
91
- - Component isolation (each manager handles its own asset type)
92
-
93
- **Available endpoints:**
94
- - `GET /{assetType}` - List all assets with metadata
95
- - `POST /{assetType}/upload` - Upload new asset with metadata
96
- - `GET /{assetType}/{id}` - Retrieve asset content for display
97
- - `GET /{assetType}/{id}/download` - Download asset with attachment headers
98
- - `PUT /{assetType}/{id}` - Update asset metadata
99
- - `DELETE /{assetType}/{id}` - Delete asset
100
-
101
- **Example usage:**
102
- ```typescript
103
- class GLTFAssetsManager extends AssetsManager {
104
- getConfiguration() {
105
- return {
106
- name: 'gltf',
107
- description: 'GLTF 3D models manager',
108
- contentType: 'model/gltf-binary',
109
- extension: '.glb', // Optional: restricts uploads to .glb files only
110
- tags: ['assets', '3d', 'gltf']
111
- }
112
- }
113
- }
114
- ```
115
-
116
- **File Extension Validation:**
117
-
118
- When the `extension` property is set in the configuration, the Assets Manager will automatically validate uploaded files:
119
- - POST `/upload` and POST `/upload-batch` endpoints will reject files that don't match the specified extension
120
- - Validation is case-insensitive (`.GLB` and `.glb` are treated the same)
121
- - If no extension is specified, all file types are accepted
122
- - Error message clearly indicates the expected extension
123
-
124
- ```typescript
125
- // Example with extension validation
126
- class DocumentsManager extends AssetsManager {
127
- getConfiguration() {
128
- return {
129
- name: 'documents',
130
- description: 'PDF documents manager',
131
- contentType: 'application/pdf',
132
- extension: '.pdf' // Only PDF files allowed
133
- }
134
- }
135
- }
136
-
137
- // Upload attempt with wrong extension will return:
138
- // Status: 400
139
- // Error: "Invalid file extension. Expected: .pdf"
140
- ```
141
-
142
- ### Custom Table Manager
143
-
144
- The Custom Table Manager provides a powerful solution for managing structured data with custom database tables. It automatically generates CRUD endpoints and supports custom business logic endpoints.
145
-
146
- **Key features:**
147
- - Custom database table creation with configurable columns and SQL types
148
- - Automatic CRUD endpoints (GET, POST, PUT, DELETE)
149
- - Custom business logic endpoints with full request/response control
150
- - Query validation and field requirements
151
- - Built-in search and filtering capabilities
152
- - Support for complex data relationships
153
-
154
- **Available endpoints (automatic):**
155
- - `GET /{tableName}` - List all records
156
- - `POST /{tableName}` - Create new record
157
- - `GET /{tableName}/{id}` - Get specific record
158
- - `PUT /{tableName}/{id}` - Update specific record
159
- - `DELETE /{tableName}/{id}` - Delete specific record
160
-
161
- **Example usage:**
162
- ```typescript
163
- class WMSLayersManager extends CustomTableManager {
164
- getConfiguration() {
165
- return {
166
- name: 'wms_layers',
167
- description: 'Manage WMS layers for mapping applications',
168
- columns: {
169
- 'wms_url': 'text not null',
170
- 'layer_name': 'text not null',
171
- 'description': 'text',
172
- 'active': 'boolean default true',
173
- 'created_by': 'text',
174
- 'projection': 'text default "EPSG:4326"'
175
- },
176
- // Custom endpoints for business logic
177
- endpoints: [
178
- { path: '/add-layers', method: 'post', handler: 'addMultipleLayers' },
179
- { path: '/activate/:id', method: 'put', handler: 'toggleLayerStatus' },
180
- { path: '/search', method: 'get', handler: 'searchLayers' },
181
- { path: '/by-projection/:projection', method: 'get', handler: 'findByProjection' }
182
- ]
183
- }
184
- }
185
-
186
- // Custom endpoint: Add multiple layers at once
187
- async addMultipleLayers(req: any): Promise<DataResponse> {
188
- try {
189
- const { layers } = req.body
190
- const results = []
191
-
192
- for (const layerData of layers) {
193
- // Use built-in validation
194
- const id = await this.create({
195
- wms_url: layerData.url,
196
- layer_name: layerData.name,
197
- description: layerData.description || '',
198
- active: true,
199
- created_by: layerData.user || 'system'
200
- })
201
- results.push({ id, name: layerData.name })
202
- }
203
-
204
- return {
205
- status: 200,
206
- content: JSON.stringify({
207
- message: `Successfully added ${results.length} layers`,
208
- layers: results
209
- }),
210
- headers: { 'Content-Type': 'application/json' }
211
- }
212
- } catch (error) {
213
- return {
214
- status: 400,
215
- content: JSON.stringify({ error: error.message }),
216
- headers: { 'Content-Type': 'application/json' }
217
- }
218
- }
219
- }
220
-
221
- // Custom endpoint: Toggle layer active status
222
- async toggleLayerStatus(req: any): Promise<DataResponse> {
223
- try {
224
- const { id } = req.params
225
- const layer = await this.findById(parseInt(id))
226
-
227
- if (!layer) {
228
- return {
229
- status: 404,
230
- content: JSON.stringify({ error: 'Layer not found' }),
231
- headers: { 'Content-Type': 'application/json' }
232
- }
233
- }
234
-
235
- const newStatus = !layer.active
236
- await this.update(parseInt(id), { active: newStatus })
237
-
238
- return {
239
- status: 200,
240
- content: JSON.stringify({
241
- message: `Layer ${newStatus ? 'activated' : 'deactivated'}`,
242
- layer_id: id,
243
- active: newStatus
244
- }),
245
- headers: { 'Content-Type': 'application/json' }
246
- }
247
- } catch (error) {
248
- return {
249
- status: 500,
250
- content: JSON.stringify({ error: error.message }),
251
- headers: { 'Content-Type': 'application/json' }
252
- }
253
- }
254
- }
255
-
256
- // Custom endpoint: Advanced search with validation
257
- async searchLayers(req: any): Promise<DataResponse> {
258
- try {
259
- const { query, active_only, projection } = req.query
260
- const conditions: Record<string, any> = {}
261
-
262
- if (active_only === 'true') {
263
- conditions.active = true
264
- }
265
-
266
- if (projection) {
267
- conditions.projection = projection
268
- }
269
-
270
- // Use built-in search with validation
271
- const layers = await this.findByColumns(conditions, {
272
- validate: (conditions) => {
273
- if (query && query.length < 3) {
274
- throw new Error('Search query must be at least 3 characters long')
275
- }
276
- }
277
- })
278
-
279
- // Filter by text search if provided
280
- let results = layers
281
- if (query) {
282
- results = layers.filter(layer =>
283
- layer.layer_name.toLowerCase().includes(query.toLowerCase()) ||
284
- layer.description?.toLowerCase().includes(query.toLowerCase())
285
- )
286
- }
287
-
288
- return {
289
- status: 200,
290
- content: JSON.stringify({
291
- results,
292
- total: results.length,
293
- query: { query, active_only, projection }
294
- }),
295
- headers: { 'Content-Type': 'application/json' }
296
- }
297
- } catch (error) {
298
- return {
299
- status: 400,
300
- content: JSON.stringify({ error: error.message }),
301
- headers: { 'Content-Type': 'application/json' }
302
- }
303
- }
304
- }
305
- }
306
- ```
307
-
308
- **Generated endpoints for above example:**
309
- - Standard CRUD: `GET /wms_layers`, `POST /wms_layers`, etc.
310
- - Custom business logic: `POST /wms_layers/add-layers`, `PUT /wms_layers/activate/:id`, `GET /wms_layers/search`
311
-
312
- **SQL Types supported:**
313
- - `text` / `text not null` - Variable length text
314
- - `varchar(255)` / `varchar(100) not null` - Fixed length text
315
- - `integer` / `integer not null` - Whole numbers
316
- - `boolean` / `boolean default true` - True/false values
317
- - `datetime` / `timestamp` - Date and time values
318
- - `real` / `decimal` / `float` - Decimal numbers
319
-
320
- **Built-in query methods:**
321
- - `findAll()` - Get all records
322
- - `findById(id)` - Get specific record
323
- - `findByColumn(column, value)` - Search by single column
324
- - `findByColumns(conditions, validation)` - Advanced search with validation
325
- - `create(data)` - Create new record
326
- - `update(id, data)` - Update existing record
327
- - `delete(id)` - Delete record
328
-
329
- ## Project Scaffolding
330
-
331
- Use [create-digitaltwin](https://github.com/CePseudoBE/create-digitaltwin) to quickly bootstrap new projects:
332
-
333
- ```bash
334
- npm init digitaltwin my-project
335
- cd my-project
336
- npm install
337
- npm run dev
338
- ```
339
-
340
- Generated projects include [digitaltwin-cli](https://github.com/CePseudoBE/digitaltwin-cli) for component generation:
341
-
342
- ```bash
343
- node dt make:collector WeatherCollector --description "Weather data collector"
344
- node dt make:handler ApiHandler --method post
345
- node dt make:harvester DataProcessor --source weather-collector
346
- ```
347
-
348
- ## Folder structure
349
-
350
- - `src/` framework sources
351
- - `components/` – base classes for collectors, harvesters, handlers and assets manager
352
- - `engine/` – orchestration logic
353
- - `storage/` – storage service abstractions and adapters
354
- - `database/` – metadata database adapter
355
- - `env/` – environment configuration helper
356
- - `tests/` – unit tests
357
-
358
- ---
359
-
1
+ # Digital Twin Core
2
+
3
+ Digital Twin Core is a minimalist TypeScript framework used to collect and process data for Digital Twin projects. It provides building blocks to create scheduled collectors, harvesters and HTTP handlers while abstracting storage and database access.
4
+
5
+ ## Features
6
+
7
+ - **Collectors** - fetch regular data from APIs (typically JSON) based on a Buffer schedule, store it and expose it via GET endpoints.
8
+ - **Harvesters** – transform data collected by collectors, store the results and expose them via GET endpoints.
9
+ - **Handlers** – expose GET endpoints that directly return the result of the method defined in the decorator.
10
+ - **Assets Manager** – upload, store and manage file assets with metadata, providing RESTful endpoints for CRUD operations.
11
+ - **Custom Table Manager** – manage structured data in custom database tables with automatic CRUD endpoints and custom business logic endpoints.
12
+ - **Storage adapters** – currently local filesystem and OVH Object Storage via S3 API.
13
+ - **Database adapter** – implemented with [Knex](https://knexjs.org/) to index metadata.
14
+ - **Engine** – orchestrates components, schedules jobs with BullMQ and exposes endpoints via Express.
15
+ - **Authentication** – pluggable authentication system supporting API gateway headers, JWT tokens, or no-auth mode.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add digitaltwin-core
21
+ ```
22
+
23
+ The project requires Node.js 20 or later.
24
+
25
+ ## Building
26
+
27
+ Compile the TypeScript sources to `dist/`:
28
+
29
+ ```bash
30
+ npm run build
31
+ ```
32
+
33
+ During development you can use the watcher:
34
+
35
+ ```bash
36
+ npm run dev
37
+ ```
38
+
39
+ ## Running tests
40
+
41
+ The test suite uses [Japa](https://github.com/japa/runner). Run all tests with:
42
+
43
+ ```bash
44
+ npm test
45
+ ```
46
+
47
+ ## Example usage
48
+
49
+ Below is a very small example showing how the engine may be instantiated. Storage and database implementations are selected through the provided factories.
50
+
51
+ ```ts
52
+ import { DigitalTwinEngine } from './src/engine/digital_twin_engine.js';
53
+ import { StorageServiceFactory } from './src/storage/storage_factory.js';
54
+ import { KnexDatabaseAdapter } from './src/database/adapters/knex_database_adapter.js';
55
+ import { Env } from './src/.env/.env.js';
56
+
57
+ // Validate environment variables and bootstrap services
58
+ const env = Env.validate({
59
+ STORAGE_CONFIG: Env.schema.enum(['local', 'ovh'])
60
+ });
61
+
62
+ const storage = StorageServiceFactory.create();
63
+ const database = new KnexDatabaseAdapter({ client: 'sqlite3', connection: ':memory:' }, storage);
64
+
65
+ const engine = new DigitalTwinEngine({ storage, database });
66
+ engine.start();
67
+ ```
68
+
69
+ ## Components
70
+
71
+ ### Collectors
72
+
73
+ Collectors are scheduled components that fetch data from external sources at regular intervals. They implement a `collect()` method that returns a Buffer, which is then stored and exposed via HTTP endpoints.
74
+
75
+ **Key features:**
76
+ - Cron-based scheduling
77
+ - Automatic storage and metadata indexing
78
+ - HTTP GET endpoint for retrieving latest data
79
+ - Event emission on successful collection
80
+
81
+ ### Assets Manager
82
+
83
+ The Assets Manager provides a complete solution for file asset management with metadata support. It's an abstract base class that can be extended for specific asset types.
84
+
85
+ **Key features:**
86
+ - File upload with metadata (description, source URL, owner, filename)
87
+ - RESTful CRUD operations via HTTP endpoints
88
+ - Content-type aware storage and retrieval
89
+ - Separate display and download endpoints
90
+ - Source URL validation for data provenance
91
+ - File extension validation for upload security
92
+ - Component isolation (each manager handles its own asset type)
93
+
94
+ **Available endpoints:**
95
+ - `GET /{assetType}` - List all assets with metadata
96
+ - `POST /{assetType}/upload` - Upload new asset with metadata
97
+ - `GET /{assetType}/{id}` - Retrieve asset content for display
98
+ - `GET /{assetType}/{id}/download` - Download asset with attachment headers
99
+ - `PUT /{assetType}/{id}` - Update asset metadata
100
+ - `DELETE /{assetType}/{id}` - Delete asset
101
+
102
+ **Example usage:**
103
+ ```typescript
104
+ class GLTFAssetsManager extends AssetsManager {
105
+ getConfiguration() {
106
+ return {
107
+ name: 'gltf',
108
+ description: 'GLTF 3D models manager',
109
+ contentType: 'model/gltf-binary',
110
+ extension: '.glb', // Optional: restricts uploads to .glb files only
111
+ tags: ['assets', '3d', 'gltf']
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ **File Extension Validation:**
118
+
119
+ When the `extension` property is set in the configuration, the Assets Manager will automatically validate uploaded files:
120
+ - POST `/upload` and POST `/upload-batch` endpoints will reject files that don't match the specified extension
121
+ - Validation is case-insensitive (`.GLB` and `.glb` are treated the same)
122
+ - If no extension is specified, all file types are accepted
123
+ - Error message clearly indicates the expected extension
124
+
125
+ ```typescript
126
+ // Example with extension validation
127
+ class DocumentsManager extends AssetsManager {
128
+ getConfiguration() {
129
+ return {
130
+ name: 'documents',
131
+ description: 'PDF documents manager',
132
+ contentType: 'application/pdf',
133
+ extension: '.pdf' // Only PDF files allowed
134
+ }
135
+ }
136
+ }
137
+
138
+ // Upload attempt with wrong extension will return:
139
+ // Status: 400
140
+ // Error: "Invalid file extension. Expected: .pdf"
141
+ ```
142
+
143
+ ### Custom Table Manager
144
+
145
+ The Custom Table Manager provides a powerful solution for managing structured data with custom database tables. It automatically generates CRUD endpoints and supports custom business logic endpoints.
146
+
147
+ **Key features:**
148
+ - Custom database table creation with configurable columns and SQL types
149
+ - Automatic CRUD endpoints (GET, POST, PUT, DELETE)
150
+ - Custom business logic endpoints with full request/response control
151
+ - Query validation and field requirements
152
+ - Built-in search and filtering capabilities
153
+ - Support for complex data relationships
154
+
155
+ **Available endpoints (automatic):**
156
+ - `GET /{tableName}` - List all records
157
+ - `POST /{tableName}` - Create new record
158
+ - `GET /{tableName}/{id}` - Get specific record
159
+ - `PUT /{tableName}/{id}` - Update specific record
160
+ - `DELETE /{tableName}/{id}` - Delete specific record
161
+
162
+ **Example usage:**
163
+ ```typescript
164
+ class WMSLayersManager extends CustomTableManager {
165
+ getConfiguration() {
166
+ return {
167
+ name: 'wms_layers',
168
+ description: 'Manage WMS layers for mapping applications',
169
+ columns: {
170
+ 'wms_url': 'text not null',
171
+ 'layer_name': 'text not null',
172
+ 'description': 'text',
173
+ 'active': 'boolean default true',
174
+ 'created_by': 'text',
175
+ 'projection': 'text default "EPSG:4326"'
176
+ },
177
+ // Custom endpoints for business logic
178
+ endpoints: [
179
+ { path: '/add-layers', method: 'post', handler: 'addMultipleLayers' },
180
+ { path: '/activate/:id', method: 'put', handler: 'toggleLayerStatus' },
181
+ { path: '/search', method: 'get', handler: 'searchLayers' },
182
+ { path: '/by-projection/:projection', method: 'get', handler: 'findByProjection' }
183
+ ]
184
+ }
185
+ }
186
+
187
+ // Custom endpoint: Add multiple layers at once
188
+ async addMultipleLayers(req: any): Promise<DataResponse> {
189
+ try {
190
+ const { layers } = req.body
191
+ const results = []
192
+
193
+ for (const layerData of layers) {
194
+ // Use built-in validation
195
+ const id = await this.create({
196
+ wms_url: layerData.url,
197
+ layer_name: layerData.name,
198
+ description: layerData.description || '',
199
+ active: true,
200
+ created_by: layerData.user || 'system'
201
+ })
202
+ results.push({ id, name: layerData.name })
203
+ }
204
+
205
+ return {
206
+ status: 200,
207
+ content: JSON.stringify({
208
+ message: `Successfully added ${results.length} layers`,
209
+ layers: results
210
+ }),
211
+ headers: { 'Content-Type': 'application/json' }
212
+ }
213
+ } catch (error) {
214
+ return {
215
+ status: 400,
216
+ content: JSON.stringify({ error: error.message }),
217
+ headers: { 'Content-Type': 'application/json' }
218
+ }
219
+ }
220
+ }
221
+
222
+ // Custom endpoint: Toggle layer active status
223
+ async toggleLayerStatus(req: any): Promise<DataResponse> {
224
+ try {
225
+ const { id } = req.params
226
+ const layer = await this.findById(parseInt(id))
227
+
228
+ if (!layer) {
229
+ return {
230
+ status: 404,
231
+ content: JSON.stringify({ error: 'Layer not found' }),
232
+ headers: { 'Content-Type': 'application/json' }
233
+ }
234
+ }
235
+
236
+ const newStatus = !layer.active
237
+ await this.update(parseInt(id), { active: newStatus })
238
+
239
+ return {
240
+ status: 200,
241
+ content: JSON.stringify({
242
+ message: `Layer ${newStatus ? 'activated' : 'deactivated'}`,
243
+ layer_id: id,
244
+ active: newStatus
245
+ }),
246
+ headers: { 'Content-Type': 'application/json' }
247
+ }
248
+ } catch (error) {
249
+ return {
250
+ status: 500,
251
+ content: JSON.stringify({ error: error.message }),
252
+ headers: { 'Content-Type': 'application/json' }
253
+ }
254
+ }
255
+ }
256
+
257
+ // Custom endpoint: Advanced search with validation
258
+ async searchLayers(req: any): Promise<DataResponse> {
259
+ try {
260
+ const { query, active_only, projection } = req.query
261
+ const conditions: Record<string, any> = {}
262
+
263
+ if (active_only === 'true') {
264
+ conditions.active = true
265
+ }
266
+
267
+ if (projection) {
268
+ conditions.projection = projection
269
+ }
270
+
271
+ // Use built-in search with validation
272
+ const layers = await this.findByColumns(conditions, {
273
+ validate: (conditions) => {
274
+ if (query && query.length < 3) {
275
+ throw new Error('Search query must be at least 3 characters long')
276
+ }
277
+ }
278
+ })
279
+
280
+ // Filter by text search if provided
281
+ let results = layers
282
+ if (query) {
283
+ results = layers.filter(layer =>
284
+ layer.layer_name.toLowerCase().includes(query.toLowerCase()) ||
285
+ layer.description?.toLowerCase().includes(query.toLowerCase())
286
+ )
287
+ }
288
+
289
+ return {
290
+ status: 200,
291
+ content: JSON.stringify({
292
+ results,
293
+ total: results.length,
294
+ query: { query, active_only, projection }
295
+ }),
296
+ headers: { 'Content-Type': 'application/json' }
297
+ }
298
+ } catch (error) {
299
+ return {
300
+ status: 400,
301
+ content: JSON.stringify({ error: error.message }),
302
+ headers: { 'Content-Type': 'application/json' }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ ```
308
+
309
+ **Generated endpoints for above example:**
310
+ - Standard CRUD: `GET /wms_layers`, `POST /wms_layers`, etc.
311
+ - Custom business logic: `POST /wms_layers/add-layers`, `PUT /wms_layers/activate/:id`, `GET /wms_layers/search`
312
+
313
+ **SQL Types supported:**
314
+ - `text` / `text not null` - Variable length text
315
+ - `varchar(255)` / `varchar(100) not null` - Fixed length text
316
+ - `integer` / `integer not null` - Whole numbers
317
+ - `boolean` / `boolean default true` - True/false values
318
+ - `datetime` / `timestamp` - Date and time values
319
+ - `real` / `decimal` / `float` - Decimal numbers
320
+
321
+ **Built-in query methods:**
322
+ - `findAll()` - Get all records
323
+ - `findById(id)` - Get specific record
324
+ - `findByColumn(column, value)` - Search by single column
325
+ - `findByColumns(conditions, validation)` - Advanced search with validation
326
+ - `create(data)` - Create new record
327
+ - `update(id, data)` - Update existing record
328
+ - `delete(id)` - Delete record
329
+
330
+ ## Request Validation
331
+
332
+ The framework includes VineJS integration for type-safe request validation:
333
+
334
+ ```typescript
335
+ import { validate, AssetUploadSchema } from 'digitaltwin-core'
336
+
337
+ // In your component
338
+ async handleUpload(req: any) {
339
+ const data = await validate(AssetUploadSchema, req.body)
340
+ // data is now typed and validated
341
+ }
342
+ ```
343
+
344
+ Validation errors return HTTP 422 with detailed error messages:
345
+
346
+ ```json
347
+ {
348
+ "error": "Validation failed",
349
+ "details": [
350
+ { "field": "description", "message": "The description field must be a string" }
351
+ ]
352
+ }
353
+ ```
354
+
355
+ ## Error Handling
356
+
357
+ The framework provides custom error classes for structured error handling:
358
+
359
+ ```typescript
360
+ import { CollectorError, ValidationError, StorageError } from 'digitaltwin-core'
361
+
362
+ // Errors include context
363
+ throw new CollectorError('Failed to fetch data', 'weather-collector', originalError)
364
+
365
+ // Validation errors return 422
366
+ throw new ValidationError('Invalid input', details)
367
+ ```
368
+
369
+ All component errors are caught and logged with context (component name, stack trace). Non-critical operations use `safeAsync` to log errors without crashing:
370
+
371
+ ```typescript
372
+ import { safeAsync } from 'digitaltwin-core'
373
+
374
+ // Won't throw, just logs on failure
375
+ await safeAsync(() => cleanup(), 'cleanup temporary files', logger)
376
+ ```
377
+
378
+ ## Production Features
379
+
380
+ ### Graceful Shutdown
381
+
382
+ The engine supports graceful shutdown with configurable timeout:
383
+
384
+ ```typescript
385
+ const engine = new DigitalTwinEngine({ database, storage })
386
+
387
+ // Configure shutdown timeout (default: 30s)
388
+ engine.setShutdownTimeout(60000)
389
+
390
+ // Check if shutting down
391
+ if (engine.isShuttingDown()) {
392
+ // Don't accept new work
393
+ }
394
+
395
+ // Graceful stop
396
+ await engine.stop()
397
+ ```
398
+
399
+ ### Health Checks
400
+
401
+ Register custom health checks for monitoring:
402
+
403
+ ```typescript
404
+ engine.registerHealthCheck('external-api', async () => {
405
+ const response = await fetch('https://api.example.com/health')
406
+ return { status: response.ok ? 'up' : 'down' }
407
+ })
408
+
409
+ // Built-in checks: database, redis (if configured)
410
+ const names = engine.getHealthCheckNames() // ['database', 'redis', 'external-api']
411
+
412
+ // Remove check
413
+ engine.removeHealthCheck('external-api')
414
+ ```
415
+
416
+ ### OpenAPI Specification
417
+
418
+ Generate OpenAPI 3.0 specs from your components:
419
+
420
+ ```typescript
421
+ import { OpenAPIGenerator } from 'digitaltwin-core'
422
+
423
+ const spec = OpenAPIGenerator.generate({
424
+ info: { title: 'My API', version: '1.0.0' },
425
+ components: [collector, assetsManager, handler]
426
+ })
427
+
428
+ // Output as JSON or YAML
429
+ const json = OpenAPIGenerator.toJSON(spec)
430
+ const yaml = OpenAPIGenerator.toYAML(spec)
431
+ ```
432
+
433
+ ## Authentication
434
+
435
+ The framework supports multiple authentication modes:
436
+
437
+ - **Gateway** (default): Uses headers from API gateways (Apache APISIX, KrakenD)
438
+ - **JWT**: Direct JWT token validation
439
+ - **None**: Disabled for development/testing
440
+
441
+ ### Gateway Mode (Default)
442
+
443
+ No configuration needed. The framework reads `x-user-id` and `x-user-roles` headers set by your API gateway.
444
+
445
+ ### JWT Mode
446
+
447
+ ```bash
448
+ export AUTH_MODE=jwt
449
+ export JWT_SECRET=your-secret-key
450
+ # Or for RSA: JWT_PUBLIC_KEY or JWT_PUBLIC_KEY_FILE
451
+ ```
452
+
453
+ ### Disable Authentication
454
+
455
+ ```bash
456
+ export DIGITALTWIN_DISABLE_AUTH=true
457
+ # Or
458
+ export AUTH_MODE=none
459
+ ```
460
+
461
+ For detailed configuration options, see [src/auth/README.md](src/auth/README.md).
462
+
463
+ ## Project Scaffolding
464
+
465
+ Use [create-digitaltwin](https://github.com/CePseudoBE/create-digitaltwin) to quickly bootstrap new projects:
466
+
467
+ ```bash
468
+ npm init digitaltwin my-project
469
+ cd my-project
470
+ npm install
471
+ npm run dev
472
+ ```
473
+
474
+ Generated projects include [digitaltwin-cli](https://github.com/CePseudoBE/digitaltwin-cli) for component generation:
475
+
476
+ ```bash
477
+ node dt make:collector WeatherCollector --description "Weather data collector"
478
+ node dt make:handler ApiHandler --method post
479
+ node dt make:harvester DataProcessor --source weather-collector
480
+ ```
481
+
482
+ ## Folder structure
483
+
484
+ - `src/` – framework sources
485
+ - `auth/` – authentication providers and user management
486
+ - `components/` – base classes for collectors, harvesters, handlers and assets manager
487
+ - `engine/` – orchestration logic
488
+ - `storage/` – storage service abstractions and adapters
489
+ - `database/` – metadata database adapter
490
+ - `env/` – environment configuration helper
491
+ - `tests/` – unit tests
492
+
493
+ ---
494
+
360
495
  This project is licensed under the MIT License.