feat: initial test suite framework (#4929)
This commit is contained in:
3
__mocks__/payload-config.ts
Normal file
3
__mocks__/payload-config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default typeof process.env.PAYLOAD_CONFIG_PATH === 'string'
|
||||
? require(process.env.PAYLOAD_CONFIG_PATH)
|
||||
: {}
|
||||
@@ -1,9 +1,10 @@
|
||||
module.exports = {
|
||||
const customJestConfig = {
|
||||
globalSetup: './test/jest.setup.ts',
|
||||
moduleNameMapper: {
|
||||
'\\.(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)$':
|
||||
'<rootDir>/packages/payload/src/bundlers/mocks/fileMock.js',
|
||||
'payload-config': '<rootDir>/__mocks__/payload-config.ts',
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/packages/payload/src/**/*.spec.ts', '<rootDir>/test/**/*int.spec.ts'],
|
||||
@@ -13,3 +14,5 @@ module.exports = {
|
||||
},
|
||||
verbose: true,
|
||||
}
|
||||
|
||||
module.exports = customJestConfig
|
||||
|
||||
@@ -80,16 +80,18 @@
|
||||
"lexical": "0.12.5",
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "8.13.0",
|
||||
"next": "14.0.2",
|
||||
"next": "14.1.1-canary.26",
|
||||
"node-fetch": "2.6.12",
|
||||
"nodemon": "3.0.2",
|
||||
"pino": "8.15.0",
|
||||
"pino-pretty": "10.2.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prompts": "2.4.2",
|
||||
"qs": "6.11.2",
|
||||
"read-stream": "^2.1.1",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.32.6",
|
||||
"shelljs": "0.8.5",
|
||||
"simple-git": "^3.20.0",
|
||||
"slash": "3.0.0",
|
||||
@@ -102,7 +104,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-i18next": "11.18.6",
|
||||
"react-router-dom": "5.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -27,18 +27,17 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"bson-ext": "^4.0.3",
|
||||
"bson-objectid": "2.0.4",
|
||||
"deepmerge": "4.3.1",
|
||||
"get-port": "5.1.1",
|
||||
"mongoose": "6.12.0",
|
||||
"mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
|
||||
"mongodb-memory-server": "8.13.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
|
||||
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) {
|
||||
urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { Init } from 'payload/database'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
import {
|
||||
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(
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import type {
|
||||
AggregatePaginateModel,
|
||||
IndexDefinition,
|
||||
IndexOptions,
|
||||
Model,
|
||||
PaginateModel,
|
||||
SchemaOptions,
|
||||
} from 'mongoose'
|
||||
import type { IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'
|
||||
import type { Payload } from 'payload'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type {
|
||||
@@ -34,11 +27,7 @@ import type {
|
||||
|
||||
import type { BuildQueryArgs } from './queries/buildQuery'
|
||||
|
||||
export interface CollectionModel
|
||||
extends Model<any>,
|
||||
PaginateModel<any>,
|
||||
AggregatePaginateModel<any>,
|
||||
PassportLocalModel {
|
||||
export interface CollectionModel extends Model<any>, PaginateModel<any>, PassportLocalModel {
|
||||
/** 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
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ export const upsertRow = async <T extends TypeWithID>({
|
||||
message: req.t('error:valueMustBeUnique'),
|
||||
},
|
||||
],
|
||||
req?.t ?? i18nInit(req.payload.config.i18n).t,
|
||||
req.t,
|
||||
)
|
||||
: error
|
||||
}
|
||||
|
||||
1
packages/dev/mocks.js
Normal file
1
packages/dev/mocks.js
Normal file
@@ -0,0 +1 @@
|
||||
export const mongooseAdapter = () => ({})
|
||||
@@ -8,8 +8,6 @@ const nextConfig = {
|
||||
},
|
||||
serverComponentsExternalPackages: ['drizzle-kit', 'drizzle-kit/utils', 'pino', 'pino-pretty'],
|
||||
},
|
||||
reactStrictMode: false,
|
||||
// transpilePackages: ['@payloadcms/db-mongodb', 'mongoose'],
|
||||
webpack: (config) => {
|
||||
return {
|
||||
...config,
|
||||
@@ -19,15 +17,30 @@ const nextConfig = {
|
||||
'drizzle-kit/utils',
|
||||
'pino',
|
||||
'pino-pretty',
|
||||
'mongoose',
|
||||
'sharp',
|
||||
],
|
||||
ignoreWarnings: [
|
||||
...(config.ignoreWarnings || []),
|
||||
{ module: /node_modules\/mongodb\/lib\/utils\.js/ },
|
||||
{ file: /node_modules\/mongodb\/lib\/utils\.js/ },
|
||||
],
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
graphql$: path.resolve(__dirname, '../next/node_modules/graphql/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
6
packages/dev/server.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { bootAdminPanel } = require('../../test/helpers/bootAdminPanel.ts')
|
||||
|
||||
bootAdminPanel({
|
||||
appDir: __dirname,
|
||||
port: 3000,
|
||||
})
|
||||
@@ -36,7 +36,6 @@ export default buildConfig({
|
||||
// title: 'Test Page',
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// await payload.update({
|
||||
// collection: 'pages',
|
||||
// id: page.id,
|
||||
|
||||
@@ -17,21 +17,20 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
"name": "next",
|
||||
},
|
||||
],
|
||||
"paths": {
|
||||
"payload": ["../payload/src"],
|
||||
"payload/*": ["../payload/src/exports/*"],
|
||||
"payload-config": ["./src/payload.config.ts"],
|
||||
"@payloadcms/db-mongodb": ["../db-mongodb/src"],
|
||||
"@payloadcms/richtext-lexical": ["../richtext-lexical/src"],
|
||||
"@payloadcms/ui/*": ["../ui/src/exports/*"],
|
||||
"@payloadcms/translations": ["../translations/src/exports/index.ts"],
|
||||
"@payloadcms/translations/client": ["../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"],
|
||||
"exclude": ["node_modules"],
|
||||
@@ -43,6 +42,6 @@
|
||||
{ "path": "../translations" },
|
||||
{ "path": "../db-mongodb" },
|
||||
{ "path": "../db-postgres" },
|
||||
{ "path": "../richtext-lexical" }
|
||||
]
|
||||
{ "path": "../richtext-lexical" },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -2,5 +2,7 @@ import { PayloadRequest } from 'payload/types'
|
||||
|
||||
export type Context = {
|
||||
req: PayloadRequest
|
||||
headers: Headers
|
||||
headers: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"http-status": "1.6.2",
|
||||
"i18next": "22.5.1",
|
||||
"next": "^14.0.0",
|
||||
"next": "14.1.1-canary.26",
|
||||
"payload": "^2.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -111,8 +111,13 @@ export const POST = (config: Promise<SanitizedConfig>) => async (request: Reques
|
||||
validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)),
|
||||
})(originalRequest)
|
||||
|
||||
const resHeaders = new Headers(apiResponse.headers)
|
||||
for (let key in headers) {
|
||||
resHeaders.append(key, headers[key])
|
||||
}
|
||||
|
||||
return new Response(apiResponse.body, {
|
||||
status: apiResponse.status,
|
||||
headers: new Headers(headers),
|
||||
headers: new Headers(resHeaders),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"paths": {
|
||||
"payload-config": ["./src/config.ts"],
|
||||
"@payloadcms/graphql": ["../graphql/dist/index.ts"],
|
||||
"@payloadcms/ui": ["../ui/src/exports/index.ts"],
|
||||
"@payloadcms/translations/*": ["../translations/dist/*"]
|
||||
|
||||
@@ -9,7 +9,7 @@ export const APIKeyAuthentication =
|
||||
async ({ headers, payload }) => {
|
||||
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 apiKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex')
|
||||
|
||||
@@ -38,8 +38,6 @@ export const APIKeyAuthentication =
|
||||
collection: collectionConfig.slug,
|
||||
depth: collectionConfig.auth.depth,
|
||||
overrideAccess: true,
|
||||
// TODO(JAMES)(REVIEW): had to remove with new pattern
|
||||
// req,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ export const build = async (): Promise<void> => {
|
||||
disableOnInit: true,
|
||||
local: true,
|
||||
})
|
||||
|
||||
await payload.config.admin.bundler.build(payload.config)
|
||||
}
|
||||
|
||||
// when build.js is launched directly
|
||||
|
||||
@@ -238,7 +238,7 @@ export type PayloadHandler = ({
|
||||
/**
|
||||
* 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. */
|
||||
custom?: Record<string, any>
|
||||
/**
|
||||
|
||||
@@ -47,7 +47,6 @@ import { decrypt, encrypt } from './auth/crypto'
|
||||
import { APIKeyAuthentication } from './auth/strategies/apiKey'
|
||||
import { JWTAuthentication } from './auth/strategies/jwt'
|
||||
import localOperations from './collections/operations/local'
|
||||
import findConfig from './config/find'
|
||||
import buildEmail from './email/build'
|
||||
import { defaults as emailDefaults } from './email/defaults'
|
||||
import sendEmail from './email/sendEmail'
|
||||
@@ -311,14 +310,14 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
this.config = await options.config
|
||||
|
||||
// TODO(JARROD/JAMES): can we keep this?
|
||||
const configPath = findConfig()
|
||||
// const configPath = findConfig()
|
||||
this.config = {
|
||||
...this.config,
|
||||
paths: {
|
||||
config: configPath,
|
||||
configDir: path.dirname(configPath),
|
||||
rawConfig: configPath,
|
||||
},
|
||||
// paths: {
|
||||
// config: configPath,
|
||||
// configDir: path.dirname(configPath),
|
||||
// rawConfig: configPath,
|
||||
// },
|
||||
}
|
||||
|
||||
if (!this.config.secret) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
import { Tooltip } from 'payload/components'
|
||||
import { Tooltip } from '@payloadcms/ui'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useSlate } from 'slate-react'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { ShimmerEffect } from 'payload/components'
|
||||
import { ShimmerEffect } from '@payloadcms/ui'
|
||||
import React, { Suspense, lazy } from 'react'
|
||||
|
||||
import type { FieldProps } from '../types'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
|
||||
|
||||
@@ -34,3 +34,4 @@ export { useDrawerSlug } from '../elements/Drawer/useDrawerSlug'
|
||||
export { default as Popup } from '../elements/Popup'
|
||||
// export { useThumbnail } from '../elements/Upload'
|
||||
export { Translation } from '../elements/Translation'
|
||||
export { Tooltip } from '../elements/Tooltip'
|
||||
|
||||
@@ -290,8 +290,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
await priorRelation
|
||||
|
||||
const idsToLoad = ids.filter((id) => {
|
||||
return !options.find(
|
||||
(optionGroup) =>
|
||||
return !options.find((optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
|
||||
643
pnpm-lock.yaml
generated
643
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
packages:
|
||||
# all packages in direct subdirs of packages/
|
||||
- 'packages/*'
|
||||
- 'test/REST_API'
|
||||
# exclude packages that are inside test directories
|
||||
- '!**/test/**'
|
||||
# - '!**/test/**'
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import payload from '../../packages/payload/src'
|
||||
import type { Payload } from '../../packages/payload/src'
|
||||
|
||||
import { devUser } from '../credentials'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { postsSlug } from './collections/Posts'
|
||||
|
||||
require('isomorphic-fetch')
|
||||
|
||||
let apiUrl
|
||||
let payload: Payload
|
||||
let apiURL: string
|
||||
let jwt
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
const { email, password } = devUser
|
||||
|
||||
describe('_Community Tests', () => {
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// Boilerplate test setup/teardown
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
|
||||
apiUrl = `${serverURL}/api`
|
||||
const { payload: payloadClient, serverURL } = await initPayloadTest({
|
||||
__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({
|
||||
email,
|
||||
password,
|
||||
@@ -56,7 +64,7 @@ describe('_Community Tests', () => {
|
||||
})
|
||||
|
||||
it('rest API example', async () => {
|
||||
const newPost = await fetch(`${apiUrl}/${postsSlug}`, {
|
||||
const newPost = await fetch(`${apiURL}/${postsSlug}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { User } from '../../packages/payload/src/auth'
|
||||
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> = () => {
|
||||
const [state, setState] = useState<User | null | undefined>()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
import type { Payload } from '../../packages/payload/src'
|
||||
import type { User } from '../../packages/payload/src/auth'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import configPromise from '../collections-graphql/config'
|
||||
import { devUser } from '../credentials'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
@@ -13,6 +13,7 @@ require('isomorphic-fetch')
|
||||
|
||||
let apiUrl
|
||||
let client: GraphQLClient
|
||||
let payload: Payload
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -22,7 +23,11 @@ const { email, password } = devUser
|
||||
|
||||
describe('Auth', () => {
|
||||
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`
|
||||
const config = await configPromise
|
||||
const url = `${serverURL}${config.routes.api}${config.routes.graphQL}`
|
||||
@@ -35,6 +40,10 @@ describe('Auth', () => {
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
describe('GraphQL - admin user', () => {
|
||||
let token
|
||||
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', () => {
|
||||
it('should authenticate via the correct API key user', async () => {
|
||||
const usersQuery = await payload.find({
|
||||
|
||||
@@ -2,8 +2,6 @@ import path from 'path'
|
||||
|
||||
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 { postgresAdapter } from '../packages/db-postgres/src'
|
||||
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'
|
||||
|
||||
const bundlerAdapters = {
|
||||
vite: viteBundler(),
|
||||
webpack: webpackBundler(),
|
||||
}
|
||||
|
||||
const databaseAdapters = {
|
||||
mongoose: mongooseAdapter({
|
||||
migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'),
|
||||
@@ -30,10 +23,9 @@ const databaseAdapters = {
|
||||
}
|
||||
|
||||
export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> {
|
||||
const [name] = process.argv.slice(2)
|
||||
|
||||
const config: Config = {
|
||||
editor: slateEditor({}),
|
||||
secret: 'TEST_SECRET',
|
||||
editor: undefined,
|
||||
rateLimit: {
|
||||
max: 9999999999,
|
||||
window: 15 * 60 * 1000, // 15min default,
|
||||
@@ -53,49 +45,6 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
},
|
||||
...(config.admin || {}),
|
||||
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') {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default buildConfigWithDefaults({
|
||||
{
|
||||
path: '/send-test-email',
|
||||
method: 'get',
|
||||
handler: async (req, res) => {
|
||||
handler: async ({ req }) => {
|
||||
await req.payload.sendEmail({
|
||||
from: 'dev@payloadcms.com',
|
||||
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',
|
||||
method: 'get',
|
||||
handler: async (req, res, next) => {
|
||||
try {
|
||||
handler: () => {
|
||||
// Throwing an internal error with potentially sensitive data
|
||||
throw new Error('Lost connection to the Pentagon. Secret data: ******')
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -15,8 +15,8 @@ export default buildConfigWithDefaults({
|
||||
{
|
||||
path: '/hello',
|
||||
method: 'get',
|
||||
handler: (_, res): void => {
|
||||
res.json({ message: 'hi' })
|
||||
handler: () => {
|
||||
return Response.json({ message: 'hi' })
|
||||
},
|
||||
custom: { examples: [{ type: 'response', value: { message: 'hi' } }] },
|
||||
},
|
||||
@@ -38,9 +38,9 @@ export default buildConfigWithDefaults({
|
||||
{
|
||||
path: '/greet',
|
||||
method: 'get',
|
||||
handler: (req, res): void => {
|
||||
const { name } = req.query
|
||||
res.json({ message: `Hi ${name}!` })
|
||||
handler: ({ req }) => {
|
||||
const sp = new URL(req.url).searchParams
|
||||
return Response.json({ message: `Hi ${sp.get('name')}!` })
|
||||
},
|
||||
custom: { params: [{ in: 'query', name: 'name', type: 'string' }] },
|
||||
},
|
||||
@@ -60,8 +60,8 @@ export default buildConfigWithDefaults({
|
||||
path: '/config',
|
||||
method: 'get',
|
||||
root: true,
|
||||
handler: (req, res): void => {
|
||||
res.json(req.payload.config)
|
||||
handler: ({ req }) => {
|
||||
return Response.json(req.payload.config)
|
||||
},
|
||||
custom: { description: 'Get the sanitized payload config' },
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Config', () => {
|
||||
|
||||
it('allows a custom field in collection endpoints', () => {
|
||||
const [collection] = payload.config.collections
|
||||
const [endpoint] = collection.endpoints
|
||||
const [endpoint] = collection.endpoints || []
|
||||
|
||||
expect(endpoint.custom).toEqual({
|
||||
examples: [{ type: 'response', value: { message: 'hi' } }],
|
||||
@@ -52,7 +52,7 @@ describe('Config', () => {
|
||||
|
||||
it('allows a custom field in global endpoints', () => {
|
||||
const [global] = payload.config.globals
|
||||
const [endpoint] = global.endpoints
|
||||
const [endpoint] = global.endpoints || []
|
||||
|
||||
expect(endpoint.custom).toEqual({ params: [{ in: 'query', name: 'name', type: 'string' }] })
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
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 { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction'
|
||||
|
||||
27
test/dev.ts
27
test/dev.ts
@@ -1,11 +1,10 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import express from 'express'
|
||||
import fs from 'fs'
|
||||
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 { bootAdminPanel } from './helpers/bootAdminPanel'
|
||||
import { startLivePreviewDemo } from './live-preview/startLivePreviewDemo'
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
const expressApp = express()
|
||||
|
||||
const startDev = async () => {
|
||||
await payload.init({
|
||||
const payload = await getPayload({
|
||||
email: {
|
||||
fromAddress: 'hello@payloadcms.com',
|
||||
fromName: 'Payload',
|
||||
logMockCredentials: false,
|
||||
},
|
||||
express: expressApp,
|
||||
secret: uuid(),
|
||||
config: require(configPath).default,
|
||||
...prettySyncLogger,
|
||||
onInit: async (payload) => {
|
||||
onInit: (payload) => {
|
||||
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') {
|
||||
await startLivePreviewDemo({
|
||||
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(`API URL on http://localhost:3000${payload.getAPIURL()}`)
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
|
||||
import { devUser } from '../credentials'
|
||||
import { collectionEndpoints } from './endpoints/collections'
|
||||
@@ -61,20 +59,6 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
endpoints,
|
||||
admin: {
|
||||
webpack: (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
express: path.resolve(__dirname, './mocks/emptyModule.js'),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
import type { Response } from 'express'
|
||||
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
|
||||
|
||||
export const collectionEndpoints: CollectionConfig['endpoints'] = [
|
||||
{
|
||||
path: '/say-hello/joe-bloggs',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: 'Hey Joey!' })
|
||||
handler: () => {
|
||||
return Response.json({ message: 'Hey Joey!' })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/say-hello/:group/:name',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: `Hello ${req.params.name} @ ${req.params.group}` })
|
||||
handler: ({ routeParams }) => {
|
||||
return Response.json({ message: `Hello ${routeParams.name} @ ${routeParams.group}` })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/say-hello/:name',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: `Hello ${req.params.name}!` })
|
||||
handler: ({ routeParams }) => {
|
||||
return Response.json({ message: `Hello ${routeParams.name}!` })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/whoami',
|
||||
method: 'post',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({
|
||||
name: req.body.name,
|
||||
age: req.body.age,
|
||||
handler: ({ req }) => {
|
||||
return Response.json({
|
||||
name: req.data.name,
|
||||
age: req.data.age,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 { globalEndpoint } from '../shared'
|
||||
@@ -9,8 +6,8 @@ export const globalEndpoints: GlobalConfig['endpoints'] = [
|
||||
{
|
||||
path: `/${globalEndpoint}`,
|
||||
method: 'post',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json(req.body)
|
||||
handler: ({ req }) => {
|
||||
return Response.json(req.body)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import type { Response } from 'express'
|
||||
|
||||
import express from 'express'
|
||||
|
||||
import type { Config } from '../../../packages/payload/src/config/types'
|
||||
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
|
||||
|
||||
import { applicationEndpoint, rootEndpoint } from '../shared'
|
||||
|
||||
@@ -11,41 +6,38 @@ export const endpoints: Config['endpoints'] = [
|
||||
{
|
||||
path: `/${applicationEndpoint}`,
|
||||
method: 'post',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json(req.body)
|
||||
handler: ({ req }) => {
|
||||
return Response.json(req.body)
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${applicationEndpoint}`,
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: 'Hello, world!' })
|
||||
handler: ({ req }) => {
|
||||
return Response.json({ message: 'Hello, world!' })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${applicationEndpoint}/i18n`,
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: req.t('general:backToDashboard') })
|
||||
handler: ({ req }) => {
|
||||
return Response.json({ message: req.t('general:backToDashboard') })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${rootEndpoint}`,
|
||||
method: 'get',
|
||||
root: true,
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: 'Hello, world!' })
|
||||
handler: () => {
|
||||
return Response.json({ message: 'Hello, world!' })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${rootEndpoint}`,
|
||||
method: 'post',
|
||||
root: true,
|
||||
handler: [
|
||||
express.json({ type: 'application/json' }),
|
||||
(req: PayloadRequest, res: Response): void => {
|
||||
res.json(req.body)
|
||||
handler: ({ req }) => {
|
||||
return Response.json(req.body)
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
39
test/helpers/bootAdminPanel.ts
Normal file
39
test/helpers/bootAdminPanel.ts
Normal 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}`)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import swcRegister from '@swc/register'
|
||||
import express from 'express'
|
||||
import getPort from 'get-port'
|
||||
import path from 'path'
|
||||
import shelljs from 'shelljs'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { Payload } from '../../packages/payload/src'
|
||||
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 = {
|
||||
__dirname: string
|
||||
@@ -29,42 +27,31 @@ export async function initPayloadE2E(__dirname: string): Promise<InitializedPayl
|
||||
}
|
||||
|
||||
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,
|
||||
secret: uuid(),
|
||||
mongoURL: `mongodb://localhost/${uuid()}`,
|
||||
config: require(process.env.PAYLOAD_CONFIG_PATH).default,
|
||||
// loggerOptions: {
|
||||
// enabled: false,
|
||||
// },
|
||||
...(options.init || {}),
|
||||
}
|
||||
|
||||
process.env.PAYLOAD_DROP_DATABASE = 'true'
|
||||
process.env.NODE_ENV = 'test'
|
||||
process.env.PAYLOAD_CONFIG_PATH = path.resolve(options.__dirname, './config.ts')
|
||||
|
||||
if (!initOptions?.local) {
|
||||
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 payload = await getPayload(initOptions)
|
||||
|
||||
const port = await getPort()
|
||||
if (initOptions.express) {
|
||||
initOptions.express.listen(port)
|
||||
const serverURL = `http://localhost:${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 }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import payload from '../../packages/payload/src'
|
||||
import { AuthenticationError } from '../../packages/payload/src/errors'
|
||||
import { devUser, regularUser } from '../credentials'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
import { afterOperationSlug } from './collections/AfterOperation'
|
||||
import { chainingHooksSlug } from './collections/ChainingHooks'
|
||||
import { contextHooksSlug } from './collections/ContextHooks'
|
||||
@@ -17,17 +16,14 @@ import {
|
||||
import { relationsSlug } from './collections/Relations'
|
||||
import { transformSlug } from './collections/Transform'
|
||||
import { hooksUsersSlug } from './collections/Users'
|
||||
import configPromise, { HooksConfig } from './config'
|
||||
import { HooksConfig } from './config'
|
||||
import { dataHooksGlobalSlug } from './globals/Data'
|
||||
|
||||
let client: RESTClient
|
||||
let apiUrl
|
||||
|
||||
describe('Hooks', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
|
||||
const config = await configPromise
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug })
|
||||
apiUrl = `${serverURL}/api`
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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> => {
|
||||
payload.logger.info('Seeding data...')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import wait from '../../packages/payload/dist/utilities/wait'
|
||||
import payload from '../../packages/payload/src'
|
||||
import wait from '../../packages/payload/src/utilities/wait'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
|
||||
describe('Search Plugin', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
import type { PayloadRequest } from '../../packages/payload/src/express/types'
|
||||
import type { PayloadRequest } from '../../packages/payload/src/types'
|
||||
import type {
|
||||
ChainedRelation,
|
||||
CustomIdNumberRelation,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"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"],
|
||||
"include": [
|
||||
@@ -14,6 +14,11 @@
|
||||
"src/**/*.json",
|
||||
"**/*.ts",
|
||||
"test/**/*.tsx",
|
||||
"../packages/**/src/**/*.ts"
|
||||
]
|
||||
"../packages/**/src/**/*.ts",
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
|
||||
import AutosavePosts from './collections/Autosave'
|
||||
import DisablePublish from './collections/DisablePublish'
|
||||
@@ -12,18 +10,6 @@ import DraftGlobal from './globals/Draft'
|
||||
import { clearAndSeedEverything } from './seed'
|
||||
|
||||
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],
|
||||
globals: [AutosaveGlobal, DraftGlobal, DisablePublishGlobal],
|
||||
indexSortableFields: true,
|
||||
|
||||
Reference in New Issue
Block a user