diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 74f4f00cea..c51de58e2b 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -63,41 +63,41 @@ export default buildConfig({ The following options are available: -| Option | Description | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). | -| **`bin`** | Register custom bin scripts for Payload to execute. | -| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). | -| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). | -| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. | -| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). | -| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). | -| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). | -| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). | -| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). | -| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). | -| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. | -| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). | -| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. | -| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). | -| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). | -| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. | -| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). | -| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. | -| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). | -| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). | -| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). | -| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. | -| **`debug`** | Enable to expose more detailed error information. | -| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). | -| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). | -| **`plugins`** | An array of Plugins. [More details](../plugins/overview). | -| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins). | -| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). | -| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. | -| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. | -| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). | +| Option | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). | +| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). | +| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). | +| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). | +| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. | +| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). | +| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). | +| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). | +| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). | +| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). | +| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). | +| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. | +| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). | +| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. | +| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). | +| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). | +| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. | +| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). | +| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. | +| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). | +| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). | +| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). | +| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. | +| **`debug`** | Enable to expose more detailed error information. | +| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). | +| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). | +| **`plugins`** | An array of Plugins. [More details](../plugins/overview). | +| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins). | +| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). | +| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. | +| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. | +| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). | _* An asterisk denotes that a property is required._ @@ -265,3 +265,43 @@ The Payload Config can accept compatibility flags for running the newest version Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized. By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating. + + +## Custom bin scripts + +Using the `bin` configuration property, you can inject your own scripts to `npx payload`. +Example for `pnpm payload seed`: + + +Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with: + +```ts +import type { SanitizedConfig } from 'payload' + +import payload from 'payload' + +// Script must define a "script" function export that accepts the sanitized config +export const script = async (config: SanitizedConfig) => { + await payload.init({ config }) + await payload.create({ collection: 'pages', data: { title: 'my title' } }) + payload.logger.info('Succesffully seeded!') + process.exit(0) +} +``` + +Step 2: add the `seed` script to `bin`: +```ts +export default buildConfig({ + bin: [ + { + scriptPath: path.resolve(dirname, 'seed.ts'), + key: 'seed', + }, + ], +}) +``` + +Now you can run the command using: +```sh +pnpm payload seed +``` \ No newline at end of file diff --git a/packages/payload/src/bin/index.ts b/packages/payload/src/bin/index.ts index f9eb3714b9..2faa0697ad 100755 --- a/packages/payload/src/bin/index.ts +++ b/packages/payload/src/bin/index.ts @@ -76,8 +76,18 @@ export const bin = async () => { if (userBinScript) { try { - const script: BinScript = await import(pathToFileURL(userBinScript.scriptPath).toString()) - await script(config) + const module = await import(pathToFileURL(userBinScript.scriptPath).toString()) + + if (!module.script || typeof module.script !== 'function') { + console.error( + `Could not find "script" function export for script ${userBinScript.key} in ${userBinScript.scriptPath}`, + ) + } else { + await module.script(config).catch((err: unknown) => { + console.log(`Script ${userBinScript.key} failed, details:`) + console.error(err) + }) + } } catch (err) { console.log(`Could not find associated bin script for the ${userBinScript.key} command`) console.error(err) diff --git a/test/config/bin.ts b/test/config/bin.ts new file mode 100644 index 0000000000..0ab75bdeb8 --- /dev/null +++ b/test/config/bin.ts @@ -0,0 +1,8 @@ +import path from 'path' +import { fileURLToPath } from 'url' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) +const { bin } = await import(path.resolve(dirname, '../../packages/payload/src/bin/index.js')) + +await bin() diff --git a/test/config/config.ts b/test/config/config.ts index c777de4113..957b7c7996 100644 --- a/test/config/config.ts +++ b/test/config/config.ts @@ -80,6 +80,12 @@ export default buildConfigWithDefaults({ path: '/config', }, ], + bin: [ + { + scriptPath: path.resolve(dirname, 'customScript.ts'), + key: 'start-server', + }, + ], globals: [ { slug: 'my-global', @@ -107,13 +113,17 @@ export default buildConfigWithDefaults({ }, ], onInit: async (payload) => { - await payload.create({ - collection: 'users', - data: { - email: devUser.email, - password: devUser.password, - }, - }) + const { totalDocs } = await payload.count({ collection: 'users' }) + + if (totalDocs === 0) { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + } }, typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), diff --git a/test/config/customScript.ts b/test/config/customScript.ts new file mode 100644 index 0000000000..24989e5791 --- /dev/null +++ b/test/config/customScript.ts @@ -0,0 +1,13 @@ +import type { SanitizedConfig } from 'payload' + +import { writeFileSync } from 'fs' +import payload from 'payload' + +import { testFilePath } from './testFilePath.js' + +export const script = async (config: SanitizedConfig) => { + await payload.init({ config }) + const data = await payload.find({ collection: 'users' }) + writeFileSync(testFilePath, JSON.stringify(data), 'utf-8') + process.exit(0) +} diff --git a/test/config/int.spec.ts b/test/config/int.spec.ts index c6750ad690..c3a843cc00 100644 --- a/test/config/int.spec.ts +++ b/test/config/int.spec.ts @@ -1,11 +1,14 @@ import type { BlockField, Payload } from 'payload' +import { execSync } from 'child_process' +import { existsSync, readFileSync, rmSync } from 'fs' import path from 'path' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' +import { testFilePath } from './testFilePath.js' let restClient: NextRESTClient let payload: Payload @@ -106,4 +109,31 @@ describe('Config', () => { 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('should execute a custom script', () => { + deleteTestFile() + executeCLI('start-server') + expect(JSON.parse(readFileSync(testFilePath, 'utf-8')).docs).toHaveLength(1) + deleteTestFile() + }) + }) }) diff --git a/test/config/testFilePath.ts b/test/config/testFilePath.ts new file mode 100644 index 0000000000..a65672e109 --- /dev/null +++ b/test/config/testFilePath.ts @@ -0,0 +1,7 @@ +import path from 'path' +import { fileURLToPath } from 'url' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export const testFilePath = path.resolve(dirname, '_data.json')