chore: merge with master

This commit is contained in:
Jarrod Flesch
2023-01-19 11:26:28 -05:00
156 changed files with 3192 additions and 3200 deletions

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import { useRouteMatch } from 'react-router-dom';
import format from 'date-fns/format';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config';
import usePayloadAPI from '../../../hooks/usePayloadAPI';
import Eyebrow from '../../elements/Eyebrow';
import { LoadingOverlayToggle } from '../../elements/Loading';
import { useStepNav } from '../../elements/StepNav';
import { StepNavItem } from '../../elements/StepNav/types';
import Meta from '../../utilities/Meta';
@@ -15,20 +15,15 @@ import Table from '../../elements/Table';
import Paginator from '../../elements/Paginator';
import PerPage from '../../elements/PerPage';
import { useSearchParams } from '../../utilities/SearchParams';
import { Banner, Pill } from '../..';
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { shouldIncrementVersionCount } from '../../../../versions/shouldIncrementVersionCount';
import { Gutter } from '../../elements/Gutter';
import { getTranslation } from '../../../../utilities/getTranslation';
import { LoadingOverlayToggle } from '../../elements/Loading';
import './index.scss';
const baseClass = 'versions';
const Versions: React.FC<Props> = ({ collection, global }) => {
const { serverURL, routes: { admin, api }, admin: { dateFormat } } = useConfig();
const { serverURL, routes: { admin, api } } = useConfig();
const { setStepNav } = useStepNav();
const { params: { id } } = useRouteMatch<{ id: string }>();
const { t, i18n } = useTranslation('version');
@@ -39,14 +34,12 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
let docURL: string;
let entityLabel: string;
let slug: string;
let entity: SanitizedCollectionConfig | SanitizedGlobalConfig;
let editURL: string;
if (collection) {
({ slug } = collection);
docURL = `${serverURL}${api}/${slug}/${id}`;
entityLabel = getTranslation(collection.labels.singular, i18n);
entity = collection;
editURL = `${admin}/collections/${collection.slug}/${id}`;
}
@@ -54,13 +47,12 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
({ slug } = global);
docURL = `${serverURL}${api}/globals/${slug}`;
entityLabel = getTranslation(global.label, i18n);
entity = global;
editURL = `${admin}/globals/${global.slug}`;
}
const useAsTitle = collection?.admin?.useAsTitle || 'id';
const [{ data: doc }] = usePayloadAPI(docURL, { initialParams: { draft: 'true' } });
const [{ data: versionsData, isLoading: isLoadingData }, { setParams }] = usePayloadAPI(fetchURL);
const [{ data: versionsData, isLoading: isLoadingVersions }, { setParams }] = usePayloadAPI(fetchURL);
useEffect(() => {
let nav: StepNavItem[] = [];
@@ -164,17 +156,12 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
useIDLabel = false;
}
const docStatus = doc?._status;
const docUpdatedAt = doc?.updatedAt;
const showParentDoc = versionsData?.page === 1 && shouldIncrementVersionCount({ entity, docStatus, versions: versionsData });
return (
<React.Fragment>
<LoadingOverlayToggle
show={isLoadingData}
show={isLoadingVersions}
name="versions"
/>
<div className={baseClass}>
<Meta
title={metaTitle}
@@ -194,26 +181,6 @@ const Versions: React.FC<Props> = ({ collection, global }) => {
)}
</header>
{showParentDoc && (
<Banner
type={docStatus === 'published' ? 'success' : undefined}
className={`${baseClass}__parent-doc`}
>
{t('currentDocumentStatus', { docStatus })}
-
{' '}
{format(new Date(docUpdatedAt), dateFormat)}
<div className={`${baseClass}__parent-doc-pills`}>
&nbsp;&nbsp;
<Pill
pillStyle="white"
to={editURL}
>
{t('general:edit')}
</Pill>
</div>
</Banner>
)}
{versionsData?.totalDocs > 0 && (
<React.Fragment>
<Table

View File

@@ -52,13 +52,13 @@ const RelationshipCell = (props) => {
{values.map(({ relationTo, value }, i) => {
const document = documents[relationTo][value];
const relatedCollection = collections.find(({ slug }) => slug === relationTo);
const label = document?.[relatedCollection.admin.useAsTitle] ? document[relatedCollection.admin.useAsTitle] : `${t('untitled')} - ID: ${value}`;
return (
<React.Fragment key={i}>
{document === false && `${t('untitled')} - ID: ${value}`}
{document === null && `${t('loading')}...`}
{document && (
document[relatedCollection.admin.useAsTitle] ? document[relatedCollection.admin.useAsTitle] : `${t('untitled')} - ID: ${value}`
)}
{document && label}
{values.length > i + 1 && ', '}
</React.Fragment>
);

View File

@@ -1,6 +1,6 @@
import formatName from '../../../graphql/utilities/formatName';
import access from '../../operations/access';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
const formatConfigNames = (results, configs) => {
const formattedResults = { ...results };

View File

@@ -1,6 +1,6 @@
import passport from 'passport';
import AnonymousStrategy from 'passport-anonymous';
import { Payload } from '../index';
import { Payload } from '../payload';
import jwtStrategy from './strategies/jwt';
function initAuth(ctx: Payload): void {

View File

@@ -1,12 +1,13 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import forgotPassword, { Result } from '../forgotPassword';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
data: {
email: string
}
@@ -15,7 +16,10 @@ export type Options = {
req?: PayloadRequest
}
async function localForgotPassword(payload: Payload, options: Options): Promise<Result> {
async function localForgotPassword<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<Result> {
const {
collection: collectionSlug,
data,
@@ -27,7 +31,7 @@ async function localForgotPassword(payload: Payload, options: Options): Promise<
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';

View File

@@ -1,15 +1,15 @@
import { Response } from 'express';
import { Config as GeneratedTypes } from 'payload/generated-types';
import login, { Result } from '../login';
import { PayloadRequest } from '../../../express/types';
import { TypeWithID } from '../../../collections/config/types';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
data: {
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug
data: Omit<GeneratedTypes['collections'][TSlug], 'id'> & {
email: string
password: string
}
@@ -22,7 +22,10 @@ export type Options = {
showHiddenFields?: boolean
}
async function localLogin<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<Result & { user: T }> {
async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<TSlug>,
): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> {
const {
collection: collectionSlug,
req = {} as PayloadRequest,
@@ -38,7 +41,7 @@ async function localLogin<T extends TypeWithID = any>(payload: Payload, options:
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';
@@ -63,7 +66,7 @@ async function localLogin<T extends TypeWithID = any>(payload: Payload, options:
if (locale) args.req.locale = locale;
if (fallbackLocale) args.req.fallbackLocale = fallbackLocale;
return login(args);
return login<TSlug>(args);
}
export default localLogin;

View File

@@ -1,12 +1,13 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import resetPassword, { Result } from '../resetPassword';
import { PayloadRequest } from '../../../express/types';
import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
data: {
token: string
password: string
@@ -15,7 +16,10 @@ export type Options = {
req?: PayloadRequest
}
async function localResetPassword(payload: Payload, options: Options): Promise<Result> {
async function localResetPassword<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<Result> {
const {
collection: collectionSlug,
data,
@@ -26,7 +30,7 @@ async function localResetPassword(payload: Payload, options: Options): Promise<R
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payload = payload;

View File

@@ -1,12 +1,13 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import unlock from '../unlock';
import { getDataLoader } from '../../../collections/dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
data: {
email
}
@@ -14,7 +15,10 @@ export type Options = {
overrideAccess: boolean
}
async function localUnlock(payload: Payload, options: Options): Promise<boolean> {
async function localUnlock<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<boolean> {
const {
collection: collectionSlug,
data,
@@ -25,7 +29,7 @@ async function localUnlock(payload: Payload, options: Options): Promise<boolean>
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payload = payload;

View File

@@ -1,13 +1,17 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { APIError } from '../../../errors';
import { Payload } from '../../../index';
import { Payload } from '../../../payload';
import verifyEmail from '../verifyEmail';
export type Options = {
export type Options<T extends keyof GeneratedTypes['collections']> = {
token: string,
collection: string
collection: T
}
async function localVerifyEmail(payload: Payload, options: Options): Promise<boolean> {
async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<boolean> {
const {
collection: collectionSlug,
token,
@@ -16,7 +20,7 @@ async function localVerifyEmail(payload: Payload, options: Options): Promise<boo
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
return verifyEmail({

View File

@@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { CookieOptions, Response } from 'express';
import { AuthenticationError, LockedAuth } from '../../errors';
import { PayloadRequest } from '../../express/types';
@@ -17,9 +18,9 @@ export type Result = {
exp?: number,
}
export type Arguments = {
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
collection: Collection,
data: {
data: Omit<T, 'id'> & {
email: string
password: string
}
@@ -30,7 +31,9 @@ export type Arguments = {
showHiddenFields?: boolean
}
async function login<T>(incomingArgs: Arguments): Promise<Result & { user: T}> {
async function login<TSlug extends keyof GeneratedTypes['collections']>(
incomingArgs: Arguments<GeneratedTypes['collections'][TSlug]>,
): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> {
let args = incomingArgs;
// /////////////////////////////////////

View File

@@ -1,12 +1,12 @@
import { Response } from 'express';
import { Document } from '../../types';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Forbidden } from '../../errors';
import { PayloadRequest } from '../../express/types';
import { Collection, TypeWithID } from '../../collections/config/types';
export type Arguments = {
collection: Collection
data: {
data: Omit<TypeWithID, 'id'> & {
email: string
password: string
}
@@ -14,12 +14,14 @@ export type Arguments = {
res: Response
}
export type Result = {
export type Result<T> = {
message: string,
user: Document
user: T
}
async function registerFirstUser(args: Arguments): Promise<Result> {
async function registerFirstUser<TSlug extends keyof GeneratedTypes['collections']>(
args: Arguments,
): Promise<Result<GeneratedTypes['collections'][TSlug]>> {
const {
collection: {
Model,
@@ -45,9 +47,9 @@ async function registerFirstUser(args: Arguments): Promise<Result> {
// Register first user
// /////////////////////////////////////
const result = await payload.create<TypeWithID>({
const result = await payload.create<TSlug>({
req,
collection: slug,
collection: slug as TSlug,
data,
overrideAccess: true,
});

View File

@@ -9,7 +9,7 @@ import { PayloadRequest } from '../../express/types';
export type Result = {
token: string
user: UserDocument
user: Record<string, unknown>
}
export type Arguments = {

View File

@@ -1,4 +1,4 @@
import { Payload } from '..';
import { Payload } from '../payload';
import { PayloadRequest } from '../express/types';
import { SanitizedConfig, EmailOptions } from '../config/types';
import { Collection } from '../collections/config/types';

View File

@@ -1,7 +1,7 @@
import PassportAPIKey from 'passport-headerapikey';
import crypto from 'crypto';
import { PayloadRequest } from '../../express/types';
import { Payload } from '../..';
import { Payload } from '../../payload';
import find from '../../collections/operations/find';
export default (payload: Payload, { Model, config }): PassportAPIKey => {

View File

@@ -1,7 +1,7 @@
import url from 'url';
import passportJwt, { StrategyOptions } from 'passport-jwt';
import { Strategy as PassportStrategy } from 'passport-strategy';
import { Payload } from '../..';
import { Payload } from '../../payload';
import getExtractJWT from '../getExtractJWT';
const JwtStrategy = passportJwt.Strategy;

View File

@@ -2,7 +2,7 @@ import { Strategy } from 'passport';
import { DeepRequired } from 'ts-essentials';
import { PayloadRequest } from '../express/types';
import { Where, PayloadMongooseDocument } from '../types';
import { Payload } from '..';
import { Payload } from '../payload';
export type Permission = {
permission: boolean

View File

@@ -411,8 +411,30 @@ function entityToJsonSchema(config: SanitizedConfig, incomingEntity: SanitizedCo
};
}
function generateEntityObject(config: SanitizedConfig, type: 'collections' | 'globals'): JSONSchema4 {
return {
type: 'object',
properties: Object.fromEntries(config[type].map(({ slug }) => [
slug,
{
$ref: `#/definitions/${slug}`,
},
])),
required: config[type].map(({ slug }) => slug),
additionalProperties: false,
};
}
function configToJsonSchema(config: SanitizedConfig): JSONSchema4 {
return {
title: 'Config',
type: 'object',
additionalProperties: false,
properties: {
collections: generateEntityObject(config, 'collections'),
globals: generateEntityObject(config, 'globals'),
},
required: ['collections', 'globals'],
definitions: Object.fromEntries(
[
...config.globals.map((global) => [
@@ -425,7 +447,6 @@ function configToJsonSchema(config: SanitizedConfig): JSONSchema4 {
]),
],
),
additionalProperties: false,
};
}
@@ -439,7 +460,6 @@ export function generateTypes(): void {
const jsonSchema = configToJsonSchema(config);
compile(jsonSchema, 'Config', {
unreachableDefinitions: true,
bannerComment: '/* tslint:disable */\n/**\n* This file was automatically generated by Payload CMS.\n* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,\n* and re-run `payload generate:types` to regenerate this file.\n*/',
style: {
singleQuote: true,

View File

@@ -1,8 +1,24 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import minimist from 'minimist';
import swcRegister from '@swc/register';
import { generateTypes } from './generateTypes';
import { generateGraphQLSchema } from './generateGraphQLSchema';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - bad @swc/register types
swcRegister({
sourceMaps: 'inline',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
},
module: {
type: 'commonjs',
},
});
const { build } = require('./build');
const args = minimist(process.argv.slice(2));

View File

@@ -323,10 +323,12 @@ export type AuthCollection = {
export type TypeWithID = {
id: string | number
[key: string]: unknown
}
export type TypeWithTimestamps = {
id: string | number
createdAt: string
updatedAt: string
[key: string]: unknown
}

View File

@@ -1,6 +1,6 @@
import DataLoader, { BatchLoadFn } from 'dataloader';
import { PayloadRequest } from '../express/types';
import { TypeWithID } from '../globals/config/types';
import { TypeWithID } from './config/types';
import { isValidID } from '../utilities/isValidID';
import { getIDType } from '../utilities/getIDType';
import { fieldAffectsData } from '../fields/config/types';

View File

@@ -28,7 +28,7 @@ import resetPassword from '../../auth/graphql/resolvers/resetPassword';
import verifyEmail from '../../auth/graphql/resolvers/verifyEmail';
import unlock from '../../auth/graphql/resolvers/unlock';
import refresh from '../../auth/graphql/resolvers/refresh';
import { Payload } from '../..';
import { Payload } from '../../payload';
import { Field, fieldAffectsData } from '../../fields/config/types';
import buildObjectType, { ObjectTypeConfig } from '../../graphql/schema/buildObjectType';
import buildWhereInputType from '../../graphql/schema/buildWhereInputType';

View File

@@ -1,11 +1,12 @@
/* eslint-disable no-param-reassign */
import { Response } from 'express';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import { Collection } from '../../config/types';
import create from '../../operations/create';
export type Resolver = (_: unknown, args: {
data: Record<string, unknown>,
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (_: unknown, args: {
data: Omit<GeneratedTypes['collections'][TSlug], 'id'>,
locale?: string
draft: boolean
},
@@ -13,9 +14,11 @@ export type Resolver = (_: unknown, args: {
req: PayloadRequest,
res: Response
}
) => Promise<Document>
) => Promise<GeneratedTypes['collections'][TSlug]>
export default function createResolver(collection: Collection): Resolver {
export default function createResolver<TSlug extends keyof GeneratedTypes['collections']>(
collection: Collection,
): Resolver<TSlug> {
return async function resolver(_, args, context) {
if (args.locale) {
context.req.locale = args.locale;

View File

@@ -1,10 +1,11 @@
/* eslint-disable no-param-reassign */
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Response } from 'express';
import { PayloadRequest } from '../../../express/types';
import { Collection } from '../../config/types';
import deleteOperation from '../../operations/delete';
export type Resolver = (
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
_: unknown,
args: {
locale?: string
@@ -14,9 +15,11 @@ export type Resolver = (
req: PayloadRequest,
res: Response
}
) => Promise<Document>
) => Promise<GeneratedTypes['collections'][TSlug]>
export default function getDeleteResolver(collection: Collection): Resolver {
export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['collections']>(
collection: Collection,
): Resolver<TSlug> {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;

View File

@@ -1,8 +1,9 @@
import { Config as SchemaConfig } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import { Collection } from '../../config/types';
import findByID from '../../operations/findByID';
export type Resolver = (_: unknown, args: {
export type Resolver<T> = (_: unknown, args: {
locale?: string
draft: boolean
id: string
@@ -12,9 +13,9 @@ export type Resolver = (_: unknown, args: {
req: PayloadRequest,
res: Response
}
) => Promise<Document>
) => Promise<T>
export default function findByIDResolver(collection: Collection): Resolver {
export default function findByIDResolver<T extends keyof SchemaConfig['collections']>(collection: Collection): Resolver<SchemaConfig['collections'][T]> {
return async function resolver(_, args, context) {
const { req } = context;
if (args.locale) req.locale = args.locale;

View File

@@ -1,12 +1,13 @@
/* eslint-disable no-param-reassign */
import { Response } from 'express';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Collection } from '../../config/types';
import update from '../../operations/update';
import { PayloadRequest } from '../../../express/types';
export type Resolver = (_: unknown, args: {
export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (_: unknown, args: {
id: string | number
data: Record<string, unknown>,
data: GeneratedTypes['collections'][TSlug]
locale?: string
draft: boolean
autosave: boolean
@@ -15,9 +16,11 @@ export type Resolver = (_: unknown, args: {
req: PayloadRequest,
res: Response
}
) => Promise<Document>
) => Promise<GeneratedTypes['collections'][TSlug]>
export default function updateResolver(collection: Collection): Resolver {
export default function updateResolver<TSlug extends keyof GeneratedTypes['collections']>(
collection: Collection,
): Resolver<TSlug> {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
@@ -32,7 +35,7 @@ export default function updateResolver(collection: Collection): Resolver {
autosave: args.autosave,
};
const result = await update(options);
const result = await update<TSlug>(options);
return result;
}

View File

@@ -0,0 +1,41 @@
import express from 'express';
import passport from 'passport';
import apiKeyStrategy from '../auth/strategies/apiKey';
import bindCollectionMiddleware from './bindCollection';
import { SanitizedCollectionConfig } from './config/types';
import mountEndpoints from '../express/mountEndpoints';
import buildEndpoints from './buildEndpoints';
import { Payload } from '../payload';
export default function initCollectionsHTTP(ctx: Payload): void {
ctx.config.collections = ctx.config.collections.map((collection: SanitizedCollectionConfig) => {
const formattedCollection = collection;
const router = express.Router();
const { slug } = collection;
router.all('*', bindCollectionMiddleware(ctx.collections[formattedCollection.slug]));
if (collection.auth) {
const AuthCollection = ctx.collections[formattedCollection.slug];
if (collection.auth.useAPIKey) {
passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(ctx, AuthCollection));
}
if (Array.isArray(collection.auth.strategies)) {
collection.auth.strategies.forEach(({ name, strategy }, index) => {
const passportStrategy = typeof strategy === 'object' ? strategy : strategy(ctx);
passport.use(`${AuthCollection.config.slug}-${name ?? index}`, passportStrategy);
});
}
}
const endpoints = buildEndpoints(collection);
mountEndpoints(ctx.express, router, endpoints);
ctx.router.use(`/${slug}`, router);
return formattedCollection;
});
}

View File

@@ -1,21 +1,16 @@
import mongoose, { UpdateAggregationStage, UpdateQuery } from 'mongoose';
import paginate from 'mongoose-paginate-v2';
import express from 'express';
import passport from 'passport';
import passportLocalMongoose from 'passport-local-mongoose';
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2';
import { buildVersionCollectionFields } from '../versions/buildCollectionFields';
import buildQueryPlugin from '../mongoose/buildQuery';
import apiKeyStrategy from '../auth/strategies/apiKey';
import buildCollectionSchema from './buildSchema';
import buildSchema from '../mongoose/buildSchema';
import bindCollectionMiddleware from './bindCollection';
import { CollectionModel, SanitizedCollectionConfig } from './config/types';
import { Payload } from '../index';
import { Payload } from '../payload';
import { getVersionsModelName } from '../versions/getVersionsModelName';
import mountEndpoints from '../express/mountEndpoints';
import buildEndpoints from './buildEndpoints';
export default function registerCollections(ctx: Payload): void {
export default function initCollectionsLocal(ctx: Payload): void {
ctx.config.collections = ctx.config.collections.map((collection: SanitizedCollectionConfig) => {
const formattedCollection = collection;
@@ -74,7 +69,7 @@ export default function registerCollections(ctx: Payload): void {
disableUnique: true,
draftsEnabled: true,
options: {
timestamps: true,
timestamps: false,
},
},
);
@@ -82,6 +77,10 @@ export default function registerCollections(ctx: Payload): void {
versionSchema.plugin(paginate, { useEstimatedCount: true })
.plugin(buildQueryPlugin);
if (collection.versions?.drafts) {
versionSchema.plugin(mongooseAggregatePaginate);
}
ctx.versions[collection.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
}
@@ -91,34 +90,6 @@ export default function registerCollections(ctx: Payload): void {
config: formattedCollection,
};
// If not local, open routes
if (!ctx.local) {
const router = express.Router();
const { slug } = collection;
router.all('*', bindCollectionMiddleware(ctx.collections[formattedCollection.slug]));
if (collection.auth) {
const AuthCollection = ctx.collections[formattedCollection.slug];
if (collection.auth.useAPIKey) {
passport.use(`${AuthCollection.config.slug}-api-key`, apiKeyStrategy(ctx, AuthCollection));
}
if (Array.isArray(collection.auth.strategies)) {
collection.auth.strategies.forEach(({ name, strategy }, index) => {
const passportStrategy = typeof strategy === 'object' ? strategy : strategy(ctx);
passport.use(`${AuthCollection.config.slug}-${name ?? index}`, passportStrategy);
});
}
}
const endpoints = buildEndpoints(collection);
mountEndpoints(ctx.express, router, endpoints);
ctx.router.use(`/${slug}`, router);
}
return formattedCollection;
});
}

View File

@@ -1,5 +1,5 @@
import crypto from 'crypto';
import { Config as GeneratedTypes } from 'payload/generated-types';
import executeAccess from '../../auth/executeAccess';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
@@ -16,20 +16,24 @@ import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead';
import { generateFileData } from '../../uploads/generateFileData';
import { saveVersion } from '../../versions/saveVersion';
export type Arguments = {
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
collection: Collection
req: PayloadRequest
depth?: number
disableVerificationEmail?: boolean
overrideAccess?: boolean
showHiddenFields?: boolean
data: Record<string, unknown>
data: Omit<T, 'id'>
overwriteExistingFiles?: boolean
draft?: boolean
autosave?: boolean
}
async function create(incomingArgs: Arguments): Promise<Document> {
async function create<TSlug extends keyof GeneratedTypes['collections']>(
incomingArgs: Arguments<GeneratedTypes['collections'][TSlug]>,
): Promise<GeneratedTypes['collections'][TSlug]> {
let args = incomingArgs;
// /////////////////////////////////////
@@ -65,6 +69,7 @@ async function create(incomingArgs: Arguments): Promise<Document> {
showHiddenFields,
overwriteExistingFiles = false,
draft = false,
autosave = false,
} = args;
let { data } = args;
@@ -159,7 +164,7 @@ async function create(incomingArgs: Arguments): Promise<Document> {
// beforeChange - Fields
// /////////////////////////////////////
const resultWithLocales = await beforeChange({
const resultWithLocales = await beforeChange<Record<string, unknown>>({
data,
doc: {},
docWithLocales: {},
@@ -213,6 +218,23 @@ async function create(incomingArgs: Arguments): Promise<Document> {
result = JSON.parse(result);
result = sanitizeInternalFields(result);
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
await saveVersion({
payload,
collection: collectionConfig,
req,
id: result.id,
docWithLocales: result,
autosave,
createdAt: result.createdAt,
onCreate: true,
});
}
// /////////////////////////////////////
// Send verification email if applicable
// /////////////////////////////////////

View File

@@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../express/types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { NotFound, Forbidden, ErrorDeletingFile } from '../../errors';
@@ -11,6 +12,7 @@ import { hasWhereAccessResult } from '../../auth/types';
import { FileData } from '../../uploads/types';
import fileExists from '../../uploads/fileExists';
import { afterRead } from '../../fields/hooks/afterRead';
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions';
export type Arguments = {
depth?: number
@@ -21,7 +23,9 @@ export type Arguments = {
showHiddenFields?: boolean
}
async function deleteOperation(incomingArgs: Arguments): Promise<Document> {
async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']>(
incomingArgs: Arguments,
): Promise<GeneratedTypes['collections'][TSlug]> {
let args = incomingArgs;
// /////////////////////////////////////
@@ -48,6 +52,7 @@ async function deleteOperation(incomingArgs: Arguments): Promise<Document> {
req: {
t,
locale,
payload,
payload: {
config,
preferences,
@@ -164,6 +169,18 @@ async function deleteOperation(incomingArgs: Arguments): Promise<Document> {
}
await preferences.Model.deleteMany({ key: `collection-${collectionConfig.slug}-${id}` });
// /////////////////////////////////////
// Delete versions
// /////////////////////////////////////
if (!collectionConfig.versions.retainDeleted) {
deleteCollectionVersions({
payload,
id,
slug: collectionConfig.slug,
});
}
// /////////////////////////////////////
// afterDelete - Collection
// /////////////////////////////////////
@@ -174,7 +191,6 @@ async function deleteOperation(incomingArgs: Arguments): Promise<Document> {
result = await hook({ req, id, doc: result }) || result;
}, Promise.resolve());
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////

View File

@@ -9,7 +9,7 @@ import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import { buildSortParam } from '../../mongoose/buildSortParam';
import { AccessResult } from '../../config/types';
import { afterRead } from '../../fields/hooks/afterRead';
import { mergeDrafts } from '../../versions/drafts/mergeDrafts';
import { queryDrafts } from '../../versions/drafts/queryDrafts';
export type Arguments = {
collection: Collection
@@ -28,7 +28,9 @@ export type Arguments = {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promise<PaginatedDocs<T>> {
async function find<T extends TypeWithID>(
incomingArgs: Arguments,
): Promise<PaginatedDocs<T>> {
let args = incomingArgs;
// /////////////////////////////////////
@@ -160,13 +162,12 @@ async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promis
};
if (collectionConfig.versions?.drafts && draftsEnabled) {
result = await mergeDrafts({
result = await queryDrafts<T>({
accessResult,
collection,
locale,
paginationOptions,
payload,
query,
where,
});
} else {

View File

@@ -22,8 +22,9 @@ export type Arguments = {
draft?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function findByID<T extends TypeWithID = any>(incomingArgs: Arguments): Promise<T> {
async function findByID<T extends TypeWithID>(
incomingArgs: Arguments,
): Promise<T> {
let args = incomingArgs;
// /////////////////////////////////////

View File

@@ -23,7 +23,9 @@ export type Arguments = {
showHiddenFields?: boolean
}
async function findVersions<T extends TypeWithVersion<T> = any>(args: Arguments): Promise<PaginatedDocs<T>> {
async function findVersions<T extends TypeWithVersion<T>>(
args: Arguments,
): Promise<PaginatedDocs<T>> {
const {
where,
page,

View File

@@ -1,5 +1,6 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { UploadedFile } from 'express-fileupload';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
@@ -9,9 +10,9 @@ import { File } from '../../../uploads/types';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T> = {
collection: string
data: Record<string, unknown>
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug
data: Omit<GeneratedTypes['collections'][TSlug], 'id'>
depth?: number
locale?: string
fallbackLocale?: string
@@ -26,7 +27,10 @@ export type Options<T> = {
draft?: boolean
}
export default async function createLocal<T = any>(payload: Payload, options: Options<T>): Promise<T> {
export default async function createLocal<TSlug extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<TSlug>,
): Promise<GeneratedTypes['collections'][TSlug]> {
const {
collection: collectionSlug,
depth,
@@ -48,7 +52,7 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';
@@ -65,7 +69,7 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
if (!req.t) req.t = req.i18n.t;
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req);
return create({
return create<TSlug>({
depth,
data,
collection,

View File

@@ -1,14 +1,14 @@
import { TypeWithID } from '../../config/types';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Document } from '../../../types';
import { PayloadRequest } from '../../../express/types';
import { Payload } from '../../../index';
import { Payload } from '../../../payload';
import deleteOperation from '../delete';
import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
id: string
depth?: number
locale?: string
@@ -18,7 +18,10 @@ export type Options = {
showHiddenFields?: boolean
}
export default async function deleteLocal<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<T> {
export default async function deleteLocal<TSlug extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<TSlug>,
): Promise<GeneratedTypes['collections'][TSlug]> {
const {
collection: collectionSlug,
depth,
@@ -35,7 +38,7 @@ export default async function deleteLocal<T extends TypeWithID = any>(payload: P
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
const req = {
@@ -50,7 +53,7 @@ export default async function deleteLocal<T extends TypeWithID = any>(payload: P
if (!req.t) req.t = req.i18n.t;
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req);
return deleteOperation({
return deleteOperation<TSlug>({
depth,
id,
collection,

View File

@@ -1,15 +1,15 @@
import { TypeWithID } from '../../config/types';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PaginatedDocs } from '../../../mongoose/types';
import { Document, Where } from '../../../types';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { PayloadRequest } from '../../../express/types';
import find from '../find';
import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
depth?: number
currentDepth?: number
page?: number
@@ -27,8 +27,10 @@ export type Options = {
req?: PayloadRequest
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default async function findLocal<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<PaginatedDocs<T>> {
export default async function findLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<PaginatedDocs<GeneratedTypes['collections'][T]>> {
const {
collection: collectionSlug,
depth,
@@ -52,7 +54,7 @@ export default async function findLocal<T extends TypeWithID = any>(payload: Pay
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';
@@ -66,7 +68,7 @@ export default async function findLocal<T extends TypeWithID = any>(payload: Pay
if (typeof user !== 'undefined') req.user = user;
return find({
return find<GeneratedTypes['collections'][T]>({
depth,
currentDepth,
sort,

View File

@@ -1,14 +1,14 @@
import { TypeWithID } from '../../config/types';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
import findByID from '../findByID';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
id: string
depth?: number
currentDepth?: number
@@ -22,8 +22,10 @@ export type Options = {
draft?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default async function findByIDLocal<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<T> {
export default async function findByIDLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<GeneratedTypes['collections'][T]> {
const {
collection: collectionSlug,
depth,
@@ -43,7 +45,7 @@ export default async function findByIDLocal<T extends TypeWithID = any>(payload:
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';
@@ -57,7 +59,7 @@ export default async function findByIDLocal<T extends TypeWithID = any>(payload:
if (!req.t) req.t = req.i18n.t;
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req);
return findByID({
return findByID<GeneratedTypes['collections'][T]>({
depth,
currentDepth,
id,

View File

@@ -1,4 +1,5 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { Document } from '../../../types';
import { PayloadRequest } from '../../../express/types';
import { TypeWithVersion } from '../../../versions/types';
@@ -7,8 +8,8 @@ import { getDataLoader } from '../../dataloader';
import i18n from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
id: string
depth?: number
locale?: string
@@ -20,7 +21,10 @@ export type Options = {
req?: PayloadRequest
}
export default async function findVersionByIDLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<T> {
export default async function findVersionByIDLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<TypeWithVersion<GeneratedTypes['collections'][T]>> {
const {
collection: collectionSlug,
depth,
@@ -37,7 +41,7 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
req.payloadAPI = 'local';

View File

@@ -1,4 +1,5 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { Document, Where } from '../../../types';
import { PaginatedDocs } from '../../../mongoose/types';
import { TypeWithVersion } from '../../../versions/types';
@@ -8,8 +9,8 @@ import { getDataLoader } from '../../dataloader';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
depth?: number
page?: number
limit?: number
@@ -22,7 +23,10 @@ export type Options = {
where?: Where
}
export default async function findVersionsLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<PaginatedDocs<T>> {
export default async function findVersionsLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<PaginatedDocs<TypeWithVersion<GeneratedTypes['collections'][T]>>> {
const {
collection: collectionSlug,
depth,
@@ -41,7 +45,7 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
const defaultLocale = payload?.config?.localization ? payload?.config?.localization?.defaultLocale : null;
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n);

View File

@@ -1,14 +1,14 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
import { TypeWithVersion } from '../../../versions/types';
import { getDataLoader } from '../../dataloader';
import restoreVersion from '../restoreVersion';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
collection: string
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
id: string
depth?: number
locale?: string
@@ -18,7 +18,10 @@ export type Options = {
showHiddenFields?: boolean
}
export default async function restoreVersionLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<T> {
export default async function restoreVersionLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<GeneratedTypes['collections'][T]> {
const {
collection: collectionSlug,
depth,
@@ -33,7 +36,7 @@ export default async function restoreVersionLocal<T extends TypeWithVersion<T> =
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n);

View File

@@ -1,4 +1,5 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
import update from '../update';
@@ -8,10 +9,10 @@ import { File } from '../../../uploads/types';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options<T> = {
collection: string
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug
id: string | number
data: Partial<T>
data: Omit<GeneratedTypes['collections'][TSlug], 'id'>
depth?: number
locale?: string
fallbackLocale?: string
@@ -25,7 +26,10 @@ export type Options<T> = {
autosave?: boolean
}
export default async function updateLocal<T = any>(payload: Payload, options: Options<T>): Promise<T> {
export default async function updateLocal<TSlug extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<TSlug>,
): Promise<GeneratedTypes['collections'][TSlug]> {
const {
collection: collectionSlug,
depth,
@@ -46,7 +50,7 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
const collection = payload.collections[collectionSlug];
if (!collection) {
throw new APIError(`The collection with slug ${collectionSlug} can't be found.`);
throw new APIError(`The collection with slug ${String(collectionSlug)} can't be found.`);
}
const i18n = i18nInit(payload.config.i18n);
@@ -81,5 +85,5 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
req,
};
return update(args);
return update<TSlug>(args);
}

View File

@@ -1,4 +1,5 @@
import httpStatus from 'http-status';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Where, Document } from '../../types';
import { Collection } from '../config/types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
@@ -6,11 +7,8 @@ import executeAccess from '../../auth/executeAccess';
import { NotFound, Forbidden, APIError, ValidationError } from '../../errors';
import { PayloadRequest } from '../../express/types';
import { hasWhereAccessResult } from '../../auth/types';
import { saveCollectionDraft } from '../../versions/drafts/saveCollectionDraft';
import { saveCollectionVersion } from '../../versions/saveCollectionVersion';
import { saveVersion } from '../../versions/saveVersion';
import { uploadFiles } from '../../uploads/uploadFiles';
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
import { ensurePublishedCollectionVersion } from '../../versions/ensurePublishedCollectionVersion';
import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange';
@@ -18,11 +16,11 @@ import { afterRead } from '../../fields/hooks/afterRead';
import { generateFileData } from '../../uploads/generateFileData';
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion';
export type Arguments = {
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
collection: Collection
req: PayloadRequest
id: string | number
data: Record<string, unknown>
data: Omit<T, 'id'>
depth?: number
disableVerificationEmail?: boolean
overrideAccess?: boolean
@@ -32,7 +30,9 @@ export type Arguments = {
autosave?: boolean
}
async function update(incomingArgs: Arguments): Promise<Document> {
async function update<TSlug extends keyof GeneratedTypes['collections']>(
incomingArgs: Arguments<GeneratedTypes['collections'][TSlug]>,
): Promise<GeneratedTypes['collections'][TSlug]> {
let args = incomingArgs;
// /////////////////////////////////////
@@ -147,7 +147,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
// beforeValidate - Fields
// /////////////////////////////////////
data = await beforeValidate({
data = await beforeValidate<GeneratedTypes['collections'][TSlug]>({
data,
doc: originalDoc,
entityConfig: collectionConfig,
@@ -199,7 +199,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
// beforeChange - Fields
// /////////////////////////////////////
let result = await beforeChange({
let result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
data,
doc: originalDoc,
docWithLocales,
@@ -215,50 +215,17 @@ async function update(incomingArgs: Arguments): Promise<Document> {
// /////////////////////////////////////
if (shouldSavePassword) {
await doc.setPassword(password as string);
await doc.setPassword(password);
await doc.save();
delete data.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
// /////////////////////////////////////
if (shouldSaveDraft) {
await ensurePublishedCollectionVersion({
payload,
config: collectionConfig,
req,
docWithLocales,
id,
});
result = await saveCollectionDraft({
payload,
config: collectionConfig,
req,
data: result,
id,
autosave,
});
} else {
if (!shouldSaveDraft) {
try {
result = await Model.findByIdAndUpdate(
{ _id: id },
@@ -266,27 +233,34 @@ async function update(incomingArgs: Arguments): Promise<Document> {
{ new: true },
);
} catch (error) {
cleanUpFailedVersion({
payload,
entityConfig: collectionConfig,
version: createdVersion,
});
// Handle uniqueness error from MongoDB
throw error.code === 11000 && error.keyValue
? new ValidationError([{ message: 'Value must be unique', field: Object.keys(error.keyValue)[0] }], t)
: error;
}
const resultString = JSON.stringify(result);
result = JSON.parse(resultString);
// custom id type reset
result.id = result._id;
}
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
// /////////////////////////////////////
@@ -317,7 +291,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange({
result = await afterChange<GeneratedTypes['collections'][TSlug]>({
data,
doc: result,
previousDoc: originalDoc,

View File

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

View File

@@ -1,7 +1,6 @@
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
// eslint-disable-next-line import/no-extraneous-dependencies
import swcRegister from '@swc/register';
import path from 'path';
import pino from 'pino';
import Logger from '../utilities/logger';
@@ -15,24 +14,6 @@ const loadConfig = (logger?: pino.Logger): SanitizedConfig => {
const configPath = findConfig();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
swcRegister({
sourceMaps: 'inline',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
},
ignore: [
/node_modules[\\/](?!.pnpm[\\/].*[\\/]node_modules[\\/])(?!payload[\\/]dist[\\/]admin|payload[\\/]components).*/,
],
module: {
type: 'commonjs',
},
});
clientFiles.forEach((ext) => {
require.extensions[ext] = () => null;
});

View File

@@ -9,7 +9,7 @@ import { ConnectOptions } from 'mongoose';
import React from 'react';
import { LoggerOptions } from 'pino';
import type { InitOptions as i18nInitOptions } from 'i18next';
import { Payload } from '..';
import { Payload } from '../payload';
import {
AfterErrorHook,
CollectionConfig,

View File

@@ -1,7 +1,7 @@
import { Message } from './types';
import { SendMailOptions } from 'nodemailer';
import logger from '../utilities/logger';
export default async function sendEmail(message: Message): Promise<unknown> {
export default async function sendEmail(message: SendMailOptions): Promise<unknown> {
let result;
try {
const email = await this.email;

View File

@@ -3,7 +3,7 @@ import compression from 'compression';
import history from 'connect-history-api-fallback';
import path from 'path';
import initWebpack from '../webpack/init';
import { Payload } from '../index';
import { Payload } from '../payload';
const router = express.Router();

View File

@@ -9,7 +9,7 @@ import rateLimit from 'express-rate-limit';
import localizationMiddleware from '../../localization/middleware';
import authenticate from './authenticate';
import identifyAPI from './identifyAPI';
import { Payload } from '../..';
import { Payload } from '../../payload';
import { PayloadRequest } from '../types';
import corsHeaders from './corsHeaders';
import convertPayload from './convertPayload';

View File

@@ -3,7 +3,7 @@ import passport from 'passport';
import path from 'path';
import getExecuteStaticAccess from '../auth/getExecuteStaticAccess';
import authenticate from './middleware/authenticate';
import { Payload } from '../index';
import { Payload } from '../payload';
import corsHeaders from './middleware/corsHeaders';
function initStatic(ctx: Payload): void {

View File

@@ -2,11 +2,10 @@ import { Request } from 'express';
import type { i18n as Ii18n, TFunction } from 'i18next';
import DataLoader from 'dataloader';
import { UploadedFile } from 'express-fileupload';
import { Payload } from '../index';
import { Collection } from '../collections/config/types';
import { Payload } from '../payload';
import { Collection, TypeWithID } from '../collections/config/types';
import { User } from '../auth/types';
import { Document } from '../types';
import { TypeWithID } from '../globals/config/types';
/** Express request with some Payload related context added */
export declare type PayloadRequest<U = any> = Request & {

View File

@@ -9,7 +9,7 @@ import { PayloadRequest } from '../../express/types';
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
import { Description } from '../../admin/components/forms/FieldDescription/types';
import { User } from '../../auth';
import { Payload } from '../..';
import { Payload } from '../../payload';
import { RowLabel } from '../../admin/components/forms/RowLabel/types';
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {

View File

@@ -4,23 +4,23 @@ import { PayloadRequest } from '../../../express/types';
import { traverseFields } from './traverseFields';
import deepCopyObject from '../../../utilities/deepCopyObject';
type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
previousDoc: Record<string, unknown>
type Args<T> = {
data: Omit<T, 'id'>
doc: T
previousDoc: T
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
operation: 'create' | 'update'
req: PayloadRequest
}
export const afterChange = async ({
export const afterChange = async <T extends Record<string, unknown>>({
data,
doc: incomingDoc,
previousDoc,
entityConfig,
operation,
req,
}: Args): Promise<Record<string, unknown>> => {
}: Args<T>): Promise<T> => {
const doc = deepCopyObject(incomingDoc);
await traverseFields({

View File

@@ -6,9 +6,9 @@ import { traverseFields } from './traverseFields';
import { ValidationError } from '../../../errors';
import deepCopyObject from '../../../utilities/deepCopyObject';
type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
type Args<T> = {
data: Omit<T, 'id'>
doc: T
docWithLocales: Record<string, unknown>
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
id?: string | number
@@ -17,7 +17,7 @@ type Args = {
skipValidation?: boolean
}
export const beforeChange = async ({
export const beforeChange = async <T extends Record<string, unknown>>({
data: incomingData,
doc,
docWithLocales,
@@ -26,7 +26,7 @@ export const beforeChange = async ({
operation,
req,
skipValidation,
}: Args): Promise<Record<string, unknown>> => {
}: Args<T>): Promise<T> => {
const data = deepCopyObject(incomingData);
const mergeLocaleActions = [];
const errors: { message: string, field: string }[] = [];

View File

@@ -4,9 +4,9 @@ import { PayloadRequest } from '../../../express/types';
import { traverseFields } from './traverseFields';
import deepCopyObject from '../../../utilities/deepCopyObject';
type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
type Args<T> = {
data: Omit<T, 'id'>
doc?: T | Record<string, unknown>
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
id?: string | number
operation: 'create' | 'update'
@@ -14,7 +14,7 @@ type Args = {
req: PayloadRequest
}
export const beforeValidate = async ({
export const beforeValidate = async <T extends Record<string, unknown>>({
data: incomingData,
doc,
entityConfig,
@@ -22,7 +22,7 @@ export const beforeValidate = async ({
operation,
overrideAccess,
req,
}: Args): Promise<Record<string, unknown>> => {
}: Args<T>): Promise<T> => {
const data = deepCopyObject(incomingData);
await traverseFields({

View File

@@ -3,9 +3,9 @@ import { PayloadRequest } from '../../../express/types';
import { Field, fieldAffectsData, TabAsField, tabHasName, valueIsValueWithRelation } from '../../config/types';
import { traverseFields } from './traverseFields';
type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
type Args<T> = {
data: T
doc: T
field: Field | TabAsField
id?: string | number
operation: 'create' | 'update'
@@ -20,7 +20,7 @@ type Args = {
// - Execute field hooks
// - Execute field access control
export const promise = async ({
export const promise = async <T>({
data,
doc,
field,
@@ -30,7 +30,7 @@ export const promise = async ({
req,
siblingData,
siblingDoc,
}: Args): Promise<void> => {
}: Args<T>): Promise<void> => {
if (fieldAffectsData(field)) {
if (field.name === 'id') {
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {

View File

@@ -2,9 +2,9 @@ import { PayloadRequest } from '../../../express/types';
import { Field, TabAsField } from '../../config/types';
import { promise } from './promise';
type Args = {
data: Record<string, unknown>
doc: Record<string, unknown>
type Args<T> = {
data: T
doc: T
fields: (Field | TabAsField)[]
id?: string | number
operation: 'create' | 'update'
@@ -14,7 +14,7 @@ type Args = {
siblingDoc: Record<string, unknown>
}
export const traverseFields = async ({
export const traverseFields = async <T>({
data,
doc,
fields,
@@ -24,7 +24,7 @@ export const traverseFields = async ({
req,
siblingData,
siblingDoc,
}: Args): Promise<void> => {
}: Args<T>): Promise<void> => {
const promises = [];
fields.forEach((field) => {
promises.push(promise({

View File

@@ -212,7 +212,7 @@ const validateFilterOptions: Validate = async (value, { t, filterOptions, id, us
}
});
const result = await payload.find<TypeWithID>({
const result = await payload.find({
collection,
depth: 0,
where: {

15
src/generated-types.ts Normal file
View File

@@ -0,0 +1,15 @@
// This is a stub for Payload's generated types.
// It will not be used.
// Instead, configure a path within your `tsconfig.json`'s `compilerOptions.paths` to point to your generated types.
import { TypeWithID } from './collections/config/types';
import { TypeWithID as GlobalTypeWithID } from './globals/config/types';
export type Config = {
collections: {
[slug: string | number | symbol]: TypeWithID
}
globals: {
[slug: string | number | symbol]: GlobalTypeWithID
}
}

View File

@@ -9,7 +9,7 @@ import updateResolver from './resolvers/update';
import findVersionByIDResolver from './resolvers/findVersionByID';
import findVersionsResolver from './resolvers/findVersions';
import restoreVersionResolver from './resolvers/restoreVersion';
import { Payload } from '../..';
import { Payload } from '../../payload';
import buildObjectType from '../../graphql/schema/buildObjectType';
import buildMutationInputType from '../../graphql/schema/buildMutationInputType';
import buildWhereInputType from '../../graphql/schema/buildWhereInputType';

View File

@@ -1,24 +1,26 @@
/* eslint-disable no-param-reassign */
import { Config as GeneratedTypes } from 'payload/generated-types';
import { PayloadRequest } from '../../../express/types';
import { SanitizedGlobalConfig } from '../../config/types';
import update from '../../operations/update';
type Resolver = (
type Resolver<TSlug extends keyof GeneratedTypes['globals']> = (
_: unknown,
args: {
locale?: string
fallbackLocale?: string
data?: Record<string, unknown>
data?: GeneratedTypes['globals'][TSlug]
draft?: boolean
},
context: {
req: PayloadRequest,
res: Response
}
) => Promise<Document>
) => Promise<GeneratedTypes['globals'][TSlug]>
export default function updateResolver(globalConfig: SanitizedGlobalConfig): Resolver {
export default function updateResolver<TSlug extends keyof GeneratedTypes['globals']>(
globalConfig: SanitizedGlobalConfig,
): Resolver<TSlug> {
return async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
@@ -34,7 +36,7 @@ export default function updateResolver(globalConfig: SanitizedGlobalConfig): Res
draft: args.draft,
};
const result = await update(options);
const result = await update<TSlug>(options);
return result;
};
}

19
src/globals/initHTTP.ts Normal file
View File

@@ -0,0 +1,19 @@
import express from 'express';
import mountEndpoints from '../express/mountEndpoints';
import buildEndpoints from './buildEndpoints';
import { SanitizedGlobalConfig } from './config/types';
import { Payload } from '../payload';
export default function initGlobals(ctx: Payload): void {
if (ctx.config.globals) {
ctx.config.globals.forEach((global: SanitizedGlobalConfig) => {
const router = express.Router();
const { slug } = global;
const endpoints = buildEndpoints(global);
mountEndpoints(ctx.express, router, endpoints);
ctx.router.use(`/globals/${slug}`, router);
});
}
}

View File

@@ -1,18 +1,14 @@
import express from 'express';
import mongoose from 'mongoose';
import paginate from 'mongoose-paginate-v2';
import buildQueryPlugin from '../mongoose/buildQuery';
import buildModel from './buildModel';
import { Payload } from '../index';
import { Payload } from '../payload';
import { getVersionsModelName } from '../versions/getVersionsModelName';
import { buildVersionGlobalFields } from '../versions/buildGlobalFields';
import buildSchema from '../mongoose/buildSchema';
import { CollectionModel } from '../collections/config/types';
import mountEndpoints from '../express/mountEndpoints';
import buildEndpoints from './buildEndpoints';
import { SanitizedGlobalConfig } from './config/types';
export default function initGlobals(ctx: Payload): void {
export default function initGlobalsLocal(ctx: Payload): void {
if (ctx.config.globals) {
ctx.globals = {
Model: buildModel(ctx.config),
@@ -30,7 +26,7 @@ export default function initGlobals(ctx: Payload): void {
disableUnique: true,
draftsEnabled: true,
options: {
timestamps: true,
timestamps: false,
},
},
);
@@ -41,18 +37,5 @@ export default function initGlobals(ctx: Payload): void {
ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel;
}
});
// If not local, open routes
if (!ctx.local) {
ctx.config.globals.forEach((global: SanitizedGlobalConfig) => {
const router = express.Router();
const { slug } = global;
const endpoints = buildEndpoints(global);
mountEndpoints(ctx.express, router, endpoints);
ctx.router.use(`/globals/${slug}`, router);
});
}
}
}

View File

@@ -5,7 +5,7 @@ import { AccessResult } from '../../config/types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
import { afterRead } from '../../fields/hooks/afterRead';
import { SanitizedGlobalConfig, TypeWithID } from '../config/types';
import { SanitizedGlobalConfig } from '../config/types';
import { PayloadRequest } from '../../express/types';
type Args = {
@@ -19,7 +19,7 @@ type Args = {
overrideAccess?: boolean
}
async function findOne<T extends TypeWithID = any>(args: Args): Promise<T> {
async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T> {
const {
globalConfig,
locale,

View File

@@ -6,10 +6,10 @@ import { PaginatedDocs } from '../../mongoose/types';
import { hasWhereAccessResult } from '../../auth/types';
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import { buildSortParam } from '../../mongoose/buildSortParam';
import { TypeWithVersion } from '../../versions/types';
import { SanitizedGlobalConfig } from '../config/types';
import { afterRead } from '../../fields/hooks/afterRead';
import { buildVersionGlobalFields } from '../../versions/buildGlobalFields';
import { TypeWithVersion } from '../../versions/types';
export type Arguments = {
globalConfig: SanitizedGlobalConfig
@@ -23,7 +23,9 @@ export type Arguments = {
showHiddenFields?: boolean
}
async function findVersions<T extends TypeWithVersion<T> = any>(args: Arguments): Promise<PaginatedDocs<T>> {
async function findVersions<T extends TypeWithVersion<T>>(
args: Arguments,
): Promise<PaginatedDocs<T>> {
const {
where,
page,

View File

@@ -1,14 +1,14 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
import { TypeWithID } from '../../config/types';
import findOne from '../findOne';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
slug: string
export type Options<T extends keyof GeneratedTypes['globals']> = {
slug: T
depth?: number
locale?: string
fallbackLocale?: string
@@ -18,7 +18,10 @@ export type Options = {
draft?: boolean
}
export default async function findOneLocal<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<T> {
export default async function findOneLocal<T extends keyof GeneratedTypes['globals']>(
payload: Payload,
options: Options<T>,
): Promise<GeneratedTypes['globals'][T]> {
const {
slug: globalSlug,
depth,
@@ -35,7 +38,7 @@ export default async function findOneLocal<T extends TypeWithID = any>(payload:
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`);
}
const req = {
@@ -51,7 +54,7 @@ export default async function findOneLocal<T extends TypeWithID = any>(payload:
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req);
return findOne({
slug: globalSlug,
slug: globalSlug as string,
depth,
globalConfig,
overrideAccess,

View File

@@ -1,4 +1,5 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
@@ -7,8 +8,8 @@ import findVersionByID from '../findVersionByID';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
slug: string
export type Options<T extends keyof GeneratedTypes['globals']> = {
slug: T
id: string
depth?: number
locale?: string
@@ -19,7 +20,10 @@ export type Options = {
disableErrors?: boolean
}
export default async function findVersionByIDLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<T> {
export default async function findVersionByIDLocal<T extends keyof GeneratedTypes['globals']>(
payload: Payload,
options: Options<T>,
): Promise<TypeWithVersion<GeneratedTypes['globals'][T]>> {
const {
slug: globalSlug,
depth,
@@ -36,7 +40,7 @@ export default async function findVersionByIDLocal<T extends TypeWithVersion<T>
const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`);
}
const req = {

View File

@@ -1,15 +1,16 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Document, Where } from '../../../types';
import { PaginatedDocs } from '../../../mongoose/types';
import { TypeWithVersion } from '../../../versions/types';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
import { PayloadRequest } from '../../../express/types';
import findVersions from '../findVersions';
import { getDataLoader } from '../../../collections/dataloader';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
import { TypeWithVersion } from '../../../versions/types';
export type Options = {
slug: string
export type Options<T extends keyof GeneratedTypes['globals']> = {
slug: T
depth?: number
page?: number
limit?: number
@@ -22,7 +23,10 @@ export type Options = {
where?: Where
}
export default async function findVersionsLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<PaginatedDocs<T>> {
export default async function findVersionsLocal<T extends keyof GeneratedTypes['globals']>(
payload: Payload,
options: Options<T>,
): Promise<PaginatedDocs<TypeWithVersion<GeneratedTypes['globals'][T]>>> {
const {
slug: globalSlug,
depth,
@@ -41,7 +45,7 @@ export default async function findVersionsLocal<T extends TypeWithVersion<T> = a
const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`);
}
const req = {

View File

@@ -1,13 +1,13 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { getDataLoader } from '../../../collections/dataloader';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
import { TypeWithVersion } from '../../../versions/types';
import restoreVersion from '../restoreVersion';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
export type Options<T extends keyof GeneratedTypes['globals']> = {
slug: string
id: string
depth?: number
@@ -18,7 +18,10 @@ export type Options = {
showHiddenFields?: boolean
}
export default async function restoreVersionLocal<T extends TypeWithVersion<T> = any>(payload: Payload, options: Options): Promise<T> {
export default async function restoreVersionLocal<T extends keyof GeneratedTypes['globals']>(
payload: Payload,
options: Options<T>,
): Promise<GeneratedTypes['globals'][T]> {
const {
slug: globalSlug,
depth,
@@ -34,7 +37,7 @@ export default async function restoreVersionLocal<T extends TypeWithVersion<T> =
const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`);
}
const req = {

View File

@@ -1,25 +1,28 @@
import { Payload } from '../../..';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { Payload } from '../../../payload';
import { Document } from '../../../types';
import { PayloadRequest } from '../../../express/types';
import { TypeWithID } from '../../config/types';
import update from '../update';
import { getDataLoader } from '../../../collections/dataloader';
import i18nInit from '../../../translations/init';
import { APIError } from '../../../errors';
export type Options = {
slug: string
export type Options<TSlug extends keyof GeneratedTypes['globals']> = {
slug: TSlug
depth?: number
locale?: string
fallbackLocale?: string
data: Record<string, unknown>
data: Omit<GeneratedTypes['globals'][TSlug], 'id'>
user?: Document
overrideAccess?: boolean
showHiddenFields?: boolean
draft?: boolean
}
export default async function updateLocal<T extends TypeWithID = any>(payload: Payload, options: Options): Promise<T> {
export default async function updateLocal<TSlug extends keyof GeneratedTypes['globals']>(
payload: Payload,
options: Options<TSlug>,
): Promise<GeneratedTypes['globals'][TSlug]> {
const {
slug: globalSlug,
depth,
@@ -36,7 +39,7 @@ export default async function updateLocal<T extends TypeWithID = any>(payload: P
const i18n = i18nInit(payload.config.i18n);
if (!globalConfig) {
throw new APIError(`The global with slug ${globalSlug} can't be found.`);
throw new APIError(`The global with slug ${String(globalSlug)} can't be found.`);
}
const req = {
@@ -51,7 +54,7 @@ export default async function updateLocal<T extends TypeWithID = any>(payload: P
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req);
return update({
return update<TSlug>({
slug: globalSlug,
data,
depth,

View File

@@ -1,31 +1,31 @@
import { Config as GeneratedTypes } from 'payload/generated-types';
import { docHasTimestamps, Where } from '../../types';
import { SanitizedGlobalConfig, TypeWithID } from '../config/types';
import { SanitizedGlobalConfig } from '../config/types';
import executeAccess from '../../auth/executeAccess';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { saveGlobalVersion } from '../../versions/saveGlobalVersion';
import { saveGlobalDraft } from '../../versions/drafts/saveGlobalDraft';
import { ensurePublishedGlobalVersion } from '../../versions/ensurePublishedGlobalVersion';
import cleanUpFailedVersion from '../../versions/cleanUpFailedVersion';
import { hasWhereAccessResult } from '../../auth';
import { beforeChange } from '../../fields/hooks/beforeChange';
import { beforeValidate } from '../../fields/hooks/beforeValidate';
import { afterChange } from '../../fields/hooks/afterChange';
import { afterRead } from '../../fields/hooks/afterRead';
import { PayloadRequest } from '../../express/types';
import { saveVersion } from '../../versions/saveVersion';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
type Args = {
type Args<T extends { [field: string | number | symbol]: unknown }> = {
globalConfig: SanitizedGlobalConfig
slug: string
slug: string | number | symbol
req: PayloadRequest
depth?: number
overrideAccess?: boolean
showHiddenFields?: boolean
draft?: boolean
autosave?: boolean
data: Record<string, unknown>
data: Omit<T, 'id'>
}
async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
async function update<TSlug extends keyof GeneratedTypes['globals']>(
args: Args<GeneratedTypes['globals'][TSlug]>,
): Promise<GeneratedTypes['globals'][TSlug]> {
const {
globalConfig,
slug,
@@ -182,57 +182,20 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
skipValidation: shouldSaveDraft,
});
// /////////////////////////////////////
// Create version from existing doc
// /////////////////////////////////////
let createdVersion;
if (globalConfig.versions && !shouldSaveDraft) {
createdVersion = await saveGlobalVersion({
payload,
config: globalConfig,
req,
docWithLocales: result,
});
}
// /////////////////////////////////////
// Update
// /////////////////////////////////////
if (shouldSaveDraft) {
await ensurePublishedGlobalVersion({
payload,
config: globalConfig,
req,
docWithLocales: result,
});
global = await saveGlobalDraft({
payload,
config: globalConfig,
data: result,
autosave,
});
} else {
try {
if (existingGlobal) {
global = await Model.findOneAndUpdate(
{ globalType: slug },
result,
{ new: true },
);
} else {
result.globalType = slug;
global = await Model.create(result);
}
} catch (error) {
cleanUpFailedVersion({
payload,
entityConfig: globalConfig,
version: createdVersion,
});
if (!shouldSaveDraft) {
if (existingGlobal) {
global = await Model.findOneAndUpdate(
{ globalType: slug },
result,
{ new: true },
);
} else {
result.globalType = slug;
global = await Model.create(result);
}
}
@@ -240,6 +203,22 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
global = JSON.parse(global);
global = sanitizeInternalFields(global);
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (globalConfig.versions) {
global = await saveVersion({
payload,
global: globalConfig,
req,
docWithLocales: result,
autosave,
draft: shouldSaveDraft,
createdAt: global.createdAt,
});
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////

View File

@@ -1,5 +1,5 @@
import graphQLPlayground from 'graphql-playground-middleware-express';
import { Payload } from '../index';
import { Payload } from '../payload';
function initPlayground(ctx: Payload): void {
if ((!ctx.config.graphQL.disable && !ctx.config.graphQL.disablePlaygroundInProduction && process.env.NODE_ENV === 'production') || process.env.NODE_ENV !== 'production') {

View File

@@ -2,7 +2,7 @@
import * as GraphQL from 'graphql';
import { GraphQLObjectType, GraphQLSchema } from 'graphql';
import queryComplexity, { fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity';
import { Payload } from '..';
import { Payload } from '../payload';
import buildLocaleInputType from './schema/buildLocaleInputType';
import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType';
import initCollections from '../collections/graphql/init';

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */
import { Payload } from '../..';
import { Payload } from '../../payload';
import { Block } from '../../fields/config/types';
import buildObjectType from './buildObjectType';
import { toWords } from '../../utilities/formatLabels';

View File

@@ -18,7 +18,7 @@ import formatName from '../utilities/formatName';
import combineParentName from '../utilities/combineParentName';
import { ArrayField, CodeField, JSONField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, CollapsibleField, TabsField, CheckboxField, BlockField, tabHasName } from '../../fields/config/types';
import { toWords } from '../../utilities/formatLabels';
import { Payload } from '../../index';
import { Payload } from '../../payload';
import { SanitizedCollectionConfig } from '../../collections/config/types';
import { groupOrTabHasRequiredSubfield } from '../../utilities/groupOrTabHasRequiredSubfield';

View File

@@ -46,7 +46,7 @@ import withNullableType from './withNullableType';
import { toWords } from '../../utilities/formatLabels';
import createRichTextRelationshipPromise from '../../fields/richText/richTextRelationshipPromise';
import formatOptions from '../utilities/formatOptions';
import { Payload } from '../..';
import { Payload } from '../../payload';
import buildWhereInputType from './buildWhereInputType';
import buildBlockType from './buildBlockType';
import isFieldNullable from './isFieldNullable';

View File

@@ -5,7 +5,7 @@ import formatName from '../utilities/formatName';
import { CollectionConfig, SanitizedCollectionConfig } from '../../collections/config/types';
import { GlobalConfig, SanitizedGlobalConfig } from '../../globals/config/types';
import { Field } from '../../fields/config/types';
import { Payload } from '../..';
import { Payload } from '../../payload';
import { toWords } from '../../utilities/formatLabels';
type OperationType = 'create' | 'read' | 'update' | 'delete' | 'unlock' | 'readVersions';

View File

@@ -1,280 +1,23 @@
import { Express, Router } from 'express';
import pino from 'pino';
import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql';
import { Config as GeneratedTypes } from 'payload/generated-types';
import {
TypeWithID,
Collection,
CollectionModel,
} from './collections/config/types';
import {
SanitizedConfig,
EmailOptions,
InitOptions,
} from './config/types';
import { TypeWithVersion } from './versions/types';
import { PaginatedDocs } from './mongoose/types';
import { initHTTP } from './initHTTP';
import { Payload, BasePayload } from './payload';
import { PayloadAuthenticate } from './express/middleware/authenticate';
import { Globals, TypeWithID as GlobalTypeWithID } from './globals/config/types';
import { ErrorHandler } from './express/middleware/errorHandler';
import localOperations from './collections/operations/local';
import localGlobalOperations from './globals/operations/local';
import { encrypt, decrypt } from './auth/crypto';
import { BuildEmailResult, Message } from './email/types';
import { Preferences } from './preferences/types';
import { Options as CreateOptions } from './collections/operations/local/create';
import { Options as FindOptions } from './collections/operations/local/find';
import { Options as FindByIDOptions } from './collections/operations/local/findByID';
import { Options as UpdateOptions } from './collections/operations/local/update';
import { Options as DeleteOptions } from './collections/operations/local/delete';
import { Options as FindVersionsOptions } from './collections/operations/local/findVersions';
import { Options as FindVersionByIDOptions } from './collections/operations/local/findVersionByID';
import { Options as RestoreVersionOptions } from './collections/operations/local/restoreVersion';
import { Options as FindGlobalVersionsOptions } from './globals/operations/local/findVersions';
import { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID';
import { Options as RestoreGlobalVersionOptions } from './globals/operations/local/restoreVersion';
import { Options as ForgotPasswordOptions } from './auth/operations/local/forgotPassword';
import { Options as LoginOptions } from './auth/operations/local/login';
import { Options as ResetPasswordOptions } from './auth/operations/local/resetPassword';
import { Options as UnlockOptions } from './auth/operations/local/unlock';
import { Options as VerifyEmailOptions } from './auth/operations/local/verifyEmail';
import { Result as ForgotPasswordResult } from './auth/operations/forgotPassword';
import { Result as ResetPasswordResult } from './auth/operations/resetPassword';
import { Result as LoginResult } from './auth/operations/login';
import { Options as FindGlobalOptions } from './globals/operations/local/findOne';
import { Options as UpdateGlobalOptions } from './globals/operations/local/update';
import { initSync, initAsync } from './init';
export { getPayload } from './payload';
require('isomorphic-fetch');
/**
* @description Payload
*/
export class Payload {
config: SanitizedConfig;
collections: {
[slug: string]: Collection;
} = {}
versions: {
[slug: string]: CollectionModel;
} = {}
preferences: Preferences;
globals: Globals;
logger: pino.Logger;
express: Express
router: Router;
emailOptions: EmailOptions;
email: BuildEmailResult;
sendEmail: (message: Message) => Promise<unknown>;
secret: string;
mongoURL: string | false;
mongoMemoryServer: any
local: boolean;
encrypt = encrypt;
decrypt = decrypt;
errorHandler: ErrorHandler;
authenticate: PayloadAuthenticate;
types: {
blockTypes: any;
blockInputTypes: any;
localeInputType?: any;
fallbackLocaleInputType?: any;
};
Query: { name: string; fields: { [key: string]: any } } = { name: 'Query', fields: {} };
Mutation: { name: string; fields: { [key: string]: any } } = { name: 'Mutation', fields: {} };
schema: GraphQLSchema;
extensions: (info: any) => Promise<any>;
customFormatErrorFn: (error: GraphQLError) => GraphQLFormattedError;
validationRules: any;
errorResponses: GraphQLFormattedError[] = [];
errorIndex: number;
/**
* @description Initializes Payload
* @param options
*/
init(options: InitOptions): void {
initSync(this, options);
}
async initAsync(options: InitOptions): Promise<void> {
await initAsync(this, options);
}
getAdminURL = (): string => `${this.config.serverURL}${this.config.routes.admin}`;
getAPIURL = (): string => `${this.config.serverURL}${this.config.routes.api}`;
/**
* @description Performs create operation
* @param options
* @returns created document
*/
create = async <T = any>(options: CreateOptions<T>): Promise<T> => {
const { create } = localOperations;
return create(this, options);
}
/**
* @description Find documents with criteria
* @param options
* @returns documents satisfying query
*/
find = async <T extends TypeWithID = any>(options: FindOptions): Promise<PaginatedDocs<T>> => {
const { find } = localOperations;
return find(this, options);
}
findGlobal = async <T extends GlobalTypeWithID = any>(options: FindGlobalOptions): Promise<T> => {
const { findOne } = localGlobalOperations;
return findOne(this, options);
}
updateGlobal = async <T extends GlobalTypeWithID = any>(options: UpdateGlobalOptions): Promise<T> => {
const { update } = localGlobalOperations;
return update(this, options);
}
/**
* @description Find global versions with criteria
* @param options
* @returns versions satisfying query
*/
findGlobalVersions = async <T extends TypeWithVersion<T> = any>(options: FindGlobalVersionsOptions): Promise<PaginatedDocs<T>> => {
const { findVersions } = localGlobalOperations;
return findVersions<T>(this, options);
}
/**
* @description Find global version by ID
* @param options
* @returns global version with specified ID
*/
findGlobalVersionByID = async <T extends TypeWithVersion<T> = any>(options: FindGlobalVersionByIDOptions): Promise<T> => {
const { findVersionByID } = localGlobalOperations;
return findVersionByID(this, options);
}
/**
* @description Restore global version by ID
* @param options
* @returns version with specified ID
*/
restoreGlobalVersion = async <T extends TypeWithVersion<T> = any>(options: RestoreGlobalVersionOptions): Promise<T> => {
const { restoreVersion } = localGlobalOperations;
return restoreVersion(this, options);
}
/**
* @description Find document by ID
* @param options
* @returns document with specified ID
*/
findByID = async <T extends TypeWithID = any>(options: FindByIDOptions): Promise<T> => {
const { findByID } = localOperations;
return findByID<T>(this, options);
}
/**
* @description Update document
* @param options
* @returns Updated document
*/
update = async <T = any>(options: UpdateOptions<T>): Promise<T> => {
const { update } = localOperations;
return update<T>(this, options);
}
delete = async <T extends TypeWithID = any>(options: DeleteOptions): Promise<T> => {
const { localDelete } = localOperations;
return localDelete<T>(this, options);
}
/**
* @description Find versions with criteria
* @param options
* @returns versions satisfying query
*/
findVersions = async <T extends TypeWithVersion<T> = any>(options: FindVersionsOptions): Promise<PaginatedDocs<T>> => {
const { findVersions } = localOperations;
return findVersions<T>(this, options);
}
/**
* @description Find version by ID
* @param options
* @returns version with specified ID
*/
findVersionByID = async <T extends TypeWithVersion<T> = any>(options: FindVersionByIDOptions): Promise<T> => {
const { findVersionByID } = localOperations;
return findVersionByID(this, options);
}
/**
* @description Restore version by ID
* @param options
* @returns version with specified ID
*/
restoreVersion = async <T extends TypeWithVersion<T> = any>(options: RestoreVersionOptions): Promise<T> => {
const { restoreVersion } = localOperations;
return restoreVersion(this, options);
}
login = async <T extends TypeWithID = any>(options: LoginOptions): Promise<LoginResult & { user: T }> => {
const { login } = localOperations.auth;
return login(this, options);
}
forgotPassword = async (options: ForgotPasswordOptions): Promise<ForgotPasswordResult> => {
const { forgotPassword } = localOperations.auth;
return forgotPassword(this, options);
}
resetPassword = async (options: ResetPasswordOptions): Promise<ResetPasswordResult> => {
const { resetPassword } = localOperations.auth;
return resetPassword(this, options);
}
unlock = async (options: UnlockOptions): Promise<boolean> => {
const { unlock } = localOperations.auth;
return unlock(this, options);
}
verifyEmail = async (options: VerifyEmailOptions): Promise<boolean> => {
const { verifyEmail } = localOperations.auth;
return verifyEmail(this, options);
export class PayloadHTTP extends BasePayload<GeneratedTypes> {
async init(options: InitOptions): Promise<Payload> {
const payload = await initHTTP(options);
Object.assign(this, payload);
return payload;
}
}
const payload = new Payload();
const payload = new PayloadHTTP();
export default payload;
module.exports = payload;

View File

@@ -1,178 +0,0 @@
/* eslint-disable no-param-reassign */
import express, { NextFunction, Response } from 'express';
import crypto from 'crypto';
import path from 'path';
import mongoose from 'mongoose';
import { InitOptions } from './config/types';
import authenticate from './express/middleware/authenticate';
import connectMongoose from './mongoose/connect';
import expressMiddleware from './express/middleware';
import initAdmin from './express/admin';
import initAuth from './auth/init';
import access from './auth/requestHandlers/access';
import initCollections from './collections/init';
import initPreferences from './preferences/init';
import initGlobals from './globals/init';
import initGraphQLPlayground from './graphql/initPlayground';
import initStatic from './express/static';
import registerSchema from './graphql/registerSchema';
import graphQLHandler from './graphql/graphQLHandler';
import buildEmail from './email/build';
import identifyAPI from './express/middleware/identifyAPI';
import errorHandler from './express/middleware/errorHandler';
import { PayloadRequest } from './express/types';
import sendEmail from './email/sendEmail';
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit';
import { Payload } from '.';
import loadConfig from './config/load';
import Logger from './utilities/logger';
import { getDataLoader } from './collections/dataloader';
import mountEndpoints from './express/mountEndpoints';
import PreferencesModel from './preferences/model';
import findConfig from './config/find';
export const init = (payload: Payload, options: InitOptions): void => {
payload.logger.info('Starting Payload...');
if (!options.secret) {
throw new Error(
'Error: missing secret key. A secret key is needed to secure Payload.',
);
}
if (options.mongoURL !== false && typeof options.mongoURL !== 'string') {
throw new Error('Error: missing MongoDB connection URL.');
}
payload.emailOptions = { ...(options.email) };
payload.secret = crypto
.createHash('sha256')
.update(options.secret)
.digest('hex')
.slice(0, 32);
payload.local = options.local;
if (options.config) {
payload.config = options.config;
const configPath = findConfig();
payload.config = {
...options.config,
paths: {
configDir: path.dirname(configPath),
config: configPath,
rawConfig: configPath,
},
};
} else {
payload.config = loadConfig(payload.logger);
}
// If not initializing locally, scaffold router
if (!payload.local) {
payload.router = express.Router();
payload.router.use(...expressMiddleware(payload));
initAuth(payload);
}
// Configure email service
payload.email = buildEmail(payload.emailOptions, payload.logger);
payload.sendEmail = sendEmail.bind(payload);
// Initialize collections & globals
initCollections(payload);
initGlobals(payload);
if (!payload.config.graphQL.disable) {
registerSchema(payload);
}
payload.preferences = { Model: PreferencesModel };
// If not initializing locally, set up HTTP routing
if (!payload.local) {
options.express.use((req: PayloadRequest, res, next) => {
req.payload = payload;
next();
});
options.express.use((req: PayloadRequest, res: Response, next: NextFunction): void => {
req.payloadDataLoader = getDataLoader(req);
return next();
});
payload.express = options.express;
if (payload.config.rateLimit.trustProxy) {
payload.express.set('trust proxy', 1);
}
initAdmin(payload);
initPreferences(payload);
payload.router.get('/access', access);
if (!payload.config.graphQL.disable) {
payload.router.use(
payload.config.routes.graphQL,
(req, res, next): void => {
if (req.method === 'OPTIONS') {
res.sendStatus(204);
} else {
next();
}
},
identifyAPI('GraphQL'),
(req: PayloadRequest, res: Response) => graphQLHandler(req, res)(req, res),
);
initGraphQLPlayground(payload);
}
mountEndpoints(options.express, payload.router, payload.config.endpoints);
// Bind router to API
payload.express.use(payload.config.routes.api, payload.router);
// Enable static routes for all collections permitting upload
initStatic(payload);
payload.errorHandler = errorHandler(payload.config, payload.logger);
payload.router.use(payload.errorHandler);
payload.authenticate = authenticate(payload.config);
}
serverInitTelemetry(payload);
};
export const initAsync = async (payload: Payload, options: InitOptions): Promise<void> => {
payload.logger = Logger('payload', options.loggerOptions);
payload.mongoURL = options.mongoURL;
if (payload.mongoURL) {
mongoose.set('strictQuery', false);
payload.mongoMemoryServer = await connectMongoose(payload.mongoURL, options.mongoOptions, payload.logger);
}
init(payload, options);
if (typeof options.onInit === 'function') await options.onInit(payload);
if (typeof payload.config.onInit === 'function') await payload.config.onInit(payload);
};
export const initSync = (payload: Payload, options: InitOptions): void => {
payload.logger = Logger('payload', options.loggerOptions);
payload.mongoURL = options.mongoURL;
if (payload.mongoURL) {
mongoose.set('strictQuery', false);
connectMongoose(payload.mongoURL, options.mongoOptions, payload.logger);
}
init(payload, options);
if (typeof options.onInit === 'function') options.onInit(payload);
if (typeof payload.config.onInit === 'function') payload.config.onInit(payload);
};

87
src/initHTTP.ts Normal file
View File

@@ -0,0 +1,87 @@
/* eslint-disable no-param-reassign */
import express, { NextFunction, Response } from 'express';
import { Config as GeneratedTypes } from 'payload/generated-types';
import { InitOptions } from './config/types';
import authenticate from './express/middleware/authenticate';
import expressMiddleware from './express/middleware';
import initAdmin from './express/admin';
import initAuth from './auth/init';
import access from './auth/requestHandlers/access';
import initCollectionsHTTP from './collections/initHTTP';
import initPreferences from './preferences/init';
import initGlobalsHTTP from './globals/initHTTP';
import initGraphQLPlayground from './graphql/initPlayground';
import initStatic from './express/static';
import graphQLHandler from './graphql/graphQLHandler';
import identifyAPI from './express/middleware/identifyAPI';
import errorHandler from './express/middleware/errorHandler';
import { PayloadRequest } from './express/types';
import { getDataLoader } from './collections/dataloader';
import mountEndpoints from './express/mountEndpoints';
import { Payload, getPayload } from './payload';
export const initHTTP = async (options: InitOptions): Promise<Payload> => {
const payload = await getPayload(options);
if (!options.local) {
payload.router = express.Router();
payload.router.use(...expressMiddleware(payload));
initAuth(payload);
initCollectionsHTTP(payload);
initGlobalsHTTP(payload);
options.express.use((req: PayloadRequest, res, next) => {
req.payload = payload;
next();
});
options.express.use((req: PayloadRequest, res: Response, next: NextFunction): void => {
req.payloadDataLoader = getDataLoader(req);
return next();
});
payload.express = options.express;
if (payload.config.rateLimit.trustProxy) {
payload.express.set('trust proxy', 1);
}
initAdmin(payload);
initPreferences(payload);
payload.router.get('/access', access);
if (!payload.config.graphQL.disable) {
payload.router.use(
payload.config.routes.graphQL,
(req, res, next): void => {
if (req.method === 'OPTIONS') {
res.sendStatus(204);
} else {
next();
}
},
identifyAPI('GraphQL'),
(req: PayloadRequest, res: Response) => graphQLHandler(req, res)(req, res),
);
initGraphQLPlayground(payload);
}
mountEndpoints(options.express, payload.router, payload.config.endpoints);
// Bind router to API
payload.express.use(payload.config.routes.api, payload.router);
// Enable static routes for all collections permitting upload
initStatic(payload);
payload.errorHandler = errorHandler(payload.config, payload.logger);
payload.router.use(payload.errorHandler);
payload.authenticate = authenticate(payload.config);
}
return payload;
};

412
src/payload.ts Normal file
View File

@@ -0,0 +1,412 @@
import pino from 'pino';
import type { Express, Router } from 'express';
import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql';
import crypto from 'crypto';
import path from 'path';
import mongoose from 'mongoose';
import { Config as GeneratedTypes } from 'payload/generated-types';
import {
Collection,
CollectionModel,
} from './collections/config/types';
import {
SanitizedConfig,
EmailOptions,
InitOptions,
} from './config/types';
import { TypeWithVersion } from './versions/types';
import { PaginatedDocs } from './mongoose/types';
import { PayloadAuthenticate } from './express/middleware/authenticate';
import { Globals } from './globals/config/types';
import { ErrorHandler } from './express/middleware/errorHandler';
import localOperations from './collections/operations/local';
import localGlobalOperations from './globals/operations/local';
import { encrypt, decrypt } from './auth/crypto';
import { BuildEmailResult, Message } from './email/types';
import { Preferences } from './preferences/types';
import { Options as CreateOptions } from './collections/operations/local/create';
import { Options as FindOptions } from './collections/operations/local/find';
import { Options as FindByIDOptions } from './collections/operations/local/findByID';
import { Options as UpdateOptions } from './collections/operations/local/update';
import { Options as DeleteOptions } from './collections/operations/local/delete';
import { Options as FindVersionsOptions } from './collections/operations/local/findVersions';
import { Options as FindVersionByIDOptions } from './collections/operations/local/findVersionByID';
import { Options as RestoreVersionOptions } from './collections/operations/local/restoreVersion';
import { Options as FindGlobalVersionsOptions } from './globals/operations/local/findVersions';
import { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID';
import { Options as RestoreGlobalVersionOptions } from './globals/operations/local/restoreVersion';
import { Options as ForgotPasswordOptions } from './auth/operations/local/forgotPassword';
import { Options as LoginOptions } from './auth/operations/local/login';
import { Options as ResetPasswordOptions } from './auth/operations/local/resetPassword';
import { Options as UnlockOptions } from './auth/operations/local/unlock';
import { Options as VerifyEmailOptions } from './auth/operations/local/verifyEmail';
import { Result as ForgotPasswordResult } from './auth/operations/forgotPassword';
import { Result as ResetPasswordResult } from './auth/operations/resetPassword';
import { Result as LoginResult } from './auth/operations/login';
import { Options as FindGlobalOptions } from './globals/operations/local/findOne';
import { Options as UpdateGlobalOptions } from './globals/operations/local/update';
import connectMongoose from './mongoose/connect';
import initCollections from './collections/initLocal';
import initGlobals from './globals/initLocal';
import registerSchema from './graphql/registerSchema';
import buildEmail from './email/build';
import sendEmail from './email/sendEmail';
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit';
import loadConfig from './config/load';
import Logger from './utilities/logger';
import PreferencesModel from './preferences/model';
import findConfig from './config/find';
/**
* @description Payload
*/
export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
config: SanitizedConfig;
collections: {
[slug: string | number | symbol]: Collection;
} = {}
versions: {
[slug: string]: CollectionModel;
} = {}
preferences: Preferences;
globals: Globals;
logger: pino.Logger;
emailOptions: EmailOptions;
email: BuildEmailResult;
sendEmail: (message: Message) => Promise<unknown>;
secret: string;
mongoURL: string | false;
mongoMemoryServer: any
local: boolean;
encrypt = encrypt;
decrypt = decrypt;
errorHandler: ErrorHandler;
authenticate: PayloadAuthenticate;
express?: Express
router?: Router
types: {
blockTypes: any;
blockInputTypes: any;
localeInputType?: any;
fallbackLocaleInputType?: any;
};
Query: { name: string; fields: { [key: string]: any } } = { name: 'Query', fields: {} };
Mutation: { name: string; fields: { [key: string]: any } } = { name: 'Mutation', fields: {} };
schema: GraphQLSchema;
extensions: (info: any) => Promise<any>;
customFormatErrorFn: (error: GraphQLError) => GraphQLFormattedError;
validationRules: any;
errorResponses: GraphQLFormattedError[] = [];
errorIndex: number;
getAdminURL = (): string => `${this.config.serverURL}${this.config.routes.admin}`;
getAPIURL = (): string => `${this.config.serverURL}${this.config.routes.api}`;
/**
* @description Initializes Payload
* @param options
*/
async init(options: InitOptions): Promise<Payload> {
this.logger = Logger('payload', options.loggerOptions);
this.mongoURL = options.mongoURL;
if (this.mongoURL) {
mongoose.set('strictQuery', false);
this.mongoMemoryServer = await connectMongoose(this.mongoURL, options.mongoOptions, this.logger);
}
this.logger.info('Starting Payload...');
if (!options.secret) {
throw new Error(
'Error: missing secret key. A secret key is needed to secure Payload.',
);
}
if (options.mongoURL !== false && typeof options.mongoURL !== 'string') {
throw new Error('Error: missing MongoDB connection URL.');
}
this.emailOptions = { ...(options.email) };
this.secret = crypto
.createHash('sha256')
.update(options.secret)
.digest('hex')
.slice(0, 32);
this.local = options.local;
if (options.config) {
this.config = options.config;
const configPath = findConfig();
this.config = {
...options.config,
paths: {
configDir: path.dirname(configPath),
config: configPath,
rawConfig: configPath,
},
};
} else {
this.config = loadConfig(this.logger);
}
// Configure email service
this.email = buildEmail(this.emailOptions, this.logger);
this.sendEmail = sendEmail.bind(this);
// Initialize collections & globals
initCollections(this);
initGlobals(this);
if (!this.config.graphQL.disable) {
registerSchema(this);
}
this.preferences = { Model: PreferencesModel };
serverInitTelemetry(this);
if (typeof options.onInit === 'function') await options.onInit(this);
if (typeof this.config.onInit === 'function') await this.config.onInit(this);
return this;
}
/**
* @description Performs create operation
* @param options
* @returns created document
*/
create = async <T extends keyof TGeneratedTypes['collections']>(
options: CreateOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { create } = localOperations;
return create<T>(this, options);
}
/**
* @description Find documents with criteria
* @param options
* @returns documents satisfying query
*/
find = async <T extends keyof TGeneratedTypes['collections']>(
options: FindOptions<T>,
): Promise<PaginatedDocs<TGeneratedTypes['collections'][T]>> => {
const { find } = localOperations;
return find<T>(this, options);
}
/**
* @description Find document by ID
* @param options
* @returns document with specified ID
*/
findByID = async <T extends keyof TGeneratedTypes['collections']>(
options: FindByIDOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { findByID } = localOperations;
return findByID<T>(this, options);
}
/**
* @description Update document
* @param options
* @returns Updated document
*/
update = async <T extends keyof TGeneratedTypes['collections']>(
options: UpdateOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { update } = localOperations;
return update<T>(this, options);
}
delete = async <T extends keyof TGeneratedTypes['collections']>(
options: DeleteOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { localDelete } = localOperations;
return localDelete<T>(this, options);
}
/**
* @description Find versions with criteria
* @param options
* @returns versions satisfying query
*/
findVersions = async <T extends keyof TGeneratedTypes['collections']>(
options: FindVersionsOptions<T>,
): Promise<PaginatedDocs<TypeWithVersion<TGeneratedTypes['collections'][T]>>> => {
const { findVersions } = localOperations;
return findVersions<T>(this, options);
}
/**
* @description Find version by ID
* @param options
* @returns version with specified ID
*/
findVersionByID = async <T extends keyof TGeneratedTypes['collections']>(
options: FindVersionByIDOptions<T>,
): Promise<TypeWithVersion<TGeneratedTypes['collections'][T]>> => {
const { findVersionByID } = localOperations;
return findVersionByID<T>(this, options);
}
/**
* @description Restore version by ID
* @param options
* @returns version with specified ID
*/
restoreVersion = async <T extends keyof TGeneratedTypes['collections']>(
options: RestoreVersionOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { restoreVersion } = localOperations;
return restoreVersion<T>(this, options);
}
login = async <T extends keyof TGeneratedTypes['collections']>(
options: LoginOptions<T>,
): Promise<LoginResult & { user: TGeneratedTypes['collections'][T] }> => {
const { login } = localOperations.auth;
return login<T>(this, options);
}
forgotPassword = async <T extends keyof TGeneratedTypes['collections']>(
options: ForgotPasswordOptions<T>,
): Promise<ForgotPasswordResult> => {
const { forgotPassword } = localOperations.auth;
return forgotPassword<T>(this, options);
}
resetPassword = async <T extends keyof TGeneratedTypes['collections']>(
options: ResetPasswordOptions<T>,
): Promise<ResetPasswordResult> => {
const { resetPassword } = localOperations.auth;
return resetPassword<T>(this, options);
}
unlock = async <T extends keyof TGeneratedTypes['collections']>(
options: UnlockOptions<T>,
): Promise<boolean> => {
const { unlock } = localOperations.auth;
return unlock(this, options);
}
verifyEmail = async <T extends keyof TGeneratedTypes['collections']>(
options: VerifyEmailOptions<T>,
): Promise<boolean> => {
const { verifyEmail } = localOperations.auth;
return verifyEmail(this, options);
}
findGlobal = async <T extends keyof TGeneratedTypes['globals']>(
options: FindGlobalOptions<T>,
): Promise<TGeneratedTypes['globals'][T]> => {
const { findOne } = localGlobalOperations;
return findOne<T>(this, options);
}
updateGlobal = async <T extends keyof TGeneratedTypes['globals']>(
options: UpdateGlobalOptions<T>,
): Promise<TGeneratedTypes['globals'][T]> => {
const { update } = localGlobalOperations;
return update<T>(this, options);
}
/**
* @description Find global versions with criteria
* @param options
* @returns versions satisfying query
*/
findGlobalVersions = async <T extends keyof TGeneratedTypes['globals']>(
options: FindGlobalVersionsOptions<T>,
): Promise<PaginatedDocs<TypeWithVersion<TGeneratedTypes['globals'][T]>>> => {
const { findVersions } = localGlobalOperations;
return findVersions<T>(this, options);
}
/**
* @description Find global version by ID
* @param options
* @returns global version with specified ID
*/
findGlobalVersionByID = async <T extends keyof TGeneratedTypes['globals']>(
options: FindGlobalVersionByIDOptions<T>,
): Promise<TypeWithVersion<TGeneratedTypes['globals'][T]>> => {
const { findVersionByID } = localGlobalOperations;
return findVersionByID<T>(this, options);
}
/**
* @description Restore global version by ID
* @param options
* @returns version with specified ID
*/
restoreGlobalVersion = async <T extends keyof TGeneratedTypes['globals']>(
options: RestoreGlobalVersionOptions<T>,
): Promise<TGeneratedTypes['globals'][T]> => {
const { restoreVersion } = localGlobalOperations;
return restoreVersion<T>(this, options);
}
}
export type Payload = BasePayload<GeneratedTypes>
let cached = global._payload;
if (!cached) {
// eslint-disable-next-line no-multi-assign
cached = global._payload = { payload: null, promise: null };
}
export const getPayload = async (options: InitOptions): Promise<Payload> => {
if (cached.payload) {
return cached.payload;
}
if (!cached.promise) {
cached.promise = new BasePayload<GeneratedTypes>().init(options);
}
try {
cached.payload = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.payload;
};

View File

@@ -9,7 +9,7 @@ import { DateTimeResolver } from 'graphql-scalars';
import findOne from '../operations/findOne';
import update from '../operations/update';
import deleteOperation from '../operations/delete';
import { Payload } from '../..';
import { Payload } from '../../payload';
function initCollectionsGraphQL(payload: Payload): void {
const valueType = GraphQLJSON;

View File

@@ -1,9 +1,8 @@
import express from 'express';
import { Payload } from '../index';
import findOne from './requestHandlers/findOne';
import update from './requestHandlers/update';
import deleteHandler from './requestHandlers/delete';
import { Payload } from '../payload';
export default function initPreferences(ctx: Payload): void {
if (!ctx.local) {

View File

@@ -93,7 +93,7 @@
"block": "blok",
"blocks": "blokken",
"addLabel": "Voeg {{label}} toe",
"addLink": "Voeg een link tsoe",
"addLink": "Voeg een link toe",
"addNew": "Nieuw(e)",
"addNewLabel": "Nieuw(e) {{label}} toevoegen",
"addRelationship": "Nieuwe Relatie",
@@ -112,7 +112,7 @@
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen.",
"relatedDocument": "Gerelateerd document",
"relationTo": "Relatie tot",
"removeRelationship": "Relatie Vserwijderen",
"removeRelationship": "Relatie Verwijderen",
"removeUpload": "Verwijder Upload",
"saveChanges": "Bewaar aanpassingen",
"searchForBlock": "Zoeken naar een blok",

View File

@@ -14,21 +14,21 @@ import { FileData, FileToSave } from './types';
import canResizeImage from './canResizeImage';
import isImage from './isImage';
type Args = {
type Args<T> = {
config: SanitizedConfig,
collection: Collection
throwOnMissingFile?: boolean
req: PayloadRequest
data: Record<string, unknown>
data: T
overwriteExistingFiles?: boolean
}
type Result = Promise<{
data: Record<string, unknown>
type Result<T> = Promise<{
data: T
files: FileToSave[]
}>
export const generateFileData = async ({
export const generateFileData = async <T>({
config,
collection: {
config: collectionConfig,
@@ -38,7 +38,7 @@ export const generateFileData = async ({
data,
throwOnMissingFile,
overwriteExistingFiles,
}: Args): Result => {
}: Args<T>): Result<T> => {
let newData = data;
const filesToSave: FileToSave[] = [];

View File

@@ -2,7 +2,7 @@ import type { TFunction } from 'i18next';
import { FileUploadError } from '../errors';
import saveBufferToFile from './saveBufferToFile';
import { FileToSave } from './types';
import { Payload } from '..';
import { Payload } from '../payload';
export const uploadFiles = async (payload: Payload, files: FileToSave[], t: TFunction): Promise<void> => {
try {

View File

@@ -3,10 +3,14 @@ import memoize from 'micro-memoize';
export type PayloadLogger = pino.Logger;
const defaultLoggerOptions = {
prettyPrint: {
ignore: 'pid,hostname',
translateTime: 'HH:MM:ss',
const defaultLoggerOptions: pino.LoggerOptions = {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
ignore: 'pid,hostname',
translateTime: 'HH:MM:ss',
},
},
};

View File

@@ -1,8 +1,6 @@
import { TypeWithID } from '../collections/config/types';
const internalFields = ['__v', 'salt', 'hash'];
const sanitizeInternalFields = <T extends TypeWithID = any>(incomingDoc): T => Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id') {
return {
...newDoc,

View File

@@ -1,5 +1,5 @@
import { sendEvent } from '..';
import { Payload } from '../../..';
import { Payload } from '../../../payload';
export type ServerInitEvent = {
type: 'server-init'

View File

@@ -3,7 +3,7 @@ import Conf from 'conf';
import { randomBytes } from 'crypto';
import findUp from 'find-up';
import fs from 'fs';
import { Payload } from '../../index';
import { Payload } from '../../payload';
import { ServerInitEvent } from './events/serverInit';
import { AdminInitEvent } from './events/adminInit';
import { oneWayHash } from './oneWayHash';
@@ -28,7 +28,7 @@ type Args = {
event: TelemetryEvent
}
export const sendEvent = async ({ payload, event } : Args): Promise<void> => {
export const sendEvent = async ({ payload, event }: Args): Promise<void> => {
if (payload.config.telemetry !== false) {
try {
const packageJSON = await getPackageJSON();
@@ -49,7 +49,7 @@ export const sendEvent = async ({ payload, event } : Args): Promise<void> => {
body: JSON.stringify({ ...baseEvent, ...event }),
});
} catch (_) {
// Eat any errors in sending telemetry event
// Eat any errors in sending telemetry event
}
}
};

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ export const versionCollectionDefaults: IncomingCollectionVersions = {
interval: 2000, // in milliseconds
},
},
retainDeleted: true,
retainDeleted: false,
};
export const versionGlobalDefaults: IncomingGlobalVersions = {

View File

@@ -0,0 +1,25 @@
import { Payload } from '../payload';
type Args = {
payload: Payload
slug: string
id?: string | number
}
export const deleteCollectionVersions = async ({
payload,
slug,
id,
}: Args): Promise<void> => {
const VersionsModel = payload.versions[slug];
try {
await VersionsModel.deleteMany({
parent: {
$eq: id,
},
});
} catch (err) {
payload.logger.error(`There was an error removing versions for the deleted ${slug} document with ID ${id}.`);
}
};

View File

@@ -1,229 +0,0 @@
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';
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 TypeWithID>({
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 (incomingWhere) {
finalQueryToBuild.where.and[0].or.push(incomingWhere);
}
if (includedParentIDs.length > 0) {
finalQueryToBuild.where.and[0].or.push({
id: {
in: includedParentIDs,
},
});
}
if (excludedParentIDs.length > 0) {
finalQueryToBuild.where.and.push({
id: {
not_in: excludedParentIDs,
},
});
}
const finalQuery = await collection.Model.buildQuery(finalQueryToBuild, locale);
let result = await collection.Model.paginate(finalQuery, paginationOptions);
result = {
...result,
docs: await Promise.all(result.docs.map(async (doc) => {
const matchedVersion = versionCollectionMatchMap[doc.id];
if (matchedVersion && matchedVersion.updatedAt > doc.updatedAt) {
return {
...doc,
...matchedVersion.version,
createdAt: matchedVersion.createdAt,
updatedAt: matchedVersion.updatedAt,
};
}
return replaceWithDraftIfAvailable({
accessResult,
payload,
entity: collection.config,
entityType: 'collection',
doc,
locale,
});
})),
};
return result;
};

View File

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

View File

@@ -1,9 +1,8 @@
import { Payload } from '../..';
import { Payload } from '../../payload';
import { docHasTimestamps, Where } from '../../types';
import { hasWhereAccessResult } from '../../auth';
import { AccessResult } from '../../config/types';
import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types';
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { appendVersionToQueryKey } from './appendVersionToQueryKey';
import { SanitizedGlobalConfig } from '../../globals/config/types';

View File

@@ -1,79 +0,0 @@
import { Payload } from '../..';
import { SanitizedCollectionConfig } from '../../collections/config/types';
import { enforceMaxVersions } from '../enforceMaxVersions';
import { PayloadRequest } from '../../express/types';
type Args = {
payload: Payload
config?: SanitizedCollectionConfig
req: PayloadRequest
data: any
id: string | number
autosave: boolean
}
export const saveCollectionDraft = async ({
payload,
config,
id,
data,
autosave,
}: Args): Promise<Record<string, unknown>> => {
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;
};

Some files were not shown because too many files have changed in this diff Show More