@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.
- package/README.md +2 -2
- package/cli.md +3 -1
- package/conf.js +2 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/src/api/core/core.service.js +0 -5
- package/src/api/default/default.service.js +7 -5
- package/src/api/document/document.model.js +1 -1
- package/src/api/document/document.router.js +5 -0
- package/src/api/document/document.service.js +105 -47
- package/src/api/file/file.model.js +112 -4
- package/src/api/file/file.ref.json +42 -0
- package/src/api/file/file.service.js +380 -32
- package/src/api/user/user.model.js +38 -1
- package/src/api/user/user.router.js +96 -63
- package/src/api/user/user.service.js +81 -48
- package/src/cli/db.js +424 -166
- package/src/cli/index.js +8 -0
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +1 -0
- package/src/cli/ssh.js +10 -10
- package/src/client/components/core/Account.js +327 -36
- package/src/client/components/core/AgGrid.js +3 -0
- package/src/client/components/core/Auth.js +9 -3
- package/src/client/components/core/Chat.js +2 -2
- package/src/client/components/core/Content.js +159 -78
- package/src/client/components/core/CssCore.js +16 -12
- package/src/client/components/core/FileExplorer.js +115 -8
- package/src/client/components/core/Input.js +204 -11
- package/src/client/components/core/LogIn.js +42 -20
- package/src/client/components/core/Modal.js +138 -24
- package/src/client/components/core/Panel.js +69 -31
- package/src/client/components/core/PanelForm.js +262 -77
- package/src/client/components/core/PublicProfile.js +888 -0
- package/src/client/components/core/Router.js +117 -15
- package/src/client/components/core/SearchBox.js +329 -13
- package/src/client/components/core/SignUp.js +26 -7
- package/src/client/components/core/SocketIo.js +6 -3
- package/src/client/components/core/Translate.js +98 -0
- package/src/client/components/core/Validator.js +15 -0
- package/src/client/components/core/windowGetDimensions.js +6 -6
- package/src/client/components/default/MenuDefault.js +59 -12
- package/src/client/components/default/RoutesDefault.js +1 -0
- package/src/client/services/core/core.service.js +163 -1
- package/src/client/services/default/default.management.js +451 -64
- package/src/client/services/default/default.service.js +13 -6
- package/src/client/services/file/file.service.js +43 -16
- package/src/client/services/user/user.service.js +13 -9
- package/src/db/DataBaseProvider.js +1 -1
- package/src/db/mongo/MongooseDB.js +1 -1
- package/src/index.js +1 -1
- package/src/mailer/MailerProvider.js +4 -4
- package/src/runtime/express/Express.js +2 -1
- package/src/runtime/lampp/Lampp.js +2 -2
- package/src/server/auth.js +3 -6
- package/src/server/data-query.js +449 -0
- package/src/server/object-layer.js +0 -3
- package/src/ws/IoInterface.js +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- badges -->
|
|
20
20
|
|
|
21
|
-
[](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [](https://www.npmjs.com/package/underpost) [](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [](https://www.npmjs.com/package/underpost) [](https://socket.dev/npm/package/underpost/overview/2.97.5) [](https://coveralls.io/github/underpostnet/engine?branch=master) [](https://www.npmjs.org/package/underpost) [](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.
|
|
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
|
+
## 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
const skip
|
|
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(
|
|
21
|
-
Default.countDocuments(
|
|
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);
|
|
@@ -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
|
|
123
|
-
|
|
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 -
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
queryPayload
|
|
342
|
-
$in:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
+
]
|