digitaltwin-core 0.13.0 → 0.13.2

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 (36) hide show
  1. package/dist/components/tileset_manager.d.ts +46 -65
  2. package/dist/components/tileset_manager.d.ts.map +1 -1
  3. package/dist/components/tileset_manager.js +415 -652
  4. package/dist/components/tileset_manager.js.map +1 -1
  5. package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
  6. package/dist/database/adapters/knex_database_adapter.js +9 -12
  7. package/dist/database/adapters/knex_database_adapter.js.map +1 -1
  8. package/dist/engine/upload_processor.d.ts +7 -37
  9. package/dist/engine/upload_processor.d.ts.map +1 -1
  10. package/dist/engine/upload_processor.js +45 -92
  11. package/dist/engine/upload_processor.js.map +1 -1
  12. package/dist/storage/adapters/local_storage_service.d.ts +14 -0
  13. package/dist/storage/adapters/local_storage_service.d.ts.map +1 -1
  14. package/dist/storage/adapters/local_storage_service.js +46 -0
  15. package/dist/storage/adapters/local_storage_service.js.map +1 -1
  16. package/dist/storage/adapters/ovh_storage_service.d.ts +15 -0
  17. package/dist/storage/adapters/ovh_storage_service.d.ts.map +1 -1
  18. package/dist/storage/adapters/ovh_storage_service.js +54 -2
  19. package/dist/storage/adapters/ovh_storage_service.js.map +1 -1
  20. package/dist/storage/storage_service.d.ts +36 -0
  21. package/dist/storage/storage_service.d.ts.map +1 -1
  22. package/dist/storage/storage_service.js.map +1 -1
  23. package/dist/types/data_record.d.ts +22 -15
  24. package/dist/types/data_record.d.ts.map +1 -1
  25. package/dist/utils/index.d.ts +1 -1
  26. package/dist/utils/index.d.ts.map +1 -1
  27. package/dist/utils/index.js +1 -1
  28. package/dist/utils/index.js.map +1 -1
  29. package/dist/utils/map_to_data_record.d.ts.map +1 -1
  30. package/dist/utils/map_to_data_record.js +8 -2
  31. package/dist/utils/map_to_data_record.js.map +1 -1
  32. package/dist/utils/zip_utils.d.ts +14 -30
  33. package/dist/utils/zip_utils.d.ts.map +1 -1
  34. package/dist/utils/zip_utils.js +50 -69
  35. package/dist/utils/zip_utils.js.map +1 -1
  36. package/package.json +1 -1
@@ -3,42 +3,50 @@ import { extractAndStoreArchive } from '../utils/zip_utils.js';
3
3
  import { ApisixAuthParser } from '../auth/apisix_parser.js';
4
4
  import { AuthConfig } from '../auth/auth_config.js';
5
5
  import fs from 'fs/promises';
6
+ import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, notFoundResponse, forbiddenResponse } from '../utils/http_responses.js';
7
+ /** Threshold for async upload (50MB) */
8
+ const ASYNC_UPLOAD_THRESHOLD = 50 * 1024 * 1024;
6
9
  /**
7
10
  * Specialized Assets Manager for handling 3D Tiles tilesets.
8
11
  *
9
- * This manager extracts uploaded ZIP files and stores each file individually,
10
- * allowing Cesium and other 3D viewers to load tilesets directly via URL.
12
+ * This manager extracts uploaded ZIP files and stores each file in cloud storage (OVH S3),
13
+ * allowing Cesium and other 3D viewers to load tilesets directly via public URLs.
11
14
  *
12
- * Key features:
13
- * - Extracts ZIP archives on upload (no ZIP stored, only extracted files)
14
- * - Detects and tracks the root tileset.json file
15
- * - Serves individual files via dedicated endpoint
16
- * - Returns tileset.json URL for direct Cesium loading
15
+ * ## How it works
17
16
  *
18
- * Endpoints:
19
- * - GET /{endpoint} - List all tilesets with tileset.json URLs
20
- * - POST /{endpoint} - Upload tileset ZIP (extracts and stores files)
21
- * - GET /{endpoint}/:id - Get tileset metadata
22
- * - GET /{endpoint}/:id/files/* - Serve extracted files (tileset.json, .b3dm, etc.)
17
+ * 1. User uploads a ZIP containing a 3D Tiles tileset
18
+ * 2. ZIP is extracted and all files are stored in OVH with public-read ACL
19
+ * 3. Database stores only the tileset.json URL and base path
20
+ * 4. Cesium loads tileset.json directly from OVH
21
+ * 5. Cesium fetches tiles using relative paths in tileset.json (directly from OVH)
22
+ *
23
+ * ## Endpoints
24
+ *
25
+ * - GET /{endpoint} - List all tilesets with their public URLs
26
+ * - POST /{endpoint} - Upload tileset ZIP (sync < 50MB, async >= 50MB)
27
+ * - GET /{endpoint}/:id/status - Poll async upload status
23
28
  * - PUT /{endpoint}/:id - Update tileset metadata
24
- * - DELETE /{endpoint}/:id - Delete tileset and all extracted files
29
+ * - DELETE /{endpoint}/:id - Delete tileset and all files from storage
25
30
  *
26
31
  * @example
27
32
  * ```typescript
28
33
  * class MyTilesetManager extends TilesetManager {
29
34
  * getConfiguration() {
30
35
  * return {
31
- * name: 'tilesets_manager',
36
+ * name: 'tilesets',
32
37
  * description: 'Manage 3D Tiles tilesets',
33
38
  * contentType: 'application/json',
34
39
  * endpoint: 'api/tilesets',
35
- * tags: ['Tileset']
40
+ * extension: '.zip'
36
41
  * }
37
42
  * }
38
43
  * }
39
44
  *
40
- * // After upload, Cesium can load:
41
- * // Cesium3DTileset.fromUrl('/api/tilesets/123/files/tileset.json')
45
+ * // After upload, response contains:
46
+ * // { tileset_url: 'https://bucket.s3.../tilesets/123/tileset.json' }
47
+ * //
48
+ * // Cesium loads directly:
49
+ * // Cesium.Cesium3DTileset.fromUrl(tileset_url)
42
50
  * ```
43
51
  */
