digitaltwin-core 0.14.0 → 0.14.1

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 (169) hide show
  1. package/package.json +101 -106
  2. package/dist/auth/apisix_parser.d.ts +0 -146
  3. package/dist/auth/apisix_parser.d.ts.map +0 -1
  4. package/dist/auth/apisix_parser.js +0 -185
  5. package/dist/auth/apisix_parser.js.map +0 -1
  6. package/dist/auth/auth_config.d.ts +0 -126
  7. package/dist/auth/auth_config.d.ts.map +0 -1
  8. package/dist/auth/auth_config.js +0 -169
  9. package/dist/auth/auth_config.js.map +0 -1
  10. package/dist/auth/index.d.ts +0 -5
  11. package/dist/auth/index.d.ts.map +0 -1
  12. package/dist/auth/index.js +0 -4
  13. package/dist/auth/index.js.map +0 -1
  14. package/dist/auth/types.d.ts +0 -100
  15. package/dist/auth/types.d.ts.map +0 -1
  16. package/dist/auth/types.js +0 -2
  17. package/dist/auth/types.js.map +0 -1
  18. package/dist/auth/user_service.d.ts +0 -86
  19. package/dist/auth/user_service.d.ts.map +0 -1
  20. package/dist/auth/user_service.js +0 -237
  21. package/dist/auth/user_service.js.map +0 -1
  22. package/dist/components/assets_manager.d.ts +0 -662
  23. package/dist/components/assets_manager.d.ts.map +0 -1
  24. package/dist/components/assets_manager.js +0 -1529
  25. package/dist/components/assets_manager.js.map +0 -1
  26. package/dist/components/async_upload.d.ts +0 -20
  27. package/dist/components/async_upload.d.ts.map +0 -1
  28. package/dist/components/async_upload.js +0 -10
  29. package/dist/components/async_upload.js.map +0 -1
  30. package/dist/components/collector.d.ts +0 -203
  31. package/dist/components/collector.d.ts.map +0 -1
  32. package/dist/components/collector.js +0 -202
  33. package/dist/components/collector.js.map +0 -1
  34. package/dist/components/custom_table_manager.d.ts +0 -503
  35. package/dist/components/custom_table_manager.d.ts.map +0 -1
  36. package/dist/components/custom_table_manager.js +0 -1052
  37. package/dist/components/custom_table_manager.js.map +0 -1
  38. package/dist/components/global_assets_handler.d.ts +0 -63
  39. package/dist/components/global_assets_handler.d.ts.map +0 -1
  40. package/dist/components/global_assets_handler.js +0 -127
  41. package/dist/components/global_assets_handler.js.map +0 -1
  42. package/dist/components/handler.d.ts +0 -104
  43. package/dist/components/handler.d.ts.map +0 -1
  44. package/dist/components/handler.js +0 -110
  45. package/dist/components/handler.js.map +0 -1
  46. package/dist/components/harvester.d.ts +0 -182
  47. package/dist/components/harvester.d.ts.map +0 -1
  48. package/dist/components/harvester.js +0 -393
  49. package/dist/components/harvester.js.map +0 -1
  50. package/dist/components/index.d.ts +0 -11
  51. package/dist/components/index.d.ts.map +0 -1
  52. package/dist/components/index.js +0 -9
  53. package/dist/components/index.js.map +0 -1
  54. package/dist/components/interfaces.d.ts +0 -126
  55. package/dist/components/interfaces.d.ts.map +0 -1
  56. package/dist/components/interfaces.js +0 -8
  57. package/dist/components/interfaces.js.map +0 -1
  58. package/dist/components/map_manager.d.ts +0 -61
  59. package/dist/components/map_manager.d.ts.map +0 -1
  60. package/dist/components/map_manager.js +0 -242
  61. package/dist/components/map_manager.js.map +0 -1
  62. package/dist/components/tileset_manager.d.ts +0 -125
  63. package/dist/components/tileset_manager.d.ts.map +0 -1
  64. package/dist/components/tileset_manager.js +0 -618
  65. package/dist/components/tileset_manager.js.map +0 -1
  66. package/dist/components/types.d.ts +0 -226
  67. package/dist/components/types.d.ts.map +0 -1
  68. package/dist/components/types.js +0 -8
  69. package/dist/components/types.js.map +0 -1
  70. package/dist/database/adapters/knex_database_adapter.d.ts +0 -92
  71. package/dist/database/adapters/knex_database_adapter.d.ts.map +0 -1
  72. package/dist/database/adapters/knex_database_adapter.js +0 -647
  73. package/dist/database/adapters/knex_database_adapter.js.map +0 -1
  74. package/dist/database/database_adapter.d.ts +0 -251
  75. package/dist/database/database_adapter.d.ts.map +0 -1
  76. package/dist/database/database_adapter.js +0 -46
  77. package/dist/database/database_adapter.js.map +0 -1
  78. package/dist/engine/digital_twin_engine.d.ts +0 -253
  79. package/dist/engine/digital_twin_engine.d.ts.map +0 -1
  80. package/dist/engine/digital_twin_engine.js +0 -790
  81. package/dist/engine/digital_twin_engine.js.map +0 -1
  82. package/dist/engine/endpoints.d.ts +0 -47
  83. package/dist/engine/endpoints.d.ts.map +0 -1
  84. package/dist/engine/endpoints.js +0 -56
  85. package/dist/engine/endpoints.js.map +0 -1
  86. package/dist/engine/events.d.ts +0 -93
  87. package/dist/engine/events.d.ts.map +0 -1
  88. package/dist/engine/events.js +0 -71
  89. package/dist/engine/events.js.map +0 -1
  90. package/dist/engine/initializer.d.ts +0 -62
  91. package/dist/engine/initializer.d.ts.map +0 -1
  92. package/dist/engine/initializer.js +0 -108
  93. package/dist/engine/initializer.js.map +0 -1
  94. package/dist/engine/queue_manager.d.ts +0 -87
  95. package/dist/engine/queue_manager.d.ts.map +0 -1
  96. package/dist/engine/queue_manager.js +0 -196
  97. package/dist/engine/queue_manager.js.map +0 -1
  98. package/dist/engine/scheduler.d.ts +0 -30
  99. package/dist/engine/scheduler.d.ts.map +0 -1
  100. package/dist/engine/scheduler.js +0 -370
  101. package/dist/engine/scheduler.js.map +0 -1
  102. package/dist/engine/upload_processor.d.ts +0 -36
  103. package/dist/engine/upload_processor.d.ts.map +0 -1
  104. package/dist/engine/upload_processor.js +0 -101
  105. package/dist/engine/upload_processor.js.map +0 -1
  106. package/dist/env/env.d.ts +0 -134
  107. package/dist/env/env.d.ts.map +0 -1
  108. package/dist/env/env.js +0 -177
  109. package/dist/env/env.js.map +0 -1
  110. package/dist/index.d.ts +0 -49
  111. package/dist/index.d.ts.map +0 -1
  112. package/dist/index.js +0 -57
  113. package/dist/index.js.map +0 -1
  114. package/dist/openapi/generator.d.ts +0 -93
  115. package/dist/openapi/generator.d.ts.map +0 -1
  116. package/dist/openapi/generator.js +0 -293
  117. package/dist/openapi/generator.js.map +0 -1
  118. package/dist/openapi/index.d.ts +0 -9
  119. package/dist/openapi/index.d.ts.map +0 -1
  120. package/dist/openapi/index.js +0 -9
  121. package/dist/openapi/index.js.map +0 -1
  122. package/dist/openapi/types.d.ts +0 -182
  123. package/dist/openapi/types.d.ts.map +0 -1
  124. package/dist/openapi/types.js +0 -16
  125. package/dist/openapi/types.js.map +0 -1
  126. package/dist/storage/adapters/local_storage_service.d.ts +0 -51
  127. package/dist/storage/adapters/local_storage_service.d.ts.map +0 -1
  128. package/dist/storage/adapters/local_storage_service.js +0 -110
  129. package/dist/storage/adapters/local_storage_service.js.map +0 -1
  130. package/dist/storage/adapters/ovh_storage_service.d.ts +0 -72
  131. package/dist/storage/adapters/ovh_storage_service.d.ts.map +0 -1
  132. package/dist/storage/adapters/ovh_storage_service.js +0 -206
  133. package/dist/storage/adapters/ovh_storage_service.js.map +0 -1
  134. package/dist/storage/storage_factory.d.ts +0 -14
  135. package/dist/storage/storage_factory.d.ts.map +0 -1
  136. package/dist/storage/storage_factory.js +0 -40
  137. package/dist/storage/storage_factory.js.map +0 -1
  138. package/dist/storage/storage_service.d.ts +0 -163
  139. package/dist/storage/storage_service.d.ts.map +0 -1
  140. package/dist/storage/storage_service.js +0 -54
  141. package/dist/storage/storage_service.js.map +0 -1
  142. package/dist/types/data_record.d.ts +0 -123
  143. package/dist/types/data_record.d.ts.map +0 -1
  144. package/dist/types/data_record.js +0 -8
  145. package/dist/types/data_record.js.map +0 -1
  146. package/dist/utils/http_responses.d.ts +0 -155
  147. package/dist/utils/http_responses.d.ts.map +0 -1
  148. package/dist/utils/http_responses.js +0 -190
  149. package/dist/utils/http_responses.js.map +0 -1
  150. package/dist/utils/index.d.ts +0 -8
  151. package/dist/utils/index.d.ts.map +0 -1
  152. package/dist/utils/index.js +0 -6
  153. package/dist/utils/index.js.map +0 -1
  154. package/dist/utils/logger.d.ts +0 -74
  155. package/dist/utils/logger.d.ts.map +0 -1
  156. package/dist/utils/logger.js +0 -92
  157. package/dist/utils/logger.js.map +0 -1
  158. package/dist/utils/map_to_data_record.d.ts +0 -10
  159. package/dist/utils/map_to_data_record.d.ts.map +0 -1
  160. package/dist/utils/map_to_data_record.js +0 -36
  161. package/dist/utils/map_to_data_record.js.map +0 -1
  162. package/dist/utils/servable_endpoint.d.ts +0 -63
  163. package/dist/utils/servable_endpoint.d.ts.map +0 -1
  164. package/dist/utils/servable_endpoint.js +0 -67
  165. package/dist/utils/servable_endpoint.js.map +0 -1
  166. package/dist/utils/zip_utils.d.ts +0 -66
  167. package/dist/utils/zip_utils.d.ts.map +0 -1
  168. package/dist/utils/zip_utils.js +0 -169
  169. package/dist/utils/zip_utils.js.map +0 -1
