feat: initial test suite framework (#4929)

This commit is contained in:
Jarrod Flesch
2024-02-14 09:46:11 -05:00
committed by GitHub
parent 018755516b
commit 717a6b6d07
50 changed files with 663 additions and 486 deletions

View File

@@ -0,0 +1,3 @@
export default typeof process.env.PAYLOAD_CONFIG_PATH === 'string'
? require(process.env.PAYLOAD_CONFIG_PATH)
: {}

View File

@@ -1,9 +1,10 @@
module.exports = { const customJestConfig = {
globalSetup: './test/jest.setup.ts', globalSetup: './test/jest.setup.ts',
moduleNameMapper: { moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/packages/payload/src/bundlers/mocks/emptyModule.js', '\\.(css|scss)$': '<rootDir>/packages/payload/src/bundlers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/packages/payload/src/bundlers/mocks/fileMock.js', '<rootDir>/packages/payload/src/bundlers/mocks/fileMock.js',
'payload-config': '<rootDir>/__mocks__/payload-config.ts',
}, },
testEnvironment: 'node', testEnvironment: 'node',
testMatch: ['<rootDir>/packages/payload/src/**/*.spec.ts', '<rootDir>/test/**/*int.spec.ts'], testMatch: ['<rootDir>/packages/payload/src/**/*.spec.ts', '<rootDir>/test/**/*int.spec.ts'],
@@ -13,3 +14,5 @@ module.exports = {
}, },
verbose: true, verbose: true,
} }
module.exports = customJestConfig

View File

@@ -80,16 +80,18 @@
"lexical": "0.12.5", "lexical": "0.12.5",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"minimist": "1.2.8", "minimist": "1.2.8",
"mongodb-memory-server": "8.13.0", "next": "14.1.1-canary.26",
"next": "14.0.2",
"node-fetch": "2.6.12", "node-fetch": "2.6.12",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prompts": "2.4.2", "prompts": "2.4.2",
"qs": "6.11.2", "qs": "6.11.2",
"read-stream": "^2.1.1", "read-stream": "^2.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semver": "^7.5.4", "semver": "^7.5.4",
"sharp": "0.32.6",
"shelljs": "0.8.5", "shelljs": "0.8.5",
"simple-git": "^3.20.0", "simple-git": "^3.20.0",
"slash": "3.0.0", "slash": "3.0.0",
@@ -102,7 +104,6 @@
}, },
"peerDependencies": { "peerDependencies": {
"react": "18.2.0", "react": "18.2.0",
"react-i18next": "11.18.6",
"react-router-dom": "5.3.4" "react-router-dom": "5.3.4"
}, },
"engines": { "engines": {

View File

@@ -27,18 +27,17 @@
"prepublishOnly": "pnpm clean && pnpm build" "prepublishOnly": "pnpm clean && pnpm build"
}, },
"dependencies": { "dependencies": {
"bson-ext": "^4.0.3",
"bson-objectid": "2.0.4", "bson-objectid": "2.0.4",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"get-port": "5.1.1", "get-port": "5.1.1",
"mongoose": "6.12.0", "mongoose": "6.12.0",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22", "mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2", "prompts": "2.4.2",
"uuid": "9.0.0" "uuid": "9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"mongodb-memory-server": "8.13.0", "mongodb-memory-server": "8.13.0",
"payload": "workspace:*" "payload": "workspace:*"
}, },

View File

