cyberia 3.0.1 → 3.0.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 (49) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +1 -0
  2. package/CHANGELOG.md +56 -1
  3. package/CLI-HELP.md +2 -4
  4. package/README.md +139 -0
  5. package/bin/build.js +5 -0
  6. package/bin/cyberia.js +385 -71
  7. package/bin/deploy.js +18 -26
  8. package/bin/file.js +3 -0
  9. package/bin/index.js +385 -71
  10. package/conf.js +32 -3
  11. package/deployment.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  13. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  14. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  15. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  16. package/manifests/ipfs/configmap.yaml +7 -0
  17. package/package.json +8 -8
  18. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
  19. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
  20. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
  21. package/src/api/file/file.controller.js +3 -13
  22. package/src/api/file/file.ref.json +0 -21
  23. package/src/api/ipfs/ipfs.controller.js +104 -0
  24. package/src/api/ipfs/ipfs.model.js +71 -0
  25. package/src/api/ipfs/ipfs.router.js +31 -0
  26. package/src/api/ipfs/ipfs.service.js +193 -0
  27. package/src/api/object-layer/README.md +139 -0
  28. package/src/api/object-layer/object-layer.controller.js +3 -0
  29. package/src/api/object-layer/object-layer.model.js +15 -1
  30. package/src/api/object-layer/object-layer.router.js +6 -10
  31. package/src/api/object-layer/object-layer.service.js +311 -182
  32. package/src/cli/cluster.js +30 -38
  33. package/src/cli/index.js +0 -1
  34. package/src/cli/run.js +14 -0
  35. package/src/client/components/core/LoadingAnimation.js +2 -3
  36. package/src/client/components/core/Modal.js +1 -1
  37. package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
  38. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
  39. package/src/client/services/ipfs/ipfs.service.js +144 -0
  40. package/src/client/services/object-layer/object-layer.management.js +161 -8
  41. package/src/index.js +1 -1
  42. package/src/runtime/express/Express.js +1 -1
  43. package/src/server/auth.js +18 -18
  44. package/src/server/ipfs-client.js +433 -0
  45. package/src/server/object-layer.js +649 -18
  46. package/src/server/semantic-layer-generator.js +1083 -0
  47. package/src/server/shape-generator.js +952 -0
  48. package/test/shape-generator.test.js +457 -0
  49. package/bin/ssl.js +0 -63
@@ -1,18 +1,64 @@
1
+ /**
2
+ * Object Layer service module for handling CRUD operations on object layer assets.
3
+ * Provides REST API handlers for creating, reading, updating, and deleting object layers
4
+ * including frame image management, metadata processing, and WebP animation generation.
5
+ *
6
+ * Delegates shared object layer creation and update logic to {@link ObjectLayerEngine} in
7
+ * `src/server/object-layer.js` to keep a single source of truth shared with the Cyberia CLI.
8
+ *
9
+ * @module src/api/object-layer/object-layer.service.js
10
+ * @namespace CyberiaObjectLayerService
11
+ */
12
+
1
13
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
2
14
  import { loggerFactory } from '../../server/logger.js';
15
+ import { ObjectLayerRenderFramesDto } from '../object-layer-render-frames/object-layer-render-frames.model.js';
3
16
  import { FileFactory } from '../file/file.service.js';
4
17
  import fs from 'fs-extra';
5
- import crypto from 'crypto';
6
- import stringify from 'fast-json-stable-stringify';
7
18
  import { ObjectLayerDto } from './object-layer.model.js';
8
19
  import { ObjectLayerEngine } from '../../server/object-layer.js';
9
20
  import { shellExec } from '../../server/process.js';
10
21
  import { DataQuery } from '../../server/data-query.js';
11
22
  import { AtlasSpriteSheetService } from '../atlas-sprite-sheet/atlas-sprite-sheet.service.js';
