digitaltwin-core 0.13.3 → 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 -61
  131. package/dist/storage/adapters/ovh_storage_service.d.ts.map +0 -1
  132. package/dist/storage/adapters/ovh_storage_service.js +0 -172
  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 -36
  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,1529 +0,0 @@
1
- import { ApisixAuthParser } from '../auth/apisix_parser.js';
2
- import { UserService } from '../auth/user_service.js';
3
- import { AuthConfig } from '../auth/auth_config.js';
4
- import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, textResponse, fileResponse, multiStatusResponse, HttpStatus } from '../utils/http_responses.js';
5
- import fs from 'fs/promises';
6
- /**
7
- * Abstract base class for Assets Manager components with authentication and access control.
8
- *
9
- * Provides secure file upload, storage, and retrieval capabilities following the Digital Twin framework patterns.
10
- * Each concrete implementation manages a specific type of asset and creates its own database table.
11
- *
12
- * ## Authentication & Authorization
13
- *
14
- * - **Write Operations** (POST, PUT, DELETE): Require authentication via Apache APISIX headers
15
- * - **User Management**: Automatically creates/updates user records from Keycloak data
16
- * - **Access Control**: Users can only modify/delete their own assets (ownership-based)
17
- * - **Resource Linking**: Assets are automatically linked to their owners via user_id foreign key
18
- *
19
- * ## Required Headers for Authenticated Endpoints
20
- *
21
- * - `x-user-id`: Keycloak user UUID (required)
22
- * - `x-user-roles`: Comma-separated list of user roles (optional)
23
- *
24
- * These headers are automatically added by Apache APISIX after successful Keycloak authentication.
25
- *
26
- * @abstract
27
- * @class AssetsManager
28
- * @implements {Component}
29
- * @implements {Servable}
30
- *
31
- * @example
32
- * ```typescript
33
- * // Create concrete implementations for different asset types
34
- * class GLTFAssetsManager extends AssetsManager {
35
- * getConfiguration() {
36
- * return { name: 'gltf', description: 'GLTF 3D models manager', ... }
37
- * }
38
- * }
39
- *
40
- * class PointCloudAssetsManager extends AssetsManager {
41
- * getConfiguration() {
42
- * return { name: 'pointcloud', description: 'Point cloud data manager', ... }
43
- * }
44
- * }
45
- *
46
- * // Usage in engine
47
- * const gltfManager = new GLTFAssetsManager()
48
- * gltfManager.setDependencies(database, storage)
49
- *
50
- * // Each creates its own table and endpoints:
51
- * // - GLTFAssetsManager → table 'gltf', endpoints /gltf/*
52
- * // - PointCloudAssetsManager → table 'pointcloud', endpoints /pointcloud/*
53
- * ```
54
- *
55
- * @remarks
56
- * Asset metadata is stored as dedicated columns in the database table:
57
- * - id, name, url, date (standard columns)
58
- * - description, source, owner_id, filename (asset-specific columns)
59
- *
60
- * Each concrete AssetsManager creates its own table based on the configuration name.
61
- */
62
- export class AssetsManager {
63
- /**
64
- * Injects dependencies into the assets manager.
65
- *
66
- * Called by the framework during component initialization.
67
- *
68
- * @param {DatabaseAdapter} db - The database adapter for metadata storage
69
- * @param {StorageService} storage - The storage service for file persistence
70
- * @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
71
- *
72
- * @example
73
- * ```typescript
74
- * // Standard usage (UserService created automatically)
75
- * const assetsManager = new MyAssetsManager()
76
- * assetsManager.setDependencies(databaseAdapter, storageService)
77
- *
78
- * // For testing (inject mock UserService)
79
- * const mockUserService = new MockUserService()
80
- * assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
81
- * ```
82
- */
83
- setDependencies(db, storage, userService) {
84
- this.db = db;
85
- this.storage = storage;
86
- this.userService = userService ?? new UserService(db);
87
- }
88
- /**
89
- * Validates that a source string is a valid URL.
90
- *
91
- * Used internally to ensure data provenance URLs are properly formatted.
92
- *
93
- * @private
94
- * @param {string} source - The source URL to validate
95
- * @returns {boolean} True if the source is a valid URL, false otherwise
96
- *
97
- * @example
98
- * ```typescript
99
- * this.validateSourceURL('https://example.com/data') // returns true
100
- * this.validateSourceURL('not-a-url') // returns false
101
- * ```
102
- */
103
- validateSourceURL(source) {
104
- try {
105
- new URL(source);
106
- return true;
107
- }
108
- catch {
109
- return false;
110
- }
111
- }
112
- /**
113
- * Validates that a filename has the correct extension as configured.
114
- *
115
- * Used internally to ensure uploaded files match the expected extension.
116
- *
117
- * @private
118
- * @param {string} filename - The filename to validate
119
- * @returns {boolean} True if the filename has the correct extension or no extension is configured, false otherwise
120
- *
121
- * @example
122
- * ```typescript
123
- * // If config.extension = '.glb'
124
- * this.validateFileExtension('model.glb') // returns true
125
- * this.validateFileExtension('model.json') // returns false
126
- * this.validateFileExtension('model') // returns false
127
- *
128
- * // If config.extension is undefined
129
- * this.validateFileExtension('any-file.ext') // returns true
130
- * ```
131
- */
132
- validateFileExtension(filename) {
133
- const config = this.getConfiguration();
134
- // If no extension is configured, allow any file
135
- if (!config.extension) {
136
- return true;
137
- }
138
- // Ensure the configured extension starts with a dot
139
- const requiredExtension = config.extension.startsWith('.') ? config.extension : `.${config.extension}`;
140
- // Check if the filename ends with the required extension (case-insensitive)
141
- return filename.toLowerCase().endsWith(requiredExtension.toLowerCase());
142
- }
143
- /**
144
- * Validates that a string is valid base64-encoded data.
145
- *
146
- * Used internally to ensure file data in batch uploads is properly base64-encoded
147
- * before attempting to decode it.
148
- *
149
- * @private
150
- * @param {any} data - Data to validate as base64
151
- * @returns {boolean} True if data is a valid base64 string, false otherwise
152
- *
153
- * @example
154
- * ```typescript
155
- * this.validateBase64('SGVsbG8gV29ybGQ=') // returns true
156
- * this.validateBase64('not-base64!@#') // returns false
157
- * this.validateBase64(123) // returns false (not a string)
158
- * this.validateBase64('') // returns false (empty string)
159
- * ```
160
- */
161
- validateBase64(data) {
162
- // Must be a non-empty string
163
- if (typeof data !== 'string' || data.length === 0) {
164
- return false;
165
- }
166
- // Base64 regex: only A-Z, a-z, 0-9, +, /, and = for padding
167
- // Padding (=) can only appear at the end, max 2 times
168
- const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
169
- const trimmed = data.trim();
170
- // Must match regex
171
- if (!base64Regex.test(trimmed)) {
172
- return false;
173
- }
174
- // Length must be multiple of 4
175
- if (trimmed.length % 4 !== 0) {
176
- return false;
177
- }
178
- // Try to decode to verify it's valid base64
179
- try {
180
- const decoded = Buffer.from(trimmed, 'base64').toString('base64');
181
- // Re-encode and compare to ensure no data loss (valid base64)
182
- return decoded === trimmed;
183
- }
184
- catch {
185
- return false;
186
- }
187
- }
188
- // ============================================================================
189
- // Authentication & Request Processing Helpers
190
- // ============================================================================
191
- /**
192
- * Authenticates a request and returns the user record.
193
- *
194
- * This method consolidates the authentication flow:
195
- * 1. Validates APISIX headers are present
196
- * 2. Parses authentication headers
197
- * 3. Finds or creates user record in database
198
- *
199
- * @param req - HTTP request object
200
- * @returns AuthResult with either userRecord on success or DataResponse on failure
201
- *
202
- * @example
203
- * ```typescript
204
- * const authResult = await this.authenticateRequest(req)
205
- * if (!authResult.success) {
206
- * return authResult.response
207
- * }
208
- * const userRecord = authResult.userRecord
209
- * ```
210
- */
211
- async authenticateRequest(req) {
212
- // If auth is disabled, create an anonymous user
213
- if (AuthConfig.isAuthDisabled()) {
214
- const anonymousUser = {
215
- id: AuthConfig.getAnonymousUserId(),
216
- roles: []
217
- };
218
- const userRecord = await this.userService.findOrCreateUser(anonymousUser);
219
- return { success: true, userRecord };
220
- }
221
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
222
- return { success: false, response: unauthorizedResponse() };
223
- }
224
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
225
- if (!authUser) {
226
- return { success: false, response: unauthorizedResponse('Invalid authentication headers') };
227
- }
228
- const userRecord = await this.userService.findOrCreateUser(authUser);
229
- if (!userRecord.id) {
230
- return { success: false, response: errorResponse('Failed to retrieve user information') };
231
- }
232
- return { success: true, userRecord };
233
- }
234
- /**
235
- * Extracts upload data from multipart form request.
236
- *
237
- * @param req - HTTP request object with body and file
238
- * @returns UploadData object with extracted fields
239
- */
240
- extractUploadData(req) {
241
- return {
242
- description: req.body?.description,
243
- source: req.body?.source,
244
- is_public: req.body?.is_public,
245
- filePath: req.file?.path,
246
- fileBuffer: req.file?.buffer,
247
- filename: req.file?.originalname || req.body?.filename
248
- };
249
- }
250
- /**
251
- * Validates required fields for asset upload and returns validated data.
252
- *
253
- * @param data - Upload data to validate
254
- * @returns UploadValidationResult with validated data on success or error response on failure
255
- */
256
- validateUploadFields(data) {
257
- const hasFile = data.filePath || data.fileBuffer;
258
- if (!hasFile || !data.description || !data.source) {
259
- return {
260
- success: false,
261
- response: badRequestResponse('Missing required fields: description, source, file')
262
- };
263
- }
264
- if (!data.filename) {
265
- return {
266
- success: false,
267
- response: badRequestResponse('Filename could not be determined from uploaded file')
268
- };
269
- }
270
- if (!this.validateFileExtension(data.filename)) {
271
- const config = this.getConfiguration();
272
- return {
273
- success: false,
274
- response: badRequestResponse(`Invalid file extension. Expected: ${config.extension}`)
275
- };
276
- }
277
- return {
278
- success: true,
279
- data: {
280
- description: data.description,
281
- source: data.source,
282
- filePath: data.filePath,
283
- fileBuffer: data.fileBuffer,
284
- filename: data.filename,
285
- is_public: data.is_public !== undefined ? Boolean(data.is_public) : true
286
- }
287
- };
288
- }
289
- /**
290
- * Reads file content from temporary upload path.
291
- *
292
- * @param filePath - Path to temporary file
293
- * @returns Buffer with file content
294
- * @throws Error if file cannot be read
295
- */
296
- async readTempFile(filePath) {
297
- return fs.readFile(filePath);
298
- }
299
- /**
300
- * Cleans up temporary file after processing.
301
- * Silently ignores cleanup errors.
302
- *
303
- * @param filePath - Path to temporary file
304
- */
305
- async cleanupTempFile(filePath) {
306
- await fs.unlink(filePath).catch(() => {
307
- // Ignore cleanup errors
308
- });
309
- }
310
- /**
311
- * Validates ownership of an asset.
312
- *
313
- * Admins can modify any asset. Regular users can only modify their own assets
314
- * or assets with no owner (owner_id = null).
315
- *
316
- * @param asset - Asset record to check
317
- * @param userId - User ID to validate against
318
- * @param headers - HTTP request headers (optional, for admin check)
319
- * @returns DataResponse with error if not owner/admin, undefined if valid
320
- */
321
- validateOwnership(asset, userId, headers) {
322
- // Admins can modify any asset
323
- if (headers && ApisixAuthParser.isAdmin(headers)) {
324
- return undefined;
325
- }
326
- // Assets with no owner (null) can be modified by anyone
327
- if (asset.owner_id === null) {
328
- return undefined;
329
- }
330
- if (asset.owner_id !== userId) {
331
- return forbiddenResponse('You can only modify your own assets');
332
- }
333
- return undefined;
334
- }
335
- /**
336
- * Checks if a user can access a private asset.
337
- *
338
- * @param asset - Asset record to check
339
- * @param req - HTTP request for authentication context
340
- * @returns DataResponse with error if access denied, undefined if allowed
341
- */
342
- async checkPrivateAssetAccess(asset, req) {
343
- // Public assets are always accessible
344
- if (asset.is_public) {
345
- return undefined;
346
- }
347
- // Admins can access everything
348
- if (ApisixAuthParser.isAdmin(req.headers || {})) {
349
- return undefined;
350
- }
351
- // Private asset - require authentication
352
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
353
- return unauthorizedResponse('Authentication required for private assets');
354
- }
355
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
356
- if (!authUser) {
357
- return unauthorizedResponse('Invalid authentication headers');
358
- }
359
- const userRecord = await this.userService.findOrCreateUser(authUser);
360
- if (!userRecord.id || asset.owner_id !== userRecord.id) {
361
- return forbiddenResponse('This asset is private');
362
- }
363
- return undefined;
364
- }
365
- /**
366
- * Fetches an asset by ID with full access control validation.
367
- *
368
- * This method consolidates the common logic for retrieving an asset:
369
- * 1. Validates that ID is provided
370
- * 2. Fetches the asset from database
371
- * 3. Verifies the asset belongs to this component
372
- * 4. Checks access permissions for private assets
373
- *
374
- * @param req - HTTP request with params.id
375
- * @returns Object with asset on success, or DataResponse on failure
376
- */
377
- async fetchAssetWithAccessCheck(req) {
378
- const { id } = req.params || {};
379
- if (!id) {
380
- return { success: false, response: badRequestResponse('Asset ID is required') };
381
- }
382
- const asset = await this.getAssetById(id);
383
- if (!asset) {
384
- return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
385
- }
386
- // Verify this asset belongs to our component
387
- const config = this.getConfiguration();
388
- if (asset.name !== config.name) {
389
- return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
390
- }
391
- // Check access permissions for private assets
392
- const accessError = await this.checkPrivateAssetAccess(asset, req);
393
- if (accessError) {
394
- return { success: false, response: accessError };
395
- }
396
- return { success: true, asset };
397
- }
398
- /**
399
- * Upload a new asset file with metadata.
400
- *
401
- * Stores the file using the storage service and saves metadata to the database.
402
- * Asset metadata is stored as dedicated columns in the database table.
403
- *
404
- * @param {CreateAssetRequest} request - The asset upload request
405
- * @throws {Error} If source URL is invalid
406
- *
407
- * @example
408
- * ```typescript
409
- * await assetsManager.uploadAsset({
410
- * description: '3D building model',
411
- * source: 'https://city-data.example.com/buildings',
412
- * owner_id: 'user123',
413
- * filename: 'building.glb',
414
- * file: fileBuffer,
415
- * is_public: true
416
- * })
417
- * ```
418
- */
419
- async uploadAsset(request) {
420
- if (!this.validateSourceURL(request.source)) {
421
- throw new Error('Invalid source URL');
422
- }
423
- if (!this.validateFileExtension(request.filename)) {
424
- const config = this.getConfiguration();
425
- throw new Error(`Invalid file extension. Expected: ${config.extension}`);
426
- }
427
- const config = this.getConfiguration();
428
- const now = new Date();
429
- // Store file using framework pattern
430
- const url = await this.storage.save(request.file, config.name, request.filename);
431
- // Create metadata with all asset-specific fields
432
- const metadata = {
433
- name: config.name,
434
- type: config.contentType,
435
- url,
436
- date: now,
437
- description: request.description,
438
- source: request.source,
439
- owner_id: request.owner_id,
440
- filename: request.filename,
441
- is_public: request.is_public ?? true // Default to public if not specified
442
- };
443
- await this.db.save(metadata);
444
- }
445
- /**
446
- * Retrieve all assets for this component (like other components).
447
- *
448
- * Returns a JSON list of all assets with their metadata, following the
449
- * framework pattern but adapted for assets management.
450
- *
451
- * Access control:
452
- * - Unauthenticated users: Can only see public assets
453
- * - Authenticated users: Can see public assets + their own private assets
454
- * - Admin users: Can see all assets (public and private from all users)
455
- *
456
- * @returns {Promise<DataResponse>} JSON response with all assets
457
- */
458
- async retrieve(req) {
459
- try {
460
- const assets = await this.getAllAssets();
461
- const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
462
- // Admin can see everything
463
- if (isAdmin) {
464
- return successResponse(this.formatAssetsForResponse(assets));
465
- }
466
- // Get authenticated user ID if available
467
- const authenticatedUserId = await this.getAuthenticatedUserId(req);
468
- // Filter to visible assets only
469
- const visibleAssets = assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
470
- return successResponse(this.formatAssetsForResponse(visibleAssets));
471
- }
472
- catch (error) {
473
- return errorResponse(error);
474
- }
475
- }
476
- /**
477
- * Gets the authenticated user's database ID from request headers.
478
- *
479
- * @param req - HTTP request object
480
- * @returns User ID or null if not authenticated
481
- */
482
- async getAuthenticatedUserId(req) {
483
- if (!req || !ApisixAuthParser.hasValidAuth(req.headers || {})) {
484
- return null;
485
- }
486
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
487
- if (!authUser) {
488
- return null;
489
- }
490
- const userRecord = await this.userService.findOrCreateUser(authUser);
491
- return userRecord.id || null;
492
- }
493
- /**
494
- * Formats assets for API response with metadata and URLs.
495
- *
496
- * @param assets - Array of asset records
497
- * @returns Formatted assets array ready for JSON serialization
498
- */
499
- formatAssetsForResponse(assets) {
500
- const config = this.getConfiguration();
501
- return assets.map(asset => ({
502
- id: asset.id,
503
- name: asset.name,
504
- date: asset.date,
505
- contentType: asset.contentType,
506
- description: asset.description || '',
507
- source: asset.source || '',
508
- owner_id: asset.owner_id || null,
509
- filename: asset.filename || '',
510
- is_public: asset.is_public ?? true,
511
- url: `/${config.endpoint}/${asset.id}`,
512
- download_url: `/${config.endpoint}/${asset.id}/download`
513
- }));
514
- }
515
- /**
516
- * Get all assets for this component type.
517
- *
518
- * Retrieves all assets managed by this component, with their metadata.
519
- * Uses a very old start date to get all records.
520
- *
521
- * @returns {Promise<DataRecord[]>} Array of all asset records
522
- *
523
- * @example
524
- * ```typescript
525
- * const allAssets = await assetsManager.getAllAssets()
526
- * // Returns: [{ id, name, type, url, date, contentType }, ...]
527
- * ```
528
- */
529
- async getAllAssets() {
530
- const config = this.getConfiguration();
531
- // Get all assets and sort by date descending (newest first)
532
- const veryOldDate = new Date('1970-01-01');
533
- const farFutureDate = new Date('2099-12-31');
534
- const assets = await this.db.getByDateRange(config.name, veryOldDate, farFutureDate, 1000); // Max 1000 assets
535
- // Sort by date descending (newest first) since getByDateRange returns ascending
536
- return assets.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
537
- }
538
- /**
539
- * Get asset by specific ID.
540
- *
541
- * @param {string} id - The asset ID to retrieve
542
- * @returns {Promise<DataRecord | undefined>} The asset record or undefined if not found
543
- *
544
- * @example
545
- * ```typescript
546
- * const asset = await assetsManager.getAssetById('123')
547
- * if (asset) {
548
- * const fileData = await asset.data()
549
- * }
550
- * ```
551
- */
552
- async getAssetById(id) {
553
- return await this.db.getById(id, this.getConfiguration().name);
554
- }
555
- /**
556
- * Update asset metadata by ID.
557
- *
558
- * Updates the metadata (description, source, and/or visibility) of a specific asset.
559
- * Asset metadata is stored as dedicated columns in the database.
560
- *
561
- * @param {string} id - The ID of the asset to update
562
- * @param {UpdateAssetRequest} updates - The metadata updates to apply
563
- * @throws {Error} If source URL is invalid or asset not found
564
- *
565
- * @example
566
- * ```typescript
567
- * await assetsManager.updateAssetMetadata('123', {
568
- * description: 'Updated building model with new textures',
569
- * source: 'https://updated-source.example.com',
570
- * is_public: false
571
- * })
572
- * ```
573
- */
574
- async updateAssetMetadata(id, updates) {
575
- if (updates.source && !this.validateSourceURL(updates.source)) {
576
- throw new Error('Invalid source URL');
577
- }
578
- const record = await this.db.getById(id, this.getConfiguration().name);
579
- if (!record) {
580
- throw new Error(`Asset with ID ${id} not found`);
581
- }
582
- // Verify this asset belongs to our component
583
- const config = this.getConfiguration();
584
- if (record.name !== config.name) {
585
- throw new Error(`Asset ${id} does not belong to component ${config.name}`);
586
- }
587
- // Apply updates, keeping existing values for non-updated fields
588
- const updatedMetadata = {
589
- id: parseInt(id),
590
- name: config.name,
591
- type: record.contentType,
592
- url: record.url,
593
- date: record.date, // Keep original date
594
- description: updates.description ?? record.description ?? '',
595
- source: updates.source ?? record.source ?? '',
596
- owner_id: record.owner_id ?? null,
597
- filename: record.filename ?? '',
598
- is_public: updates.is_public !== undefined ? updates.is_public : (record.is_public ?? true)
599
- };
600
- // Update the record - delete and re-create with updated metadata
601
- await this.db.delete(id, this.getConfiguration().name);
602
- await this.db.save(updatedMetadata);
603
- }
604
- /**
605
- * Delete asset by ID.
606
- *
607
- * Removes a specific asset.
608
- *
609
- * @param {string} id - The ID of the asset to delete
610
- * @throws {Error} If asset not found or doesn't belong to this component
611
- *
612
- * @example
613
- * ```typescript
614
- * await assetsManager.deleteAssetById('123')
615
- * ```
616
- */
617
- async deleteAssetById(id) {
618
- const record = await this.db.getById(id, this.getConfiguration().name);
619
- if (!record) {
620
- throw new Error(`Asset with ID ${id} not found`);
621
- }
622
- // Verify this asset belongs to our component
623
- const config = this.getConfiguration();
624
- if (record.name !== config.name) {
625
- throw new Error(`Asset ${id} does not belong to component ${config.name}`);
626
- }
627
- await this.db.delete(id, this.getConfiguration().name);
628
- }
629
- /**
630
- * Delete latest asset (simplified)
631
- *
632
- * Removes the most recently uploaded asset for this component type.
633
- *
634
- * @throws {Error} If no assets exist to delete
635
- *
636
- * @example
637
- * ```typescript
638
- * await assetsManager.deleteLatestAsset()
639
- * ```
640
- */
641
- async deleteLatestAsset() {
642
- const config = this.getConfiguration();
643
- const record = await this.db.getLatestByName(config.name);
644
- if (record) {
645
- await this.db.delete(record.id.toString(), this.getConfiguration().name);
646
- }
647
- }
648
- /**
649
- * Upload multiple assets in batch for better performance
650
- *
651
- * @param {CreateAssetRequest[]} requests - Array of asset upload requests
652
- * @throws {Error} If any source URL is invalid
653
- *
654
- * @example
655
- * ```typescript
656
- * await assetsManager.uploadAssetsBatch([
657
- * { description: 'Model 1', source: 'https://example.com/1', file: buffer1, ... },
658
- * { description: 'Model 2', source: 'https://example.com/2', file: buffer2, ... }
659
- * ])
660
- * ```
661
- */
662
- async uploadAssetsBatch(requests) {
663
- if (requests.length === 0)
664
- return;
665
- // Validate all URLs and extensions first
666
- for (const request of requests) {
667
- if (!this.validateSourceURL(request.source)) {
668
- throw new Error(`Invalid source URL: ${request.source}`);
669
- }
670
- if (!this.validateFileExtension(request.filename)) {
671
- const config = this.getConfiguration();
672
- throw new Error(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
673
- }
674
- }
675
- const config = this.getConfiguration();
676
- const now = new Date();
677
- const metadataList = [];
678
- // Store files and prepare metadata
679
- for (const request of requests) {
680
- const url = await this.storage.save(request.file, config.name, request.filename);
681
- const metadata = {
682
- name: config.name,
683
- type: config.contentType,
684
- url,
685
- date: now,
686
- description: request.description,
687
- source: request.source,
688
- owner_id: request.owner_id,
689
- filename: request.filename,
690
- is_public: request.is_public ?? true
691
- };
692
- metadataList.push(metadata);
693
- }
694
- // Save all metadata individually (compatible with all adapters)
695
- for (const metadata of metadataList) {
696
- await this.db.save(metadata);
697
- }
698
- }
699
- /**
700
- * Delete multiple assets by IDs in batch
701
- *
702
- * @param {string[]} ids - Array of asset IDs to delete
703
- * @throws {Error} If any asset not found or doesn't belong to this component
704
- */
705
- async deleteAssetsBatch(ids) {
706
- if (ids.length === 0)
707
- return;
708
- // Delete assets individually (compatible with all adapters)
709
- for (const id of ids) {
710
- await this.deleteAssetById(id);
711
- }
712
- }
713
- /**
714
- * Get endpoints following the framework pattern
715
- */
716
- /**
717
- * Get HTTP endpoints exposed by this assets manager.
718
- *
719
- * Returns the standard CRUD endpoints following the framework pattern.
720
- *
721
- * @returns {Array} Array of endpoint descriptors with methods, paths, and handlers
722
- *
723
- * @example
724
- * ```typescript
725
- * // For a manager with assetType: 'gltf', provides:
726
- * GET /gltf - Get all assets
727
- * POST /gltf/upload - Upload new asset
728
- * GET /gltf/123 - Get specific asset
729
- * PUT /gltf/123 - Update asset metadata
730
- * GET /gltf/123/download - Download asset
731
- * DELETE /gltf/123 - Delete asset
732
- * ```
733
- */
734
- getEndpoints() {
735
- const config = this.getConfiguration();
736
- return [
737
- {
738
- method: 'get',
739
- path: `/${config.endpoint}`,
740
- handler: this.retrieve.bind(this),
741
- responseType: 'application/json'
742
- },
743
- {
744
- method: 'post',
745
- path: `/${config.endpoint}`,
746
- handler: this.handleUpload.bind(this),
747
- responseType: 'application/json'
748
- },
749
- {
750
- method: 'get',
751
- path: `/${config.endpoint}/:id`,
752
- handler: this.handleGetAsset.bind(this),
753
- responseType: config.contentType
754
- },
755
- {
756
- method: 'put',
757
- path: `/${config.endpoint}/:id`,
758
- handler: this.handleUpdate.bind(this),
759
- responseType: 'application/json'
760
- },
761
- {
762
- method: 'get',
763
- path: `/${config.endpoint}/:id/download`,
764
- handler: this.handleDownload.bind(this),
765
- responseType: config.contentType
766
- },
767
- {
768
- method: 'delete',
769
- path: `/${config.endpoint}/:id`,
770
- handler: this.handleDelete.bind(this),
771
- responseType: 'application/json'
772
- },
773
- {
774
- method: 'post',
775
- path: `/${config.endpoint}/batch`,
776
- handler: this.handleUploadBatch.bind(this),
777
- responseType: 'application/json'
778
- },
779
- {
780
- method: 'delete',
781
- path: `/${config.endpoint}/batch`,
782
- handler: this.handleDeleteBatch.bind(this),
783
- responseType: 'application/json'
784
- }
785
- ];
786
- }
787
- /**
788
- * Returns the OpenAPI specification for this assets manager's endpoints.
789
- *
790
- * Generates documentation for all CRUD endpoints including batch operations.
791
- * Can be overridden by subclasses for more detailed specifications.
792
- *
793
- * @returns {OpenAPIComponentSpec} OpenAPI paths, tags, and schemas for this assets manager
794
- */
795
- getOpenAPISpec() {
796
- const config = this.getConfiguration();
797
- const basePath = `/${config.endpoint}`;
798
- const tagName = config.tags?.[0] || config.name;
799
- return {
800
- paths: {
801
- [basePath]: {
802
- get: {
803
- summary: `List all ${config.name} assets`,
804
- description: config.description,
805
- tags: [tagName],
806
- responses: {
807
- '200': {
808
- description: 'List of assets',
809
- content: {
810
- 'application/json': {
811
- schema: {
812
- type: 'array',
813
- items: { $ref: '#/components/schemas/AssetResponse' }
814
- }
815
- }
816
- }
817
- }
818
- }
819
- },
820
- post: {
821
- summary: `Upload a new ${config.name} asset`,
822
- description: 'Upload a new asset file with metadata. Requires authentication.',
823
- tags: [tagName],
824
- security: [{ ApiKeyAuth: [] }],
825
- requestBody: {
826
- required: true,
827
- content: {
828
- 'multipart/form-data': {
829
- schema: {
830
- type: 'object',
831
- required: ['file', 'description', 'source'],
832
- properties: {
833
- file: {
834
- type: 'string',
835
- format: 'binary',
836
- description: 'The file to upload'
837
- },
838
- description: { type: 'string', description: 'Asset description' },
839
- source: {
840
- type: 'string',
841
- format: 'uri',
842
- description: 'Source URL for provenance'
843
- },
844
- is_public: {
845
- type: 'boolean',
846
- description: 'Whether asset is public (default: true)'
847
- }
848
- }
849
- }
850
- }
851
- }
852
- },
853
- responses: {
854
- '200': {
855
- description: 'Asset uploaded successfully',
856
- content: {
857
- 'application/json': {
858
- schema: { $ref: '#/components/schemas/SuccessResponse' }
859
- }
860
- }
861
- },
862
- '400': { description: 'Bad request - missing or invalid fields' },
863
- '401': { description: 'Unauthorized - authentication required' }
864
- }
865
- }
866
- },
867
- [`${basePath}/{id}`]: {
868
- get: {
869
- summary: `Get ${config.name} asset by ID`,
870
- description: 'Returns the asset file content',
871
- tags: [tagName],
872
- parameters: [
873
- {
874
- name: 'id',
875
- in: 'path',
876
- required: true,
877
- schema: { type: 'string' },
878
- description: 'Asset ID'
879
- }
880
- ],
881
- responses: {
882
- '200': {
883
- description: 'Asset file content',
884
- content: {
885
- [config.contentType]: {
886
- schema: { type: 'string', format: 'binary' }
887
- }
888
- }
889
- },
890
- '404': { description: 'Asset not found' }
891
- }
892
- },
893
- put: {
894
- summary: `Update ${config.name} asset metadata`,
895
- description: 'Update asset description, source, or visibility. Requires authentication and ownership.',
896
- tags: [tagName],
897
- security: [{ ApiKeyAuth: [] }],
898
- parameters: [
899
- {
900
- name: 'id',
901
- in: 'path',
902
- required: true,
903
- schema: { type: 'string' },
904
- description: 'Asset ID'
905
- }
906
- ],
907
- requestBody: {
908
- required: true,
909
- content: {
910
- 'application/json': {
911
- schema: {
912
- type: 'object',
913
- properties: {
914
- description: { type: 'string' },
915
- source: { type: 'string', format: 'uri' },
916
- is_public: { type: 'boolean' }
917
- }
918
- }
919
- }
920
- }
921
- },
922
- responses: {
923
- '200': {
924
- description: 'Asset updated successfully',
925
- content: {
926
- 'application/json': {
927
- schema: { $ref: '#/components/schemas/SuccessResponse' }
928
- }
929
- }
930
- },
931
- '400': { description: 'Bad request' },
932
- '401': { description: 'Unauthorized' },
933
- '403': { description: 'Forbidden - not owner' },
934
- '404': { description: 'Asset not found' }
935
- }
936
- },
937
- delete: {
938
- summary: `Delete ${config.name} asset`,
939
- description: 'Delete an asset. Requires authentication and ownership.',
940
- tags: [tagName],
941
- security: [{ ApiKeyAuth: [] }],
942
- parameters: [
943
- {
944
- name: 'id',
945
- in: 'path',
946
- required: true,
947
- schema: { type: 'string' },
948
- description: 'Asset ID'
949
- }
950
- ],
951
- responses: {
952
- '200': {
953
- description: 'Asset deleted successfully',
954
- content: {
955
- 'application/json': {
956
- schema: { $ref: '#/components/schemas/SuccessResponse' }
957
- }
958
- }
959
- },
960
- '401': { description: 'Unauthorized' },
961
- '403': { description: 'Forbidden - not owner' },
962
- '404': { description: 'Asset not found' }
963
- }
964
- }
965
- },
966
- [`${basePath}/{id}/download`]: {
967
- get: {
968
- summary: `Download ${config.name} asset`,
969
- description: 'Download the asset file with Content-Disposition header',
970
- tags: [tagName],
971
- parameters: [
972
- {
973
- name: 'id',
974
- in: 'path',
975
- required: true,
976
- schema: { type: 'string' },
977
- description: 'Asset ID'
978
- }
979
- ],
980
- responses: {
981
- '200': {
982
- description: 'Asset file download',
983
- content: {
984
- [config.contentType]: {
985
- schema: { type: 'string', format: 'binary' }
986
- }
987
- }
988
- },
989
- '404': { description: 'Asset not found' }
990
- }
991
- }
992
- },
993
- [`${basePath}/batch`]: {
994
- post: {
995
- summary: `Batch upload ${config.name} assets`,
996
- description: 'Upload multiple assets in one request. Files must be base64 encoded.',
997
- tags: [tagName],
998
- security: [{ ApiKeyAuth: [] }],
999
- requestBody: {
1000
- required: true,
1001
- content: {
1002
- 'application/json': {
1003
- schema: {
1004
- type: 'object',
1005
- required: ['requests'],
1006
- properties: {
1007
- requests: {
1008
- type: 'array',
1009
- items: {
1010
- type: 'object',
1011
- required: ['file', 'description', 'source', 'filename'],
1012
- properties: {
1013
- file: {
1014
- type: 'string',
1015
- format: 'byte',
1016
- description: 'Base64 encoded file'
1017
- },
1018
- filename: { type: 'string' },
1019
- description: { type: 'string' },
1020
- source: { type: 'string', format: 'uri' },
1021
- is_public: { type: 'boolean' }
1022
- }
1023
- }
1024
- }
1025
- }
1026
- }
1027
- }
1028
- }
1029
- },
1030
- responses: {
1031
- '200': { description: 'All assets uploaded successfully' },
1032
- '207': { description: 'Partial success - some uploads failed' },
1033
- '400': { description: 'Bad request' },
1034
- '401': { description: 'Unauthorized' }
1035
- }
1036
- },
1037
- delete: {
1038
- summary: `Batch delete ${config.name} assets`,
1039
- description: 'Delete multiple assets by IDs. Requires authentication and ownership. Pass IDs as comma-separated query parameter.',
1040
- tags: [tagName],
1041
- security: [{ ApiKeyAuth: [] }],
1042
- parameters: [
1043
- {
1044
- name: 'ids',
1045
- in: 'query',
1046
- required: true,
1047
- schema: {
1048
- type: 'string'
1049
- },
1050
- description: 'Comma-separated list of asset IDs to delete (e.g., 1,2,3)'
1051
- }
1052
- ],
1053
- responses: {
1054
- '200': { description: 'All assets deleted successfully' },
1055
- '207': { description: 'Partial success - some deletions failed' },
1056
- '400': { description: 'Bad request' },
1057
- '401': { description: 'Unauthorized' }
1058
- }
1059
- }
1060
- }
1061
- },
1062
- tags: [
1063
- {
1064
- name: tagName,
1065
- description: config.description
1066
- }
1067
- ],
1068
- schemas: {
1069
- AssetResponse: {
1070
- type: 'object',
1071
- properties: {
1072
- id: { type: 'integer' },
1073
- name: { type: 'string' },
1074
- date: { type: 'string', format: 'date-time' },
1075
- contentType: { type: 'string' },
1076
- description: { type: 'string' },
1077
- source: { type: 'string' },
1078
- owner_id: { type: 'integer', nullable: true },
1079
- filename: { type: 'string' },
1080
- is_public: { type: 'boolean' },
1081
- url: { type: 'string' },
1082
- download_url: { type: 'string' }
1083
- }
1084
- },
1085
- SuccessResponse: {
1086
- type: 'object',
1087
- properties: {
1088
- message: { type: 'string' }
1089
- }
1090
- }
1091
- }
1092
- };
1093
- }
1094
- /**
1095
- * Handle single asset upload via HTTP POST.
1096
- *
1097
- * Flow:
1098
- * 1. Validate request structure and authentication
1099
- * 2. Extract user identity from Apache APISIX headers
1100
- * 3. Validate file extension and read uploaded file
1101
- * 4. Store file via storage service and metadata in database
1102
- * 5. Set owner_id to authenticated user (prevents ownership spoofing)
1103
- * 6. Apply is_public setting (defaults to true if not specified)
1104
- *
1105
- * Authentication: Required
1106
- * Ownership: Automatically set to authenticated user
1107
- *
1108
- * @param req - HTTP request with multipart/form-data file upload
1109
- * @returns HTTP response with success/error status
1110
- *
1111
- * @example
1112
- * POST /assets
1113
- * Content-Type: multipart/form-data
1114
- * x-user-id: user-uuid
1115
- * x-user-roles: user,premium
1116
- *
1117
- * Form data:
1118
- * - file: <binary file>
1119
- * - description: "3D model of building"
1120
- * - source: "https://source.com"
1121
- * - is_public: true
1122
- */
1123
- async handleUpload(req) {
1124
- try {
1125
- // Validate request structure
1126
- if (!req?.body) {
1127
- return badRequestResponse('Invalid request: missing request body');
1128
- }
1129
- // Authenticate user
1130
- const authResult = await this.authenticateRequest(req);
1131
- if (!authResult.success) {
1132
- return authResult.response;
1133
- }
1134
- const userId = authResult.userRecord.id;
1135
- if (!userId) {
1136
- return errorResponse('Failed to retrieve user information');
1137
- }
1138
- // Extract and validate upload data
1139
- const uploadData = this.extractUploadData(req);
1140
- const validation = this.validateUploadFields(uploadData);
1141
- if (!validation.success) {
1142
- return validation.response;
1143
- }
1144
- const validData = validation.data;
1145
- // Get file buffer from memory or read from temporary location
1146
- let fileBuffer;
1147
- if (validData.fileBuffer) {
1148
- // Memory storage: buffer already available
1149
- fileBuffer = validData.fileBuffer;
1150
- }
1151
- else if (validData.filePath) {
1152
- // Disk storage: read from temp file
1153
- try {
1154
- fileBuffer = await this.readTempFile(validData.filePath);
1155
- }
1156
- catch (error) {
1157
- return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
1158
- }
1159
- }
1160
- else {
1161
- return badRequestResponse('No file data available');
1162
- }
1163
- // Upload asset and cleanup
1164
- try {
1165
- await this.uploadAsset({
1166
- description: validData.description,
1167
- source: validData.source,
1168
- owner_id: userId,
1169
- filename: validData.filename,
1170
- file: fileBuffer,
1171
- is_public: validData.is_public
1172
- });
1173
- if (validData.filePath) {
1174
- await this.cleanupTempFile(validData.filePath);
1175
- }
1176
- }
1177
- catch (error) {
1178
- if (validData.filePath) {
1179
- await this.cleanupTempFile(validData.filePath);
1180
- }
1181
- throw error;
1182
- }
1183
- return successResponse({ message: 'Asset uploaded successfully' });
1184
- }
1185
- catch (error) {
1186
- return errorResponse(error);
1187
- }
1188
- }
1189
- /**
1190
- * Handle update endpoint (PUT).
1191
- *
1192
- * Updates metadata for a specific asset by ID.
1193
- *
1194
- * @param {any} req - HTTP request object with params.id and body containing updates
1195
- * @returns {Promise<DataResponse>} HTTP response
1196
- *
1197
- * @example
1198
- * ```typescript
1199
- * // PUT /gltf/123
1200
- * // Body: { "description": "Updated model", "source": "https://new-source.com" }
1201
- * ```
1202
- */
1203
- async handleUpdate(req) {
1204
- try {
1205
- if (!req) {
1206
- return badRequestResponse('Invalid request: missing request object');
1207
- }
1208
- // Authenticate user
1209
- const authResult = await this.authenticateRequest(req);
1210
- if (!authResult.success) {
1211
- return authResult.response;
1212
- }
1213
- const userId = authResult.userRecord.id;
1214
- if (!userId) {
1215
- return errorResponse('Failed to retrieve user information');
1216
- }
1217
- const { id } = req.params || {};
1218
- const { description, source, is_public } = req.body || {};
1219
- if (!id) {
1220
- return badRequestResponse('Asset ID is required');
1221
- }
1222
- if (!description && !source && is_public === undefined) {
1223
- return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
1224
- }
1225
- // Check if asset exists
1226
- const asset = await this.getAssetById(id);
1227
- if (!asset) {
1228
- return notFoundResponse('Asset not found');
1229
- }
1230
- // Check ownership (admins can modify any asset)
1231
- const ownershipError = this.validateOwnership(asset, userId, req.headers);
1232
- if (ownershipError) {
1233
- return ownershipError;
1234
- }
1235
- // Build and apply updates
1236
- const updates = {};
1237
- if (description !== undefined)
1238
- updates.description = description;
1239
- if (source !== undefined)
1240
- updates.source = source;
1241
- if (is_public !== undefined)
1242
- updates.is_public = Boolean(is_public);
1243
- await this.updateAssetMetadata(id, updates);
1244
- return successResponse({ message: 'Asset metadata updated successfully' });
1245
- }
1246
- catch (error) {
1247
- return errorResponse(error);
1248
- }
1249
- }
1250
- /**
1251
- * Handle get asset endpoint (GET).
1252
- *
1253
- * Returns the file content of a specific asset by ID for display/use in front-end.
1254
- * No download headers - just the raw file content.
1255
- *
1256
- * Access control:
1257
- * - Public assets: Accessible to everyone
1258
- * - Private assets: Accessible only to owner
1259
- * - Admin users: Can access all assets (public and private)
1260
- *
1261
- * @param {any} req - HTTP request object with params.id
1262
- * @returns {Promise<DataResponse>} HTTP response with file content
1263
- *
1264
- * @example
1265
- * ```typescript
1266
- * // GET /gltf/123
1267
- * // Returns the .glb file content for display in 3D viewer
1268
- * ```
1269
- */
1270
- async handleGetAsset(req) {
1271
- try {
1272
- const result = await this.fetchAssetWithAccessCheck(req);
1273
- if (!result.success) {
1274
- return result.response;
1275
- }
1276
- const fileContent = await result.asset.data();
1277
- return fileResponse(fileContent, this.getConfiguration().contentType);
1278
- }
1279
- catch (error) {
1280
- return errorResponse(error);
1281
- }
1282
- }
1283
- /**
1284
- * Handle download endpoint (GET).
1285
- *
1286
- * Downloads the file content of a specific asset by ID with download headers.
1287
- * Forces browser to download the file rather than display it.
1288
- *
1289
- * Access control:
1290
- * - Public assets: Accessible to everyone
1291
- * - Private assets: Accessible only to owner
1292
- * - Admin users: Can download all assets (public and private)
1293
- *
1294
- * @param {any} req - HTTP request object with params.id
1295
- * @returns {Promise<DataResponse>} HTTP response with file content and download headers
1296
- *
1297
- * @example
1298
- * ```typescript
1299
- * // GET /gltf/123/download
1300
- * // Returns the .glb file with download headers - browser will save it
1301
- * ```
1302
- */
1303
- async handleDownload(req) {
1304
- try {
1305
- const result = await this.fetchAssetWithAccessCheck(req);
1306
- if (!result.success) {
1307
- return result.response;
1308
- }
1309
- const fileContent = await result.asset.data();
1310
- const filename = result.asset.filename || `asset_${req.params?.id}`;
1311
- return fileResponse(fileContent, this.getConfiguration().contentType, filename);
1312
- }
1313
- catch (error) {
1314
- return errorResponse(error);
1315
- }
1316
- }
1317
- /**
1318
- * Handle delete endpoint (DELETE).
1319
- *
1320
- * Deletes a specific asset by ID.
1321
- *
1322
- * @param {any} req - HTTP request object with params.id
1323
- * @returns {Promise<DataResponse>} HTTP response
1324
- *
1325
- * @example
1326
- * ```typescript
1327
- * // DELETE /gltf/123
1328
- * ```
1329
- */
1330
- async handleDelete(req) {
1331
- try {
1332
- // Authenticate user
1333
- const authResult = await this.authenticateRequest(req);
1334
- if (!authResult.success) {
1335
- return authResult.response;
1336
- }
1337
- const userId = authResult.userRecord.id;
1338
- if (!userId) {
1339
- return errorResponse('Failed to retrieve user information');
1340
- }
1341
- const { id } = req.params || {};
1342
- if (!id) {
1343
- return badRequestResponse('Asset ID is required');
1344
- }
1345
- // Check if asset exists
1346
- const asset = await this.getAssetById(id);
1347
- if (!asset) {
1348
- return notFoundResponse('Asset not found');
1349
- }
1350
- // Check ownership (admins can delete any asset)
1351
- const ownershipError = this.validateOwnership(asset, userId, req.headers);
1352
- if (ownershipError) {
1353
- return ownershipError;
1354
- }
1355
- await this.deleteAssetById(id);
1356
- return successResponse({ message: 'Asset deleted successfully' });
1357
- }
1358
- catch (error) {
1359
- return errorResponse(error);
1360
- }
1361
- }
1362
- /**
1363
- * Handle batch upload endpoint
1364
- */
1365
- async handleUploadBatch(req) {
1366
- try {
1367
- if (!req?.body) {
1368
- return badRequestResponse('Invalid request: missing request body');
1369
- }
1370
- // Authenticate user
1371
- const authResult = await this.authenticateRequest(req);
1372
- if (!authResult.success) {
1373
- return authResult.response;
1374
- }
1375
- const userId = authResult.userRecord.id;
1376
- if (!userId) {
1377
- return errorResponse('Failed to retrieve user information');
1378
- }
1379
- const requests = req.body.requests;
1380
- if (!Array.isArray(requests) || requests.length === 0) {
1381
- return badRequestResponse('Requests array is required and must not be empty');
1382
- }
1383
- // Validate all requests first
1384
- const validationError = this.validateBatchRequests(requests);
1385
- if (validationError) {
1386
- return validationError;
1387
- }
1388
- // Process each request
1389
- const results = await this.processBatchUploads(requests, userId);
1390
- const successCount = results.filter(r => r.success).length;
1391
- const failureCount = results.length - successCount;
1392
- const message = `${successCount}/${requests.length} assets uploaded successfully`;
1393
- if (failureCount > 0) {
1394
- return multiStatusResponse(message, results);
1395
- }
1396
- return successResponse({ message, results });
1397
- }
1398
- catch (error) {
1399
- return errorResponse(error);
1400
- }
1401
- }
1402
- /**
1403
- * Validates all requests in a batch upload.
1404
- *
1405
- * @param requests - Array of upload requests to validate
1406
- * @returns DataResponse with error if validation fails, undefined if valid
1407
- */
1408
- validateBatchRequests(requests) {
1409
- const config = this.getConfiguration();
1410
- for (const request of requests) {
1411
- if (!request.file || !request.description || !request.source || !request.filename) {
1412
- return badRequestResponse('Each request must have description, source, filename, and file');
1413
- }
1414
- if (!this.validateBase64(request.file)) {
1415
- return badRequestResponse(`Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`);
1416
- }
1417
- if (!this.validateFileExtension(request.filename)) {
1418
- return badRequestResponse(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
1419
- }
1420
- }
1421
- return undefined;
1422
- }
1423
- /**
1424
- * Processes batch upload requests.
1425
- *
1426
- * @param requests - Array of upload requests
1427
- * @param ownerId - Owner user ID
1428
- * @returns Array of results for each upload
1429
- */
1430
- async processBatchUploads(requests, ownerId) {
1431
- const results = [];
1432
- for (const request of requests) {
1433
- try {
1434
- await this.uploadAsset({
1435
- description: request.description,
1436
- source: request.source,
1437
- owner_id: ownerId,
1438
- filename: request.filename,
1439
- file: Buffer.from(request.file, 'base64'),
1440
- is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
1441
- });
1442
- results.push({ success: true, filename: request.filename });
1443
- }
1444
- catch (error) {
1445
- results.push({
1446
- success: false,
1447
- filename: request.filename,
1448
- error: error instanceof Error ? error.message : 'Unknown error'
1449
- });
1450
- }
1451
- }
1452
- return results;
1453
- }
1454
- /**
1455
- * Handle batch delete endpoint
1456
- */
1457
- async handleDeleteBatch(req) {
1458
- try {
1459
- if (!req?.body) {
1460
- return badRequestResponse('Invalid request: missing request body');
1461
- }
1462
- // Authenticate user
1463
- const authResult = await this.authenticateRequest(req);
1464
- if (!authResult.success) {
1465
- return authResult.response;
1466
- }
1467
- const userId = authResult.userRecord.id;
1468
- if (!userId) {
1469
- return errorResponse('Failed to retrieve user information');
1470
- }
1471
- const { ids } = req.body;
1472
- if (!Array.isArray(ids) || ids.length === 0) {
1473
- return badRequestResponse('IDs array is required and must not be empty');
1474
- }
1475
- // Process deletions (admins can delete any asset)
1476
- const results = await this.processBatchDeletes(ids, userId, req.headers);
1477
- const successCount = results.filter(r => r.success).length;
1478
- const failureCount = results.length - successCount;
1479
- const message = `${successCount}/${ids.length} assets deleted successfully`;
1480
- if (failureCount > 0) {
1481
- return multiStatusResponse(message, results);
1482
- }
1483
- return successResponse({ message, results });
1484
- }
1485
- catch (error) {
1486
- return errorResponse(error);
1487
- }
1488
- }
1489
- /**
1490
- * Processes batch delete requests.
1491
- *
1492
- * Admins can delete any asset. Regular users can only delete their own assets
1493
- * or assets with no owner.
1494
- *
1495
- * @param ids - Array of asset IDs to delete
1496
- * @param userId - User ID for ownership validation
1497
- * @param headers - HTTP request headers (for admin check)
1498
- * @returns Array of results for each deletion
1499
- */
1500
- async processBatchDeletes(ids, userId, headers) {
1501
- const results = [];
1502
- const isAdmin = headers && ApisixAuthParser.isAdmin(headers);
1503
- for (const id of ids) {
1504
- try {
1505
- const asset = await this.getAssetById(id);
1506
- if (!asset) {
1507
- results.push({ success: false, id, error: 'Asset not found' });
1508
- continue;
1509
- }
1510
- // Allow deletion if: admin OR owner is the current user OR asset has no owner
1511
- if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
1512
- results.push({ success: false, id, error: 'You can only delete your own assets' });
1513
- continue;
1514
- }
1515
- await this.deleteAssetById(id);
1516
- results.push({ success: true, id });
1517
- }
1518
- catch (error) {
1519
- results.push({
1520
- success: false,
1521
- id,
1522
- error: error instanceof Error ? error.message : 'Unknown error'
1523
- });
1524
- }
1525
- }
1526
- return results;
1527
- }
1528
- }
1529
- //# sourceMappingURL=assets_manager.js.map