Compare commits

..

20 Commits

Author SHA1 Message Date
Elliot DeNolf
5d1cad3adb chore(release): db-postgres/0.7.0 [skip ci] 2024-02-23 14:31:55 -05:00
Elliot DeNolf
e31f72da8e chore(release): plugin-nested-docs/1.0.12 [skip ci] 2024-02-23 14:31:26 -05:00
Elliot DeNolf
7aa058d604 chore(release): db-mongodb/1.4.3 [skip ci] 2024-02-23 14:31:11 -05:00
Elliot DeNolf
64e80d242e chore(release): payload/2.11.2 [skip ci] 2024-02-23 14:29:37 -05:00
Dan Ribbens
e8f2ca484e feat(db-postgres): configurable custom schema to use (#5047)
* feat(db-postgres): configurable custom schema to use

* test(db-postgres): use public schema

* chore(db-postgres): simplify drop schema

* chore: add postgres-custom-schema test to ci

* chore: add custom schema to postgres ci

* chore(db-postgres): custom schema in migrate

* chore: ci postgres wait condition
2024-02-23 12:48:06 -05:00
Dan Ribbens
ceca5c4e97 fix(db-postgres): set _parentID for array nested localized fields (#5117)
* fix(db-postgres): find missing path for nested arrays

* fix(db-postgres): set _parentID for array nested localized fields

* fix: afterRead fallbackLocale causing locale data loss

* chore(richtext-lexical): updated args to match payload type change

* test: simplify localization e2e duplicate
2024-02-23 12:44:30 -05:00
Dan Ribbens
ee13736288 chore(plugin-nested-docs): payload added to peerDependencies (#5143) 2024-02-23 12:37:42 -05:00
Dan Ribbens
815bdfac0b fix(db-mongodb): unique sparse for not required fields (#5114)
* fix(db-mongodb): unique sparse for not required fields

* chore(db-mongodb): cleanup sparse index condition

* test: indexed field fix
2024-02-23 12:36:34 -05:00
Dan Ribbens
7a7f0ed7e8 fix: disabling API Key does not remove the key (#5145)
* fix: disabling API Key does not remove the key

* chore: encryptKey hook return null

* chore: fix auth e2e test setup
2024-02-23 12:31:14 -05:00
Dan Ribbens
ad42d541b3 fix: transaction error from access endpoint (#5156)
* fix: transaction error from access endpoint

* chore: fix async race condition in getEntityPolicies
2024-02-23 12:28:43 -05:00
Elliot DeNolf
32ed95e1ee fix: handle thrown errors in config-level afterError hook (#5147) 2024-02-21 16:44:16 -05:00
Yunsup Sim
70e57fef18 fix: Add Context Provider in EditMany Component (#5005)
* fix: Add  Context Provider in EditMany Component

* test: Fix e2e test error
2024-02-21 16:39:34 -05:00
Jarrod Flesch
0a07f607b9 fix: only replace the drawer content with full edit component if it exists (#5144) 2024-02-21 15:44:09 -05:00
Piotr Rogowski
3918fc7c21 chore(plugin-seo): add pl translations (#5021) 2024-02-21 15:01:38 -05:00
Martin Chełminiak
13f71ac475 chore: console error for missing script when running npm run payload (#5078) 2024-02-19 10:13:50 -05:00
Ben Regenspan
07720e777a docs: Reword Hooks Overview re: server-only execution (#5070) 2024-02-19 09:56:55 -05:00
Sondre Ørland
efff47e400 chore: translation for image cropping in norwegian bokmål (#5113) 2024-02-19 09:47:32 -05:00
Elliot DeNolf
453ac218ea chore: reorder changelog 2024-02-17 01:38:13 -05:00
Elliot DeNolf
d4b09bd9cd chore(release): richtext-lexical/0.7.0 [skip ci] 2024-02-16 15:00:13 -05:00
Elliot DeNolf
dd67e03fc1 chore(release): plugin-search/1.1.0 [skip ci] 2024-02-16 15:00:03 -05:00
66 changed files with 715 additions and 247 deletions

View File

@@ -85,7 +85,7 @@ jobs:
strategy:
fail-fast: false
matrix:
database: [ mongoose, postgres, postgres-uuid, supabase ]
database: [mongoose, postgres, postgres-custom-schema, postgres-uuid, supabase]
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -123,7 +123,7 @@ jobs:
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
if: startsWith(matrix.database, 'postgres')
- name: Install Supabase CLI
uses: supabase/setup-cli@v1
@@ -139,14 +139,19 @@ jobs:
- name: Wait for PostgreSQL
run: sleep 30
if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL with custom schema
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
if: matrix.database == 'postgres-custom-schema'
- name: Configure Supabase
run: |

View File

@@ -1,10 +1,24 @@
## [2.11.2](https://github.com/payloadcms/payload/compare/v2.11.1...v2.11.2) (2024-02-23)
### Features
* **db-postgres:** configurable custom schema to use ([#5047](https://github.com/payloadcms/payload/issues/5047)) ([e8f2ca4](https://github.com/payloadcms/payload/commit/e8f2ca484ee56cd7767d5111e46ebd24752ff8de))
### Bug Fixes
* Add Context Provider in EditMany Component ([#5005](https://github.com/payloadcms/payload/issues/5005)) ([70e57fe](https://github.com/payloadcms/payload/commit/70e57fef184f7fcf56344ea755465f246f2253a5))
* **db-mongodb:** unique sparse for not required fields ([#5114](https://github.com/payloadcms/payload/issues/5114)) ([815bdfa](https://github.com/payloadcms/payload/commit/815bdfac0b0afbff2a20e54d5aee64b90f6b3a77))
* **db-postgres:** set _parentID for array nested localized fields ([#5117](https://github.com/payloadcms/payload/issues/5117)) ([ceca5c4](https://github.com/payloadcms/payload/commit/ceca5c4e97f53f1346797a31b6abfc0375e98215))
* disabling API Key does not remove the key ([#5145](https://github.com/payloadcms/payload/issues/5145)) ([7a7f0ed](https://github.com/payloadcms/payload/commit/7a7f0ed7e8132253be607c111c160163b84bd770))
* handle thrown errors in config-level afterError hook ([#5147](https://github.com/payloadcms/payload/issues/5147)) ([32ed95e](https://github.com/payloadcms/payload/commit/32ed95e1ee87409db234f1b7bd6d2e462fd9ed5d))
* only replace the drawer content with full edit component if it exists ([#5144](https://github.com/payloadcms/payload/issues/5144)) ([0a07f60](https://github.com/payloadcms/payload/commit/0a07f607b9fb1217ad956cd05b2a84a4042a19ca))
* transaction error from access endpoint ([#5156](https://github.com/payloadcms/payload/issues/5156)) ([ad42d54](https://github.com/payloadcms/payload/commit/ad42d541b342ed56463b81cee6d6307df6f06d7f))
## [2.11.1](https://github.com/payloadcms/payload/compare/v2.11.0...v2.11.1) (2024-02-16)
### ⚠ BREAKING CHANGES
* **richtext-lexical:** Update lexical from 0.12.6 to 0.13.1, port over all useful changes from playground (#5066)
### Features
* **db-postgres:** adds idType to use uuid or serial id columns ([#3864](https://github.com/payloadcms/payload/issues/3864)) ([d6c2578](https://github.com/payloadcms/payload/commit/d6c25783cfa97983bf9db27ceb5ccd39a62c62f1))
@@ -25,6 +39,14 @@
* **richtext-lexical:** do not remove adjacent paragraph node when inserting certain nodes in empty editor ([#5061](https://github.com/payloadcms/payload/issues/5061)) ([6323965](https://github.com/payloadcms/payload/commit/6323965c652ea68dffeb716957b124d165b9ce96))
* **uploads:** account for serverURL when retrieving external file ([#5102](https://github.com/payloadcms/payload/issues/5102)) ([25cee8b](https://github.com/payloadcms/payload/commit/25cee8bb102bf80b3a4bfb4b4e46712722cc7f0d))
### ⚠ BREAKING CHANGES: @payloadcms/richtext-lexical
* **richtext-lexical:** Update lexical from 0.12.6 to 0.13.1, port over all useful changes from playground (#5066)
- You HAVE to make sure that any versions of the lexical packages (IF you have any installed) match the lexical version which richtext-lexical uses: v0.13.1. If you do not do this, you may be plagued by React useContext / "cannot find active editor state" errors
- Updates to lexical's API, e.g. the removal of INTERNAL_isPointSelection, could be breaking depending on your code. Please consult the [lexical changelog](https://github.com/facebook/lexical/blob/main/CHANGELOG.md).
## [2.11.0](https://github.com/payloadcms/payload/compare/v2.10.1...v2.11.0) (2024-02-09)

View File

@@ -37,11 +37,12 @@ export default buildConfig({
### 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`. |
| `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. |
| Option | Description |
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-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. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
### Access to Drizzle

View File

@@ -36,7 +36,7 @@ If your Hook simply performs a side-effect, such as updating a CRM, it might be
#### Server-only execution
Payload Hooks do not have any effect within the Payload Admin panel. You can safely [remove your hooks](/docs/admin/webpack#aliasing-server-only-modules) from your Admin panel's code by customizing the Webpack config, which not only keeps your Admin bundles' filesize small but also ensures that any server-side only code does not cause problems within browser environments.
Payload Hooks are only triggered on the server. You can safely [remove your hooks](/docs/admin/webpack#aliasing-server-only-modules) from your Admin panel's client-side code by customizing the Webpack config, which not only keeps your Admin bundles' filesize small but also ensures that any server-side only code does not cause problems within browser environments.
## Hook Types

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.4.2",
"version": "1.4.3",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -14,8 +14,10 @@ import type {
DateField,
EmailField,
Field,
FieldAffectingData,
GroupField,
JSONField,
NonPresentationalField,
NumberField,
PointField,
RadioField,
@@ -23,12 +25,12 @@ import type {
RichTextField,
RowField,
SelectField,
Tab,
TabsField,
TextField,
TextareaField,
UploadField,
} from 'payload/types'
import type { FieldAffectingData, NonPresentationalField, Tab, UnnamedTab } from 'payload/types'
import { Schema } from 'mongoose'
import {
@@ -61,7 +63,15 @@ const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSc
unique: (!disableUnique && field.unique) || false,
}
if (schema.unique && (field.localized || draftsEnabled)) {
if (
schema.unique &&
(field.localized ||
draftsEnabled ||
(fieldAffectsData(field) &&
field.type !== 'group' &&
field.type !== 'tab' &&
field.required !== true))
) {
schema.sparse = true
}
@@ -79,7 +89,6 @@ const localizeSchema = (
) => {
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
return {
localized: true,
type: localization.localeCodes.reduce(
(localeSchema, locale) => ({
...localeSchema,
@@ -89,6 +98,7 @@ const localizeSchema = (
_id: false,
},
),
localized: true,
}
}
return schema
@@ -140,7 +150,6 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
) => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
default: undefined,
type: [
buildSchema(config, field.fields, {
allowIDField: true,
@@ -153,6 +162,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
}),
],
default: undefined,
}
schema.add({
@@ -166,8 +176,8 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
buildSchemaOptions: BuildSchemaOptions,
): void => {
const fieldSchema = {
default: undefined,
type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })],
default: undefined,
}
schema.add({
@@ -187,12 +197,12 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (field.localized && config.localization) {
config.localization.localeCodes.forEach((localeCode) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Possible incorrect typing in mongoose types, this works
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
})
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Possible incorrect typing in mongoose types, this works
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(field.name).discriminator(blockItem.slug, blockSchema)
}
})
@@ -325,14 +335,14 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
type: {
type: String,
enum: ['Point'],
},
coordinates: {
type: [Number],
default: field.defaultValue || undefined,
required: false,
type: [Number],
},
type: {
enum: ['Point'],
type: String,
},
}
if (buildSchemaOptions.disableUnique && field.unique && field.localized) {
@@ -366,11 +376,11 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') return option.value
return option
}),
type: String,
}
schema.add({
@@ -388,7 +398,6 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (field.localized && config.localization) {
schemaToReturn = {
localized: true,
type: config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
@@ -396,56 +405,57 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
relationTo: { enum: field.relationTo, type: String },
type: Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
refPath: `${field.name}.${locale}.relationTo`,
type: Schema.Types.Mixed,
refPath: `${field.name}.${locale}.relationTo`,
},
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
ref: field.relationTo,
type: Schema.Types.Mixed,
ref: field.relationTo,
}
}
return {
...locales,
[locale]: field.hasMany ? { default: undefined, type: [localeSchema] } : localeSchema,
[locale]: field.hasMany ? { type: [localeSchema], default: undefined } : localeSchema,
}
}, {}),
localized: true,
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
relationTo: { enum: field.relationTo, type: String },
type: Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
refPath: `${field.name}.relationTo`,
type: Schema.Types.Mixed,
refPath: `${field.name}.relationTo`,
},
}
if (field.hasMany) {
schemaToReturn = {
default: undefined,
type: [schemaToReturn],
default: undefined,
}
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
ref: field.relationTo,
type: Schema.Types.Mixed,
ref: field.relationTo,
}
if (field.hasMany) {
schemaToReturn = {
default: undefined,
type: [schemaToReturn],
default: undefined,
}
}
}
@@ -488,11 +498,11 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') return option.value
return option
}),
type: String,
}
if (buildSchemaOptions.draftsEnabled || !field.required) {
@@ -576,8 +586,8 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
ref: field.relationTo,
type: Schema.Types.Mixed,
ref: field.relationTo,
}
schema.add({

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.6.0",
"version": "0.7.0",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -3,7 +3,7 @@ import type { Connect } from 'payload/database'
import { eq, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
import { Pool } from 'pg'
import prompts from 'prompts'
@@ -61,9 +61,13 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
this.drizzle = drizzle(this.pool, { logger, schema: this.schema })
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING TABLES ----')
await this.drizzle.execute(sql`drop schema public cascade;
create schema public;`)
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
await this.drizzle.execute(
sql.raw(`
drop schema if exists ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};
`),
)
this.payload.logger.info('---- DROPPED TABLES ----')
}
} catch (err) {
@@ -120,7 +124,7 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
await apply()
// Migration table def in order to use query using drizzle
const migrationsSchema = pgTable('payload_migrations', {
const migrationsSchema = this.pgSchema.table('payload_migrations', {
name: varchar('name'),
batch: numeric('batch'),
created_at: timestamp('created_at'),

View File

@@ -78,7 +78,7 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = `${currentTableName}_${toSnakeCase(field.name)}`
const arrayTableName = `${currentTableName}_${path}${toSnakeCase(field.name)}`
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray

View File

@@ -52,11 +52,13 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
fieldConstraints: {},
idType,
logger: args.logger,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,
relations: {},
schema: {},
schemaName: args.schemaName,
sessions: {},
tables: {},

View File

@@ -2,7 +2,7 @@
import type { Init } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum } from 'drizzle-orm/pg-core'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
@@ -11,6 +11,12 @@ import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
export const init: Init = async function init(this: PostgresAdapter) {
if (this.schemaName) {
this.pgSchema = pgSchema(this.schemaName)
} else {
this.pgSchema = { table: pgTable }
}
if (this.payload.config.localization) {
this.enums.enum__locales = pgEnum(
'_locales',

View File

@@ -39,7 +39,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
latestBatch = Number(migrationsInDB[0]?.batch)
}
} else {
await createMigrationTable(this.drizzle)
await createMigrationTable(this)
}
if (migrationsInDB.find((m) => m.batch === -1)) {

View File

@@ -44,8 +44,10 @@ export async function migrateFresh(
msg: `Dropping database.`,
})
await this.drizzle.execute(sql`drop schema public cascade;
create schema public;`)
await this.drizzle.execute(
sql.raw(`drop schema ${this.schemaName || 'public'} cascade;
create schema ${this.schemaName || 'public'};`),
)
const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({

View File

@@ -1,4 +1,5 @@
import type { SQL } from 'drizzle-orm'
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
import type { Field, Where } from 'payload/types'
import { asc, desc } from 'drizzle-orm'
@@ -12,7 +13,7 @@ export type BuildQueryJoins = Record<string, SQL>
export type BuildQueryJoinAliases = {
condition: SQL
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
}[]
type BuildQueryArgs = {

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import type { SQL } from 'drizzle-orm'
import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload/types'
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload/types'
import { and, eq, like, sql } from 'drizzle-orm'
import { alias } from 'drizzle-orm/pg-core'
@@ -15,7 +16,7 @@ import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
type Constraint = {
columnName: string
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
value: unknown
}
@@ -26,12 +27,12 @@ type TableColumn = {
getNotNullColumnByValue?: (val: unknown) => string
pathSegments?: string[]
rawColumn?: SQL
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
}
type Args = {
adapter: PostgresAdapter
aliasTable?: GenericTable
aliasTable?: GenericTable | PgTableWithColumns<any>
collectionPath: string
columnPrefix?: string
constraintPath?: string

View File

@@ -1,19 +1,15 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type {
IndexBuilder,
PgColumnBuilder,
PgTableWithColumns,
UniqueConstraintBuilder,
} from 'drizzle-orm/pg-core'
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import {
index,
integer,
numeric,
pgTable,
serial,
timestamp,
unique,
varchar,
} from 'drizzle-orm/pg-core'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
@@ -77,14 +73,14 @@ export const buildTable = ({
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable
let textsTable: GenericTable
let numbersTable: GenericTable
let localesTable: GenericTable | PgTableWithColumns<any>
let textsTable: GenericTable | PgTableWithColumns<any>
let numbersTable: GenericTable | PgTableWithColumns<any>
// Relationships to the base collection
const relationships: Set<string> = rootRelationships || new Set()
let relationshipsTable: GenericTable
let relationshipsTable: GenericTable | PgTableWithColumns<any>
// Drizzle relations
const relationsToBuild: Map<string, string> = new Map()
@@ -136,7 +132,7 @@ export const buildTable = ({
.notNull()
}
const table = pgTable(tableName, columns, (cols) => {
const table = adapter.pgSchema.table(tableName, columns, (cols) => {
const extraConfig = Object.entries(baseExtraConfig).reduce((config, [key, func]) => {
config[key] = func(cols)
return config
@@ -158,7 +154,7 @@ export const buildTable = ({
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
localesTable = pgTable(localeTableName, localesColumns, (cols) => {
localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
(acc, [colName, func]) => {
acc[colName] = func(cols)
@@ -201,7 +197,7 @@ export const buildTable = ({
columns.locale = adapter.enums.enum__locales('locale')
}
textsTable = pgTable(textsTableName, columns, (cols) => {
textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
@@ -245,7 +241,7 @@ export const buildTable = ({
columns.locale = adapter.enums.enum__locales('locale')
}
numbersTable = pgTable(numbersTableName, columns, (cols) => {
numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
@@ -307,19 +303,23 @@ export const buildTable = ({
const relationshipsTableName = `${tableName}_rels`
relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => {
const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
}
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
}
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
}
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
}
return result
})
return result
},
)
adapter.tables[relationshipsTableName] = relationshipsTable

View File

@@ -7,7 +7,14 @@ import type {
Relations,
} from 'drizzle-orm'
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres'
import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
import type {
PgColumn,
PgEnum,
PgSchema,
PgTableWithColumns,
PgTransaction,
} from 'drizzle-orm/pg-core'
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
@@ -21,6 +28,7 @@ export type Args = {
migrationDir?: string
pool: PoolConfig
push?: boolean
schemaName?: string
}
export type GenericColumn = PgColumn<
@@ -59,11 +67,13 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: DrizzleTransaction
@@ -71,7 +81,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'

View File

@@ -40,7 +40,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
// Add any sub arrays that need to be created
// We will call this recursively below
arrayRows.forEach((arrayRow) => {
arrayRows.forEach((arrayRow, i) => {
if (Object.keys(arrayRow.arrays).length > 0) {
rowsByTable[tableName].arrays.push(arrayRow.arrays)
}
@@ -53,6 +53,9 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
arrayRowLocaleData._parentID = arrayRow.row.id
arrayRowLocaleData._locale = arrayRowLocale
rowsByTable[tableName].locales.push(arrayRowLocaleData)
if (!arrayRow.row.id) {
arrayRowLocaleData._getParentID = (rows) => rows[i].id
}
})
})
})
@@ -69,6 +72,15 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
// Insert locale rows
if (adapter.tables[`${tableName}_locales`] && row.locales.length > 0) {
if (!row.locales[0]._parentID) {
row.locales = row.locales.map((localeRow, i) => {
if (typeof localeRow._getParentID === 'function') {
localeRow._parentID = localeRow._getParentID(insertedRows)
delete localeRow._getParentID
}
return localeRow
})
}
await db.insert(adapter.tables[`${tableName}_locales`]).values(row.locales).returning()
}

View File

@@ -1,13 +1,17 @@
import { sql } from 'drizzle-orm'
import type { DrizzleDB } from '../types'
import type { PostgresAdapter } from '../types'
export const createMigrationTable = async (db: DrizzleDB): Promise<void> => {
await db.execute(sql`CREATE TABLE IF NOT EXISTS "payload_migrations" (
export const createMigrationTable = async (adapter: PostgresAdapter): Promise<void> => {
const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
await adapter.drizzle.execute(
sql.raw(`CREATE TABLE IF NOT EXISTS ${prependSchema}"payload_migrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"batch" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);`)
);`),
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.11.1",
"version": "2.11.2",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -52,21 +52,9 @@ const Content: React.FC<DocumentDrawerProps> = ({
const { id, docPermissions, getDocPreferences } = useDocumentInfo()
// The component definition could come from multiple places in the config
// we need to cascade into the proper component from the top-down
// 1. "components.Edit"
// 2. "components.Edit.Default"
// 3. "components.Edit.Default.Component"
const CustomEditView =
typeof Edit === 'function'
? Edit
: typeof Edit === 'object' && typeof Edit.Default === 'function'
? Edit.Default
: typeof Edit?.Default === 'object' &&
'Component' in Edit.Default &&
typeof Edit.Default.Component === 'function'
? Edit.Default.Component
: undefined
// If they are replacing the entire edit view, use that.
// Else let the DefaultEdit determine what to render.
const CustomEditView = typeof Edit === 'function' ? Edit : undefined
const [fields, setFields] = useState(() => formatFields(collectionConfig, true))

View File

@@ -13,6 +13,7 @@ import { fieldTypes } from '../../forms/field-types'
import X from '../../icons/X'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { DocumentInfoProvider } from '../../utilities/DocumentInfo'
import { OperationContext } from '../../utilities/OperationProvider'
import { SelectAllStatus, useSelection } from '../../views/collections/List/SelectionProvider'
import { Drawer, DrawerToggler } from '../Drawer'
@@ -120,53 +121,55 @@ const EditMany: React.FC<Props> = (props) => {
{t('edit')}
</DrawerToggler>
<Drawer header={null} slug={drawerSlug}>
<OperationContext.Provider value="update">
<Form className={`${baseClass}__form`} onSuccess={onSuccess}>
<div className={`${baseClass}__main`}>
<div className={`${baseClass}__header`}>
<h2 className={`${baseClass}__header__title`}>
{t('editingLabel', { count, label: getTranslation(plural, i18n) })}
</h2>
<button
aria-label={t('close')}
className={`${baseClass}__header__close`}
id={`close-drawer__${drawerSlug}`}
onClick={() => closeModal(drawerSlug)}
type="button"
>
<X />
</button>
</div>
<FieldSelect fields={fields} setSelected={setSelected} />
<RenderFields fieldSchema={selected} fieldTypes={fieldTypes} />
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions`}>
{collection.versions ? (
<React.Fragment>
<Publish
<DocumentInfoProvider collection={collection}>
<OperationContext.Provider value="update">
<Form className={`${baseClass}__form`} onSuccess={onSuccess}>
<div className={`${baseClass}__main`}>
<div className={`${baseClass}__header`}>
<h2 className={`${baseClass}__header__title`}>
{t('editingLabel', { count, label: getTranslation(plural, i18n) })}
</h2>
<button
aria-label={t('close')}
className={`${baseClass}__header__close`}
id={`close-drawer__${drawerSlug}`}
onClick={() => closeModal(drawerSlug)}
type="button"
>
<X />
</button>
</div>
<FieldSelect fields={fields} setSelected={setSelected} />
<RenderFields fieldSchema={selected} fieldTypes={fieldTypes} />
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions`}>
{collection.versions ? (
<React.Fragment>
<Publish
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>
) : (
<Submit
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>
) : (
<Submit
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
)}
)}
</div>
</div>
</div>
</div>
</div>
</div>
</Form>
</OperationContext.Provider>
</Form>
</OperationContext.Provider>
</DocumentInfoProvider>
</Drawer>
</div>
)

View File

@@ -7,13 +7,14 @@ import { extractTranslations } from '../../translations/extractTranslations'
const labels = extractTranslations(['authentication:enableAPIKey', 'authentication:apiKey'])
const encryptKey: FieldHook = ({ req, value }) =>
value ? req.payload.encrypt(value as string) : undefined
value ? req.payload.encrypt(value as string) : null
const decryptKey: FieldHook = ({ req, value }) =>
value ? req.payload.decrypt(value as string) : undefined
export default [
{
name: 'enableAPIKey',
type: 'checkbox',
admin: {
components: {
Field: () => null,
@@ -21,10 +22,10 @@ export default [
},
defaultValue: false,
label: labels['authentication:enableAPIKey'],
type: 'checkbox',
},
{
name: 'apiKey',
type: 'text',
admin: {
components: {
Field: () => null,
@@ -35,10 +36,10 @@ export default [
beforeChange: [encryptKey],
},
label: labels['authentication:apiKey'],
type: 'text',
},
{
name: 'apiKeyIndex',
type: 'text',
admin: {
disabled: true,
},
@@ -59,6 +60,5 @@ export default [
},
],
},
type: 'text',
},
] as Field[]

View File

@@ -72,6 +72,8 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
overrideAccess,
req,
req: {
fallbackLocale,
locale,
payload,
payload: { config, secret },
},
@@ -196,7 +198,9 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: user,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -56,30 +56,33 @@ const args = minimist(process.argv.slice(2))
const scriptIndex = args._.findIndex((x) => x === 'build')
const script = scriptIndex === -1 ? args._[0] : args._[scriptIndex]
if (script) {
if (script.startsWith('migrate')) {
migrate(args).then(() => process.exit(0))
} else {
switch (script.toLowerCase()) {
case 'build': {
build()
break
}
if (script.startsWith('migrate')) {
migrate(args).then(() => process.exit(0))
} else {
switch (script.toLowerCase()) {
case 'build': {
build()
break
case 'generate:types': {
generateTypes()
break
}
case 'generate:graphqlschema': {
generateGraphQLSchema()
break
}
default:
console.log(`Unknown script "${script}".`)
break
}
case 'generate:types': {
generateTypes()
break
}
case 'generate:graphqlschema': {
generateGraphQLSchema()
break
}
default:
console.log(`Unknown script "${script}".`)
break
}
} else {
console.error('No payload script specified. Did you mean to run `payload migrate`?')
}
/**

View File

@@ -17,7 +17,6 @@ import type {
import executeAccess from '../../auth/executeAccess'
import sendVerificationEmail from '../../auth/sendVerificationEmail'
import { registerLocalStrategy } from '../../auth/strategies/local/register'
import { fieldAffectsData } from '../../fields/config/types'
import { afterChange } from '../../fields/hooks/afterChange'
import { afterRead } from '../../fields/hooks/afterRead'
import { beforeChange } from '../../fields/hooks/beforeChange'
@@ -26,12 +25,12 @@ import { generateFileData } from '../../uploads/generateFileData'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles'
import { uploadFiles } from '../../uploads/uploadFiles'
import { commitTransaction } from '../../utilities/commitTransaction'
import flattenFields from '../../utilities/flattenTopLevelFields'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'
import { saveVersion } from '../../versions/saveVersion'
import { buildAfterOperation } from './utils'
import flattenFields from '../../utilities/flattenTopLevelFields'
const unlinkFile = promisify(fs.unlink)
@@ -88,6 +87,8 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
overrideAccess,
overwriteExistingFiles = false,
req: {
fallbackLocale,
locale,
payload,
payload: { config, emailOptions },
},
@@ -289,7 +290,9 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -66,6 +66,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
depth,
overrideAccess,
req: {
fallbackLocale,
locale,
payload: { config },
payload,
@@ -149,9 +150,9 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
if (collectionConfig.versions) {
await deleteCollectionVersions({
id,
slug: collectionConfig.slug,
payload,
req,
slug: collectionConfig.slug,
})
}
@@ -178,7 +179,9 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
context: req.context,
depth,
doc: result || doc,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -59,6 +59,8 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
depth,
overrideAccess,
req: {
fallbackLocale,
locale,
payload: { config },
payload,
t,
@@ -120,9 +122,9 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
if (collectionConfig.versions) {
await deleteCollectionVersions({
id,
slug: collectionConfig.slug,
payload,
req,
slug: collectionConfig.slug,
})
}
@@ -156,7 +158,9 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -68,7 +68,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
overrideAccess,
page,
pagination = true,
req: { locale, payload },
req: { fallbackLocale, locale, payload },
req,
showHiddenFields,
sort,
@@ -196,8 +196,10 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
currentDepth,
depth,
doc,
fallbackLocale,
findMany: true,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -56,7 +56,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
disableErrors,
draft: draftEnabled = false,
overrideAccess = false,
req: { locale, t },
req: { fallbackLocale, locale, t },
req,
showHiddenFields,
} = args
@@ -139,7 +139,9 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
currentDepth,
depth,
doc: result,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -34,7 +34,7 @@ async function findVersionByID<T extends TypeWithID = any>(
depth,
disableErrors,
overrideAccess,
req: { locale, payload, t },
req: { fallbackLocale, locale, payload, t },
req,
showHiddenFields,
} = args
@@ -112,7 +112,9 @@ async function findVersionByID<T extends TypeWithID = any>(
currentDepth,
depth,
doc: result.version,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -37,7 +37,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
overrideAccess,
page,
pagination = true,
req: { locale, payload },
req: { fallbackLocale, locale, payload },
req,
showHiddenFields,
sort,
@@ -125,8 +125,10 @@ async function findVersions<T extends TypeWithVersion<T>>(
context: req.context,
depth,
doc: data.version,
fallbackLocale,
findMany: true,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -34,7 +34,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
depth,
overrideAccess = false,
req,
req: { locale, payload, t },
req: { fallbackLocale, locale, payload, t },
showHiddenFields,
} = args
@@ -140,7 +140,9 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
context: req.context,
depth,
doc: result,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -75,6 +75,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
overrideAccess,
overwriteExistingFiles = false,
req: {
fallbackLocale,
locale,
payload: { config },
payload,
@@ -176,7 +177,9 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth: 0,
doc,
fallbackLocale,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
@@ -309,7 +312,9 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
fallbackLocale: null,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -76,6 +76,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
overrideAccess,
overwriteExistingFiles = false,
req: {
fallbackLocale,
locale,
payload: { config },
payload,
@@ -130,7 +131,9 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth: 0,
doc: docWithLocales,
fallbackLocale: null,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
@@ -297,7 +300,9 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
fallbackLocale,
global: null,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -56,7 +56,7 @@ const errorHandler =
err,
response,
req.context,
req.collection.config,
null,
)) || {
response,
status,

View File

@@ -11,9 +11,11 @@ type Args = {
currentDepth?: number
depth: number
doc: Record<string, unknown>
fallbackLocale: null | string
findMany?: boolean
flattenLocales?: boolean
global: SanitizedGlobalConfig | null
locale: string
overrideAccess: boolean
req: PayloadRequest
showHiddenFields: boolean
@@ -26,9 +28,11 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
currentDepth: incomingCurrentDepth,
depth: incomingDepth,
doc: incomingDoc,
fallbackLocale,
findMany,
flattenLocales = true,
global,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -52,11 +56,13 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: collection?.fields || global?.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,

View File

@@ -16,11 +16,13 @@ type Args = {
currentDepth: number
depth: number
doc: Record<string, unknown>
fallbackLocale: null | string
field: Field | TabAsField
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
global: SanitizedGlobalConfig | null
locale: null | string
overrideAccess: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
@@ -44,11 +46,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
field,
fieldPromises,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -72,18 +76,13 @@ export const promise = async ({
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null &&
field.localized &&
req.locale !== 'all' &&
locale !== 'all' &&
req.payload.config.localization
if (shouldHoistLocalizedValue) {
// replace actual value with localized value before sanitizing
// { [locale]: fields } -> fields
const { locale } = req
const value = siblingDoc[field.name][locale]
const fallbackLocale =
req.payload.config.localization &&
req.payload.config.localization?.fallback &&
req.fallbackLocale
let hoistedValue = value
@@ -201,7 +200,7 @@ export const promise = async ({
const shouldRunHookOnAllLocales =
field.localized &&
(req.locale === 'all' || !flattenLocales) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) {
@@ -277,7 +276,7 @@ export const promise = async ({
) {
siblingDoc[field.name] = await getValueWithDefault({
defaultValue: field.defaultValue,
locale: req.locale,
locale,
user: req.user,
value: siblingDoc[field.name],
})
@@ -288,7 +287,9 @@ export const promise = async ({
relationshipPopulationPromise({
currentDepth,
depth,
fallbackLocale,
field,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -309,11 +310,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -337,11 +340,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -361,11 +366,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -397,11 +404,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: block.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -425,11 +434,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: block.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -457,11 +468,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -487,11 +500,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -511,11 +526,13 @@ export const promise = async ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,

View File

@@ -8,9 +8,11 @@ type PopulateArgs = {
data: Record<string, unknown>
dataReference: Record<string, any>
depth: number
fallbackLocale: null | string
field: RelationshipField | UploadField
index?: number
key?: string
locale: null | string
overrideAccess: boolean
req: PayloadRequest
showHiddenFields: boolean
@@ -21,9 +23,11 @@ const populate = async ({
data,
dataReference,
depth,
fallbackLocale,
field,
index,
key,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -54,8 +58,8 @@ const populate = async ({
id,
depth,
currentDepth + 1,
req.locale,
req.fallbackLocale,
locale,
fallbackLocale,
overrideAccess,
showHiddenFields,
]),
@@ -90,7 +94,9 @@ const populate = async ({
type PromiseArgs = {
currentDepth: number
depth: number
fallbackLocale: null | string
field: RelationshipField | UploadField
locale: null | string
overrideAccess: boolean
req: PayloadRequest
showHiddenFields: boolean
@@ -100,7 +106,9 @@ type PromiseArgs = {
const relationshipPopulationPromise = async ({
currentDepth,
depth,
fallbackLocale,
field,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -112,7 +120,7 @@ const relationshipPopulationPromise = async ({
if (fieldSupportsMany(field) && field.hasMany) {
if (
req.locale === 'all' &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null
) {
@@ -125,9 +133,11 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name][key][index],
dataReference: resultingDoc,
depth: populateDepth,
fallbackLocale,
field,
index,
key,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -146,8 +156,10 @@ const relationshipPopulationPromise = async ({
data: relatedDoc,
dataReference: resultingDoc,
depth: populateDepth,
fallbackLocale,
field,
index,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -161,7 +173,7 @@ const relationshipPopulationPromise = async ({
} else if (
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null &&
req.locale === 'all'
locale === 'all'
) {
Object.keys(siblingDoc[field.name]).forEach((key) => {
const rowPromise = async () => {
@@ -170,8 +182,10 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name][key],
dataReference: resultingDoc,
depth: populateDepth,
fallbackLocale,
field,
key,
locale,
overrideAccess,
req,
showHiddenFields,
@@ -187,7 +201,9 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name],
dataReference: resultingDoc,
depth: populateDepth,
fallbackLocale,
field,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -11,11 +11,13 @@ type Args = {
currentDepth: number
depth: number
doc: Record<string, unknown>
fallbackLocale: null | string
fieldPromises: Promise<void>[]
fields: (Field | TabAsField)[]
findMany: boolean
flattenLocales: boolean
global: SanitizedGlobalConfig | null
locale: null | string
overrideAccess: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
@@ -31,11 +33,13 @@ export const traverseFields = ({
currentDepth,
depth,
doc,
fallbackLocale,
fieldPromises,
fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,
@@ -52,11 +56,13 @@ export const traverseFields = ({
currentDepth,
depth,
doc,
fallbackLocale,
field,
fieldPromises,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
populationPromises,
req,

View File

@@ -23,14 +23,14 @@ type Args = {
async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T> {
const {
slug,
depth,
draft: draftEnabled = false,
globalConfig,
overrideAccess = false,
req: { locale, payload },
req: { fallbackLocale, locale, payload },
req,
showHiddenFields,
slug,
} = args
try {
@@ -51,9 +51,9 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
// /////////////////////////////////////
let doc = await req.payload.db.findGlobal({
slug,
locale,
req,
slug,
where: overrideAccess ? undefined : (accessResult as Where),
})
if (!doc) {
@@ -100,7 +100,9 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
context: req.context,
depth,
doc,
fallbackLocale,
global: globalConfig,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -31,7 +31,7 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
disableErrors,
globalConfig,
overrideAccess,
req: { locale, payload, t },
req: { fallbackLocale, locale, payload, t },
req,
showHiddenFields,
} = args
@@ -108,7 +108,9 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
currentDepth,
depth,
doc: result.version,
fallbackLocale,
global: globalConfig,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -35,7 +35,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
limit,
overrideAccess,
page,
req: { locale, payload },
req: { fallbackLocale, locale, payload },
req,
showHiddenFields,
sort,
@@ -97,8 +97,10 @@ async function findVersions<T extends TypeWithVersion<T>>(
// Patch globalType onto version doc
globalType: globalConfig.slug,
},
fallbackLocale,
findMany: true,
global: globalConfig,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -25,7 +25,7 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
depth,
globalConfig,
overrideAccess,
req: { payload, t },
req: { fallbackLocale, locale, payload, t },
req,
showHiddenFields,
} = args
@@ -66,9 +66,9 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
// /////////////////////////////////////
const previousDoc = await payload.findGlobal({
slug: globalConfig.slug,
depth,
req,
slug: globalConfig.slug,
})
// /////////////////////////////////////
@@ -76,23 +76,23 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
// /////////////////////////////////////
const global = await payload.db.findGlobal({
req,
slug: globalConfig.slug,
req,
})
let result = rawVersion.version
if (global) {
result = await payload.db.updateGlobal({
slug: globalConfig.slug,
data: result,
req,
slug: globalConfig.slug,
})
} else {
result = await payload.db.createGlobal({
slug: globalConfig.slug,
data: result,
req,
slug: globalConfig.slug,
})
}
@@ -105,7 +105,9 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
context: req.context,
depth,
doc: result,
fallbackLocale,
global: globalConfig,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -32,15 +32,15 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
args: Args<GeneratedTypes['globals'][TSlug]>,
): Promise<GeneratedTypes['globals'][TSlug]> {
const {
slug,
autosave,
depth,
draft: draftArg,
globalConfig,
overrideAccess,
req: { locale, payload },
req: { fallbackLocale, locale, payload },
req,
showHiddenFields,
slug,
} = args
try {
@@ -74,11 +74,11 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
// 2. Retrieve document
// /////////////////////////////////////
const { global, globalExists } = await getLatestGlobalVersion({
slug,
config: globalConfig,
locale,
payload,
req,
slug,
where: query,
})
@@ -97,7 +97,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
context: req.context,
depth: 0,
doc: globalJSON,
fallbackLocale,
global: globalConfig,
locale,
overrideAccess: true,
req,
showHiddenFields,
@@ -175,15 +177,15 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
if (!shouldSaveDraft) {
if (globalExists) {
result = await payload.db.updateGlobal({
slug,
data: result,
req,
slug,
})
} else {
result = await payload.db.createGlobal({
slug,
data: result,
req,
slug,
})
}
}
@@ -218,7 +220,9 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
context: req.context,
depth,
doc: result,
fallbackLocale: null,
global: globalConfig,
locale,
overrideAccess,
req,
showHiddenFields,

View File

@@ -274,7 +274,7 @@
"near": "nær"
},
"upload": {
"crop": "Avling",
"crop": "Beskjær",
"cropToolDescription": "Dra hjørnene av det valgte området, tegn et nytt område eller juster verdiene nedenfor.",
"dragAndDrop": "Dra og slipp en fil",
"dragAndDropHere": "eller dra og slipp en fil her",

View File

@@ -57,8 +57,10 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (typeof where === 'object') {
const paginatedRes = await req.payload.find({
collection: entity.slug,
depth: 0,
limit: 1,
overrideAccess: true,
pagination: false,
req,
where: {
...where,
@@ -79,6 +81,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
return req.payload.findByID({
id,
collection: entity.slug,
depth: 0,
overrideAccess: true,
req,
})
@@ -98,8 +101,13 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
const mutablePolicies = policiesObj
if (accessLevel === 'field' && docBeingAccessed === undefined) {
docBeingAccessed = await getEntityDoc()
// assign docBeingAccessed first as the promise to avoid multiple calls to getEntityDoc
docBeingAccessed = getEntityDoc().then((doc) => {
docBeingAccessed = doc
})
}
// awaiting the promise to ensure docBeingAccessed is assigned before it is used
await docBeingAccessed
const data = req?.body

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "1.0.11",
"version": "1.0.12",
"description": "The official Nested Docs plugin for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -20,6 +20,9 @@
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^0.18.5 || ^1.0.0 || ^2.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "1.0.1",
"version": "1.1.0",
"homepage:": "https://payloadcms.com",
"repository": "git@github.com:payloadcms/plugin-search.git",
"description": "Search plugin for Payload",

View File

@@ -2,10 +2,12 @@ import en from './en.json'
import es from './es.json'
import fa from './fa.json'
import fr from './fr.json'
import pl from './pl.json'
export default {
en,
es,
fa,
fr,
pl,
}

View File

@@ -0,0 +1,22 @@
{
"$schema": "./translation-schema.json",
"plugin-seo": {
"autoGenerate": "Wygeneruj automatycznie",
"imageAutoGenerationTip": "Automatyczne generowanie pobierze wybrany główny obraz.",
"bestPractices": "najlepsze praktyki",
"lengthTipTitle": "Długość powinna wynosić od {{minLength}} do {{maxLength}} znaków. Po porady dotyczące pisania wysokiej jakości meta tytułów zobacz ",
"lengthTipDescription": "Długość powinna wynosić od {{minLength}} do {{maxLength}} znaków. Po porady dotyczące pisania wysokiej jakości meta opisów zobacz ",
"good": "Dobrze",
"tooLong": "Zbyt długie",
"tooShort": "Zbyt krótkie",
"almostThere": "Prawie gotowe",
"characterCount": "{{current}}/{{minLength}}-{{maxLength}} znaków, ",
"charactersToGo": "pozostało {{characters}} znaków",
"charactersLeftOver": "zostało {{characters}} znaków",
"charactersTooMany": "{{characters}} znaków za dużo",
"noImage": "Brak obrazu",
"checksPassing": "{{current}}/{{max}} testów zakończonych pomyślnie",
"preview": "Podgląd",
"previewDescription": "Dokładne wyniki listowania mogą się różnić w zależności od treści i zgodności z kryteriami wyszukiwania."
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.6.1",
"version": "0.7.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -46,11 +46,13 @@ export const recurseNestedFields = ({
currentDepth,
depth,
doc: data as any, // Looks like it's only needed for hooks and access control, so doesn't matter what we pass here right now
fallbackLocale: req.fallbackLocale,
fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways
fields,
findMany,
flattenLocales,
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
locale: req.locale,
overrideAccess,
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
req,

View File

@@ -4,7 +4,7 @@ import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import { devUser } from '../credentials'
import { AuthDebug } from './AuthDebug'
import { namedSaveToJWTValue, saveToJWTKey, slug } from './shared'
import { apiKeysSlug, namedSaveToJWTValue, saveToJWTKey, slug } from './shared'
export default buildConfigWithDefaults({
admin: {
@@ -171,11 +171,7 @@ export default buildConfigWithDefaults({
],
},
{
slug: 'api-keys',
labels: {
singular: 'API Key',
plural: 'API Keys',
},
slug: apiKeysSlug,
access: {
read: ({ req: { user } }) => {
if (user.collection === 'api-keys') {
@@ -193,6 +189,10 @@ export default buildConfigWithDefaults({
useAPIKey: true,
},
fields: [],
labels: {
plural: 'API Keys',
singular: 'API Key',
},
},
{
slug: 'public-users',

View File

@@ -2,28 +2,34 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import payload from '../../packages/payload/src'
import { initPageConsoleErrorCatch, login, saveDocAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import { slug } from './shared'
import { apiKeysSlug, slug } from './shared'
/**
* TODO: Auth
* create first user
* unlock
* generate api key
* log out
*/
const { beforeAll, describe } = test
const headers = {
'Content-Type': 'application/json',
}
describe('auth', () => {
let page: Page
let url: AdminUrlUtil
let serverURL: string
let apiURL: string
beforeAll(async ({ browser }) => {
const { serverURL } = await initPayloadE2E(__dirname)
url = new AdminUrlUtil(serverURL, slug)
serverURL = (await initPayloadE2E(__dirname)).serverURL
apiURL = `${serverURL}/api`
const context = await browser.newContext()
page = await context.newPage()
@@ -36,6 +42,10 @@ describe('auth', () => {
})
describe('authenticated users', () => {
beforeAll(async ({ browser }) => {
url = new AdminUrlUtil(serverURL, slug)
})
test('should allow change password', async () => {
await page.goto(url.account)
const emailBeforeSave = await page.locator('#field-email').inputValue()
@@ -62,4 +72,56 @@ describe('auth', () => {
await expect(page.locator('#use-auth-result')).toHaveText('Goodbye, world!')
})
})
describe('api-keys', () => {
let user
beforeAll(async () => {
url = new AdminUrlUtil(serverURL, apiKeysSlug)
user = await payload.create({
collection: apiKeysSlug,
data: {
enableAPIKey: true,
},
})
})
test('should enable api key', async () => {
await page.goto(url.create)
// click enable api key checkbox
await page.locator('#field-enableAPIKey').click()
// assert that the value is set
const apiKey = await page.locator('#apiKey').inputValue()
expect(apiKey).toBeDefined()
await saveDocAndAssert(page)
expect(await page.locator('#apiKey').inputValue()).toStrictEqual(apiKey)
})
test('should disable api key', async () => {
await page.goto(url.edit(user.id))
// click enable api key checkbox
await page.locator('#field-enableAPIKey').click()
// assert that the apiKey field is hidden
await expect(page.locator('#apiKey')).toBeHidden()
await saveDocAndAssert(page)
// use the api key in a fetch to assert that it is disabled
const response = await fetch(`${apiURL}/${apiKeysSlug}/me`, {
headers: {
...headers,
Authorization: `${slug} API-Key ${user.apiKey}`,
},
}).then((res) => res.json())
expect(response.user).toBeNull()
})
})
})

View File

@@ -1,4 +1,5 @@
export const slug = 'users'
export const apiKeysSlug = 'api-keys'
export const namedSaveToJWTValue = 'namedSaveToJWT value'

View File

@@ -35,6 +35,13 @@ const databaseAdapters = {
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
},
}),
'postgres-custom-schema': postgresAdapter({
migrationDir,
pool: {
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
},
schemaName: 'custom',
}),
'postgres-uuid': postgresAdapter({
idType: 'uuid',
migrationDir,

View File

@@ -3,7 +3,7 @@ import fs from 'fs'
import { GraphQLClient } from 'graphql-request'
import path from 'path'
import type { DrizzleDB } from '../../packages/db-postgres/src/types'
import type { PostgresAdapter } from '../../packages/db-postgres/src/types'
import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../packages/payload/src/express/types'
@@ -44,10 +44,14 @@ describe('database', () => {
describe('migrations', () => {
beforeAll(async () => {
if (process.env.PAYLOAD_DROP_DATABASE === 'true' && 'drizzle' in payload.db) {
const drizzle = payload.db.drizzle as DrizzleDB
// @ts-expect-error drizzle raw sql typing
await drizzle.execute(sql`drop schema public cascade;
create schema public;`)
const db = payload.db as unknown as PostgresAdapter
const drizzle = db.drizzle
const schemaName = db.schemaName || 'public'
await drizzle.execute(
sql.raw(`drop schema ${schemaName} cascade;
create schema ${schemaName};`),
)
}
})

View File

@@ -21,8 +21,13 @@ const ArrayFields: CollectionConfig = {
fields: [
{
name: 'text',
required: true,
type: 'text',
required: true,
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
{
name: 'subArray',

View File

@@ -32,26 +32,34 @@ const IndexedFields: CollectionConfig = {
fields: [
{
name: 'text',
type: 'text',
index: true,
required: true,
type: 'text',
},
{
name: 'uniqueText',
type: 'text',
unique: true,
},
{
name: 'uniqueRequiredText',
type: 'text',
defaultValue: 'uniqueRequired',
required: true,
unique: true,
},
{
name: 'point',
type: 'point',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'localizedUnique',
localized: true,
type: 'text',
localized: true,
unique: true,
},
{
@@ -64,25 +72,24 @@ const IndexedFields: CollectionConfig = {
type: 'point',
},
],
type: 'group',
},
{
type: 'collapsible',
fields: [
{
name: 'collapsibleLocalizedUnique',
localized: true,
type: 'text',
localized: true,
unique: true,
},
{
name: 'collapsibleTextUnique',
label: 'collapsibleTextUnique',
type: 'text',
label: 'collapsibleTextUnique',
unique: true,
},
],
label: 'Collapsible',
type: 'collapsible',
},
],
versions: true,

View File

@@ -249,6 +249,7 @@ describe('fields', () => {
unique: uniqueText,
},
text: 'text',
uniqueRequiredText: 'text',
uniqueText,
},
})
@@ -814,6 +815,95 @@ describe('fields', () => {
).toHaveValue(`${assertGroupText3} duplicate`)
})
})
test('should bulk update', async () => {
await Promise.all([
payload.create({
collection: 'array-fields',
data: {
title: 'for test 1',
items: [
{
text: 'test 1',
},
{
text: 'test 2',
},
],
},
}),
payload.create({
collection: 'array-fields',
data: {
title: 'for test 2',
items: [
{
text: 'test 3',
},
],
},
}),
payload.create({
collection: 'array-fields',
data: {
title: 'for test 3',
items: [
{
text: 'test 4',
},
{
text: 'test 5',
},
{
text: 'test 6',
},
],
},
}),
])
const bulkText = 'Bulk update text'
await page.goto(url.list)
await page.waitForSelector('.table > table > tbody > tr td.cell-title')
const rows = page.locator('.table > table > tbody > tr', {
has: page.locator('td.cell-title span', {
hasText: 'for test',
}),
})
const count = await rows.count()
for (let i = 0; i < count; i++) {
await rows
.nth(i)
.locator('td.cell-_select .checkbox-input__input > input[type="checkbox"]')
.check()
}
await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click()
const arrayOption = page.locator('.rs__option', {
hasText: exactText('Items'),
})
await expect(arrayOption).toBeVisible()
await arrayOption.click()
const addRowButton = page.locator('#field-items > .btn.array-field__add-row')
await expect(addRowButton).toBeVisible()
await addRowButton.click()
const targetInput = page.locator('#field-items__0__text')
await expect(targetInput).toBeVisible()
await targetInput.fill(bulkText)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.Toastify__toast--success')).toContainText(
'Updated 3 Array Fields successfully.',
)
})
})
describe('tabs', () => {

View File

@@ -424,7 +424,7 @@ describe('Fields', () => {
const definitions: Record<string, IndexDirection> = {}
const options: Record<string, IndexOptions> = {}
beforeEach(() => {
beforeAll(() => {
indexes = (payload.db as MongooseAdapter).collections[
'indexed-fields'
].schema.indexes() as [Record<string, IndexDirection>, IndexOptions]
@@ -441,8 +441,13 @@ describe('Fields', () => {
expect(definitions.text).toEqual(1)
})
it('should have unique indexes', () => {
it('should have unique sparse indexes when field is not required', () => {
expect(definitions.uniqueText).toEqual(1)
expect(options.uniqueText).toMatchObject({ sparse: true, unique: true })
})
it('should have unique indexes that are not sparse when field is required', () => {
expect(definitions.uniqueRequiredText).toEqual(1)
expect(options.uniqueText).toMatchObject({ unique: true })
})
@@ -594,6 +599,25 @@ describe('Fields', () => {
return result.error
}).toBeDefined()
})
it('should not throw validation error saving multiple null values for unique fields', async () => {
const data = {
text: 'a',
uniqueRequiredText: 'a',
// uniqueText omitted on purpose
}
await payload.create({
collection: 'indexed-fields',
data,
})
data.uniqueRequiredText = 'b'
const result = await payload.create({
collection: 'indexed-fields',
data,
})
expect(result.id).toBeDefined()
})
})
describe('array', () => {
@@ -620,6 +644,51 @@ describe('Fields', () => {
expect(doc.localized).toMatchObject(arrayDefaultValue)
})
it('should create and update localized subfields with versions', async () => {
const doc = await payload.create({
collection,
data: {
items: [
{
localizedText: 'test',
text: 'required',
},
],
localized: [
{
text: 'english',
},
],
},
})
const spanish = await payload.update({
id: doc.id,
collection,
data: {
items: [
{
id: doc.items[0].id,
localizedText: 'spanish',
text: 'required',
},
],
},
locale: 'es',
})
const result = await payload.findByID({
id: doc.id,
collection,
locale: 'all',
})
expect(doc.items[0].localizedText).toStrictEqual('test')
expect(spanish.items[0].localizedText).toStrictEqual('spanish')
expect(result.items[0].localizedText.en).toStrictEqual('test')
expect(result.items[0].localizedText.es).toStrictEqual('spanish')
})
it('should create with nested array', async () => {
const subArrayText = 'something expected'
const doc = await payload.create({

View File

@@ -20,7 +20,7 @@ export async function resetDB(_payload: Payload, collectionSlugs: string[]) {
return
}
const queries = Object.values(schema).map((table: any) => {
return sql.raw(`DELETE FROM ${table.dbName}`)
return sql.raw(`DELETE FROM ${db.schemaName ? db.schemaName + '.' : ''}${table.dbName}`)
})
await db.drizzle.transaction(async (trx) => {

View File

@@ -1,5 +1,6 @@
import type { SanitizedConfig } from '../../packages/payload/src/config/types'
import { APIError } from '../../packages/payload/errors'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
import AfterOperation from './collections/AfterOperation'
import ChainingHooks from './collections/ChainingHooks'
@@ -24,6 +25,18 @@ export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
DataHooks,
],
globals: [DataHooksGlobal],
endpoints: [
{
path: '/throw-to-after-error',
method: 'get',
handler: () => {
throw new APIError("I'm a teapot", 418)
},
},
],
hooks: {
afterError: () => console.log('Running afterError hook'),
},
onInit: async (payload) => {
await seedHooksUsers(payload)
await payload.create({

View File

@@ -462,4 +462,13 @@ describe('Hooks', () => {
expect(doc.field_globalAndField).toStrictEqual(globalAndFieldString + globalAndFieldString)
})
})
describe('config level after error hook', () => {
it('should handle error', async () => {
const response = await fetch(`${apiUrl}/throw-to-after-error`)
const body = await response.json()
expect(response.status).toEqual(418)
expect(body).toEqual({ errors: [{ message: "I'm a teapot" }] })
})
})
})

View File

@@ -157,6 +157,7 @@ describe('Localization', () => {
id,
collection: localizedPostsSlug,
data: {
localizedCheckbox: false,
title: spanishTitle,
},
locale: spanishLocale,
@@ -174,8 +175,6 @@ describe('Localization', () => {
await changeLocale(page, spanishLocale)
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
// click checkbox manually
await page.locator('#field-localizedCheckbox').click()
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
})