chore: merges versions simplification

This commit is contained in:
James
2023-01-17 15:44:11 -05:00
51 changed files with 605 additions and 953 deletions

View File

@@ -1,5 +1,5 @@
{ {
"preReleaseId": "beta", "preReleaseId": "canary",
"git": { "git": {
"requireCleanWorkingDir": false, "requireCleanWorkingDir": false,
"commit": false, "commit": false,
@@ -11,7 +11,7 @@
}, },
"npm": { "npm": {
"skipChecks": true, "skipChecks": true,
"tag": "beta" "tag": "canary"
}, },
"hooks": { "hooks": {
"before:init": ["yarn", "yarn clean", "yarn test"] "before:init": ["yarn", "yarn clean", "yarn test"]

View File

@@ -1,5 +1,27 @@
## [1.5.9](https://github.com/payloadcms/payload/compare/v1.5.8...v1.5.9) (2023-01-15)
### Bug Fixes
* [#1877](https://github.com/payloadcms/payload/issues/1877), [#1867](https://github.com/payloadcms/payload/issues/1867) - mimeTypes and imageSizes no longer cause error in admin ui ([b06ca70](https://github.com/payloadcms/payload/commit/b06ca700be36cc3a945f81e3fa23ebb53d06ca23))
## [1.5.8](https://github.com/payloadcms/payload/compare/v1.5.7...v1.5.8) (2023-01-12)
### Features
* throws descriptive error when collection or global slug not found ([b847d85](https://github.com/payloadcms/payload/commit/b847d85e60032b47a8eacc2c9426fdd373dff879))
## [1.5.7](https://github.com/payloadcms/payload/compare/v1.5.6...v1.5.7) (2023-01-12)
### Bug Fixes
* ensures find with draft=true does not improperly exclude draft ids ([69026c5](https://github.com/payloadcms/payload/commit/69026c577914ba029f2c45423d9f621b605a3ca0))
* ensures querying with drafts works on both published and non-published posts ([f018fc0](https://github.com/payloadcms/payload/commit/f018fc04b02f70d0e6ea545d5eb36ea860206964))
## [1.5.6](https://github.com/payloadcms/payload/compare/v1.5.5...v1.5.6) (2023-01-11) ## [1.5.6](https://github.com/payloadcms/payload/compare/v1.5.5...v1.5.6) (2023-01-11)

View File

@@ -1,6 +1,6 @@
{ {
"name": "payload", "name": "payload",
"version": "1.5.6", "version": "1.5.9",
"description": "Node, React and MongoDB Headless CMS and Application Framework", "description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -134,6 +134,7 @@
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"mongoose": "6.5.0", "mongoose": "6.5.0",
"mongoose-aggregate-paginate-v2": "^1.0.6",
"mongoose-paginate-v2": "^1.6.1", "mongoose-paginate-v2": "^1.6.1",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.2",
"object-to-formdata": "^4.1.0", "object-to-formdata": "^4.1.0",

View File

@@ -44,7 +44,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
modifiedRef.current = modified; modifiedRef.current = modified;
const createCollectionDoc = useCallback(async () => { const createCollectionDoc = useCallback(async () => {
const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true`, { const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true&autosave=true`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@@ -95,13 +95,13 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
} }
if (url) { if (url) {
const body = {
...reduceFieldsToValues(fieldRef.current, true),
_status: 'draft',
};
setTimeout(async () => { setTimeout(async () => {
if (modifiedRef.current) { if (modifiedRef.current) {
const body = {
...reduceFieldsToValues(fieldRef.current, true),
_status: 'draft',
};
const res = await fetch(url, { const res = await fetch(url, {
method, method,
credentials: 'include', credentials: 'include',

View File

@@ -4,9 +4,6 @@ import { useConfig } from '../../utilities/Config';
import Button from '../Button'; import Button from '../Button';
import { Props } from './types'; import { Props } from './types';
import { useDocumentInfo } from '../../utilities/DocumentInfo'; import { useDocumentInfo } from '../../utilities/DocumentInfo';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { shouldIncrementVersionCount } from '../../../../versions/shouldIncrementVersionCount';
import './index.scss'; import './index.scss';
@@ -14,35 +11,20 @@ const baseClass = 'versions-count';
const VersionsCount: React.FC<Props> = ({ collection, global, id }) => { const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
const { routes: { admin } } = useConfig(); const { routes: { admin } } = useConfig();
const { versions, publishedDoc, unpublishedVersions } = useDocumentInfo(); const { versions } = useDocumentInfo();
const { t } = useTranslation('version'); const { t } = useTranslation('version');
// Doc status could come from three places:
// 1. the newest unpublished version (a draft)
// 2. the published doc's status, in the event that the doc is published and there are no newer versions
// 3. if there is no published doc, it's a draft
const docStatus = unpublishedVersions?.docs?.[0]?.version?._status || publishedDoc?._status || 'draft';
let versionsURL: string; let versionsURL: string;
let entity: SanitizedCollectionConfig | SanitizedGlobalConfig;
if (collection) { if (collection) {
versionsURL = `${admin}/collections/${collection.slug}/${id}/versions`; versionsURL = `${admin}/collections/${collection.slug}/${id}/versions`;
entity = collection;
} }
if (global) { if (global) {
versionsURL = `${admin}/globals/${global.slug}/versions`; versionsURL = `${admin}/globals/${global.slug}/versions`;
entity = global;
} }
let initialVersionsCount = 0; const versionCount = versions?.totalDocs || 0;
if (shouldIncrementVersionCount({ entity, versions, docStatus })) {
initialVersionsCount = 1;
}
const versionCount = (versions?.totalDocs || 0) + initialVersionsCount;
return ( return (
<div className={baseClass}> <div className={baseClass}>

View File

@@ -191,7 +191,7 @@ export const addFieldStatePromise = async ({
id, id,
operation, operation,
fields: field.fields, fields: field.fields,
data: data?.[field.name], data: data?.[field.name] || {},
fullData, fullData,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path: `${path}${field.name}.`, path: `${path}${field.name}.`,

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
import format from 'date-fns/format';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import usePayloadAPI from '../../../hooks/usePayloadAPI'; import usePayloadAPI from '../../../hooks/usePayloadAPI';
@@ -16,10 +15,6 @@ import Table from '../../elements/Table';
import Paginator from '../../elements/Paginator'; import Paginator from '../../elements/Paginator';
import PerPage from '../../elements/PerPage'; import PerPage from '../../elements/PerPage';
import { useSearchParams } from '../../utilities/SearchParams'; import { useSearchParams } from '../../utilities/SearchParams';
import { Banner, Pill } from '../..';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { shouldIncrementVersionCount } from '../../../../versions/shouldIncrementVersionCount';
import { Gutter } from '../../elements/Gutter'; import { Gutter } from '../../elements/Gutter';
import { getTranslation } from '../../../../utilities/getTranslation'; import { getTranslation } from '../../../../utilities/getTranslation';
@@ -28,7 +23,7 @@ import './index.scss';
const baseClass = 'versions'; const baseClass = 'versions';
const Versions: React.FC<Props> = ({ collection, global }) => { const Versions: React.FC<Props> = ({ collection, global }) => {
const { serverURL, routes: { admin, api }, admin: { dateFormat } } = useConfig(); const { serverURL, routes: { admin, api } } = useConfig();
const { setStepNav } = useStepNav(); const { setStepNav } = useStepNav();
const { params: { id } } = useRouteMatch<{ id: string }>(); const { params: { id } } = useRouteMatch<{ id: string }>();
const { t, i18n } = useTranslation('version'); const { t, i18n } = useTranslation('version');
@@ -39,14 +34,12 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
let docURL: string; let docURL: string;
let entityLabel: string; let entityLabel: string;
let slug: string; let slug: string;
let entity: SanitizedCollectionConfig | SanitizedGlobalConfig;
let editURL: string; let editURL: string;
if (collection) { if (collection) {
({ slug } = collection); ({ slug } = collection);
docURL = `${serverURL}${api}/${slug}/${id}`; docURL = `${serverURL}${api}/${slug}/${id}`;
entityLabel = getTranslation(collection.labels.singular, i18n); entityLabel = getTranslation(collection.labels.singular, i18n);
entity = collection;
editURL = `${admin}/collections/${collection.slug}/${id}`; editURL = `${admin}/collections/${collection.slug}/${id}`;
} }
@@ -54,7 +47,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
({ slug } = global); ({ slug } = global);
docURL = `${serverURL}${api}/globals/${slug}`; docURL = `${serverURL}${api}/globals/${slug}`;
entityLabel = getTranslation(global.label, i18n); entityLabel = getTranslation(global.label, i18n);
entity = global;
editURL = `${admin}/globals/${global.slug}`; editURL = `${admin}/globals/${global.slug}`;
} }
@@ -164,10 +156,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
useIDLabel = false; useIDLabel = false;
} }
const docStatus = doc?._status;
const docUpdatedAt = doc?.updatedAt;
const showParentDoc = versionsData?.page === 1 && shouldIncrementVersionCount({ entity, docStatus, versions: versionsData });
return ( return (
<div className={baseClass}> <div className={baseClass}>
<Meta <Meta
@@ -190,26 +178,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
{isLoadingVersions && ( {isLoadingVersions && (
<Loading /> <Loading />
)} )}
{showParentDoc && (
<Banner
type={docStatus === 'published' ? 'success' : undefined}
className={`${baseClass}__parent-doc`}
>
{t('currentDocumentStatus', { docStatus })}
-
{' '}
{format(new Date(docUpdatedAt), dateFormat)}
<div className={`${baseClass}__parent-doc-pills`}>
&nbsp;&nbsp;
<Pill
pillStyle="white"
to={editURL}
>
{t('general:edit')}
</Pill>
</div>
</Banner>
)}
{versionsData?.totalDocs > 0 && ( {versionsData?.totalDocs > 0 && (
<React.Fragment> <React.Fragment>
<Table <Table
@@ -228,22 +196,22 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
numberOfNeighbors={1} numberOfNeighbors={1}
/> />
{versionsData?.totalDocs > 0 && ( {versionsData?.totalDocs > 0 && (
<React.Fragment> <React.Fragment>
<div className={`${baseClass}__page-info`}> <div className={`${baseClass}__page-info`}>
{(versionsData.page * versionsData.limit) - (versionsData.limit - 1)} {(versionsData.page * versionsData.limit) - (versionsData.limit - 1)}
- -
{versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page ? (versionsData.limit * versionsData.page) : versionsData.totalDocs} {versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page ? (versionsData.limit * versionsData.page) : versionsData.totalDocs}
{' '} {' '}
{t('of')} {t('of')}
{' '} {' '}
{versionsData.totalDocs} {versionsData.totalDocs}
</div> </div>
<PerPage <PerPage
limits={collection?.admin?.pagination?.limits} limits={collection?.admin?.pagination?.limits}
limit={limit ? Number(limit) : 10} limit={limit ? Number(limit) : 10}
/> />
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
</React.Fragment> </React.Fragment>
)} )}

View File

@@ -3,6 +3,7 @@ import forgotPassword, { Result } from '../forgotPassword';
import { Payload } from '../../../payload'; import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -25,6 +26,10 @@ async function localForgotPassword(payload: Payload, options: Options): Promise<
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.i18n = i18n(payload.config.i18n); req.i18n = i18n(payload.config.i18n);

View File

@@ -5,6 +5,7 @@ import { TypeWithID } from '../../../collections/config/types';
import { Payload } from '../../../payload'; import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -36,6 +37,10 @@ async function localLogin<T extends TypeWithID = any>(payload: Payload, options:
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.payload = payload; req.payload = payload;
req.i18n = i18n(payload.config.i18n); req.i18n = i18n(payload.config.i18n);

View File

@@ -3,6 +3,7 @@ import resetPassword, { Result } from '../resetPassword';
import { PayloadRequest } from '../../../express/types'; import { PayloadRequest } from '../../../express/types';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -24,6 +25,10 @@ async function localResetPassword(payload: Payload, options: Options): Promise<R
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payload = payload; req.payload = payload;
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.i18n = i18n(payload.config.i18n); req.i18n = i18n(payload.config.i18n);

View File

@@ -3,6 +3,7 @@ import { Payload } from '../../../payload';
import unlock from '../unlock'; import unlock from '../unlock';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -23,6 +24,10 @@ async function localUnlock(payload: Payload, options: Options): Promise<boolean>
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payload = payload; req.payload = payload;
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.i18n = i18n(payload.config.i18n); req.i18n = i18n(payload.config.i18n);

View File

@@ -1,3 +1,4 @@
import { APIError } from '../../../errors';
import { Payload } from '../../../payload'; import { Payload } from '../../../payload';
import verifyEmail from '../verifyEmail'; import verifyEmail from '../verifyEmail';
@@ -14,6 +15,10 @@ async function localVerifyEmail(payload: Payload, options: Options): Promise<boo
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
return verifyEmail({ return verifyEmail({
token, token,
collection, collection,

View File

@@ -1,6 +1,7 @@
import mongoose, { UpdateAggregationStage, UpdateQuery } from 'mongoose'; import mongoose, { UpdateAggregationStage, UpdateQuery } from 'mongoose';
import paginate from 'mongoose-paginate-v2'; import paginate from 'mongoose-paginate-v2';
import passportLocalMongoose from 'passport-local-mongoose'; import passportLocalMongoose from 'passport-local-mongoose';
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2';
import { buildVersionCollectionFields } from '../versions/buildCollectionFields'; import { buildVersionCollectionFields } from '../versions/buildCollectionFields';
import buildQueryPlugin from '../mongoose/buildQuery'; import buildQueryPlugin from '../mongoose/buildQuery';
import buildCollectionSchema from './buildSchema'; import buildCollectionSchema from './buildSchema';
@@ -68,7 +69,7 @@ export default function initCollectionsLocal(ctx: Payload): void {
disableUnique: true, disableUnique: true,
draftsEnabled: true, draftsEnabled: true,
options: { options: {
timestamps: true, timestamps: false,
}, },
}, },
); );
@@ -76,6 +77,10 @@ export default function initCollectionsLocal(ctx: Payload): void {
versionSchema.plugin(paginate, { useEstimatedCount: true }) versionSchema.plugin(paginate, { useEstimatedCount: true })
.plugin(buildQueryPlugin); .plugin(buildQueryPlugin);
if (collection.versions?.drafts) {
versionSchema.plugin(mongooseAggregatePaginate);
}
ctx.versions[collection.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel; ctx.versions[collection.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
} }

View File

@@ -16,6 +16,7 @@ import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange'; import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead'; import { afterRead } from '../../fields/hooks/afterRead';
import { generateFileData } from '../../uploads/generateFileData'; import { generateFileData } from '../../uploads/generateFileData';
import { saveVersion } from '../../versions/saveVersion';
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = { export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
collection: Collection collection: Collection
@@ -27,6 +28,7 @@ export type Arguments<T extends { [field: string | number | symbol]: unknown }>
data: Omit<T, 'id'> data: Omit<T, 'id'>
overwriteExistingFiles?: boolean overwriteExistingFiles?: boolean
draft?: boolean draft?: boolean
autosave?: boolean
} }
async function create<TSlug extends keyof GeneratedTypes['collections']>( async function create<TSlug extends keyof GeneratedTypes['collections']>(
@@ -67,6 +69,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
showHiddenFields, showHiddenFields,
overwriteExistingFiles = false, overwriteExistingFiles = false,
draft = false, draft = false,
autosave = false,
} = args; } = args;
let { data } = args; let { data } = args;
@@ -215,6 +218,23 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
result = JSON.parse(result); result = JSON.parse(result);
result = sanitizeInternalFields(result); result = sanitizeInternalFields(result);
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
await saveVersion({
payload,
collection: collectionConfig,
req,
id: result.id,
docWithLocales: result,
autosave,
createdAt: result.createdAt,
onCreate: true,
});
}
// ///////////////////////////////////// // /////////////////////////////////////
// Send verification email if applicable // Send verification email if applicable
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -9,7 +9,7 @@ import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import { buildSortParam } from '../../mongoose/buildSortParam'; import { buildSortParam } from '../../mongoose/buildSortParam';
import { AccessResult } from '../../config/types'; import { AccessResult } from '../../config/types';
import { afterRead } from '../../fields/hooks/afterRead'; import { afterRead } from '../../fields/hooks/afterRead';
import { mergeDrafts } from '../../versions/drafts/mergeDrafts'; import { queryDrafts } from '../../versions/drafts/queryDrafts';
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
@@ -162,13 +162,12 @@ async function find<T extends Record<string, unknown>>(
}; };
if (collectionConfig.versions?.drafts && draftsEnabled) { if (collectionConfig.versions?.drafts && draftsEnabled) {
result = await mergeDrafts({ result = await queryDrafts({
accessResult, accessResult,
collection, collection,
locale, locale,
paginationOptions, paginationOptions,
payload, payload,
query,
where, where,
}); });
} else { } else {

View File

@@ -8,6 +8,7 @@ import create from '../create';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import { File } from '../../../uploads/types'; import { File } from '../../../uploads/types';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<TSlug extends keyof GeneratedTypes['collections']> = { export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug collection: TSlug
@@ -50,6 +51,10 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.locale = locale ?? req?.locale ?? defaultLocale; req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale; req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;

View File

@@ -5,6 +5,7 @@ import { Payload } from '../../../payload';
import deleteOperation from '../delete'; import deleteOperation from '../delete';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T collection: T
@@ -35,6 +36,11 @@ export default async function deleteLocal<TSlug extends keyof GeneratedTypes['co
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
import find from '../find'; import find from '../find';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T collection: T
@@ -53,6 +54,10 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.locale = locale ?? req?.locale ?? defaultLocale; req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale; req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;

View File

@@ -5,6 +5,7 @@ import findByID from '../findByID';
import { Payload } from '../../../payload'; import { Payload } from '../../../payload';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T collection: T
@@ -43,6 +44,10 @@ export default async function findByIDLocal<T extends keyof GeneratedTypes['coll
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.locale = locale ?? req?.locale ?? defaultLocale; req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale; req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;

View File

@@ -5,6 +5,7 @@ import { TypeWithVersion } from '../../../versions/types';
import findVersionByID from '../findVersionByID'; import findVersionByID from '../findVersionByID';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init'; import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -35,6 +36,10 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
req.payloadAPI = 'local'; req.payloadAPI = 'local';
req.locale = locale ?? req?.locale ?? defaultLocale; req.locale = locale ?? req?.locale ?? defaultLocale;
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale; req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;

View File

@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
import findVersions from '../findVersions'; import findVersions from '../findVersions';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -39,6 +40,10 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null; const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
const req = { const req = {
user, user,

View File

@@ -5,6 +5,7 @@ import { TypeWithVersion } from '../../../versions/types';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import restoreVersion from '../restoreVersion'; import restoreVersion from '../restoreVersion';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
collection: string collection: string
@@ -30,6 +31,11 @@ export default async function restoreVersionLocal<T extends TypeWithVersion<T> =
} = options; } = options;
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
const req = { const req = {
user, user,

View File

@@ -7,6 +7,7 @@ import { PayloadRequest } from '../../../express/types';
import { getDataLoader } from '../../dataloader'; import { getDataLoader } from '../../dataloader';
import { File } from '../../../uploads/types'; import { File } from '../../../uploads/types';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<TSlug extends keyof GeneratedTypes['collections']> = { export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug collection: TSlug
@@ -47,6 +48,11 @@ export default async function updateLocal<TSlug extends keyof GeneratedTypes['co
} = options; } = options;
const collection = payload.collections[collectionSlug]; const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
const defaultLocale = payload.config.localization ? payload.config.localization?.defaultLocale : null; const defaultLocale = payload.config.localization ? payload.config.localization?.defaultLocale : null;

View File

@@ -7,11 +7,8 @@ import executeAccess from '../../auth/executeAccess';
import { NotFound, Forbidden, APIError, ValidationError } from '../../errors'; import { NotFound, Forbidden, APIError, ValidationError } from '../../errors';
import { PayloadRequest } from '../../express/types'; import { PayloadRequest } from '../../express/types';
import { hasWhereAccessResult } from '../../auth/types'; import { hasWhereAccessResult } from '../../auth/types';
import { saveCollectionDraft } from '../../versions/drafts/saveCollectionDraft'; import { saveVersion } from '../../versions/saveVersion';
import { saveCollectionVersion } from '../../versions/saveCollectionVersion';
import { uploadFiles } from '../../uploads/uploadFiles'; import { uploadFiles } from '../../uploads/uploadFiles';
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
import { ensurePublishedCollectionVersion } from '../../versions/ensurePublishedCollectionVersion';
import { beforeChange } from '../../fields/hooks/beforeChange'; import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate'; import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange'; import { afterChange } from '../../fields/hooks/afterChange';
@@ -224,44 +221,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
delete result.password; delete result.password;
} }
// /////////////////////////////////////
// Create version from existing doc
// /////////////////////////////////////
let createdVersion;
if (collectionConfig.versions && !shouldSaveDraft) {
createdVersion = await saveCollectionVersion({
payload,
config: collectionConfig,
req,
docWithLocales,
id,
});
}
// ///////////////////////////////////// // /////////////////////////////////////
// Update // Update
// ///////////////////////////////////// // /////////////////////////////////////
if (shouldSaveDraft) { if (!shouldSaveDraft) {
await ensurePublishedCollectionVersion({
payload,
config: collectionConfig,
req,
docWithLocales,
id,
});
result = await saveCollectionDraft<GeneratedTypes['collections'][TSlug]>({
payload,
config: collectionConfig,
req,
data: result,
id,
autosave,
});
} else {
try { try {
result = await Model.findByIdAndUpdate( result = await Model.findByIdAndUpdate(
{ _id: id }, { _id: id },
@@ -269,23 +233,33 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
{ new: true }, { new: true },
); );
} catch (error) { } catch (error) {
cleanUpFailedVersion({
payload,
entityConfig: collectionConfig,
version: createdVersion,
});
// Handle uniqueness error from MongoDB // Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue throw error.code === 11000 && error.keyValue
? new ValidationError([{ message: 'Value must be unique', field: Object.keys(error.keyValue)[0] }], t) ? new ValidationError([{ message: 'Value must be unique', field: Object.keys(error.keyValue)[0] }], t)
: error; : error;
} }
const resultString = JSON.stringify(result);
result = JSON.parse(resultString);
} }
result = sanitizeInternalFields<GeneratedTypes['collections'][TSlug]>(result); result = JSON.parse(JSON.stringify(result));
result.id = result._id as string | number;
result = sanitizeInternalFields(result);
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
result = await saveVersion({
payload,
collection: collectionConfig,
req,
docWithLocales: result,
id,
autosave,
draft: shouldSaveDraft,
createdAt: originalDoc.createdAt,
});
}
// ///////////////////////////////////// // /////////////////////////////////////
// afterRead - Fields // afterRead - Fields

View File

@@ -13,12 +13,16 @@ export type CreateResult = {
export default async function createHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<CreateResult> | void> { export default async function createHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<CreateResult> | void> {
try { try {
const autosave = req.query.autosave === 'true';
const draft = req.query.draft === 'true';
const doc = await create({ const doc = await create({
req, req,
collection: req.collection, collection: req.collection,
data: req.body, data: req.body,
depth: Number(req.query.depth), depth: Number(req.query.depth),
draft: req.query.draft === 'true', draft,
autosave,
}); });
return res.status(httpStatus.CREATED).json({ return res.status(httpStatus.CREATED).json({

View File

@@ -26,7 +26,7 @@ export default function initGlobalsLocal(ctx: Payload): void {
disableUnique: true, disableUnique: true,
draftsEnabled: true, draftsEnabled: true,
options: { options: {
timestamps: true, timestamps: false,
}, },
}, },
); );

View File

@@ -5,6 +5,7 @@ import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types'; import { Document } from '../../../types';
import findOne from '../findOne'; import findOne from '../findOne';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T extends keyof GeneratedTypes['globals']> = { export type Options<T extends keyof GeneratedTypes['globals']> = {
slug: T slug: T
@@ -35,6 +36,11 @@ export default async function findOneLocal<T extends keyof GeneratedTypes['globa
const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug); const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug);
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -5,6 +5,7 @@ import { Document } from '../../../types';
import { TypeWithVersion } from '../../../versions/types'; import { TypeWithVersion } from '../../../versions/types';
import findVersionByID from '../findVersionByID'; import findVersionByID from '../findVersionByID';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
slug: string slug: string
@@ -34,6 +35,10 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug); const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug);
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
import findVersions from '../findVersions'; import findVersions from '../findVersions';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
slug: string slug: string
@@ -39,6 +40,10 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug); const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug);
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -5,6 +5,7 @@ import { Document } from '../../../types';
import { TypeWithVersion } from '../../../versions/types'; import { TypeWithVersion } from '../../../versions/types';
import restoreVersion from '../restoreVersion'; import restoreVersion from '../restoreVersion';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
slug: string slug: string
@@ -32,6 +33,10 @@ export default async function restoreVersionLocal<T extends TypeWithVersion<T> =
const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug); const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug);
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -5,6 +5,7 @@ import { TypeWithID } from '../../config/types';
import update from '../update'; import update from '../update';
import { getDataLoader } from '../../../collections/dataloader'; import { getDataLoader } from '../../../collections/dataloader';
import i18nInit from '../../../translations/init'; import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = { export type Options = {
slug: string slug: string
@@ -34,6 +35,10 @@ export default async function updateLocal<T extends TypeWithID = any>(payload: P
const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug); const globalConfig = payload.globals.config.find((config) => config.slug === globalSlug);
const i18n = i18nInit(payload.config.i18n); const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
}
const req = { const req = {
user, user,
payloadAPI: 'local', payloadAPI: 'local',

View File

@@ -1,17 +1,14 @@
import { docHasTimestamps, Where } from '../../types'; import { docHasTimestamps, Where } from '../../types';
import { SanitizedGlobalConfig, TypeWithID } from '../config/types'; import { SanitizedGlobalConfig, TypeWithID } from '../config/types';
import executeAccess from '../../auth/executeAccess'; import executeAccess from '../../auth/executeAccess';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { saveGlobalVersion } from '../../versions/saveGlobalVersion';
import { saveGlobalDraft } from '../../versions/drafts/saveGlobalDraft';
import { ensurePublishedGlobalVersion } from '../../versions/ensurePublishedGlobalVersion';
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
import { hasWhereAccessResult } from '../../auth'; import { hasWhereAccessResult } from '../../auth';
import { beforeChange } from '../../fields/hooks/beforeChange'; import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate'; import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange'; import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead'; import { afterRead } from '../../fields/hooks/afterRead';
import { PayloadRequest } from '../../express/types'; import { PayloadRequest } from '../../express/types';
import { saveVersion } from '../../versions/saveVersion';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
type Args = { type Args = {
globalConfig: SanitizedGlobalConfig globalConfig: SanitizedGlobalConfig
@@ -182,57 +179,20 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
skipValidation: shouldSaveDraft, skipValidation: shouldSaveDraft,
}); });
// /////////////////////////////////////
// Create version from existing doc
// /////////////////////////////////////
let createdVersion;
if (globalConfig.versions && !shouldSaveDraft) {
createdVersion = await saveGlobalVersion({
payload,
config: globalConfig,
req,
docWithLocales: result,
});
}
// ///////////////////////////////////// // /////////////////////////////////////
// Update // Update
// ///////////////////////////////////// // /////////////////////////////////////
if (shouldSaveDraft) { if (!shouldSaveDraft) {
await ensurePublishedGlobalVersion({ if (existingGlobal) {
payload, global = await Model.findOneAndUpdate(
config: globalConfig, { globalType: slug },
req, result,
docWithLocales: result, { new: true },
}); );
} else {
global = await saveGlobalDraft({ result.globalType = slug;
payload, global = await Model.create(result);
config: globalConfig,
data: result,
autosave,
});
} else {
try {
if (existingGlobal) {
global = await Model.findOneAndUpdate(
{ globalType: slug },
result,
{ new: true },
);
} else {
result.globalType = slug;
global = await Model.create(result);
}
} catch (error) {
cleanUpFailedVersion({
payload,
entityConfig: globalConfig,
version: createdVersion,
});
} }
} }
@@ -240,6 +200,22 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
global = JSON.parse(global); global = JSON.parse(global);
global = sanitizeInternalFields(global); global = sanitizeInternalFields(global);
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (globalConfig.versions) {
global = await saveVersion({
payload,
global: globalConfig,
req,
docWithLocales: result,
autosave,
draft: shouldSaveDraft,
createdAt: global.createdAt,
});
}
// ///////////////////////////////////// // /////////////////////////////////////
// afterRead - Fields // afterRead - Fields
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -14,6 +14,20 @@ export const buildVersionCollectionFields = (collection: SanitizedCollectionConf
type: 'group', type: 'group',
fields: collection.fields, fields: collection.fields,
}, },
{
name: 'createdAt',
type: 'date',
admin: {
disabled: true,
},
},
{
name: 'updatedAt',
type: 'date',
admin: {
disabled: true,
},
},
]; ];
if (collection?.versions?.drafts && collection?.versions?.drafts?.autosave) { if (collection?.versions?.drafts && collection?.versions?.drafts?.autosave) {

View File

@@ -8,6 +8,20 @@ export const buildVersionGlobalFields = (global: SanitizedGlobalConfig): Field[]
type: 'group', type: 'group',
fields: global.fields, fields: global.fields,
}, },
{
name: 'createdAt',
type: 'date',
admin: {
disabled: true,
},
},
{
name: 'updatedAt',
type: 'date',
admin: {
disabled: true,
},
},
]; ];
if (global?.versions?.drafts && global?.versions?.drafts?.autosave) { if (global?.versions?.drafts && global?.versions?.drafts?.autosave) {

View File

@@ -1,21 +0,0 @@
import { Payload } from '../payload';
import { SanitizedCollectionConfig } from '../collections/config/types';
import { SanitizedGlobalConfig } from '../globals/config/types';
import { TypeWithVersion } from './types';
type Args = {
payload: Payload,
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig,
version: TypeWithVersion<any>
}
const cleanUpFailedVersion = (args: Args) => {
const { payload, entityConfig, version } = args;
if (version) {
const VersionModel = payload.versions[entityConfig.slug];
VersionModel.findOneAndDelete({ _id: version.id });
}
};
export default cleanUpFailedVersion;

View File

@@ -1,229 +0,0 @@
import { AccessResult } from '../../config/types';
import { Where } from '../../types';
import { Payload } from '../../payload';
import { PaginatedDocs } from '../../mongoose/types';
import { Collection, CollectionModel } from '../../collections/config/types';
import { hasWhereAccessResult } from '../../auth';
import { appendVersionToQueryKey } from './appendVersionToQueryKey';
import replaceWithDraftIfAvailable from './replaceWithDraftIfAvailable';
type AggregateVersion<T> = {
_id: string
version: T
updatedAt: string
createdAt: string
}
type VersionCollectionMatchMap<T> = {
[_id: string | number]: {
updatedAt: string
createdAt: string
version: T
}
}
type Args = {
accessResult: AccessResult
collection: Collection
locale: string
paginationOptions: any
payload: Payload
query: Record<string, unknown>
where: Where
}
export const mergeDrafts = async <T extends Record<string, unknown>>({
accessResult,
collection,
locale,
payload,
paginationOptions,
query,
where: incomingWhere,
}: Args): Promise<PaginatedDocs<T>> => {
// Query the main collection for any IDs that match the query
// Create object "map" for performant lookup
const mainCollectionMatchMap = await collection.Model.find(query, { updatedAt: 1 }, { limit: paginationOptions.limit, sort: paginationOptions.sort })
.lean().then((res) => res.reduce((map, { _id, updatedAt }) => {
const newMap = map;
newMap[_id] = updatedAt;
return newMap;
}, {}));
// Query the versions collection with a version-specific query
const VersionModel = payload.versions[collection.config.slug] as CollectionModel;
const where = appendVersionToQueryKey(incomingWhere || {});
const versionQueryToBuild: { where: Where } = {
where: {
...where,
and: [
...where?.and || [],
{
'version._status': {
equals: 'draft',
},
},
],
},
};
if (hasWhereAccessResult(accessResult)) {
const versionAccessResult = appendVersionToQueryKey(accessResult);
versionQueryToBuild.where.and.push(versionAccessResult);
}
const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale);
const includedParentIDs: (string | number)[] = [];
// Create version "map" for performant lookup
// and in the same loop, check if there are matched versions without a matched parent
// This means that the newer version's parent should appear in the main query.
// To do so, add the version's parent ID into an explicit `includedIDs` array
const versionCollectionMatchMap = await VersionModel.aggregate<AggregateVersion<T>>([
{
$sort: Object.entries(paginationOptions.sort).reduce((sort, [key, order]) => {
return {
...sort,
[key]: order === 'asc' ? 1 : -1,
};
}, {}),
},
{
$group: {
_id: '$parent',
versionID: { $first: '$_id' },
version: { $first: '$version' },
updatedAt: { $first: '$updatedAt' },
createdAt: { $first: '$createdAt' },
},
},
{
$addFields: {
id: {
$toObjectId: '$_id',
},
},
},
{
$lookup: {
from: collection.config.slug,
localField: 'id',
foreignField: '_id',
as: 'parent',
},
},
{
$match: {
parent: {
$size: 1,
},
},
},
{ $match: versionQuery },
{ $limit: paginationOptions.limit },
]).then((res) => res.reduce<VersionCollectionMatchMap<T>>((map, { _id, updatedAt, createdAt, version }) => {
const newMap = map;
newMap[_id] = { version, updatedAt, createdAt };
const matchedParent = mainCollectionMatchMap[_id];
if (!matchedParent) includedParentIDs.push(_id);
return newMap;
}, {}));
// Now we need to explicitly exclude any parent matches that have newer versions
// which did NOT appear in the versions query
const excludedParentIDs = await Promise.all(Object.entries(mainCollectionMatchMap).map(async ([parentDocID, parentDocUpdatedAt]) => {
// If there is a matched version, and it's newer, this parent should remain
if (versionCollectionMatchMap[parentDocID] && versionCollectionMatchMap[parentDocID].updatedAt > parentDocUpdatedAt) {
return null;
}
// Otherwise, we need to check if there are newer versions present
// that did not get returned from the versions query
const versionsQuery = await VersionModel.find({
updatedAt: {
$gt: parentDocUpdatedAt,
},
parent: {
$eq: parentDocID,
},
}, {}, { limit: 1 }).lean();
// If there are,
// this says that the newest version does not match the incoming query,
// and the parent ID should be excluded
if (versionsQuery.length > 0) {
return parentDocID;
}
return null;
})).then((res) => res.filter((result) => Boolean(result)));
// Run a final query against the main collection,
// passing in any ids to exclude and include
// so that they appear properly paginated
const finalQueryToBuild: { where: Where } = {
where: {
and: [],
},
};
finalQueryToBuild.where.and.push({ or: [] });
if (hasWhereAccessResult(accessResult)) {
finalQueryToBuild.where.and.push(accessResult);
}
if (where) {
finalQueryToBuild.where.and[0].or.push(where);
}
if (includedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
in: includedParentIDs,
},
});
}
if (excludedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
not_in: excludedParentIDs,
},
});
}
const finalQuery = await collection.Model.buildQuery(finalQueryToBuild, locale);
let result = await collection.Model.paginate(finalQuery, paginationOptions);
result = {
...result,
docs: await Promise.all(result.docs.map(async (doc) => {
const matchedVersion = versionCollectionMatchMap[doc.id];
if (matchedVersion && matchedVersion.updatedAt > doc.updatedAt) {
return {
...doc,
...matchedVersion.version,
createdAt: matchedVersion.createdAt,
updatedAt: matchedVersion.updatedAt,
};
}
return replaceWithDraftIfAvailable({
accessResult,
payload,
entity: collection.config,
entityType: 'collection',
doc,
locale,
});
})),
};
return result;
};

View File

@@ -0,0 +1,91 @@
import { AccessResult } from '../../config/types';
import { Where } from '../../types';
import { Payload } from '../..';
import { PaginatedDocs } from '../../mongoose/types';
import { Collection, CollectionModel, TypeWithID } from '../../collections/config/types';
import { hasWhereAccessResult } from '../../auth';
import { appendVersionToQueryKey } from './appendVersionToQueryKey';
type AggregateVersion<T> = {
_id: string
version: T
updatedAt: string
createdAt: string
}
type Args = {
accessResult: AccessResult
collection: Collection
locale: string
paginationOptions: any
payload: Payload
where: Where
}
export const queryDrafts = async <T extends TypeWithID>({
accessResult,
collection,
locale,
payload,
paginationOptions,
where: incomingWhere,
}: Args): Promise<PaginatedDocs<T>> => {
const VersionModel = payload.versions[collection.config.slug] as CollectionModel;
const where = appendVersionToQueryKey(incomingWhere || {});
const versionQueryToBuild: { where: Where } = {
where: {
...where,
and: [
...where?.and || [],
],
},
};
if (hasWhereAccessResult(accessResult)) {
const versionAccessResult = appendVersionToQueryKey(accessResult);
versionQueryToBuild.where.and.push(versionAccessResult);
}
const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale);
const aggregate = VersionModel.aggregate<AggregateVersion<T>>([
// Sort so that newest are first
{ $sort: { updatedAt: -1 } },
// Group by parent ID, and take the first of each
{
$group: {
_id: '$parent',
version: { $first: '$version' },
updatedAt: { $first: '$updatedAt' },
createdAt: { $first: '$createdAt' },
},
},
// Filter based on incoming query
{ $match: versionQuery },
// Re-sort based on incoming sort
{
$sort: Object.entries(paginationOptions.sort).reduce((sort, [key, order]) => {
return {
...sort,
[key]: order === 'asc' ? 1 : -1,
};
}, {}),
},
// Add pagination limit
{ $limit: paginationOptions.limit },
]);
const result = await VersionModel.aggregatePaginate(aggregate, paginationOptions);
return {
...result,
docs: result.docs.map((doc) => ({
_id: doc._id,
...doc.version,
updatedAt: doc.updatedAt,
createdAt: doc.createdAt,
})),
};
};

View File

@@ -3,7 +3,6 @@ import { docHasTimestamps, Where } from '../../types';
import { hasWhereAccessResult } from '../../auth'; import { hasWhereAccessResult } from '../../auth';
import { AccessResult } from '../../config/types'; import { AccessResult } from '../../config/types';
import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types'; import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types';
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'; import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { appendVersionToQueryKey } from './appendVersionToQueryKey'; import { appendVersionToQueryKey } from './appendVersionToQueryKey';
import { SanitizedGlobalConfig } from '../../globals/config/types'; import { SanitizedGlobalConfig } from '../../globals/config/types';

View File

@@ -1,79 +0,0 @@
import { Payload } from '../../payload';
import { SanitizedCollectionConfig } from '../../collections/config/types';
import { enforceMaxVersions } from '../enforceMaxVersions';
import { PayloadRequest } from '../../express/types';
type Args<T> = {
payload: Payload
config?: SanitizedCollectionConfig
req: PayloadRequest
data: T
id: string | number
autosave: boolean
}
export const saveCollectionDraft = async <T extends Record<string, unknown>>({
payload,
config,
id,
data,
autosave,
}: Args<T>): Promise<T> => {
const VersionsModel = payload.versions[config.slug];
const dataAsDraft = { ...data, _status: 'draft' };
let existingAutosaveVersion;
if (autosave) {
existingAutosaveVersion = await VersionsModel.findOne({
parent: id,
}, {}, { sort: { updatedAt: 'desc' } });
}
let result;
try {
// If there is an existing autosave document,
// Update it
if (autosave && existingAutosaveVersion?.autosave === true) {
result = await VersionsModel.findByIdAndUpdate(
{
_id: existingAutosaveVersion._id,
},
{
version: dataAsDraft,
},
{ new: true, lean: true },
);
// Otherwise, create a new one
} else {
result = await VersionsModel.create({
parent: id,
version: dataAsDraft,
autosave: Boolean(autosave),
});
}
} catch (err) {
payload.logger.error(`There was an error while creating a draft ${config.labels.singular} with ID ${id}.`);
payload.logger.error(err);
}
if (config.versions.maxPerDoc) {
enforceMaxVersions({
id,
payload,
Model: VersionsModel,
slug: config.slug,
entityType: 'collection',
max: config.versions.maxPerDoc,
});
}
result = result.version;
result = JSON.stringify(result);
result = JSON.parse(result);
result.id = id;
return result;
};

View File

@@ -1,70 +0,0 @@
import { Payload } from '../../payload';
import { enforceMaxVersions } from '../enforceMaxVersions';
import { SanitizedGlobalConfig } from '../../globals/config/types';
type Args = {
payload: Payload
config?: SanitizedGlobalConfig
data: any
autosave: boolean
}
export const saveGlobalDraft = async ({
payload,
config,
data,
autosave,
}: Args): Promise<void> => {
const VersionsModel = payload.versions[config.slug];
const dataAsDraft = { ...data, _status: 'draft' };
let existingAutosaveVersion;
if (autosave) {
existingAutosaveVersion = await VersionsModel.findOne();
}
let result;
try {
// If there is an existing autosave document,
// Update it
if (autosave && existingAutosaveVersion?.autosave === true) {
result = await VersionsModel.findByIdAndUpdate(
{
_id: existingAutosaveVersion._id,
},
{
version: dataAsDraft,
},
{ new: true, lean: true },
);
// Otherwise, create a new one
} else {
result = await VersionsModel.create({
version: dataAsDraft,
autosave: Boolean(autosave),
});
}
} catch (err) {
payload.logger.error(`There was an error while saving a draft for the Global ${config.slug}.`);
payload.logger.error(err);
}
if (config.versions.max) {
enforceMaxVersions({
payload: this,
Model: VersionsModel,
slug: config.slug,
entityType: 'global',
max: config.versions.max,
});
}
result = result.version;
result = JSON.stringify(result);
result = JSON.parse(result);
return result;
};

View File

@@ -1,80 +0,0 @@
import { Payload } from '../payload';
import { SanitizedCollectionConfig } from '../collections/config/types';
import { enforceMaxVersions } from './enforceMaxVersions';
import { PayloadRequest } from '../express/types';
import { afterRead } from '../fields/hooks/afterRead';
type Args = {
payload: Payload
config?: SanitizedCollectionConfig
req: PayloadRequest
docWithLocales: any
id: string | number
}
export const ensurePublishedCollectionVersion = async ({
payload,
config,
req,
id,
docWithLocales,
}: Args): Promise<void> => {
// If there are no newer drafts,
// And the current doc is published,
// We need to keep a version of the published document
if (docWithLocales?._status === 'published') {
const VersionModel = payload.versions[config.slug];
const moreRecentDrafts = await VersionModel.find({
parent: {
$eq: docWithLocales.id,
},
updatedAt: {
$gt: docWithLocales.updatedAt,
},
},
{},
{
lean: true,
leanWithId: true,
sort: {
updatedAt: 'desc',
},
});
if (moreRecentDrafts?.length === 0) {
const version = await afterRead({
depth: 0,
doc: docWithLocales,
entityConfig: config,
req,
overrideAccess: true,
showHiddenFields: true,
flattenLocales: false,
});
try {
await VersionModel.create({
parent: id,
version,
autosave: false,
});
} catch (err) {
payload.logger.error(`There was an error while saving a version for the ${config.slug} with ID ${id}.`);
payload.logger.error(err);
}
if (config.versions.maxPerDoc) {
enforceMaxVersions({
id,
payload,
Model: VersionModel,
slug: config.slug,
entityType: 'collection',
max: config.versions.maxPerDoc,
});
}
}
}
};

View File

@@ -1,73 +0,0 @@
import { Payload } from '../payload';
import { enforceMaxVersions } from './enforceMaxVersions';
import { PayloadRequest } from '../express/types';
import { SanitizedGlobalConfig } from '../globals/config/types';
import { afterRead } from '../fields/hooks/afterRead';
type Args = {
payload: Payload
config?: SanitizedGlobalConfig
req: PayloadRequest
docWithLocales: any
}
export const ensurePublishedGlobalVersion = async ({
payload,
config,
req,
docWithLocales,
}: Args): Promise<void> => {
// If there are no newer drafts,
// And the current doc is published,
// We need to keep a version of the published document
if (docWithLocales?._status === 'published') {
const VersionModel = payload.versions[config.slug];
const moreRecentDrafts = await VersionModel.find({
updatedAt: {
$gt: docWithLocales.updatedAt,
},
},
{},
{
lean: true,
leanWithId: true,
sort: {
updatedAt: 'desc',
},
});
if (moreRecentDrafts?.length === 0) {
const version = await afterRead({
depth: 0,
doc: docWithLocales,
entityConfig: config,
req,
overrideAccess: true,
showHiddenFields: true,
flattenLocales: false,
});
try {
await VersionModel.create({
version,
autosave: false,
});
} catch (err) {
payload.logger.error(`There was an error while saving a version for the Global ${config.label}.`);
payload.logger.error(err);
}
if (config.versions.max) {
enforceMaxVersions({
payload: this,
Model: VersionModel,
slug: config.slug,
entityType: 'global',
max: config.versions.max,
});
}
}
}
};

View File

@@ -1,101 +0,0 @@
import { Payload } from '../payload';
import { SanitizedCollectionConfig } from '../collections/config/types';
import { enforceMaxVersions } from './enforceMaxVersions';
import { PayloadRequest } from '../express/types';
import sanitizeInternalFields from '../utilities/sanitizeInternalFields';
import { afterRead } from '../fields/hooks/afterRead';
type Args = {
payload: Payload
config?: SanitizedCollectionConfig
req: PayloadRequest
docWithLocales: any
id: string | number
}
export const saveCollectionVersion = async ({
payload,
config,
req,
id,
docWithLocales,
}: Args): Promise<void> => {
const VersionModel = payload.versions[config.slug];
let version = docWithLocales;
if (config.versions?.drafts) {
const latestVersion = await VersionModel.findOne({
parent: {
$eq: docWithLocales.id,
},
updatedAt: {
$gt: docWithLocales.updatedAt,
},
},
{},
{
lean: true,
leanWithId: true,
sort: {
updatedAt: 'desc',
},
});
if (latestVersion) {
// If the latest version is a draft, no need to re-save it
// Example: when "promoting" a draft to published, the draft already exists.
// Instead, return null
if (latestVersion?.version?._status === 'draft') {
return null;
}
version = latestVersion.version;
version = JSON.parse(JSON.stringify(version));
version = sanitizeInternalFields(version);
}
}
version = await afterRead({
depth: 0,
doc: version,
entityConfig: config,
req,
overrideAccess: true,
showHiddenFields: true,
flattenLocales: false,
});
if (version._id) delete version._id;
let createdVersion;
try {
createdVersion = await VersionModel.create({
parent: id,
version,
autosave: false,
});
} catch (err) {
payload.logger.error(`There was an error while saving a version for the ${config.labels.singular} with ID ${id}.`);
payload.logger.error(err);
}
if (config.versions.maxPerDoc) {
enforceMaxVersions({
id,
payload,
Model: VersionModel,
slug: config.slug,
entityType: 'collection',
max: config.versions.maxPerDoc,
});
}
if (createdVersion) {
createdVersion = JSON.parse(JSON.stringify(createdVersion));
createdVersion = sanitizeInternalFields(createdVersion);
}
return createdVersion;
};

View File

@@ -1,94 +0,0 @@
import { Payload } from '../payload';
import { enforceMaxVersions } from './enforceMaxVersions';
import { PayloadRequest } from '../express/types';
import { SanitizedGlobalConfig } from '../globals/config/types';
import sanitizeInternalFields from '../utilities/sanitizeInternalFields';
import { afterRead } from '../fields/hooks/afterRead';
type Args = {
payload: Payload
config?: SanitizedGlobalConfig
req: PayloadRequest
docWithLocales: any
}
export const saveGlobalVersion = async ({
payload,
config,
req,
docWithLocales,
}: Args): Promise<void> => {
const VersionModel = payload.versions[config.slug];
let version = docWithLocales;
if (config.versions?.drafts) {
const latestVersion = await VersionModel.findOne({
updatedAt: {
$gt: docWithLocales.updatedAt,
},
},
{},
{
lean: true,
leanWithId: true,
sort: {
updatedAt: 'desc',
},
});
if (latestVersion) {
// If the latest version is a draft, no need to re-save it
// Example: when "promoting" a draft to published, the draft already exists.
// Instead, return null
if (latestVersion?.version?._status === 'draft') {
return null;
}
version = latestVersion.version;
version = JSON.parse(JSON.stringify(version));
version = sanitizeInternalFields(version);
}
}
version = await afterRead({
depth: 0,
doc: version,
entityConfig: config,
flattenLocales: false,
overrideAccess: true,
req,
showHiddenFields: true,
});
if (version._id) delete version._id;
let createdVersion;
try {
createdVersion = await VersionModel.create({
version,
autosave: false,
});
} catch (err) {
payload.logger.error(`There was an error while saving a version for the Global ${config.slug}.`);
payload.logger.error(err);
}
if (config.versions.max) {
enforceMaxVersions({
payload: this,
Model: VersionModel,
slug: config.slug,
entityType: 'global',
max: config.versions.max,
});
}
if (createdVersion) {
createdVersion = JSON.parse(JSON.stringify(createdVersion));
createdVersion = sanitizeInternalFields(createdVersion);
}
return createdVersion;
};

124
src/versions/saveVersion.ts Normal file
View File

@@ -0,0 +1,124 @@
import { FilterQuery } from 'mongoose';
import { Payload } from '../payload';
import { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types';
import { enforceMaxVersions } from './enforceMaxVersions';
import { PayloadRequest } from '../express/types';
import { SanitizedGlobalConfig } from '../globals/config/types';
import sanitizeInternalFields from '../utilities/sanitizeInternalFields';
type Args = {
payload: Payload
global?: SanitizedGlobalConfig
collection?: SanitizedCollectionConfig
req: PayloadRequest
docWithLocales: any
id?: string | number
autosave?: boolean
draft?: boolean
createdAt: string
onCreate?: boolean
}
export const saveVersion = async ({
payload,
collection,
global,
id,
docWithLocales,
autosave,
draft,
createdAt,
onCreate = false,
}: Args): Promise<TypeWithID> => {
let entityConfig;
let entityType: 'global' | 'collection';
if (collection) {
entityConfig = collection;
entityType = 'collection';
}
if (global) {
entityConfig = global;
entityType = 'global';
}
const VersionModel = payload.versions[entityConfig.slug];
const versionData = { ...docWithLocales };
if (draft) versionData._status = 'draft';
if (versionData._id) delete versionData._id;
let existingAutosaveVersion;
if (autosave) {
const query: FilterQuery<unknown> = {};
if (collection) query.parent = id;
existingAutosaveVersion = await VersionModel.findOne(query, {}, { sort: { updatedAt: 'desc' } });
}
let result;
const now = new Date().toISOString();
try {
if (autosave && existingAutosaveVersion?.autosave === true) {
const data: Record<string, unknown> = {
version: versionData,
createdAt,
updatedAt: now,
};
if (createdAt) data.updatedAt = createdAt;
result = await VersionModel.findByIdAndUpdate(
{
_id: existingAutosaveVersion._id,
},
data,
{ new: true, lean: true },
);
// Otherwise, create a new one
} else {
const data: Record<string, unknown> = {
version: versionData,
autosave: Boolean(autosave),
updatedAt: onCreate ? createdAt : now,
createdAt: createdAt || now,
};
if (collection) data.parent = id;
result = await VersionModel.create(data);
}
} catch (err) {
let errorMessage: string;
if (collection) errorMessage = `There was an error while saving a version for the ${collection.labels.singular} with ID ${id}.`;
if (global) errorMessage = `There was an error while saving a version for the global ${global.label}.`;
payload.logger.error(errorMessage);
payload.logger.error(err);
}
let max: number;
if (collection && typeof collection.versions.maxPerDoc === 'number') max = collection.versions.maxPerDoc;
if (global && typeof global.versions.max === 'number') max = global.versions.max;
if (collection && collection.versions.maxPerDoc) {
enforceMaxVersions({
id,
payload,
Model: VersionModel,
slug: entityConfig.slug,
entityType,
max,
});
}
result = result.version;
result = JSON.parse(JSON.stringify(result));
result = sanitizeInternalFields(result);
result.id = id;
return result;
};

View File

@@ -1,14 +0,0 @@
import { SanitizedCollectionConfig } from '../collections/config/types';
import { SanitizedGlobalConfig } from '../globals/config/types';
import { PaginatedDocs } from '../mongoose/types';
type ShouldIncrementVersionCount = (args: {
entity: SanitizedGlobalConfig | SanitizedCollectionConfig
versions: PaginatedDocs<{ version?: { _status: string} }>
docStatus: string
}) => boolean
export const shouldIncrementVersionCount: ShouldIncrementVersionCount = ({ entity, docStatus, versions }) => {
return !(entity?.versions?.drafts && entity.versions.drafts?.autosave)
&& (docStatus === 'published' || (docStatus === 'draft' && versions?.docs?.[0]?.version?._status !== 'draft'));
};

View File

@@ -39,6 +39,7 @@ export default buildConfig({
upload: { upload: {
staticURL: '/media', staticURL: '/media',
staticDir: './media', staticDir: './media',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg', 'image/svg+xml'],
resizeOptions: { resizeOptions: {
width: 1280, width: 1280,
height: 720, height: 720,

View File

@@ -0,0 +1,56 @@
import type { CollectionConfig } from '../../../src/collections/config/types';
const VersionPosts: CollectionConfig = {
slug: 'version-posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'description', 'createdAt'],
preview: () => 'https://payloadcms.com',
},
versions: {
drafts: false,
maxPerDoc: 35,
retainDeleted: false,
},
access: {
read: ({ req: { user } }) => {
if (user) {
return true;
}
return {
or: [
{
_status: {
equals: 'published',
},
},
{
_status: {
exists: false,
},
},
],
};
},
readVersions: ({ req: { user } }) => Boolean(user),
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
unique: true,
localized: true,
},
{
name: 'description',
label: 'Description',
type: 'textarea',
required: true,
},
],
};
export default VersionPosts;

View File

@@ -4,11 +4,13 @@ import DraftPosts from './collections/Drafts';
import AutosaveGlobal from './globals/Autosave'; import AutosaveGlobal from './globals/Autosave';
import { devUser } from '../credentials'; import { devUser } from '../credentials';
import DraftGlobal from './globals/Draft'; import DraftGlobal from './globals/Draft';
import VersionPosts from './collections/Versions';
export default buildConfig({ export default buildConfig({
collections: [ collections: [
AutosavePosts, AutosavePosts,
DraftPosts, DraftPosts,
VersionPosts,
], ],
globals: [ globals: [
AutosaveGlobal, AutosaveGlobal,

View File

@@ -157,9 +157,14 @@ describe('Versions', () => {
const versions = await payload.findVersions({ const versions = await payload.findVersions({
collection, collection,
locale: 'all', locale: 'all',
where: {
parent: {
equals: collectionLocalPostID,
},
},
}); });
expect(versions.docs[0].version.title.en).toStrictEqual(englishTitle); expect(versions.docs[0].version.title.en).toStrictEqual(newEnglishTitle);
expect(versions.docs[0].version.title.es).toStrictEqual(spanishTitle); expect(versions.docs[0].version.title.es).toStrictEqual(spanishTitle);
}); });
}); });
@@ -184,7 +189,7 @@ describe('Versions', () => {
const restore = await payload.restoreVersion({ const restore = await payload.restoreVersion({
collection, collection,
id: versions.docs[0].id, id: versions.docs[1].id,
}); });
expect(restore.title).toBeDefined(); expect(restore.title).toBeDefined();
@@ -195,7 +200,7 @@ describe('Versions', () => {
draft: true, draft: true,
}); });
expect(restoredPost.title).toBe(restore.title); expect(restoredPost.title).toBe(versions.docs[1].version.title);
}); });
}); });
@@ -226,13 +231,15 @@ describe('Versions', () => {
draft: true, draft: true,
}); });
const spanishTitle = 'es title';
// second update to existing draft // second update to existing draft
await payload.update({ await payload.update({
id: collectionLocalPostID, id: collectionLocalPostID,
collection, collection,
locale: 'es', locale: 'es',
data: { data: {
title: updatedTitle, title: spanishTitle,
}, },
draft: true, draft: true,
}); });
@@ -251,7 +258,62 @@ describe('Versions', () => {
expect(publishedPost.title).toBe(originalTitle); expect(publishedPost.title).toBe(originalTitle);
expect(draftPost.title.en).toBe(updatedTitle); expect(draftPost.title.en).toBe(updatedTitle);
expect(draftPost.title.es).toBe(updatedTitle); expect(draftPost.title.es).toBe(spanishTitle);
});
});
describe('Draft Count', () => {
it('creates proper number of drafts', async () => {
const originalDraft = await payload.create({
collection: 'draft-posts',
draft: true,
data: {
title: 'A',
description: 'A',
_status: 'draft',
},
});
await payload.update({
collection: 'draft-posts',
id: originalDraft.id,
draft: true,
data: {
title: 'B',
description: 'B',
_status: 'draft',
},
});
await payload.update({
collection: 'draft-posts',
id: originalDraft.id,
draft: true,
data: {
title: 'C',
description: 'C',
_status: 'draft',
},
});
const mostRecentDraft = await payload.findByID({
collection: 'draft-posts',
id: originalDraft.id,
draft: true,
});
expect(mostRecentDraft.title).toStrictEqual('C');
const versions = await payload.findVersions({
collection: 'draft-posts',
where: {
parent: {
equals: originalDraft.id,
},
},
});
expect(versions.docs).toHaveLength(3);
}); });
}); });
}); });
@@ -423,7 +485,7 @@ describe('Versions', () => {
expect(data.id).toBeDefined(); expect(data.id).toBeDefined();
expect(data.parent.id).toStrictEqual(collectionGraphQLPostID); expect(data.parent.id).toStrictEqual(collectionGraphQLPostID);
expect(data.version.title).toStrictEqual(collectionGraphQLOriginalTitle); expect(data.version.title).toStrictEqual(updatedTitle);
}); });
it('should allow read of versions by querying version content', async () => { it('should allow read of versions by querying version content', async () => {

View File

@@ -8514,6 +8514,11 @@ mongodb@^3.7.3:
optionalDependencies: optionalDependencies:
saslprep "^1.0.0" saslprep "^1.0.0"
mongoose-aggregate-paginate-v2@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/mongoose-aggregate-paginate-v2/-/mongoose-aggregate-paginate-v2-1.0.6.tgz#fd2f2564d1bbf52f49a196f0b7b03675913dacca"
integrity sha512-UuALu+mjhQa1K9lMQvjLL3vm3iALvNw8PQNIh2gp1b+tO5hUa0NC0Wf6/8QrT9PSJVTihXaD8hQVy3J4e0jO0Q==
mongoose-paginate-v2@*, mongoose-paginate-v2@^1.6.1: mongoose-paginate-v2@*, mongoose-paginate-v2@^1.6.1:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/mongoose-paginate-v2/-/mongoose-paginate-v2-1.7.1.tgz#0b390f5eb8e5dca55ffcb1fd7b4d8078636cb8f1" resolved "https://registry.yarnpkg.com/mongoose-paginate-v2/-/mongoose-paginate-v2-1.7.1.tgz#0b390f5eb8e5dca55ffcb1fd7b4d8078636cb8f1"