Compare commits
45 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cda7d2363 | ||
|
|
ea48cfbfe9 | ||
|
|
1aeb912762 | ||
|
|
ce2cb35d71 | ||
|
|
d3ec68ac2f | ||
|
|
05bf52aac3 | ||
|
|
fed7f2fa5b | ||
|
|
686b0865b2 | ||
|
|
dfb4c8eb4c | ||
|
|
ad7a387e19 | ||
|
|
d05be016ce | ||
|
|
ec3bb71e7c | ||
|
|
825d8b83d1 | ||
|
|
83022f6d55 | ||
|
|
4bbc593dc5 | ||
|
|
03440f5eca | ||
|
|
0fa6611260 | ||
|
|
a2d68f84e1 | ||
|
|
49c0709fed | ||
|
|
350a4a0718 | ||
|
|
6349cd42e9 | ||
|
|
c2b2f10676 | ||
|
|
95ebead464 | ||
|
|
3eed8b11cb | ||
|
|
404008dc4e | ||
|
|
c7c6fca537 | ||
|
|
9de3ffdcfe | ||
|
|
1eefb12070 | ||
|
|
2d8b752ef2 | ||
|
|
3e5c31a024 | ||
|
|
631431e006 | ||
|
|
492d920133 | ||
|
|
f754edc375 | ||
|
|
d2571e10d6 | ||
|
|
a687cb9c5b | ||
|
|
cf6634111f | ||
|
|
1ee19d3016 | ||
|
|
9beaa281dc | ||
|
|
5174c7092f | ||
|
|
d894ac75f0 | ||
|
|
af0105ced5 | ||
|
|
93e81314df | ||
|
|
163d1c85da | ||
|
|
cb9b80aaf9 | ||
|
|
cad1906725 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -154,6 +154,7 @@ out
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
dist_optimized
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
@@ -31,7 +31,7 @@ The following options are available:
|
||||
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| **`description`** | Text or React component to display below the Collection label in the List View to give editors more information. |
|
||||
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#components). |
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
@@ -69,7 +69,8 @@ The following options are available:
|
||||
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
|
||||
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
|
||||
| **`afterList`** | An array of components to inject _after_ the built-in List View |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table
|
||||
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
|
||||
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
|
||||
|
||||
@@ -36,7 +36,6 @@ Here is one of the simplest possible Payload configs:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
// import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
|
||||
@@ -8,18 +8,20 @@ keywords: Postgres, documentation, typescript, Content Management System, cms, h
|
||||
|
||||
To use Payload with Postgres, install the package `@payloadcms/db-postgres`. It leverages Drizzle ORM and `node-postgres` to interact with a Postgres database that you provide.
|
||||
|
||||
Alternatively, the `@payloadcms/db-vercel-postgres` package is also available and is optimized for use with Vercel.
|
||||
|
||||
It automatically manages changes to your database for you in development mode, and exposes a full suite of migration controls for you to leverage in order to keep other database environments in sync with your schema. DDL transformations are automatically generated.
|
||||
|
||||
To configure Payload to use Postgres, pass the `postgresAdapter` to your Payload Config as follows:
|
||||
|
||||
### Usage
|
||||
|
||||
`@payloadcms/db-postgres`:
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
// Your config goes here
|
||||
collections: [
|
||||
// Collections go here
|
||||
],
|
||||
// Configure the Postgres adapter here
|
||||
db: postgresAdapter({
|
||||
// Postgres-specific arguments go here.
|
||||
@@ -31,11 +33,28 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
`@payloadcms/db-vercel-postgres`:
|
||||
|
||||
```ts
|
||||
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
// Automatically uses proces.env.POSTGRES_URL if no options are provided.
|
||||
db: vercelPostgresAdapter(),
|
||||
// Optionally, can accept the same options as the @vercel/postgres package.
|
||||
db: vercelPostgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URL
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres` or to `@vercel/postgres` |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
|
||||
@@ -162,7 +162,7 @@ All of Payload's GraphQL functionality is abstracted into a separate package. Pa
|
||||
|
||||
This is the UI library that Payload's Admin Panel uses. All components are exported from this package and can be re-used as you build extensions to the Payload admin UI, or want to use Payload components in your own React apps. Some exports are server components and some are client components.
|
||||
|
||||
`@payloadcms/db-postgres`, `@payloadcms/db-mongodb`
|
||||
`@payloadcms/db-postgres`, `@payloadcms/db-vercel-postgres`, `@payloadcms/db-mongodb`
|
||||
|
||||
You can choose which Database Adapter you'd like to use for your project, and no matter which you choose, the entire data layer for Payload is contained within these packages. You can only use one at a time for any given project.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ IMPORTANT: This will overwrite all slate data. We recommend doing the following
|
||||
1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
|
||||
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data
|
||||
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
|
||||
4. If this works as expected, add the `disableHooks: true` prop everywhere you're initializing `SlateToLexicalFeature`. Example: `SlateToLexicalFeature({ disableHooks: true })`. Once you did that, you're ready to run the migration script.
|
||||
|
||||
```ts
|
||||
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate'
|
||||
|
||||
@@ -92,6 +92,7 @@ _An asterisk denotes that an option is required._
|
||||
| Option | Description |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
||||
|
||||
@@ -23,7 +23,7 @@ export default withBundleAnalyzer(
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
PAYLOAD_DISABLE_DEPENDENCY_CHECKER: 'true',
|
||||
PAYLOAD_CI_DEPENDENCY_CHECKER: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -16,6 +16,7 @@
|
||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||
"build:db-postgres": "turbo build --filter db-postgres",
|
||||
"build:db-sqlite": "turbo build --filter db-sqlite",
|
||||
"build:db-vercel-postgres": "turbo build --filter db-vercel-postgres",
|
||||
"build:drizzle": "turbo build --filter drizzle",
|
||||
"build:email-nodemailer": "turbo build --filter email-nodemailer",
|
||||
"build:email-resend": "turbo build --filter email-resend",
|
||||
@@ -26,7 +27,7 @@
|
||||
"build:live-preview": "turbo build --filter live-preview",
|
||||
"build:live-preview-react": "turbo build --filter live-preview-react",
|
||||
"build:live-preview-vue": "turbo build --filter live-preview-vue",
|
||||
"build:next": "turbo build --filter next",
|
||||
"build:next": "turbo build --filter \"@payloadcms/next\"",
|
||||
"build:payload": "turbo build --filter payload",
|
||||
"build:plugin-cloud": "turbo build --filter plugin-cloud",
|
||||
"build:plugin-cloud-storage": "turbo build --filter plugin-cloud-storage",
|
||||
@@ -53,11 +54,12 @@
|
||||
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
|
||||
"dev": "tsx ./test/dev.ts",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts",
|
||||
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
|
||||
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
|
||||
@@ -71,7 +73,7 @@
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
"runts": "node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"script:gen-templates": "pnpm runts ./scripts/generate-template-variations.ts",
|
||||
"script:list-published": "pnpm runts scripts/lib/getPackageRegistryVersions.ts",
|
||||
"script:pack": "pnpm runts scripts/pack-all-to-dest.ts",
|
||||
@@ -157,13 +159,14 @@
|
||||
"swc-plugin-transform-remove-imports": "1.15.0",
|
||||
"tempy": "1.0.1",
|
||||
"tsx": "4.17.0",
|
||||
"turbo": "^1.13.3",
|
||||
"turbo": "^2.0.14",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
"pnpm": "^9.7.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -29,6 +29,18 @@ const postgresReplacement: DbAdapterReplacement = {
|
||||
packageName: '@payloadcms/db-postgres',
|
||||
}
|
||||
|
||||
const vercelPostgresReplacement: DbAdapterReplacement = {
|
||||
configReplacement: (envName = 'POSTGRES_URL') => [
|
||||
' db: vercelPostgresAdapter({',
|
||||
' pool: {',
|
||||
` connectionString: process.env.${envName} || '',`,
|
||||
' },',
|
||||
' }),',
|
||||
],
|
||||
importReplacement: "import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'",
|
||||
packageName: '@payloadcms/db-vercel-postgres',
|
||||
}
|
||||
|
||||
const sqliteReplacement: DbAdapterReplacement = {
|
||||
configReplacement: (envName = 'DATABASE_URI') => [
|
||||
' db: sqliteAdapter({',
|
||||
@@ -45,6 +57,7 @@ export const dbReplacements: Record<DbType, DbAdapterReplacement> = {
|
||||
mongodb: mongodbReplacement,
|
||||
postgres: postgresReplacement,
|
||||
sqlite: sqliteReplacement,
|
||||
vercelPostgres: vercelPostgresReplacement,
|
||||
}
|
||||
|
||||
type StorageAdapterReplacement = {
|
||||
|
||||
@@ -27,6 +27,11 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
|
||||
title: 'SQLite (beta)',
|
||||
value: 'sqlite',
|
||||
},
|
||||
vercelPostgres: {
|
||||
dbConnectionPrefix: 'postgres://postgres:<password>@127.0.0.1:5432/',
|
||||
title: 'Vercel Postgres (beta)',
|
||||
value: 'vercelPostgres',
|
||||
},
|
||||
}
|
||||
|
||||
export async function selectDb(args: CliArgs, projectName: string): Promise<DbDetails> {
|
||||
|
||||
@@ -42,7 +42,12 @@ export async function writeEnvFile(args: {
|
||||
const key = split[0]
|
||||
let value = split[1]
|
||||
|
||||
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
|
||||
if (
|
||||
key === 'MONGODB_URI' ||
|
||||
key === 'MONGO_URL' ||
|
||||
key === 'DATABASE_URI' ||
|
||||
key === 'POSTGRES_URL'
|
||||
) {
|
||||
value = databaseUri
|
||||
}
|
||||
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
|
||||
|
||||
@@ -57,7 +57,7 @@ interface Template {
|
||||
|
||||
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
|
||||
|
||||
export type DbType = 'mongodb' | 'postgres' | 'sqlite'
|
||||
export type DbType = 'mongodb' | 'postgres' | 'sqlite' | 'vercelPostgres'
|
||||
|
||||
export type DbDetails = {
|
||||
dbUri: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -182,6 +182,7 @@ export function mongooseAdapter({
|
||||
init,
|
||||
migrateFresh,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-mongodb',
|
||||
payload,
|
||||
prodMigrations,
|
||||
queryDrafts,
|
||||
|
||||
@@ -595,14 +595,77 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
config: SanitizedConfig,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...locales,
|
||||
[locale]: field.hasMany
|
||||
? { type: [localeSchema], default: formatDefaultValue(field) }
|
||||
: localeSchema,
|
||||
}
|
||||
}, {}),
|
||||
localized: true,
|
||||
}
|
||||
} else if (hasManyRelations) {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
},
|
||||
}
|
||||
|
||||
if (field.hasMany) {
|
||||
schemaToReturn = {
|
||||
type: [schemaToReturn],
|
||||
default: formatDefaultValue(field),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
if (field.hasMany) {
|
||||
schemaToReturn = {
|
||||
type: [schemaToReturn],
|
||||
default: formatDefaultValue(field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: schemaToReturn,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -59,7 +59,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.23.0",
|
||||
"esbuild": "0.23.1",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -139,6 +139,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-postgres',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -140,6 +140,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-sqlite',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
|
||||
@@ -717,7 +717,7 @@ export const traverseFields = ({
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||
} else if (field.type === 'relationship' && field.hasMany) {
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
|
||||
1
packages/db-vercel-postgres/.gitignore
vendored
Normal file
1
packages/db-vercel-postgres/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/migrations
|
||||
10
packages/db-vercel-postgres/.prettierignore
Normal file
10
packages/db-vercel-postgres/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
15
packages/db-vercel-postgres/.swcrc
Normal file
15
packages/db-vercel-postgres/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
43
packages/db-vercel-postgres/README.md
Normal file
43
packages/db-vercel-postgres/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Payload Postgres Adapter
|
||||
|
||||
[Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres) adapter for [Payload](https://payloadcms.com).
|
||||
|
||||
- [Main Repository](https://github.com/payloadcms/payload)
|
||||
- [Payload Docs](https://payloadcms.com/docs)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/db-vercel-postgres
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Explicit Connection String
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
db: vercelPostgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI,
|
||||
},
|
||||
}),
|
||||
// ...rest of config
|
||||
})
|
||||
```
|
||||
|
||||
### Automatic Connection String Detection
|
||||
|
||||
Have Vercel automatically detect from environment variable (typically `process.env.POSTGRES_URL`)
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
db: postgresAdapter(),
|
||||
// ...rest of config
|
||||
})
|
||||
```
|
||||
|
||||
More detailed usage can be found in the [Payload Docs](https://payloadcms.com/docs/configuration/overview).
|
||||
20
packages/db-vercel-postgres/eslint.config.js
Normal file
20
packages/db-vercel-postgres/eslint.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
|
||||
/** @type {FlatConfig[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigDirName: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
89
packages/db-vercel-postgres/package.json
Normal file
89
packages/db-vercel-postgres/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/payloadcms/payload.git",
|
||||
"directory": "packages/db-vercel-postgres"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
"types": "./src/exports/migration-utils.ts",
|
||||
"default": "./src/exports/migration-utils.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf .dist && rimraf tsconfig.tsbuildinfo && pnpm build:types && pnpm build:swc && pnpm build:esbuild && pnpm renamePredefinedMigrations",
|
||||
"build:esbuild": "echo skipping esbuild",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepack": "pnpm clean && pnpm turbo build",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.23.2-df9e596",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.23.1",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
"types": "./dist/exports/migration-utils.d.ts",
|
||||
"default": "./dist/exports/migration-utils.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
13
packages/db-vercel-postgres/relationships-v2-v3.mjs
Normal file
13
packages/db-vercel-postgres/relationships-v2-v3.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
const imports = `import { migratePostgresV2toV3 } from '@payloadcms/migratePostgresV2toV3'`
|
||||
const up = ` await migratePostgresV2toV3({
|
||||
// enables logging of changes that will be made to the database
|
||||
debug: false,
|
||||
// skips calls that modify schema or data
|
||||
dryRun: false,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
`
|
||||
export { imports, up }
|
||||
|
||||
//# sourceMappingURL=relationships-v2-v3.js.map
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Changes built .js files to .mjs to for ESM imports
|
||||
*/
|
||||
const rename = () => {
|
||||
fs.readdirSync(path.resolve('./dist/predefinedMigrations'))
|
||||
.filter((f) => {
|
||||
return f.endsWith('.js')
|
||||
})
|
||||
.forEach((file) => {
|
||||
const newPath = path.join('./dist/predefinedMigrations', file)
|
||||
fs.renameSync(newPath, newPath.replace('.js', '.mjs'))
|
||||
})
|
||||
console.log('done')
|
||||
}
|
||||
|
||||
rename()
|
||||
61
packages/db-vercel-postgres/src/connect.ts
Normal file
61
packages/db-vercel-postgres/src/connect.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { VercelPool, sql } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
|
||||
import type { VercelPostgresAdapter } from './types.js'
|
||||
|
||||
export const connect: Connect = async function connect(
|
||||
this: VercelPostgresAdapter,
|
||||
options = {
|
||||
hotReload: false,
|
||||
},
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
const logger = this.logger || false
|
||||
// Passed the poolOptions if provided,
|
||||
// else have vercel/postgres detect the connection string from the environment
|
||||
this.drizzle = drizzle(this.poolOptions ? new VercelPool(this.poolOptions) : sql, {
|
||||
logger,
|
||||
schema: this.schema,
|
||||
})
|
||||
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
await this.dropDatabase({ adapter: this })
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
if (typeof this.rejectInitializing === 'function') this.rejectInitializing()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_MIGRATING !== 'true' &&
|
||||
this.push !== false
|
||||
) {
|
||||
await pushDevSchema(this as unknown as DrizzleAdapter)
|
||||
}
|
||||
|
||||
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { migratePostgresV2toV3 } from '../predefinedMigrations/v2-v3/index.js'
|
||||
163
packages/db-vercel-postgres/src/index.ts
Normal file
163
packages/db-vercel-postgres/src/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { DatabaseAdapterObj, Payload } from 'payload'
|
||||
|
||||
import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createVersion,
|
||||
deleteMany,
|
||||
deleteOne,
|
||||
deleteVersions,
|
||||
destroy,
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
findMigrationDir,
|
||||
findOne,
|
||||
findVersions,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
operatorMap,
|
||||
queryDrafts,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
deleteWhere,
|
||||
dropDatabase,
|
||||
execute,
|
||||
getMigrationTemplate,
|
||||
init,
|
||||
insert,
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createDatabaseAdapter } from 'payload'
|
||||
|
||||
import type { Args, VercelPostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<VercelPostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = findMigrationDir(args.migrationDir)
|
||||
let resolveInitializing
|
||||
let rejectInitializing
|
||||
let adapterSchema: VercelPostgresAdapter['pgSchema']
|
||||
|
||||
const initializing = new Promise<void>((res, rej) => {
|
||||
resolveInitializing = res
|
||||
rejectInitializing = rej
|
||||
})
|
||||
|
||||
if (args.schemaName) {
|
||||
adapterSchema = pgSchema(args.schemaName)
|
||||
} else {
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
return createDatabaseAdapter<VercelPostgresAdapter>({
|
||||
name: 'postgres',
|
||||
defaultDrizzleSnapshot,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
features: {
|
||||
json: true,
|
||||
},
|
||||
fieldConstraints: {},
|
||||
getMigrationTemplate,
|
||||
idType: postgresIDType,
|
||||
initializing,
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
operators: operatorMap,
|
||||
pgSchema: adapterSchema,
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
schema: {},
|
||||
schemaName: args.schemaName,
|
||||
sessions: {},
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
// DatabaseAdapter
|
||||
beginTransaction: args.transactionOptions === false ? undefined : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
deleteOne,
|
||||
deleteVersions,
|
||||
deleteWhere,
|
||||
destroy,
|
||||
dropDatabase,
|
||||
execute,
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
findOne,
|
||||
findVersions,
|
||||
init,
|
||||
insert,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-vercel-postgres',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
@@ -0,0 +1,10 @@
|
||||
const imports = `import { migratePostgresV2toV3 } from '@payloadcms/db-postgres/migration-utils'`
|
||||
const upSQL = ` await migratePostgresV2toV3({
|
||||
// enables logging of changes that will be made to the database
|
||||
debug: false,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
`
|
||||
|
||||
export { imports, upSQL }
|
||||
@@ -0,0 +1,237 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { upsertRow } from '@payloadcms/drizzle'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../../types.js'
|
||||
import type { DocsToResave } from '../types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
db: TransactionPg
|
||||
debug: boolean
|
||||
docsToResave: DocsToResave
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
payload: Payload
|
||||
req: PayloadRequest
|
||||
tableName: string
|
||||
}
|
||||
|
||||
export const fetchAndResave = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
docsToResave,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
}: Args) => {
|
||||
for (const [id, rows] of Object.entries(docsToResave)) {
|
||||
if (collectionSlug) {
|
||||
const collectionConfig = payload.collections[collectionSlug].config
|
||||
|
||||
if (collectionConfig) {
|
||||
if (isVersions) {
|
||||
const doc = await payload.findVersionByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" version with ID ${id} will be migrated`,
|
||||
)
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(
|
||||
`"${collectionConfig.slug}" version with ID ${doc.id} FAILED TO MIGRATE`,
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`"${collectionConfig.slug}" version with ID ${doc.id} migrated successfully!`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const doc = await payload.findByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} will be migrated`,
|
||||
)
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} has FAILED TO MIGRATE`,
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} has migrated successfully!`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
const globalConfig = payload.config.globals?.find((global) => global.slug === globalSlug)
|
||||
|
||||
if (globalConfig) {
|
||||
if (isVersions) {
|
||||
const { docs } = await payload.findGlobalVersions({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
limit: 0,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(`${docs.length} global "${globalSlug}" versions will be migrated`)
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(`"${globalSlug}" version with ID ${doc.id} FAILED TO MIGRATE`)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`"${globalSlug}" version with ID ${doc.id} migrated successfully!`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const doc = await payload.findGlobal({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(`The global "${globalSlug}" has FAILED TO MIGRATE`)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(`The global "${globalSlug}" has migrated successfully!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { tabHasName } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
doc: Record<string, unknown>
|
||||
fields: Field[]
|
||||
locale?: string
|
||||
path: string
|
||||
rows: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
const newPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
const newDoc = doc?.[field.name]
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rowData = doc?.[field.name]
|
||||
|
||||
if (field.localized && typeof rowData === 'object' && rowData !== null) {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rowData = doc?.[field.name]
|
||||
|
||||
if (field.localized && typeof rowData === 'object' && rowData !== null) {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: matchedBlock.fields,
|
||||
locale,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: matchedBlock.fields,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const newDoc = doc?.[tab.name]
|
||||
const newPath = `${path ? `${path}.` : ''}${tab.name}`
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: tab.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields: tab.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
const relationshipPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
|
||||
if (field.localized) {
|
||||
const matchedRelationshipsWithLocales = rows.filter(
|
||||
(row) => row.path === relationshipPath,
|
||||
)
|
||||
|
||||
if (matchedRelationshipsWithLocales.length && !doc[field.name]) {
|
||||
doc[field.name] = {}
|
||||
}
|
||||
|
||||
const newDoc = doc[field.name] as Record<string, unknown>
|
||||
|
||||
matchedRelationshipsWithLocales.forEach((localeRow) => {
|
||||
if (typeof localeRow.locale === 'string') {
|
||||
const [, id] = Object.entries(localeRow).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key),
|
||||
)
|
||||
|
||||
newDoc[localeRow.locale] = id
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const matchedRelationship = rows.find((row) => {
|
||||
const matchesPath = row.path === relationshipPath
|
||||
|
||||
if (locale) return matchesPath && locale === row.locale
|
||||
|
||||
return row.path === relationshipPath
|
||||
})
|
||||
|
||||
if (matchedRelationship) {
|
||||
const [, id] = Object.entries(matchedRelationship).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key),
|
||||
)
|
||||
|
||||
doc[field.name] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Groups =
|
||||
| 'addColumn'
|
||||
| 'addConstraint'
|
||||
| 'dropColumn'
|
||||
| 'dropConstraint'
|
||||
| 'dropTable'
|
||||
| 'notNull'
|
||||
|
||||
/**
|
||||
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
|
||||
* example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
|
||||
* to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
* @param sql
|
||||
*/
|
||||
function convertAddColumnToAlterColumn(sql) {
|
||||
// Regular expression to match the ADD COLUMN statement with its constraints
|
||||
const regex = /ALTER TABLE ("[^"]+") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/
|
||||
|
||||
// Replace the matched part with "ALTER COLUMN ... SET NOT NULL;"
|
||||
return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;')
|
||||
}
|
||||
|
||||
export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> => {
|
||||
const groups = {
|
||||
addColumn: 'ADD COLUMN',
|
||||
// example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
|
||||
|
||||
addConstraint: 'ADD CONSTRAINT',
|
||||
//example:
|
||||
// DO $$ BEGIN
|
||||
// ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
// EXCEPTION
|
||||
// WHEN duplicate_object THEN null;
|
||||
// END $$;
|
||||
|
||||
dropColumn: 'DROP COLUMN',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
|
||||
|
||||
dropConstraint: 'DROP CONSTRAINT',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
|
||||
|
||||
dropTable: 'DROP TABLE',
|
||||
// example: DROP TABLE "pages_rels";
|
||||
|
||||
notNull: 'NOT NULL',
|
||||
// example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
}
|
||||
|
||||
const result = Object.keys(groups).reduce((result, group: Groups) => {
|
||||
result[group] = []
|
||||
return result
|
||||
}, {}) as Record<Groups, string[]>
|
||||
|
||||
for (const line of list) {
|
||||
Object.entries(groups).some(([key, value]) => {
|
||||
if (line.endsWith('NOT NULL;')) {
|
||||
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements
|
||||
// example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
|
||||
// becomes two separate statements:
|
||||
// 1. ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer;
|
||||
// 2. ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
result.addColumn.push(line.replace(' NOT NULL;', ';'))
|
||||
result.notNull.push(convertAddColumnToAlterColumn(line))
|
||||
return true
|
||||
}
|
||||
if (line.includes(value)) {
|
||||
result[key].push(line)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import fs from 'fs'
|
||||
import { createRequire } from 'module'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { PathsToQuery } from './types.js'
|
||||
|
||||
import { groupUpSQLStatements } from './groupUpSQLStatements.js'
|
||||
import { migrateRelationships } from './migrateRelationships.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
type Args = {
|
||||
debug?: boolean
|
||||
payload: Payload
|
||||
req?: Partial<PayloadRequest>
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves upload and relationship columns from the join table and into the tables while moving data
|
||||
* This is done in the following order:
|
||||
* ADD COLUMNs
|
||||
* -- manipulate data to move relationships to new columns
|
||||
* ADD CONSTRAINTs
|
||||
* NOT NULLs
|
||||
* DROP TABLEs
|
||||
* DROP CONSTRAINTs
|
||||
* DROP COLUMNs
|
||||
* @param debug
|
||||
* @param payload
|
||||
* @param req
|
||||
*/
|
||||
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
const adapter = payload.db as unknown as VercelPostgresAdapter
|
||||
const db = adapter.sessions[await req.transactionID].db as TransactionPg
|
||||
const dir = payload.db.migrationDir
|
||||
|
||||
// get the drizzle migrateUpSQL from drizzle using the last schema
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api')
|
||||
const drizzleJsonAfter = generateDrizzleJson(adapter.schema)
|
||||
|
||||
// Get the previous migration snapshot
|
||||
const previousSnapshot = fs
|
||||
.readdirSync(dir)
|
||||
.filter((file) => file.endsWith('.json') && !file.endsWith('relationships_v2_v3.json'))
|
||||
.sort()
|
||||
.reverse()?.[0]
|
||||
|
||||
if (!previousSnapshot) {
|
||||
throw new Error(
|
||||
`No previous migration schema file found! A prior migration from v2 is required to migrate to v3.`,
|
||||
)
|
||||
}
|
||||
|
||||
const drizzleJsonBefore = JSON.parse(
|
||||
fs.readFileSync(`${dir}/${previousSnapshot}`, 'utf8'),
|
||||
) as DrizzleSnapshotJSON
|
||||
|
||||
const generatedSQL = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
|
||||
|
||||
if (!generatedSQL.length) {
|
||||
payload.logger.info(`No schema changes needed.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
|
||||
|
||||
const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
|
||||
payload.logger.info(addColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addColumnsStatement))
|
||||
|
||||
for (const collection of payload.config.collections) {
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const pathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: false,
|
||||
fields: collection.fields,
|
||||
isVersions: false,
|
||||
newTableName: tableName,
|
||||
parentTableName: tableName,
|
||||
path: '',
|
||||
pathsToQuery,
|
||||
payload,
|
||||
rootTableName: tableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
db,
|
||||
debug,
|
||||
fields: collection.fields,
|
||||
isVersions: false,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
if (collection.versions) {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: true,
|
||||
fields: versionFields,
|
||||
isVersions: true,
|
||||
newTableName: versionsTableName,
|
||||
parentTableName: versionsTableName,
|
||||
path: '',
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
rootTableName: versionsTableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
db,
|
||||
debug,
|
||||
fields: versionFields,
|
||||
isVersions: true,
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName: versionsTableName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const global of payload.config.globals) {
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(global.slug))
|
||||
|
||||
const pathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: false,
|
||||
fields: global.fields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: false,
|
||||
newTableName: tableName,
|
||||
parentTableName: tableName,
|
||||
path: '',
|
||||
pathsToQuery,
|
||||
payload,
|
||||
rootTableName: tableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
db,
|
||||
debug,
|
||||
fields: global.fields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: false,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
if (global.versions) {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: true,
|
||||
fields: versionFields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: true,
|
||||
newTableName: versionsTableName,
|
||||
parentTableName: versionsTableName,
|
||||
path: '',
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
rootTableName: versionsTableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
db,
|
||||
debug,
|
||||
fields: versionFields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: true,
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName: versionsTableName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ADD CONSTRAINT
|
||||
const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('ADDING CONSTRAINTS')
|
||||
payload.logger.info(addConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addConstraintsStatement))
|
||||
|
||||
// NOT NULL
|
||||
const notNullStatements = sqlUpStatements.notNull.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('NOT NULL CONSTRAINTS')
|
||||
payload.logger.info(notNullStatements)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(notNullStatements))
|
||||
|
||||
// DROP TABLE
|
||||
const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING TABLES')
|
||||
payload.logger.info(dropTablesStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropTablesStatement))
|
||||
|
||||
// DROP CONSTRAINT
|
||||
const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING CONSTRAINTS')
|
||||
payload.logger.info(dropConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropConstraintsStatement))
|
||||
|
||||
// DROP COLUMN
|
||||
const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING COLUMNS')
|
||||
payload.logger.info(dropColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropColumnsStatement))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { DocsToResave, PathsToQuery } from './types.js'
|
||||
|
||||
import { fetchAndResave } from './fetchAndResave/index.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
db: TransactionPg
|
||||
debug: boolean
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
pathsToQuery: PathsToQuery
|
||||
payload: Payload
|
||||
req?: Partial<PayloadRequest>
|
||||
tableName: string
|
||||
}
|
||||
|
||||
export const migrateRelationships = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
}: Args) => {
|
||||
if (pathsToQuery.size === 0) return
|
||||
|
||||
let offset = 0
|
||||
|
||||
let paginationResult
|
||||
|
||||
const where = Array.from(pathsToQuery).reduce((statement, path, i) => {
|
||||
return (statement += `
|
||||
"${tableName}${adapter.relationshipsSuffix}"."path" LIKE '${path}'${pathsToQuery.size !== i + 1 ? ' OR' : ''}
|
||||
`)
|
||||
}, '')
|
||||
|
||||
while (typeof paginationResult === 'undefined' || paginationResult.rows.length > 0) {
|
||||
const paginationStatement = `SELECT DISTINCT parent_id FROM ${tableName}${adapter.relationshipsSuffix} WHERE
|
||||
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
|
||||
`
|
||||
|
||||
paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
|
||||
|
||||
if (paginationResult.rows.length === 0) return
|
||||
|
||||
offset += 1
|
||||
|
||||
const statement = `SELECT * FROM ${tableName}${adapter.relationshipsSuffix} WHERE
|
||||
(${where}) AND parent_id IN (${paginationResult.rows.map((row) => row.parent_id).join(', ')});
|
||||
`
|
||||
if (debug) {
|
||||
payload.logger.info('FINDING ROWS TO MIGRATE')
|
||||
payload.logger.info(statement)
|
||||
}
|
||||
|
||||
const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
|
||||
|
||||
const docsToResave: DocsToResave = {}
|
||||
|
||||
result.rows.forEach((row) => {
|
||||
const parentID = row.parent_id
|
||||
|
||||
if (typeof parentID === 'string' || typeof parentID === 'number') {
|
||||
if (!docsToResave[parentID]) docsToResave[parentID] = []
|
||||
docsToResave[parentID].push(row)
|
||||
}
|
||||
})
|
||||
|
||||
await fetchAndResave({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
docsToResave,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
payload,
|
||||
req: req as unknown as PayloadRequest,
|
||||
tableName,
|
||||
})
|
||||
}
|
||||
|
||||
const deleteStatement = `DELETE FROM ${tableName}${adapter.relationshipsSuffix} WHERE ${where}`
|
||||
if (debug) {
|
||||
payload.logger.info('DELETING ROWS')
|
||||
payload.logger.info(deleteStatement)
|
||||
}
|
||||
await db.execute(sql.raw(`${deleteStatement}`))
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload } from 'payload'
|
||||
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { PathsToQuery } from './types.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
columnPrefix: string
|
||||
db: TransactionPg
|
||||
disableNotNull: boolean
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
newTableName: string
|
||||
parentTableName: string
|
||||
path: string
|
||||
pathsToQuery: PathsToQuery
|
||||
payload: Payload
|
||||
rootTableName: string
|
||||
}
|
||||
|
||||
export const traverseFields = (args: Args) => {
|
||||
args.fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
|
||||
|
||||
if (field.localized && args.payload.config.localization) {
|
||||
newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.newTableName}_${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: '',
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
parentTableName: newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}.%`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
return field.blocks.forEach((block) => {
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
traverseFields({
|
||||
...args,
|
||||
columnPrefix: '',
|
||||
fields: block.fields,
|
||||
newTableName,
|
||||
parentTableName: newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}.%`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
args.columnPrefix = `${args.columnPrefix}_${toSnakeCase(tab.name)}_`
|
||||
args.path = `${args.path ? `${args.path}.` : ''}${tab.name}`
|
||||
args.newTableName = `${args.newTableName}_${toSnakeCase(tab.name)}`
|
||||
|
||||
if (tab.localized && args.payload.config.localization) {
|
||||
args.newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: tab.fields,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Set of all paths which should be moved
|
||||
* This will be built up into one WHERE query
|
||||
*/
|
||||
export type PathsToQuery = Set<string>
|
||||
|
||||
export type DocsToResave = {
|
||||
[id: number | string]: Record<string, unknown>[]
|
||||
}
|
||||
78
packages/db-vercel-postgres/src/types.ts
Normal file
78
packages/db-vercel-postgres/src/types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
BasePostgresAdapter,
|
||||
GenericEnum,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
|
||||
export type Args = {
|
||||
connectionString?: string
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
/**
|
||||
* Optional pool configuration for Vercel Postgres
|
||||
* If not provided, vercel/postgres will attempt to use the Vercel environment variables
|
||||
*/
|
||||
pool?: VercelPostgresPoolConfig
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
transactionOptions?: PgTransactionConfig | false
|
||||
versionsSuffix?: string
|
||||
}
|
||||
|
||||
export type VercelPostgresAdapter = {
|
||||
pool?: VercelPool
|
||||
poolOptions?: Args['pool']
|
||||
} & BasePostgresAdapter
|
||||
|
||||
declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
idType: Args['idType']
|
||||
initializing: Promise<void>
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pool: VercelPool
|
||||
poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relationshipsSuffix?: string
|
||||
resolveInitializing: () => void
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
tableNameMap: Map<string, string>
|
||||
versionsSuffix?: string
|
||||
}
|
||||
}
|
||||
38
packages/db-vercel-postgres/tsconfig.json
Normal file
38
packages/db-vercel-postgres/tsconfig.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
"eslint.config.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": [
|
||||
"src",
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../payload"
|
||||
},
|
||||
{
|
||||
"path": "../translations"
|
||||
},
|
||||
{
|
||||
"path": "../drizzle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
@@ -34,8 +33,9 @@ export const traverseFields = ({
|
||||
// handle simple relationship
|
||||
if (
|
||||
depth > 0 &&
|
||||
(field.type === 'upload' ||
|
||||
(field.type === 'relationship' && !field.hasMany && typeof field.relationTo === 'string'))
|
||||
(field.type === 'upload' || field.type === 'relationship') &&
|
||||
!field.hasMany &&
|
||||
typeof field.relationTo === 'string'
|
||||
) {
|
||||
if (field.localized) {
|
||||
_locales.with[`${path}${field.name}`] = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { CreateMigration } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
@@ -112,6 +111,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
getMigrationTemplate({
|
||||
downSQL: downSQL || ` // Migration code`,
|
||||
imports,
|
||||
packageName: payload.db.packageName,
|
||||
upSQL: upSQL || ` // Migration code`,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -9,8 +9,9 @@ export const indent = (text: string) =>
|
||||
export const getMigrationTemplate = ({
|
||||
downSQL,
|
||||
imports,
|
||||
packageName,
|
||||
upSQL,
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '${packageName}'
|
||||
${imports ? `${imports}\n` : ''}
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
${indent(upSQL)}
|
||||
|
||||
@@ -726,7 +726,7 @@ export const traverseFields = ({
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||
} else if (field.type === 'relationship' && field.hasMany) {
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
|
||||
@@ -111,8 +111,6 @@ export type BasePostgresAdapter = {
|
||||
logger: DrizzleConfig['logger']
|
||||
operators: Operators
|
||||
pgSchema?: Schema
|
||||
// pool: Pool
|
||||
// poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
|
||||
@@ -445,7 +445,7 @@ export const getTableColumnFromPath = ({
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
if (Array.isArray(field.relationTo) || (field.type === 'relationship' && field.hasMany)) {
|
||||
if (Array.isArray(field.relationTo) || field.hasMany) {
|
||||
let relationshipFields
|
||||
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
||||
const {
|
||||
|
||||
@@ -351,6 +351,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
throw error.code === '23505'
|
||||
? new ValidationError(
|
||||
{
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
field: adapter.fieldConstraints[tableName][error.constraint],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -9,7 +9,6 @@ export const rule = {
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node --no-deprecation
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -307,10 +307,51 @@ export function buildMutationInputType({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
||||
}),
|
||||
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
||||
}),
|
||||
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
type PayloadGraphQLRelationshipType =
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLList<GraphQLScalarType>
|
||||
| GraphQLScalarType
|
||||
let type: PayloadGraphQLRelationshipType
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
const fullName = `${combineParentName(
|
||||
parentName,
|
||||
toWords(field.name, true),
|
||||
)}RelationshipInput`
|
||||
type = new GraphQLInputObjectType({
|
||||
name: fullName,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: new GraphQLEnumType({
|
||||
name: `${fullName}RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(values, option) => ({
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
},
|
||||
value: { type: GraphQLJSON },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
type = getCollectionIDType(
|
||||
config.db.defaultIDType,
|
||||
graphqlResult.collections[relationTo].config,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const fieldName = formatName(name)
|
||||
|
||||
@@ -594,49 +594,164 @@ export function buildObjectType({
|
||||
}),
|
||||
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const uploadName = combineParentName(parentName, toWords(field.name, true))
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data, { req }) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
const type = withNullableType(
|
||||
field,
|
||||
graphqlResult.collections[relationTo].graphQL.type || newlyCreatedBlockType,
|
||||
forceNullable,
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
|
||||
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
|
||||
)
|
||||
|
||||
const uploadArgs = {} as LocaleInputType
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
uploadArgs.locale = {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
uploadArgs.fallbackLocale = {
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relatedCollectionSlug = field.relationTo
|
||||
|
||||
const upload = {
|
||||
type,
|
||||
args: uploadArgs,
|
||||
extensions: { complexity: 20 },
|
||||
const relationship = {
|
||||
type: withNullableType(
|
||||
field,
|
||||
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
forceNullable,
|
||||
),
|
||||
args: relationshipArgs,
|
||||
extensions: { complexity: 10 },
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
const id = value
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: collectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug,
|
||||
collectionSlug: relatedCollectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
@@ -649,26 +764,30 @@ export function buildObjectType({
|
||||
}),
|
||||
)
|
||||
|
||||
return relatedDocument || null
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
const whereFields = graphqlResult.collections[relationTo].config.fields
|
||||
|
||||
upload.args.where = {
|
||||
type: buildWhereInputType({
|
||||
name: uploadName,
|
||||
fields: whereFields,
|
||||
parentName: uploadName,
|
||||
}),
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: upload,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,9 +130,36 @@ const fieldToSchemaMap = ({ nestedFieldName, parentName }: Args): any => ({
|
||||
textarea: (field: TextareaField) => ({
|
||||
type: withOperators(field, parentName),
|
||||
}),
|
||||
upload: (field: UploadField) => ({
|
||||
type: withOperators(field, parentName),
|
||||
}),
|
||||
upload: (field: UploadField) => {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
return {
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${combineParentName(parentName, field.name)}_Relation`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: new GraphQLEnumType({
|
||||
name: `${combineParentName(parentName, field.name)}_Relation_RelationTo`,
|
||||
values: field.relationTo.reduce(
|
||||
(values, relation) => ({
|
||||
...values,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
},
|
||||
value: { type: GraphQLJSON },
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: withOperators(field, parentName),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default fieldToSchemaMap
|
||||
|
||||
@@ -230,9 +230,9 @@ const defaults: DefaultsType = {
|
||||
},
|
||||
upload: {
|
||||
operators: [
|
||||
...operators.equality.map((operator) => ({
|
||||
...[...operators.equality, ...operators.contains].map((operator) => ({
|
||||
name: operator,
|
||||
type: GraphQLString,
|
||||
type: GraphQLJSON,
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
36
packages/next/babel.config.cjs
Normal file
36
packages/next/babel.config.cjs
Normal file
@@ -0,0 +1,36 @@
|
||||
const fs = require('fs')
|
||||
|
||||
// Plugin options can be found here: https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L38
|
||||
const ReactCompilerConfig = {
|
||||
sources: (filename) => {
|
||||
const isInNodeModules = filename.includes('node_modules')
|
||||
if (isInNodeModules || ( !filename.endsWith('.tsx') && !filename.endsWith('.jsx') && !filename.endsWith('.js'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only compile files with 'use client' directives. We do not want to
|
||||
// accidentally compile React Server Components
|
||||
const file = fs.readFileSync(filename, 'utf8')
|
||||
if (file.includes("'use client'")) {
|
||||
return true
|
||||
}
|
||||
console.log('React compiler - skipping file: ' + filename)
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = function (api) {
|
||||
api.cache(false)
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
['babel-plugin-react-compiler', ReactCompilerConfig], // must run first!
|
||||
/* [
|
||||
'babel-plugin-transform-remove-imports',
|
||||
{
|
||||
test: '\\.(scss|css)$',
|
||||
},
|
||||
],*/
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
const { rules } = reactCompiler
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
@@ -20,6 +22,16 @@ export const index = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react-compiler/react-compiler': 'error',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -50,13 +50,16 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:cjs && pnpm build:esbuild",
|
||||
"build": "pnpm build:reactcompiler",
|
||||
"build:babel": "rm -rf dist_optimized && babel dist --out-dir dist_optimized --source-maps --extensions .ts,.js,.tsx,.jsx,.cjs,.mjs && rm -rf dist && mv dist_optimized dist",
|
||||
"build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs --strip-leading-paths",
|
||||
"build:esbuild": "node bundleScss.js",
|
||||
"build:reactcompiler": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm build:swc && pnpm build:cjs && pnpm build:babel && pnpm copyfiles && pnpm build:types && pnpm build:esbuild",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"build:without_reactcompiler": "rm -rf dist && rm -rf tsconfig.tsbuildinfo && pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:cjs && pnpm build:esbuild",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" \"src/app/api/**\" dist/",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
@@ -80,15 +83,22 @@
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^14.1.0",
|
||||
"@babel/cli": "^7.24.5",
|
||||
"@babel/core": "^7.24.5",
|
||||
"@babel/preset-env": "^7.24.5",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@next/eslint-plugin-next": "15.0.0-canary.104",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/busboy": "^1.5.3",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.0",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/ws": "^8.5.10",
|
||||
"esbuild": "0.23.0",
|
||||
"babel-plugin-react-compiler": "0.0.0-experimental-48eb8f4-20240822",
|
||||
"esbuild": "0.23.1",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "0.0.0-experimental-72f06b2-20240822",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "1.15.0"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
// Credit: @Taiki92777
|
||||
// - Source: https://github.com/vercel/next.js/discussions/32231#discussioncomment-7284386
|
||||
// Credit: `react-use` maintainers
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
--theme-baseline-body-size: #{$baseline-body-size};
|
||||
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
--font-serif: Georgia, 'Bitstream Charter', 'Charis SIL', Utopia, 'URW Bookman L', serif;
|
||||
--font-mono: monospace;
|
||||
--font-serif: 'Georgia', 'Bitstream Charter', 'Charis SIL', Utopia, 'URW Bookman L', serif;
|
||||
--font-mono: 'SF Mono', Menlo, Consolas, Monaco, monospace;
|
||||
|
||||
--style-radius-s: #{$style-radius-s};
|
||||
--style-radius-m: #{$style-radius-m};
|
||||
@@ -146,13 +146,13 @@ h6 {
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $baseline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: $baseline;
|
||||
margin: 0 0 $baseline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
%h1 {
|
||||
margin: 0 0 base(1);
|
||||
margin: 0;
|
||||
font-size: base(1.6);
|
||||
line-height: base(1.8);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
%h2 {
|
||||
margin: 0 0 base(1);
|
||||
margin: 0;
|
||||
font-size: base(1.3);
|
||||
line-height: base(1.6);
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
%h3 {
|
||||
margin: 0 0 base(1);
|
||||
margin: 0;
|
||||
font-size: base(1);
|
||||
line-height: base(1.2);
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
%h4 {
|
||||
margin: 0 0 $baseline;
|
||||
margin: 0;
|
||||
font-size: base(0.8);
|
||||
line-height: base(1);
|
||||
letter-spacing: -0.375px;
|
||||
|
||||
@@ -62,7 +62,9 @@ export const reload = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const getPayloadHMR = async (options: InitOptions): Promise<Payload> => {
|
||||
export const getPayloadHMR = async (
|
||||
options: Pick<InitOptions, 'config' | 'importMap'>,
|
||||
): Promise<Payload> => {
|
||||
if (!options?.config) {
|
||||
throw new Error('Error: the payload config is required for getPayloadHMR to work.')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { SelectField, useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
req,
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
||||
id,
|
||||
collectionConfig,
|
||||
|
||||
@@ -11,21 +11,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: base(0.8);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pill {
|
||||
.btn--withoutPopup,
|
||||
.btn--withPopup {
|
||||
position: relative;
|
||||
margin: 0 0 base(0.2);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
BulkUploadDrawer,
|
||||
Button,
|
||||
DeleteMany,
|
||||
EditMany,
|
||||
Gutter,
|
||||
ListControls,
|
||||
ListHeader,
|
||||
ListSelection,
|
||||
Pagination,
|
||||
PerPage,
|
||||
Pill,
|
||||
PopupList,
|
||||
PublishMany,
|
||||
RelationshipProvider,
|
||||
RenderComponent,
|
||||
@@ -22,10 +24,13 @@ import {
|
||||
Table,
|
||||
UnpublishMany,
|
||||
ViewDescription,
|
||||
bulkUploadDrawerSlug,
|
||||
useConfig,
|
||||
useEditDepth,
|
||||
useListInfo,
|
||||
useListQuery,
|
||||
useModal,
|
||||
useRouteCache,
|
||||
useSearchParams,
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
@@ -41,9 +46,20 @@ const baseClass = 'collection-list'
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const DefaultListView: React.FC = () => {
|
||||
const { Header, collectionSlug, hasCreatePermission, newDocumentURL } = useListInfo()
|
||||
const {
|
||||
Header,
|
||||
beforeActions,
|
||||
collectionSlug,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
hasCreatePermission,
|
||||
newDocumentURL,
|
||||
} = useListInfo()
|
||||
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
|
||||
const { searchParams } = useSearchParams()
|
||||
const { openModal } = useModal()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
@@ -67,7 +83,7 @@ export const DefaultListView: React.FC = () => {
|
||||
labels,
|
||||
} = collectionConfig
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const drawerDepth = useEditDepth()
|
||||
|
||||
@@ -79,7 +95,9 @@ export const DefaultListView: React.FC = () => {
|
||||
|
||||
let docs = data.docs || []
|
||||
|
||||
if (collectionConfig.upload) {
|
||||
const isUploadCollection = Boolean(collectionConfig.upload)
|
||||
|
||||
if (isUploadCollection) {
|
||||
docs = docs?.map((doc) => {
|
||||
return {
|
||||
...doc,
|
||||
@@ -104,35 +122,47 @@ export const DefaultListView: React.FC = () => {
|
||||
<RenderComponent mappedComponent={beforeList} />
|
||||
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs}>
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
{Header || (
|
||||
<Fragment>
|
||||
<h1>{getTranslation(labels?.plural, i18n)}</h1>
|
||||
{hasCreatePermission && (
|
||||
<Pill
|
||||
aria-label={i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
to={newDocumentURL}
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</Pill>
|
||||
)}
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
)}
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<RenderComponent
|
||||
Component={ViewDescription}
|
||||
clientProps={{ description }}
|
||||
mappedComponent={Description}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</header>
|
||||
{Header || (
|
||||
<ListHeader heading={getTranslation(labels?.plural, i18n)}>
|
||||
{hasCreatePermission && (
|
||||
<Button
|
||||
Link={Link}
|
||||
SubMenuPopupContent={
|
||||
isUploadCollection && collectionConfig.upload.bulkUpload ? (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button onClick={() => openModal(bulkUploadDrawerSlug)}>
|
||||
{t('upload:bulkUpload')}
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
) : null
|
||||
}
|
||||
aria-label={i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
buttonStyle="pill"
|
||||
el="link"
|
||||
size="small"
|
||||
to={newDocumentURL}
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</Button>
|
||||
)}
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
)}
|
||||
{(description || Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription Description={Description} description={description} />
|
||||
</div>
|
||||
)}
|
||||
{isUploadCollection && collectionConfig.upload.bulkUpload ? (
|
||||
<BulkUploadDrawer
|
||||
collectionSlug={collectionSlug}
|
||||
onSuccess={() => clearRouteCache()}
|
||||
/>
|
||||
) : null}
|
||||
</ListHeader>
|
||||
)}
|
||||
<ListControls collectionConfig={collectionConfig} fields={fields} />
|
||||
<RenderComponent mappedComponent={beforeListTable} />
|
||||
{!data.docs && (
|
||||
@@ -174,7 +204,7 @@ export const DefaultListView: React.FC = () => {
|
||||
limit={data.limit}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={handlePageChange}
|
||||
onChange={(page) => void handlePageChange(page)}
|
||||
page={data.page}
|
||||
prevPage={data.prevPage}
|
||||
totalPages={data.totalPages}
|
||||
@@ -189,7 +219,7 @@ export const DefaultListView: React.FC = () => {
|
||||
{i18n.t('general:of')} {data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
handleChange={handlePerPageChange}
|
||||
handleChange={(limit) => void handlePerPageChange(limit)}
|
||||
limit={
|
||||
isNumber(searchParams?.limit) ? Number(searchParams.limit) : defaultLimit
|
||||
}
|
||||
@@ -200,10 +230,15 @@ export const DefaultListView: React.FC = () => {
|
||||
<div className={`${baseClass}__list-selection`}>
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
<div className={`${baseClass}__list-selection-actions`}>
|
||||
<EditMany collection={collectionConfig} fields={fields} />
|
||||
<PublishMany collection={collectionConfig} />
|
||||
<UnpublishMany collection={collectionConfig} />
|
||||
<DeleteMany collection={collectionConfig} />
|
||||
{beforeActions && beforeActions}
|
||||
{!disableBulkEdit && (
|
||||
<Fragment>
|
||||
<EditMany collection={collectionConfig} fields={fields} />
|
||||
<PublishMany collection={collectionConfig} />
|
||||
<UnpublishMany collection={collectionConfig} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
import type { LivePreviewConfig } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
import type { Dispatch } from 'react'
|
||||
import type React from 'react'
|
||||
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ export const DeviceContainer: React.FC<{
|
||||
x = '-50%'
|
||||
|
||||
if (
|
||||
desiredSize &&
|
||||
measuredDeviceSize &&
|
||||
typeof zoom === 'number' &&
|
||||
typeof desiredSize.width === 'number' &&
|
||||
typeof desiredSize.height === 'number' &&
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { StepNavItem } from '@payloadcms/ui'
|
||||
import type { ClientCollectionConfig, ClientField, ClientGlobalConfig } from 'payload'
|
||||
import type React from 'react'
|
||||
@@ -30,7 +31,7 @@ export const SetStepNav: React.FC<{
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (collectionSlug) {
|
||||
if (collectionSlug && collectionConfig) {
|
||||
let docLabel = ''
|
||||
|
||||
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
|
||||
@@ -82,7 +83,7 @@ export const SetStepNav: React.FC<{
|
||||
]
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
if (globalSlug && globalConfig) {
|
||||
nav = [
|
||||
{
|
||||
label: globalConfig.label,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
'use client'
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientField,
|
||||
OptionObject,
|
||||
SelectField,
|
||||
SelectFieldClient,
|
||||
SelectFieldProps,
|
||||
} from 'payload'
|
||||
import type { OptionObject, SelectField, SelectFieldClient } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { TabsFieldClient } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import type { TextFieldClient } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S node --no-deprecation
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.88",
|
||||
"version": "3.0.0-beta.92",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -124,7 +124,7 @@
|
||||
"@types/uuid": "10.0.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"esbuild": "0.23.0",
|
||||
"esbuild": "0.23.1",
|
||||
"graphql-http": "^1.22.0",
|
||||
"rimraf": "3.0.2",
|
||||
"sharp": "0.32.6"
|
||||
|
||||
@@ -85,6 +85,7 @@ export async function generateImportMap(
|
||||
if (shouldLog) {
|
||||
console.log('Generating import map')
|
||||
}
|
||||
|
||||
const importMap: InternalImportMap = {}
|
||||
const imports: Imports = {}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ export function iterateCollections({
|
||||
addToImportMap(collection.admin?.components?.afterListTable)
|
||||
addToImportMap(collection.admin?.components?.beforeList)
|
||||
addToImportMap(collection.admin?.components?.beforeListTable)
|
||||
addToImportMap(collection.admin?.components?.Description)
|
||||
|
||||
addToImportMap(collection.admin?.components?.edit?.Description)
|
||||
addToImportMap(collection.admin?.components?.edit?.PreviewButton)
|
||||
addToImportMap(collection.admin?.components?.edit?.PublishButton)
|
||||
addToImportMap(collection.admin?.components?.edit?.SaveButton)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { BinScript } from '../config/types.js'
|
||||
import { findConfig } from '../config/find.js'
|
||||
import { generateImportMap } from './generateImportMap/index.js'
|
||||
import { generateTypes } from './generateTypes.js'
|
||||
import { info } from './info.js'
|
||||
import { loadEnv } from './loadEnv.js'
|
||||
import { migrate } from './migrate.js'
|
||||
|
||||
@@ -16,6 +17,11 @@ export const bin = async () => {
|
||||
const args = minimist(process.argv.slice(2))
|
||||
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
|
||||
|
||||
if (script === 'info') {
|
||||
await info()
|
||||
return
|
||||
}
|
||||
|
||||
if (script === 'run') {
|
||||
const scriptPath = args._[1]
|
||||
if (!scriptPath) {
|
||||
|
||||
63
packages/payload/src/bin/info.ts
Normal file
63
packages/payload/src/bin/info.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { execFileSync } from 'child_process'
|
||||
import os from 'os'
|
||||
|
||||
import { getDependencies } from '../index.js'
|
||||
import { PAYLOAD_PACKAGE_LIST } from '../versions/payloadPackageList.js'
|
||||
|
||||
export const info = async () => {
|
||||
const deps = await getDependencies(process.cwd(), [
|
||||
...PAYLOAD_PACKAGE_LIST,
|
||||
'next',
|
||||
'react',
|
||||
'react-dom',
|
||||
])
|
||||
|
||||
const formattedDeps = Array.from(deps.resolved.entries()).map(([name, { version }]) => ({
|
||||
name,
|
||||
version,
|
||||
}))
|
||||
|
||||
console.log(generateOutput(formattedDeps))
|
||||
}
|
||||
|
||||
function generateOutput(packages: Array<{ name: string; version: string }>) {
|
||||
const cpuCores = os.cpus().length
|
||||
|
||||
const primaryDeps = packages.filter(({ name }) => name === 'payload' || name === 'next')
|
||||
const otherDeps = packages
|
||||
.filter(({ name }) => name !== 'payload' && name !== 'next')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const formattedDeps = [...primaryDeps, ...otherDeps]
|
||||
.map(({ name, version }) => ` ${name}: ${version}`)
|
||||
.join('\n')
|
||||
|
||||
return `
|
||||
Binaries:
|
||||
Node: ${process.versions.node}
|
||||
npm: ${getBinaryVersion('npm')}
|
||||
Yarn: ${getBinaryVersion('yarn')}
|
||||
pnpm: ${getBinaryVersion('pnpm')}
|
||||
Relevant Packages:
|
||||
${formattedDeps}
|
||||
Operating System:
|
||||
Platform: ${os.platform()}
|
||||
Arch: ${os.arch()}
|
||||
Version: ${os.version()}
|
||||
Available memory (MB): ${Math.ceil(os.totalmem() / 1024 / 1024)}
|
||||
Available CPU cores: ${cpuCores > 0 ? cpuCores : 'N/A'}
|
||||
`
|
||||
}
|
||||
|
||||
function getBinaryVersion(binaryName: string) {
|
||||
try {
|
||||
return execFileSync(binaryName, ['--version']).toString().trim()
|
||||
} catch {
|
||||
return 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
// Direct execution
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
void info()
|
||||
}
|
||||
59
packages/payload/src/checkPayloadDependencies.ts
Normal file
59
packages/payload/src/checkPayloadDependencies.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { CustomVersionParser } from './utilities/dependencies/dependencyChecker.js'
|
||||
|
||||
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
|
||||
import { PAYLOAD_PACKAGE_LIST } from './versions/payloadPackageList.js'
|
||||
|
||||
const customReactVersionParser: CustomVersionParser = (version) => {
|
||||
const [mainVersion, ...preReleases] = version.split('-')
|
||||
|
||||
if (preReleases?.length === 3) {
|
||||
// Needs different handling, as it's in a format like 19.0.0-rc-06d0b89e-20240801 format
|
||||
const date = preReleases[2]
|
||||
|
||||
const parts = mainVersion.split('.').map(Number)
|
||||
return { parts, preReleases: [date] }
|
||||
}
|
||||
|
||||
const parts = mainVersion.split('.').map(Number)
|
||||
return { parts, preReleases }
|
||||
}
|
||||
|
||||
export async function checkPayloadDependencies() {
|
||||
const dependencies = [...PAYLOAD_PACKAGE_LIST]
|
||||
|
||||
if (process.env.PAYLOAD_CI_DEPENDENCY_CHECKER !== 'true') {
|
||||
dependencies.push('@payloadcms/plugin-sentry')
|
||||
}
|
||||
|
||||
// First load. First check if there are mismatching dependency versions of payload packages
|
||||
await checkDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'payload',
|
||||
dependencies,
|
||||
targetVersionDependency: 'payload',
|
||||
},
|
||||
{
|
||||
name: 'react',
|
||||
dependencies: ['react', 'react-dom'],
|
||||
targetVersionDependency: 'react',
|
||||
},
|
||||
],
|
||||
dependencyVersions: {
|
||||
next: {
|
||||
required: false,
|
||||
version: '>=15.0.0-canary.104',
|
||||
},
|
||||
react: {
|
||||
customVersionParser: customReactVersionParser,
|
||||
required: false,
|
||||
version: '>=19.0.0-rc-06d0b89e-20240801',
|
||||
},
|
||||
'react-dom': {
|
||||
customVersionParser: customReactVersionParser,
|
||||
required: false,
|
||||
version: '>=19.0.0-rc-06d0b89e-20240801',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,15 @@ export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||
'hidden' | 'preview'
|
||||
>
|
||||
|
||||
export type ServerOnlyUploadProperties = keyof Pick<
|
||||
SanitizedCollectionConfig['upload'],
|
||||
| 'adminThumbnail'
|
||||
| 'externalFileHeaderFilter'
|
||||
| 'handlers'
|
||||
| 'modifyResponseHeaders'
|
||||
| 'withMetadata'
|
||||
>
|
||||
|
||||
export type ClientCollectionConfig = {
|
||||
_isPreviewEnabled?: true
|
||||
admin: {
|
||||
@@ -24,7 +33,6 @@ export type ClientCollectionConfig = {
|
||||
beforeList: MappedComponent[]
|
||||
beforeListTable: MappedComponent[]
|
||||
edit: {
|
||||
Description: MappedComponent
|
||||
PreviewButton: MappedComponent
|
||||
PublishButton: MappedComponent
|
||||
SaveButton: MappedComponent
|
||||
|
||||
@@ -120,6 +120,7 @@ export const sanitizeCollection = async (
|
||||
// disable duplicate for uploads by default
|
||||
sanitized.disableDuplicate = sanitized.disableDuplicate || true
|
||||
|
||||
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
|
||||
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
|
||||
sanitized.admin.useAsTitle =
|
||||
sanitized.admin.useAsTitle && sanitized.admin.useAsTitle !== 'id'
|
||||
|
||||
@@ -250,6 +250,7 @@ export type CollectionAdminOptions = {
|
||||
* Custom admin components
|
||||
*/
|
||||
components?: {
|
||||
Description?: EntityDescriptionComponent
|
||||
afterList?: CustomComponent[]
|
||||
afterListTable?: CustomComponent[]
|
||||
beforeList?: CustomComponent[]
|
||||
@@ -258,8 +259,6 @@ export type CollectionAdminOptions = {
|
||||
* Components within the edit view
|
||||
*/
|
||||
edit?: {
|
||||
Description?: EntityDescriptionComponent
|
||||
|
||||
/**
|
||||
* Replaces the "Preview" button
|
||||
*/
|
||||
|
||||
@@ -105,7 +105,10 @@ export async function validateSearchParam({
|
||||
fieldPath = path.slice(0, -(req.locale.length + 1))
|
||||
}
|
||||
// remove ".value" from ends of polymorphic relationship paths
|
||||
if (field.type === 'relationship' && Array.isArray(field.relationTo)) {
|
||||
if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
Array.isArray(field.relationTo)
|
||||
) {
|
||||
fieldPath = fieldPath.replace('.value', '')
|
||||
}
|
||||
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
|
||||
|
||||
@@ -100,6 +100,13 @@ export interface BaseDatabaseAdapter {
|
||||
* The name of the database adapter
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* Full package name of the database adapter
|
||||
*
|
||||
* @example @payloadcms/db-postgres
|
||||
*/
|
||||
packageName: string
|
||||
|
||||
/**
|
||||
* reference to the instance of payload
|
||||
*/
|
||||
@@ -434,5 +441,6 @@ export type DBIdentifierName =
|
||||
export type MigrationTemplateArgs = {
|
||||
downSQL?: string
|
||||
imports?: string
|
||||
packageName?: string
|
||||
upSQL?: string
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ export class ValidationError extends APIError<{
|
||||
global?: string
|
||||
}> {
|
||||
constructor(
|
||||
results: { collection?: string; errors: ValidationFieldError[]; global?: string },
|
||||
results: {
|
||||
collection?: string
|
||||
errors: ValidationFieldError[]
|
||||
global?: string
|
||||
id?: number | string
|
||||
},
|
||||
t?: TFunction,
|
||||
) {
|
||||
const message = t
|
||||
|
||||
@@ -99,19 +99,26 @@ export const sanitizeFields = async ({
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'relationship') {
|
||||
if (field.min && !field.minRows) {
|
||||
console.warn(
|
||||
`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`,
|
||||
)
|
||||
if (field.min && !field.minRows) {
|
||||
console.warn(
|
||||
`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`,
|
||||
)
|
||||
}
|
||||
if (field.max && !field.maxRows) {
|
||||
console.warn(
|
||||
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
|
||||
)
|
||||
}
|
||||
field.minRows = field.minRows || field.min
|
||||
field.maxRows = field.maxRows || field.max
|
||||
}
|
||||
|
||||
if (field.type === 'upload') {
|
||||
if (!field.admin || !('isSortable' in field.admin)) {
|
||||
field.admin = {
|
||||
isSortable: true,
|
||||
...field.admin,
|
||||
}
|
||||
if (field.max && !field.maxRows) {
|
||||
console.warn(
|
||||
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
|
||||
)
|
||||
}
|
||||
field.minRows = field.minRows || field.min
|
||||
field.maxRows = field.maxRows || field.max
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -824,35 +824,106 @@ export type UIFieldClient = {
|
||||
} & Omit<DeepUndefinable<FieldBaseClient>, '_isPresentational' | 'admin'> & // still include FieldBaseClient (even if it's undefinable) so that we don't need constant type checks (e.g. if('xy' in field))
|
||||
Pick<UIField, 'label' | 'name' | 'type'>
|
||||
|
||||
export type UploadField = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: CustomComponent<UploadFieldErrorClientComponent | UploadFieldErrorServerComponent>
|
||||
Label?: CustomComponent<UploadFieldLabelClientComponent | UploadFieldLabelServerComponent>
|
||||
} & Admin['components']
|
||||
}
|
||||
type SharedUploadProperties = {
|
||||
/**
|
||||
* Toggle the preview in the admin interface.
|
||||
*/
|
||||
displayPreview?: boolean
|
||||
filterOptions?: FilterOptions
|
||||
hasMany?: boolean
|
||||
/**
|
||||
* Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached.
|
||||
*
|
||||
* {@link https://payloadcms.com/docs/getting-started/concepts#field-level-max-depth}
|
||||
*/
|
||||
maxDepth?: number
|
||||
relationTo: CollectionSlug
|
||||
type: 'upload'
|
||||
validate?: Validate<unknown, unknown, unknown, UploadField>
|
||||
} & FieldBase
|
||||
validate?: Validate<unknown, unknown, unknown, SharedUploadProperties>
|
||||
} & (
|
||||
| {
|
||||
hasMany: true
|
||||
/**
|
||||
* @deprecated Use 'maxRows' instead
|
||||
*/
|
||||
max?: number
|
||||
maxRows?: number
|
||||
/**
|
||||
* @deprecated Use 'minRows' instead
|
||||
*/
|
||||
min?: number
|
||||
minRows?: number
|
||||
}
|
||||
| {
|
||||
hasMany?: false | undefined
|
||||
/**
|
||||
* @deprecated Use 'maxRows' instead
|
||||
*/
|
||||
max?: undefined
|
||||
maxRows?: undefined
|
||||
/**
|
||||
* @deprecated Use 'minRows' instead
|
||||
*/
|
||||
min?: undefined
|
||||
minRows?: undefined
|
||||
}
|
||||
) &
|
||||
FieldBase
|
||||
|
||||
export type UploadFieldClient = {
|
||||
type SharedUploadPropertiesClient = FieldBaseClient &
|
||||
Pick<
|
||||
SharedUploadProperties,
|
||||
'hasMany' | 'max' | 'maxDepth' | 'maxRows' | 'min' | 'minRows' | 'type'
|
||||
>
|
||||
|
||||
type UploadAdmin = {
|
||||
allowCreate?: boolean
|
||||
components?: {
|
||||
Error?: CustomComponent<
|
||||
RelationshipFieldErrorClientComponent | RelationshipFieldErrorServerComponent
|
||||
>
|
||||
Label?: CustomComponent<
|
||||
RelationshipFieldLabelClientComponent | RelationshipFieldLabelServerComponent
|
||||
>
|
||||
} & Admin['components']
|
||||
isSortable?: boolean
|
||||
} & Admin
|
||||
type UploadAdminClient = {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
|
||||
|
||||
export type PolymorphicUploadField = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
}
|
||||
} & FieldBaseClient &
|
||||
Pick<UploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'>
|
||||
sortOptions?: { [collectionSlug: CollectionSlug]: string }
|
||||
} & UploadAdmin
|
||||
relationTo: CollectionSlug[]
|
||||
} & SharedUploadProperties
|
||||
|
||||
export type PolymorphicUploadFieldClient = {
|
||||
admin?: {
|
||||
sortOptions?: Pick<PolymorphicUploadField['admin'], 'sortOptions'>
|
||||
} & UploadAdminClient
|
||||
} & Pick<PolymorphicUploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'> &
|
||||
SharedUploadPropertiesClient
|
||||
|
||||
export type SingleUploadField = {
|
||||
admin?: {
|
||||
sortOptions?: string
|
||||
} & UploadAdmin
|
||||
relationTo: CollectionSlug
|
||||
} & SharedUploadProperties
|
||||
|
||||
export type SingleUploadFieldClient = {
|
||||
admin?: Pick<SingleUploadField['admin'], 'sortOptions'> & UploadAdminClient
|
||||
} & Pick<SingleUploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'> &
|
||||
SharedUploadPropertiesClient
|
||||
|
||||
export type UploadField = /* PolymorphicUploadField | */ SingleUploadField
|
||||
|
||||
export type UploadFieldClient = /* PolymorphicUploadFieldClient | */ SingleUploadFieldClient
|
||||
|
||||
export type CodeField = {
|
||||
admin?: {
|
||||
|
||||
@@ -71,6 +71,7 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationError(
|
||||
{
|
||||
id,
|
||||
collection: collection?.slug,
|
||||
errors,
|
||||
global: global?.slug,
|
||||
|
||||
@@ -138,7 +138,7 @@ export const promise = async <T>({
|
||||
siblingData[field.name] === 'null' ||
|
||||
siblingData[field.name] === null
|
||||
) {
|
||||
if (field.type === 'relationship' && field.hasMany === true) {
|
||||
if (field.hasMany === true) {
|
||||
siblingData[field.name] = []
|
||||
} else {
|
||||
siblingData[field.name] = null
|
||||
@@ -153,32 +153,32 @@ export const promise = async <T>({
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === relatedDoc.relationTo,
|
||||
)
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = {
|
||||
...relatedDoc,
|
||||
value: parseFloat(relatedDoc.value as string),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && valueIsValueWithRelation(value)) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === value.relationTo,
|
||||
)
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = {
|
||||
...relatedDoc,
|
||||
value: parseFloat(relatedDoc.value as string),
|
||||
}
|
||||
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
|
||||
}
|
||||
})
|
||||
}
|
||||
if (
|
||||
field.type === 'relationship' &&
|
||||
field.hasMany !== true &&
|
||||
valueIsValueWithRelation(value)
|
||||
) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === value.relationTo,
|
||||
)
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -187,25 +187,31 @@ export const promise = async <T>({
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = parseFloat(relatedDoc as string)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && value) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = parseFloat(relatedDoc as string)
|
||||
siblingData[field.name] = parseFloat(value as string)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && value) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = parseFloat(value as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,18 +571,76 @@ const validateFilterOptions: Validate<
|
||||
}
|
||||
|
||||
export type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
|
||||
export const upload: UploadFieldValidation = (value: string, options) => {
|
||||
if (!value && options.required) {
|
||||
return options?.req?.t('validation:required')
|
||||
|
||||
export const upload: UploadFieldValidation = async (value, options) => {
|
||||
const {
|
||||
maxRows,
|
||||
minRows,
|
||||
relationTo,
|
||||
req: { payload, t },
|
||||
required,
|
||||
} = options
|
||||
|
||||
if (
|
||||
((!value && typeof value !== 'number') || (Array.isArray(value) && value.length === 0)) &&
|
||||
required
|
||||
) {
|
||||
return t('validation:required')
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
if (minRows && value.length < minRows) {
|
||||
return t('validation:lessThanMin', {
|
||||
label: t('general:rows'),
|
||||
min: minRows,
|
||||
value: value.length,
|
||||
})
|
||||
}
|
||||
|
||||
if (maxRows && value.length > maxRows) {
|
||||
return t('validation:greaterThanMax', {
|
||||
label: t('general:rows'),
|
||||
max: maxRows,
|
||||
value: value.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== 'undefined' && value !== null) {
|
||||
const idType =
|
||||
options?.req?.payload?.collections[options.relationTo]?.customIDType ||
|
||||
options?.req?.payload?.db?.defaultIDType
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
|
||||
if (!isValidID(value, idType)) {
|
||||
return options.req?.t('validation:validUploadID')
|
||||
const invalidRelationships = values.filter((val) => {
|
||||
let collectionSlug: string
|
||||
let requestedID
|
||||
|
||||
if (typeof relationTo === 'string') {
|
||||
collectionSlug = relationTo
|
||||
|
||||
// custom id
|
||||
if (val || typeof val === 'number') {
|
||||
requestedID = val
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
|
||||
collectionSlug = val.relationTo
|
||||
requestedID = val.value
|
||||
}
|
||||
|
||||
if (requestedID === null) return false
|
||||
|
||||
const idType =
|
||||
payload.collections[collectionSlug]?.customIDType || payload?.db?.defaultIDType || 'text'
|
||||
|
||||
return !isValidID(requestedID, idType)
|
||||
})
|
||||
|
||||
if (invalidRelationships.length > 0) {
|
||||
return `This relationship field has the following invalid relationships: ${invalidRelationships
|
||||
.map((err, invalid) => {
|
||||
return `${err} ${JSON.stringify(invalid)}`
|
||||
})
|
||||
.join(', ')}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user