Fixes https://github.com/payloadcms/payload/issues/13433. Testing release: `3.54.0-internal.90cf7d5` Previously, when calling `getPayload`, you would always use the same, cached payload instance within a single process, regardless of the arguments passed to the `getPayload` function. This resulted in the following issues - both are fixed by this PR: - If, in your frontend you're calling `getPayload` without `cron: true`, and you're hosting the Payload Admin Panel in the same process, crons will not be enabled even if you visit the admin panel which calls `getPayload` with `cron: true`. This will break jobs autorun depending on which page you visit first - admin panel or frontend - Within the same process, you are unable to use `getPayload` twice for different instances of payload with different Payload Configs. On postgres, you can get around this by manually calling new `BasePayload()` which skips the cache. This did not work on mongoose though, as mongoose was caching the models on a global singleton (this PR addresses this). In order to bust the cache for different Payload Config, this PR introduces a new, optional `key` property to `getPayload`. ## Mongoose - disable using global singleton This PR refactors the Payload Mongoose adapter to stop relying on the global mongoose singleton. Instead, each adapter instance now creates and manages its own scoped Connection object. ### Motivation Previously, calling `getPayload()` more than once in the same process would throw `Cannot overwrite model` errors because models were compiled into the global singleton. This prevented running multiple Payload instances side-by-side, even when pointing at different databases. ### Changes - Replace usage of `mongoose.connect()` / `mongoose.model()` with instance-scoped `createConnection()` and `connection.model()`. - Ensure models, globals, and versions are compiled per connection, not globally. - Added proper `close()` handling on `this.connection` instead of `mongoose.disconnect()`. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211114366468745
188 lines
5.4 KiB
TypeScript
188 lines
5.4 KiB
TypeScript
import { execSync } from 'child_process'
|
|
import { existsSync, readFileSync, rmSync } from 'fs'
|
|
import path from 'path'
|
|
import { type BlocksField, getPayload, type Payload } from 'payload'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
|
|
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|
import { testFilePath } from './testFilePath.js'
|
|
|
|
let restClient: NextRESTClient
|
|
let payload: Payload
|
|
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
describe('Config', () => {
|
|
beforeAll(async () => {
|
|
;({ payload, restClient } = await initPayloadInt(dirname))
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await payload.destroy()
|
|
})
|
|
|
|
describe('payload config', () => {
|
|
it('allows a custom field at the config root', () => {
|
|
const { config } = payload
|
|
expect(config.custom).toEqual({
|
|
name: 'Customer portal',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in the root endpoints', () => {
|
|
const [endpoint] = payload.config.endpoints
|
|
|
|
expect(endpoint.custom).toEqual({
|
|
description: 'Get the sanitized payload config',
|
|
})
|
|
})
|
|
|
|
it('should allow multiple getPayload calls using different configs in same process', async () => {
|
|
const payload2 = await getPayload({
|
|
key: 'payload2',
|
|
config: await buildConfigWithDefaults({
|
|
collections: [
|
|
{
|
|
slug: 'payload2',
|
|
fields: [{ name: 'title2', type: 'text' }],
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
|
|
// Use payload2 instance before creating payload3 instance, as we share the same db connection => each instance
|
|
// creation will reset the db schema.
|
|
const result2: any = await payload2.create({
|
|
collection: 'payload2',
|
|
data: {
|
|
title2: 'Payload 2',
|
|
},
|
|
} as any)
|
|
|
|
expect(result2.title2).toBe('Payload 2')
|
|
|
|
const payload3 = await getPayload({
|
|
key: 'payload3',
|
|
config: await buildConfigWithDefaults({
|
|
collections: [
|
|
{
|
|
slug: 'payload3',
|
|
fields: [{ name: 'title3', type: 'text' }],
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
|
|
// If payload was still incorrectly cached, this would fail, as the old payload config would still be used
|
|
const result3: any = await payload3.create({
|
|
collection: 'payload3',
|
|
data: {
|
|
title3: 'Payload 3',
|
|
},
|
|
} as any)
|
|
|
|
expect(result3.title3).toBe('Payload 3')
|
|
|
|
await payload2.destroy()
|
|
await payload3.destroy()
|
|
})
|
|
})
|
|
|
|
describe('collection config', () => {
|
|
it('allows a custom field in collections', () => {
|
|
const [collection] = payload.config.collections
|
|
expect(collection.custom).toEqual({
|
|
externalLink: 'https://foo.bar',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in collection endpoints', () => {
|
|
const [collection] = payload.config.collections
|
|
const [endpoint] = collection.endpoints || []
|
|
|
|
expect(endpoint.custom).toEqual({
|
|
examples: [{ type: 'response', value: { message: 'hi' } }],
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in collection fields', () => {
|
|
const [collection] = payload.config.collections
|
|
const [field] = collection.fields
|
|
|
|
expect(field.custom).toEqual({
|
|
description: 'The title of this page',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in blocks in collection fields', () => {
|
|
const [collection] = payload.config.collections
|
|
const [, blocksField] = collection.fields
|
|
|
|
expect((blocksField as BlocksField).blocks[0].custom).toEqual({
|
|
description: 'The blockOne of this page',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('global config', () => {
|
|
it('allows a custom field in globals', () => {
|
|
const [global] = payload.config.globals
|
|
expect(global.custom).toEqual({ foo: 'bar' })
|
|
})
|
|
|
|
it('allows a custom field in global endpoints', () => {
|
|
const [global] = payload.config.globals
|
|
const [endpoint] = global.endpoints || []
|
|
|
|
expect(endpoint.custom).toEqual({ params: [{ name: 'name', type: 'string', in: 'query' }] })
|
|
})
|
|
|
|
it('allows a custom field in global fields', () => {
|
|
const [global] = payload.config.globals
|
|
const [field] = global.fields
|
|
|
|
expect(field.custom).toEqual({
|
|
description: 'The title of my global',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('cors config', () => {
|
|
it('includes a custom header in Access-Control-Allow-Headers', async () => {
|
|
const response = await restClient.GET(`/pages`)
|
|
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
|
|
})
|
|
})
|
|
|
|
describe('bin config', () => {
|
|
const executeCLI = (command: string) => {
|
|
execSync(`pnpm tsx "${path.resolve(dirname, 'bin.ts')}" ${command}`, {
|
|
env: {
|
|
...process.env,
|
|
PAYLOAD_CONFIG_PATH: path.resolve(dirname, 'config.ts'),
|
|
PAYLOAD_DROP_DATABASE: 'false',
|
|
},
|
|
stdio: 'inherit',
|
|
cwd: path.resolve(dirname, '../..'), // from root
|
|
})
|
|
}
|
|
|
|
const deleteTestFile = () => {
|
|
if (existsSync(testFilePath)) {
|
|
rmSync(testFilePath)
|
|
}
|
|
}
|
|
|
|
it.skip('should execute a custom script', () => {
|
|
deleteTestFile()
|
|
executeCLI('start-server')
|
|
expect(JSON.parse(readFileSync(testFilePath, 'utf-8')).docs).toHaveLength(1)
|
|
deleteTestFile()
|
|
})
|
|
})
|
|
})
|