Skip to content

Commit

Permalink
fix(api-aco): flp entry decorator (#3664)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric committed Nov 2, 2023
1 parent bad0068 commit 607b8af
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 28 deletions.
32 changes: 29 additions & 3 deletions packages/api-aco/__tests__/record.so.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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: {
Expand All @@ -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: {
Expand Down
117 changes: 93 additions & 24 deletions packages/api-aco/src/utils/decorators/CmsEntriesCrudDecorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AcoContext, "aco" | "cms">;
/**
* 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<CmsModel, "modelId">): 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}`;
};

Expand Down Expand Up @@ -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
});
};

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
};
}
}
1 change: 1 addition & 0 deletions packages/api-aco/src/utils/decorators/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ROOT_FOLDER = "root";
53 changes: 53 additions & 0 deletions packages/api-aco/src/utils/decorators/where.ts
Original file line number Diff line number Diff line change
@@ -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
};
};
6 changes: 6 additions & 0 deletions packages/api-headless-cms/src/crud/contentEntry.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type ResolveUpdate = ResolverFactory<any, ResolveUpdateArgs>;

export const resolveUpdate: ResolveUpdate =
({ model }) =>
async (_, args: any, context) => {
async (_, args, context) => {
try {
const entry = await context.cms.updateEntry(
model,
Expand Down
11 changes: 11 additions & 0 deletions packages/api-headless-cms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down

0 comments on commit 607b8af

Please sign in to comment.