12
-
23
+ import { IpfsClient } from '../../server/ipfs-client.js';
24
+ import { createPinRecord, removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
25
+
26
+ /**
27
+ * Logger instance for this module.
28
+ * @type {Function}
29
+ * @memberof CyberiaObjectLayerService
30
+ * @private
31
+ */
13
32
  const logger = loggerFactory(import.meta);
14
33
 
34
+ /**
35
+ * Object Layer Service providing REST API handlers for object layer operations.
36
+ * @namespace CyberiaObjectLayerService.ObjectLayerService
37
+ * @memberof CyberiaObjectLayerService
38
+ */
15
39
  const ObjectLayerService = {
40
+ /**
41
+ * POST handler for creating object layers and uploading frame images.
42
+ *
43
+ * Supports three sub-routes:
44
+ * - `/frame-image/:itemType/:itemId/:directionCode` — Upload PNG frame images for a direction.
45
+ * - `/metadata/:itemType/:itemId` — Create an object layer from uploaded frames and metadata.
46
+ * - Default — Create an object layer directly from the request body.
47
+ *
48
+ * The `/metadata` and default routes delegate to {@link ObjectLayerEngine.createObjectLayerDocuments}
49
+ * for centralized document creation, atlas generation, SHA-256 computation, and IPFS pinning.
50
+ *
51
+ * @async
52
+ * @function post
53
+ * @memberof CyberiaObjectLayerService.ObjectLayerService
54
+ * @param {Object} req - Express request object.
55
+ * @param {Object} res - Express response object.
56
+ * @param {Object} options - Server options containing host and path.
57
+ * @param {string} options.host - The deployment host.
58
+ * @param {string} options.path - The deployment path.
59
+ * @returns {Promise<Object>} The created object layer document or frame upload result.
60
+ * @throws {Error} If file validation fails or required parameters are missing.
61
+ */
16
62
  post: async (req, res, options) => {
17
63
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
18
64
 
@@ -88,8 +134,10 @@ const ObjectLayerService = {
88
134
  }
89
135
 
90
136
  if (req.path.startsWith('/metadata')) {
91
- const folder = `./src/client/public/cyberia/assets/${req.params.itemType}/${req.params.itemId}`;
92
- const publicFolder = `./public/${options.host}${options.path}/assets/${req.params.itemType}/${req.params.itemId}`;
137
+ const itemType = req.params.itemType;
138
+ const itemId = req.params.itemId;
139
+ const folder = `./src/client/public/cyberia/assets/${itemType}/${itemId}`;
140
+ const publicFolder = `./public/${options.host}${options.path}/assets/${itemType}/${itemId}`;
93
141
 
94
142
  // Ensure both folders exist
95
143
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
@@ -100,113 +148,90 @@ const ObjectLayerService = {
100
148
  fs.writeFileSync(`${folder}/metadata.json`, metadataContent);
101
149
  fs.writeFileSync(`${publicFolder}/metadata.json`, metadataContent);
102
150
 
103
- // Create object layer from PNG saved and metadata
151
+ // Build object layer data from the asset directory using centralized logic
104
152
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
105
153
  const ObjectLayerRenderFrames =
106
154
  DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
107
155
 
108
- const objectLayerRenderFramesData = {
109
- frame_duration: req.body.objectLayerRenderFramesData.frame_duration,
110
- is_stateless: req.body.objectLayerRenderFramesData.is_stateless,
111
- frames: {},
112
- colors: [],
113
- };
114
-
115
- const objectLayerData = {
116
- data: {
117
- item: req.body.data.item,
118
- stats: req.body.data.stats,
119
- seed: crypto.randomUUID(),
120
- },
121
- };
122
-
123
- // Process all PNG files to generate frames and colors
124
- const directionFolders = await fs.readdir(folder);
125
- for (const directionCode of directionFolders) {
126
- const directionPath = `${folder}/${directionCode}`;
127
- if (!fs.statSync(directionPath).isDirectory()) continue;
128
-
129
- const frameFiles = await fs.readdir(directionPath);
130
- // Sort frame files numerically
131
- frameFiles.sort((a, b) => {
132
- const numA = parseInt(a.split('.')[0]);
133
- const numB = parseInt(b.split('.')[0]);
134
- return numA - numB;
156
+ const { objectLayerRenderFramesData, objectLayerData } =
157
+ await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
158
+ folder,
159
+ objectLayerType: itemType,
160
+ objectLayerId: itemId,
161
+ metadataOverride: req.body,
135
162
  });
136
163
 
137
- for (const frameFile of frameFiles) {
138
- if (!frameFile.endsWith('.png')) continue;
139
-
140
- const framePath = `${directionPath}/${frameFile}`;
141
-
142
- // Process image and push frame to render data with color palette management
143
- await ObjectLayerEngine.processAndPushFrame(objectLayerRenderFramesData, framePath, directionCode);
144
- }
145
- }
146
-
147
- // Create ObjectLayerRenderFrames document
148
- const objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
149
-
150
- // Add reference to render frames (top-level, not in data)
151
- objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
152
-
153
- // Generate SHA256 hash using fast-json-stable-stringify (seed is part of data)
154
- objectLayerData.sha256 = crypto.createHash('sha256').update(stringify(objectLayerData.data)).digest('hex');
155
-
156
- // Save to MongoDB
157
- try {
158
- const existingObjectLayer = await ObjectLayer.findOne({ sha256: objectLayerData.sha256 });
159
- if (existingObjectLayer) {
160
- logger.info(`ObjectLayer with sha256 ${objectLayerData.sha256} already exists, updating...`);
161
- const updated = await ObjectLayer.findByIdAndUpdate(existingObjectLayer._id, objectLayerData, {
162
- new: true,
163
- }).populate('objectLayerRenderFramesId');
164
-
165
- // Update atlas sprite sheet
166
- try {
167
- await AtlasSpriteSheetService.generate({ params: { id: updated._id }, objectLayer: updated }, res, options);
168
- } catch (atlasError) {
169
- logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
170
- }
171
-
172
- return updated;
173
- }
174
- const newObjectLayer = await (await ObjectLayer.create(objectLayerData)).populate('objectLayerRenderFramesId');
175
- logger.info(`ObjectLayer created successfully with id: ${newObjectLayer._id}`);
176
-
177
- // Generate atlas sprite sheet
178
- try {
179
- await AtlasSpriteSheetService.generate(
180
- { params: { id: newObjectLayer._id }, objectLayer: newObjectLayer },
164
+ // Create documents using centralized engine method (with atlas generation)
165
+ const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
166
+ ObjectLayer,
167
+ ObjectLayerRenderFrames,
168
+ objectLayerRenderFramesData,
169
+ objectLayerData,
170
+ createOptions: {
171
+ generateAtlas: true,
172
+ atlasServiceContext: {
173
+ req,
181
174
  res,
182
175
  options,
183
- );
184
- } catch (atlasError) {
185
- logger.error('Failed to auto-generate atlas for new ObjectLayer:', atlasError);
186
- }
176
+ AtlasSpriteSheetService,
177
+ IpfsClient,
178
+ createPinRecord,
179
+ },
180
+ },
181
+ });
187
182
 
188
- return newObjectLayer;
189
- } catch (error) {
190
- logger.error('Error creating ObjectLayer:', error);
191
- throw error;
192
- }
183
+ return objectLayer;
193
184
  }
194
185
 
195
186
  // create object layer from body
196
187
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
197
188
  const ObjectLayerRenderFrames =
198
189
  DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
199
- const newObjectLayer = await new ObjectLayer(req.body).save();
190
+ let newObjectLayer = await new ObjectLayer(req.body).save();
200
191
 
201
- // Generate atlas sprite sheet
192
+ // Generate atlas sprite sheet – this sets data.atlasSpriteSheetCid and saves
202
193
  try {
203
- await AtlasSpriteSheetService.generate({ params: { id: newObjectLayer._id } }, res, options);
194
+ await AtlasSpriteSheetService.generate({ params: { id: newObjectLayer._id }, auth: req.auth }, res, options);
204
195
  } catch (atlasError) {
205
196
  logger.error('Failed to auto-generate atlas for new ObjectLayer:', atlasError);
206
197
  }
207
198
 
199
+ // Re-read so data.atlasSpriteSheetCid is up-to-date, then recompute SHA-256 & IPFS CID
200
+ newObjectLayer = await ObjectLayer.findById(newObjectLayer._id).populate('objectLayerRenderFramesId');
201
+ if (newObjectLayer) {
202
+ newObjectLayer = await ObjectLayerEngine.computeAndSaveFinalSha256({
203
+ objectLayer: newObjectLayer,
204
+ ipfsClient: IpfsClient,
205
+ createPinRecord,
206
+ userId: req.auth && req.auth.user ? req.auth.user._id : undefined,
207
+ options,
208
+ });
209
+ }
210
+
208
211
  return newObjectLayer;
209
212
  },
213
+
214
+ /**
215
+ * GET handler for retrieving object layers.
216
+ *
217
+ * Supports multiple sub-routes:
218
+ * - `/frame-counts/:id` — Get frame counts for each direction using numeric codes.
219
+ * - `/render/:id` — Get only render data (populated ObjectLayerRenderFrames) for a specific object layer.
220
+ * - `/metadata/:id` — Get metadata (no full render frames/colors) with atlas sprite sheet validation.
221
+ * - `/:id` — Get a single object layer by ID.
222
+ * - `/` — Get a paginated list of object layers.
223
+ *
224
+ * @async
225
+ * @function get
226
+ * @memberof CyberiaObjectLayerService.ObjectLayerService
227
+ * @param {Object} req - Express request object.
228
+ * @param {Object} res - Express response object.
229
+ * @param {Object} options - Server options containing host and path.
230
+ * @param {string} options.host - The deployment host.
231
+ * @param {string} options.path - The deployment path.
232
+ * @returns {Promise<Object>} The requested object layer data, list, or frame counts.
233
+ * @throws {Error} If the requested object layer is not found.
234
+ */
210
235
  get: async (req, res, options) => {
211
236
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
212
237
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
@@ -271,7 +296,9 @@ const ObjectLayerService = {
271
296
 
272
297
  // GET /metadata/:id - Get only metadata (no render frames/colors) for specific object layer
273
298
  if (req.path.startsWith('/metadata/')) {
274
- const objectLayer = await ObjectLayer.findById(req.params.id).select(ObjectLayerDto.select.getMetadata());
299
+ const objectLayer = await ObjectLayer.findById(req.params.id)
300
+ .select(ObjectLayerDto.select.getMetadata())
301
+ .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
275
302
  if (!objectLayer) {
276
303
  throw new Error('ObjectLayer not found');
277
304
  }
@@ -326,7 +353,10 @@ const ObjectLayerService = {
326
353
  logger.info(`ObjectLayerService.get - filtering check - id: ${id}`);
327
354
  if (id && id !== 'undefined' && !['render', 'metadata', 'frame-counts'].includes(id)) {
328
355
  try {
329
- const objectLayer = await ObjectLayer.findById(id).select(ObjectLayerDto.select.get());
356
+ const objectLayer = await ObjectLayer.findById(id)
357
+ .select(ObjectLayerDto.select.get())
358
+ .populate('atlasSpriteSheetId', 'cid')
359
+ .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get());
330
360
  if (objectLayer) {
331
361
  logger.info(`ObjectLayerService.get - found record by id: ${id}`);
332
362
  return { data: [objectLayer], total: 1, page: 1, totalPages: 1 };
@@ -344,11 +374,29 @@ const ObjectLayerService = {
344
374
  .sort(sort)
345
375
  .limit(limit)
346
376
  .skip(skip)
347
- .select(ObjectLayerDto.select.get()),
377
+ .select(ObjectLayerDto.select.get())
378
+ .populate('atlasSpriteSheetId', 'cid')
379
+ .populate('objectLayerRenderFramesId', ObjectLayerRenderFramesDto.select.get()),
348
380
  ObjectLayer.countDocuments(query), // { userId: req.auth.user._id }
349
381
  ]);
350
382
  return { data, total, page, totalPages: Math.ceil(total / limit) };
351
383
  },
384
+
385
+ /**
386
+ * Generates a WebP animation from PNG frame images for a specific direction of an object layer.
387
+ * Uses the `img2webp` CLI tool to assemble the animation.
388
+ *
389
+ * @async
390
+ * @function generateWebp
391
+ * @memberof CyberiaObjectLayerService.ObjectLayerService
392
+ * @param {Object} req - Express request object with params: itemType, itemId, directionCode.
393
+ * @param {Object} res - Express response object.
394
+ * @param {Object} options - Server options containing host and path.
395
+ * @param {string} options.host - The deployment host.
396
+ * @param {string} options.path - The deployment path.
397
+ * @returns {Promise<Buffer>} The generated WebP animation as a Buffer.
398
+ * @throws {Error} If required parameters are missing, frames directory is not found, or img2webp fails.
399
+ */
352
400
  generateWebp: async (req, res, options) => {
353
401
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
354
402
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
@@ -461,6 +509,29 @@ const ObjectLayerService = {
461
509
  throw error;
462
510
  }
463
511
  },
512
+
513
+ /**
514
+ * PUT handler for updating object layers, their frame images, and metadata.
515
+ *
516
+ * Supports three sub-routes:
517
+ * - `/:id/frame-image/:itemType/:itemId/:directionCode` — Replace frame images for a direction.
518
+ * - `/:id/metadata/:itemType/:itemId` — Update metadata and reprocess all frames.
519
+ * - `/:id` — Standard update from request body.
520
+ *
521
+ * The `/metadata` route delegates to {@link ObjectLayerEngine.updateObjectLayerDocuments}
522
+ * for centralized document update, atlas regeneration, SHA-256 computation, and IPFS pinning.
523
+ *
524
+ * @async
525
+ * @function put
526
+ * @memberof CyberiaObjectLayerService.ObjectLayerService
527
+ * @param {Object} req - Express request object.
528
+ * @param {Object} res - Express response object.
529
+ * @param {Object} options - Server options containing host and path.
530
+ * @param {string} options.host - The deployment host.
531
+ * @param {string} options.path - The deployment path.
532
+ * @returns {Promise<Object>} The updated object layer document or frame upload result.
533
+ * @throws {Error} If file validation fails, object layer is not found, or required parameters are missing.
534
+ */
464
535
  put: async (req, res, options) => {
465
536
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
466
537
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
@@ -565,125 +636,183 @@ const ObjectLayerService = {
565
636
  const ObjectLayerRenderFrames =
566
637
  DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
567
638
 
568
- const objectLayerRenderFramesData = {
569
- frame_duration: req.body.objectLayerRenderFramesData.frame_duration,
570
- is_stateless: req.body.objectLayerRenderFramesData.is_stateless,
571
- frames: {},
572
- colors: [],
573
- };
574
-
575
- const objectLayerData = {
576
- data: {
577
- item: req.body.data.item,
578
- stats: req.body.data.stats,
579
- seed: req.body.data.seed || crypto.randomUUID(),
580
- },
581
- };
582
-
583
- // Process all PNG files from direction folders (uploaded via /frame-image/)
584
- const directionFolders = await fs.readdir(folder);
585
- for (const directionCode of directionFolders) {
586
- const directionPath = `${folder}/${directionCode}`;
587
-
588
- // Skip non-directories (like metadata.json)
589
- try {
590
- const stat = await fs.stat(directionPath);
591
- if (!stat.isDirectory()) continue;
592
- } catch (error) {
593
- logger.warn(`Skipping ${directionCode}: ${error.message}`);
594
- continue;
595
- }
596
-
597
- const frameFiles = await fs.readdir(directionPath);
598
- // Sort frame files numerically
599
- frameFiles.sort((a, b) => {
600
- const numA = parseInt(a.split('.')[0]);
601
- const numB = parseInt(b.split('.')[0]);
602
- return numA - numB;
639
+ // Build object layer data from the asset directory using centralized logic
640
+ const { objectLayerRenderFramesData, objectLayerData } =
641
+ await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
642
+ folder,
643
+ objectLayerType: itemType,
644
+ objectLayerId: itemId,
645
+ metadataOverride: req.body,
603
646
  });
604
647
 
605
- for (const frameFile of frameFiles) {
606
- if (!frameFile.endsWith('.png')) continue;
607
-
608
- const framePath = `${directionPath}/${frameFile}`;
609
-
610
- // Process image and push frame to render data with color palette management
611
- await ObjectLayerEngine.processAndPushFrame(objectLayerRenderFramesData, framePath, directionCode);
612
- }
648
+ // Preserve the existing seed if provided in the request body
649
+ if (req.body.data && req.body.data.seed) {
650
+ objectLayerData.data.seed = req.body.data.seed;
613
651
  }
614
652
 
615
- // Update or create ObjectLayerRenderFrames document
616
- const existingObjectLayer = await ObjectLayer.findById(objectLayerId);
617
- if (existingObjectLayer && existingObjectLayer.objectLayerRenderFramesId) {
618
- // Update existing render frames
619
- await ObjectLayerRenderFrames.findByIdAndUpdate(
620
- existingObjectLayer.objectLayerRenderFramesId,
621
- objectLayerRenderFramesData,
622
- );
623
- objectLayerData.objectLayerRenderFramesId = existingObjectLayer.objectLayerRenderFramesId;
624
- } else {
625
- // Create new render frames document
626
- const objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
627
- objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
628
- }
629
-
630
- // Generate SHA256 hash using fast-json-stable-stringify (seed is part of data)
631
- objectLayerData.sha256 = crypto.createHash('sha256').update(stringify(objectLayerData.data)).digest('hex');
632
-
633
- // Save to MongoDB
634
- try {
635
- const updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(objectLayerId, objectLayerData, {
636
- new: true,
637
- }).populate('objectLayerRenderFramesId');
638
- if (!updatedObjectLayer) {
639
- throw new Error('ObjectLayer not found for update');
640
- }
641
- logger.info(`ObjectLayer updated successfully with id: ${objectLayerId}`);
642
-
643
- // Update atlas sprite sheet
644
- try {
645
- await AtlasSpriteSheetService.generate(
646
- { params: { id: objectLayerId }, objectLayer: updatedObjectLayer },
653
+ // Update documents using centralized engine method (with atlas generation)
654
+ const { objectLayer } = await ObjectLayerEngine.updateObjectLayerDocuments({
655
+ objectLayerId,
656
+ ObjectLayer,
657
+ ObjectLayerRenderFrames,
658
+ objectLayerRenderFramesData,
659
+ objectLayerData,
660
+ updateOptions: {
661
+ generateAtlas: true,
662
+ atlasServiceContext: {
663
+ req,
647
664
  res,
648
665
  options,
649
- );
650
- } catch (atlasError) {
651
- logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
652
- }
666
+ AtlasSpriteSheetService,
667
+ IpfsClient,
668
+ createPinRecord,
669
+ },
670
+ },
671
+ });
653
672
 
654
- return updatedObjectLayer;
655
- } catch (error) {
656
- logger.error('Error updating ObjectLayer:', error);
657
- throw error;
658
- }
673
+ return objectLayer;
659
674
  }
660
675
 
661
676
  // PUT /:id - Standard update
662
- const updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(req.params.id, req.body, { new: true });
677
+ let updatedObjectLayer = await ObjectLayer.findByIdAndUpdate(req.params.id, req.body, { new: true });
663
678
 
664
679
  if (updatedObjectLayer) {
665
- // Update atlas sprite sheet
680
+ // Generate atlas sprite sheet – this sets data.atlasSpriteSheetCid and saves
666
681
  try {
667
- await AtlasSpriteSheetService.generate({ params: { id: req.params.id } }, res, options);
682
+ await AtlasSpriteSheetService.generate({ params: { id: req.params.id }, auth: req.auth }, res, options);
668
683
  } catch (atlasError) {
669
684
  logger.error('Failed to auto-update atlas for ObjectLayer:', atlasError);
670
685
  }
686
+
687
+ // Re-read so data.atlasSpriteSheetCid is up-to-date, then recompute SHA-256 & IPFS CID
688
+ updatedObjectLayer = await ObjectLayer.findById(req.params.id).populate('objectLayerRenderFramesId');
689
+ if (updatedObjectLayer) {
690
+ updatedObjectLayer = await ObjectLayerEngine.computeAndSaveFinalSha256({
691
+ objectLayer: updatedObjectLayer,
692
+ ipfsClient: IpfsClient,
693
+ createPinRecord,
694
+ userId: req.auth && req.auth.user ? req.auth.user._id : undefined,
695
+ options,
696
+ });
697
+ }
671
698
  }
672
699
 
673
700
  return updatedObjectLayer;
674
701
  },
702
+
703
+ /**
704
+ * DELETE handler for removing object layers and all associated resources.
705
+ *
706
+ * When a specific ID is provided, performs a cascading delete that removes:
707
+ * 1. The AtlasSpriteSheet and its File document.
708
+ * 2. The ObjectLayerRenderFrames document.
709
+ * 3. IPFS pin records and CIDs (both data JSON and atlas PNG).
710
+ * 4. The MFS directory for this object layer.
711
+ * 5. Static asset files from disk.
712
+ * 6. The ObjectLayer document itself.
713
+ *
714
+ * When no ID is provided, performs a bulk delete of all object layers.
715
+ *
716
+ * @async
717
+ * @function delete
718
+ * @memberof CyberiaObjectLayerService.ObjectLayerService
719
+ * @param {Object} req - Express request object.
720
+ * @param {Object} res - Express response object.
721
+ * @param {Object} options - Server options containing host and path.
722
+ * @param {string} options.host - The deployment host.
723
+ * @param {string} options.path - The deployment path.
724
+ * @returns {Promise<Object>} The deleted object layer document or bulk delete count.
725
+ * @throws {Error} If the object layer is not found.
726
+ */
675
727
  delete: async (req, res, options) => {
676
728
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
677
729
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
730
+ const ObjectLayerRenderFrames =
731
+ DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayerRenderFrames;
732
+
678
733
  if (req.params.id) {
679
- // Clean up atlas and associated files
734
+ // Load the full object layer so we can access all references
735
+ const objectLayer = await ObjectLayer.findById(req.params.id);
736
+ if (!objectLayer) {
737
+ throw new Error('ObjectLayer not found');
738
+ }
739
+
740
+ const itemType = objectLayer.data?.item?.type;
741
+ const itemId = objectLayer.data?.item?.id;
742
+
743
+ // ── 1. Clean up AtlasSpriteSheet, its File, and atlas IPFS CID ──
680
744
  try {
681
745
  await AtlasSpriteSheetService.deleteByObjectLayerId(req, res, options);
682
746
  } catch (atlasError) {
683
747
  logger.error('Failed to clean up atlas during ObjectLayer deletion:', atlasError);
684
748
  }
685
- return await ObjectLayer.findByIdAndDelete(req.params.id);
686
- } else return await ObjectLayer.deleteMany();
749
+
750
+ // ── 2. Clean up ObjectLayerRenderFrames document ──
751
+ if (objectLayer.objectLayerRenderFramesId) {
752
+ try {
753
+ await ObjectLayerRenderFrames.findByIdAndDelete(objectLayer.objectLayerRenderFramesId);
754
+ logger.info(
755
+ `Deleted ObjectLayerRenderFrames ${objectLayer.objectLayerRenderFramesId} for ObjectLayer ${req.params.id}`,
756
+ );
757
+ } catch (renderFramesError) {
758
+ logger.error('Failed to delete ObjectLayerRenderFrames:', renderFramesError);
759
+ }
760
+ }
761
+
762
+ // ── 3. Remove pin records and unpin object layer data JSON CID from IPFS ──
763
+ if (objectLayer.cid) {
764
+ try {
765
+ await removePinRecordsAndUnpin(objectLayer.cid, options);
766
+ logger.info(`Cleaned up IPFS data CID ${objectLayer.cid} for ObjectLayer ${req.params.id}`);
767
+ } catch (ipfsError) {
768
+ logger.warn(`Failed to clean up IPFS data CID ${objectLayer.cid}: ${ipfsError.message}`);
769
+ }
770
+ }
771
+
772
+ // ── 4. Remove MFS directory for this object layer (covers both data JSON and atlas PNG) ──
773
+ if (itemId) {
774
+ try {
775
+ await IpfsClient.removeMfsPath(`/object-layer/${itemId}`);
776
+ logger.info(`Removed MFS directory /object-layer/${itemId}`);
777
+ } catch (mfsError) {
778
+ logger.warn(`Failed to remove MFS path /object-layer/${itemId}: ${mfsError.message}`);
779
+ }
780
+ }
781
+
782
+ // ── 5. Remove static asset files from disk ──
783
+ if (itemType && itemId) {
784
+ const staticPaths = [
785
+ `./src/client/public/cyberia/assets/${itemType}/${itemId}`,
786
+ `./public/${options.host}${options.path}/assets/${itemType}/${itemId}`,
787
+ ];
788
+ for (const assetDir of staticPaths) {
789
+ try {
790
+ if (fs.existsSync(assetDir)) {
791
+ await fs.remove(assetDir);
792
+ logger.info(`Removed static asset directory: ${assetDir}`);
793
+ }
794
+ } catch (fsError) {
795
+ logger.warn(`Failed to remove static asset directory ${assetDir}: ${fsError.message}`);
796
+ }
797
+ }
798
+ }
799
+
800
+ // ── 6. Delete the ObjectLayer document itself ──
801
+ const deleted = await ObjectLayer.findByIdAndDelete(req.params.id);
802
+ logger.info(`ObjectLayer ${req.params.id} and all associated resources deleted successfully`);
803
+ return deleted;
804
+ } else {
805
+ // Bulk delete: clean up all object layers
806
+ const allObjectLayers = await ObjectLayer.find({});
807
+ for (const ol of allObjectLayers) {
808
+ try {
809
+ await ObjectLayerService.delete({ params: { id: ol._id.toString() }, auth: req.auth }, res, options);
810
+ } catch (err) {
811
+ logger.error(`Failed to delete ObjectLayer ${ol._id} during bulk delete: ${err.message}`);
812
+ }
813
+ }
814
+ return { deletedCount: allObjectLayers.length };
815
+ }
687
816
  },
688
817
  };
689
818