44
52
  export class TilesetManager extends AssetsManager {
@@ -55,524 +63,240 @@ export class TilesetManager extends AssetsManager {
55
63
  this.uploadQueue = queue;
56
64
  }
57
65
  /**
58
- * Override the upload handler to support async processing for large files.
59
- *
60
- * When uploadQueue is available (async mode):
61
- * 1. Validates request and authentication
62
- * 2. Creates a database record with status 'pending'
63
- * 3. Queues a job for background processing
64
- * 4. Returns immediately with job ID
65
- *
66
- * When uploadQueue is not available (sync mode - fallback):
67
- * 1. Validates request and authentication
68
- * 2. Extracts ZIP and stores files synchronously
69
- * 3. Returns with completed tileset info
66
+ * Handle tileset upload.
70
67
  *
71
- * @param req - HTTP request with ZIP file upload
72
- * @returns DataResponse with upload result (async: job info, sync: tileset info)
68
+ * - Files < 50MB: Synchronous extraction and upload
69
+ * - Files >= 50MB: Queued for async processing (returns 202)
73
70
  */
74
71
  async handleUpload(req) {
75
72
  try {
76
- if (!req || !req.body) {
77
- return {
78
- status: 400,
79
- content: JSON.stringify({
80
- error: 'Invalid request: missing request body'
81
- }),
82
- headers: { 'Content-Type': 'application/json' }
83
- };
73
+ if (!req?.body) {
74
+ return badRequestResponse('Invalid request: missing request body');
84
75
  }
85
- // Check authentication (skip if auth is disabled)
86
- let userId;
87
- if (AuthConfig.isAuthDisabled()) {
88
- const anonymousUser = {
89
- id: AuthConfig.getAnonymousUserId(),
90
- roles: []
91
- };
92
- const userRecord = await this.userService.findOrCreateUser(anonymousUser);
93
- userId = userRecord.id;
94
- }
95
- else {
96
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
97
- return {
98
- status: 401,
99
- content: JSON.stringify({
100
- error: 'Authentication required'
101
- }),
102
- headers: { 'Content-Type': 'application/json' }
103
- };
104
- }
105
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
106
- if (!authUser) {
107
- return {
108
- status: 401,
109
- content: JSON.stringify({
110
- error: 'Invalid authentication headers'
111
- }),
112
- headers: { 'Content-Type': 'application/json' }
113
- };
114
- }
115
- const userRecord = await this.userService.findOrCreateUser(authUser);
116
- if (!userRecord.id) {
117
- return {
118
- status: 500,
119
- content: JSON.stringify({
120
- error: 'Failed to retrieve user information'
121
- }),
122
- headers: { 'Content-Type': 'application/json' }
123
- };
124
- }
125
- userId = userRecord.id;
76
+ // Authenticate user
77
+ const userId = await this.authenticateUser(req);
78
+ if (typeof userId !== 'number') {
79
+ return userId; // Returns error response
126
80
  }
81
+ // Validate request
127
82
  const { description } = req.body;
128
83
  const filePath = req.file?.path;
129
84
  const fileBuffer = req.file?.buffer;
130
85
  const filename = req.file?.originalname || req.body.filename;
131
- // Support both disk storage (filePath) and memory storage (fileBuffer)
86
+ const fileSize = req.file?.size || fileBuffer?.length || 0;
132
87
  if (!filePath && !fileBuffer) {
133
- return {
134
- status: 400,
135
- content: JSON.stringify({
136
- error: 'Missing required fields: ZIP file'
137
- }),
138
- headers: { 'Content-Type': 'application/json' }
139
- };
88
+ return badRequestResponse('Missing required field: ZIP file');
140
89
  }
141
90
  if (!description) {
142
- return {
143
- status: 400,
144
- content: JSON.stringify({
145
- error: 'Missing required fields: description'
146
- }),
147
- headers: { 'Content-Type': 'application/json' }
148
- };
91
+ if (filePath)
92
+ await fs.unlink(filePath).catch(() => { });
93
+ return badRequestResponse('Missing required field: description');
149
94
  }
150
95
  if (!filename) {
151
- return {
152
- status: 400,
153
- content: JSON.stringify({
154
- error: 'Filename could not be determined from uploaded file'
155
- }),
156
- headers: { 'Content-Type': 'application/json' }
157
- };
96
+ if (filePath)
97
+ await fs.unlink(filePath).catch(() => { });
98
+ return badRequestResponse('Filename could not be determined from uploaded file');
158
99
  }
159
- // Validate ZIP file extension
160
100
  if (!filename.toLowerCase().endsWith('.zip')) {
161
- return {
162
- status: 400,
163
- content: JSON.stringify({
164
- error: 'Invalid file extension. Expected: .zip'
165
- }),
166
- headers: { 'Content-Type': 'application/json' }
167
- };
101
+ if (filePath)
102
+ await fs.unlink(filePath).catch(() => { });
103
+ return badRequestResponse('Invalid file extension. Expected: .zip');
168
104
  }
169
105
  const config = this.getConfiguration();
170
- // Debug: log upload mode selection
171
- console.log(`[TilesetManager] Upload: queue=${!!this.uploadQueue}, filePath=${!!filePath}, fileBuffer=${!!fileBuffer}`);
172
- // ASYNC MODE: Queue job for background processing (requires disk storage)
173
- if (this.uploadQueue && filePath) {
174
- console.log(`[TilesetManager] Using ASYNC mode for ${filename}`);
175
- return this.handleAsyncUpload(req, userId, filePath, filename, description, config);
106
+ const isPublic = req.body.is_public !== undefined ? Boolean(req.body.is_public) : true;
107
+ // Route to async or sync based on file size and queue availability
108
+ if (this.uploadQueue && filePath && fileSize >= ASYNC_UPLOAD_THRESHOLD) {
109
+ return this.handleAsyncUpload(userId, filePath, filename, description, isPublic, config);
176
110
  }
177
- // SYNC MODE: Process immediately (fallback or memory buffer)
178
- console.log(`[TilesetManager] Using SYNC mode for ${filename}`);
179
- return this.handleSyncUpload(req, userId, filePath, fileBuffer, filename, description, config);
111
+ return this.handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config);
180
112
  }
181
113
  catch (error) {
182
- return {
183
- status: 500,
184
- content: JSON.stringify({
185
- error: error instanceof Error ? error.message : 'Unknown error'
186
- }),
187
- headers: { 'Content-Type': 'application/json' }
188
- };
114
+ if (req.file?.path)
115
+ await fs.unlink(req.file.path).catch(() => { });
116
+ return errorResponse(error);
189
117
  }
190
118
  }
191
119
  /**
192
- * Handle upload asynchronously by queuing a job
193
- * @private
120
+ * Authenticate user from request headers.
121
+ * Returns user ID on success, or error response on failure.
194
122
  */
195
- async handleAsyncUpload(req, userId, filePath, filename, description, config) {
196
- const now = new Date();
197
- // Create a pending record in the database
198
- const metadata = {
199
- name: config.name,
200
- type: 'application/json',
201
- url: '', // Will be set by worker after extraction
202
- date: now,
203
- description,
204
- source: req.body.source || '',
205
- owner_id: userId,
206
- filename: filename,
207
- is_public: req.body.is_public !== undefined ? Boolean(req.body.is_public) : true,
208
- upload_status: 'pending'
209
- };
210
- const savedRecord = await this.db.save(metadata);
211
- const recordId = savedRecord.id;
212
- // Queue the processing job
213
- const jobData = {
214
- type: 'tileset',
215
- recordId,
216
- tempFilePath: filePath,
217
- componentName: config.name,
218
- userId,
219
- filename,
220
- description
221
- };
222
- // uploadQueue is guaranteed to exist here (checked in handleUpload)
223
- let job;
123
+ async authenticateUser(req) {
124
+ if (AuthConfig.isAuthDisabled()) {
125
+ const userRecord = await this.userService.findOrCreateUser({
126
+ id: AuthConfig.getAnonymousUserId(),
127
+ roles: []
128
+ });
129
+ return userRecord.id;
130
+ }
131
+ if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
132
+ return unauthorizedResponse();
133
+ }
134
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
135
+ if (!authUser) {
136
+ return unauthorizedResponse('Invalid authentication headers');
137
+ }
138
+ const userRecord = await this.userService.findOrCreateUser(authUser);
139
+ if (!userRecord.id) {
140
+ return errorResponse('Failed to retrieve user information');
141
+ }
142
+ return userRecord.id;
143
+ }
144
+ /**
145
+ * Queue upload for background processing. Returns HTTP 202 immediately.
146
+ */
147
+ async handleAsyncUpload(userId, filePath, filename, description, isPublic, config) {
148
+ let recordId = null;
224
149
  try {
225
- job = await this.uploadQueue?.add(`tileset-${recordId}`, jobData, {
150
+ // Create pending record (url will be updated after extraction)
151
+ const metadata = {
152
+ name: config.name,
153
+ type: 'application/json',
154
+ url: '',
155
+ tileset_url: '',
156
+ date: new Date(),
157
+ description,
158
+ filename,
159
+ owner_id: userId,
160
+ is_public: isPublic,
161
+ upload_status: 'pending'
162
+ };
163
+ const savedRecord = await this.db.save(metadata);
164
+ recordId = savedRecord.id;
165
+ const jobData = {
166
+ type: 'tileset',
167
+ recordId,
168
+ tempFilePath: filePath,
169
+ componentName: config.name,
170
+ userId,
171
+ filename,
172
+ description
173
+ };
174
+ const job = await this.uploadQueue?.add(`tileset-${recordId}`, jobData, {
226
175
  jobId: `tileset-upload-${recordId}`
227
176
  });
228
- if (!job) {
177
+ if (!job)
229
178
  throw new Error('Failed to queue upload job');
230
- }
179
+ await this.db.updateById(config.name, recordId, { upload_job_id: job.id });
180
+ return {
181
+ status: 202,
182
+ content: JSON.stringify({
183
+ message: 'Tileset upload accepted, processing in background',
184
+ id: recordId,
185
+ job_id: job.id,
186
+ status: 'pending',
187
+ status_url: `/${config.endpoint}/${recordId}/status`
188
+ }),
189
+ headers: { 'Content-Type': 'application/json' }
190
+ };
231
191
  }
232
- catch (queueError) {
233
- // Clean up: delete the DB record if queue fails
234
- await this.db.delete(String(recordId), config.name).catch(() => { });
235
- // Clean up temp file
192
+ catch (error) {
193
+ if (recordId !== null)
194
+ await this.db.delete(String(recordId), config.name).catch(() => { });
236
195
  await fs.unlink(filePath).catch(() => { });
237
- throw queueError;
196
+ throw error;
238
197
  }
239
- // Update record with job ID
240
- await this.db.updateById(config.name, recordId, {
241
- upload_job_id: job.id
242
- });
243
- return {
244
- status: 202, // Accepted - processing started
245
- content: JSON.stringify({
246
- message: 'Tileset upload accepted, processing in background',
247
- id: recordId,
248
- job_id: job.id,
249
- status: 'pending',
250
- status_url: `/${config.endpoint}/${recordId}/status`
251
- }),
252
- headers: { 'Content-Type': 'application/json' }
253
- };
254
198
  }
255
199
  /**
256
- * Handle upload synchronously (original behavior)
257
- * @private
200
+ * Process upload synchronously.
258
201
  */
259
- async handleSyncUpload(req, userId, filePath, fileBuffer, filename, description, config) {
260
- // Read ZIP file content
202
+ async handleSyncUpload(userId, filePath, fileBuffer, filename, description, isPublic, config) {
261
203
  let zipBuffer;
262
204
  try {
263
- if (fileBuffer) {
264
- zipBuffer = fileBuffer;
265
- }
266
- else if (filePath) {
267
- zipBuffer = await fs.readFile(filePath);
268
- }
269
- else {
205
+ const readBuffer = fileBuffer || (filePath ? await fs.readFile(filePath) : null);
206
+ if (!readBuffer)
270
207
  throw new Error('No file data available');
271
- }
208
+ zipBuffer = readBuffer;
272
209
  }
273
210
  catch (error) {
274
- return {
275
- status: 500,
276
- content: JSON.stringify({
277
- error: `Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`
278
- }),
279
- headers: { 'Content-Type': 'application/json' }
280
- };
211
+ return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
281
212
  }
282
213
  try {
283
- const now = new Date();
284
- // Generate unique ID for this tileset (timestamp-based)
285
- const tilesetId = now.getTime().toString();
286
- // Base path for storing extracted files: componentName/tilesetId
287
- const basePath = `${config.name}/${tilesetId}`;
288
- // Extract ZIP and store all files individually
214
+ // Generate unique base path using timestamp
215
+ const basePath = `${config.name}/${Date.now()}`;
216
+ // Extract ZIP and upload all files to storage
289
217
  const extractResult = await extractAndStoreArchive(zipBuffer, this.storage, basePath);
290
- // Validate that we found a tileset.json
291
218
  if (!extractResult.root_file) {
292
- // Clean up stored files if no tileset.json found using batch delete
293
- if (extractResult.files.length > 0) {
294
- const filePaths = extractResult.files.map(file => file.path);
295
- await this.storage.deleteBatch(filePaths);
296
- }
297
- return {
298
- status: 400,
299
- content: JSON.stringify({
300
- error: 'Invalid tileset: no tileset.json found in the ZIP archive'
301
- }),
302
- headers: { 'Content-Type': 'application/json' }
303
- };
304
- }
305
- // Create metadata with file_index
306
- // url stores the base path for all extracted files
219
+ // Clean up uploaded files
220
+ await this.storage.deleteByPrefix(basePath).catch(() => { });
221
+ return badRequestResponse('Invalid tileset: no tileset.json found in the ZIP archive');
222
+ }
223
+ // Build the public URL for tileset.json
224
+ const tilesetPath = `${basePath}/${extractResult.root_file}`;
225
+ const tilesetUrl = this.storage.getPublicUrl(tilesetPath);
226
+ // Save metadata to database (url = basePath for deletion)
307
227
  const metadata = {
308
228
  name: config.name,
309
- type: 'application/json', // tileset.json content type
310
- url: basePath, // Base path for all files (componentName/timestamp)
311
- date: now,
229
+ type: 'application/json',
230
+ url: basePath,
231
+ tileset_url: tilesetUrl,
232
+ date: new Date(),
312
233
  description,
313
- source: req.body.source || '',
234
+ filename,
314
235
  owner_id: userId,
315
- filename: filename,
316
- is_public: req.body.is_public !== undefined ? Boolean(req.body.is_public) : true,
317
- file_index: {
318
- files: extractResult.files,
319
- root_file: extractResult.root_file
320
- }
236
+ is_public: isPublic,
237
+ upload_status: 'completed'
321
238
  };
322
239
  const savedRecord = await this.db.save(metadata);
323
- // Clean up temporary file after successful upload
324
- if (filePath) {
240
+ // Clean up temp file
241
+ if (filePath)
325
242
  await fs.unlink(filePath).catch(() => { });
326
- }
327
- return {
328
- status: 200,
329
- content: JSON.stringify({
330
- message: 'Tileset uploaded and extracted successfully',
331
- id: savedRecord.id,
332
- file_count: extractResult.file_count,
333
- root_file: extractResult.root_file,
334
- tileset_url: `/${config.endpoint}/${savedRecord.id}/files/${extractResult.root_file}`
335
- }),
336
- headers: { 'Content-Type': 'application/json' }
337
- };
243
+ return successResponse({
244
+ message: 'Tileset uploaded successfully',
245
+ id: savedRecord.id,
246
+ tileset_url: tilesetUrl,
247
+ file_count: extractResult.file_count
248
+ });
338
249
  }
339
250
  catch (error) {
340
- // Clean up temporary file even on error
341
- if (filePath) {
251
+ if (filePath)
342
252
  await fs.unlink(filePath).catch(() => { });
343
- }
344
253
  throw error;
345
254
  }
346
255
  }
347
256
  /**
348
- * Get the status of an upload job
257
+ * Get upload status for async uploads.
349
258
  */
350
259
  async handleGetStatus(req) {
351
260
  try {
352
261
  const { id } = req.params || {};
353
262
  if (!id) {
354
- return {
355
- status: 400,
356
- content: JSON.stringify({ error: 'Asset ID is required' }),
357
- headers: { 'Content-Type': 'application/json' }
358
- };
263
+ return badRequestResponse('Asset ID is required');
359
264
  }
360
265
  const asset = await this.getAssetById(id);
361
266
  if (!asset) {
362
- return {
363
- status: 404,
364
- content: JSON.stringify({ error: 'Tileset not found' }),
365
- headers: { 'Content-Type': 'application/json' }
366
- };
267
+ return notFoundResponse('Tileset not found');
367
268
  }
368
- const config = this.getConfiguration();
369
269
  const record = asset;
370
- const fileIndex = record.file_index;
371
- // Determine actual status:
372
- // - If upload_status is 'completed' OR
373
- // - If upload_status is null/undefined BUT file_index exists (legacy sync uploads)
374
- const isCompleted = record.upload_status === 'completed' ||
375
- (!record.upload_status && fileIndex && fileIndex.files && fileIndex.files.length > 0);
376
- if (isCompleted) {
377
- const rootFile = fileIndex?.root_file || 'tileset.json';
378
- return {
379
- status: 200,
380
- content: JSON.stringify({
381
- id: record.id,
382
- status: 'completed',
383
- tileset_url: `/${config.endpoint}/${record.id}/files/${rootFile}`,
384
- file_count: fileIndex?.files?.length || 0
385
- }),
386
- headers: { 'Content-Type': 'application/json' }
387
- };
388
- }
389
- // If no upload_status and no file_index, it's a broken record
390
- if (!record.upload_status) {
391
- return {
392
- status: 200,
393
- content: JSON.stringify({
394
- id: record.id,
395
- status: 'failed',
396
- error: 'Upload incomplete - no files found'
397
- }),
398
- headers: { 'Content-Type': 'application/json' }
399
- };
400
- }
401
- // Return current status (pending, processing, or failed)
402
- return {
403
- status: 200,
404
- content: JSON.stringify({
270
+ if (record.upload_status === 'completed') {
271
+ return successResponse({
405
272
  id: record.id,
406
- status: record.upload_status,
407
- job_id: record.upload_job_id,
408
- error: record.upload_error || undefined
409
- }),
410
- headers: { 'Content-Type': 'application/json' }
411
- };
412
- }
413
- catch (error) {
414
- return {
415
- status: 500,
416
- content: JSON.stringify({
417
- error: error instanceof Error ? error.message : 'Unknown error'
418
- }),
419
- headers: { 'Content-Type': 'application/json' }
420
- };
421
- }
422
- }
423
- /**
424
- * Serve an extracted file from the tileset.
425
- *
426
- * This endpoint allows Cesium to load tileset.json and all referenced files
427
- * (tiles, textures, etc.) using relative paths.
428
- *
429
- * @param req - HTTP request with params.id and params['0'] (file path)
430
- * @returns DataResponse with file content
431
- */
432
- async handleGetFile(req) {
433
- try {
434
- const { id } = req.params || {};
435
- // Express/ultimate-express captures wildcard path in different ways:
436
- // - Express 4.x: params[0] or params['0']
437
- // - Express 5.x / ultimate-express: params['*'] or via URL parsing
438
- let filePath = req.params[0] || req.params['0'] || req.params['*'] || req.params.path || req.params['*0'];
439
- // Fallback: extract from URL if params didn't capture it
440
- if (!filePath && req.url) {
441
- const match = req.url.match(/\/files\/(.+)$/);
442
- if (match) {
443
- filePath = decodeURIComponent(match[1]);
444
- }
445
- }
446
- // Another fallback: use originalUrl
447
- if (!filePath && req.originalUrl) {
448
- const match = req.originalUrl.match(/\/files\/(.+)$/);
449
- if (match) {
450
- filePath = decodeURIComponent(match[1]);
451
- }
452
- }
453
- if (!id) {
454
- return {
455
- status: 400,
456
- content: JSON.stringify({ error: 'Asset ID is required' }),
457
- headers: { 'Content-Type': 'application/json' }
458
- };
459
- }
460
- if (!filePath) {
461
- return {
462
- status: 400,
463
- content: JSON.stringify({ error: 'File path is required' }),
464
- headers: { 'Content-Type': 'application/json' }
465
- };
273
+ status: 'completed',
274
+ tileset_url: record.tileset_url
275
+ });
466
276
  }
467
- // Get asset metadata
468
- const asset = await this.getAssetById(id);
469
- if (!asset) {
470
- return {
471
- status: 404,
472
- content: JSON.stringify({ error: 'Tileset not found' }),
473
- headers: { 'Content-Type': 'application/json' }
474
- };
475
- }
476
- // Verify this asset belongs to our component
477
- const config = this.getConfiguration();
478
- if (asset.name !== config.name) {
479
- return {
480
- status: 404,
481
- content: JSON.stringify({ error: 'Tileset not found' }),
482
- headers: { 'Content-Type': 'application/json' }
483
- };
484
- }
485
- // Check access permissions for private assets
486
- if (!asset.is_public) {
487
- if (ApisixAuthParser.isAdmin(req.headers || {})) {
488
- // Admin can access everything
489
- }
490
- else if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
491
- return {
492
- status: 401,
493
- content: JSON.stringify({ error: 'Authentication required for private assets' }),
494
- headers: { 'Content-Type': 'application/json' }
495
- };
496
- }
497
- else {
498
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
499
- if (authUser) {
500
- const userRecord = await this.userService.findOrCreateUser(authUser);
501
- if (!userRecord.id || asset.owner_id !== userRecord.id) {
502
- return {
503
- status: 403,
504
- content: JSON.stringify({ error: 'This asset is private' }),
505
- headers: { 'Content-Type': 'application/json' }
506
- };
507
- }
508
- }
509
- }
510
- }
511
- // Build storage path using asset.url as base path
512
- // asset.url contains the base path (e.g., 'tilesets_manager/1733064000000')
513
- const storagePath = `${asset.url}/${filePath}`;
514
- // Retrieve file from storage
515
- let fileContent;
516
- try {
517
- fileContent = await this.storage.retrieve(storagePath);
518
- }
519
- catch {
520
- return {
521
- status: 404,
522
- content: JSON.stringify({ error: 'File not found' }),
523
- headers: { 'Content-Type': 'application/json' }
524
- };
525
- }
526
- // Determine content type based on file extension
527
- const contentType = this.getContentTypeForFile(filePath);
528
- return {
529
- status: 200,
530
- content: fileContent,
531
- headers: {
532
- 'Content-Type': contentType,
533
- 'Access-Control-Allow-Origin': '*'
534
- }
535
- };
277
+ if (record.upload_status === 'failed') {
278
+ return successResponse({
279
+ id: record.id,
280
+ status: 'failed',
281
+ error: record.upload_error || 'Upload failed'
282
+ });
283
+ }
284
+ return successResponse({
285
+ id: record.id,
286
+ status: record.upload_status || 'unknown',
287
+ job_id: record.upload_job_id
288
+ });
536
289
  }
537
290
  catch (error) {
538
- return {
539
- status: 500,
540
- content: JSON.stringify({
541
- error: error instanceof Error ? error.message : 'Unknown error'
542
- }),
543
- headers: { 'Content-Type': 'application/json' }
544
- };
291
+ return errorResponse(error);
545
292
  }
546
293
  }
547
294
  /**
548
- * Determines the MIME content type based on file extension.
549
- */
550
- getContentTypeForFile(filePath) {
551
- const ext = filePath.toLowerCase().split('.').pop();
552
- const contentTypes = {
553
- json: 'application/json',
554
- b3dm: 'application/octet-stream',
555
- i3dm: 'application/octet-stream',
556
- pnts: 'application/octet-stream',
557
- cmpt: 'application/octet-stream',
558
- glb: 'model/gltf-binary',
559
- gltf: 'model/gltf+json',
560
- bin: 'application/octet-stream',
561
- png: 'image/png',
562
- jpg: 'image/jpeg',
563
- jpeg: 'image/jpeg',
564
- webp: 'image/webp',
565
- ktx2: 'image/ktx2'
566
- };
567
- return contentTypes[ext || ''] || 'application/octet-stream';
568
- }
569
- /**
570
- * Override retrieve to include tileset.json URL in the response.
295
+ * List all tilesets with their public URLs.
571
296
  */
572
297
  async retrieve(req) {
573
298
  try {
574
299
  const assets = await this.getAllAssets();
575
- const config = this.getConfiguration();
576
300
  const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
577
301
  // Get authenticated user ID if available
578
302
  let authenticatedUserId = null;
@@ -587,167 +311,114 @@ export class TilesetManager extends AssetsManager {
587
311
  const visibleAssets = isAdmin
588
312
  ? assets
589
313
  : assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
590
- // Transform to include tileset URLs
591
- const assetsWithUrls = visibleAssets.map(asset => {
592
- const fileIndex = asset.file_index;
593
- const rootFile = fileIndex?.root_file || 'tileset.json';
594
- return {
595
- id: asset.id,
596
- name: asset.name,
597
- date: asset.date,
598
- contentType: asset.contentType,
599
- description: asset.description || '',
600
- source: asset.source || '',
601
- owner_id: asset.owner_id || null,
602
- filename: asset.filename || '',
603
- is_public: asset.is_public ?? true,
604
- // Tileset-specific URLs
605
- file_count: fileIndex?.files?.length || 0,
606
- root_file: rootFile,
607
- tileset_url: `/${config.endpoint}/${asset.id}/files/${rootFile}`,
608
- files_base_url: `/${config.endpoint}/${asset.id}/files`
609
- };
610
- });
611
- return {
612
- status: 200,
613
- content: JSON.stringify(assetsWithUrls),
614
- headers: { 'Content-Type': 'application/json' }
615
- };
314
+ // Transform to response format
315
+ const response = visibleAssets.map(asset => ({
316
+ id: asset.id,
317
+ description: asset.description || '',
318
+ filename: asset.filename || '',
319
+ date: asset.date,
320
+ owner_id: asset.owner_id || null,
321
+ is_public: asset.is_public ?? true,
322
+ tileset_url: asset.tileset_url || '',
323
+ upload_status: asset.upload_status || 'completed'
324
+ }));
325
+ return successResponse(response);
616
326
  }
617
327
  catch (error) {
618
- return {
619
- status: 500,
620
- content: JSON.stringify({
621
- error: error instanceof Error ? error.message : 'Unknown error'
622
- }),
623
- headers: { 'Content-Type': 'application/json' }
624
- };
328
+ return errorResponse(error);
625
329
  }
626
330
  }
627
331
  /**
628
- * Override delete to clean up all extracted files.
332
+ * Delete tileset and all files from storage.
629
333
  */
630
334
  async handleDelete(req) {
631
335
  try {
632
- // Check authentication (skip if auth is disabled)
633
- let userId;
634
- if (AuthConfig.isAuthDisabled()) {
635
- const anonymousUser = {
636
- id: AuthConfig.getAnonymousUserId(),
637
- roles: []
638
- };
639
- const userRecord = await this.userService.findOrCreateUser(anonymousUser);
640
- userId = userRecord.id;
641
- }
642
- else {
643
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
644
- return {
645
- status: 401,
646
- content: JSON.stringify({ error: 'Authentication required' }),
647
- headers: { 'Content-Type': 'application/json' }
648
- };
649
- }
650
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
651
- if (!authUser) {
652
- return {
653
- status: 401,
654
- content: JSON.stringify({ error: 'Invalid authentication headers' }),
655
- headers: { 'Content-Type': 'application/json' }
656
- };
657
- }
658
- const userRecord = await this.userService.findOrCreateUser(authUser);
659
- if (!userRecord.id) {
660
- return {
661
- status: 500,
662
- content: JSON.stringify({ error: 'Failed to retrieve user information' }),
663
- headers: { 'Content-Type': 'application/json' }
664
- };
665
- }
666
- userId = userRecord.id;
336
+ // Authenticate user
337
+ const userId = await this.authenticateUser(req);
338
+ if (typeof userId !== 'number') {
339
+ return userId;
667
340
  }
668
341
  const { id } = req.params || {};
669
342
  if (!id) {
670
- return {
671
- status: 400,
672
- content: JSON.stringify({ error: 'Asset ID is required' }),
673
- headers: { 'Content-Type': 'application/json' }
674
- };
343
+ return badRequestResponse('Asset ID is required');
675
344
  }
676
- // Get asset metadata
677
345
  const asset = await this.getAssetById(id);
678
346
  if (!asset) {
679
- return {
680
- status: 404,
681
- content: JSON.stringify({ error: 'Tileset not found' }),
682
- headers: { 'Content-Type': 'application/json' }
683
- };
347
+ return notFoundResponse('Tileset not found');
684
348
  }
685
- // Check ownership (admins can delete any asset)
349
+ // Check ownership (admins can delete any)
686
350
  const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
687
351
  if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
352
+ return forbiddenResponse('You can only delete your own assets');
353
+ }
354
+ // Block deletion while upload in progress
355
+ if (asset.upload_status === 'pending' || asset.upload_status === 'processing') {
688
356
  return {
689
- status: 403,
690
- content: JSON.stringify({ error: 'You can only delete your own assets' }),
357
+ status: 409,
358
+ content: JSON.stringify({ error: 'Cannot delete tileset while upload is in progress' }),
691
359
  headers: { 'Content-Type': 'application/json' }
692
360
  };
693
361
  }
694
- // Delete all extracted files from storage using batch delete for performance
695
- const fileIndex = asset.file_index;
696
- if (fileIndex?.files && fileIndex.files.length > 0) {
697
- const filePaths = fileIndex.files.map(file => file.path);
698
- await this.storage.deleteBatch(filePaths);
362
+ // Delete all files from storage
363
+ // Support both new format (url = basePath) and legacy format (file_index.files)
364
+ const legacyFileIndex = asset.file_index;
365
+ if (legacyFileIndex?.files && legacyFileIndex.files.length > 0) {
366
+ // Legacy format: delete individual files from file_index
367
+ console.log(`[TilesetManager] Deleting ${legacyFileIndex.files.length} files (legacy format)`);
368
+ for (const file of legacyFileIndex.files) {
369
+ await this.storage.delete(file.path).catch(() => {
370
+ // Ignore individual file deletion errors
371
+ });
372
+ }
373
+ }
374
+ else if (asset.url) {
375
+ // New format: url contains basePath, use deleteByPrefix
376
+ const deletedCount = await this.storage.deleteByPrefix(asset.url);
377
+ console.log(`[TilesetManager] Deleted ${deletedCount} files from ${asset.url}`);
699
378
  }
700
379
  // Delete database record
701
380
  await this.deleteAssetById(id);
702
- return {
703
- status: 200,
704
- content: JSON.stringify({ message: 'Tileset deleted successfully' }),
705
- headers: { 'Content-Type': 'application/json' }
706
- };
381
+ return successResponse({ message: 'Tileset deleted successfully' });
707
382
  }
708
383
  catch (error) {
709
- return {
710
- status: 500,
711
- content: JSON.stringify({
712
- error: error instanceof Error ? error.message : 'Unknown error'
713
- }),
714
- headers: { 'Content-Type': 'application/json' }
715
- };
384
+ return errorResponse(error);
716
385
  }
717
386
  }
718
387
  /**
719
- * Override getEndpoints to add the file serving endpoint.
388
+ * Get HTTP endpoints for this manager.
720
389
  */
721
390
  getEndpoints() {
722
391
  const config = this.getConfiguration();
723
- // Get base endpoints from parent but filter out ones we override
724
- const baseEndpoints = super.getEndpoints().filter(ep =>
725
- // Keep all except GET /:id (we don't serve the raw asset, only files)
726
- // and DELETE /:id (we override to clean up files)
727
- !(ep.method === 'get' && ep.path === `/${config.endpoint}/:id`) &&
728
- !(ep.method === 'delete' && ep.path === `/${config.endpoint}/:id`) &&
729
- // Also remove download endpoint - not applicable for extracted tilesets
730
- !(ep.method === 'get' && ep.path === `/${config.endpoint}/:id/download`));
731
- // IMPORTANT: More specific routes must come BEFORE less specific ones
732
- // /:id/files/* and /:id/status must be registered before /:id to avoid incorrect matching
733
392
  return [
734
- // Status endpoint FIRST (most specific)
393
+ // Status endpoint (for async upload polling)
735
394
  {
736
395
  method: 'get',
737
396
  path: `/${config.endpoint}/:id/status`,
738
397
  handler: this.handleGetStatus.bind(this),
739
398
  responseType: 'application/json'
740
399
  },
741
- // File serving endpoint (also specific)
400
+ // List tilesets
742
401
  {
743
402
  method: 'get',
744
- path: `/${config.endpoint}/:id/files/*`,
745
- handler: this.handleGetFile.bind(this),
746
- responseType: 'application/octet-stream'
403
+ path: `/${config.endpoint}`,
404
+ handler: this.retrieve.bind(this),
405
+ responseType: 'application/json'
406
+ },
407
+ // Upload tileset
408
+ {
409
+ method: 'post',
410
+ path: `/${config.endpoint}`,
411
+ handler: this.handleUpload.bind(this),
412
+ responseType: 'application/json'
413
+ },
414
+ // Update metadata
415
+ {
416
+ method: 'put',
417
+ path: `/${config.endpoint}/:id`,
418
+ handler: this.handleUpdate.bind(this),
419
+ responseType: 'application/json'
747
420
  },
748
- // Then base endpoints (list, create, update, etc.)
749
- ...baseEndpoints,
750
- // Override delete to clean up files (after base to override)
421
+ // Delete tileset
751
422
  {
752
423
  method: 'delete',
753
424
  path: `/${config.endpoint}/:id`,
@@ -757,99 +428,191 @@ export class TilesetManager extends AssetsManager {
757
428
  ];
758
429
  }
759
430
  /**
760
- * Override OpenAPI specification to include tileset-specific endpoints.
431
+ * Generate OpenAPI specification.
761
432
  */
762
433
  getOpenAPISpec() {
763
- const parentSpec = super.getOpenAPISpec();
764
434
  const config = this.getConfiguration();
765
435
  const basePath = `/${config.endpoint}`;
766
436
  const tagName = config.tags?.[0] || config.name;
767
- // Update POST response
768
- if (parentSpec.paths[basePath]?.post?.responses?.['200']) {
769
- parentSpec.paths[basePath].post.responses['200'] = {
770
- description: 'Tileset uploaded and extracted successfully',
771
- content: {
772
- 'application/json': {
773
- schema: {
774
- type: 'object',
775
- properties: {
776
- message: { type: 'string' },
777
- file_count: { type: 'integer', description: 'Number of files extracted' },
778
- root_file: { type: 'string', description: 'Root tileset.json path' },
779
- tileset_url: { type: 'string', description: 'URL to load in Cesium' }
437
+ return {
438
+ paths: {
439
+ [basePath]: {
440
+ get: {
441
+ summary: 'List all tilesets',
442
+ description: 'Returns all tilesets with their public URLs for Cesium loading',
443
+ tags: [tagName],
444
+ responses: {
445
+ '200': {
446
+ description: 'List of tilesets',
447
+ content: {
448
+ 'application/json': {
449
+ schema: {
450
+ type: 'array',
451
+ items: { $ref: '#/components/schemas/TilesetResponse' }
452
+ }
453
+ }
454
+ }
780
455
  }
781
456
  }
782
- }
783
- }
784
- };
785
- }
786
- // Add file serving endpoint
787
- parentSpec.paths[`${basePath}/{id}/files/{filePath}`] = {
788
- get: {
789
- summary: 'Get tileset file',
790
- description: 'Retrieve an extracted file from the tileset (tileset.json, .b3dm, etc.)',
791
- tags: [tagName],
792
- parameters: [
793
- {
794
- name: 'id',
795
- in: 'path',
796
- required: true,
797
- schema: { type: 'string' },
798
- description: 'Tileset ID'
799
457
  },
800
- {
801
- name: 'filePath',
802
- in: 'path',
803
- required: true,
804
- schema: { type: 'string' },
805
- description: 'Path to file within tileset (e.g., tileset.json, tiles/tile_0.b3dm)'
458
+ post: {
459
+ summary: 'Upload a tileset',
460
+ description: 'Upload a ZIP file containing a 3D Tiles tileset. Files < 50MB are processed synchronously, larger files are queued.',
461
+ tags: [tagName],
462
+ security: [{ ApiKeyAuth: [] }],
463
+ requestBody: {
464
+ required: true,
465
+ content: {
466
+ 'multipart/form-data': {
467
+ schema: {
468
+ type: 'object',
469
+ required: ['file', 'description'],
470
+ properties: {
471
+ file: {
472
+ type: 'string',
473
+ format: 'binary',
474
+ description: 'ZIP file containing tileset'
475
+ },
476
+ description: { type: 'string', description: 'Tileset description' },
477
+ is_public: {
478
+ type: 'boolean',
479
+ description: 'Whether tileset is public (default: true)'
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
485
+ },
486
+ responses: {
487
+ '200': {
488
+ description: 'Tileset uploaded successfully (sync)',
489
+ content: {
490
+ 'application/json': {
491
+ schema: {
492
+ type: 'object',
493
+ properties: {
494
+ message: { type: 'string' },
495
+ id: { type: 'integer' },
496
+ tileset_url: {
497
+ type: 'string',
498
+ description: 'Public URL to load in Cesium'
499
+ },
500
+ file_count: { type: 'integer' }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ },
506
+ '202': {
507
+ description: 'Upload accepted for async processing',
508
+ content: {
509
+ 'application/json': {
510
+ schema: {
511
+ type: 'object',
512
+ properties: {
513
+ message: { type: 'string' },
514
+ id: { type: 'integer' },
515
+ status: { type: 'string' },
516
+ status_url: { type: 'string' }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ },
522
+ '400': { description: 'Bad request - missing fields or invalid file' },
523
+ '401': { description: 'Unauthorized' }
524
+ }
525
+ }
526
+ },
527
+ [`${basePath}/{id}/status`]: {
528
+ get: {
529
+ summary: 'Get upload status',
530
+ description: 'Poll the status of an async upload',
531
+ tags: [tagName],
532
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
533
+ responses: {
534
+ '200': {
535
+ description: 'Upload status',
536
+ content: {
537
+ 'application/json': {
538
+ schema: {
539
+ type: 'object',
540
+ properties: {
541
+ id: { type: 'integer' },
542
+ status: {
543
+ type: 'string',
544
+ enum: ['pending', 'processing', 'completed', 'failed']
545
+ },
546
+ tileset_url: { type: 'string' },
547
+ error: { type: 'string' }
548
+ }
549
+ }
550
+ }
551
+ }
552
+ },
553
+ '404': { description: 'Tileset not found' }
554
+ }
806
555
  }
807
- ],
808
- responses: {
809
- '200': {
810
- description: 'File content',
811
- content: {
812
- 'application/octet-stream': {
813
- schema: { type: 'string', format: 'binary' }
556
+ },
557
+ [`${basePath}/{id}`]: {
558
+ put: {
559
+ summary: 'Update tileset metadata',
560
+ tags: [tagName],
561
+ security: [{ ApiKeyAuth: [] }],
562
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
563
+ requestBody: {
564
+ content: {
565
+ 'application/json': {
566
+ schema: {
567
+ type: 'object',
568
+ properties: {
569
+ description: { type: 'string' },
570
+ is_public: { type: 'boolean' }
571
+ }
572
+ }
573
+ }
814
574
  }
575
+ },
576
+ responses: {
577
+ '200': { description: 'Updated successfully' },
578
+ '401': { description: 'Unauthorized' },
579
+ '403': { description: 'Forbidden' },
580
+ '404': { description: 'Not found' }
815
581
  }
816
582
  },
817
- '404': { description: 'Tileset or file not found' }
583
+ delete: {
584
+ summary: 'Delete tileset',
585
+ description: 'Delete tileset and all files from storage',
586
+ tags: [tagName],
587
+ security: [{ ApiKeyAuth: [] }],
588
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
589
+ responses: {
590
+ '200': { description: 'Deleted successfully' },
591
+ '401': { description: 'Unauthorized' },
592
+ '403': { description: 'Forbidden' },
593
+ '404': { description: 'Not found' },
594
+ '409': { description: 'Upload in progress' }
595
+ }
596
+ }
597
+ }
598
+ },
599
+ tags: [{ name: tagName, description: config.description }],
600
+ schemas: {
601
+ TilesetResponse: {
602
+ type: 'object',
603
+ properties: {
604
+ id: { type: 'integer' },
605
+ description: { type: 'string' },
606
+ filename: { type: 'string' },
607
+ date: { type: 'string', format: 'date-time' },
608
+ owner_id: { type: 'integer', nullable: true },
609
+ is_public: { type: 'boolean' },
610
+ tileset_url: { type: 'string', description: 'Public URL to load in Cesium' },
611
+ upload_status: { type: 'string', enum: ['pending', 'processing', 'completed', 'failed'] }
612
+ }
818
613
  }
819
614
  }
820
615
  };
821
- // Update GET list response schema
822
- if (parentSpec.schemas) {
823
- parentSpec.schemas.TilesetResponse = {
824
- type: 'object',
825
- properties: {
826
- id: { type: 'integer' },
827
- name: { type: 'string' },
828
- date: { type: 'string', format: 'date-time' },
829
- contentType: { type: 'string' },
830
- description: { type: 'string' },
831
- source: { type: 'string' },
832
- owner_id: { type: 'integer', nullable: true },
833
- filename: { type: 'string' },
834
- is_public: { type: 'boolean' },
835
- file_count: { type: 'integer', description: 'Number of files in tileset' },
836
- root_file: { type: 'string', description: 'Root tileset.json filename' },
837
- tileset_url: { type: 'string', description: 'URL to load tileset in Cesium' },
838
- files_base_url: { type: 'string', description: 'Base URL for all tileset files' }
839
- }
840
- };
841
- }
842
- if (parentSpec.paths[basePath]?.get?.responses?.['200']?.content?.['application/json']) {
843
- parentSpec.paths[basePath].get.responses['200'].content['application/json'].schema = {
844
- type: 'array',
845
- items: { $ref: '#/components/schemas/TilesetResponse' }
846
- };
847
- }
848
- // Remove the single asset GET endpoint since we don't serve raw assets
849
- delete parentSpec.paths[`${basePath}/{id}`]?.get;
850
- // Remove download endpoint
851
- delete parentSpec.paths[`${basePath}/{id}/download`];
852
- return parentSpec;
853
616
  }
854
617
  }
855
618
  //# sourceMappingURL=tileset_manager.js.map