From a55158d897276b1a3bc0a0c93b64ff3bb83427f1 Mon Sep 17 00:00:00 2001 From: Dylan Hart Date: Thu, 17 Jul 2025 19:06:11 -0400 Subject: [PATCH 1/3] feat: add folderByPath GraphQL resolver --- server/graph/resolvers/asset.js | 19 +++++++++++++++++++ server/graph/schemas/asset.graphql | 2 ++ 2 files changed, 21 insertions(+) diff --git a/server/graph/resolvers/asset.js b/server/graph/resolvers/asset.js index 91efbdda8..618c3769a 100644 --- a/server/graph/resolvers/asset.js +++ b/server/graph/resolvers/asset.js @@ -41,6 +41,25 @@ module.exports = { const path = parentPath ? `${parentPath}/${r.slug}` : r.slug return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path }) }) + }, + async folderByPath(obj, args, context) { + const parts = args.path.toLowerCase().split('/').filter(Boolean) + let parentId = null + for (const slug of parts) { + const folder = await WIKI.models.assetFolders.query().where({ + parentId: parentId, + slug: slug + }).first() + if (!folder) { + return null + } + const currentPath = [...parts.slice(0, parts.indexOf(slug) + 1)].join('/') + if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: currentPath })) { + throw new WIKI.Error.AssetAccessForbidden() + } + parentId = folder.id + } + return WIKI.models.assetFolders.query().findById(parentId) } }, AssetMutation: { diff --git a/server/graph/schemas/asset.graphql b/server/graph/schemas/asset.graphql index f07f1307f..0bfd344f9 100644 --- a/server/graph/schemas/asset.graphql +++ b/server/graph/schemas/asset.graphql @@ -23,6 +23,8 @@ type AssetQuery { folders( parentFolderId: Int! ): [AssetFolder] @auth(requires: ["manage:system", "read:assets"]) + + folderByPath(path: String!): AssetFolder @auth(requires: ["manage:system", "read:assets"]) } # ----------------------------------------------- From 4ac09525734760797d8998735db44374f87e6308 Mon Sep 17 00:00:00 2001 From: Dylan Hart Date: Thu, 17 Jul 2025 20:44:18 -0400 Subject: [PATCH 2/3] Query database once in folderByPath --- server/graph/resolvers/asset.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/graph/resolvers/asset.js b/server/graph/resolvers/asset.js index 618c3769a..2b11f1846 100644 --- a/server/graph/resolvers/asset.js +++ b/server/graph/resolvers/asset.js @@ -43,23 +43,23 @@ module.exports = { }) }, async folderByPath(obj, args, context) { - const parts = args.path.toLowerCase().split('/').filter(Boolean) + const segments = args.path.toLowerCase().split('/').filter(Boolean) + const allFolders = await WIKI.models.assetFolders.query() let parentId = null - for (const slug of parts) { - const folder = await WIKI.models.assetFolders.query().where({ - parentId: parentId, - slug: slug - }).first() - if (!folder) { + let currentFolder = null + let builtPath = '' + for (const segment of segments) { + currentFolder = allFolders.find(f => + f.parentId === parentId && f.slug === segment + ) + if (!currentFolder) return null + builtPath = builtPath ? `${builtPath}/${segment}` : segment + if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: builtPath })) { return null } - const currentPath = [...parts.slice(0, parts.indexOf(slug) + 1)].join('/') - if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: currentPath })) { - throw new WIKI.Error.AssetAccessForbidden() - } - parentId = folder.id + parentId = currentFolder.id } - return WIKI.models.assetFolders.query().findById(parentId) + return currentFolder } }, AssetMutation: { From 1bf3ae6b61963aa109cdc4ada00fe0b44ca9d514 Mon Sep 17 00:00:00 2001 From: Dylan Hart Date: Sun, 27 Jul 2025 20:50:44 -0400 Subject: [PATCH 3/3] Query asset folder using recursive CTE --- server/graph/resolvers/asset.js | 20 ++++---------------- server/models/assetFolders.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/server/graph/resolvers/asset.js b/server/graph/resolvers/asset.js index 2b11f1846..38c4c5489 100644 --- a/server/graph/resolvers/asset.js +++ b/server/graph/resolvers/asset.js @@ -43,23 +43,11 @@ module.exports = { }) }, async folderByPath(obj, args, context) { - const segments = args.path.toLowerCase().split('/').filter(Boolean) - const allFolders = await WIKI.models.assetFolders.query() - let parentId = null - let currentFolder = null - let builtPath = '' - for (const segment of segments) { - currentFolder = allFolders.find(f => - f.parentId === parentId && f.slug === segment - ) - if (!currentFolder) return null - builtPath = builtPath ? `${builtPath}/${segment}` : segment - if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path: builtPath })) { - return null - } - parentId = currentFolder.id + const path = args.path.toLowerCase() + if (!WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path })) { + return null } - return currentFolder + return WIKI.models.assetFolders.getFolderByPath(path) } }, AssetMutation: { diff --git a/server/models/assetFolders.js b/server/models/assetFolders.js index 4acb496cc..752181638 100644 --- a/server/models/assetFolders.js +++ b/server/models/assetFolders.js @@ -75,4 +75,32 @@ module.exports = class AssetFolder extends Model { }) return folders } + + /** + * Get asset folder by resolving its path from root + * @param {string} path Folder path, e.g. 'abc/def/ghi' + */ + static async getFolderByPath(path) { + const segments = String(path).split('/').filter(Boolean) + if (segments.length === 0) return null + const db = this.knex() + const targetPath = segments.join('/') + let results + if (WIKI.config.db.type === 'mssql') { + results = await db + .with('folder_path', (qb) => { + qb.select('af.*').select(db.raw('CAST(af.slug AS VARCHAR(MAX)) AS current_path')).from('assetFolders as af').whereNull('af.parentId').unionAll((uqb) => { + uqb.select('af.*').select(db.raw("CAST(fp.current_path + '/' + af.slug AS VARCHAR(MAX)) AS current_path")).from('assetFolders as af').join('folder_path as fp', 'af.parentId', 'fp.id') + }) + }).select('*').from('folder_path').where('current_path', targetPath).limit(1) + } else { + results = await db + .withRecursive('folder_path', (qb) => { + qb.select('af.*').select(db.raw('af.slug::TEXT AS current_path')).from('assetFolders as af').whereNull('af.parentId').unionAll((uqb) => { + uqb.select('af.*').select(db.raw("fp.current_path || '/' || af.slug AS current_path")).from('assetFolders as af').join('folder_path as fp', 'af.parentId', 'fp.id') + }, true) + }).select('*').from('folder_path').where('current_path', targetPath).limit(1) + } + return results[0] || null + } }