Compare commits

..

8 Commits

Author SHA1 Message Date
Elliot DeNolf
b3a6bfacf2 chore(release): db-postgres/0.5.0 [skip ci] 2024-01-26 13:57:34 -05:00
Elliot DeNolf
e1d9accb27 chore(release): db-mongodb/1.4.0 [skip ci] 2024-01-26 13:57:23 -05:00
Elliot DeNolf
f2f55a84cc chore(release): payload/2.9.0 [skip ci] 2024-01-26 13:55:20 -05:00
Dan Ribbens
eba53ba60a feat: forceAcceptWarning migration arg added to accept prompts (#4874)
* chore: gitignore test migrations

* feat: `forceAcceptWarning` migration args added to accept prompts

* chore: migrationDir env variable fallback

* chore: migrationDir testSuiteDir fallback

* chore: migrationDir testSuiteDir fallback fix

* chore: skip migrate down test
2024-01-26 13:48:53 -05:00
Dan Ribbens
f73d503fec fix(plugin-cloud-storage): slow get file performance large collections (#4927) 2024-01-26 13:43:55 -05:00
Dan Ribbens
6930c4e9f2 fix: upload input drawer does not show draft versions (#4903)
* chore: add field classname to upload field

* fix: upload input drawer does not show draft versions
2024-01-26 13:42:32 -05:00
Dan Ribbens
3eb681e847 fix: afterLogin hook write conflicts (#4904)
* fix: afterLogin hook conflict

* test: afterLogin hook returns for assertion

* chore: commit increment login attempt
2024-01-26 13:39:45 -05:00
Jarrod Flesch
cb4638cfa1 chore: make default views callable (#4928) 2024-01-26 13:38:36 -05:00
72 changed files with 584 additions and 615 deletions

View File

@@ -1,3 +1,20 @@
## [2.9.0](https://github.com/payloadcms/payload/compare/v2.8.2...v2.9.0) (2024-01-26)
### Features
* forceAcceptWarning migration arg added to accept prompts ([#4874](https://github.com/payloadcms/payload/issues/4874)) ([eba53ba](https://github.com/payloadcms/payload/commit/eba53ba60afd7c5d37389377ed06a9b556058d49))
### Bug Fixes
* afterLogin hook write conflicts ([#4904](https://github.com/payloadcms/payload/issues/4904)) ([3eb681e](https://github.com/payloadcms/payload/commit/3eb681e847e9c55eaaa69c22bea4f4e66c7eac36))
* **db-postgres:** migrate down error ([#4861](https://github.com/payloadcms/payload/issues/4861)) ([dfba522](https://github.com/payloadcms/payload/commit/dfba5222f3abf3f236dc9212a28e1aec7d7214d5))
* **db-postgres:** query unset relation ([#4862](https://github.com/payloadcms/payload/issues/4862)) ([8ce15c8](https://github.com/payloadcms/payload/commit/8ce15c8b07800397a50dcf790c263ed5b3cfad53))
* migrate down missing filter for latest batch ([#4860](https://github.com/payloadcms/payload/issues/4860)) ([b99d24f](https://github.com/payloadcms/payload/commit/b99d24fcfa698c493ea01c41621201abe18fabe3))
* **plugin-cloud-storage:** slow get file performance large collections ([#4927](https://github.com/payloadcms/payload/issues/4927)) ([f73d503](https://github.com/payloadcms/payload/commit/f73d503fecdfa5cefdc26ab9aad60b00563f881e))
* remove No Options dropdown from hasMany fields ([#4899](https://github.com/payloadcms/payload/issues/4899)) ([e5a7907](https://github.com/payloadcms/payload/commit/e5a7907a72c1371447ac2f71fce213ed22246092))
* upload input drawer does not show draft versions ([#4903](https://github.com/payloadcms/payload/issues/4903)) ([6930c4e](https://github.com/payloadcms/payload/commit/6930c4e9f2200853121391ad8f8df48ea66c40a4))
## [2.8.2](https://github.com/payloadcms/payload/compare/v2.8.1...v2.8.2) (2024-01-16)

View File

@@ -30,7 +30,6 @@ It's often best practice to write your Collections in separate files and then im
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`tableName`** | Custom table name for this collection when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -26,7 +26,6 @@ As with Collection configs, it's often best practice to write your Globals in se
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`tableName`** | Custom table name for this global when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -45,7 +45,6 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`tableName`** | Custom table name for the field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -72,7 +72,7 @@ Blocks are defined as separate configs of their own.
</Banner>
| Option | Description |
|----------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`slug`** \* | Identifier for this block type. Will be saved on each block as the `blockType` property. |
| **`fields`** \* | Array of fields to be stored in this block. |
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
@@ -80,7 +80,6 @@ Blocks are defined as separate configs of their own.
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
| **`tableName`** | Custom table name for this block type when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
#### Auto-generated data per block

View File

@@ -36,7 +36,6 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,8 +38,6 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
| **`tableName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
_\* An asterisk denotes that a property is required._

View File

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

View File

@@ -11,9 +11,13 @@ import type { MongooseAdapter } from '.'
/**
* Drop the current database and run all migrate up functions
*/
export async function migrateFresh(this: MongooseAdapter): Promise<void> {
export async function migrateFresh(
this: MongooseAdapter,
{ forceAcceptWarning = false }: { forceAcceptWarning?: boolean },
): Promise<void> {
const { payload } = this
if (!forceAcceptWarning) {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
@@ -31,6 +35,7 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
if (!acceptWarning) {
process.exit(0)
}
}
payload.logger.info({
msg: `Dropping database.`,

View File

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

View File

@@ -1,9 +1,10 @@
import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export const create: Create = async function create(
this: PostgresAdapter,
@@ -18,8 +19,8 @@ export const create: Create = async function create(
db,
fields: collection.fields,
operation: 'create',
tableName: toSnakeCase(collectionSlug),
req,
tableName: getTableName(collection),
})
return result

View File

@@ -1,10 +1,11 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function createGlobal<T extends TypeWithID>(
this: PostgresAdapter,
@@ -19,8 +20,8 @@ export async function createGlobal<T extends TypeWithID>(
db,
fields: globalConfig.fields,
operation: 'create',
tableName: toSnakeCase(slug),
req,
tableName: getTableName(globalConfig),
})
return result

View File

@@ -1,14 +1,14 @@
import type { TypeWithVersion } from 'payload/database'
import { type CreateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
@@ -16,7 +16,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const globalTableName = getTableName(global)
const globalTableName = toSnakeCase(globalSlug)
const tableName = `_${globalTableName}_v`
const result = await upsertRow<TypeWithVersion<T>>({
@@ -29,8 +29,8 @@ export async function createGlobalVersion<T extends TypeWithID>(
db,
fields: buildVersionGlobalFields(global),
operation: 'create',
req,
tableName,
req,
})
const table = this.tables[tableName]

View File

@@ -53,7 +53,7 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
export const createMigration: CreateMigration = async function createMigration(
this: PostgresAdapter,
{ migrationName, payload },
{ forceAcceptWarning, migrationName, payload },
) {
const dir = payload.db.migrationDir
if (!fs.existsSync(dir)) {
@@ -95,7 +95,7 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
if (!sqlStatementsUp.length && !sqlStatementsDown.length) {
if (!sqlStatementsUp.length && !sqlStatementsDown.length && !forceAcceptWarning) {
const { confirm: shouldCreateBlankMigration } = await prompts(
{
name: 'confirm',

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function createVersion<T extends TypeWithID>(
this: PostgresAdapter,
@@ -21,7 +21,7 @@ export async function createVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const collectionTableName = getTableName(collection)
const collectionTableName = toSnakeCase(collectionSlug)
const tableName = `_${collectionTableName}_v`
const result = await upsertRow<TypeWithVersion<T>>({
@@ -35,8 +35,8 @@ export async function createVersion<T extends TypeWithID>(
db,
fields: buildVersionCollectionFields(collection),
operation: 'create',
req,
tableName,
req,
})
const table = this.tables[tableName]

View File

@@ -2,11 +2,11 @@ import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
@@ -14,7 +14,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = getTableName(collectionConfig)
const tableName = toSnakeCase(collection)
const result = await findMany({
adapter: this,

View File

@@ -1,12 +1,13 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildFindManyArgs } from './find/buildFindManyArgs'
import buildQuery from './queries/buildQuery'
import { transform } from './transform/read'
import { getTableName } from './utilities/getTableName'
export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter,
@@ -14,7 +15,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = getTableName(collectionConfig)
const tableName = toSnakeCase(collection)
const { where } = await buildQuery({
adapter: this,

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,7 +16,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${getTableName(collectionConfig)}_v`
const tableName = `_${toSnakeCase(collection)}_v`
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -1,10 +1,11 @@
import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const find: Find = async function find(
this: PostgresAdapter,
@@ -31,7 +32,7 @@ export const find: Find = async function find(
pagination,
req,
sort,
tableName: getTableName(collectionConfig),
tableName: toSnakeCase(collection),
where: whereArg,
})
}

View File

@@ -7,8 +7,6 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types'
import type { Result } from './buildFindManyArgs'
import { getTableName } from '../utilities/getTableName'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -80,7 +78,7 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = `${currentTableName}_${getTableName(field)}`
const arrayTableName = `${currentTableName}_${toSnakeCase(field.name)}`
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
@@ -130,7 +128,7 @@ export const traverseFields = ({
with: {},
}
const tableName = `${topLevelTableName}_blocks_${getTableName(block)}`
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
topLevelArgs.with[blockKey] = withBlock

View File

@@ -1,16 +1,17 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ locale, req, slug, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = getTableName(globalConfig)
const tableName = toSnakeCase(slug)
const {
docs: [doc],

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,7 +27,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = `_${getTableName(globalConfig)}_v`
const tableName = `_${toSnakeCase(global)}_v`
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,17 +1,17 @@
import type { FindOneArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: incomingWhere }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName(collectionConfig)
const { docs } = await findMany({
adapter: this,
@@ -22,7 +22,7 @@ export async function findOne<T extends TypeWithID>(
pagination: false,
req,
sort: undefined,
tableName,
tableName: toSnakeCase(collection),
where: incomingWhere,
})

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,7 +25,7 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = `_${getTableName(collectionConfig)}_v`
const tableName = `_${toSnakeCase(collection)}_v`
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -4,12 +4,12 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
import { getConfigIDType } from './schema/getConfigIDType'
import { getTableName } from './utilities/getTableName'
export const init: Init = async function init(this: PostgresAdapter) {
if (this.payload.config.localization) {
@@ -20,7 +20,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = getTableName(collection)
const tableName = toSnakeCase(collection.slug)
buildTable({
adapter: this,
@@ -55,7 +55,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
})
this.payload.config.globals.forEach((global) => {
const tableName = getTableName(global)
const tableName = toSnakeCase(global.slug)
buildTable({
adapter: this,

View File

@@ -14,9 +14,13 @@ import { parseError } from './utilities/parseError'
/**
* Drop the current database and run all migrate up functions
*/
export async function migrateFresh(this: PostgresAdapter): Promise<void> {
export async function migrateFresh(
this: PostgresAdapter,
{ forceAcceptWarning = false },
): Promise<void> {
const { payload } = this
if (forceAcceptWarning === false) {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
@@ -34,6 +38,7 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
if (!acceptWarning) {
process.exit(0)
}
}
payload.logger.info({
msg: `Dropping database.`,

View File

@@ -13,8 +13,6 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
import { getTableName } from '../utilities/getTableName'
type Constraint = {
columnName: string
table: GenericTable
@@ -201,7 +199,7 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = `${tableName}_${getTableName(field)}`
newTableName = `${tableName}_${toSnakeCase(field.name)}`
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -241,7 +239,7 @@ export const getTableColumnFromPath = ({
let blockTableColumn: TableColumn
let newTableName: string
const hasBlockField = field.blocks.some((block) => {
newTableName = `${tableName}_blocks_${getTableName(block)}`
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
@@ -331,10 +329,9 @@ export const getTableColumnFromPath = ({
let newAliasTable
if (typeof field.relationTo === 'string') {
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName(relationshipConfig)
newTableName = `${toSnakeCase(field.relationTo)}`
// parent to relationship join table
relationshipFields = relationshipConfig.fields
relationshipFields = adapter.payload.collections[field.relationTo].config.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -353,10 +350,7 @@ export const getTableColumnFromPath = ({
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName(
adapter.payload.collections[relationTo].config,
)}_id"`,
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`,
)
return {
constraints,

View File

@@ -2,9 +2,9 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import { findMany } from './find/findMany'
import { getTableName } from './utilities/getTableName'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
@@ -17,7 +17,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${getTableName(collectionConfig)}_v`
const tableName = `_${toSnakeCase(collection)}_v`
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -15,10 +15,10 @@ import {
varchar,
} from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, PostgresAdapter } from '../types'
import { getTableName } from '../utilities/getTableName'
import { getConfigIDType } from './getConfigIDType'
import { parentIDColumnMap } from './parentIDColumnMap'
import { traverseFields } from './traverseFields'
@@ -296,12 +296,11 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName(relationshipConfig)
const formattedRelationTo = toSnakeCase(relationTo)
let colType = 'integer'
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const relatedCollectionCustomID = adapter.payload.collections[
relationTo
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
@@ -338,7 +337,7 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = getTableName(adapter.payload.collections[relationTo].config)
const relatedTableName = toSnakeCase(relationTo)
const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], {
fields: [relationshipsTable[idColumnName]],

View File

@@ -23,7 +23,6 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, PostgresAdapter } from '../types'
import { getTableName } from '../utilities/getTableName'
import { hasLocalesTable } from '../utilities/hasLocalesTable'
import { buildTable } from './build'
import { createIndex } from './createIndex'
@@ -215,7 +214,7 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = `enum_${newTableName}_${getTableName(field, { prefer: 'enumName' })}`
const enumName = `enum_${newTableName}_${toSnakeCase(field.name)}`
adapter.enums[enumName] = pgEnum(
enumName,
@@ -229,7 +228,7 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = `${newTableName}_${getTableName(field, { prefer: 'tableName' })}`
const selectTableName = `${newTableName}_${toSnakeCase(field.name)}`
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -293,7 +292,7 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${getTableName(field)}`
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -371,7 +370,7 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${getTableName(block)}`
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),

View File

@@ -1,10 +1,11 @@
/* eslint-disable no-param-reassign */
import type { BlockField } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { BlockRowToInsert, RelationshipToDelete } from './types'
import { getTableName } from '../../utilities/getTableName'
import { traverseFields } from './traverseFields'
type Args = {
@@ -45,7 +46,7 @@ export const transformBlocks = ({
if (typeof blockRow.blockType !== 'string') return
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
if (!matchedBlock) return
const blockType = getTableName(matchedBlock)
const blockType = toSnakeCase(blockRow.blockType)
if (!blocks[blockType]) blocks[blockType] = []

View File

@@ -7,7 +7,6 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { getTableName } from '../../utilities/getTableName'
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
import { transformArray } from './array'
import { transformBlocks } from './blocks'
@@ -89,7 +88,7 @@ export const traverseFields = ({
let fieldData: unknown
if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${getTableName(field)}`
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`
fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name]
}
@@ -148,8 +147,8 @@ export const traverseFields = ({
}
if (field.type === 'blocks') {
field.blocks.forEach((block) => {
blocksToDelete.add(getTableName(block))
field.blocks.forEach(({ slug }) => {
blocksToDelete.add(toSnakeCase(slug))
})
if (field.localized) {

View File

@@ -10,22 +10,15 @@ import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-post
import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { BaseCollectionConfig } from 'payload/dist/collections/config/types'
import type {
BaseArrayField,
BaseBlock,
BaseRadioField,
BaseSelectField,
} from 'payload/dist/fields/config/types'
import type { BaseGlobalConfig } from 'payload/dist/globals/config/types'
import type { PayloadRequest } from 'payload/types'
import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
logger?: DrizzleConfig['logger']
push?: boolean
}
@@ -57,13 +50,13 @@ export type DrizzleTransaction = PgTransaction<
export type PostgresAdapter = BaseDatabaseAdapter & {
drizzle: DrizzleDB
logger: DrizzleConfig['logger']
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>>
logger: DrizzleConfig['logger']
pool: Pool
poolOptions: Args['pool']
push: boolean
@@ -81,8 +74,8 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload }
export type MigrateDownArgs = { payload: Payload }
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
declare module 'payload' {
export interface DatabaseAdapter
@@ -105,51 +98,3 @@ declare module 'payload' {
tables: Record<string, GenericTable>
}
}
declare module 'payload/types' {
export interface CollectionConfig extends BaseCollectionConfig {
/**
* Customize the SQL table name
*/
tableName?: string
}
export interface GlobalConfig extends BaseGlobalConfig {
/**
* Customize the SQL table name
*/
tableName?: string
}
export interface ArrayField extends BaseArrayField {
/**
* Customize the SQL table name
*/
tableName?: string
}
export interface RadioField extends BaseRadioField {
/**
* Customize the SQL enum name
*/
enumName?: string
}
export interface Block extends BaseBlock {
/**
* Customize the SQL table name
*/
tableName?: string
}
export interface SelectField extends BaseSelectField {
/**
* Customize the SQL enum name
*/
enumName?: string
/**
* Customize the SQL table name when using `hasMany: true`
*/
tableName?: string
}
}

View File

@@ -1,12 +1,12 @@
import type { UpdateOne } from 'payload/database'
import type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods'
import { chainMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export const updateOne: UpdateOne = async function updateOne(
this: PostgresAdapter,
@@ -14,7 +14,7 @@ export const updateOne: UpdateOne = async function updateOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = getTableName(collection)
const tableName = toSnakeCase(collectionSlug)
const whereToUse = whereArg || { id: { equals: id } }
const { joinAliases, joins, selectFields, where } = await buildQuery({
@@ -70,8 +70,8 @@ export const updateOne: UpdateOne = async function updateOne(
db,
fields: collection.fields,
operation: 'update',
tableName: toSnakeCase(collectionSlug),
req,
tableName,
})
return result

View File

@@ -1,10 +1,11 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function updateGlobal<T extends TypeWithID>(
this: PostgresAdapter,
@@ -12,7 +13,7 @@ export async function updateGlobal<T extends TypeWithID>(
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = getTableName(globalConfig)
const tableName = toSnakeCase(slug)
const existingGlobal = await db.query[tableName].findFirst({})
@@ -22,8 +23,8 @@ export async function updateGlobal<T extends TypeWithID>(
data,
db,
fields: globalConfig.fields,
req,
tableName,
req,
})
return result

View File

@@ -2,12 +2,12 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function updateGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
@@ -25,7 +25,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${getTableName(globalConfig)}_v`
const tableName = `_${toSnakeCase(global)}_v`
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({
@@ -43,9 +43,9 @@ export async function updateGlobalVersion<T extends TypeWithID>(
db,
fields,
operation: 'update',
req,
tableName,
where,
req,
})
return result

View File

@@ -2,12 +2,12 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { upsertRow } from './upsertRow'
import { getTableName } from './utilities/getTableName'
export async function updateVersion<T extends TypeWithID>(
this: PostgresAdapter,
@@ -23,7 +23,7 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${getTableName(collectionConfig)}_v`
const tableName = `_${toSnakeCase(collection)}_v`
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({
@@ -41,9 +41,9 @@ export async function updateVersion<T extends TypeWithID>(
db,
fields,
operation: 'update',
req,
tableName,
where,
req,
})
return result

View File

@@ -1,21 +0,0 @@
import toSnakeCase from 'to-snake-case'
type Options = {
prefer: 'enumName' | 'tableName'
}
type EntityConfig = {
enumName?: string
name?: string
slug?: string
tableName?: string
}
export const getTableName = (
{ name, enumName, slug, tableName }: EntityConfig,
{ prefer }: Options = { prefer: 'tableName' },
) => {
const generated = toSnakeCase(name ?? slug)
const custom = prefer === 'tableName' ? tableName ?? enumName : enumName ?? tableName
return custom ?? generated
}

View File

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

View File

@@ -20,7 +20,7 @@ export const getCustomViews = (args: {
? collection?.admin?.components?.views?.Edit
: undefined
const defaultViewKeys = Object.keys(defaultCollectionViews)
const defaultViewKeys = Object.keys(defaultCollectionViews())
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) {
@@ -38,7 +38,7 @@ export const getCustomViews = (args: {
? global?.admin?.components?.views?.Edit
: undefined
const defaultViewKeys = Object.keys(defaultGlobalViews)
const defaultViewKeys = Object.keys(defaultGlobalViews())
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) {

View File

@@ -133,9 +133,10 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => {
const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
const { slug, admin: { listSearchableFields } = {}, versions } = selectedCollectionConfig
const params: {
cacheBust?: number
draft?: string
limit?: number
page?: number
search?: string
@@ -172,6 +173,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
if (sort) params.sort = sort
if (cacheBust) params.cacheBust = cacheBust
if (copyOfWhere) params.where = copyOfWhere
if (versions?.drafts) params.draft = 'true'
setParams(params)
}, [

View File

@@ -137,6 +137,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
fieldBaseClass,
baseClass,
className,
`field-${path.replace(/\./g, '__')}`,
showError && 'error',
readOnly && 'read-only',
]

View File

@@ -17,9 +17,9 @@ export type globalViewType =
| 'Version'
| 'Versions'
export const defaultGlobalViews: {
export const defaultGlobalViews = (): {
[key in globalViewType]: React.ComponentType<any>
} = {
} => ({
API,
Default: DefaultGlobalEdit,
LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultGlobalViews: {
Relationships: null,
Version: VersionView,
Versions: VersionsView,
}
})
export const CustomGlobalComponent = (
args: GlobalEditViewProps & {
@@ -43,18 +43,14 @@ export const CustomGlobalComponent = (
// For example, the Edit view:
// 1. Edit?.Default
// 2. Edit?.Default?.Component
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view]
: typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function'
? // @ts-ignore
Edit[view].Component
: defaultGlobalViews[view]
? Edit[view].Component
: defaultGlobalViews()[view]
if (Component) {
return <Component {...args} />

View File

@@ -17,9 +17,9 @@ export type collectionViewType =
| 'Version'
| 'Versions'
export const defaultCollectionViews: {
export const defaultCollectionViews = (): {
[key in collectionViewType]: React.ComponentType<any>
} = {
} => ({
API,
Default: DefaultCollectionEdit,
LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultCollectionViews: {
Relationships: null,
Version: VersionView,
Versions: VersionsView,
}
})
export const CustomCollectionComponent = (
args: CollectionEditViewProps & {
@@ -43,18 +43,15 @@ export const CustomCollectionComponent = (
// For example, the Edit view:
// 1. Edit?.Default
// 2. Edit?.Default?.Component
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view]
: typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function'
? // @ts-ignore
Edit[view].Component
: defaultCollectionViews[view]
? Edit[view].Component
: defaultCollectionViews()[view]
if (Component) {
return <Component {...args} />

View File

@@ -18,8 +18,8 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'
import isLocked from '../isLocked'
import { authenticateLocalStrategy } from '../strategies/local/authenticate'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts'
import { getFieldsToSign } from './getFieldsToSign'
import unlock from './unlock'
export type Result = {
exp?: number
@@ -115,16 +115,16 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
})
}
if (shouldCommit) await commitTransaction(req)
throw new AuthenticationError(req.t)
}
if (maxLoginAttemptsEnabled) {
await unlock({
collection: {
config: collectionConfig,
},
data,
overrideAccess: true,
await resetLoginAttempts({
collection: collectionConfig,
doc: user,
payload: req.payload,
req,
})
}

View File

@@ -52,5 +52,6 @@ export const incrementLoginAttempts = async ({
id: doc.id,
collection: collection.slug,
data,
req,
})
}

View File

@@ -15,6 +15,7 @@ export const resetLoginAttempts = async ({
payload,
req,
}: Args): Promise<void> => {
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) return
await payload.update({
id: doc.id,
collection: collection.slug,
@@ -22,6 +23,7 @@ export const resetLoginAttempts = async ({
lockUntil: null,
loginAttempts: 0,
},
overrideAccess: true,
req,
})
}

View File

@@ -27,7 +27,7 @@ const availableCommands = [
const availableCommandsMsg = `Available commands: ${availableCommands.join(', ')}`
export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
const { _: args, file, help } = parsedArgs
const { _: args, file, forceAcceptWarning, help } = parsedArgs
if (help) {
// eslint-disable-next-line no-console
console.log(`\n\n${availableCommandsMsg}\n`) // Avoid having to init payload to get the logger
@@ -74,11 +74,16 @@ export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
await adapter.migrateReset()
break
case 'migrate:fresh':
await adapter.migrateFresh()
await adapter.migrateFresh({ forceAcceptWarning })
break
case 'migrate:create':
try {
await adapter.createMigration({ file, migrationName: args[1], payload })
await adapter.createMigration({
file,
forceAcceptWarning,
migrationName: args[1],
payload,
})
} catch (err) {
throw new Error(`Error creating migration: ${err.message}`)
}

View File

@@ -159,7 +159,6 @@ const collectionSchema = joi.object().keys({
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
slug: joi.string().required(),
tableName: joi.string(),
timestamps: joi.boolean(),
typescript: joi.object().keys({
interface: joi.string(),

View File

@@ -332,7 +332,8 @@ export type CollectionAdminOptions = {
useAsTitle?: string
}
export type BaseCollectionConfig = {
/** Manage all aspects of a data collection */
export type CollectionConfig = {
/**
* Access control
*/
@@ -425,7 +426,6 @@ export type BaseCollectionConfig = {
* @default false // disable uploads
*/
upload?: IncomingUploadType | boolean
/**
* Customize the handling of incoming file uploads
*
@@ -434,9 +434,6 @@ export type BaseCollectionConfig = {
versions?: IncomingCollectionVersions | boolean
}
/** Manage all aspects of a data collection */
export type CollectionConfig = BaseCollectionConfig
export interface SanitizedCollectionConfig
extends Omit<
DeepRequired<CollectionConfig>,

View File

@@ -39,7 +39,7 @@ export function createDatabaseAdapter<T extends BaseDatabaseAdapter>(
createMigration,
migrate,
migrateDown,
migrateFresh: async () => null,
migrateFresh: async ({ forceAcceptWarning = null }) => null,
migrateRefresh,
migrateReset,
migrateStatus,

View File

@@ -78,7 +78,7 @@ export interface BaseDatabaseAdapter {
/**
* Drop the current database and run all migrate up functions
*/
migrateFresh: () => Promise<void>
migrateFresh: (args: { forceAcceptWarning?: boolean }) => Promise<void>
/**
* Run all migration down functions before running up
*/
@@ -138,6 +138,10 @@ export type Destroy = (payload: Payload) => Promise<void>
export type CreateMigration = (args: {
file?: string
/**
* Skips the prompt asking to create empty migrations
*/
forceAcceptWarning?: boolean
migrationName?: string
payload: Payload
}) => Promise<void>

View File

@@ -196,7 +196,6 @@ export const select = baseField.keys({
defaultValue: joi
.alternatives()
.try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()),
enumName: joi.string(),
hasMany: joi.boolean().default(false),
options: joi
.array()
@@ -213,7 +212,6 @@ export const select = baseField.keys({
),
)
.required(),
tableName: joi.string(),
type: joi.string().valid('select').required(),
})
@@ -227,7 +225,6 @@ export const radio = baseField.keys({
layout: joi.string().valid('vertical', 'horizontal'),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
enumName: joi.string(),
options: joi
.array()
.min(1)
@@ -318,7 +315,6 @@ export const array = baseField.keys({
}),
maxRows: joi.number(),
minRows: joi.number(),
tableName: joi.string(),
type: joi.string().valid('array').required(),
})
@@ -420,7 +416,6 @@ export const blocks = baseField.keys({
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
slug: joi.string().required(),
tableName: joi.string(),
custom: joi.object().pattern(joi.string(), joi.any()),
}),
)

View File

@@ -433,7 +433,7 @@ export type JSONField = Omit<FieldBase, 'admin'> & {
type: 'json'
}
export type BaseSelectField = FieldBase & {
export type SelectField = FieldBase & {
admin?: Admin & {
components?: {
Error?: React.ComponentType<ErrorProps>
@@ -447,8 +447,6 @@ export type BaseSelectField = FieldBase & {
type: 'select'
}
export type SelectField = BaseSelectField
type SharedRelationshipProperties = FieldBase & {
filterOptions?: FilterOptions
hasMany?: boolean
@@ -530,7 +528,7 @@ export type RichTextField<
type: 'richText'
} & ExtraProperties
export type BaseArrayField = FieldBase & {
export type ArrayField = FieldBase & {
admin?: Admin & {
components?: {
RowLabel?: RowLabel
@@ -551,9 +549,7 @@ export type BaseArrayField = FieldBase & {
type: 'array'
}
export type ArrayField = BaseArrayField
export type BaseRadioField = FieldBase & {
export type RadioField = FieldBase & {
admin?: Admin & {
components?: {
Error?: React.ComponentType<ErrorProps>
@@ -565,27 +561,6 @@ export type BaseRadioField = FieldBase & {
type: 'radio'
}
export type RadioField = BaseRadioField
export type BaseBlock = {
fields: Field[]
/** @deprecated - please migrate to the interfaceName property instead. */
graphQL?: {
singularName?: string
}
imageAltText?: string
imageURL?: string
/** Customize generated GraphQL and Typescript schema names.
* The slug is used by default.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique among collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
labels?: Labels
slug: string
}
export type Block = {
fields: Field[]
/** @deprecated - please migrate to the interfaceName property instead. */

View File

@@ -65,7 +65,6 @@ const globalSchema = joi
}),
label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
slug: joi.string().required(),
tableName: joi.string(),
typescript: joi.object().keys({
interface: joi.string(),
}),

View File

@@ -160,7 +160,7 @@ export type GlobalAdminOptions = {
preview?: GeneratePreviewURL
}
export type BaseGlobalConfig = {
export type GlobalConfig = {
access?: {
read?: Access
readDrafts?: Access
@@ -198,8 +198,6 @@ export type BaseGlobalConfig = {
versions?: IncomingGlobalVersions | boolean
}
export type GlobalConfig = BaseGlobalConfig
export interface SanitizedGlobalConfig
extends Omit<DeepRequired<GlobalConfig>, 'endpoints' | 'fields' | 'versions'> {
endpoints: Omit<Endpoint, 'root'>[] | false

View File

@@ -10,6 +10,9 @@ export async function getFilePrefix({
const imageSizes = (collection?.upload as IncomingUploadType)?.imageSizes || []
const files = await req.payload.find({
collection: collection.slug,
depth: 0,
limit: 1,
pagination: false,
where: {
or: [
{

View File

@@ -4,10 +4,10 @@ import { usersCollectionSlug } from '../slugs'
export const Users: CollectionConfig = {
slug: usersCollectionSlug,
auth: true,
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
{
name: 'textField',

View File

@@ -16,13 +16,21 @@ const bundlerAdapters = {
webpack: webpackBundler(),
}
const [testSuiteDir] = process.argv.slice(4)
const migrationDir = path.resolve(
(process.env.PAYLOAD_CONFIG_PATH
? path.join(process.env.PAYLOAD_CONFIG_PATH, '..')
: testSuiteDir) || __dirname,
'migrations',
)
const databaseAdapters = {
mongoose: mongooseAdapter({
migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'),
migrationDir,
url: 'mongodb://127.0.0.1/payloadtests',
}),
postgres: postgresAdapter({
migrationDir: path.resolve(__dirname, '../packages/db-postgres/migrations'),
migrationDir,
pool: {
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
},
@@ -33,6 +41,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
const [name] = process.argv.slice(2)
const config: Config = {
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
editor: slateEditor({}),
rateLimit: {
max: 9999999999,
@@ -40,7 +49,6 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
},
telemetry: false,
...testConfig,
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
}
config.admin = {

1
test/database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
migrations

View File

@@ -4,14 +4,16 @@ import { devUser } from '../credentials'
export default buildConfigWithDefaults({
collections: [
{
slug: 'posts',
fields: [
{
name: 'title',
required: true,
type: 'text',
required: true,
},
{
name: 'throwAfterChange',
type: 'checkbox',
defaultValue: false,
hooks: {
afterChange: [
@@ -22,17 +24,16 @@ export default buildConfigWithDefaults({
},
],
},
type: 'checkbox',
},
],
slug: 'posts',
},
{
slug: 'relation-a',
fields: [
{
name: 'relationship',
relationTo: 'relation-b',
type: 'relationship',
relationTo: 'relation-b',
},
{
name: 'richText',
@@ -43,14 +44,14 @@ export default buildConfigWithDefaults({
plural: 'Relation As',
singular: 'Relation A',
},
slug: 'relation-a',
},
{
slug: 'relation-b',
fields: [
{
name: 'relationship',
relationTo: 'relation-a',
type: 'relationship',
relationTo: 'relation-a',
},
{
name: 'richText',
@@ -61,97 +62,8 @@ export default buildConfigWithDefaults({
plural: 'Relation Bs',
singular: 'Relation B',
},
slug: 'relation-b',
},
{
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
localized: true,
type: 'text',
},
{
name: 'relationship',
hasMany: true,
relationTo: 'relation-a',
type: 'relationship',
},
{
name: 'select',
enumName: 'selectEnum',
hasMany: true,
options: ['a', 'b', 'c'],
tableName: 'customSelect',
type: 'select',
},
{
name: 'radio',
enumName: 'radioEnum',
options: ['a', 'b', 'c'],
type: 'select',
},
{
name: 'array',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
localized: true,
type: 'text',
},
],
tableName: 'customArrays',
type: 'array',
},
{
name: 'blocks',
blocks: [
{
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
localized: true,
type: 'text',
},
],
slug: 'block',
tableName: 'customBlocks',
},
],
type: 'blocks',
},
],
slug: 'custom-schema',
tableName: 'customs',
},
],
globals: [
{
fields: [
{
name: 'text',
type: 'text',
},
],
slug: 'global',
tableName: 'customGlobal',
},
],
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
onInit: async (payload) => {
await payload.create({
collection: 'users',

View File

@@ -1,14 +1,19 @@
import { sql } from 'drizzle-orm'
import fs from 'fs'
import { GraphQLClient } from 'graphql-request'
import path from 'path'
import type { PostgresAdapter } from '../../packages/db-postgres/src/types'
import type { DrizzleDB } 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'
import payload from '../../packages/payload/src'
import { migrate } from '../../packages/payload/src/bin/migrate'
import { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction'
import { initTransaction } from '../../packages/payload/src/utilities/initTransaction'
import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import removeFiles from '../helpers/removeFiles'
describe('database', () => {
let serverURL
@@ -17,16 +22,12 @@ describe('database', () => {
const collection = 'posts'
const title = 'title'
let user: TypeWithID & Record<string, unknown>
let checkSchema = true
beforeAll(async () => {
const init = await initPayloadTest({ __dirname, init: { local: false } })
serverURL = init.serverURL
const url = `${serverURL}/api/graphql`
client = new GraphQLClient(url)
if (payload.db.name === 'mongoose') {
checkSchema = false
}
const loginResult = await payload.login({
collection: 'users',
@@ -40,44 +41,99 @@ describe('database', () => {
user = loginResult.user
})
describe('schema', () => {
it('should use custom tableNames', () => {
const db: PostgresAdapter = payload.db
expect(db).toBeDefined()
if (checkSchema) {
// collection
expect(db.tables['customs']).toBeDefined()
// collection relationships
expect(db.tables.customs_rels).toBeDefined()
// collection localized
expect(db.tables.customs_locales).toBeDefined()
// global
expect(db.tables.customGlobal).toBeDefined()
// select
expect(db.tables.customs_customSelect).toBeDefined()
// array
expect(db.tables.customs_customArrays).toBeDefined()
// array localized
expect(db.tables.customs_customArrays_locales).toBeDefined()
// blocks
expect(db.tables.customs_blocks_customBlocks).toBeDefined()
// localized blocks
expect(db.tables.customs_blocks_customBlocks_locales).toBeDefined()
// enum names
expect(db.enums.enum_customs_selectEnum).toBeDefined()
expect(db.enums.enum_customs_radioEnum).toBeDefined()
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;`)
}
})
afterAll(() => {
removeFiles(path.normalize(payload.db.migrationDir))
})
it('should run migrate:create', async () => {
const args = {
_: ['migrate:create', 'test'],
forceAcceptWarning: true,
}
await migrate(args)
// read files names in migrationsDir
const migrationFile = path.normalize(fs.readdirSync(payload.db.migrationDir)[0])
expect(migrationFile).toContain('_test')
})
it('should run migrate', async () => {
const args = {
_: ['migrate'],
}
await migrate(args)
const { docs } = await payload.find({
collection: 'payload-migrations',
})
const migration = docs[0]
expect(migration.name).toContain('_test')
expect(migration.batch).toStrictEqual(1)
})
it('should run migrate:status', async () => {
let error
const args = {
_: ['migrate:status'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
it('should run migrate:fresh', async () => {
const args = {
_: ['migrate:fresh'],
forceAcceptWarning: true,
}
await migrate(args)
const { docs } = await payload.find({
collection: 'payload-migrations',
})
const migration = docs[0]
expect(migration.name).toContain('_test')
expect(migration.batch).toStrictEqual(1)
})
// known issue: https://github.com/payloadcms/payload/issues/4597
it.skip('should run migrate:down', async () => {
let error
const args = {
_: ['migrate:down'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
// known issue: https://github.com/payloadcms/payload/issues/4597
it.skip('should run migrate:refresh', async () => {
let error
const args = {
_: ['migrate:refresh'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
})
describe('transactions', () => {

View File

@@ -2,13 +2,14 @@ import fs from 'fs'
import path from 'path'
import { generateGraphQLSchema } from '../packages/payload/src/bin/generateGraphQLSchema'
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
const [testConfigDir] = process.argv.slice(2)
let testDir
if (testConfigDir) {
testDir = path.resolve(__dirname, testConfigDir)
setPaths(testDir)
setTestEnvPaths(testDir)
generateGraphQLSchema()
} else {
// Generate graphql schema for entire directory
@@ -18,17 +19,7 @@ if (testConfigDir) {
.filter((f) => f.isDirectory())
.forEach((dir) => {
const suiteDir = path.resolve(testDir, dir.name)
const configFound = setPaths(suiteDir)
const configFound = setTestEnvPaths(suiteDir)
if (configFound) generateGraphQLSchema()
})
}
// Set config path and TS output path using test dir
function setPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
return true
}
return false
}

View File

@@ -2,13 +2,14 @@ import fs from 'fs'
import path from 'path'
import { generateTypes } from '../packages/payload/src/bin/generateTypes'
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
const [testConfigDir] = process.argv.slice(2)
let testDir
if (testConfigDir) {
testDir = path.resolve(__dirname, testConfigDir)
setPaths(testDir)
setTestEnvPaths(testDir)
generateTypes()
} else {
// Generate types for entire directory
@@ -18,19 +19,7 @@ if (testConfigDir) {
.filter((f) => f.isDirectory())
.forEach((dir) => {
const suiteDir = path.resolve(testDir, dir.name)
const configFound = setPaths(suiteDir)
const configFound = setTestEnvPaths(suiteDir)
if (configFound) generateTypes()
})
}
// Set config path and TS output path using test dir
function setPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
const outputPath = path.resolve(dir, 'payload-types.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
return true
}
return false
}

View File

@@ -0,0 +1,14 @@
// Set config path and TS output path using test dir
import fs from 'fs'
import path from 'path'
export function setTestEnvPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
const outputPath = path.resolve(dir, 'payload-types.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
return true
}
return false
}

View File

@@ -0,0 +1,12 @@
import type { AfterLoginHook } from '../../../../packages/payload/src/collections/config/types'
export const afterLoginHook: AfterLoginHook = async ({ req, user }) => {
return req.payload.update({
id: user.id,
collection: 'hooks-users',
data: {
afterLoginHook: true,
},
req,
})
}

View File

@@ -6,8 +6,9 @@ import type { Payload } from '../../../../packages/payload/src/payload'
import { AuthenticationError } from '../../../../packages/payload/src/errors'
import { devUser, regularUser } from '../../../credentials'
import { afterLoginHook } from './afterLoginHook'
const beforeLoginHook: BeforeLoginHook = ({ user, req }) => {
const beforeLoginHook: BeforeLoginHook = ({ req, user }) => {
const isAdmin = user.roles.includes('admin') ? user : undefined
if (!isAdmin) {
throw new AuthenticationError(req.t)
@@ -33,16 +34,21 @@ const Users: CollectionConfig = {
fields: [
{
name: 'roles',
label: 'Role',
type: 'select',
options: ['admin', 'user'],
defaultValue: 'user',
hasMany: true,
label: 'Role',
options: ['admin', 'user'],
required: true,
saveToJWT: true,
hasMany: true,
},
{
name: 'afterLoginHook',
type: 'checkbox',
},
],
hooks: {
afterLogin: [afterLoginHook],
beforeLogin: [beforeLoginHook],
},
}

View File

@@ -27,7 +27,7 @@ describe('Hooks', () => {
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
const config = await configPromise
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug })
client = new RESTClient(config, { defaultSlug: transformSlug, serverURL })
apiUrl = `${serverURL}/api`
})
@@ -43,8 +43,8 @@ describe('Hooks', () => {
const doc = await payload.create({
collection: transformSlug,
data: {
transform: [2, 8],
localizedTransform: [2, 8],
transform: [2, 8],
},
})
@@ -59,15 +59,15 @@ describe('Hooks', () => {
doc = await payload.create({
collection: hooksSlug,
data: {
fieldBeforeValidate: false,
collectionBeforeValidate: false,
fieldBeforeChange: false,
collectionBeforeChange: false,
fieldAfterChange: false,
collectionAfterChange: false,
collectionBeforeRead: false,
fieldAfterRead: false,
collectionAfterRead: false,
collectionBeforeChange: false,
collectionBeforeRead: false,
collectionBeforeValidate: false,
fieldAfterChange: false,
fieldAfterRead: false,
fieldBeforeChange: false,
fieldBeforeValidate: false,
},
})
@@ -84,10 +84,10 @@ describe('Hooks', () => {
const document: NestedAfterReadHook = await payload.create({
collection: nestedAfterReadHooksSlug,
data: {
text: 'ok',
group: {
array: [{ input: 'input' }],
},
text: 'ok',
},
})
@@ -106,7 +106,6 @@ describe('Hooks', () => {
const document = await payload.create({
collection: nestedAfterReadHooksSlug,
data: {
text: 'ok',
group: {
array: [
{
@@ -117,12 +116,13 @@ describe('Hooks', () => {
shouldPopulate: relation.id,
},
},
text: 'ok',
},
})
const retrievedDoc = await payload.findByID({
collection: nestedAfterReadHooksSlug,
id: document.id,
collection: nestedAfterReadHooksSlug,
})
expect(retrievedDoc.group.array[0].shouldPopulate.title).toEqual(relation.title)
@@ -138,8 +138,8 @@ describe('Hooks', () => {
})
const retrievedDoc = await payload.findByID({
collection: chainingHooksSlug,
id: document.id,
collection: chainingHooksSlug,
})
expect(retrievedDoc.text).toEqual('ok!!')
@@ -189,15 +189,15 @@ describe('Hooks', () => {
const [updatedDoc1, updatedDoc2] = await Promise.all([
await payload.update({
collection: afterOperationSlug,
id: doc1.id,
collection: afterOperationSlug,
data: {
title: 'Title',
},
}),
await payload.update({
collection: afterOperationSlug,
id: doc2.id,
collection: afterOperationSlug,
data: {
title: 'Title',
},
@@ -225,8 +225,8 @@ describe('Hooks', () => {
})
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('secret')
@@ -235,17 +235,17 @@ describe('Hooks', () => {
it('should pass context from local API to hooks', async () => {
const document = await payload.create({
collection: contextHooksSlug,
data: {
value: 'wrongvalue',
},
context: {
secretValue: 'data from local API',
},
data: {
value: 'wrongvalue',
},
})
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('data from local API')
@@ -282,8 +282,8 @@ describe('Hooks', () => {
const document = (await response.json()).doc
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('data from rest API')
@@ -291,7 +291,7 @@ describe('Hooks', () => {
})
describe('auth collection hooks', () => {
it('allow admin login', async () => {
it('should call afterLogin hook', async () => {
const { user } = await payload.login({
collection: hooksUsersSlug,
data: {
@@ -299,7 +299,15 @@ describe('Hooks', () => {
password: devUser.password,
},
})
const result = await payload.findByID({
id: user.id,
collection: hooksUsersSlug,
})
expect(user).toBeDefined()
expect(user.afterLoginHook).toStrictEqual(true)
expect(result.afterLoginHook).toStrictEqual(true)
})
it('deny user login', async () => {
@@ -342,8 +350,8 @@ describe('Hooks', () => {
// BeforeRead is only run for find operations
const foundDoc = await payload.findByID({
collection: dataHooksSlug,
id: doc.id,
collection: dataHooksSlug,
})
expect(JSON.parse(foundDoc.collection_beforeRead_collection)).toStrictEqual(

View File

@@ -1,3 +1,4 @@
media
uploads
versions
/media-gif

View File

@@ -7,12 +7,11 @@ import removeFiles from '../helpers/removeFiles'
import { Uploads1 } from './collections/Upload1'
import Uploads2 from './collections/Upload2'
import AdminThumbnailCol from './collections/admin-thumbnail'
import { audioSlug, enlargeSlug, mediaSlug, reduceSlug, relationSlug } from './shared'
import { audioSlug, enlargeSlug, mediaSlug, reduceSlug, relationSlug, versionSlug } from './shared'
const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js')
export default buildConfigWithDefaults({
serverURL: undefined,
admin: {
webpack: (config) => ({
...config,
@@ -34,6 +33,11 @@ export default buildConfigWithDefaults({
type: 'upload',
relationTo: 'media',
},
{
name: 'versionedImage',
type: 'upload',
relationTo: versionSlug,
},
],
},
{
@@ -42,164 +46,157 @@ export default buildConfigWithDefaults({
{
name: 'audio',
type: 'upload',
relationTo: 'media',
filterOptions: {
mimeType: {
in: ['audio/mpeg'],
},
},
relationTo: 'media',
},
],
},
{
slug: 'gif-resize',
fields: [],
upload: {
staticURL: '/media-gif',
staticDir: './media-gif',
mimeTypes: ['image/gif'],
resizeOptions: {
position: 'center',
width: 200,
height: 200,
},
formatOptions: {
format: 'gif',
},
imageSizes: [
{
name: 'small',
width: 100,
height: 100,
formatOptions: { format: 'gif', options: { quality: 90 } },
height: 100,
width: 100,
},
{
name: 'large',
width: 1000,
height: 1000,
formatOptions: { format: 'gif', options: { quality: 90 } },
height: 1000,
width: 1000,
},
],
mimeTypes: ['image/gif'],
resizeOptions: {
height: 200,
position: 'center',
width: 200,
},
staticDir: './media-gif',
staticURL: '/media-gif',
},
fields: [],
},
{
slug: 'no-image-sizes',
fields: [],
upload: {
staticURL: '/no-image-sizes',
staticDir: './no-image-sizes',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
resizeOptions: {
height: 200,
position: 'center',
width: 200,
height: 200,
},
staticDir: './no-image-sizes',
staticURL: '/no-image-sizes',
},
fields: [],
},
{
slug: 'object-fit',
fields: [],
upload: {
staticURL: '/object-fit',
staticDir: './object-fit',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'fitContain',
width: 400,
height: 300,
fit: 'contain',
height: 300,
width: 400,
},
{
name: 'fitInside',
width: 300,
height: 400,
fit: 'inside',
height: 400,
width: 300,
},
{
name: 'fitCover',
width: 900,
height: 300,
fit: 'cover',
height: 300,
width: 900,
},
{
name: 'fitOutside',
width: 900,
height: 200,
fit: 'outside',
height: 200,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './object-fit',
staticURL: '/object-fit',
},
fields: [],
},
{
slug: 'crop-only',
fields: [],
upload: {
focalPoint: false,
staticURL: '/crop-only',
staticDir: './crop-only',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './crop-only',
staticURL: '/crop-only',
},
fields: [],
},
{
slug: 'focal-only',
fields: [],
upload: {
crop: false,
staticURL: '/focal-only',
staticDir: './focal-only',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './focal-only',
staticURL: '/focal-only',
},
fields: [],
},
{
slug: mediaSlug,
fields: [],
upload: {
staticURL: '/media',
staticDir: './media',
staticURL: '/media',
// crop: false,
// focalPoint: false,
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
formatOptions: {
format: 'png',
options: { quality: 90 },
@@ -207,143 +204,133 @@ export default buildConfigWithDefaults({
imageSizes: [
{
name: 'maintainedAspectRatio',
width: 1024,
height: undefined,
crop: 'center',
position: 'center',
formatOptions: { format: 'png', options: { quality: 90 } },
height: undefined,
position: 'center',
width: 1024,
},
{
name: 'differentFormatFromMainImage',
width: 200,
height: undefined,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: undefined,
width: 200,
},
{
name: 'maintainedImageSize',
width: undefined,
height: undefined,
width: undefined,
},
{
name: 'maintainedImageSizeWithNewFormat',
width: undefined,
height: undefined,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: undefined,
width: undefined,
},
{
name: 'accidentalSameSize',
width: 320,
height: 80,
position: 'top',
width: 320,
},
{
name: 'tablet',
width: 640,
height: 480,
width: 640,
},
{
name: 'mobile',
width: 320,
height: 240,
crop: 'left top',
height: 240,
width: 320,
},
{
name: 'icon',
width: 16,
height: 16,
width: 16,
},
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
{
name: 'focalTest4',
width: 300,
height: 400,
width: 300,
},
{
name: 'focalTest5',
width: 300,
height: 600,
width: 300,
},
{
name: 'focalTest6',
width: 300,
height: 800,
width: 300,
},
{
name: 'focalTest7',
width: 300,
height: 300,
width: 300,
},
],
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
},
fields: [],
},
{
slug: enlargeSlug,
fields: [],
upload: {
staticURL: '/enlarge',
staticDir: './media/enlarge',
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'resizedLarger',
width: 640,
height: 480,
width: 640,
withoutEnlargement: false,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
width: 180,
},
{
name: 'widthLowerHeightLarger',
width: 300,
height: 300,
fit: 'contain',
height: 300,
width: 300,
},
],
},
fields: [],
},
{
slug: reduceSlug,
upload: {
staticURL: '/reduce',
staticDir: './media/reduce',
mimeTypes: [
'image/png',
'image/jpg',
@@ -352,105 +339,135 @@ export default buildConfigWithDefaults({
'image/svg+xml',
'audio/mpeg',
],
staticDir: './media/enlarge',
staticURL: '/enlarge',
},
},
{
slug: reduceSlug,
fields: [],
upload: {
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: 80,
width: 320,
withoutReduction: true,
},
{
name: 'resizedLarger',
width: 640,
height: 480,
width: 640,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
width: 180,
withoutReduction: true,
},
],
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
staticDir: './media/reduce',
staticURL: '/reduce',
},
fields: [],
},
{
slug: 'media-trim',
fields: [],
upload: {
staticURL: '/media-trim',
staticDir: './media-trim',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
trimOptions: 0,
imageSizes: [
{
name: 'trimNumber',
width: 1024,
height: undefined,
trimOptions: 0,
width: 1024,
},
{
name: 'trimString',
width: 1024,
height: undefined,
trimOptions: 0,
width: 1024,
},
{
name: 'trimOptions',
width: 1024,
height: undefined,
trimOptions: {
background: '#000000',
threshold: 50,
},
width: 1024,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './media-trim',
staticURL: '/media-trim',
trimOptions: 0,
},
fields: [],
},
{
slug: 'unstored-media',
upload: {
staticURL: '/media',
disableLocalStorage: true,
},
fields: [],
upload: {
disableLocalStorage: true,
staticURL: '/media',
},
},
{
slug: 'externally-served-media',
fields: [],
upload: {
// Either use another web server like `npx serve -l 4000` (http://localhost:4000) or use the static server from the previous collection to serve the media folder (http://localhost:3000/media)
staticURL: 'http://localhost:3000/media',
staticDir: './media',
staticURL: 'http://localhost:3000/media',
},
fields: [],
},
Uploads1,
Uploads2,
AdminThumbnailCol,
{
slug: 'optional-file',
upload: {
staticURL: '/optional',
staticDir: './optional',
filesRequiredOnCreate: false,
},
fields: [],
upload: {
filesRequiredOnCreate: false,
staticDir: './optional',
staticURL: '/optional',
},
},
{
slug: 'required-file',
upload: {
staticURL: '/required',
staticDir: './required',
filesRequiredOnCreate: true,
},
fields: [],
upload: {
filesRequiredOnCreate: true,
staticDir: './required',
staticURL: '/required',
},
},
{
slug: versionSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: true,
versions: {
drafts: true,
},
},
],
onInit: async (payload) => {
@@ -475,10 +492,20 @@ export default buildConfigWithDefaults({
file: imageFile,
})
const { id: versionedImage } = await payload.create({
collection: versionSlug,
data: {
_status: 'published',
title: 'upload',
},
file: imageFile,
})
await payload.create({
collection: relationSlug,
data: {
image: uploadedImage,
versionedImage,
},
})
@@ -518,4 +545,5 @@ export default buildConfigWithDefaults({
},
})
},
serverURL: undefined,
})

View File

@@ -29,7 +29,7 @@ describe('uploads', () => {
beforeAll(async ({ browser }) => {
const { serverURL } = await initPayloadE2E(__dirname)
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
@@ -149,6 +149,33 @@ describe('uploads', () => {
await expect(iconMeta).toContainText('16x16')
})
test('should show draft uploads in the relation list', async () => {
await page.goto(relationURL.list)
// from the list edit the first document
await page.locator('.row-1 a').click()
// edit the versioned image
await page.locator('.field-versionedImage .icon--edit').click()
// fill the title with 'draft'
await page.locator('#field-title').fill('draft')
// save draft
await page.locator('#action-save-draft').click()
// close the drawer
await page.locator('.doc-drawer__header-close').click()
// remove the selected versioned image
await page.locator('.field-versionedImage .icon--x').click()
// choose from existing
await page.locator('.list-drawer__toggler').click()
await expect(page.locator('.cell-title')).toContainText('draft')
})
test('should restrict mimetype based on filterOptions', async () => {
await page.goto(audioURL.edit(audioDoc.id))
await wait(200)
@@ -214,21 +241,21 @@ describe('uploads', () => {
describe('image manipulation', () => {
test('should crop image correctly', async () => {
const positions = {
'top-left': {
focalX: 25,
focalY: 25,
dragX: 0,
dragY: 0,
},
'bottom-right': {
focalX: 75,
focalY: 75,
dragX: 800,
dragY: 800,
focalX: 75,
focalY: 75,
},
'top-left': {
dragX: 0,
dragY: 0,
focalX: 25,
focalY: 25,
},
}
const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => {
const { focalX, focalY, dragX, dragY } = positions[position]
const { dragX, dragY, focalX, focalY } = positions[position]
await page.goto(mediaURL.create)
// select and upload file

View File

@@ -9,3 +9,5 @@ export const enlargeSlug = 'enlarge'
export const reduceSlug = 'reduce'
export const adminThumbnailSlug = 'admin-thumbnail'
export const versionSlug = 'versions'