@@ -24,7 +24,7 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
useFacet: undefined, useFacet: undefined,
} }
if (process.env.NODE_ENV === 'test') { if ([process.env.APP_ENV, process.env.NODE_ENV].includes('test')) {
if (process.env.PAYLOAD_TEST_MONGO_URL) { if (process.env.PAYLOAD_TEST_MONGO_URL) {
urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL
} else { } else {

View File

@@ -4,7 +4,6 @@ import type { Init } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types' import type { SanitizedCollectionConfig } from 'payload/types'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2' import paginate from 'mongoose-paginate-v2'
import { import {
buildVersionCollectionFields, buildVersionCollectionFields,
@@ -45,10 +44,6 @@ export const init: Init = async function init(this: MongooseAdapter) {
}), }),
) )
if (collection.versions?.drafts) {
versionSchema.plugin(mongooseAggregatePaginate)
}
const model = mongoose.model( const model = mongoose.model(
versionModelName, versionModelName,
versionSchema, versionSchema,

View File

@@ -1,11 +1,4 @@
import type { import type { IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'
AggregatePaginateModel,
IndexDefinition,
IndexOptions,
Model,
PaginateModel,
SchemaOptions,
} from 'mongoose'
import type { Payload } from 'payload' import type { Payload } from 'payload'
import type { SanitizedConfig } from 'payload/config' import type { SanitizedConfig } from 'payload/config'
import type { import type {
@@ -34,11 +27,7 @@ import type {
import type { BuildQueryArgs } from './queries/buildQuery' import type { BuildQueryArgs } from './queries/buildQuery'
export interface CollectionModel export interface CollectionModel extends Model<any>, PaginateModel<any>, PassportLocalModel {
extends Model<any>,
PaginateModel<any>,
AggregatePaginateModel<any>,
PassportLocalModel {
/** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */ /** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */
buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>> // TODO: Delete this buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>> // TODO: Delete this
} }

View File

@@ -286,7 +286,7 @@ export const upsertRow = async <T extends TypeWithID>({
message: req.t('error:valueMustBeUnique'), message: req.t('error:valueMustBeUnique'),
}, },
], ],
req?.t ?? i18nInit(req.payload.config.i18n).t, req.t,
) )
: error : error
} }

1
packages/dev/mocks.js Normal file
View File

@@ -0,0 +1 @@
export const mongooseAdapter = () => ({})

View File

@@ -8,8 +8,6 @@ const nextConfig = {
}, },
serverComponentsExternalPackages: ['drizzle-kit', 'drizzle-kit/utils', 'pino', 'pino-pretty'], serverComponentsExternalPackages: ['drizzle-kit', 'drizzle-kit/utils', 'pino', 'pino-pretty'],
}, },
reactStrictMode: false,
// transpilePackages: ['@payloadcms/db-mongodb', 'mongoose'],
webpack: (config) => { webpack: (config) => {
return { return {
...config, ...config,
@@ -19,15 +17,30 @@ const nextConfig = {
'drizzle-kit/utils', 'drizzle-kit/utils',
'pino', 'pino',
'pino-pretty', 'pino-pretty',
'mongoose',
'sharp', 'sharp',
], ],
ignoreWarnings: [
...(config.ignoreWarnings || []),
{ module: /node_modules\/mongodb\/lib\/utils\.js/ },
{ file: /node_modules\/mongodb\/lib\/utils\.js/ },
],
resolve: { resolve: {
...config.resolve, ...config.resolve,
alias: { alias: {
...config.resolve.alias, ...config.resolve.alias,
graphql$: path.resolve(__dirname, '../next/node_modules/graphql/index.js'), graphql$: path.resolve(__dirname, '../next/node_modules/graphql/index.js'),
'graphql-http$': path.resolve(__dirname, '../next/node_modules/graphql-http/index.js'), 'graphql-http$': path.resolve(__dirname, '../next/node_modules/graphql-http/index.js'),
'payload-config$': path.resolve(process.env.PAYLOAD_CONFIG_PATH),
},
fallback: {
...config.resolve.fallback,
'@aws-sdk/credential-providers': false,
'@mongodb-js/zstd': false,
aws4: false,
kerberos: false,
'mongodb-client-encryption': false,
snappy: false,
'supports-color': false,
}, },
}, },
} }

6
packages/dev/server.js Normal file
View File

@@ -0,0 +1,6 @@
const { bootAdminPanel } = require('../../test/helpers/bootAdminPanel.ts')
bootAdminPanel({
appDir: __dirname,
port: 3000,
})

View File

@@ -36,7 +36,6 @@ export default buildConfig({
// title: 'Test Page', // title: 'Test Page',
// }, // },
// }) // })
//
// await payload.update({ // await payload.update({
// collection: 'pages', // collection: 'pages',
// id: page.id, // id: page.id,

View File

@@ -17,21 +17,20 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next",
} },
], ],
"paths": { "paths": {
"payload": ["../payload/src"], "payload": ["../payload/src"],
"payload/*": ["../payload/src/exports/*"], "payload/*": ["../payload/src/exports/*"],
"payload-config": ["./src/payload.config.ts"],
"@payloadcms/db-mongodb": ["../db-mongodb/src"], "@payloadcms/db-mongodb": ["../db-mongodb/src"],
"@payloadcms/richtext-lexical": ["../richtext-lexical/src"], "@payloadcms/richtext-lexical": ["../richtext-lexical/src"],
"@payloadcms/ui/*": ["../ui/src/exports/*"], "@payloadcms/ui/*": ["../ui/src/exports/*"],
"@payloadcms/translations": ["../translations/src/exports/index.ts"], "@payloadcms/translations": ["../translations/src/exports/index.ts"],
"@payloadcms/translations/client": ["../translations/src/all"], "@payloadcms/translations/client": ["../translations/src/all"],
"@payloadcms/translations/api": ["../translations/src/all"], "@payloadcms/translations/api": ["../translations/src/all"],
"@payloadcms/next/*": ["../next/src/*"] "@payloadcms/next/*": ["../next/src/*"],
} },
}, },
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"], "exclude": ["node_modules"],
@@ -43,6 +42,6 @@
{ "path": "../translations" }, { "path": "../translations" },
{ "path": "../db-mongodb" }, { "path": "../db-mongodb" },
{ "path": "../db-postgres" }, { "path": "../db-postgres" },
{ "path": "../richtext-lexical" } { "path": "../richtext-lexical" },
] ],
} }

View File

@@ -2,5 +2,7 @@ import { PayloadRequest } from 'payload/types'
export type Context = { export type Context = {
req: PayloadRequest req: PayloadRequest
headers: Headers headers: {
[key: string]: string
}
} }

View File

@@ -59,8 +59,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"http-status": "1.6.2", "http-status": "1.6.2",
"i18next": "22.5.1", "next": "14.1.1-canary.26",
"next": "^14.0.0",
"payload": "^2.0.0" "payload": "^2.0.0"
}, },
"publishConfig": { "publishConfig": {

View File

@@ -111,8 +111,13 @@ export const POST = (config: Promise<SanitizedConfig>) => async (request: Reques
validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)), validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)),
})(originalRequest) })(originalRequest)
const resHeaders = new Headers(apiResponse.headers)
for (let key in headers) {
resHeaders.append(key, headers[key])
}
return new Response(apiResponse.body, { return new Response(apiResponse.body, {
status: apiResponse.status, status: apiResponse.status,
headers: new Headers(headers), headers: new Headers(resHeaders),
}) })
} }

