@underpostnet/underpost 2.97.1 → 2.97.5

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 (59) hide show
  1. package/README.md +2 -2
  2. package/cli.md +3 -1
  3. package/conf.js +2 -0
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  6. package/package.json +1 -1
  7. package/src/api/core/core.service.js +0 -5
  8. package/src/api/default/default.service.js +7 -5
  9. package/src/api/document/document.model.js +1 -1
  10. package/src/api/document/document.router.js +5 -0
  11. package/src/api/document/document.service.js +105 -47
  12. package/src/api/file/file.model.js +112 -4
  13. package/src/api/file/file.ref.json +42 -0
  14. package/src/api/file/file.service.js +380 -32
  15. package/src/api/user/user.model.js +38 -1
  16. package/src/api/user/user.router.js +96 -63
  17. package/src/api/user/user.service.js +81 -48
  18. package/src/cli/db.js +424 -166
  19. package/src/cli/index.js +8 -0
  20. package/src/cli/repository.js +1 -1
  21. package/src/cli/run.js +1 -0
  22. package/src/cli/ssh.js +10 -10
  23. package/src/client/components/core/Account.js +327 -36
  24. package/src/client/components/core/AgGrid.js +3 -0
  25. package/src/client/components/core/Auth.js +9 -3
  26. package/src/client/components/core/Chat.js +2 -2
  27. package/src/client/components/core/Content.js +159 -78
  28. package/src/client/components/core/CssCore.js +16 -12
  29. package/src/client/components/core/FileExplorer.js +115 -8
  30. package/src/client/components/core/Input.js +204 -11
  31. package/src/client/components/core/LogIn.js +42 -20
  32. package/src/client/components/core/Modal.js +138 -24
  33. package/src/client/components/core/Panel.js +69 -31
  34. package/src/client/components/core/PanelForm.js +262 -77
  35. package/src/client/components/core/PublicProfile.js +888 -0
  36. package/src/client/components/core/Router.js +117 -15
  37. package/src/client/components/core/SearchBox.js +329 -13
  38. package/src/client/components/core/SignUp.js +26 -7
  39. package/src/client/components/core/SocketIo.js +6 -3
  40. package/src/client/components/core/Translate.js +98 -0
  41. package/src/client/components/core/Validator.js +15 -0
  42. package/src/client/components/core/windowGetDimensions.js +6 -6
  43. package/src/client/components/default/MenuDefault.js +59 -12
  44. package/src/client/components/default/RoutesDefault.js +1 -0
  45. package/src/client/services/core/core.service.js +163 -1
  46. package/src/client/services/default/default.management.js +451 -64
  47. package/src/client/services/default/default.service.js +13 -6
  48. package/src/client/services/file/file.service.js +43 -16
  49. package/src/client/services/user/user.service.js +13 -9
  50. package/src/db/DataBaseProvider.js +1 -1
  51. package/src/db/mongo/MongooseDB.js +1 -1
  52. package/src/index.js +1 -1
  53. package/src/mailer/MailerProvider.js +4 -4
  54. package/src/runtime/express/Express.js +2 -1
  55. package/src/runtime/lampp/Lampp.js +2 -2
  56. package/src/server/auth.js +3 -6
  57. package/src/server/data-query.js +449 -0
  58. package/src/server/object-layer.js +0 -3
  59. package/src/ws/IoInterface.js +2 -2
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  <!-- badges -->
20
20
 
