chore: merges versions simplification
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"preReleaseId": "beta",
|
||||
"preReleaseId": "canary",
|
||||
"git": {
|
||||
"requireCleanWorkingDir": false,
|
||||
"commit": false,
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"npm": {
|
||||
"skipChecks": true,
|
||||
"tag": "beta"
|
||||
"tag": "canary"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": ["yarn", "yarn clean", "yarn test"]
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "1.5.6",
|
||||
"version": "1.5.9",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -134,6 +134,7 @@
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mongoose": "6.5.0",
|
||||
"mongoose-aggregate-paginate-v2": "^1.0.6",
|
||||
"mongoose-paginate-v2": "^1.6.1",
|
||||
"nodemailer": "^6.4.2",
|
||||
"object-to-formdata": "^4.1.0",
|
||||
|
||||
@@ -44,7 +44,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
modifiedRef.current = modified;
|
||||
|
||||
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',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
@@ -95,13 +95,13 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
}
|
||||
|
||||
if (url) {
|
||||
const body = {
|
||||
...reduceFieldsToValues(fieldRef.current, true),
|
||||
_status: 'draft',
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
if (modifiedRef.current) {
|
||||
const body = {
|
||||
...reduceFieldsToValues(fieldRef.current, true),
|
||||
_status: 'draft',
|
||||
};
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
credentials: 'include',
|
||||
|
||||
@@ -4,9 +4,6 @@ import { useConfig } from '../../utilities/Config';
|
||||
import Button from '../Button';
|
||||
import { Props } from './types';
|
||||
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';
|
||||
|
||||
@@ -14,35 +11,20 @@ const baseClass = 'versions-count';
|
||||
|
||||
const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
|
||||
const { routes: { admin } } = useConfig();
|
||||
const { versions, publishedDoc, unpublishedVersions } = useDocumentInfo();
|
||||
const { versions } = useDocumentInfo();
|
||||
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 entity: SanitizedCollectionConfig | SanitizedGlobalConfig;
|
||||
|
||||
if (collection) {
|
||||
versionsURL = `${admin}/collections/${collection.slug}/${id}/versions`;
|
||||
entity = collection;
|
||||
}
|
||||
|
||||
if (global) {
|
||||
versionsURL = `${admin}/globals/${global.slug}/versions`;
|
||||
entity = global;
|
||||
}
|
||||
|
||||
let initialVersionsCount = 0;
|
||||
|
||||
if (shouldIncrementVersionCount({ entity, versions, docStatus })) {
|
||||
initialVersionsCount = 1;
|
||||
}
|
||||
|
||||
const versionCount = (versions?.totalDocs || 0) + initialVersionsCount;
|
||||
const versionCount = versions?.totalDocs || 0;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const addFieldStatePromise = async ({
|
||||
id,
|
||||
operation,
|
||||
fields: field.fields,
|
||||
data: data?.[field.name],
|
||||
data: data?.[field.name] || {},
|
||||
fullData,
|
||||
parentPassesCondition: passesCondition,
|
||||
path: `${path}${field.name}.`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import format from 'date-fns/format';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import usePayloadAPI from '../../../hooks/usePayloadAPI';
|
||||
@@ -16,10 +15,6 @@ import Table from '../../elements/Table';
|
||||
import Paginator from '../../elements/Paginator';
|
||||
import PerPage from '../../elements/PerPage';
|
||||
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 { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
@@ -28,7 +23,7 @@ import './index.scss';
|
||||
const baseClass = 'versions';
|
||||
|
||||
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 { params: { id } } = useRouteMatch<{ id: string }>();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
@@ -39,14 +34,12 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
let docURL: string;
|
||||
let entityLabel: string;
|
||||
let slug: string;
|
||||
let entity: SanitizedCollectionConfig | SanitizedGlobalConfig;
|
||||
let editURL: string;
|
||||
|
||||
if (collection) {
|
||||
({ slug } = collection);
|
||||
docURL = `${serverURL}${api}/${slug}/${id}`;
|
||||
entityLabel = getTranslation(collection.labels.singular, i18n);
|
||||
entity = collection;
|
||||
editURL = `${admin}/collections/${collection.slug}/${id}`;
|
||||
}
|
||||
|
||||
@@ -54,7 +47,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
({ slug } = global);
|
||||
docURL = `${serverURL}${api}/globals/${slug}`;
|
||||
entityLabel = getTranslation(global.label, i18n);
|
||||
entity = global;
|
||||
editURL = `${admin}/globals/${global.slug}`;
|
||||
}
|
||||
|
||||
@@ -164,10 +156,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
useIDLabel = false;
|
||||
}
|
||||
|
||||
const docStatus = doc?._status;
|
||||
const docUpdatedAt = doc?.updatedAt;
|
||||
const showParentDoc = versionsData?.page === 1 && shouldIncrementVersionCount({ entity, docStatus, versions: versionsData });
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Meta
|
||||
@@ -190,26 +178,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
{isLoadingVersions && (
|
||||
<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`}>
|
||||
|
||||
<Pill
|
||||
pillStyle="white"
|
||||
to={editURL}
|
||||
>
|
||||
{t('general:edit')}
|
||||
</Pill>
|
||||
</div>
|
||||
</Banner>
|
||||
)}
|
||||
{versionsData?.totalDocs > 0 && (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
@@ -228,22 +196,22 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
|
||||
numberOfNeighbors={1}
|
||||
/>
|
||||
{versionsData?.totalDocs > 0 && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{(versionsData.page * versionsData.limit) - (versionsData.limit - 1)}
|
||||
-
|
||||
{versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page ? (versionsData.limit * versionsData.page) : versionsData.totalDocs}
|
||||
{' '}
|
||||
{t('of')}
|
||||
{' '}
|
||||
{versionsData.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={collection?.admin?.pagination?.limits}
|
||||
limit={limit ? Number(limit) : 10}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{(versionsData.page * versionsData.limit) - (versionsData.limit - 1)}
|
||||
-
|
||||
{versionsData.totalPages > 1 && versionsData.totalPages !== versionsData.page ? (versionsData.limit * versionsData.page) : versionsData.totalDocs}
|
||||
{' '}
|
||||
{t('of')}
|
||||
{' '}
|
||||
{versionsData.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={collection?.admin?.pagination?.limits}
|
||||
limit={limit ? Number(limit) : 10}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import forgotPassword, { Result } from '../forgotPassword';
|
||||
import { Payload } from '../../../payload';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -25,6 +26,10 @@ async function localForgotPassword(payload: Payload, options: Options): Promise<
|
||||
|
||||
const collection = payload.collections[collectionSlug];
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
|
||||
}
|
||||
|
||||
req.payloadAPI = 'local';
|
||||
req.i18n = i18n(payload.config.i18n);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TypeWithID } from '../../../collections/config/types';
|
||||
import { Payload } from '../../../payload';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -36,6 +37,10 @@ async function localLogin<T extends TypeWithID = any>(payload: Payload, options:
|
||||
|
||||
const collection = payload.collections[collectionSlug];
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
|
||||
}
|
||||
|
||||
req.payloadAPI = 'local';
|
||||
req.payload = payload;
|
||||
req.i18n = i18n(payload.config.i18n);
|
||||
|
||||
@@ -3,6 +3,7 @@ import resetPassword, { Result } from '../resetPassword';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -24,6 +25,10 @@ async function localResetPassword(payload: Payload, options: Options): Promise<R
|
||||
|
||||
const collection = payload.collections[collectionSlug];
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
|
||||
}
|
||||
|
||||
req.payload = payload;
|
||||
req.payloadAPI = 'local';
|
||||
req.i18n = i18n(payload.config.i18n);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Payload } from '../../../payload';
|
||||
import unlock from '../unlock';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -23,6 +24,10 @@ async function localUnlock(payload: Payload, options: Options): Promise<boolean>
|
||||
|
||||
const collection = payload.collections[collectionSlug];
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
|
||||
}
|
||||
|
||||
req.payload = payload;
|
||||
req.payloadAPI = 'local';
|
||||
req.i18n = i18n(payload.config.i18n);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { APIError } from '../../../errors';
|
||||
import { Payload } from '../../../payload';
|
||||
import verifyEmail from '../verifyEmail';
|
||||
|
||||
@@ -14,6 +15,10 @@ async function localVerifyEmail(payload: Payload, options: Options): Promise<boo
|
||||
|
||||
const collection = payload.collections[collectionSlug];
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
|
||||
}
|
||||
|
||||
return verifyEmail({
|
||||
token,
|
||||
collection,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mongoose, { UpdateAggregationStage, UpdateQuery } from 'mongoose';
|
||||
import paginate from 'mongoose-paginate-v2';
|
||||
import passportLocalMongoose from 'passport-local-mongoose';
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2';
|
||||
import { buildVersionCollectionFields } from '../versions/buildCollectionFields';
|
||||
import buildQueryPlugin from '../mongoose/buildQuery';
|
||||
import buildCollectionSchema from './buildSchema';
|
||||
@@ -68,7 +69,7 @@ export default function initCollectionsLocal(ctx: Payload): void {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
options: {
|
||||
timestamps: true,
|
||||
timestamps: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -76,6 +77,10 @@ export default function initCollectionsLocal(ctx: Payload): void {
|
||||
versionSchema.plugin(paginate, { useEstimatedCount: true })
|
||||
.plugin(buildQueryPlugin);
|
||||
|
||||
if (collection.versions?.drafts) {
|
||||
versionSchema.plugin(mongooseAggregatePaginate);
|
||||
}
|
||||
|
||||
ctx.versions[collection.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { beforeValidate } from '../../fields/hooks/beforeValidate';
|
||||
import { afterChange } from '../../fields/hooks/afterChange';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { generateFileData } from '../../uploads/generateFileData';
|
||||
import { saveVersion } from '../../versions/saveVersion';
|
||||
|
||||
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
|
||||
collection: Collection
|
||||
@@ -27,6 +28,7 @@ export type Arguments<T extends { [field: string | number | symbol]: unknown }>
|
||||
data: Omit<T, 'id'>
|
||||
overwriteExistingFiles?: boolean
|
||||
draft?: boolean
|
||||
autosave?: boolean
|
||||
}
|
||||
|
||||
async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
@@ -67,6 +69,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
showHiddenFields,
|
||||
overwriteExistingFiles = false,
|
||||
draft = false,
|
||||
autosave = false,
|
||||
} = args;
|
||||
|
||||
let { data } = args;
|
||||
@@ -215,6 +218,23 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
result = JSON.parse(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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -9,7 +9,7 @@ import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
|
||||
import { buildSortParam } from '../../mongoose/buildSortParam';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { mergeDrafts } from '../../versions/drafts/mergeDrafts';
|
||||
import { queryDrafts } from '../../versions/drafts/queryDrafts';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -162,13 +162,12 @@ async function find<T extends Record<string, unknown>>(
|
||||
};
|
||||
|
||||
if (collectionConfig.versions?.drafts && draftsEnabled) {
|
||||
result = await mergeDrafts({
|
||||
result = await queryDrafts({
|
||||
accessResult,
|
||||
collection,
|
||||
locale,
|
||||
paginationOptions,
|
||||
payload,
|
||||
query,
|
||||
where,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import create from '../create';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import { File } from '../../../uploads/types';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
|
||||
collection: TSlug
|
||||
@@ -50,6 +51,10 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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.locale = locale ?? req?.locale ?? defaultLocale;
|
||||
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Payload } from '../../../payload';
|
||||
import deleteOperation from '../delete';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<T extends keyof GeneratedTypes['collections']> = {
|
||||
collection: T
|
||||
@@ -35,6 +36,11 @@ export default async function deleteLocal<TSlug extends keyof GeneratedTypes['co
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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 = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
|
||||
import find from '../find';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<T extends keyof GeneratedTypes['collections']> = {
|
||||
collection: T
|
||||
@@ -53,6 +54,10 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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.locale = locale ?? req?.locale ?? defaultLocale;
|
||||
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
|
||||
|
||||
@@ -5,6 +5,7 @@ import findByID from '../findByID';
|
||||
import { Payload } from '../../../payload';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<T extends keyof GeneratedTypes['collections']> = {
|
||||
collection: T
|
||||
@@ -43,6 +44,10 @@ export default async function findByIDLocal<T extends keyof GeneratedTypes['coll
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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.locale = locale ?? req?.locale ?? defaultLocale;
|
||||
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TypeWithVersion } from '../../../versions/types';
|
||||
import findVersionByID from '../findVersionByID';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import i18n from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -35,6 +36,10 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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.locale = locale ?? req?.locale ?? defaultLocale;
|
||||
req.fallbackLocale = fallbackLocale ?? req?.fallbackLocale ?? defaultLocale;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
|
||||
import findVersions from '../findVersions';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -39,6 +40,10 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
|
||||
const collection = payload.collections[collectionSlug];
|
||||
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 req = {
|
||||
user,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TypeWithVersion } from '../../../versions/types';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import restoreVersion from '../restoreVersion';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -30,6 +31,11 @@ export default async function restoreVersionLocal<T extends TypeWithVersion<T> =
|
||||
} = options;
|
||||
|
||||
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 req = {
|
||||
user,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PayloadRequest } from '../../../express/types';
|
||||
import { getDataLoader } from '../../dataloader';
|
||||
import { File } from '../../../uploads/types';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
|
||||
collection: TSlug
|
||||
@@ -47,6 +48,11 @@ export default async function updateLocal<TSlug extends keyof GeneratedTypes['co
|
||||
} = options;
|
||||
|
||||
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 defaultLocale = payload.config.localization ? payload.config.localization?.defaultLocale : null;
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@ import executeAccess from '../../auth/executeAccess';
|
||||
import { NotFound, Forbidden, APIError, ValidationError } from '../../errors';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { hasWhereAccessResult } from '../../auth/types';
|
||||
import { saveCollectionDraft } from '../../versions/drafts/saveCollectionDraft';
|
||||
import { saveCollectionVersion } from '../../versions/saveCollectionVersion';
|
||||
import { saveVersion } from '../../versions/saveVersion';
|
||||
import { uploadFiles } from '../../uploads/uploadFiles';
|
||||
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
|
||||
import { ensurePublishedCollectionVersion } from '../../versions/ensurePublishedCollectionVersion';
|
||||
import { beforeChange } from '../../fields/hooks/beforeChange';
|
||||
import { beforeValidate } from '../../fields/hooks/beforeValidate';
|
||||
import { afterChange } from '../../fields/hooks/afterChange';
|
||||
@@ -224,44 +221,11 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
delete result.password;
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Create version from existing doc
|
||||
// /////////////////////////////////////
|
||||
|
||||
let createdVersion;
|
||||
|
||||
if (collectionConfig.versions && !shouldSaveDraft) {
|
||||
createdVersion = await saveCollectionVersion({
|
||||
payload,
|
||||
config: collectionConfig,
|
||||
req,
|
||||
docWithLocales,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Update
|
||||
// /////////////////////////////////////
|
||||
|
||||
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 {
|
||||
if (!shouldSaveDraft) {
|
||||
try {
|
||||
result = await Model.findByIdAndUpdate(
|
||||
{ _id: id },
|
||||
@@ -269,23 +233,33 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
{ new: true },
|
||||
);
|
||||
} catch (error) {
|
||||
cleanUpFailedVersion({
|
||||
payload,
|
||||
entityConfig: collectionConfig,
|
||||
version: createdVersion,
|
||||
});
|
||||
|
||||
// Handle uniqueness error from MongoDB
|
||||
throw error.code === 11000 && error.keyValue
|
||||
? new ValidationError([{ message: 'Value must be unique', field: Object.keys(error.keyValue)[0] }], t)
|
||||
: 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
|
||||
|
||||
@@ -13,12 +13,16 @@ export type CreateResult = {
|
||||
|
||||
export default async function createHandler(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<CreateResult> | void> {
|
||||
try {
|
||||
const autosave = req.query.autosave === 'true';
|
||||
const draft = req.query.draft === 'true';
|
||||
|
||||
const doc = await create({
|
||||
req,
|
||||
collection: req.collection,
|
||||
data: req.body,
|
||||
depth: Number(req.query.depth),
|
||||
draft: req.query.draft === 'true',
|
||||
draft,
|
||||
autosave,
|
||||
});
|
||||
|
||||
return res.status(httpStatus.CREATED).json({
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function initGlobalsLocal(ctx: Payload): void {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
options: {
|
||||
timestamps: true,
|
||||
timestamps: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PayloadRequest } from '../../../express/types';
|
||||
import { Document } from '../../../types';
|
||||
import findOne from '../findOne';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options<T extends keyof GeneratedTypes['globals']> = {
|
||||
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 i18n = i18nInit(payload.config.i18n);
|
||||
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
|
||||
}
|
||||
|
||||
const req = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Document } from '../../../types';
|
||||
import { TypeWithVersion } from '../../../versions/types';
|
||||
import findVersionByID from '../findVersionByID';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
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 i18n = i18nInit(payload.config.i18n);
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
|
||||
}
|
||||
|
||||
const req = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PayloadRequest } from '../../../express/types';
|
||||
import findVersions from '../findVersions';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
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 i18n = i18nInit(payload.config.i18n);
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
|
||||
}
|
||||
|
||||
const req = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Document } from '../../../types';
|
||||
import { TypeWithVersion } from '../../../versions/types';
|
||||
import restoreVersion from '../restoreVersion';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
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 i18n = i18nInit(payload.config.i18n);
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
|
||||
}
|
||||
|
||||
const req = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TypeWithID } from '../../config/types';
|
||||
import update from '../update';
|
||||
import { getDataLoader } from '../../../collections/dataloader';
|
||||
import i18nInit from '../../../translations/init';
|
||||
import { APIError } from '../../../errors';
|
||||
|
||||
export type Options = {
|
||||
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 i18n = i18nInit(payload.config.i18n);
|
||||
|
||||
if (!globalConfig) {
|
||||
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
|
||||
}
|
||||
|
||||
const req = {
|
||||
user,
|
||||
payloadAPI: 'local',
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { docHasTimestamps, Where } from '../../types';
|
||||
import { SanitizedGlobalConfig, TypeWithID } from '../config/types';
|
||||
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 { beforeChange } from '../../fields/hooks/beforeChange';
|
||||
import { beforeValidate } from '../../fields/hooks/beforeValidate';
|
||||
import { afterChange } from '../../fields/hooks/afterChange';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { saveVersion } from '../../versions/saveVersion';
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
|
||||
|
||||
type Args = {
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
@@ -182,57 +179,20 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
|
||||
skipValidation: shouldSaveDraft,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Create version from existing doc
|
||||
// /////////////////////////////////////
|
||||
|
||||
let createdVersion;
|
||||
|
||||
if (globalConfig.versions && !shouldSaveDraft) {
|
||||
createdVersion = await saveGlobalVersion({
|
||||
payload,
|
||||
config: globalConfig,
|
||||
req,
|
||||
docWithLocales: result,
|
||||
});
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Update
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (shouldSaveDraft) {
|
||||
await ensurePublishedGlobalVersion({
|
||||
payload,
|
||||
config: globalConfig,
|
||||
req,
|
||||
docWithLocales: result,
|
||||
});
|
||||
|
||||
global = await saveGlobalDraft({
|
||||
payload,
|
||||
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,
|
||||
});
|
||||
if (!shouldSaveDraft) {
|
||||
if (existingGlobal) {
|
||||
global = await Model.findOneAndUpdate(
|
||||
{ globalType: slug },
|
||||
result,
|
||||
{ new: true },
|
||||
);
|
||||
} else {
|
||||
result.globalType = slug;
|
||||
global = await Model.create(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +200,22 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
|
||||
global = JSON.parse(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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -14,6 +14,20 @@ export const buildVersionCollectionFields = (collection: SanitizedCollectionConf
|
||||
type: 'group',
|
||||
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) {
|
||||
|
||||
@@ -8,6 +8,20 @@ export const buildVersionGlobalFields = (global: SanitizedGlobalConfig): Field[]
|
||||
type: 'group',
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
91
src/versions/drafts/queryDrafts.ts
Normal file
91
src/versions/drafts/queryDrafts.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import { docHasTimestamps, Where } from '../../types';
|
||||
import { hasWhereAccessResult } from '../../auth';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types';
|
||||
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
|
||||
import { appendVersionToQueryKey } from './appendVersionToQueryKey';
|
||||
import { SanitizedGlobalConfig } from '../../globals/config/types';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
124
src/versions/saveVersion.ts
Normal 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;
|
||||
};
|
||||
@@ -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'));
|
||||
};
|
||||
@@ -39,6 +39,7 @@ export default buildConfig({
|
||||
upload: {
|
||||
staticURL: '/media',
|
||||
staticDir: './media',
|
||||
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg', 'image/svg+xml'],
|
||||
resizeOptions: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
|
||||
56
test/versions/collections/Versions.ts
Normal file
56
test/versions/collections/Versions.ts
Normal 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;
|
||||
@@ -4,11 +4,13 @@ import DraftPosts from './collections/Drafts';
|
||||
import AutosaveGlobal from './globals/Autosave';
|
||||
import { devUser } from '../credentials';
|
||||
import DraftGlobal from './globals/Draft';
|
||||
import VersionPosts from './collections/Versions';
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
AutosavePosts,
|
||||
DraftPosts,
|
||||
VersionPosts,
|
||||
],
|
||||
globals: [
|
||||
AutosaveGlobal,
|
||||
|
||||
@@ -157,9 +157,14 @@ describe('Versions', () => {
|
||||
const versions = await payload.findVersions({
|
||||
collection,
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -184,7 +189,7 @@ describe('Versions', () => {
|
||||
|
||||
const restore = await payload.restoreVersion({
|
||||
collection,
|
||||
id: versions.docs[0].id,
|
||||
id: versions.docs[1].id,
|
||||
});
|
||||
|
||||
expect(restore.title).toBeDefined();
|
||||
@@ -195,7 +200,7 @@ describe('Versions', () => {
|
||||
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,
|
||||
});
|
||||
|
||||
const spanishTitle = 'es title';
|
||||
|
||||
// second update to existing draft
|
||||
await payload.update({
|
||||
id: collectionLocalPostID,
|
||||
collection,
|
||||
locale: 'es',
|
||||
data: {
|
||||
title: updatedTitle,
|
||||
title: spanishTitle,
|
||||
},
|
||||
draft: true,
|
||||
});
|
||||
@@ -251,7 +258,62 @@ describe('Versions', () => {
|
||||
|
||||
expect(publishedPost.title).toBe(originalTitle);
|
||||
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.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 () => {
|
||||
|
||||
@@ -8514,6 +8514,11 @@ mongodb@^3.7.3:
|
||||
optionalDependencies:
|
||||
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:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/mongoose-paginate-v2/-/mongoose-paginate-v2-1.7.1.tgz#0b390f5eb8e5dca55ffcb1fd7b4d8078636cb8f1"
|
||||
|
||||
Reference in New Issue
Block a user