View File

@@ -7,7 +7,6 @@
"outDir": "./dist" /* Specify an output folder for all emitted files. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */, "rootDir": "./src" /* Specify the root folder within your source files. */,
"paths": { "paths": {
"payload-config": ["./src/config.ts"],
"@payloadcms/graphql": ["../graphql/dist/index.ts"], "@payloadcms/graphql": ["../graphql/dist/index.ts"],
"@payloadcms/ui": ["../ui/src/exports/index.ts"], "@payloadcms/ui": ["../ui/src/exports/index.ts"],
"@payloadcms/translations/*": ["../translations/dist/*"] "@payloadcms/translations/*": ["../translations/dist/*"]

View File

@@ -9,7 +9,7 @@ export const APIKeyAuthentication =
async ({ headers, payload }) => { async ({ headers, payload }) => {
const authHeader = headers.get('Authorization') const authHeader = headers.get('Authorization')
if (authHeader.startsWith(`${collectionConfig.slug} API-Key `)) { if (authHeader?.startsWith(`${collectionConfig.slug} API-Key `)) {
const apiKey = authHeader.replace(`${collectionConfig.slug} API-Key `, '') const apiKey = authHeader.replace(`${collectionConfig.slug} API-Key `, '')
const apiKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex') const apiKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex')
@@ -38,8 +38,6 @@ export const APIKeyAuthentication =
collection: collectionConfig.slug, collection: collectionConfig.slug,
depth: collectionConfig.auth.depth, depth: collectionConfig.auth.depth,
overrideAccess: true, overrideAccess: true,
// TODO(JAMES)(REVIEW): had to remove with new pattern
// req,
where, where,
}) })

View File

@@ -8,8 +8,6 @@ export const build = async (): Promise<void> => {
disableOnInit: true, disableOnInit: true,
local: true, local: true,
}) })
await payload.config.admin.bundler.build(payload.config)
} }
// when build.js is launched directly // when build.js is launched directly

View File

