diff --git a/packages/api-aco/__tests__/record.so.test.ts b/packages/api-aco/__tests__/record.so.test.ts index 56093130130..cfcc4a1f39b 100644 --- a/packages/api-aco/__tests__/record.so.test.ts +++ b/packages/api-aco/__tests__/record.so.test.ts @@ -582,7 +582,7 @@ describe("`search` CRUD", () => { ] }); - // Creating a record with same "id" + // Let's create a record into folder1 const [createResponse] = await search.createRecord({ data: { ...recordMocks.recordA, @@ -592,6 +592,7 @@ describe("`search` CRUD", () => { const record = createResponse.data.search.createRecord.data; + // Let's move the record to folder2 const [moveResponse] = await search.moveRecord({ id: record.id, folderId: folder2.id @@ -608,9 +609,33 @@ describe("`search` CRUD", () => { } }); - const [listMovedRecords] = await search.listRecords(); + // Let's list records for folder1 + const [listFolder1] = await search.listRecords({ + where: { type: "page", location: { folderId: folder1.id } } + }); + + expect(listFolder1).toMatchObject({ + data: { + search: { + listRecords: { + data: [], + error: null, + meta: { + cursor: null, + hasMoreItems: false, + totalCount: 0 + } + } + } + } + }); + + // Let's list records for folder2 + const [listFolder2] = await search.listRecords({ + where: { type: "page", location: { folderId: folder2.id } } + }); - expect(listMovedRecords).toMatchObject({ + expect(listFolder2).toMatchObject({ data: { search: { listRecords: { @@ -633,6 +658,7 @@ describe("`search` CRUD", () => { } }); + // Let's check the record itself const [movedRecord] = await search.getRecord({ id: record.id }); expect(movedRecord).toMatchObject({ data: { diff --git a/packages/api-aco/src/utils/decorators/CmsEntriesCrudDecorators.ts b/packages/api-aco/src/utils/decorators/CmsEntriesCrudDecorators.ts index 274642735b7..a0a0d999802 100644 --- a/packages/api-aco/src/utils/decorators/CmsEntriesCrudDecorators.ts +++ b/packages/api-aco/src/utils/decorators/CmsEntriesCrudDecorators.ts @@ -2,12 +2,28 @@ import { AcoContext } from "~/types"; import { CmsEntry, CmsModel } from "@webiny/api-headless-cms/types"; import { NotFoundError } from "@webiny/handler-graphql"; import { FolderLevelPermissions } from "~/utils/FolderLevelPermissions"; +import { createWhere } from "./where"; +import { ROOT_FOLDER } from "./constants"; type Context = Pick; +/** + * Keep this until we figure out how to fetch the folders. + */ +const isPageModel = (model: CmsModel): boolean => { + if (model.modelId === "pbPage") { + return true; + } else if (model.modelId === "acoSearchRecord-pbpage") { + return true; + } + return false; +}; -const ROOT_FOLDER = "root"; - -const createFolderType = (model: Pick): string => { +const createFolderType = (model: CmsModel): "FmFile" | "PbPage" | `cms:${string}` => { + if (model.modelId === "fmFile") { + return "FmFile"; + } else if (isPageModel(model)) { + return "PbPage"; + } return `cms:${model.modelId}`; }; @@ -60,21 +76,16 @@ export class CmsEntriesCrudDecorators { const originalCmsListEntries = context.cms.listEntries.bind(context.cms); context.cms.listEntries = async (model, params) => { - const folderType = model.modelId === "fmFile" ? "FmFile" : `cms:${model.modelId}`; - const allFolders = await folderLevelPermissions.listAllFoldersWithPermissions( - folderType - ); + const folderType = createFolderType(model); + const folders = await folderLevelPermissions.listAllFoldersWithPermissions(folderType); + const where = createWhere({ + where: params.where, + folders + }); return originalCmsListEntries(model, { ...params, - where: { - ...(params?.where || {}), - wbyAco_location: { - // At the moment, all users can access entries in the root folder. - // Root folder level permissions cannot be set yet. - folderId_in: [ROOT_FOLDER, ...allFolders.map(folder => folder.id)] - } - } + where }); }; @@ -128,11 +139,31 @@ export class CmsEntriesCrudDecorators { }; const originalCmsCreateEntry = context.cms.createEntry.bind(context.cms); - context.cms.createEntry = async (model, params) => { + context.cms.createEntry = async (model, params, options) => { const folderId = params.wbyAco_location?.folderId || params.location?.folderId; if (!folderId || folderId === ROOT_FOLDER) { - return originalCmsCreateEntry(model, params); + return originalCmsCreateEntry(model, params, options); + } + + const folder = await context.aco.folder.get(folderId); + await folderLevelPermissions.ensureCanAccessFolderContent({ + folder, + rwd: "w" + }); + + return originalCmsCreateEntry(model, params, options); + }; + + const originalCmsCreateFromEntry = context.cms.createEntryRevisionFrom.bind(context.cms); + context.cms.createEntryRevisionFrom = async (model, id, input, options) => { + const entry = await context.cms.storageOperations.entries.getRevisionById(model, { + id + }); + + const folderId = entry?.location?.folderId; + if (!folderId || folderId === ROOT_FOLDER) { + return originalCmsCreateFromEntry(model, id, input, options); } const folder = await context.aco.folder.get(folderId); @@ -141,18 +172,18 @@ export class CmsEntriesCrudDecorators { rwd: "w" }); - return originalCmsCreateEntry(model, params); + return originalCmsCreateFromEntry(model, id, input, options); }; const originalCmsUpdateEntry = context.cms.updateEntry.bind(context.cms); - context.cms.updateEntry = async (model, id, input, meta) => { + context.cms.updateEntry = async (model, id, input, meta, options) => { const entry = await context.cms.storageOperations.entries.getRevisionById(model, { id }); const folderId = entry?.location?.folderId; if (!folderId || folderId === ROOT_FOLDER) { - return originalCmsUpdateEntry(model, id, input, meta); + return originalCmsUpdateEntry(model, id, input, meta, options); } const folder = await context.aco.folder.get(folderId); @@ -161,18 +192,18 @@ export class CmsEntriesCrudDecorators { rwd: "w" }); - return originalCmsUpdateEntry(model, id, input, meta); + return originalCmsUpdateEntry(model, id, input, meta, options); }; const originalCmsDeleteEntry = context.cms.deleteEntry.bind(context.cms); - context.cms.deleteEntry = async (model, id) => { + context.cms.deleteEntry = async (model, id, options) => { const entry = await context.cms.storageOperations.entries.getRevisionById(model, { id }); const folderId = entry?.location?.folderId; if (!folderId || folderId === ROOT_FOLDER) { - return originalCmsDeleteEntry(model, id); + return originalCmsDeleteEntry(model, id, options); } const folder = await context.aco.folder.get(folderId); @@ -181,7 +212,7 @@ export class CmsEntriesCrudDecorators { rwd: "d" }); - return originalCmsDeleteEntry(model, id); + return originalCmsDeleteEntry(model, id, options); }; const originalCmsDeleteEntryRevision = context.cms.deleteEntryRevision.bind(context.cms); @@ -203,5 +234,43 @@ export class CmsEntriesCrudDecorators { return originalCmsDeleteEntryRevision(model, id); }; + + const originalCmsMoveEntry = context.cms.moveEntry.bind(context.cms); + context.cms.moveEntry = async (model, id, targetFolderId) => { + /** + * First we need to check if user has access to the entries existing folder. + */ + const entry = await context.cms.storageOperations.entries.getRevisionById(model, { + id + }); + const folderId = entry?.location?.folderId || ROOT_FOLDER; + /** + * If the entry is in the same folder we are trying to move it to, just continue. + */ + if (folderId === targetFolderId) { + return originalCmsMoveEntry(model, id, targetFolderId); + } else if (folderId !== ROOT_FOLDER) { + /** + * If entry current folder is not a root, check for access + */ + const folder = await context.aco.folder.get(folderId); + await folderLevelPermissions.ensureCanAccessFolderContent({ + folder, + rwd: "w" + }); + } + /** + * If target folder is not a ROOT_FOLDER, check for access. + */ + if (targetFolderId !== ROOT_FOLDER) { + const folder = await context.aco.folder.get(targetFolderId); + await folderLevelPermissions.ensureCanAccessFolderContent({ + folder, + rwd: "w" + }); + } + + return originalCmsMoveEntry(model, id, targetFolderId); + }; } } diff --git a/packages/api-aco/src/utils/decorators/constants.ts b/packages/api-aco/src/utils/decorators/constants.ts new file mode 100644 index 00000000000..4347c18a4df --- /dev/null +++ b/packages/api-aco/src/utils/decorators/constants.ts @@ -0,0 +1 @@ +export const ROOT_FOLDER = "root"; diff --git a/packages/api-aco/src/utils/decorators/where.ts b/packages/api-aco/src/utils/decorators/where.ts new file mode 100644 index 00000000000..67817d8bce8 --- /dev/null +++ b/packages/api-aco/src/utils/decorators/where.ts @@ -0,0 +1,53 @@ +import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; +import { Folder } from "~/folder/folder.types"; +import { ROOT_FOLDER } from "./constants"; + +interface Params { + where: CmsEntryListWhere | undefined; + folders: Folder[]; +} + +/** + * There are multiple cases that we need to handle: + * * existing location with no AND conditional + * * existing location with AND conditional + * * no existing location with no AND conditional + with AND conditional + */ +export const createWhere = (params: Params): CmsEntryListWhere | undefined => { + const { where, folders } = params; + if (!where) { + return undefined; + } + const whereLocation = { + wbyAco_location: { + // At the moment, all users can access entries in the root folder. + // Root folder level permissions cannot be set yet. + folderId_in: [ROOT_FOLDER, ...folders.map(folder => folder.id)] + } + }; + const whereAnd = where.AND; + if (where.wbyAco_location && !whereAnd) { + return { + ...where, + AND: [ + { + ...whereLocation + } + ] + }; + } else if (where.wbyAco_location && whereAnd) { + return { + ...where, + AND: [ + { + ...whereLocation + }, + ...whereAnd + ] + }; + } + return { + ...where, + ...whereLocation + }; +}; diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 0dd42bf1180..9c67b97ca65 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -1056,6 +1056,12 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm } const entry = await entryFromStorageTransform(context, model, originalStorageEntry); + /** + * No need to continue if the entry is already in the requested folder. + */ + if (entry.location?.folderId === folderId) { + return entry; + } try { await onEntryBeforeMove.publish({ diff --git a/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveUpdate.ts b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveUpdate.ts index 212a56360df..facf5bb9932 100644 --- a/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveUpdate.ts +++ b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolveUpdate.ts @@ -14,7 +14,7 @@ type ResolveUpdate = ResolverFactory; export const resolveUpdate: ResolveUpdate = ({ model }) => - async (_, args: any, context) => { + async (_, args, context) => { try { const entry = await context.cms.updateEntry( model, diff --git a/packages/api-headless-cms/src/types.ts b/packages/api-headless-cms/src/types.ts index 0ba58f17c5f..b166675c6fb 100644 --- a/packages/api-headless-cms/src/types.ts +++ b/packages/api-headless-cms/src/types.ts @@ -1837,6 +1837,17 @@ export interface CmsEntryListWhere { * @internal */ latest?: boolean; + /** + * ACO related parameters. + */ + wbyAco_location?: { + folderId?: string; + folderId_not?: string; + folderId_in?: string[]; + folderId_not_in?: string[]; + AND?: CmsEntryListWhere[]; + OR?: CmsEntryListWhere[]; + }; /** * This is to allow querying by any content model field defined by the user. */