chore: merges versions simplification
This commit is contained in:
@@ -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"]
|
||||||
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.5.6](https://github.com/payloadcms/payload/compare/v1.5.5...v1.5.6) (2023-01-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}.`,
|
||||||
|
|||||||
@@ -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`}>
|
|
||||||
|
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 { 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';
|
||||||
|
|||||||
@@ -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: {
|
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,
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user