@@ -238,7 +238,7 @@ export type PayloadHandler = ({
/** /**
* Docs: https://payloadcms.com/docs/rest-api/overview#custom-endpoints * Docs: https://payloadcms.com/docs/rest-api/overview#custom-endpoints
*/ */
export type Endpoint = { export type Endpoint<U = User> = {
/** Extension point to add your custom data. */ /** Extension point to add your custom data. */
custom?: Record<string, any> custom?: Record<string, any>
/** /**

View File

@@ -47,7 +47,6 @@ import { decrypt, encrypt } from './auth/crypto'
import { APIKeyAuthentication } from './auth/strategies/apiKey' import { APIKeyAuthentication } from './auth/strategies/apiKey'
import { JWTAuthentication } from './auth/strategies/jwt' import { JWTAuthentication } from './auth/strategies/jwt'
import localOperations from './collections/operations/local' import localOperations from './collections/operations/local'
import findConfig from './config/find'
import buildEmail from './email/build' import buildEmail from './email/build'
import { defaults as emailDefaults } from './email/defaults' import { defaults as emailDefaults } from './email/defaults'
import sendEmail from './email/sendEmail' import sendEmail from './email/sendEmail'
@@ -311,14 +310,14 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
this.config = await options.config this.config = await options.config
// TODO(JARROD/JAMES): can we keep this? // TODO(JARROD/JAMES): can we keep this?
const configPath = findConfig() // const configPath = findConfig()
this.config = { this.config = {
...this.config, ...this.config,
paths: { // paths: {
config: configPath, // config: configPath,
configDir: path.dirname(configPath), // configDir: path.dirname(configPath),
rawConfig: configPath, // rawConfig: configPath,
}, // },
} }
if (!this.config.secret) { if (!this.config.secret) {

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import type { ElementType } from 'react' import type { ElementType } from 'react'
import { Tooltip } from 'payload/components' import { Tooltip } from '@payloadcms/ui'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useSlate } from 'slate-react' import { useSlate } from 'slate-react'

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import { ShimmerEffect } from 'payload/components' import { ShimmerEffect } from '@payloadcms/ui'
import React, { Suspense, lazy } from 'react' import React, { Suspense, lazy } from 'react'
import type { FieldProps } from '../types' import type { FieldProps } from '../types'

View File

@@ -1,6 +1,7 @@
import type { RichTextAdapter } from 'payload/types' import type { RichTextAdapter } from 'payload/types'
import { withMergedProps, withNullableJSONSchemaType } from 'payload/utilities' import { withMergedProps } from '@payloadcms/ui/utilities'
import { withNullableJSONSchemaType } from 'payload/utilities'
import type { AdapterArguments } from './types' import type { AdapterArguments } from './types'

View File

@@ -34,3 +34,4 @@ export { useDrawerSlug } from '../elements/Drawer/useDrawerSlug'
export { default as Popup } from '../elements/Popup' export { default as Popup } from '../elements/Popup'
// export { useThumbnail } from '../elements/Upload' // export { useThumbnail } from '../elements/Upload'
export { Translation } from '../elements/Translation' export { Translation } from '../elements/Translation'
export { Tooltip } from '../elements/Tooltip'

View File

@@ -290,8 +290,7 @@ const Relationship: React.FC<Props> = (props) => {
await priorRelation await priorRelation
const idsToLoad = ids.filter((id) => { const idsToLoad = ids.filter((id) => {
return !options.find( return !options.find((optionGroup) =>
(optionGroup) =>
optionGroup?.options?.find( optionGroup?.options?.find(
(option) => option.value === id && option.relationTo === relation, (option) => option.value === id && option.relationTo === relation,
), ),

643
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
packages: packages:
# all packages in direct subdirs of packages/ # all packages in direct subdirs of packages/
- 'packages/*' - 'packages/*'
- 'test/REST_API'
# exclude packages that are inside test directories # exclude packages that are inside test directories
- '!**/test/**' # - '!**/test/**'

View File

@@ -1,26 +1,34 @@
import payload from '../../packages/payload/src' import type { Payload } from '../../packages/payload/src'
import { devUser } from '../credentials' import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
import { postsSlug } from './collections/Posts' import { postsSlug } from './collections/Posts'
require('isomorphic-fetch') require('isomorphic-fetch')
let apiUrl let payload: Payload
let apiURL: string
let jwt let jwt
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
const { email, password } = devUser const { email, password } = devUser
describe('_Community Tests', () => { describe('_Community Tests', () => {
// --__--__--__--__--__--__--__--__--__ // --__--__--__--__--__--__--__--__--__
// Boilerplate test setup/teardown // Boilerplate test setup/teardown
// --__--__--__--__--__--__--__--__--__ // --__--__--__--__--__--__--__--__--__
beforeAll(async () => { beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) const { payload: payloadClient, serverURL } = await initPayloadTest({
apiUrl = `${serverURL}/api` __dirname,
init: { local: false },
})
const response = await fetch(`${apiUrl}/users/login`, { apiURL = `${serverURL}/api`
payload = payloadClient
const response = await fetch(`${apiURL}/users/login`, {
body: JSON.stringify({ body: JSON.stringify({
email, email,
password, password,
@@ -56,7 +64,7 @@ describe('_Community Tests', () => {
}) })
it('rest API example', async () => { it('rest API example', async () => {
const newPost = await fetch(`${apiUrl}/${postsSlug}`, { const newPost = await fetch(`${apiURL}/${postsSlug}`, {
method: 'POST', method: 'POST',
headers: { headers: {
...headers, ...headers,

View File

@@ -1,9 +1,11 @@
'use client'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import type { User } from '../../packages/payload/src/auth' import type { User } from '../../packages/payload/src/auth'
import type { UIField } from '../../packages/payload/src/fields/config/types' import type { UIField } from '../../packages/payload/src/fields/config/types'
import { useAuth } from '../../packages/payload/src/admin/components/utilities/Auth' import { useAuth } from '../../packages/ui'
export const AuthDebug: React.FC<UIField> = () => { export const AuthDebug: React.FC<UIField> = () => {
const [state, setState] = useState<User | null | undefined>() const [state, setState] = useState<User | null | undefined>()

View File

@@ -1,9 +1,9 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import jwtDecode from 'jwt-decode' import jwtDecode from 'jwt-decode'
import type { Payload } from '../../packages/payload/src'
import type { User } from '../../packages/payload/src/auth' import type { User } from '../../packages/payload/src/auth'
import payload from '../../packages/payload/src'
import configPromise from '../collections-graphql/config' import configPromise from '../collections-graphql/config'
import { devUser } from '../credentials' import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
@@ -13,6 +13,7 @@ require('isomorphic-fetch')
let apiUrl let apiUrl
let client: GraphQLClient let client: GraphQLClient
let payload: Payload
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -22,7 +23,11 @@ const { email, password } = devUser
describe('Auth', () => { describe('Auth', () => {
beforeAll(async () => { beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) const { serverURL, payload: payloadClient } = await initPayloadTest({
__dirname,
init: { local: false },
})
payload = payloadClient
apiUrl = `${serverURL}/api` apiUrl = `${serverURL}/api`
const config = await configPromise const config = await configPromise
const url = `${serverURL}${config.routes.api}${config.routes.graphQL}` const url = `${serverURL}${config.routes.api}${config.routes.graphQL}`
@@ -35,6 +40,10 @@ describe('Auth', () => {
} }
}) })
beforeEach(() => {
jest.resetModules()
})
describe('GraphQL - admin user', () => { describe('GraphQL - admin user', () => {
let token let token
let user let user
@@ -653,6 +662,14 @@ describe('Auth', () => {
}) })
}) })
describe('REST API', () => {
it('should respond from route handlers', async () => {
const test = await fetch(`${apiUrl}/api/test`)
expect(test.status).toStrictEqual(200)
})
})
describe('API Key', () => { describe('API Key', () => {
it('should authenticate via the correct API key user', async () => { it('should authenticate via the correct API key user', async () => {
const usersQuery = await payload.find({ const usersQuery = await payload.find({

View File

@@ -2,8 +2,6 @@ import path from 'path'
import type { Config, SanitizedConfig } from '../packages/payload/src/config/types' import type { Config, SanitizedConfig } from '../packages/payload/src/config/types'
import { viteBundler } from '../packages/bundler-vite/src'
import { webpackBundler } from '../packages/bundler-webpack/src'
import { mongooseAdapter } from '../packages/db-mongodb/src' import { mongooseAdapter } from '../packages/db-mongodb/src'
import { postgresAdapter } from '../packages/db-postgres/src' import { postgresAdapter } from '../packages/db-postgres/src'
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build' import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
@@ -11,11 +9,6 @@ import { slateEditor } from '../packages/richtext-slate/src'
// process.env.PAYLOAD_DATABASE = 'postgres' // process.env.PAYLOAD_DATABASE = 'postgres'
const bundlerAdapters = {
vite: viteBundler(),
webpack: webpackBundler(),
}
const databaseAdapters = { const databaseAdapters = {
mongoose: mongooseAdapter({ mongoose: mongooseAdapter({
migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'), migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'),
@@ -30,10 +23,9 @@ const databaseAdapters = {
} }
export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> { export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> {
const [name] = process.argv.slice(2)
const config: Config = { const config: Config = {
editor: slateEditor({}), secret: 'TEST_SECRET',
editor: undefined,
rateLimit: { rateLimit: {
max: 9999999999, max: 9999999999,
window: 15 * 60 * 1000, // 15min default, window: 15 * 60 * 1000, // 15min default,
@@ -53,49 +45,6 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
}, },
...(config.admin || {}), ...(config.admin || {}),
buildPath: path.resolve(__dirname, '../build'), buildPath: path.resolve(__dirname, '../build'),
bundler: bundlerAdapters[process.env.PAYLOAD_BUNDLER || 'webpack'],
webpack: (webpackConfig) => {
const existingConfig =
typeof testConfig?.admin?.webpack === 'function'
? testConfig.admin.webpack(webpackConfig)
: webpackConfig
return {
...existingConfig,
name,
cache: process.env.NODE_ENV === 'test' ? { type: 'memory' } : existingConfig.cache,
entry: {
main: [
`webpack-hot-middleware/client?path=${
testConfig?.routes?.admin || '/admin'
}/__webpack_hmr`,
path.resolve(__dirname, '../packages/payload/src/admin'),
],
},
resolve: {
...existingConfig.resolve,
alias: {
...existingConfig.resolve?.alias,
[path.resolve(__dirname, '../packages/bundler-vite/src/index')]: path.resolve(
__dirname,
'../packages/bundler-vite/mock.js',
),
[path.resolve(__dirname, '../packages/bundler-webpack/src/index')]: path.resolve(
__dirname,
'../packages/bundler-webpack/src/mocks/emptyModule.js',
),
[path.resolve(__dirname, '../packages/db-mongodb/src/index')]: path.resolve(
__dirname,
'../packages/db-mongodb/mock.js',
),
[path.resolve(__dirname, '../packages/db-postgres/src/index')]: path.resolve(
__dirname,
'../packages/db-postgres/mock.js',
),
react: path.resolve(__dirname, '../packages/payload/node_modules/react'),
},
},
}
},
} }
if (process.env.PAYLOAD_DISABLE_ADMIN === 'true') { if (process.env.PAYLOAD_DISABLE_ADMIN === 'true') {

View File

@@ -40,7 +40,7 @@ export default buildConfigWithDefaults({
{ {
path: '/send-test-email', path: '/send-test-email',
method: 'get', method: 'get',
handler: async (req, res) => { handler: async ({ req }) => {
await req.payload.sendEmail({ await req.payload.sendEmail({
from: 'dev@payloadcms.com', from: 'dev@payloadcms.com',
to: devUser.email, to: devUser.email,
@@ -55,19 +55,15 @@ export default buildConfigWithDefaults({
// }, // },
}) })
res.status(200).send('Email sent') return Response.json({ message: 'Email sent' })
}, },
}, },
{ {
path: '/internal-error-here', path: '/internal-error-here',
method: 'get', method: 'get',
handler: async (req, res, next) => { handler: () => {
try {
// Throwing an internal error with potentially sensitive data // Throwing an internal error with potentially sensitive data
throw new Error('Lost connection to the Pentagon. Secret data: ******') throw new Error('Lost connection to the Pentagon. Secret data: ******')
} catch (err) {
next(err)
}
}, },
}, },
], ],

View File

@@ -15,8 +15,8 @@ export default buildConfigWithDefaults({
{ {
path: '/hello', path: '/hello',
method: 'get', method: 'get',
handler: (_, res): void => { handler: () => {
res.json({ message: 'hi' }) return Response.json({ message: 'hi' })
}, },
custom: { examples: [{ type: 'response', value: { message: 'hi' } }] }, custom: { examples: [{ type: 'response', value: { message: 'hi' } }] },
}, },
@@ -38,9 +38,9 @@ export default buildConfigWithDefaults({
{ {
path: '/greet', path: '/greet',
method: 'get', method: 'get',
handler: (req, res): void => { handler: ({ req }) => {
const { name } = req.query const sp = new URL(req.url).searchParams
res.json({ message: `Hi ${name}!` }) return Response.json({ message: `Hi ${sp.get('name')}!` })
}, },
custom: { params: [{ in: 'query', name: 'name', type: 'string' }] }, custom: { params: [{ in: 'query', name: 'name', type: 'string' }] },
}, },
@@ -60,8 +60,8 @@ export default buildConfigWithDefaults({
path: '/config', path: '/config',
method: 'get', method: 'get',
root: true, root: true,
handler: (req, res): void => { handler: ({ req }) => {
res.json(req.payload.config) return Response.json(req.payload.config)
}, },
custom: { description: 'Get the sanitized payload config' }, custom: { description: 'Get the sanitized payload config' },
}, },

View File

@@ -29,7 +29,7 @@ describe('Config', () => {
it('allows a custom field in collection endpoints', () => { it('allows a custom field in collection endpoints', () => {
const [collection] = payload.config.collections const [collection] = payload.config.collections
const [endpoint] = collection.endpoints const [endpoint] = collection.endpoints || []
expect(endpoint.custom).toEqual({ expect(endpoint.custom).toEqual({
examples: [{ type: 'response', value: { message: 'hi' } }], examples: [{ type: 'response', value: { message: 'hi' } }],
@@ -52,7 +52,7 @@ describe('Config', () => {
it('allows a custom field in global endpoints', () => { it('allows a custom field in global endpoints', () => {
const [global] = payload.config.globals const [global] = payload.config.globals
const [endpoint] = global.endpoints const [endpoint] = global.endpoints || []
expect(endpoint.custom).toEqual({ params: [{ in: 'query', name: 'name', type: 'string' }] }) expect(endpoint.custom).toEqual({ params: [{ in: 'query', name: 'name', type: 'string' }] })
}) })

View File

@@ -1,7 +1,7 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import type { TypeWithID } from '../../packages/payload/src/collections/config/types' import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../packages/payload/src/express/types' import type { PayloadRequest } from '../../packages/payload/src/types'
import payload from '../../packages/payload/src' import payload from '../../packages/payload/src'
import { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction' import { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction'

View File

@@ -1,11 +1,10 @@
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import express from 'express'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { v4 as uuid } from 'uuid'
import payload from '../packages/payload/src' import { getPayload } from '../packages/payload/src'
import { prettySyncLoggerDestination } from '../packages/payload/src/utilities/logger' import { prettySyncLoggerDestination } from '../packages/payload/src/utilities/logger'
import { bootAdminPanel } from './helpers/bootAdminPanel'
import { startLivePreviewDemo } from './live-preview/startLivePreviewDemo' import { startLivePreviewDemo } from './live-preview/startLivePreviewDemo'
dotenv.config() dotenv.config()
@@ -46,42 +45,30 @@ if (process.argv.includes('--no-auto-login') && process.env.NODE_ENV !== 'produc
process.env.PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN = 'true' process.env.PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN = 'true'
} }
const expressApp = express()
const startDev = async () => { const startDev = async () => {
await payload.init({ const payload = await getPayload({
email: { email: {
fromAddress: 'hello@payloadcms.com', fromAddress: 'hello@payloadcms.com',
fromName: 'Payload', fromName: 'Payload',
logMockCredentials: false, logMockCredentials: false,
}, },
express: expressApp, config: require(configPath).default,
secret: uuid(),
...prettySyncLogger, ...prettySyncLogger,
onInit: async (payload) => { onInit: (payload) => {
payload.logger.info('Payload Dev Server Initialized') payload.logger.info('Payload Dev Server Initialized')
}, },
}) })
// Redirect root to Admin panel
expressApp.get('/', (_, res) => {
res.redirect('/admin')
})
const externalRouter = express.Router()
externalRouter.use(payload.authenticate)
if (testSuiteDir === 'live-preview') { if (testSuiteDir === 'live-preview') {
await startLivePreviewDemo({ await startLivePreviewDemo({
payload, payload,
}) })
} }
expressApp.listen(3000, async () => { await bootAdminPanel({ appDir: path.resolve(__dirname, '../packages/dev') })
payload.logger.info(`Admin URL on http://localhost:3000${payload.getAdminURL()}`) payload.logger.info(`Admin URL on http://localhost:3000${payload.getAdminURL()}`)
payload.logger.info(`API URL on http://localhost:3000${payload.getAPIURL()}`) payload.logger.info(`API URL on http://localhost:3000${payload.getAPIURL()}`)
})
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@@ -1,5 +1,3 @@
import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials' import { devUser } from '../credentials'
import { collectionEndpoints } from './endpoints/collections' import { collectionEndpoints } from './endpoints/collections'
@@ -61,20 +59,6 @@ export default buildConfigWithDefaults({
}, },
], ],
endpoints, endpoints,
admin: {
webpack: (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
express: path.resolve(__dirname, './mocks/emptyModule.js'),
},
},
}
},
},
onInit: async (payload) => { onInit: async (payload) => {
await payload.create({ await payload.create({
collection: 'users', collection: 'users',

View File

@@ -1,37 +1,34 @@
import type { Response } from 'express'
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
export const collectionEndpoints: CollectionConfig['endpoints'] = [ export const collectionEndpoints: CollectionConfig['endpoints'] = [
{ {
path: '/say-hello/joe-bloggs', path: '/say-hello/joe-bloggs',
method: 'get', method: 'get',
handler: (req: PayloadRequest, res: Response): void => { handler: () => {
res.json({ message: 'Hey Joey!' }) return Response.json({ message: 'Hey Joey!' })
}, },
}, },
{ {
path: '/say-hello/:group/:name', path: '/say-hello/:group/:name',
method: 'get', method: 'get',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ routeParams }) => {
res.json({ message: `Hello ${req.params.name} @ ${req.params.group}` }) return Response.json({ message: `Hello ${routeParams.name} @ ${routeParams.group}` })
}, },
}, },
{ {
path: '/say-hello/:name', path: '/say-hello/:name',
method: 'get', method: 'get',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ routeParams }) => {
res.json({ message: `Hello ${req.params.name}!` }) return Response.json({ message: `Hello ${routeParams.name}!` })
}, },
}, },
{ {
path: '/whoami', path: '/whoami',
method: 'post', method: 'post',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ req }) => {
res.json({ return Response.json({
name: req.body.name, name: req.data.name,
age: req.body.age, age: req.data.age,
}) })
}, },
}, },

View File

@@ -1,6 +1,3 @@
import type { Response } from 'express'
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import { globalEndpoint } from '../shared' import { globalEndpoint } from '../shared'
@@ -9,8 +6,8 @@ export const globalEndpoints: GlobalConfig['endpoints'] = [
{ {
path: `/${globalEndpoint}`, path: `/${globalEndpoint}`,
method: 'post', method: 'post',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ req }) => {
res.json(req.body) return Response.json(req.body)
}, },
}, },
] ]

View File

@@ -1,9 +1,4 @@
import type { Response } from 'express'
import express from 'express'
import type { Config } from '../../../packages/payload/src/config/types' import type { Config } from '../../../packages/payload/src/config/types'
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
import { applicationEndpoint, rootEndpoint } from '../shared' import { applicationEndpoint, rootEndpoint } from '../shared'
@@ -11,41 +6,38 @@ export const endpoints: Config['endpoints'] = [
{ {
path: `/${applicationEndpoint}`, path: `/${applicationEndpoint}`,
method: 'post', method: 'post',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ req }) => {
res.json(req.body) return Response.json(req.body)
}, },
}, },
{ {
path: `/${applicationEndpoint}`, path: `/${applicationEndpoint}`,
method: 'get', method: 'get',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ req }) => {
res.json({ message: 'Hello, world!' }) return Response.json({ message: 'Hello, world!' })
}, },
}, },
{ {
path: `/${applicationEndpoint}/i18n`, path: `/${applicationEndpoint}/i18n`,
method: 'get', method: 'get',
handler: (req: PayloadRequest, res: Response): void => { handler: ({ req }) => {
res.json({ message: req.t('general:backToDashboard') }) return Response.json({ message: req.t('general:backToDashboard') })
}, },
}, },
{ {
path: `/${rootEndpoint}`, path: `/${rootEndpoint}`,
method: 'get', method: 'get',
root: true, root: true,
handler: (req: PayloadRequest, res: Response): void => { handler: () => {
res.json({ message: 'Hello, world!' }) return Response.json({ message: 'Hello, world!' })
}, },
}, },
{ {
path: `/${rootEndpoint}`, path: `/${rootEndpoint}`,
method: 'post', method: 'post',
root: true, root: true,
handler: [ handler: ({ req }) => {
express.json({ type: 'application/json' }), return Response.json(req.body)
(req: PayloadRequest, res: Response): void => {
res.json(req.body)
}, },
],
}, },
] ]

View File

@@ -0,0 +1,39 @@
import { createServer } from 'http'
import next from 'next'
import { parse } from 'url'
type args = {
appDir: string
port?: number
}
export const bootAdminPanel = async ({ port = 3000, appDir }: args) => {
const serverURL = `http://localhost:${port}`
const app = next({
dev: true,
hostname: 'localhost',
port,
dir: appDir,
})
const handle = app.getRequestHandler()
await app.prepare()
createServer(async (req, res) => {
try {
const parsedUrl = parse(req.url, true)
console.log('Requested path: ', parsedUrl.path)
await handle(req, res, parsedUrl)
} catch (err) {
console.error('Error occurred handling', req.url, err)
res.statusCode = 500
res.end('internal server error')
}
})
.once('error', (err) => {
console.error(err)
process.exit(1)
})
.listen(port, () => {
console.log(`> Ready on ${serverURL}`)
})
}

View File

@@ -1,14 +1,12 @@
import swcRegister from '@swc/register'
import express from 'express'
import getPort from 'get-port' import getPort from 'get-port'
import path from 'path' import path from 'path'
import shelljs from 'shelljs' import shelljs from 'shelljs'
import { v4 as uuid } from 'uuid'
import type { Payload } from '../../packages/payload/src' import type { Payload } from '../../packages/payload/src'
import type { InitOptions } from '../../packages/payload/src/config/types' import type { InitOptions } from '../../packages/payload/src/config/types'
import payload from '../../packages/payload/src' import { getPayload } from '../../packages/payload/src'
import { bootAdminPanel } from './bootAdminPanel'
type Options = { type Options = {
__dirname: string __dirname: string
@@ -29,42 +27,31 @@ export async function initPayloadE2E(__dirname: string): Promise<InitializedPayl
} }
export async function initPayloadTest(options: Options): Promise<InitializedPayload> { export async function initPayloadTest(options: Options): Promise<InitializedPayload> {
const initOptions = { process.env.PAYLOAD_CONFIG_PATH = path.resolve(options.__dirname, './config.ts')
const initOptions: InitOptions = {
local: true, local: true,
secret: uuid(), config: require(process.env.PAYLOAD_CONFIG_PATH).default,
mongoURL: `mongodb://localhost/${uuid()}`, // loggerOptions: {
// enabled: false,
// },
...(options.init || {}), ...(options.init || {}),
} }
process.env.PAYLOAD_DROP_DATABASE = 'true' process.env.PAYLOAD_DROP_DATABASE = 'true'
process.env.NODE_ENV = 'test' process.env.NODE_ENV = 'test'
process.env.PAYLOAD_CONFIG_PATH = path.resolve(options.__dirname, './config.ts')
if (!initOptions?.local) { const payload = await getPayload(initOptions)
initOptions.express = express()
}
// 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',
},
})
await payload.init(initOptions)
const port = await getPort() const port = await getPort()
if (initOptions.express) { const serverURL = `http://localhost:${port}`
initOptions.express.listen(port)
if (!initOptions?.local) {
process.env.APP_ENV = 'test'
process.env.__NEXT_TEST_MODE = 'jest'
await bootAdminPanel({ port, appDir: path.resolve(__dirname, '../../packages/dev') })
jest.resetModules()
} }
return { serverURL: `http://localhost:${port}`, payload } return { serverURL, payload }
} }

View File

@@ -4,7 +4,6 @@ import payload from '../../packages/payload/src'
import { AuthenticationError } from '../../packages/payload/src/errors' import { AuthenticationError } from '../../packages/payload/src/errors'
import { devUser, regularUser } from '../credentials' import { devUser, regularUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
import { afterOperationSlug } from './collections/AfterOperation' import { afterOperationSlug } from './collections/AfterOperation'
import { chainingHooksSlug } from './collections/ChainingHooks' import { chainingHooksSlug } from './collections/ChainingHooks'
import { contextHooksSlug } from './collections/ContextHooks' import { contextHooksSlug } from './collections/ContextHooks'
@@ -17,17 +16,14 @@ import {
import { relationsSlug } from './collections/Relations' import { relationsSlug } from './collections/Relations'
import { transformSlug } from './collections/Transform' import { transformSlug } from './collections/Transform'
import { hooksUsersSlug } from './collections/Users' import { hooksUsersSlug } from './collections/Users'
import configPromise, { HooksConfig } from './config' import { HooksConfig } from './config'
import { dataHooksGlobalSlug } from './globals/Data' import { dataHooksGlobalSlug } from './globals/Data'
let client: RESTClient
let apiUrl let apiUrl
describe('Hooks', () => { describe('Hooks', () => {
beforeAll(async () => { beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
const config = await configPromise
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug })
apiUrl = `${serverURL}/api` apiUrl = `${serverURL}/api`
}) })

View File

@@ -1,5 +1,5 @@
import type { Payload } from '../../../packages/payload/src' import type { Payload } from '../../../packages/payload/src'
import type { PayloadRequest } from '../../../packages/payload/src/express/types' import type { PayloadRequest } from '../../../packages/payload/src/types'
import { formsSlug, pagesSlug } from '../shared' import { formsSlug, pagesSlug } from '../shared'

View File

@@ -1,5 +1,5 @@
import type { Payload } from '../../../packages/payload/src' import type { Payload } from '../../../packages/payload/src'
import type { PayloadRequest } from '../../../packages/payload/src/express/types' import type { PayloadRequest } from '../../../packages/payload/src/types'
export const seed = async (payload: Payload): Promise<boolean> => { export const seed = async (payload: Payload): Promise<boolean> => {
payload.logger.info('Seeding data...') payload.logger.info('Seeding data...')

View File

@@ -1,5 +1,5 @@
import wait from '../../packages/payload/dist/utilities/wait'
import payload from '../../packages/payload/src' import payload from '../../packages/payload/src'
import wait from '../../packages/payload/src/utilities/wait'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
describe('Search Plugin', () => { describe('Search Plugin', () => {

View File

@@ -1,6 +1,6 @@
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import type { PayloadRequest } from '../../packages/payload/src/express/types' import type { PayloadRequest } from '../../packages/payload/src/types'
import type { import type {
ChainedRelation, ChainedRelation,
CustomIdNumberRelation, CustomIdNumberRelation,

View File

@@ -4,7 +4,7 @@
"noEmit": false /* Do not emit outputs. */, "noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "../" /* Specify the root folder within your source files. */ "rootDir": "../" /* Specify the root folder within your source files. */,
}, },
"exclude": ["dist", "build", "node_modules", ".eslintrc.js"], "exclude": ["dist", "build", "node_modules", ".eslintrc.js"],
"include": [ "include": [
@@ -14,6 +14,11 @@
"src/**/*.json", "src/**/*.json",
"**/*.ts", "**/*.ts",
"test/**/*.tsx", "test/**/*.tsx",
"../packages/**/src/**/*.ts" "../packages/**/src/**/*.ts",
] ],
"plugins": [
{
"name": "next",
},
],
} }

View File

@@ -1,5 +1,3 @@
import path from 'path'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import AutosavePosts from './collections/Autosave' import AutosavePosts from './collections/Autosave'
import DisablePublish from './collections/DisablePublish' import DisablePublish from './collections/DisablePublish'
@@ -12,18 +10,6 @@ import DraftGlobal from './globals/Draft'
import { clearAndSeedEverything } from './seed' import { clearAndSeedEverything } from './seed'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
admin: {
webpack: (config) => ({
...config,
resolve: {
...config.resolve,
alias: {
...config?.resolve?.alias,
fs: path.resolve(__dirname, './mocks/emptyModule.js'),
},
},
}),
},
collections: [DisablePublish, Posts, AutosavePosts, DraftPosts, VersionPosts], collections: [DisablePublish, Posts, AutosavePosts, DraftPosts, VersionPosts],
globals: [AutosaveGlobal, DraftGlobal, DisablePublishGlobal], globals: [AutosaveGlobal, DraftGlobal, DisablePublishGlobal],
indexSortableFields: true, indexSortableFields: true,