@@ -1,1052 +0,0 @@
1
- import { UserService } from '../auth/user_service.js';
2
- import { ApisixAuthParser } from '../auth/apisix_parser.js';
3
- export class CustomTableManager {
4
- /**
5
- * Injects required dependencies into the custom table manager instance.
6
- *
7
- * Called by the Digital Twin Engine during component initialization.
8
- *
9
- * @param {DatabaseAdapter} db - Database adapter for data operations
10
- *
11
- * @example
12
- * ```typescript
13
- * const customTableManager = new MyCustomTableManager()
14
- * customTableManager.setDependencies(databaseAdapter)
15
- * ```
16
- */
17
- setDependencies(db) {
18
- this.db = db;
19
- this.userService = new UserService(db);
20
- this.tableName = this.getConfiguration().name;
21
- }
22
- /**
23
- * Initialize the database table with custom columns.
24
- *
25
- * Creates the table if it doesn't exist, with standard columns (id, created_at, updated_at)
26
- * plus the custom columns defined in the configuration.
27
- * Automatically adds an 'owner_id' column for user ownership tracking.
28
- *
29
- * @returns {Promise<void>}
30
- *
31
- * @example
32
- * ```typescript
33
- * // Called automatically by the framework
34
- * await customTableManager.initializeTable()
35
- * ```
36
- */
37
- async initializeTable() {
38
- const config = this.getConfiguration();
39
- // Add owner_id column automatically for user ownership
40
- const columnsWithOwnership = {
41
- owner_id: 'integer',
42
- ...config.columns
43
- };
44
- // Use the new createTableWithColumns method from DatabaseAdapter
45
- await this.db.createTableWithColumns(config.name, columnsWithOwnership);
46
- }
47
- /**
48
- * Validate query conditions against requirements.
49
- *
50
- * @private
51
- * @param {Record<string, any>} conditions - Conditions to validate
52
- * @param {QueryValidationOptions} options - Validation options
53
- * @throws {Error} If validation fails
54
- */
55
- validateQuery(conditions, options) {
56
- if (!options)
57
- return;
58
- // Check required fields
59
- if (options.required) {
60
- for (const field of options.required) {
61
- if (!(field in conditions)) {
62
- throw new Error(`Field '${field}' is required`);
63
- }
64
- const value = conditions[field];
65
- if (value == null || value === '') {
66
- throw new Error(`Field '${field}' must have a non-empty value`);
67
- }
68
- }
69
- }
70
- // Custom validation
71
- if (options.validate) {
72
- try {
73
- options.validate(conditions);
74
- }
75
- catch (error) {
76
- throw new Error(`Validation failed: ${error instanceof Error ? error.message : 'Unknown validation error'}`);
77
- }
78
- }
79
- }
80
- /**
81
- * Create a new record in the custom table.
82
- *
83
- * @param {Record<string, any>} data - Data to insert (excluding id, created_at, updated_at)
84
- * @returns {Promise<number>} The ID of the created record
85
- *
86
- * @example
87
- * ```typescript
88
- * const id = await customTableManager.create({
89
- * sensor_id: 'TEMP001',
90
- * type: 'temperature',
91
- * location: 'Building A - Floor 1',
92
- * active: true
93
- * })
94
- * console.log(`Created sensor with ID: ${id}`)
95
- * ```
96
- */
97
- async create(data) {
98
- // Use the specialized method for custom tables
99
- return await this.db.insertCustomTableRecord(this.tableName, data);
100
- }
101
- /**
102
- * Find all records in the custom table.
103
- *
104
- * @returns {Promise<CustomTableRecord[]>} Array of all records
105
- *
106
- * @example
107
- * ```typescript
108
- * const allSensors = await customTableManager.findAll()
109
- * console.log(`Found ${allSensors.length} sensors`)
110
- * ```
111
- */
112
- async findAll() {
113
- // Use specialized method for custom tables that returns raw rows
114
- const records = await this.db.findCustomTableRecords(this.tableName, {});
115
- return records.map(record => ({
116
- id: record.id,
117
- created_at: new Date(record.created_at),
118
- updated_at: new Date(record.updated_at),
119
- ...this.extractCustomFields(record)
120
- }));
121
- }
122
- /**
123
- * Find a record by its ID.
124
- *
125
- * @param {number} id - The record ID to find
126
- * @returns {Promise<CustomTableRecord | null>} The record or null if not found
127
- *
128
- * @example
129
- * ```typescript
130
- * const sensor = await customTableManager.findById(123)
131
- * if (sensor) {
132
- * console.log(`Sensor: ${sensor.sensor_id}`)
133
- * }
134
- * ```
135
- */
136
- async findById(id) {
137
- const record = await this.db.getCustomTableRecordById(this.tableName, id);
138
- if (!record) {
139
- return null;
140
- }
141
- return {
142
- id: record.id,
143
- created_at: new Date(record.created_at),
144
- updated_at: new Date(record.updated_at),
145
- ...this.extractCustomFields(record)
146
- };
147
- }
148
- /**
149
- * Find records by a single column value with optional validation.
150
- *
151
- * @param {string} columnName - Name of the column to search by
152
- * @param {any} value - Value to search for
153
- * @param {boolean} required - Whether the value is required (default: true)
154
- * @returns {Promise<CustomTableRecord[]>} Array of matching records
155
- *
156
- * @example
157
- * ```typescript
158
- * const wmsLayers = await wmsManager.findByColumn('wms_url', 'https://example.com/wms')
159
- * const activeLayers = await wmsManager.findByColumn('active', true)
160
- *
161
- * // Optional value (won't throw if empty)
162
- * const layers = await wmsManager.findByColumn('description', '', false)
163
- * ```
164
- */
165
- async findByColumn(columnName, value, required = true) {
166
- if (required && (value == null || value === '')) {
167
- throw new Error(`Value for column '${columnName}' is required and cannot be empty`);
168
- }
169
- return this.findByCondition({ [columnName]: value });
170
- }
171
- /**
172
- * Find records by multiple column values with validation support.
173
- *
174
- * @param {Record<string, any>} conditions - Key-value pairs to match
175
- * @param {QueryValidationOptions} options - Validation options
176
- * @returns {Promise<CustomTableRecord[]>} Array of matching records
177
- *
178
- * @example
179
- * ```typescript
180
- * // Simple query
181
- * const layers = await wmsManager.findByColumns({
182
- * wms_url: 'https://example.com/wms',
183
- * active: true
184
- * })
185
- *
186
- * // With validation
187
- * const layers = await wmsManager.findByColumns(
188
- * { wms_url: wmsUrl, projection: projection },
189
- * {
190
- * required: ['wms_url', 'projection'],
191
- * validate: (conditions) => {
192
- * if (!conditions.wms_url.startsWith('http')) {
193
- * throw new Error('WMS URL must start with http or https')
194
- * }
195
- * }
196
- * }
197
- * )
198
- * ```
199
- */
200
- async findByColumns(conditions, options) {
201
- this.validateQuery(conditions, options);
202
- return this.findByCondition(conditions);
203
- }
204
- /**
205
- * Find records matching specific conditions (base method).
206
- *
207
- * @param {Record<string, any>} conditions - Key-value pairs to match
208
- * @returns {Promise<CustomTableRecord[]>} Array of matching records
209
- *
210
- * @example
211
- * ```typescript
212
- * const activeSensors = await customTableManager.findByCondition({ active: true })
213
- * const tempSensors = await customTableManager.findByCondition({ type: 'temperature' })
214
- * ```
215
- */
216
- async findByCondition(conditions) {
217
- const records = await this.db.findCustomTableRecords(this.tableName, conditions);
218
- return records.map(record => ({
219
- id: record.id,
220
- created_at: new Date(record.created_at),
221
- updated_at: new Date(record.updated_at),
222
- ...this.extractCustomFields(record)
223
- }));
224
- }
225
- /**
226
- * Update a record by its ID.
227
- *
228
- * @param {number} id - The record ID to update
229
- * @param {Record<string, any>} data - Data to update (excluding id, created_at)
230
- * @returns {Promise<void>}
231
- *
232
- * @example
233
- * ```typescript
234
- * await customTableManager.update(123, {
235
- * active: false,
236
- * last_ping: new Date()
237
- * })
238
- * ```
239
- */
240
- async update(id, data) {
241
- // Use the new efficient SQL UPDATE method
242
- await this.db.updateById(this.tableName, id, data);
243
- }
244
- /**
245
- * Delete a record by its ID.
246
- *
247
- * @param {number} id - The record ID to delete
248
- * @returns {Promise<void>}
249
- *
250
- * @example
251
- * ```typescript
252
- * await customTableManager.delete(123)
253
- * console.log('Sensor deleted successfully')
254
- * ```
255
- */
256
- async delete(id) {
257
- await this.db.delete(id.toString(), this.tableName);
258
- }
259
- /**
260
- * Delete records by a single column value.
261
- *
262
- * @param {string} columnName - Name of the column to match for deletion
263
- * @param {any} value - Value to match for deletion
264
- * @returns {Promise<number>} Number of records deleted
265
- *
266
- * @example
267
- * ```typescript
268
- * const deleted = await wmsManager.deleteByColumn('active', false)
269
- * console.log(`Deleted ${deleted} inactive layers`)
270
- * ```
271
- */
272
- async deleteByColumn(columnName, value) {
273
- return this.deleteByCondition({ [columnName]: value });
274
- }
275
- /**
276
- * Delete records matching specific conditions.
277
- *
278
- * @param {Record<string, any>} conditions - Key-value pairs to match for deletion
279
- * @returns {Promise<number>} Number of records deleted
280
- *
281
- * @example
282
- * ```typescript
283
- * const deleted = await customTableManager.deleteByCondition({ active: false })
284
- * console.log(`Deleted ${deleted} inactive sensors`)
285
- * ```
286
- */
287
- async deleteByCondition(conditions) {
288
- const recordsToDelete = await this.findByCondition(conditions);
289
- for (const record of recordsToDelete) {
290
- await this.delete(record.id);
291
- }
292
- return recordsToDelete.length;
293
- }
294
- /**
295
- * Extract custom fields from a database record, excluding framework fields.
296
- *
297
- * @private
298
- * @param {any} record - Database record
299
- * @returns {Record<string, any>} Custom fields only (including owner_id)
300
- */
301
- extractCustomFields(record) {
302
- const config = this.getConfiguration();
303
- const customFields = {};
304
- // Add owner_id if present
305
- if ('owner_id' in record) {
306
- customFields.owner_id = record.owner_id;
307
- }
308
- // Add all configured custom columns
309
- for (const columnName of Object.keys(config.columns)) {
310
- if (columnName in record) {
311
- customFields[columnName] = record[columnName];
312
- }
313
- }
314
- return customFields;
315
- }
316
- /**
317
- * AUTHENTICATION HELPERS
318
- * These methods help custom endpoints implement authentication and authorization.
319
- */
320
- /**
321
- * Authenticate a request and return the user record.
322
- *
323
- * Use this for endpoints that require authentication but don't need ownership checks.
324
- *
325
- * @protected
326
- * @param {any} req - HTTP request object
327
- * @returns {Promise<UserRecord | null>} User record if authenticated, null otherwise
328
- *
329
- * @example
330
- * ```typescript
331
- * async myCustomEndpoint(req: any): Promise<DataResponse> {
332
- * const userRecord = await this.authenticateRequest(req)
333
- * if (!userRecord) {
334
- * return this.authErrorResponse()
335
- * }
336
- *
337
- * // Use userRecord.id for your logic
338
- * console.log(`User ${userRecord.id} is authenticated`)
339
- * }
340
- * ```
341
- */
342
- async authenticateRequest(req) {
343
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
344
- return null;
345
- }
346
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
347
- if (!authUser) {
348
- return null;
349
- }
350
- const userRecord = await this.userService.findOrCreateUser(authUser);
351
- if (!userRecord.id) {
352
- return null;
353
- }
354
- return userRecord;
355
- }
356
- /**
357
- * Authenticate a request and verify the user owns the specified record.
358
- *
359
- * Use this for endpoints that modify or delete specific records.
360
- *
361
- * @protected
362
- * @param {any} req - HTTP request object
363
- * @param {number} recordId - ID of the record to check ownership for
364
- * @returns {Promise<{ userRecord: UserRecord; record: CustomTableRecord } | null>}
365
- * Both user and record if authenticated and owns the record, null otherwise
366
- *
367
- * @example
368
- * ```typescript
369
- * async deleteMyRecord(req: any): Promise<DataResponse> {
370
- * const recordId = parseInt(req.params.id)
371
- * const auth = await this.authenticateAndCheckOwnership(req, recordId)
372
- *
373
- * if (!auth) {
374
- * return this.authErrorResponse()
375
- * }
376
- *
377
- * if (auth.record.owner_id !== auth.userRecord.id) {
378
- * return this.forbiddenErrorResponse()
379
- * }
380
- *
381
- * await this.delete(recordId)
382
- * return { status: 200, content: JSON.stringify({ message: 'Deleted' }) }
383
- * }
384
- * ```
385
- */
386
- async authenticateAndCheckOwnership(req, recordId) {
387
- const userRecord = await this.authenticateRequest(req);
388
- if (!userRecord) {
389
- return null;
390
- }
391
- const record = await this.findById(recordId);
392
- if (!record) {
393
- return null;
394
- }
395
- return { userRecord, record };
396
- }
397
- /**
398
- * Get authenticated user from request, throwing an error response if not authenticated.
399
- *
400
- * Use this when authentication is required and you want to fail fast.
401
- *
402
- * @protected
403
- * @param {any} req - HTTP request object
404
- * @returns {Promise<UserRecord | DataResponse>} User record if authenticated, error response otherwise
405
- *
406
- * @example
407
- * ```typescript
408
- * async myProtectedEndpoint(req: any): Promise<DataResponse> {
409
- * const userOrError = await this.requireAuthentication(req)
410
- * if ('status' in userOrError) {
411
- * return userOrError // Return error response
412
- * }
413
- *
414
- * const userRecord = userOrError
415
- * // Continue with authenticated user
416
- * }
417
- * ```
418
- */
419
- async requireAuthentication(req) {
420
- const userRecord = await this.authenticateRequest(req);
421
- if (!userRecord) {
422
- return this.authErrorResponse();
423
- }
424
- return userRecord;
425
- }
426
- /**
427
- * Helper to return a 401 Unauthorized error response.
428
- *
429
- * @protected
430
- * @param {string} message - Custom error message (default: "Authentication required")
431
- * @returns {DataResponse} 401 error response
432
- *
433
- * @example
434
- * ```typescript
435
- * if (!userRecord) {
436
- * return this.authErrorResponse()
437
- * }
438
- * ```
439
- */
440
- authErrorResponse(message = 'Authentication required') {
441
- return {
442
- status: 401,
443
- content: JSON.stringify({ error: message }),
444
- headers: { 'Content-Type': 'application/json' }
445
- };
446
- }
447
- /**
448
- * Helper to return a 403 Forbidden error response.
449
- *
450
- * @protected
451
- * @param {string} message - Custom error message (default: "You don't have permission to access this resource")
452
- * @returns {DataResponse} 403 error response
453
- *
454
- * @example
455
- * ```typescript
456
- * if (record.owner_id !== userRecord.id) {
457
- * return this.forbiddenErrorResponse('You can only modify your own records')
458
- * }
459
- * ```
460
- */
461
- forbiddenErrorResponse(message = "You don't have permission to access this resource") {
462
- return {
463
- status: 403,
464
- content: JSON.stringify({ error: message }),
465
- headers: { 'Content-Type': 'application/json' }
466
- };
467
- }
468
- /**
469
- * Helper to check if a user owns a specific record.
470
- *
471
- * Returns false if the record doesn't have an owner_id (legacy records without ownership).
472
- *
473
- * @protected
474
- * @param {CustomTableRecord} record - The record to check
475
- * @param {UserRecord} userRecord - The user to check against
476
- * @returns {boolean} True if user owns the record, false otherwise
477
- *
478
- * @example
479
- * ```typescript
480
- * const record = await this.findById(recordId)
481
- * const userRecord = await this.authenticateRequest(req)
482
- *
483
- * if (!this.userOwnsRecord(record, userRecord)) {
484
- * return this.forbiddenErrorResponse()
485
- * }
486
- * ```
487
- */
488
- userOwnsRecord(record, userRecord) {
489
- // If record doesn't have an owner_id, deny access for security
490
- if (!record.owner_id) {
491
- return false;
492
- }
493
- return record.owner_id === userRecord.id;
494
- }
495
- /**
496
- * Get HTTP endpoints exposed by this custom table manager.
497
- *
498
- * Returns both standard CRUD endpoints and custom endpoints defined in configuration.
499
- *
500
- * @returns {Array} Array of endpoint descriptors with methods, paths, and handlers
501
- *
502
- * @example
503
- * ```typescript
504
- * // Standard endpoints for a table named 'sensors':
505
- * GET /sensors - Get all records
506
- * POST /sensors - Create new record
507
- * GET /sensors/:id - Get specific record
508
- * PUT /sensors/:id - Update specific record
509
- * DELETE /sensors/:id - Delete specific record
510
- *
511
- * // Plus any custom endpoints defined in configuration
512
- * ```
513
- */
514
- getEndpoints() {
515
- const config = this.getConfiguration();
516
- const endpoints = [];
517
- // Custom endpoints from configuration - MUST be registered BEFORE :id routes
518
- // to avoid Express matching /proxy as /:id
519
- if (config.endpoints) {
520
- for (const endpoint of config.endpoints) {
521
- const handlerMethod = this[endpoint.handler];
522
- if (typeof handlerMethod === 'function') {
523
- endpoints.push({
524
- method: endpoint.method,
525
- path: `/${config.name}${endpoint.path}`,
526
- handler: handlerMethod.bind(this),
527
- responseType: endpoint.responseType || 'application/json'
528
- });
529
- }
530
- }
531
- }
532
- // Standard CRUD endpoints
533
- endpoints.push({
534
- method: 'get',
535
- path: `/${config.name}`,
536
- handler: this.handleGetAll.bind(this),
537
- responseType: 'application/json'
538
- });
539
- endpoints.push({
540
- method: 'post',
541
- path: `/${config.name}`,
542
- handler: this.handleCreate.bind(this),
543
- responseType: 'application/json'
544
- });
545
- // :id routes MUST come AFTER custom endpoints
546
- endpoints.push({
547
- method: 'get',
548
- path: `/${config.name}/:id`,
549
- handler: this.handleGetById.bind(this),
550
- responseType: 'application/json'
551
- });
552
- endpoints.push({
553
- method: 'put',
554
- path: `/${config.name}/:id`,
555
- handler: this.handleUpdate.bind(this),
556
- responseType: 'application/json'
557
- });
558
- endpoints.push({
559
- method: 'delete',
560
- path: `/${config.name}/:id`,
561
- handler: this.handleDelete.bind(this),
562
- responseType: 'application/json'
563
- });
564
- return endpoints;
565
- }
566
- /**
567
- * Returns the OpenAPI specification for this custom table manager's endpoints.
568
- *
569
- * Generates documentation for all CRUD endpoints and custom endpoints.
570
- * Can be overridden by subclasses for more detailed specifications.
571
- *
572
- * @returns {OpenAPIComponentSpec} OpenAPI paths, tags, and schemas for this custom table manager
573
- */
574
- getOpenAPISpec() {
575
- const config = this.getConfiguration();
576
- const basePath = `/${config.name}`;
577
- const tagName = config.tags?.[0] || config.name;
578
- // Build properties schema from columns
579
- const columnProperties = {
580
- id: { type: 'integer', readOnly: true },
581
- owner_id: { type: 'integer', nullable: true },
582
- created_at: { type: 'string', format: 'date-time', readOnly: true },
583
- updated_at: { type: 'string', format: 'date-time', readOnly: true }
584
- };
585
- // Add configured columns to the schema
586
- for (const [columnName, columnDef] of Object.entries(config.columns)) {
587
- columnProperties[columnName] = this.columnDefToOpenAPISchema(columnDef);
588
- }
589
- const paths = {
590
- [basePath]: {
591
- get: {
592
- summary: `List all ${config.name} records`,
593
- description: config.description,
594
- tags: [tagName],
595
- responses: {
596
- '200': {
597
- description: 'List of records',
598
- content: {
599
- 'application/json': {
600
- schema: {
601
- type: 'array',
602
- items: { $ref: `#/components/schemas/${config.name}Record` }
603
- }
604
- }
605
- }
606
- }
607
- }
608
- },
609
- post: {
610
- summary: `Create a new ${config.name} record`,
611
- description: 'Create a new record. Requires authentication.',
612
- tags: [tagName],
613
- security: [{ ApiKeyAuth: [] }],
614
- requestBody: {
615
- required: true,
616
- content: {
617
- 'application/json': {
618
- schema: { $ref: `#/components/schemas/${config.name}Input` }
619
- }
620
- }
621
- },
622
- responses: {
623
- '201': {
624
- description: 'Record created successfully',
625
- content: {
626
- 'application/json': {
627
- schema: {
628
- type: 'object',
629
- properties: {
630
- id: { type: 'integer' },
631
- message: { type: 'string' }
632
- }
633
- }
634
- }
635
- }
636
- },
637
- '400': { description: 'Bad request' },
638
- '401': { description: 'Unauthorized' }
639
- }
640
- }
641
- },
642
- [`${basePath}/{id}`]: {
643
- get: {
644
- summary: `Get ${config.name} record by ID`,
645
- description: 'Returns a single record',
646
- tags: [tagName],
647
- parameters: [
648
- {
649
- name: 'id',
650
- in: 'path',
651
- required: true,
652
- schema: { type: 'integer' },
653
- description: 'Record ID'
654
- }
655
- ],
656
- responses: {
657
- '200': {
658
- description: 'Record details',
659
- content: {
660
- 'application/json': {
661
- schema: { $ref: `#/components/schemas/${config.name}Record` }
662
- }
663
- }
664
- },
665
- '404': { description: 'Record not found' }
666
- }
667
- },
668
- put: {
669
- summary: `Update ${config.name} record`,
670
- description: 'Update a record. Requires authentication and ownership.',
671
- tags: [tagName],
672
- security: [{ ApiKeyAuth: [] }],
673
- parameters: [
674
- {
675
- name: 'id',
676
- in: 'path',
677
- required: true,
678
- schema: { type: 'integer' },
679
- description: 'Record ID'
680
- }
681
- ],
682
- requestBody: {
683
- required: true,
684
- content: {
685
- 'application/json': {
686
- schema: { $ref: `#/components/schemas/${config.name}Input` }
687
- }
688
- }
689
- },
690
- responses: {
691
- '200': { description: 'Record updated successfully' },
692
- '400': { description: 'Bad request' },
693
- '401': { description: 'Unauthorized' },
694
- '403': { description: 'Forbidden - not owner' },
695
- '404': { description: 'Record not found' }
696
- }
697
- },
698
- delete: {
699
- summary: `Delete ${config.name} record`,
700
- description: 'Delete a record. Requires authentication and ownership.',
701
- tags: [tagName],
702
- security: [{ ApiKeyAuth: [] }],
703
- parameters: [
704
- {
705
- name: 'id',
706
- in: 'path',
707
- required: true,
708
- schema: { type: 'integer' },
709
- description: 'Record ID'
710
- }
711
- ],
712
- responses: {
713
- '200': { description: 'Record deleted successfully' },
714
- '401': { description: 'Unauthorized' },
715
- '403': { description: 'Forbidden - not owner' },
716
- '404': { description: 'Record not found' }
717
- }
718
- }
719
- }
720
- };
721
- // Add custom endpoints from configuration
722
- if (config.endpoints) {
723
- for (const endpoint of config.endpoints) {
724
- const fullPath = `${basePath}${endpoint.path}`;
725
- const method = endpoint.method.toLowerCase();
726
- if (!paths[fullPath]) {
727
- paths[fullPath] = {};
728
- }
729
- paths[fullPath][method] = {
730
- summary: `Custom endpoint: ${endpoint.path}`,
731
- tags: [tagName],
732
- responses: {
733
- '200': { description: 'Successful response' }
734
- }
735
- };
736
- }
737
- }
738
- // Build input schema (excludes readonly fields)
739
- const inputProperties = {};
740
- for (const [columnName, columnDef] of Object.entries(config.columns)) {
741
- inputProperties[columnName] = this.columnDefToOpenAPISchema(columnDef);
742
- }
743
- return {
744
- paths,
745
- tags: [
746
- {
747
- name: tagName,
748
- description: config.description
749
- }
750
- ],
751
- schemas: {
752
- [`${config.name}Record`]: {
753
- type: 'object',
754
- properties: columnProperties
755
- },
756
- [`${config.name}Input`]: {
757
- type: 'object',
758
- properties: inputProperties
759
- }
760
- }
761
- };
762
- }
763
- /**
764
- * Convert SQL column definition to OpenAPI schema type.
765
- *
766
- * @private
767
- * @param {string} columnDef - SQL column definition (e.g., 'text not null', 'integer default 0')
768
- * @returns {object} OpenAPI schema object
769
- */
770
- columnDefToOpenAPISchema(columnDef) {
771
- const lowerDef = columnDef.toLowerCase();
772
- if (lowerDef.includes('integer') || lowerDef.includes('int')) {
773
- return { type: 'integer' };
774
- }
775
- else if (lowerDef.includes('real') || lowerDef.includes('float') || lowerDef.includes('double')) {
776
- return { type: 'number' };
777
- }
778
- else if (lowerDef.includes('boolean') || lowerDef.includes('bool')) {
779
- return { type: 'boolean' };
780
- }
781
- else if (lowerDef.includes('timestamp') || lowerDef.includes('datetime')) {
782
- return { type: 'string', format: 'date-time' };
783
- }
784
- else if (lowerDef.includes('date')) {
785
- return { type: 'string', format: 'date' };
786
- }
787
- else {
788
- return { type: 'string' };
789
- }
790
- }
791
- /**
792
- * Standard endpoint handlers for CRUD operations
793
- */
794
- async handleGetAll(_req) {
795
- try {
796
- const records = await this.findAll();
797
- return {
798
- status: 200,
799
- content: JSON.stringify(records),
800
- headers: { 'Content-Type': 'application/json' }
801
- };
802
- }
803
- catch (error) {
804
- return {
805
- status: 500,
806
- content: JSON.stringify({
807
- error: error instanceof Error ? error.message : 'Unknown error'
808
- }),
809
- headers: { 'Content-Type': 'application/json' }
810
- };
811
- }
812
- }
813
- async handleCreate(req) {
814
- try {
815
- // Check authentication
816
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
817
- return {
818
- status: 401,
819
- content: JSON.stringify({ error: 'Authentication required' }),
820
- headers: { 'Content-Type': 'application/json' }
821
- };
822
- }
823
- // Parse authenticated user
824
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
825
- if (!authUser) {
826
- return {
827
- status: 401,
828
- content: JSON.stringify({ error: 'Invalid authentication headers' }),
829
- headers: { 'Content-Type': 'application/json' }
830
- };
831
- }
832
- if (!req?.body) {
833
- return {
834
- status: 400,
835
- content: JSON.stringify({ error: 'Request body is required' }),
836
- headers: { 'Content-Type': 'application/json' }
837
- };
838
- }
839
- // Find or create user in database
840
- const userRecord = await this.userService.findOrCreateUser(authUser);
841
- if (!userRecord.id) {
842
- return {
843
- status: 500,
844
- content: JSON.stringify({ error: 'Failed to retrieve user information' }),
845
- headers: { 'Content-Type': 'application/json' }
846
- };
847
- }
848
- // Add owner_id to the data
849
- const dataWithOwner = {
850
- ...req.body,
851
- owner_id: userRecord.id
852
- };
853
- const id = await this.create(dataWithOwner);
854
- return {
855
- status: 201,
856
- content: JSON.stringify({ id, message: 'Record created successfully' }),
857
- headers: { 'Content-Type': 'application/json' }
858
- };
859
- }
860
- catch (error) {
861
- return {
862
- status: 500,
863
- content: JSON.stringify({
864
- error: error instanceof Error ? error.message : 'Unknown error'
865
- }),
866
- headers: { 'Content-Type': 'application/json' }
867
- };
868
- }
869
- }
870
- async handleGetById(req) {
871
- try {
872
- const { id } = req.params || {};
873
- if (!id) {
874
- return {
875
- status: 400,
876
- content: JSON.stringify({ error: 'ID parameter is required' }),
877
- headers: { 'Content-Type': 'application/json' }
878
- };
879
- }
880
- const record = await this.findById(parseInt(id));
881
- if (!record) {
882
- return {
883
- status: 404,
884
- content: JSON.stringify({ error: 'Record not found' }),
885
- headers: { 'Content-Type': 'application/json' }
886
- };
887
- }
888
- return {
889
- status: 200,
890
- content: JSON.stringify(record),
891
- headers: { 'Content-Type': 'application/json' }
892
- };
893
- }
894
- catch (error) {
895
- return {
896
- status: 500,
897
- content: JSON.stringify({
898
- error: error instanceof Error ? error.message : 'Unknown error'
899
- }),
900
- headers: { 'Content-Type': 'application/json' }
901
- };
902
- }
903
- }
904
- async handleUpdate(req) {
905
- try {
906
- // Check authentication
907
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
908
- return {
909
- status: 401,
910
- content: JSON.stringify({ error: 'Authentication required' }),
911
- headers: { 'Content-Type': 'application/json' }
912
- };
913
- }
914
- // Parse authenticated user
915
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
916
- if (!authUser) {
917
- return {
918
- status: 401,
919
- content: JSON.stringify({ error: 'Invalid authentication headers' }),
920
- headers: { 'Content-Type': 'application/json' }
921
- };
922
- }
923
- const { id } = req.params || {};
924
- if (!id) {
925
- return {
926
- status: 400,
927
- content: JSON.stringify({ error: 'ID parameter is required' }),
928
- headers: { 'Content-Type': 'application/json' }
929
- };
930
- }
931
- if (!req?.body) {
932
- return {
933
- status: 400,
934
- content: JSON.stringify({ error: 'Request body is required' }),
935
- headers: { 'Content-Type': 'application/json' }
936
- };
937
- }
938
- // Find or create user in database
939
- const userRecord = await this.userService.findOrCreateUser(authUser);
940
- if (!userRecord.id) {
941
- return {
942
- status: 500,
943
- content: JSON.stringify({ error: 'Failed to retrieve user information' }),
944
- headers: { 'Content-Type': 'application/json' }
945
- };
946
- }
947
- // Check if record exists and belongs to this user
948
- const existingRecord = await this.findById(parseInt(id));
949
- if (!existingRecord) {
950
- return {
951
- status: 404,
952
- content: JSON.stringify({ error: 'Record not found' }),
953
- headers: { 'Content-Type': 'application/json' }
954
- };
955
- }
956
- // Check ownership (only owner can modify their records)
957
- if (!existingRecord.owner_id || existingRecord.owner_id !== userRecord.id) {
958
- return {
959
- status: 403,
960
- content: JSON.stringify({ error: 'You can only modify your own records' }),
961
- headers: { 'Content-Type': 'application/json' }
962
- };
963
- }
964
- await this.update(parseInt(id), req.body);
965
- return {
966
- status: 200,
967
- content: JSON.stringify({ message: 'Record updated successfully' }),
968
- headers: { 'Content-Type': 'application/json' }
969
- };
970
- }
971
- catch (error) {
972
- return {
973
- status: 500,
974
- content: JSON.stringify({
975
- error: error instanceof Error ? error.message : 'Unknown error'
976
- }),
977
- headers: { 'Content-Type': 'application/json' }
978
- };
979
- }
980
- }
981
- async handleDelete(req) {
982
- try {
983
- // Check authentication
984
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
985
- return {
986
- status: 401,
987
- content: JSON.stringify({ error: 'Authentication required' }),
988
- headers: { 'Content-Type': 'application/json' }
989
- };
990
- }
991
- // Parse authenticated user
992
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
993
- if (!authUser) {
994
- return {
995
- status: 401,
996
- content: JSON.stringify({ error: 'Invalid authentication headers' }),
997
- headers: { 'Content-Type': 'application/json' }
998
- };
999
- }
1000
- const { id } = req.params || {};
1001
- if (!id) {
1002
- return {
1003
- status: 400,
1004
- content: JSON.stringify({ error: 'ID parameter is required' }),
1005
- headers: { 'Content-Type': 'application/json' }
1006
- };
1007
- }
1008
- // Find or create user in database
1009
- const userRecord = await this.userService.findOrCreateUser(authUser);
1010
- if (!userRecord.id) {
1011
- return {
1012
- status: 500,
1013
- content: JSON.stringify({ error: 'Failed to retrieve user information' }),
1014
- headers: { 'Content-Type': 'application/json' }
1015
- };
1016
- }
1017
- // Check if record exists and belongs to this user
1018
- const existingRecord = await this.findById(parseInt(id));
1019
- if (!existingRecord) {
1020
- return {
1021
- status: 404,
1022
- content: JSON.stringify({ error: 'Record not found' }),
1023
- headers: { 'Content-Type': 'application/json' }
1024
- };
1025
- }
1026
- // Check ownership (only owner can delete their records)
1027
- if (!existingRecord.owner_id || existingRecord.owner_id !== userRecord.id) {
1028
- return {
1029
- status: 403,
1030
- content: JSON.stringify({ error: 'You can only delete your own records' }),
1031
- headers: { 'Content-Type': 'application/json' }
1032
- };
1033
- }
1034
- await this.delete(parseInt(id));
1035
- return {
1036
- status: 200,
1037
- content: JSON.stringify({ message: 'Record deleted successfully' }),
1038
- headers: { 'Content-Type': 'application/json' }
1039
- };
1040
- }
1041
- catch (error) {
1042
- return {
1043
- status: 500,
1044
- content: JSON.stringify({
1045
- error: error instanceof Error ? error.message : 'Unknown error'
1046
- }),
1047
- headers: { 'Content-Type': 'application/json' }
1048
- };
1049
- }
1050
- }
1051
- }
1052
- //# sourceMappingURL=custom_table_manager.js.map