21
- [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.97.1)](https://socket.dev/npm/package/underpost/overview/2.97.1) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
21
+ [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/2.97.5)](https://socket.dev/npm/package/underpost/overview/2.97.5) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
22
22
 
23
23
  <!-- end-badges -->
24
24
 
@@ -66,7 +66,7 @@ Run dev client server
66
66
  npm run dev
67
67
  ```
68
68
  <!-- -->
69
- ## underpost ci/cd cli v2.97.1
69
+ ## underpost ci/cd cli v2.97.5
70
70
 
71
71
  ### Usage: `underpost [options] [command]`
72
72
  ```
package/cli.md CHANGED
@@ -1,4 +1,4 @@
1
- ## underpost ci/cd cli v2.97.1
1
+ ## underpost ci/cd cli v2.97.5
2
2
 
3
3
  ### Usage: `underpost [options] [command]`
4
4
  ```
@@ -589,6 +589,8 @@ Options:
589
589
  --paths <paths> Comma-separated list of paths to filter database operations.
590
590
  --ns <ns-name> Kubernetes namespace context for database operations (defaults to "default").
591
591
  --macro-rollback-export <n-commits-reset> Exports a macro rollback script that reverts the last n commits (Git integration required).
592
+ --clean-fs-collection Cleans orphaned File documents from collections that are not referenced by any models.
593
+ --clean-fs-dry-run Dry run mode - shows what would be deleted without actually deleting (use with --clean-fs-collection).
592
594
  --dev Sets the development cli context
593
595
  --kubeadm Enables the kubeadm context for database operations.
594
596
  --kind Enables the kind context for database operations.
package/conf.js CHANGED
@@ -39,6 +39,7 @@ const DefaultConf = /**/ {
39
39
  'LogOut',
40
40
  'Router',
41
41
  'Account',
42
+ 'PublicProfile',
42
43
  'Auth',
43
44
  'FullScreen',
44
45
  'RichText',
@@ -91,6 +92,7 @@ const DefaultConf = /**/ {
91
92
  { path: '/account', client: 'Default', ssr: 'Default' },
92
93
  { path: '/docs', client: 'Default', ssr: 'Default' },
93
94
  { path: '/recover', client: 'Default', ssr: 'Default' },
95
+ { path: '/u', client: 'Default', ssr: 'Default' },
94
96
  { path: '/default-management', client: 'Default', ssr: 'Default' },
95
97
  { client: 'Default', ssr: 'Default', path: '/404', title: '404 Not Found' },
96
98
  { client: 'Default', ssr: 'Default', path: '/500', title: '500 Server Error' },
@@ -17,7 +17,7 @@ spec:
17
17
  spec:
18
18
  containers:
19
19
  - name: dd-default-development-blue
20
- image: localhost/rockylinux9-underpost:v2.97.1
20
+ image: localhost/rockylinux9-underpost:v2.97.5
21
21
  # resources:
22
22
  # requests:
23
23
  # memory: "124Ki"
@@ -100,7 +100,7 @@ spec:
100
100
  spec:
101
101
  containers:
102
102
  - name: dd-default-development-green
103
- image: localhost/rockylinux9-underpost:v2.97.1
103
+ image: localhost/rockylinux9-underpost:v2.97.5
104
104
  # resources:
105
105
  # requests:
106
106
  # memory: "124Ki"
@@ -18,7 +18,7 @@ spec:
18
18
  spec:
19
19
  containers:
20
20
  - name: dd-test-development-blue
21
- image: localhost/rockylinux9-underpost:v2.97.1
21
+ image: localhost/rockylinux9-underpost:v2.97.5
22
22
 
23
23
  command:
24
24
  - /bin/sh
@@ -103,7 +103,7 @@ spec:
103
103
  spec:
104
104
  containers:
105
105
  - name: dd-test-development-green
106
- image: localhost/rockylinux9-underpost:v2.97.1
106
+ image: localhost/rockylinux9-underpost:v2.97.5
107
107
 
108
108
  command:
109
109
  - /bin/sh
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "main": "src/index.js",
4
4
  "name": "@underpostnet/underpost",
5
- "version": "2.97.1",
5
+ "version": "2.97.5",
6
6
  "description": "pwa api rest template",
7
7
  "scripts": {
8
8
  "start": "env-cmd -f .env.production node --max-old-space-size=8192 src/server",
@@ -8,11 +8,6 @@ const CoreService = {
8
8
  post: async (req, res, options) => {
9
9
  /** @type {import('./core.model.js').CoreModel} */
10
10
  const Core = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Core;
11
- if (req.path.startsWith('/sh')) {
12
- if (req.body.stdout) return shellExec(req.body.sh, { stdout: true });
13
- shellExec(req.body.sh, { async: true });
14
- return 'Command "' + req.body.sh + '" running';
15
- }
16
11
  return await new Core(req.body).save();
17
12
  },
18
13
  get: async (req, res, options) => {
@@ -1,5 +1,6 @@
1
1
  import { DataBaseProvider } from '../../db/DataBaseProvider.js';
2
2
  import { loggerFactory } from '../../server/logger.js';
3
+ import { DataQuery } from '../../server/data-query.js';
3
4
 
4
5
  const logger = loggerFactory(import.meta);
5
6
 
@@ -13,12 +14,13 @@ const DefaultService = {
13
14
  /** @type {import('./default.model.js').DefaultModel} */
14
15
  const Default = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Default;
15
16
  if (req.params.id) return await Default.findById(req.params.id);
16
- const { query, page = 1, limit = 10, sort = { updatedAt: -1 } } = req.query;
17
- const queryPayload = query ? JSON.parse(query) : {};
18
- const skip = (page - 1) * limit;
17
+
18
+ // Parse query parameters using DataQuery helper
19
+ const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
20
+
19
21
  const [data, total] = await Promise.all([
20
- Default.find(queryPayload).sort(sort).limit(limit).skip(skip),
21
- Default.countDocuments(queryPayload),
22
+ Default.find(query).sort(sort).limit(limit).skip(skip),
23
+ Default.countDocuments(query),
22
24
  ]);
23
25
 
24
26
  const totalPages = Math.ceil(total / limit);
@@ -60,7 +60,7 @@ const DocumentDto = {
60
60
  return {
61
61
  path: 'userId',
62
62
  model: 'User',
63
- select: '_id email username profileImageId',
63
+ select: '_id email username profileImageId role briefDescription',
64
64
  populate: {
65
65
  path: 'profileImageId',
66
66
  model: 'File',
@@ -16,6 +16,11 @@ const DocumentRouter = (options) => {
16
16
  router.put(`/:id`, authMiddleware, async (req, res) => await DocumentController.put(req, res, options));
17
17
  router.put(`/`, authMiddleware, async (req, res) => await DocumentController.put(req, res, options));
18
18
  router.patch(`/:id/copy-share-link`, async (req, res) => await DocumentController.patch(req, res, options));
19
+ router.patch(
20
+ `/:id/toggle-public`,
21
+ authMiddleware,
22
+ async (req, res) => await DocumentController.patch(req, res, options),
23
+ );
19
24
  router.delete(`/:id`, authMiddleware, async (req, res) => await DocumentController.delete(req, res, options));
20
25
  router.delete(`/`, authMiddleware, async (req, res) => await DocumentController.delete(req, res, options));
21
26
  return router;
@@ -4,6 +4,7 @@ import { DocumentDto } from './document.model.js';
4
4
  import { uniqueArray } from '../../client/components/core/CommonJs.js';
5
5
  import { getBearerToken, verifyJWT } from '../../server/auth.js';
6
6
  import { isValidObjectId } from 'mongoose';
7
+ import { FileCleanup } from '../file/file.service.js';
7
8
 
8
9
  const logger = loggerFactory(import.meta);
9
10
 
@@ -32,8 +33,6 @@ const DocumentService = {
32
33
  const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
33
34
 
34
35
  // High-query endpoint for typeahead search
35
- // ============================================
36
- // OPTIMIZATION GOAL: MAXIMIZE search results with MINIMUM match requirements
37
36
  //
38
37
  // Security Model:
39
38
  // - Unauthenticated users: CAN see public documents (isPublic=true) from publishers (admin/moderator)
@@ -111,6 +110,7 @@ const DocumentService = {
111
110
  .sort({ createdAt: -1 })
112
111
  .limit(limit)
113
112
  .select('_id title tags createdAt userId isPublic')
113
+ .populate(DocumentDto.populate.user())
114
114
  .lean();
115
115
 
116
116
  const sanitizedData = data.map((doc) => {
@@ -118,9 +118,23 @@ const DocumentService = {
118
118
  ...doc,
119
119
  tags: DocumentDto.filterPublicTag(doc.tags),
120
120
  };
121
+ // For unauthenticated users, only include user data if document is public AND creator is publisher
121
122
  if (!user || user.role === 'guest') {
122
- const { userId, ...rest } = filteredDoc;
123
- return rest;
123
+ const isPublisher = doc.userId && (doc.userId.role === 'admin' || doc.userId.role === 'moderator');
124
+ if (!doc.isPublic || !isPublisher) {
125
+ const { userId, ...rest } = filteredDoc;
126
+ return rest;
127
+ }
128
+ // Remove role field from userId before sending to client
129
+ if (filteredDoc.userId && filteredDoc.userId.role) {
130
+ const { role, ...userWithoutRole } = filteredDoc.userId;
131
+ filteredDoc.userId = userWithoutRole;
132
+ }
133
+ }
134
+ // Remove role field from userId before sending to client (authenticated users)
135
+ if (filteredDoc.userId && filteredDoc.userId.role) {
136
+ const { role, ...userWithoutRole } = filteredDoc.userId;
137
+ filteredDoc.userId = userWithoutRole;
124
138
  }
125
139
  return filteredDoc;
126
140
  });
@@ -233,18 +247,27 @@ const DocumentService = {
233
247
  .sort({ createdAt: -1 })
234
248
  .limit(limit)
235
249
  .select('_id title tags createdAt userId isPublic')
250
+ .populate(DocumentDto.populate.user())
236
251
  .lean();
237
252
 
238
- // Sanitize response - remove userId for public users and filter 'public' from tags
253
+ // Sanitize response - include userId for public documents from publishers, filter 'public' from tags
239
254
  const sanitizedData = data.map((doc) => {
240
255
  const filteredDoc = {
241
256
  ...doc,
242
257
  tags: DocumentDto.filterPublicTag(doc.tags),
243
258
  };
259
+ // For unauthenticated users, only include user data if document is public AND creator is publisher
244
260
  if (!user || user.role === 'guest') {
245
- // Remove userId from response for unauthenticated users
246
- const { userId, ...rest } = filteredDoc;
247
- return rest;
261
+ const isPublisher = doc.userId && (doc.userId.role === 'admin' || doc.userId.role === 'moderator');
262
+ if (!doc.isPublic || !isPublisher) {
263
+ const { userId, ...rest } = filteredDoc;
264
+ return rest;
265
+ }
266
+ }
267
+ // Remove role field from userId before sending to client (all users)
268
+ if (filteredDoc.userId && filteredDoc.userId.role) {
269
+ const { role, ...userWithoutRole } = filteredDoc.userId;
270
+ filteredDoc.userId = userWithoutRole;
248
271
  }
249
272
  return filteredDoc;
250
273
  });
@@ -330,16 +353,22 @@ const DocumentService = {
330
353
  } else {
331
354
  // Unauthenticated user: only public documents from publishers
332
355
  // If 'public' tag requested, it's redundant but handled by isPublic: true
333
- queryPayload = {
334
- userId: { $in: publisherUsers.map((p) => p._id) },
335
- isPublic: true,
336
- ...(requestedTags.length > 0 ? { tags: { $all: requestedTags } } : {}),
337
- };
356
+ // When cid is provided, we relax the publisher filter and check in post-processing
357
+ const cidList = req.query.cid ? req.query.cid.split(',').filter((cid) => isValidObjectId(cid)) : null;
338
358
 
339
- // Add cid filter if present
340
- if (req.query.cid) {
341
- queryPayload._id = {
342
- $in: req.query.cid.split(',').filter((cid) => isValidObjectId(cid)),
359
+ if (cidList && cidList.length > 0) {
360
+ // For cid queries, just filter by public and tags, check publisher in post-processing
361
+ queryPayload = {
362
+ _id: { $in: cidList },
363
+ isPublic: true,
364
+ ...(requestedTags.length > 0 ? { tags: { $all: requestedTags } } : {}),
365
+ };
366
+ } else {
367
+ // For non-cid queries, filter by publisher at query level
368
+ queryPayload = {
369
+ userId: { $in: publisherUsers.map((p) => p._id) },
370
+ isPublic: true,
371
+ ...(requestedTags.length > 0 ? { tags: { $all: requestedTags } } : {}),
343
372
  };
344
373
  }
345
374
  }
@@ -358,29 +387,43 @@ const DocumentService = {
358
387
  // sort in descending (-1) order by length
359
388
  const sort = { createdAt: -1 };
360
389
 
390
+ // Populate user data for authenticated users OR for public documents from publishers
391
+ // This allows unauthenticated users to see creator profiles on public content
392
+ const shouldPopulateUser = user && user.role !== 'guest';
393
+ // Check if query contains public documents (either in $or array or flat query)
394
+ const hasPublicDocuments =
395
+ queryPayload.isPublic === true ||
396
+ queryPayload.$or?.some(
397
+ (condition) => condition.isPublic === true || (condition.userId && condition.isPublic === true),
398
+ );
399
+
361
400
  const data = await Document.find(queryPayload)
362
401
  .sort(sort)
363
402
  .limit(limit)
364
403
  .skip(skip)
365
404
  .populate(DocumentDto.populate.file())
366
405
  .populate(DocumentDto.populate.mdFile())
367
- .populate(user && user.role !== 'guest' ? DocumentDto.populate.user() : null);
406
+ .populate(shouldPopulateUser || hasPublicDocuments ? DocumentDto.populate.user() : null);
368
407
 
369
408
  const lastDoc = await Document.findOne(queryPayload, '_id').sort({ createdAt: 1 });
370
409
  const lastId = lastDoc ? lastDoc._id : null;
371
410
 
372
- // Add totalCopyShareLinkCount to each document and filter 'public' from tags
373
- const dataWithCounts = data.map((doc) => {
374
- const docObj = doc.toObject ? doc.toObject() : doc;
375
- return {
376
- ...docObj,
377
- tags: DocumentDto.filterPublicTag(docObj.tags),
378
- totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
379
- };
380
- });
381
-
382
411
  return {
383
- data: dataWithCounts,
412
+ data: data.map((doc) => {
413
+ const docObj = doc.toObject ? doc.toObject() : doc;
414
+ let userInfo = docObj.userId;
415
+ const isPublisher = userInfo && (userInfo.role === 'admin' || userInfo.role === 'moderator');
416
+ const isOwnDoc = user && user._id.toString() === docObj.userId._id.toString();
417
+ if ((!docObj.isPublic || !isPublisher) && !isOwnDoc) userInfo = undefined;
418
+ return {
419
+ ...docObj,
420
+ role: undefined,
421
+ email: undefined,
422
+ userId: userInfo,
423
+ tags: DocumentDto.filterPublicTag(docObj.tags),
424
+ totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
425
+ };
426
+ }),
384
427
  lastId,
385
428
  };
386
429
  }
@@ -398,8 +441,17 @@ const DocumentService = {
398
441
  // Add totalCopyShareLinkCount to each document and filter 'public' from tags
399
442
  return data.map((doc) => {
400
443
  const docObj = doc.toObject ? doc.toObject() : doc;
444
+
445
+ // Remove role field from userId before sending to client
446
+ let userInfo = docObj.userId;
447
+ if (userInfo && userInfo.role) {
448
+ const { role, ...userWithoutRole } = userInfo;
449
+ userInfo = userWithoutRole;
450
+ }
451
+
401
452
  return {
402
453
  ...docObj,
454
+ userId: userInfo,
403
455
  tags: DocumentDto.filterPublicTag(docObj.tags),
404
456
  totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
405
457
  };
@@ -420,15 +472,12 @@ const DocumentService = {
420
472
 
421
473
  if (document.userId.toString() !== req.auth.user._id) throw new Error('invalid user');
422
474
 
423
- if (document.mdFileId) {
424
- const file = await File.findOne({ _id: document.mdFileId });
425
- if (file) await File.findByIdAndDelete(document.mdFileId);
426
- }
427
-
428
- if (document.fileId) {
429
- const file = await File.findOne({ _id: document.fileId });
430
- if (file) await File.findByIdAndDelete(document.fileId);
431
- }
475
+ // Clean up all associated files
476
+ await FileCleanup.deleteDocumentFiles({
477
+ doc: document,
478
+ fileFields: ['fileId', 'mdFileId'],
479
+ File,
480
+ });
432
481
 
433
482
  return await Document.findByIdAndDelete(req.params.id);
434
483
  }
@@ -445,15 +494,13 @@ const DocumentService = {
445
494
  const document = await Document.findOne({ _id: req.params.id });
446
495
  if (!document) throw new Error(`Document not found`);
447
496
 
448
- if (document.mdFileId) {
449
- const file = await File.findOne({ _id: document.mdFileId });
450
- if (file) await File.findByIdAndDelete(document.mdFileId);
451
- }
452
-
453
- if (document.fileId) {
454
- const file = await File.findOne({ _id: document.fileId });
455
- if (file) await File.findByIdAndDelete(document.fileId);
456
- }
497
+ // Clean up old files if they are being replaced
498
+ await FileCleanup.cleanupReplacedFiles({
499
+ oldDoc: document,
500
+ newData: req.body,
501
+ fileFields: ['fileId', 'mdFileId'],
502
+ File,
503
+ });
457
504
 
458
505
  // Extract 'public' from tags and set isPublic field on update
459
506
  const { isPublic, tags } = DocumentDto.extractPublicFromTags(req.body.tags);
@@ -468,6 +515,17 @@ const DocumentService = {
468
515
  /** @type {import('./document.model.js').DocumentModel} */
469
516
  const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
470
517
 
518
+ if (req.path.includes('/toggle-public')) {
519
+ const document = await Document.findById(req.params.id);
520
+ if (!document) throw new Error('Document not found');
521
+
522
+ // Toggle the isPublic field
523
+ document.isPublic = !document.isPublic;
524
+ await document.save();
525
+
526
+ return { _id: document._id, isPublic: document.isPublic };
527
+ }
528
+
471
529
  if (req.path.includes('/copy-share-link')) {
472
530
  const document = await Document.findById(req.params.id);
473
531
  if (!document) throw new Error('Document not found');
@@ -1,10 +1,23 @@
1
+ /**
2
+ * File model and schema definitions for MongoDB/Mongoose.
3
+ * Provides the File schema, model, and Data Transfer Object (DTO) for file operations.
4
+ *
5
+ * @module src/api/file/file.model.js
6
+ * @namespace FileModelServer
7
+ */
8
+
1
9
  import { Schema, model } from 'mongoose';
2
10
 
11
+ /**
12
+ * Mongoose schema definition for File documents.
13
+ * @type {Schema}
14
+ * @memberof FileModelServer
15
+ */
3
16
  const FileSchema = new Schema({
4
- name: { type: String },
5
- data: { type: Buffer },
17
+ name: { type: String, required: true },
18
+ data: { type: Buffer, required: true },
6
19
  size: { type: Number },
7
- encoding: { type: String },
20
+ encoding: { type: String, default: 'utf-8' },
8
21
  tempFilePath: { type: String },
9
22
  truncated: { type: Boolean },
10
23
  mimetype: { type: String },
@@ -12,8 +25,103 @@ const FileSchema = new Schema({
12
25
  cid: { type: String },
13
26
  });
14
27
 
28
+ /**
29
+ * Mongoose model for File documents.
30
+ * @type {import('mongoose').Model}
31
+ * @memberof FileModelServer
32
+ */
15
33
  const FileModel = model('File', FileSchema);
16
34
 
35
+ /**
36
+ * Provider schema alias for File schema.
37
+ * Used for database provider compatibility.
38
+ * @type {Schema}
39
+ * @memberof FileModelServer
40
+ */
17
41
  const ProviderSchema = FileSchema;
18
42
 
19
- export { FileSchema, FileModel, ProviderSchema };
43
+ /**
44
+ * File Data Transfer Object (DTO) for the model layer.
45
+ * Provides core transformation methods for file documents including metadata extraction,
46
+ * full document conversion, and filename normalization utilities.
47
+ * @namespace FileModelServer.FileModelDto
48
+ * @memberof FileModelServer
49
+ */
50
+ const FileModelDto = {
51
+ /**
52
+ * Returns file metadata only (no buffer data).
53
+ * Used for list responses and API integration.
54
+ * @function toMetadata
55
+ * @memberof FileModelServer.FileModelDto
56
+ * @param {Object} file - File document from database.
57
+ * @returns {Object|null} File metadata object, or null if file is falsy.
58
+ */
59
+ toMetadata: (file) => {
60
+ if (!file) return null;
61
+ return {
62
+ _id: file._id,
63
+ name: file.name || '',
64
+ mimetype: file.mimetype || 'application/octet-stream',
65
+ size: file.size || 0,
66
+ md5: file.md5 || '',
67
+ cid: file.cid || '',
68
+ createdAt: file.createdAt,
69
+ updatedAt: file.updatedAt,
70
+ };
71
+ },
72
+
73
+ /**
74
+ * Returns file with complete data.
75
+ * Used only when explicitly requested (e.g., file/blob endpoint).
76
+ * @function toFull
77
+ * @memberof FileModelServer.FileModelDto
78
+ * @param {Object} file - File document from database.
79
+ * @returns {Object|null} Complete file object with buffer data, or null if file is falsy.
80
+ */
81
+ toFull: (file) => {
82
+ if (!file) return null;
83
+ return {
84
+ _id: file._id,
85
+ name: file.name || '',
86
+ mimetype: file.mimetype || 'application/octet-stream',
87
+ size: file.size || 0,
88
+ md5: file.md5 || '',
89
+ cid: file.cid || '',
90
+ data: file.data,
91
+ encoding: file.encoding || 'utf-8',
92
+ createdAt: file.createdAt,
93
+ updatedAt: file.updatedAt,
94
+ };
95
+ },
96
+
97
+ /**
98
+ * Transforms array of files to metadata only.
99
+ * @function toMetadataArray
100
+ * @memberof FileModelServer.FileModelDto
101
+ * @param {Array} files - Array of file documents.
102
+ * @returns {Array} Array of file metadata objects.
103
+ */
104
+ toMetadataArray: (files) => {
105
+ if (!Array.isArray(files)) return [];
106
+ return files.map((file) => FileModelDto.toMetadata(file));
107
+ },
108
+
109
+ /**
110
+ * Ensures UTF-8 encoding for filenames.
111
+ * Fixes issues with special characters (e.g., ñ, é, ü).
112
+ * @function normalizeFilename
113
+ * @memberof FileModelServer.FileModelDto
114
+ * @param {string} filename - Raw filename from upload.
115
+ * @returns {string} UTF-8 encoded filename.
116
+ */
117
+ normalizeFilename: (filename) => {
118
+ if (!filename) return '';
119
+ // Ensure string and normalize to UTF-8
120
+ let normalized = String(filename);
121
+ // Replace any incorrectly encoded sequences
122
+ normalized = Buffer.from(normalized, 'utf8').toString('utf8');
123
+ return normalized;
124
+ },
125
+ };
126
+
127
+ export { FileSchema, FileModel, ProviderSchema, FileModelDto };
@@ -0,0 +1,42 @@
1
+ [
2
+ {
3
+ "api": "company",
4
+ "model": {
5
+ "logo": true
6
+ }
7
+ },
8
+ {
9
+ "api": "cyberia-biome",
10
+ "model": {
11
+ "fileId": true,
12
+ "topLevelColorFileId": true
13
+ }
14
+ },
15
+ {
16
+ "api": "cyberia-tile",
17
+ "model": {
18
+ "fileId": true
19
+ }
20
+ },
21
+ {
22
+ "api": "cyberia-world",
23
+ "model": {
24
+ "adjacentFace": {
25
+ "fileId": true
26
+ }
27
+ }
28
+ },
29
+ {
30
+ "api": "document",
31
+ "model": {
32
+ "fileId": true,
33
+ "mdFileId": true
34
+ }
35
+ },
36
+ {
37
+ "api": "user",
38
+ "model": {
39
+ "profileImageId": true
40
+ }
41
+ }
42
+ ]