diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx index 4d4321c41c..31f0ad056d 100644 --- a/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx @@ -5,7 +5,7 @@ import optionsReducer from './optionsReducer'; import useDebounce from '../../../../../hooks/useDebounce'; import ReactSelect from '../../../ReactSelect'; import { Value } from '../../../ReactSelect/types'; -import { PaginatedDocs } from '../../../../../../collections/config/types'; +import { PaginatedDocs } from '../../../../../../mongoose/types'; import './index.scss'; diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts index 0071195bd9..711b7ec063 100644 --- a/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts @@ -1,5 +1,6 @@ import { RelationshipField } from '../../../../../../fields/config/types'; -import { PaginatedDocs, SanitizedCollectionConfig } from '../../../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../../../collections/config/types'; +import { PaginatedDocs } from '../../../../../../mongoose/types'; export type Props = { onChange: (val: unknown) => void, diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index 17ea530efc..472e5be244 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -10,7 +10,7 @@ import Label from '../../Label'; import Error from '../../Error'; import FieldDescription from '../../FieldDescription'; import { relationship } from '../../../../../fields/validations'; -import { PaginatedDocs } from '../../../../../collections/config/types'; +import { PaginatedDocs } from '../../../../../mongoose/types'; import { useFormProcessing } from '../../Form/context'; import optionsReducer from './optionsReducer'; import { Props, Option, ValueWithRelation } from './types'; diff --git a/src/admin/components/forms/field-types/Relationship/types.ts b/src/admin/components/forms/field-types/Relationship/types.ts index 44c12dfa78..aeced06841 100644 --- a/src/admin/components/forms/field-types/Relationship/types.ts +++ b/src/admin/components/forms/field-types/Relationship/types.ts @@ -1,4 +1,5 @@ -import { PaginatedDocs, SanitizedCollectionConfig } from '../../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; +import { PaginatedDocs } from '../../../../../mongoose/types'; import { RelationshipField } from '../../../../../fields/config/types'; export type Props = Omit & { diff --git a/src/admin/components/views/collections/List/types.ts b/src/admin/components/views/collections/List/types.ts index 946e6b89ab..3a814b9d20 100644 --- a/src/admin/components/views/collections/List/types.ts +++ b/src/admin/components/views/collections/List/types.ts @@ -1,4 +1,5 @@ -import { SanitizedCollectionConfig, PaginatedDocs } from '../../../../../collections/config/types'; +import { SanitizedCollectionConfig } from '../../../../../collections/config/types'; +import { PaginatedDocs } from '../../../../../mongoose/types'; import { Column } from '../../../elements/Table/types'; export type Props = { diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 9bce3883f9..2888bb83f1 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -222,19 +222,6 @@ export type AuthCollection = { config: SanitizedCollectionConfig; } -export type PaginatedDocs = { - docs: T[] - totalDocs: number - limit: number - totalPages: number - page: number - pagingCounter: number - hasPrevPage: boolean - hasNextPage: boolean - prevPage: number | null - nextPage: number | null -} - export type TypeWithID = { id: string | number } diff --git a/src/collections/init.ts b/src/collections/init.ts index a370521c55..605d822ce0 100644 --- a/src/collections/init.ts +++ b/src/collections/init.ts @@ -104,6 +104,8 @@ export default function registerCollections(ctx: Payload): void { find, update, findByID, + findRevisions, + findRevisionByID, delete: deleteHandler, } = ctx.requestHandlers.collections; @@ -173,6 +175,14 @@ export default function registerCollections(ctx: Payload): void { .post(resetPassword); } + if (collection.revisions) { + router.route(`/${slug}/revisions`) + .get(findRevisions); + + router.route(`/${slug}/revisions/:id`) + .get(findRevisionByID); + } + router.route(`/${slug}`) .get(find) .post(create); diff --git a/src/collections/operations/find.ts b/src/collections/operations/find.ts index 919527fe7c..c788e94360 100644 --- a/src/collections/operations/find.ts +++ b/src/collections/operations/find.ts @@ -2,9 +2,11 @@ import { Where } from '../../types'; import { PayloadRequest } from '../../express/types'; import executeAccess from '../../auth/executeAccess'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; -import { Collection, TypeWithID, PaginatedDocs } from '../config/types'; +import { Collection, TypeWithID } from '../config/types'; +import { PaginatedDocs } from '../../mongoose/types'; import { hasWhereAccessResult } from '../../auth/types'; import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; +import { buildSortParam } from '../../mongoose/buildSortParam'; export type Arguments = { collection: Collection @@ -98,28 +100,10 @@ async function find(incomingArgs: Arguments): Promis // Find // ///////////////////////////////////// - let sortParam: Record; - - if (!args.sort) { - if (collectionConfig.timestamps) { - sortParam = { createdAt: 'desc' }; - } else { - sortParam = { _id: 'desc' }; - } - } else if (args.sort.indexOf('-') === 0) { - sortParam = { - [args.sort.substring(1)]: 'desc', - }; - } else { - sortParam = { - [args.sort]: 'asc', - }; - } - const optionsToExecute = { page: page || 1, limit: limit || 10, - sort: sortParam, + sort: buildSortParam(args.sort, collectionConfig.timestamps), lean: true, leanWithId: true, useEstimatedCount, @@ -166,7 +150,6 @@ async function find(incomingArgs: Arguments): Promis flattenLocales: true, showHiddenFields, }, - find, ))), }; diff --git a/src/collections/operations/findRevisionByID.ts b/src/collections/operations/findRevisionByID.ts index e69de29bb2..8c63b8e185 100644 --- a/src/collections/operations/findRevisionByID.ts +++ b/src/collections/operations/findRevisionByID.ts @@ -0,0 +1,144 @@ +/* eslint-disable no-underscore-dangle */ +import { PayloadRequest } from '../../express/types'; +import { Collection } from '../config/types'; +import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; +import { Forbidden, NotFound } from '../../errors'; +import executeAccess from '../../auth/executeAccess'; +import { Where } from '../../types'; +import { hasWhereAccessResult } from '../../auth/types'; +import { TypeWithRevision } from '../../revisions/types'; + +export type Arguments = { + collection: Collection + id: string + req: PayloadRequest + disableErrors?: boolean + currentDepth?: number + overrideAccess?: boolean + showHiddenFields?: boolean + depth?: number +} + +async function findRevisionByID = any>(args: Arguments): Promise { + const { + depth, + collection: { + config: collectionConfig, + }, + id, + req, + req: { + locale, + }, + disableErrors, + currentDepth, + overrideAccess, + showHiddenFields, + } = args; + + const RevisionsModel = this.revisions[collectionConfig.slug]; + + // ///////////////////////////////////// + // Access + // ///////////////////////////////////// + + const accessResults = !overrideAccess ? await executeAccess({ req, disableErrors, id }, collectionConfig.access.readRevisions) : true; + + // If errors are disabled, and access returns false, return null + if (accessResults === false) return null; + + const hasWhereAccess = typeof accessResults === 'object'; + + const queryToBuild: { where: Where } = { + where: { + and: [ + { + _id: { + equals: id, + }, + }, + ], + }, + }; + + if (hasWhereAccessResult(accessResults)) { + (queryToBuild.where.and as Where[]).push(accessResults); + } + + const query = await RevisionsModel.buildQuery(queryToBuild, locale); + + // ///////////////////////////////////// + // Find by ID + // ///////////////////////////////////// + + if (!query.$and[0]._id) throw new NotFound(); + + let result = await RevisionsModel.findOne(query, {}).lean(); + + if (!result) { + if (!disableErrors) { + if (!hasWhereAccess) throw new NotFound(); + if (hasWhereAccess) throw new Forbidden(); + } + + return null; + } + + // Clone the result - it may have come back memoized + result = JSON.parse(JSON.stringify(result)); + + result = sanitizeInternalFields(result); + + // ///////////////////////////////////// + // beforeRead - Collection + // ///////////////////////////////////// + + await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => { + await priorHook; + + result.revision = await hook({ + req, + query, + doc: result.revision, + }) || result.revision; + }, Promise.resolve()); + + // ///////////////////////////////////// + // afterRead - Fields + // ///////////////////////////////////// + + result.revision = await this.performFieldOperations(collectionConfig, { + depth, + req, + id, + data: result.revision, + hook: 'afterRead', + operation: 'read', + currentDepth, + overrideAccess, + flattenLocales: true, + showHiddenFields, + }); + + // ///////////////////////////////////// + // afterRead - Collection + // ///////////////////////////////////// + + await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; + + result.revision = await hook({ + req, + query, + doc: result.revision, + }) || result.revision; + }, Promise.resolve()); + + // ///////////////////////////////////// + // Return results + // ///////////////////////////////////// + + return result; +} + +export default findRevisionByID; diff --git a/src/collections/operations/findRevisions.ts b/src/collections/operations/findRevisions.ts index e69de29bb2..cfb1cd83f5 100644 --- a/src/collections/operations/findRevisions.ts +++ b/src/collections/operations/findRevisions.ts @@ -0,0 +1,178 @@ +import { Where } from '../../types'; +import { PayloadRequest } from '../../express/types'; +import executeAccess from '../../auth/executeAccess'; +import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; +import { Collection } from '../config/types'; +import { hasWhereAccessResult } from '../../auth/types'; +import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; +import { buildSortParam } from '../../mongoose/buildSortParam'; +import { PaginatedDocs } from '../../mongoose/types'; +import { TypeWithRevision } from '../../revisions/types'; + +export type Arguments = { + collection: Collection + where?: Where + page?: number + limit?: number + sort?: string + depth?: number + req?: PayloadRequest + overrideAccess?: boolean + showHiddenFields?: boolean +} + +async function findRevisions = any>(args: Arguments): Promise> { + const { + where, + page, + limit, + depth, + collection: { + config: collectionConfig, + }, + req, + req: { + locale, + }, + overrideAccess, + showHiddenFields, + } = args; + + const RevisionsModel = this.revisions[collectionConfig.slug]; + + // ///////////////////////////////////// + // Access + // ///////////////////////////////////// + + const queryToBuild: { where?: Where} = {}; + let useEstimatedCount = false; + + if (where) { + let and = []; + + if (Array.isArray(where.and)) and = where.and; + if (Array.isArray(where.AND)) and = where.AND; + + queryToBuild.where = { + ...where, + and: [ + ...and, + ], + }; + + const constraints = flattenWhereConstraints(queryToBuild); + + useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + } + + if (!overrideAccess) { + const accessResults = await executeAccess({ req }, collectionConfig.access.readRevisions); + + if (hasWhereAccessResult(accessResults)) { + if (!where) { + queryToBuild.where = { + and: [ + accessResults, + ], + }; + } else { + (queryToBuild.where.and as Where[]).push(accessResults); + } + } + } + + const query = await RevisionsModel.buildQuery(queryToBuild, locale); + + // ///////////////////////////////////// + // Find + // ///////////////////////////////////// + + const optionsToExecute = { + page: page || 1, + limit: limit || 10, + sort: buildSortParam(args.sort, true), + lean: true, + leanWithId: true, + useEstimatedCount, + }; + + const paginatedDocs = await RevisionsModel.paginate(query, optionsToExecute); + + // ///////////////////////////////////// + // beforeRead - Collection + // ///////////////////////////////////// + + let result = { + ...paginatedDocs, + docs: await Promise.all(paginatedDocs.docs.map(async (doc) => { + const docString = JSON.stringify(doc); + const docRef = JSON.parse(docString); + + await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => { + await priorHook; + + docRef.revision = await hook({ req, query, doc: docRef.revision }) || docRef.revision; + }, Promise.resolve()); + + return docRef; + })), + } as PaginatedDocs; + + // ///////////////////////////////////// + // afterRead - Fields + // ///////////////////////////////////// + + result = { + ...result, + docs: await Promise.all(result.docs.map(async (data) => ({ + ...data, + revision: this.performFieldOperations( + collectionConfig, + { + depth, + data: data.revision, + req, + id: data.revision.id, + hook: 'afterRead', + operation: 'read', + overrideAccess, + flattenLocales: true, + showHiddenFields, + isRevision: true, + }, + ), + }))), + }; + + // ///////////////////////////////////// + // afterRead - Collection + // ///////////////////////////////////// + + result = { + ...result, + docs: await Promise.all(result.docs.map(async (doc) => { + const docRef = doc; + + await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; + + docRef.revision = await hook({ req, query, doc: doc.revision }) || doc.revision; + }, Promise.resolve()); + + return docRef; + })), + }; + + // ///////////////////////////////////// + // Return results + // ///////////////////////////////////// + + result = { + ...result, + docs: result.docs.map((doc) => sanitizeInternalFields(doc)), + }; + + return result; +} + +export default findRevisions; diff --git a/src/collections/operations/local/find.ts b/src/collections/operations/local/find.ts index 117dd80eb7..7d4c968640 100644 --- a/src/collections/operations/local/find.ts +++ b/src/collections/operations/local/find.ts @@ -1,4 +1,5 @@ -import { PaginatedDocs, TypeWithID } from '../../config/types'; +import { TypeWithID } from '../../config/types'; +import { PaginatedDocs } from '../../../mongoose/types'; import { Document, Where } from '../../../types'; export type Options = { diff --git a/src/collections/requestHandlers/find.ts b/src/collections/requestHandlers/find.ts index 028d4eeab0..6f0bb51235 100644 --- a/src/collections/requestHandlers/find.ts +++ b/src/collections/requestHandlers/find.ts @@ -1,7 +1,8 @@ import { Response, NextFunction } from 'express'; import httpStatus from 'http-status'; import { PayloadRequest } from '../../express/types'; -import { PaginatedDocs, TypeWithID } from '../config/types'; +import { TypeWithID } from '../config/types'; +import { PaginatedDocs } from '../../mongoose/types'; export default async function find(req: PayloadRequest, res: Response, next: NextFunction): Promise> | void> { try { diff --git a/src/collections/requestHandlers/findRevisionByID.ts b/src/collections/requestHandlers/findRevisionByID.ts new file mode 100644 index 0000000000..21ac9de347 --- /dev/null +++ b/src/collections/requestHandlers/findRevisionByID.ts @@ -0,0 +1,24 @@ +import { Response, NextFunction } from 'express'; +import { PayloadRequest } from '../../express/types'; +import { Document } from '../../types'; + +export type FindByIDResult = { + message: string; + doc: Document; +}; + +export default async function findRevisionByID(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { + const options = { + req, + collection: req.collection, + id: req.params.id, + depth: req.query.depth, + }; + + try { + const doc = await this.operations.collections.findRevisionByID(options); + return res.json(doc); + } catch (error) { + return next(error); + } +} diff --git a/src/collections/requestHandlers/findRevisions.ts b/src/collections/requestHandlers/findRevisions.ts new file mode 100644 index 0000000000..de4102a137 --- /dev/null +++ b/src/collections/requestHandlers/findRevisions.ts @@ -0,0 +1,35 @@ +import { Response, NextFunction } from 'express'; +import httpStatus from 'http-status'; +import { PayloadRequest } from '../../express/types'; +import { TypeWithID } from '../config/types'; +import { PaginatedDocs } from '../../mongoose/types'; + +export default async function findRevisions(req: PayloadRequest, res: Response, next: NextFunction): Promise> | void> { + try { + let page; + + if (typeof req.query.page === 'string') { + const parsedPage = parseInt(req.query.page, 10); + + if (!Number.isNaN(parsedPage)) { + page = parsedPage; + } + } + + const options = { + req, + collection: req.collection, + where: req.query.where, + page, + limit: req.query.limit, + sort: req.query.sort, + depth: req.query.depth, + }; + + const result = await this.operations.collections.findRevisions(options); + + return res.status(httpStatus.OK).json(result); + } catch (error) { + return next(error); + } +} diff --git a/src/fields/hookPromise.ts b/src/fields/hookPromise.ts index f85beb409c..8012ce42ca 100644 --- a/src/fields/hookPromise.ts +++ b/src/fields/hookPromise.ts @@ -11,6 +11,7 @@ type Arguments = { fullOriginalDoc: Record fullData: Record flattenLocales: boolean + isRevision: boolean } type ExecuteHookArguments = { diff --git a/src/fields/performFieldOperations.ts b/src/fields/performFieldOperations.ts index 3ac9dbbb3a..1d8f3bb173 100644 --- a/src/fields/performFieldOperations.ts +++ b/src/fields/performFieldOperations.ts @@ -23,6 +23,7 @@ type Arguments = { showHiddenFields?: boolean depth?: number currentDepth?: number + isRevision?: boolean } export default async function performFieldOperations(this: Payload, entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig, args: Arguments): Promise { @@ -42,6 +43,7 @@ export default async function performFieldOperations(this: Payload, entityConfig flattenLocales, unflattenLocales = false, showHiddenFields = false, + isRevision = false, } = args; const fullData = deepCopyObject(data); @@ -101,6 +103,7 @@ export default async function performFieldOperations(this: Payload, entityConfig unflattenLocaleActions, transformActions, docWithLocales, + isRevision, }); if (hook === 'afterRead') { diff --git a/src/fields/traverseFields.ts b/src/fields/traverseFields.ts index 5d74a2348c..440775b91f 100644 --- a/src/fields/traverseFields.ts +++ b/src/fields/traverseFields.ts @@ -36,6 +36,7 @@ type Arguments = { transformActions: (() => void)[] docWithLocales?: Record skipValidation?: boolean + isRevision: boolean } const traverseFields = (args: Arguments): void => { @@ -68,6 +69,7 @@ const traverseFields = (args: Arguments): void => { transformActions, docWithLocales = {}, skipValidation, + isRevision, } = args; fields.forEach((field) => { @@ -224,6 +226,7 @@ const traverseFields = (args: Arguments): void => { fullOriginalDoc, fullData, flattenLocales, + isRevision, })); } diff --git a/src/globals/config/schema.ts b/src/globals/config/schema.ts index 6f63412f26..ff793ba5f8 100644 --- a/src/globals/config/schema.ts +++ b/src/globals/config/schema.ts @@ -19,6 +19,7 @@ const globalSchema = joi.object().keys({ }), access: joi.object({ read: joi.func(), + readRevisions: joi.func(), update: joi.func(), }), fields: joi.array(), diff --git a/src/globals/config/types.ts b/src/globals/config/types.ts index 2c7e2bb5d3..873b59a855 100644 --- a/src/globals/config/types.ts +++ b/src/globals/config/types.ts @@ -1,11 +1,14 @@ import React from 'react'; import { Model, Document } from 'mongoose'; import { DeepRequired } from 'ts-essentials'; -import { IncomingRevisionsType } from '../../revisions/types'; import { PayloadRequest } from '../../express/types'; import { Access, GeneratePreviewURL } from '../../config/types'; import { Field } from '../../fields/config/types'; +export type TypeWithID = { + id: string +} + export type BeforeValidateHook = (args: { data?: any; req?: PayloadRequest; @@ -53,8 +56,8 @@ export type GlobalConfig = { } access?: { read?: Access; + readRevisions?: Access; update?: Access; - admin?: Access; } fields: Field[]; admin?: { diff --git a/src/globals/operations/findRevisionByID.ts b/src/globals/operations/findRevisionByID.ts new file mode 100644 index 0000000000..eff50bcab4 --- /dev/null +++ b/src/globals/operations/findRevisionByID.ts @@ -0,0 +1,142 @@ +/* eslint-disable no-underscore-dangle */ +import { PayloadRequest } from '../../express/types'; +import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; +import { Forbidden, NotFound } from '../../errors'; +import executeAccess from '../../auth/executeAccess'; +import { Where } from '../../types'; +import { hasWhereAccessResult } from '../../auth/types'; +import { TypeWithRevision } from '../../revisions/types'; +import { SanitizedGlobalConfig } from '../config/types'; + +export type Arguments = { + globalConfig: SanitizedGlobalConfig + id: string + req: PayloadRequest + disableErrors?: boolean + currentDepth?: number + overrideAccess?: boolean + showHiddenFields?: boolean + depth?: number +} + +async function findRevisionByID = any>(args: Arguments): Promise { + const { + depth, + globalConfig, + id, + req, + req: { + locale, + }, + disableErrors, + currentDepth, + overrideAccess, + showHiddenFields, + } = args; + + const RevisionsModel = this.revisions[globalConfig.slug]; + + // ///////////////////////////////////// + // Access + // ///////////////////////////////////// + + const accessResults = !overrideAccess ? await executeAccess({ req, disableErrors, id }, globalConfig.access.readRevisions) : true; + + // If errors are disabled, and access returns false, return null + if (accessResults === false) return null; + + const hasWhereAccess = typeof accessResults === 'object'; + + const queryToBuild: { where: Where } = { + where: { + and: [ + { + _id: { + equals: id, + }, + }, + ], + }, + }; + + if (hasWhereAccessResult(accessResults)) { + (queryToBuild.where.and as Where[]).push(accessResults); + } + + const query = await RevisionsModel.buildQuery(queryToBuild, locale); + + // ///////////////////////////////////// + // Find by ID + // ///////////////////////////////////// + + if (!query.$and[0]._id) throw new NotFound(); + + let result = await RevisionsModel.findOne(query, {}).lean(); + + if (!result) { + if (!disableErrors) { + if (!hasWhereAccess) throw new NotFound(); + if (hasWhereAccess) throw new Forbidden(); + } + + return null; + } + + // Clone the result - it may have come back memoized + result = JSON.parse(JSON.stringify(result)); + + result = sanitizeInternalFields(result); + + // ///////////////////////////////////// + // beforeRead - Collection + // ///////////////////////////////////// + + await globalConfig.hooks.beforeRead.reduce(async (priorHook, hook) => { + await priorHook; + + result.revision = await hook({ + req, + query, + doc: result.revision, + }) || result.revision; + }, Promise.resolve()); + + // ///////////////////////////////////// + // afterRead - Fields + // ///////////////////////////////////// + + result.revision = await this.performFieldOperations(globalConfig, { + depth, + req, + id, + data: result.revision, + hook: 'afterRead', + operation: 'read', + currentDepth, + overrideAccess, + flattenLocales: true, + showHiddenFields, + }); + + // ///////////////////////////////////// + // afterRead - Collection + // ///////////////////////////////////// + + await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; + + result.revision = await hook({ + req, + query, + doc: result.revision, + }) || result.revision; + }, Promise.resolve()); + + // ///////////////////////////////////// + // Return results + // ///////////////////////////////////// + + return result; +} + +export default findRevisionByID; diff --git a/src/globals/operations/findRevisions.ts b/src/globals/operations/findRevisions.ts new file mode 100644 index 0000000000..3b6ecca0f6 --- /dev/null +++ b/src/globals/operations/findRevisions.ts @@ -0,0 +1,156 @@ +import { Where } from '../../types'; +import { PayloadRequest } from '../../express/types'; +import executeAccess from '../../auth/executeAccess'; +import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; +import { PaginatedDocs } from '../../mongoose/types'; +import { hasWhereAccessResult } from '../../auth/types'; +import flattenWhereConstraints from '../../utilities/flattenWhereConstraints'; +import { buildSortParam } from '../../mongoose/buildSortParam'; +import { TypeWithRevision } from '../../revisions/types'; +import { SanitizedGlobalConfig } from '../config/types'; + +export type Arguments = { + globalConfig: SanitizedGlobalConfig + where?: Where + page?: number + limit?: number + sort?: string + depth?: number + req?: PayloadRequest + overrideAccess?: boolean + showHiddenFields?: boolean +} + +async function findRevisions = any>(args: Arguments): Promise> { + const { + where, + page, + limit, + depth, + globalConfig, + req, + req: { + locale, + }, + overrideAccess, + showHiddenFields, + } = args; + + const RevisionsModel = this.revisions[globalConfig.slug]; + + // ///////////////////////////////////// + // Access + // ///////////////////////////////////// + + const queryToBuild: { where?: Where} = {}; + let useEstimatedCount = false; + + if (where) { + let and = []; + + if (Array.isArray(where.and)) and = where.and; + if (Array.isArray(where.AND)) and = where.AND; + + queryToBuild.where = { + ...where, + and: [ + ...and, + ], + }; + + const constraints = flattenWhereConstraints(queryToBuild); + + useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + } + + if (!overrideAccess) { + const accessResults = await executeAccess({ req }, globalConfig.access.readRevisions); + + if (hasWhereAccessResult(accessResults)) { + if (!where) { + queryToBuild.where = { + and: [ + accessResults, + ], + }; + } else { + (queryToBuild.where.and as Where[]).push(accessResults); + } + } + } + + const query = await RevisionsModel.buildQuery(queryToBuild, locale); + + // ///////////////////////////////////// + // Find + // ///////////////////////////////////// + + const optionsToExecute = { + page: page || 1, + limit: limit || 10, + sort: buildSortParam(args.sort, true), + lean: true, + leanWithId: true, + useEstimatedCount, + }; + + const paginatedDocs = await RevisionsModel.paginate(query, optionsToExecute); + + // ///////////////////////////////////// + // afterRead - Fields + // ///////////////////////////////////// + + let result = { + ...paginatedDocs, + docs: await Promise.all(paginatedDocs.docs.map(async (data) => ({ + ...data, + revision: this.performFieldOperations( + globalConfig, + { + depth, + data: data.revision, + req, + id: data.revision.id, + hook: 'afterRead', + operation: 'read', + overrideAccess, + flattenLocales: true, + showHiddenFields, + isRevision: true, + }, + ), + }))), + }; + + // ///////////////////////////////////// + // afterRead - Collection + // ///////////////////////////////////// + + result = { + ...result, + docs: await Promise.all(result.docs.map(async (doc) => { + const docRef = doc; + + await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; + + docRef.revision = await hook({ req, query, doc: doc.revision }) || doc.revision; + }, Promise.resolve()); + + return docRef; + })), + }; + + // ///////////////////////////////////// + // Return results + // ///////////////////////////////////// + + result = { + ...result, + docs: result.docs.map((doc) => sanitizeInternalFields(doc)), + }; + + return result; +} + +export default findRevisions; diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index 267b0b820f..8ce77b26d4 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -1,8 +1,10 @@ +import { Payload } from '../..'; +import { TypeWithID } from '../config/types'; import executeAccess from '../../auth/executeAccess'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import { saveGlobalRevision } from '../../revisions/saveGlobalRevision'; -async function update(args) { +async function update(this: Payload, args): Promise { const { globals: { Model } } = this; const { @@ -26,10 +28,12 @@ async function update(args) { // 2. Retrieve document // ///////////////////////////////////// - let global = await Model.findOne({ globalType: slug }); + let hasExistingGlobal = false; + let global: any = await Model.findOne({ globalType: slug }); let globalJSON; if (global) { + hasExistingGlobal = true; globalJSON = global.toJSON({ virtuals: true }); globalJSON = JSON.stringify(globalJSON); globalJSON = JSON.parse(globalJSON); @@ -105,6 +109,7 @@ async function update(args) { unflattenLocales: true, originalDoc, docWithLocales: globalJSON, + overrideAccess, }); // ///////////////////////////////////// @@ -131,7 +136,7 @@ async function update(args) { // Create revision from existing doc // ///////////////////////////////////// - if (globalConfig.revisions) { + if (globalConfig.revisions && hasExistingGlobal) { saveGlobalRevision({ payload: this, config: globalConfig, @@ -152,6 +157,7 @@ async function update(args) { depth, showHiddenFields, flattenLocales: true, + overrideAccess, }); // ///////////////////////////////////// diff --git a/src/globals/requestHandlers/findRevisionByID.ts b/src/globals/requestHandlers/findRevisionByID.ts new file mode 100644 index 0000000000..a04e131998 --- /dev/null +++ b/src/globals/requestHandlers/findRevisionByID.ts @@ -0,0 +1,20 @@ +import { Response, NextFunction } from 'express'; +import { PayloadRequest } from '../../express/types'; +import { Document } from '../../types'; +import { SanitizedGlobalConfig } from '../config/types'; + +export default (globalConfig: SanitizedGlobalConfig) => async function findRevisionByID(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { + const options = { + req, + globalConfig, + id: req.params.id, + depth: req.query.depth, + }; + + try { + const doc = await this.operations.globals.findRevisionByID(options); + return res.json(doc); + } catch (error) { + return next(error); + } +}; diff --git a/src/globals/requestHandlers/findRevisions.ts b/src/globals/requestHandlers/findRevisions.ts new file mode 100644 index 0000000000..bf635525fc --- /dev/null +++ b/src/globals/requestHandlers/findRevisions.ts @@ -0,0 +1,36 @@ +import { Response, NextFunction } from 'express'; +import httpStatus from 'http-status'; +import { PayloadRequest } from '../../express/types'; +import { TypeWithID } from '../../collections/config/types'; +import { PaginatedDocs } from '../../mongoose/types'; +import { SanitizedGlobalConfig } from '../config/types'; + +export default (global: SanitizedGlobalConfig) => async function findRevisions(req: PayloadRequest, res: Response, next: NextFunction): Promise> | void> { + try { + let page; + + if (typeof req.query.page === 'string') { + const parsedPage = parseInt(req.query.page, 10); + + if (!Number.isNaN(parsedPage)) { + page = parsedPage; + } + } + + const options = { + req, + globalConfig: global, + where: req.query.where, + page, + limit: req.query.limit, + sort: req.query.sort, + depth: req.query.depth, + }; + + const result = await this.operations.globals.findRevisions(options); + + return res.status(httpStatus.OK).json(result); + } catch (error) { + return next(error); + } +}; diff --git a/src/index.ts b/src/index.ts index ec476da7ff..5e3f834d7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,13 +2,14 @@ import express, { Express, Router } from 'express'; import crypto from 'crypto'; import { TypeWithID, - Collection, CollectionModel, PaginatedDocs, + Collection, CollectionModel, } from './collections/config/types'; import { SanitizedConfig, EmailOptions, InitOptions, } from './config/types'; +import { PaginatedDocs } from './mongoose/types'; import Logger from './utilities/logger'; import bindOperations from './init/bindOperations'; diff --git a/src/init/bindOperations.ts b/src/init/bindOperations.ts index fecc3d5121..1a7fbab2a4 100644 --- a/src/init/bindOperations.ts +++ b/src/init/bindOperations.ts @@ -14,10 +14,14 @@ import unlock from '../auth/operations/unlock'; import create from '../collections/operations/create'; import find from '../collections/operations/find'; import findByID from '../collections/operations/findByID'; +import findRevisions from '../collections/operations/findRevisions'; +import findRevisionByID from '../collections/operations/findRevisionByID'; import update from '../collections/operations/update'; import deleteHandler from '../collections/operations/delete'; import findOne from '../globals/operations/findOne'; +import findGlobalRevisions from '../globals/operations/findRevisions'; +import findGlobalRevisionByID from '../globals/operations/findRevisionByID'; import globalUpdate from '../globals/operations/update'; import preferenceUpdate from '../preferences/operations/update'; @@ -30,6 +34,8 @@ function bindOperations(ctx: Payload): void { create: create.bind(ctx), find: find.bind(ctx), findByID: findByID.bind(ctx), + findRevisions: findRevisions.bind(ctx), + findRevisionByID: findRevisionByID.bind(ctx), update: update.bind(ctx), delete: deleteHandler.bind(ctx), auth: { @@ -48,6 +54,8 @@ function bindOperations(ctx: Payload): void { }, globals: { findOne: findOne.bind(ctx), + findRevisions: findGlobalRevisions.bind(ctx), + findRevisionByID: findGlobalRevisionByID.bind(ctx), update: globalUpdate.bind(ctx), }, preferences: { diff --git a/src/init/bindRequestHandlers.ts b/src/init/bindRequestHandlers.ts index 8cd8667eea..3d29f569e8 100644 --- a/src/init/bindRequestHandlers.ts +++ b/src/init/bindRequestHandlers.ts @@ -13,10 +13,14 @@ import unlock from '../auth/requestHandlers/unlock'; import create from '../collections/requestHandlers/create'; import find from '../collections/requestHandlers/find'; import findByID from '../collections/requestHandlers/findByID'; +import findRevisions from '../collections/requestHandlers/findRevisions'; +import findRevisionByID from '../collections/requestHandlers/findRevisionByID'; import update from '../collections/requestHandlers/update'; import deleteHandler from '../collections/requestHandlers/delete'; import findOne from '../globals/requestHandlers/findOne'; +import findGlobalRevisions from '../globals/requestHandlers/findRevisions'; +import findGlobalRevisionByID from '../globals/requestHandlers/findRevisionByID'; import globalUpdate from '../globals/requestHandlers/update'; import { Payload } from '../index'; import preferenceUpdate from '../preferences/requestHandlers/update'; @@ -28,6 +32,8 @@ export type RequestHandlers = { create: typeof create, find: typeof find, findByID: typeof findByID, + findRevisions: typeof findRevisions, + findRevisionByID: typeof findRevisionByID, update: typeof update, delete: typeof deleteHandler, auth: { @@ -47,6 +53,8 @@ export type RequestHandlers = { globals: { findOne: typeof findOne, update: typeof globalUpdate, + findRevisions: typeof findGlobalRevisions + findRevisionByID: typeof findGlobalRevisionByID }, preferences: { update: typeof preferenceUpdate, @@ -61,6 +69,8 @@ function bindRequestHandlers(ctx: Payload): void { create: create.bind(ctx), find: find.bind(ctx), findByID: findByID.bind(ctx), + findRevisions: findRevisions.bind(ctx), + findRevisionByID: findRevisionByID.bind(ctx), update: update.bind(ctx), delete: deleteHandler.bind(ctx), auth: { @@ -80,6 +90,8 @@ function bindRequestHandlers(ctx: Payload): void { globals: { findOne: findOne.bind(ctx), update: globalUpdate.bind(ctx), + findRevisions: findGlobalRevisions.bind(ctx), + findRevisionByID: findGlobalRevisionByID.bind(ctx), }, preferences: { update: preferenceUpdate.bind(ctx), diff --git a/src/mongoose/buildSortParam.ts b/src/mongoose/buildSortParam.ts new file mode 100644 index 0000000000..e6ede5ee12 --- /dev/null +++ b/src/mongoose/buildSortParam.ts @@ -0,0 +1,21 @@ +export const buildSortParam = (sort: string, timestamps: boolean) => { + let sortParam: Record; + + if (!sort) { + if (timestamps) { + sortParam = { createdAt: 'desc' }; + } else { + sortParam = { _id: 'desc' }; + } + } else if (sort.indexOf('-') === 0) { + sortParam = { + [sort.substring(1)]: 'desc', + }; + } else { + sortParam = { + [sort]: 'asc', + }; + } + + return sortParam; +}; diff --git a/src/mongoose/types.ts b/src/mongoose/types.ts new file mode 100644 index 0000000000..884e5e9f22 --- /dev/null +++ b/src/mongoose/types.ts @@ -0,0 +1,12 @@ +export type PaginatedDocs = { + docs: T[] + totalDocs: number + limit: number + totalPages: number + page: number + pagingCounter: number + hasPrevPage: boolean + hasNextPage: boolean + prevPage: number | null + nextPage: number | null +} diff --git a/src/revisions/types.ts b/src/revisions/types.ts index 6a8e750eb8..ee89aaa472 100644 --- a/src/revisions/types.ts +++ b/src/revisions/types.ts @@ -2,3 +2,9 @@ export type IncomingRevisionsType = { maxPerDoc?: number retainDeleted?: boolean } + +export type TypeWithRevision = { + id: string + parent: string | number + revision: T +}