@strapi/upload 5.52.3 → 5.54.0
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/dist/admin/future/components/Drawer.js +6 -3
- package/dist/admin/future/components/Drawer.js.map +1 -1
- package/dist/admin/future/components/Drawer.mjs +6 -3
- package/dist/admin/future/components/Drawer.mjs.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.js +9 -8
- package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
- package/dist/admin/future/components/UploadProgressDialog.mjs +9 -8
- package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.js +16 -4
- package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
- package/dist/admin/future/pages/Assets/AssetsPage.mjs +16 -4
- package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js +5 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs +5 -0
- package/dist/admin/future/pages/Assets/components/AssetActionsMenu.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js +19 -4
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs +19 -4
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetCropEditor.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js +35 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs +35 -1
- package/dist/admin/future/pages/Assets/components/AssetDetails/AssetDetailsDrawer.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js +10 -0
- package/dist/admin/future/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs +10 -0
- package/dist/admin/future/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.js +9 -0
- package/dist/admin/future/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs +10 -1
- package/dist/admin/future/pages/Assets/components/AssetsTable.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js +132 -88
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs +135 -91
- package/dist/admin/future/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +7 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +7 -0
- package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js +4 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js.map +1 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs +4 -1
- package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.js +181 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.js.map +1 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.mjs +179 -0
- package/dist/admin/future/pages/Assets/components/MainAreaContextMenu.mjs.map +1 -0
- package/dist/admin/future/pages/Assets/constants.js +22 -0
- package/dist/admin/future/pages/Assets/constants.js.map +1 -1
- package/dist/admin/future/pages/Assets/constants.mjs +19 -1
- package/dist/admin/future/pages/Assets/constants.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js +65 -10
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.js.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs +66 -11
- package/dist/admin/future/pages/Assets/hooks/useInfiniteAssets.mjs.map +1 -1
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.js +111 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.js.map +1 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.mjs +109 -0
- package/dist/admin/future/pages/Assets/hooks/useListScrollRestoration.mjs.map +1 -0
- package/dist/admin/future/services/api.js +17 -0
- package/dist/admin/future/services/api.js.map +1 -1
- package/dist/admin/future/services/api.mjs +18 -1
- package/dist/admin/future/services/api.mjs.map +1 -1
- package/dist/admin/future/store/uploadProgress.js +31 -22
- package/dist/admin/future/store/uploadProgress.js.map +1 -1
- package/dist/admin/future/store/uploadProgress.mjs +31 -22
- package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
- package/dist/admin/index.js +11 -10
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +11 -10
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/src/future/components/Drawer.d.ts +10 -0
- package/dist/admin/src/future/pages/Assets/components/MainAreaContextMenu.d.ts +28 -0
- package/dist/admin/src/future/pages/Assets/constants.d.ts +20 -0
- package/dist/admin/src/future/pages/Assets/hooks/useInfiniteAssets.d.ts +4 -0
- package/dist/admin/src/future/pages/Assets/hooks/useListScrollRestoration.d.ts +15 -0
- package/dist/admin/src/future/store/uploadProgress.d.ts +14 -20
- package/dist/admin/translations/en.json.js +1 -3
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +1 -3
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/server/bootstrap.js +11 -0
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/bootstrap.mjs +11 -0
- package/dist/server/bootstrap.mjs.map +1 -1
- package/dist/server/controllers/admin-upload.js +31 -1
- package/dist/server/controllers/admin-upload.js.map +1 -1
- package/dist/server/controllers/admin-upload.mjs +31 -1
- package/dist/server/controllers/admin-upload.mjs.map +1 -1
- package/dist/server/controllers/utils/find-entity-and-check-permissions.js +15 -4
- package/dist/server/controllers/utils/find-entity-and-check-permissions.js.map +1 -1
- package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs +15 -4
- package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs.map +1 -1
- package/dist/server/controllers/utils/folders.js +1 -1
- package/dist/server/controllers/utils/folders.js.map +1 -1
- package/dist/server/controllers/utils/folders.mjs +1 -1
- package/dist/server/controllers/utils/folders.mjs.map +1 -1
- package/dist/server/mcp/ambient-instance.js +46 -0
- package/dist/server/mcp/ambient-instance.js.map +1 -0
- package/dist/server/mcp/ambient-instance.mjs +43 -0
- package/dist/server/mcp/ambient-instance.mjs.map +1 -0
- package/dist/server/mcp/handlers/constants.js +60 -0
- package/dist/server/mcp/handlers/constants.js.map +1 -0
- package/dist/server/mcp/handlers/constants.mjs +45 -0
- package/dist/server/mcp/handlers/constants.mjs.map +1 -0
- package/dist/server/mcp/handlers/folder-handlers.js +308 -0
- package/dist/server/mcp/handlers/folder-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/folder-handlers.mjs +303 -0
- package/dist/server/mcp/handlers/folder-handlers.mjs.map +1 -0
- package/dist/server/mcp/handlers/read-handlers.js +134 -0
- package/dist/server/mcp/handlers/read-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/read-handlers.mjs +130 -0
- package/dist/server/mcp/handlers/read-handlers.mjs.map +1 -0
- package/dist/server/mcp/handlers/write-handlers.js +315 -0
- package/dist/server/mcp/handlers/write-handlers.js.map +1 -0
- package/dist/server/mcp/handlers/write-handlers.mjs +311 -0
- package/dist/server/mcp/handlers/write-handlers.mjs.map +1 -0
- package/dist/server/mcp/permissions.js +36 -0
- package/dist/server/mcp/permissions.js.map +1 -0
- package/dist/server/mcp/permissions.mjs +33 -0
- package/dist/server/mcp/permissions.mjs.map +1 -0
- package/dist/server/mcp/register-upload-mcp-tools.js +235 -0
- package/dist/server/mcp/register-upload-mcp-tools.js.map +1 -0
- package/dist/server/mcp/register-upload-mcp-tools.mjs +232 -0
- package/dist/server/mcp/register-upload-mcp-tools.mjs.map +1 -0
- package/dist/server/mcp/sanitizers/sanitize-media.js +97 -0
- package/dist/server/mcp/sanitizers/sanitize-media.js.map +1 -0
- package/dist/server/mcp/sanitizers/sanitize-media.mjs +93 -0
- package/dist/server/mcp/sanitizers/sanitize-media.mjs.map +1 -0
- package/dist/server/mcp/schemas/input-schemas.js +206 -0
- package/dist/server/mcp/schemas/input-schemas.js.map +1 -0
- package/dist/server/mcp/schemas/input-schemas.mjs +190 -0
- package/dist/server/mcp/schemas/input-schemas.mjs.map +1 -0
- package/dist/server/mcp/schemas/output-schemas.js +158 -0
- package/dist/server/mcp/schemas/output-schemas.js.map +1 -0
- package/dist/server/mcp/schemas/output-schemas.mjs +142 -0
- package/dist/server/mcp/schemas/output-schemas.mjs.map +1 -0
- package/dist/server/mcp/utils.js +14 -0
- package/dist/server/mcp/utils.js.map +1 -0
- package/dist/server/mcp/utils.mjs +12 -0
- package/dist/server/mcp/utils.mjs.map +1 -0
- package/dist/server/media-library-default-notice.js +42 -0
- package/dist/server/media-library-default-notice.js.map +1 -0
- package/dist/server/media-library-default-notice.mjs +40 -0
- package/dist/server/media-library-default-notice.mjs.map +1 -0
- package/dist/server/register.js +5 -0
- package/dist/server/register.js.map +1 -1
- package/dist/server/register.mjs +5 -0
- package/dist/server/register.mjs.map +1 -1
- package/dist/server/services/folder.js.map +1 -1
- package/dist/server/services/folder.mjs.map +1 -1
- package/dist/server/services/upload.js +34 -10
- package/dist/server/services/upload.js.map +1 -1
- package/dist/server/services/upload.mjs +34 -10
- package/dist/server/services/upload.mjs.map +1 -1
- package/dist/server/src/bootstrap.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-upload.d.ts +1 -0
- package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
- package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts +14 -1
- package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts.map +1 -1
- package/dist/server/src/controllers/utils/folders.d.ts +2 -1
- package/dist/server/src/controllers/utils/folders.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +10 -2
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/mcp/ambient-instance.d.ts +65 -0
- package/dist/server/src/mcp/ambient-instance.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/constants.d.ts +48 -0
- package/dist/server/src/mcp/handlers/constants.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/folder-handlers.d.ts +49 -0
- package/dist/server/src/mcp/handlers/folder-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/index.d.ts +5 -0
- package/dist/server/src/mcp/handlers/index.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/read-handlers.d.ts +35 -0
- package/dist/server/src/mcp/handlers/read-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/handlers/write-handlers.d.ts +66 -0
- package/dist/server/src/mcp/handlers/write-handlers.d.ts.map +1 -0
- package/dist/server/src/mcp/index.d.ts +3 -0
- package/dist/server/src/mcp/index.d.ts.map +1 -0
- package/dist/server/src/mcp/permissions.d.ts +22 -0
- package/dist/server/src/mcp/permissions.d.ts.map +1 -0
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts +29 -0
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts.map +1 -0
- package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts +63 -0
- package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/index.d.ts +3 -0
- package/dist/server/src/mcp/schemas/index.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/input-schemas.d.ts +148 -0
- package/dist/server/src/mcp/schemas/input-schemas.d.ts.map +1 -0
- package/dist/server/src/mcp/schemas/output-schemas.d.ts +276 -0
- package/dist/server/src/mcp/schemas/output-schemas.d.ts.map +1 -0
- package/dist/server/src/mcp/types.d.ts +19 -0
- package/dist/server/src/mcp/types.d.ts.map +1 -0
- package/dist/server/src/mcp/utils.d.ts +4 -0
- package/dist/server/src/mcp/utils.d.ts.map +1 -0
- package/dist/server/src/media-library-default-notice.d.ts +19 -0
- package/dist/server/src/media-library-default-notice.d.ts.map +1 -0
- package/dist/server/src/register.d.ts.map +1 -1
- package/dist/server/src/services/folder.d.ts +6 -2
- package/dist/server/src/services/folder.d.ts.map +1 -1
- package/dist/server/src/services/index.d.ts +10 -2
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/services/upload.d.ts +10 -1
- package/dist/server/src/services/upload.d.ts.map +1 -1
- package/dist/server/src/types.d.ts +2 -2
- package/dist/server/src/types.d.ts.map +1 -1
- package/dist/server/src/utils/index.d.ts +2 -1
- package/dist/server/src/utils/index.d.ts.map +1 -1
- package/dist/server/utils/index.js +2 -2
- package/dist/server/utils/index.js.map +1 -1
- package/dist/server/utils/index.mjs +2 -2
- package/dist/server/utils/index.mjs.map +1 -1
- package/dist/shared/contracts/files.d.ts +22 -0
- package/dist/shared/contracts/files.d.ts.map +1 -1
- package/package.json +7 -6
- package/dist/admin/future/components/BetaNotice.js +0 -72
- package/dist/admin/future/components/BetaNotice.js.map +0 -1
- package/dist/admin/future/components/BetaNotice.mjs +0 -70
- package/dist/admin/future/components/BetaNotice.mjs.map +0 -1
- package/dist/admin/src/future/components/BetaNotice.d.ts +0 -11
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { errors } from '@strapi/utils';
|
|
2
|
+
import { assertAmbientInstance, getFolderService } from '../ambient-instance.mjs';
|
|
3
|
+
import { FOLDER_MODEL_UID, FILE_MODEL_UID, ACTIONS } from '../../constants.mjs';
|
|
4
|
+
import { isFolderOrChild } from '../../controllers/utils/folders.mjs';
|
|
5
|
+
import { assertMediaPermission } from '../permissions.mjs';
|
|
6
|
+
import { sanitizeMediaFolder } from '../sanitizers/sanitize-media.mjs';
|
|
7
|
+
import { MCP_NOT_FOUND_FOLDER, MCP_DELETE_FOLDER_UNRESOLVED_IDS, MCP_PARENT_FOLDER_NOT_FOUND, MCP_FOLDER_NAME_TAKEN, MCP_FOLDER_MOVE_INTO_SELF } from './constants.mjs';
|
|
8
|
+
import { ok } from '../utils.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `withParent` populates the parent relation in the same round trip. Rename needs the parent id
|
|
12
|
+
* to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the
|
|
13
|
+
* subtree and cascade callers do not, and skip the join.
|
|
14
|
+
*/ const findFolderById = async (strapi, id, { withParent = false } = {})=>strapi.db.query(FOLDER_MODEL_UID).findOne({
|
|
15
|
+
select: [
|
|
16
|
+
'id',
|
|
17
|
+
'name',
|
|
18
|
+
'path'
|
|
19
|
+
],
|
|
20
|
+
where: {
|
|
21
|
+
id
|
|
22
|
+
},
|
|
23
|
+
...withParent ? {
|
|
24
|
+
populate: {
|
|
25
|
+
parent: {
|
|
26
|
+
select: [
|
|
27
|
+
'id'
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} : {}
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Re-reads a folder with its parent populated, so the write tools can echo back the same shape
|
|
35
|
+
* regardless of what the service returned.
|
|
36
|
+
*
|
|
37
|
+
* `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename
|
|
38
|
+
* it never touches the join table at all — so the parent has to be read back to be reported.
|
|
39
|
+
*/ const readFolderForOutput = async (strapi, id, fallback)=>{
|
|
40
|
+
const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({
|
|
41
|
+
where: {
|
|
42
|
+
id
|
|
43
|
+
},
|
|
44
|
+
populate: {
|
|
45
|
+
parent: {
|
|
46
|
+
select: [
|
|
47
|
+
'id',
|
|
48
|
+
'name'
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// The write succeeded, so report it even if the read-back comes back empty rather than
|
|
54
|
+
// failing a completed operation on a follow-up query.
|
|
55
|
+
return sanitizeMediaFolder(row ?? fallback);
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Replicates the admin folder controller's `parent` check: a named parent must exist.
|
|
59
|
+
*
|
|
60
|
+
* null is the media library root and always valid, so only a numeric id is looked up.
|
|
61
|
+
*/ const assertParentExists = async (strapi, parent)=>{
|
|
62
|
+
if (parent === null || parent === undefined) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const exists = await getFolderService(strapi).exists({
|
|
66
|
+
id: parent
|
|
67
|
+
});
|
|
68
|
+
if (!exists) {
|
|
69
|
+
throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique
|
|
74
|
+
* among its siblings.
|
|
75
|
+
*
|
|
76
|
+
* `excludeId` is the folder being renamed — without it a rename to the folder's current name
|
|
77
|
+
* would collide with itself.
|
|
78
|
+
*/ const assertNameAvailable = async (strapi, name, parent, excludeId)=>{
|
|
79
|
+
const filters = {
|
|
80
|
+
name,
|
|
81
|
+
parent: parent ?? null
|
|
82
|
+
};
|
|
83
|
+
if (excludeId !== undefined) {
|
|
84
|
+
filters.id = {
|
|
85
|
+
$ne: excludeId
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (await getFolderService(strapi).exists(filters)) {
|
|
89
|
+
throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Replicates `dont-move-inside-self`: a folder cannot become its own descendant.
|
|
94
|
+
*
|
|
95
|
+
* Without this the folder service would rewrite the subtree's materialized paths into a cycle,
|
|
96
|
+
* orphaning every descendant — the tree has no root to walk back to afterwards.
|
|
97
|
+
*/ const assertNotMovedIntoOwnSubtree = async (strapi, folder, parent)=>{
|
|
98
|
+
if (parent === null) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const destination = await findFolderById(strapi, parent);
|
|
102
|
+
// A missing destination is already reported by `assertParentExists`.
|
|
103
|
+
if (destination === null) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isFolderOrChild(destination, folder)) {
|
|
107
|
+
throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Counts the folders and files a `deleteByIds` of these paths would remove.
|
|
112
|
+
*
|
|
113
|
+
* Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the
|
|
114
|
+
* `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on
|
|
115
|
+
* rather than an independent estimate that could drift from it.
|
|
116
|
+
*/ const countCascade = async (strapi, paths)=>{
|
|
117
|
+
const pathPredicate = (field)=>({
|
|
118
|
+
$or: paths.flatMap((path)=>[
|
|
119
|
+
{
|
|
120
|
+
[field]: {
|
|
121
|
+
$eq: path
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
[field]: {
|
|
126
|
+
$startsWith: `${path}/`
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
])
|
|
130
|
+
});
|
|
131
|
+
const [totalFolderNumber, totalFileNumber] = await Promise.all([
|
|
132
|
+
strapi.db.query(FOLDER_MODEL_UID).count({
|
|
133
|
+
where: pathPredicate('path')
|
|
134
|
+
}),
|
|
135
|
+
strapi.db.query(FILE_MODEL_UID).count({
|
|
136
|
+
where: pathPredicate('folderPath')
|
|
137
|
+
})
|
|
138
|
+
]);
|
|
139
|
+
return {
|
|
140
|
+
totalFolderNumber,
|
|
141
|
+
totalFileNumber
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* `media_create_folder` — a new folder, optionally nested.
|
|
146
|
+
*
|
|
147
|
+
* Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same
|
|
148
|
+
* permission, the same uniqueness and parent-existence validation, the same `folder.create()`
|
|
149
|
+
* call, and the same `createdBy` attribution from the session user.
|
|
150
|
+
*
|
|
151
|
+
* `create` rather than the `update` the other folder tools use: creating a folder is the one
|
|
152
|
+
* folder operation whose admin route gates on `assets.create`.
|
|
153
|
+
*/ const createMediaCreateFolderHandler = (strapi, context)=>{
|
|
154
|
+
assertAmbientInstance(strapi);
|
|
155
|
+
return async ({ args })=>{
|
|
156
|
+
const { name, parent = null } = args;
|
|
157
|
+
assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);
|
|
158
|
+
await assertParentExists(strapi, parent);
|
|
159
|
+
await assertNameAvailable(strapi, name, parent);
|
|
160
|
+
const created = await getFolderService(strapi).create({
|
|
161
|
+
name,
|
|
162
|
+
parent
|
|
163
|
+
}, {
|
|
164
|
+
user: context.user
|
|
165
|
+
});
|
|
166
|
+
return ok({
|
|
167
|
+
data: await readFolderForOutput(strapi, created.id, created)
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* `media_rename_folder` — changes a folder's name and nothing else.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately does not forward `parent` to `folder.update()`: the service branches on
|
|
175
|
+
* `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant
|
|
176
|
+
* paths. Passing the folder's existing parent would take the move branch and rewrite the whole
|
|
177
|
+
* subtree to compute the identical paths.
|
|
178
|
+
*/ const createMediaRenameFolderHandler = (strapi, context)=>{
|
|
179
|
+
assertAmbientInstance(strapi);
|
|
180
|
+
return async ({ args })=>{
|
|
181
|
+
const { id, name } = args;
|
|
182
|
+
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
183
|
+
const folder = await findFolderById(strapi, id, {
|
|
184
|
+
withParent: true
|
|
185
|
+
});
|
|
186
|
+
if (folder === null) {
|
|
187
|
+
throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);
|
|
188
|
+
}
|
|
189
|
+
// Siblings are the folders sharing this folder's parent, so uniqueness is checked against
|
|
190
|
+
// the current location — a rename never changes it.
|
|
191
|
+
const parentId = folder.parent?.id ?? null;
|
|
192
|
+
await assertNameAvailable(strapi, name, parentId, id);
|
|
193
|
+
// `parent` is omitted on purpose — see the note above.
|
|
194
|
+
const renamed = await getFolderService(strapi).update(id, {
|
|
195
|
+
name
|
|
196
|
+
}, {
|
|
197
|
+
user: context.user
|
|
198
|
+
});
|
|
199
|
+
return ok({
|
|
200
|
+
data: await readFolderForOutput(strapi, id, renamed ?? {
|
|
201
|
+
...folder,
|
|
202
|
+
name
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* `media_move_folder` — re-parents a folder, carrying its whole subtree with it.
|
|
209
|
+
*
|
|
210
|
+
* `folder.update()` recalculates the materialized `path` of every descendant folder and the
|
|
211
|
+
* `folderPath` of every contained file inside a transaction, so the move is atomic and no
|
|
212
|
+
* separate bookkeeping is needed here.
|
|
213
|
+
*/ const createMediaMoveFolderHandler = (strapi, context)=>{
|
|
214
|
+
assertAmbientInstance(strapi);
|
|
215
|
+
return async ({ args })=>{
|
|
216
|
+
const { id, parent } = args;
|
|
217
|
+
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
218
|
+
const folder = await findFolderById(strapi, id);
|
|
219
|
+
if (folder === null) {
|
|
220
|
+
throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);
|
|
221
|
+
}
|
|
222
|
+
await assertParentExists(strapi, parent);
|
|
223
|
+
await assertNotMovedIntoOwnSubtree(strapi, folder, parent);
|
|
224
|
+
// A folder keeps its name across a move, so uniqueness must hold in the destination.
|
|
225
|
+
await assertNameAvailable(strapi, folder.name, parent, id);
|
|
226
|
+
const moved = await getFolderService(strapi).update(id, {
|
|
227
|
+
name: folder.name,
|
|
228
|
+
parent
|
|
229
|
+
}, {
|
|
230
|
+
user: context.user
|
|
231
|
+
});
|
|
232
|
+
return ok({
|
|
233
|
+
data: await readFolderForOutput(strapi, id, moved ?? folder)
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* `media_delete_folder` — previews or performs a cascading folder deletion.
|
|
239
|
+
*
|
|
240
|
+
* Two branches behind one tool, because the preview and the deletion must agree on what the
|
|
241
|
+
* cascade covers; splitting them across tools would let the two drift apart, and an agent could
|
|
242
|
+
* reach the destructive one without ever seeing a count.
|
|
243
|
+
*
|
|
244
|
+
* `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting
|
|
245
|
+
* takes an explicit `dryRun: false`.
|
|
246
|
+
*/ const createMediaDeleteFolderHandler = (strapi, context)=>{
|
|
247
|
+
assertAmbientInstance(strapi);
|
|
248
|
+
return async ({ args })=>{
|
|
249
|
+
const { ids, dryRun = true } = args;
|
|
250
|
+
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
251
|
+
const matched = await strapi.db.query(FOLDER_MODEL_UID).findMany({
|
|
252
|
+
select: [
|
|
253
|
+
'id',
|
|
254
|
+
'name',
|
|
255
|
+
'path'
|
|
256
|
+
],
|
|
257
|
+
where: {
|
|
258
|
+
id: {
|
|
259
|
+
$in: ids
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
/**
|
|
264
|
+
* All-or-nothing: any id that does not resolve to a folder rejects the whole call, before
|
|
265
|
+
* anything is deleted.
|
|
266
|
+
*
|
|
267
|
+
* Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the
|
|
268
|
+
* mistake this tool exists to catch — and deleting the folders that did match while
|
|
269
|
+
* reporting the rest back would be exactly the silent confusion that requirement forbids,
|
|
270
|
+
* with the cascade already gone by the time the agent reads the response.
|
|
271
|
+
*
|
|
272
|
+
* An unresolvable id cannot be diagnosed further from here (an asset id and a deleted
|
|
273
|
+
* folder id look identical), and it does not need to be: either way it is unusable.
|
|
274
|
+
*
|
|
275
|
+
* The dry run rejects on the same rule. A preview that reported a cascade for a request the
|
|
276
|
+
* executing call would refuse is worse than no preview — it is a confirmation an agent
|
|
277
|
+
* cannot act on.
|
|
278
|
+
*/ const matchedIds = new Set(matched.map((folder)=>folder.id));
|
|
279
|
+
const unresolvedIds = ids.filter((id)=>matchedIds.has(id) === false);
|
|
280
|
+
if (unresolvedIds.length > 0) {
|
|
281
|
+
throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));
|
|
282
|
+
}
|
|
283
|
+
const folders = matched.map((folder)=>sanitizeMediaFolder(folder));
|
|
284
|
+
if (dryRun) {
|
|
285
|
+
const counts = await countCascade(strapi, matched.map((folder)=>folder.path));
|
|
286
|
+
return ok({
|
|
287
|
+
dryRun: true,
|
|
288
|
+
folders,
|
|
289
|
+
...counts
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(matched.map((folder)=>folder.id));
|
|
293
|
+
return ok({
|
|
294
|
+
dryRun: false,
|
|
295
|
+
folders,
|
|
296
|
+
totalFolderNumber,
|
|
297
|
+
totalFileNumber
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export { createMediaCreateFolderHandler, createMediaDeleteFolderHandler, createMediaMoveFolderHandler, createMediaRenameFolderHandler };
|
|
303
|
+
//# sourceMappingURL=folder-handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-handlers.mjs","sources":["../../../../server/src/mcp/handlers/folder-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { assertAmbientInstance, getFolderService } from '../ambient-instance';\nimport { ACTIONS, FOLDER_MODEL_UID, FILE_MODEL_UID } from '../../constants';\nimport { isFolderOrChild } from '../../controllers/utils/folders';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaFolder } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_FOLDER,\n MCP_FOLDER_NAME_TAKEN,\n MCP_PARENT_FOLDER_NOT_FOUND,\n MCP_FOLDER_MOVE_INTO_SELF,\n MCP_DELETE_FOLDER_UNRESOLVED_IDS,\n} from './constants';\nimport { ok } from '../utils';\n\nimport type { Folder } from '../../types';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's strict Zod\n// input schema before the handler runs, so unknown keys never reach here.\ntype CreateFolderArgs = {\n name: string;\n parent?: number | null;\n};\n\ntype RenameFolderArgs = {\n id: number;\n name: string;\n};\n\ntype MoveFolderArgs = {\n id: number;\n parent: number | null;\n};\n\ntype DeleteFolderArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The folder fields the handlers need; `path` drives the cascade and subtree checks. */\ntype FolderRow = Pick<Folder, 'id' | 'name' | 'path'>;\n\n/**\n * `withParent` populates the parent relation in the same round trip. Rename needs the parent id\n * to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the\n * subtree and cascade callers do not, and skip the join.\n */\nconst findFolderById = async (\n strapi: Core.Strapi,\n id: number,\n { withParent = false }: { withParent?: boolean } = {}\n): Promise<(FolderRow & { parent?: { id: number } | null }) | null> =>\n strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name', 'path'],\n where: { id },\n ...(withParent ? { populate: { parent: { select: ['id'] } } } : {}),\n });\n\n/**\n * Re-reads a folder with its parent populated, so the write tools can echo back the same shape\n * regardless of what the service returned.\n *\n * `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename\n * it never touches the join table at all — so the parent has to be read back to be reported.\n */\nconst readFolderForOutput = async (\n strapi: Core.Strapi,\n id: number,\n fallback: Record<string, unknown>\n) => {\n const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n where: { id },\n populate: { parent: { select: ['id', 'name'] } },\n });\n\n // The write succeeded, so report it even if the read-back comes back empty rather than\n // failing a completed operation on a follow-up query.\n return sanitizeMediaFolder(row ?? fallback);\n};\n\n/**\n * Replicates the admin folder controller's `parent` check: a named parent must exist.\n *\n * null is the media library root and always valid, so only a numeric id is looked up.\n */\nconst assertParentExists = async (strapi: Core.Strapi, parent: number | null | undefined) => {\n if (parent === null || parent === undefined) {\n return;\n }\n\n const exists = await getFolderService(strapi).exists({ id: parent });\n\n if (!exists) {\n throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);\n }\n};\n\n/**\n * Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique\n * among its siblings.\n *\n * `excludeId` is the folder being renamed — without it a rename to the folder's current name\n * would collide with itself.\n */\nconst assertNameAvailable = async (\n strapi: Core.Strapi,\n name: string,\n parent: number | null,\n excludeId?: number\n) => {\n const filters: Record<string, unknown> = { name, parent: parent ?? null };\n\n if (excludeId !== undefined) {\n filters.id = { $ne: excludeId };\n }\n\n if (await getFolderService(strapi).exists(filters)) {\n throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);\n }\n};\n\n/**\n * Replicates `dont-move-inside-self`: a folder cannot become its own descendant.\n *\n * Without this the folder service would rewrite the subtree's materialized paths into a cycle,\n * orphaning every descendant — the tree has no root to walk back to afterwards.\n */\nconst assertNotMovedIntoOwnSubtree = async (\n strapi: Core.Strapi,\n folder: FolderRow,\n parent: number | null\n) => {\n if (parent === null) {\n return;\n }\n\n const destination = await findFolderById(strapi, parent);\n\n // A missing destination is already reported by `assertParentExists`.\n if (destination === null) {\n return;\n }\n\n if (isFolderOrChild(destination, folder)) {\n throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);\n }\n};\n\n/**\n * Counts the folders and files a `deleteByIds` of these paths would remove.\n *\n * Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the\n * `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on\n * rather than an independent estimate that could drift from it.\n */\nconst countCascade = async (strapi: Core.Strapi, paths: string[]) => {\n const pathPredicate = (field: string) => ({\n $or: paths.flatMap((path) => [\n { [field]: { $eq: path } },\n { [field]: { $startsWith: `${path}/` } },\n ]),\n });\n\n const [totalFolderNumber, totalFileNumber] = await Promise.all([\n strapi.db.query(FOLDER_MODEL_UID).count({ where: pathPredicate('path') }),\n strapi.db.query(FILE_MODEL_UID).count({ where: pathPredicate('folderPath') }),\n ]);\n\n return { totalFolderNumber, totalFileNumber };\n};\n\n/**\n * `media_create_folder` — a new folder, optionally nested.\n *\n * Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same\n * permission, the same uniqueness and parent-existence validation, the same `folder.create()`\n * call, and the same `createdBy` attribution from the session user.\n *\n * `create` rather than the `update` the other folder tools use: creating a folder is the one\n * folder operation whose admin route gates on `assets.create`.\n */\nexport const createMediaCreateFolderHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { name, parent = null } = args as CreateFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);\n\n await assertParentExists(strapi, parent);\n await assertNameAvailable(strapi, name, parent);\n\n const created = await getFolderService(strapi).create({ name, parent }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, created.id, created) });\n };\n};\n\n/**\n * `media_rename_folder` — changes a folder's name and nothing else.\n *\n * Deliberately does not forward `parent` to `folder.update()`: the service branches on\n * `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant\n * paths. Passing the folder's existing parent would take the move branch and rewrite the whole\n * subtree to compute the identical paths.\n */\nexport const createMediaRenameFolderHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, name } = args as RenameFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id, { withParent: true });\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n // Siblings are the folders sharing this folder's parent, so uniqueness is checked against\n // the current location — a rename never changes it.\n const parentId = folder.parent?.id ?? null;\n\n await assertNameAvailable(strapi, name, parentId, id);\n\n // `parent` is omitted on purpose — see the note above.\n const renamed = await getFolderService(strapi).update(id, { name }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, id, renamed ?? { ...folder, name }) });\n };\n};\n\n/**\n * `media_move_folder` — re-parents a folder, carrying its whole subtree with it.\n *\n * `folder.update()` recalculates the materialized `path` of every descendant folder and the\n * `folderPath` of every contained file inside a transaction, so the move is atomic and no\n * separate bookkeeping is needed here.\n */\nexport const createMediaMoveFolderHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, parent } = args as MoveFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id);\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n await assertParentExists(strapi, parent);\n await assertNotMovedIntoOwnSubtree(strapi, folder, parent);\n\n // A folder keeps its name across a move, so uniqueness must hold in the destination.\n await assertNameAvailable(strapi, folder.name, parent, id);\n\n const moved = await getFolderService(strapi).update(\n id,\n { name: folder.name, parent },\n { user: context.user }\n );\n\n return ok({ data: await readFolderForOutput(strapi, id, moved ?? folder) });\n };\n};\n\n/**\n * `media_delete_folder` — previews or performs a cascading folder deletion.\n *\n * Two branches behind one tool, because the preview and the deletion must agree on what the\n * cascade covers; splitting them across tools would let the two drift apart, and an agent could\n * reach the destructive one without ever seeing a count.\n *\n * `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting\n * takes an explicit `dryRun: false`.\n */\nexport const createMediaDeleteFolderHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as DeleteFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const matched: FolderRow[] = await strapi.db.query(FOLDER_MODEL_UID).findMany({\n select: ['id', 'name', 'path'],\n where: { id: { $in: ids } },\n });\n\n /**\n * All-or-nothing: any id that does not resolve to a folder rejects the whole call, before\n * anything is deleted.\n *\n * Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the\n * mistake this tool exists to catch — and deleting the folders that did match while\n * reporting the rest back would be exactly the silent confusion that requirement forbids,\n * with the cascade already gone by the time the agent reads the response.\n *\n * An unresolvable id cannot be diagnosed further from here (an asset id and a deleted\n * folder id look identical), and it does not need to be: either way it is unusable.\n *\n * The dry run rejects on the same rule. A preview that reported a cascade for a request the\n * executing call would refuse is worse than no preview — it is a confirmation an agent\n * cannot act on.\n */\n const matchedIds = new Set(matched.map((folder) => folder.id));\n const unresolvedIds = ids.filter((id) => matchedIds.has(id) === false);\n\n if (unresolvedIds.length > 0) {\n throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));\n }\n\n const folders = matched.map((folder) => sanitizeMediaFolder(folder));\n\n if (dryRun) {\n const counts = await countCascade(\n strapi,\n matched.map((folder) => folder.path)\n );\n\n return ok({ dryRun: true, folders, ...counts });\n }\n\n const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(\n matched.map((folder) => folder.id)\n );\n\n return ok({\n dryRun: false,\n folders,\n totalFolderNumber,\n totalFileNumber,\n });\n };\n};\n"],"names":["findFolderById","strapi","id","withParent","db","query","FOLDER_MODEL_UID","findOne","select","where","populate","parent","readFolderForOutput","fallback","row","sanitizeMediaFolder","assertParentExists","undefined","exists","getFolderService","errors","ValidationError","MCP_PARENT_FOLDER_NOT_FOUND","assertNameAvailable","name","excludeId","filters","$ne","MCP_FOLDER_NAME_TAKEN","assertNotMovedIntoOwnSubtree","folder","destination","isFolderOrChild","MCP_FOLDER_MOVE_INTO_SELF","countCascade","paths","pathPredicate","field","$or","flatMap","path","$eq","$startsWith","totalFolderNumber","totalFileNumber","Promise","all","count","FILE_MODEL_UID","createMediaCreateFolderHandler","context","assertAmbientInstance","args","assertMediaPermission","ACTIONS","create","created","user","ok","data","createMediaRenameFolderHandler","update","NotFoundError","MCP_NOT_FOUND_FOLDER","parentId","renamed","createMediaMoveFolderHandler","moved","createMediaDeleteFolderHandler","ids","dryRun","matched","findMany","$in","matchedIds","Set","map","unresolvedIds","filter","has","length","MCP_DELETE_FOLDER_UNRESOLVED_IDS","folders","counts","deleteByIds"],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils$1 = require('@strapi/utils');
|
|
4
|
+
var index = require('../../utils/index.js');
|
|
5
|
+
var ambientInstance = require('../ambient-instance.js');
|
|
6
|
+
var constants = require('../../constants.js');
|
|
7
|
+
var permissions = require('../permissions.js');
|
|
8
|
+
var findEntityAndCheckPermissions = require('../../controllers/utils/find-entity-and-check-permissions.js');
|
|
9
|
+
var sanitizeMedia = require('../sanitizers/sanitize-media.js');
|
|
10
|
+
var constants$1 = require('./constants.js');
|
|
11
|
+
var utils = require('../utils.js');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SORT = 'createdAt:DESC';
|
|
14
|
+
/**
|
|
15
|
+
* Builds the `filters` clause for `media_list_assets`.
|
|
16
|
+
*
|
|
17
|
+
* `folderId: null` is meaningfully different from an omitted `folderId`: null means "assets at
|
|
18
|
+
* the media library root" (no folder relation), while omitting it means "any folder".
|
|
19
|
+
*
|
|
20
|
+
* `mime` accepts both a full type and a bare prefix. An exact match on "image" would never hit,
|
|
21
|
+
* so a value without a slash is matched as a prefix ("image" → every "image/*").
|
|
22
|
+
*/ const buildAssetFilters = (args)=>{
|
|
23
|
+
const filters = {};
|
|
24
|
+
if (args.folderId === null) {
|
|
25
|
+
filters.folder = null;
|
|
26
|
+
} else if (args.folderId !== undefined) {
|
|
27
|
+
filters.folder = {
|
|
28
|
+
id: args.folderId
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (args.mime !== undefined) {
|
|
32
|
+
filters.mime = args.mime.includes('/') ? {
|
|
33
|
+
$eqi: args.mime
|
|
34
|
+
} : {
|
|
35
|
+
$startsWithi: `${args.mime}/`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (args.name !== undefined) {
|
|
39
|
+
filters.name = {
|
|
40
|
+
$containsi: args.name
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return filters;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `media_list_assets` — paginated, filtered listing of media files.
|
|
47
|
+
*
|
|
48
|
+
* Permission conditions are applied through the permissions manager
|
|
49
|
+
* (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —
|
|
50
|
+
* sees the same subset it would through the admin API.
|
|
51
|
+
*/ const createMediaListAssetsHandler = (strapi, context)=>{
|
|
52
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
53
|
+
return async ({ args })=>{
|
|
54
|
+
const { folderId, mime, name, page, pageSize, sort } = args;
|
|
55
|
+
const pm = permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FILE_MODEL_UID);
|
|
56
|
+
const query = await pm.addPermissionsQueryTo({
|
|
57
|
+
filters: buildAssetFilters({
|
|
58
|
+
folderId,
|
|
59
|
+
mime,
|
|
60
|
+
name
|
|
61
|
+
}),
|
|
62
|
+
sort: sort ?? DEFAULT_SORT,
|
|
63
|
+
page: page ?? 1,
|
|
64
|
+
pageSize: pageSize ?? 25,
|
|
65
|
+
populate: {
|
|
66
|
+
folder: {
|
|
67
|
+
fields: [
|
|
68
|
+
'id',
|
|
69
|
+
'name'
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const { results, pagination } = await index.getService('upload', strapi).findPage(query);
|
|
75
|
+
return utils.ok({
|
|
76
|
+
results: results.map(sanitizeMedia.sanitizeMediaAsset),
|
|
77
|
+
pagination
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* `media_get_asset` — a single asset by numeric id.
|
|
83
|
+
*
|
|
84
|
+
* Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same
|
|
85
|
+
* helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles
|
|
86
|
+
* before building the CASL subject, which is what the admin conditions read. Checking a subject
|
|
87
|
+
* without those fields would deny a conditioned grant its own assets, and would disagree with
|
|
88
|
+
* `media_list_assets`, where the condition is pushed into the query instead.
|
|
89
|
+
*/ const createMediaGetAssetHandler = (strapi, context)=>{
|
|
90
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
91
|
+
return async ({ args })=>{
|
|
92
|
+
const { id } = args;
|
|
93
|
+
// Model-level gate first, so a token without upload read at all is refused before a lookup.
|
|
94
|
+
permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FILE_MODEL_UID);
|
|
95
|
+
let asset;
|
|
96
|
+
try {
|
|
97
|
+
({ file: asset } = await findEntityAndCheckPermissions.findEntityAndCheckPermissions(context.userAbility, constants.ACTIONS.read, constants.FILE_MODEL_UID, id, strapi));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an
|
|
100
|
+
// agent, so restate it with the message the other media tools use.
|
|
101
|
+
if (error instanceof utils$1.errors.NotFoundError) {
|
|
102
|
+
throw new utils$1.errors.NotFoundError(constants$1.MCP_NOT_FOUND_ASSET);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return utils.ok({
|
|
107
|
+
data: sanitizeMedia.sanitizeMediaAsset(asset)
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.
|
|
113
|
+
*
|
|
114
|
+
* `getStructure()` returns the whole tree in one query and has no permission-condition
|
|
115
|
+
* filtering, so this is gated on the model-level read permission only — matching
|
|
116
|
+
* `GET /upload/folder-structure` in the admin API.
|
|
117
|
+
*
|
|
118
|
+
* The factory asserts the handler's instance is the ambient `global.strapi` the folder service
|
|
119
|
+
* actually queries — see `assertAmbientInstance` for why.
|
|
120
|
+
*/ const createMediaListFoldersHandler = (strapi, context)=>{
|
|
121
|
+
ambientInstance.assertAmbientInstance(strapi);
|
|
122
|
+
return async ()=>{
|
|
123
|
+
permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FOLDER_MODEL_UID);
|
|
124
|
+
const structure = await ambientInstance.getFolderService(strapi).getStructure();
|
|
125
|
+
return utils.ok({
|
|
126
|
+
data: sanitizeMedia.sanitizeMediaFolderTree(structure)
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
exports.createMediaGetAssetHandler = createMediaGetAssetHandler;
|
|
132
|
+
exports.createMediaListAssetsHandler = createMediaListAssetsHandler;
|
|
133
|
+
exports.createMediaListFoldersHandler = createMediaListFoldersHandler;
|
|
134
|
+
//# sourceMappingURL=read-handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-handlers.js","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { assertAmbientInstance, getFolderService } from '../ambient-instance';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { assertMediaPermission } from '../permissions';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { sanitizeMediaAsset, sanitizeMediaFolderTree } from '../sanitizers/sanitize-media';\nimport { MCP_NOT_FOUND_ASSET } from './constants';\nimport { ok } from '../utils';\n\nconst DEFAULT_SORT = 'createdAt:DESC';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's Zod input\n// schema before the handler runs, so handlers accept the erased `Record<string, unknown>` the\n// registry hands them and narrow it here.\ntype ListMediaArgs = {\n folderId?: number | null;\n mime?: string;\n name?: string;\n page?: number;\n pageSize?: number;\n sort?: string;\n};\n\ntype GetMediaArgs = {\n id: number;\n};\n\n/**\n * Builds the `filters` clause for `media_list_assets`.\n *\n * `folderId: null` is meaningfully different from an omitted `folderId`: null means \"assets at\n * the media library root\" (no folder relation), while omitting it means \"any folder\".\n *\n * `mime` accepts both a full type and a bare prefix. An exact match on \"image\" would never hit,\n * so a value without a slash is matched as a prefix (\"image\" → every \"image/*\").\n */\nconst buildAssetFilters = (args: ListMediaArgs): Record<string, unknown> => {\n const filters: Record<string, unknown> = {};\n\n if (args.folderId === null) {\n filters.folder = null;\n } else if (args.folderId !== undefined) {\n filters.folder = { id: args.folderId };\n }\n\n if (args.mime !== undefined) {\n filters.mime = args.mime.includes('/')\n ? { $eqi: args.mime }\n : { $startsWithi: `${args.mime}/` };\n }\n\n if (args.name !== undefined) {\n filters.name = { $containsi: args.name };\n }\n\n return filters;\n};\n\n/**\n * `media_list_assets` — paginated, filtered listing of media files.\n *\n * Permission conditions are applied through the permissions manager\n * (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —\n * sees the same subset it would through the admin API.\n */\nexport const createMediaListAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { folderId, mime, name, page, pageSize, sort } = args as ListMediaArgs;\n const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n const query = await pm.addPermissionsQueryTo({\n filters: buildAssetFilters({ folderId, mime, name }),\n sort: sort ?? DEFAULT_SORT,\n page: page ?? 1,\n pageSize: pageSize ?? 25,\n populate: { folder: { fields: ['id', 'name'] } },\n });\n\n const { results, pagination } = await getService('upload', strapi).findPage(query);\n\n return ok({\n results: results.map(sanitizeMediaAsset),\n pagination,\n });\n };\n};\n\n/**\n * `media_get_asset` — a single asset by numeric id.\n *\n * Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same\n * helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles\n * before building the CASL subject, which is what the admin conditions read. Checking a subject\n * without those fields would deny a conditioned grant its own assets, and would disagree with\n * `media_list_assets`, where the condition is pushed into the query instead.\n */\nexport const createMediaGetAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id } = args as GetMediaArgs;\n\n // Model-level gate first, so a token without upload read at all is refused before a lookup.\n assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n let asset;\n try {\n ({ file: asset } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.read,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n // The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an\n // agent, so restate it with the message the other media tools use.\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n throw error;\n }\n\n return ok({ data: sanitizeMediaAsset(asset) });\n };\n};\n\n/**\n * `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.\n *\n * `getStructure()` returns the whole tree in one query and has no permission-condition\n * filtering, so this is gated on the model-level read permission only — matching\n * `GET /upload/folder-structure` in the admin API.\n *\n * The factory asserts the handler's instance is the ambient `global.strapi` the folder service\n * actually queries — see `assertAmbientInstance` for why.\n */\nexport const createMediaListFoldersHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async (): Promise<Modules.MCP.McpToolHandlerReturn> => {\n assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);\n\n const structure = await getFolderService(strapi).getStructure();\n\n return ok({ data: sanitizeMediaFolderTree(structure) });\n };\n};\n"],"names":["DEFAULT_SORT","buildAssetFilters","args","filters","folderId","folder","undefined","id","mime","includes","$eqi","$startsWithi","name","$containsi","createMediaListAssetsHandler","strapi","context","assertAmbientInstance","page","pageSize","sort","pm","assertMediaPermission","ACTIONS","read","FILE_MODEL_UID","query","addPermissionsQueryTo","populate","fields","results","pagination","getService","findPage","ok","map","sanitizeMediaAsset","createMediaGetAssetHandler","asset","file","findEntityAndCheckPermissions","userAbility","error","errors","NotFoundError","MCP_NOT_FOUND_ASSET","data","createMediaListFoldersHandler","FOLDER_MODEL_UID","structure","getFolderService","getStructure","sanitizeMediaFolderTree"],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { errors } from '@strapi/utils';
|
|
2
|
+
import { getService } from '../../utils/index.mjs';
|
|
3
|
+
import { assertAmbientInstance, getFolderService } from '../ambient-instance.mjs';
|
|
4
|
+
import { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants.mjs';
|
|
5
|
+
import { assertMediaPermission } from '../permissions.mjs';
|
|
6
|
+
import { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions.mjs';
|
|
7
|
+
import { sanitizeMediaAsset, sanitizeMediaFolderTree } from '../sanitizers/sanitize-media.mjs';
|
|
8
|
+
import { MCP_NOT_FOUND_ASSET } from './constants.mjs';
|
|
9
|
+
import { ok } from '../utils.mjs';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SORT = 'createdAt:DESC';
|
|
12
|
+
/**
|
|
13
|
+
* Builds the `filters` clause for `media_list_assets`.
|
|
14
|
+
*
|
|
15
|
+
* `folderId: null` is meaningfully different from an omitted `folderId`: null means "assets at
|
|
16
|
+
* the media library root" (no folder relation), while omitting it means "any folder".
|
|
17
|
+
*
|
|
18
|
+
* `mime` accepts both a full type and a bare prefix. An exact match on "image" would never hit,
|
|
19
|
+
* so a value without a slash is matched as a prefix ("image" → every "image/*").
|
|
20
|
+
*/ const buildAssetFilters = (args)=>{
|
|
21
|
+
const filters = {};
|
|
22
|
+
if (args.folderId === null) {
|
|
23
|
+
filters.folder = null;
|
|
24
|
+
} else if (args.folderId !== undefined) {
|
|
25
|
+
filters.folder = {
|
|
26
|
+
id: args.folderId
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (args.mime !== undefined) {
|
|
30
|
+
filters.mime = args.mime.includes('/') ? {
|
|
31
|
+
$eqi: args.mime
|
|
32
|
+
} : {
|
|
33
|
+
$startsWithi: `${args.mime}/`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (args.name !== undefined) {
|
|
37
|
+
filters.name = {
|
|
38
|
+
$containsi: args.name
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return filters;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* `media_list_assets` — paginated, filtered listing of media files.
|
|
45
|
+
*
|
|
46
|
+
* Permission conditions are applied through the permissions manager
|
|
47
|
+
* (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —
|
|
48
|
+
* sees the same subset it would through the admin API.
|
|
49
|
+
*/ const createMediaListAssetsHandler = (strapi, context)=>{
|
|
50
|
+
assertAmbientInstance(strapi);
|
|
51
|
+
return async ({ args })=>{
|
|
52
|
+
const { folderId, mime, name, page, pageSize, sort } = args;
|
|
53
|
+
const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);
|
|
54
|
+
const query = await pm.addPermissionsQueryTo({
|
|
55
|
+
filters: buildAssetFilters({
|
|
56
|
+
folderId,
|
|
57
|
+
mime,
|
|
58
|
+
name
|
|
59
|
+
}),
|
|
60
|
+
sort: sort ?? DEFAULT_SORT,
|
|
61
|
+
page: page ?? 1,
|
|
62
|
+
pageSize: pageSize ?? 25,
|
|
63
|
+
populate: {
|
|
64
|
+
folder: {
|
|
65
|
+
fields: [
|
|
66
|
+
'id',
|
|
67
|
+
'name'
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const { results, pagination } = await getService('upload', strapi).findPage(query);
|
|
73
|
+
return ok({
|
|
74
|
+
results: results.map(sanitizeMediaAsset),
|
|
75
|
+
pagination
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* `media_get_asset` — a single asset by numeric id.
|
|
81
|
+
*
|
|
82
|
+
* Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same
|
|
83
|
+
* helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles
|
|
84
|
+
* before building the CASL subject, which is what the admin conditions read. Checking a subject
|
|
85
|
+
* without those fields would deny a conditioned grant its own assets, and would disagree with
|
|
86
|
+
* `media_list_assets`, where the condition is pushed into the query instead.
|
|
87
|
+
*/ const createMediaGetAssetHandler = (strapi, context)=>{
|
|
88
|
+
assertAmbientInstance(strapi);
|
|
89
|
+
return async ({ args })=>{
|
|
90
|
+
const { id } = args;
|
|
91
|
+
// Model-level gate first, so a token without upload read at all is refused before a lookup.
|
|
92
|
+
assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);
|
|
93
|
+
let asset;
|
|
94
|
+
try {
|
|
95
|
+
({ file: asset } = await findEntityAndCheckPermissions(context.userAbility, ACTIONS.read, FILE_MODEL_UID, id, strapi));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an
|
|
98
|
+
// agent, so restate it with the message the other media tools use.
|
|
99
|
+
if (error instanceof errors.NotFoundError) {
|
|
100
|
+
throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return ok({
|
|
105
|
+
data: sanitizeMediaAsset(asset)
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.
|
|
111
|
+
*
|
|
112
|
+
* `getStructure()` returns the whole tree in one query and has no permission-condition
|
|
113
|
+
* filtering, so this is gated on the model-level read permission only — matching
|
|
114
|
+
* `GET /upload/folder-structure` in the admin API.
|
|
115
|
+
*
|
|
116
|
+
* The factory asserts the handler's instance is the ambient `global.strapi` the folder service
|
|
117
|
+
* actually queries — see `assertAmbientInstance` for why.
|
|
118
|
+
*/ const createMediaListFoldersHandler = (strapi, context)=>{
|
|
119
|
+
assertAmbientInstance(strapi);
|
|
120
|
+
return async ()=>{
|
|
121
|
+
assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);
|
|
122
|
+
const structure = await getFolderService(strapi).getStructure();
|
|
123
|
+
return ok({
|
|
124
|
+
data: sanitizeMediaFolderTree(structure)
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export { createMediaGetAssetHandler, createMediaListAssetsHandler, createMediaListFoldersHandler };
|
|
130
|
+
//# sourceMappingURL=read-handlers.mjs.map
|