feat: custom db naming for postgres and mongodb (#5697)

This commit is contained in:
Dan Ribbens
2024-04-05 17:09:08 -04:00
committed by GitHub
parent 6fec2bbe1c
commit f39f95af6d
50 changed files with 634 additions and 208 deletions

View File

@@ -30,6 +30,7 @@ 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. |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs. |
| **`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. |
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._

View File

@@ -26,6 +26,7 @@ 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) |
| **`dbName`** | Custom table or collection name for this global depending on the database adapter. Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,12 +38,18 @@ 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. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
### Access to Drizzle

View File

@@ -45,6 +45,7 @@ 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). |
| **`dbName`** | 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

@@ -80,6 +80,7 @@ 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`. |
| **`dbName`** | 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

@@ -20,27 +20,27 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
### Config
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`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) |
| Option | Description |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`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) |
_\* An asterisk denotes that a property is required._

View File

@@ -36,6 +36,7 @@ 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,6 +38,8 @@ 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. |
| **`dbName`** | 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

@@ -30,7 +30,6 @@
"get-port": "5.1.1",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "9.0.0"

View File

@@ -5,11 +5,7 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import mongoose from 'mongoose'
import paginate from 'mongoose-paginate-v2'
import {
buildVersionCollectionFields,
buildVersionGlobalFields,
getVersionsModelName,
} from 'payload/versions'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import type { MongooseAdapter } from './index.js'
import type { CollectionModel } from './types.js'
@@ -18,13 +14,14 @@ import buildCollectionSchema from './models/buildCollectionSchema.js'
import { buildGlobalModel } from './models/buildGlobalModel.js'
import buildSchema from './models/buildSchema.js'
import getBuildQueryPlugin from './queries/buildQuery.js'
import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload.config)
if (collection.versions) {
const versionModelName = getVersionsModelName(collection)
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection)
@@ -54,7 +51,7 @@ export const init: Init = function init(this: MongooseAdapter) {
}
const model = mongoose.model(
collection.slug,
getDBName({ config: collection }),
schema,
this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel
@@ -72,7 +69,7 @@ export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.globals.forEach((global) => {
if (global.versions) {
const versionModelName = getVersionsModelName(global)
const versionModelName = getDBName({ config: global, versions: true })
const versionGlobalFields = buildVersionGlobalFields(global)

View File

@@ -363,7 +363,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions)
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
} else {
schema.index({ [field.name]: '2dsphere' }, indexOptions)

View File

@@ -0,0 +1,41 @@
import type { DBIdentifierName } from 'payload/database'
type Args = {
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
locales?: boolean
target?: 'dbName' | 'enumName'
versions?: boolean
}
/**
* Used to name database enums and collections
* Returns the collection or enum name for a given entity
*/
export const getDBName = ({
config: { name, slug },
config,
target = 'dbName',
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({}) : custom
} else {
result = name ?? slug
}
if (versions) result = `_${result}_versions`
return result
}

View File

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

View File

@@ -1,10 +1,9 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends TypeWithID>(
@@ -21,7 +20,10 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName: toSnakeCase(slug),
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
})
return result

View File

@@ -4,10 +4,10 @@ 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.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
@@ -16,8 +16,11 @@ 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 = toSnakeCase(globalSlug)
const tableName = `_${globalTableName}_v`
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,

View File

@@ -3,10 +3,10 @@ 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.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>(
@@ -21,8 +21,11 @@ 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 = toSnakeCase(collectionSlug)
const tableName = `_${collectionTableName}_v`
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -40,7 +43,15 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable = this.tables[`${tableName}_rels`]
const relationshipsTable =
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) {
await db.execute(sql`

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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
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 = toSnakeCase(collection)
const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({
adapter: this,

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
@@ -17,7 +17,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({

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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,7 +16,11 @@ 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 = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -1,38 +1,41 @@
import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const find: Find = async function find(
this: PostgresAdapter,
{
collection,
limit: limitArg,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort: sortArg,
where: whereArg,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({
adapter: this,
fields: collectionConfig.fields,
limit: limitArg,
limit,
locale,
page,
pagination,
req,
sort,
tableName: toSnakeCase(collection),
where: whereArg,
tableName,
where,
})
}

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import { getTableName } from '../schema/getTableName.js'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,9 +79,22 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = `${currentTableName}_${path}${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales
const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
traverseFields({
@@ -128,9 +142,16 @@ export const traverseFields = ({
with: {},
}
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
withBlock.with._locales = _locales
}
topLevelArgs.with[blockKey] = withBlock
traverseFields({

View File

@@ -1,17 +1,19 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ slug, locale, req, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,7 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,17 +1,20 @@
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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: incomingWhere }: FindOneArgs,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({
adapter: this,
@@ -22,8 +25,8 @@ export async function findOne<T extends TypeWithID>(
pagination: false,
req,
sort: undefined,
tableName: toSnakeCase(collection),
where: incomingWhere,
tableName,
where,
})
return docs?.[0] || null

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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,7 +25,11 @@ 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 = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -1,6 +1,5 @@
import type { Payload } from 'payload'
import type { DatabaseAdapterObj } from 'payload/database'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
import fs from 'fs'
import path from 'path'
@@ -39,6 +38,8 @@ import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateVersion } from './updateVersion.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
@@ -50,20 +51,24 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},
schemaName: args.schemaName,
sessions: {},
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction,

View File

@@ -4,11 +4,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildTable } from './schema/build.js'
import { getTableName } from './schema/getTableName.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,10 @@ export const init: Init = function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = toSnakeCase(collection.slug)
const tableName = getTableName({
adapter: this,
config: collection,
})
buildTable({
adapter: this,
@@ -37,10 +40,15 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: collection.fields,
tableName,
timestamps: collection.timestamps,
versions: false,
})
if (collection.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const versionFields = buildVersionCollectionFields(collection)
buildTable({
@@ -53,12 +61,13 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
this.payload.config.globals.forEach((global) => {
const tableName = toSnakeCase(global.slug)
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -70,10 +79,11 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: global.fields,
tableName,
timestamps: false,
versions: false,
})
if (global.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({
@@ -86,6 +96,7 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})

View File

@@ -92,7 +92,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
payload.logger.info({ msg: `Migrating: ${migration.name}` })
const pgAdapter = payload.db as PostgresAdapter
const pgAdapter = payload.db
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try {

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { getTableName } from '../schema/getTableName.js'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}_locales`
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -218,7 +226,12 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -265,7 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -285,7 +303,12 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
@@ -351,7 +374,7 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
let relationshipFields
const relationTableName = `${rootTableName}_rels`
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias(
@@ -392,9 +415,13 @@ export const getTableColumnFromPath = ({
let newAliasTable
if (typeof field.relationTo === 'string') {
newTableName = `${toSnakeCase(field.relationTo)}`
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table
relationshipFields = adapter.payload.collections[field.relationTo].config.fields
relationshipFields = relationshipConfig.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -413,7 +440,11 @@ export const getTableColumnFromPath = ({
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`,
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
@@ -460,7 +491,7 @@ export const getTableColumnFromPath = ({
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}_locales`
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]

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.js'
import { getTableName } from './schema/getTableName.js'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
@@ -17,7 +17,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -11,10 +11,10 @@ import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { getTableName } from './getTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
@@ -35,6 +35,7 @@ type Args = {
rootTableName?: string
tableName: string
timestamps?: boolean
versions: boolean
}
type Result = {
@@ -59,6 +60,7 @@ export const buildTable = ({
rootTableName: incomingRootTableName,
tableName,
timestamps,
versions,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
@@ -113,6 +115,7 @@ export const buildTable = ({
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
if (timestamps) {
@@ -147,7 +150,7 @@ export const buildTable = ({
adapter.tables[tableName] = table
if (hasLocalizedField) {
const localeTableName = `${tableName}_locales`
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
@@ -288,11 +291,16 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const formattedRelationTo = toSnakeCase(relationTo)
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = adapter.payload.collections[
relationTo
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
@@ -301,7 +309,7 @@ export const buildTable = ({
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}_rels`
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
@@ -333,7 +341,11 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = toSnakeCase(relationTo)
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,
})
const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], {
fields: [relationshipsTable[idColumnName]],

View File

@@ -0,0 +1,75 @@
import type { DBIdentifierName } from 'payload/database'
import { APIError } from 'payload/errors'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
type Args = {
adapter: PostgresAdapter
/** The collection, global or field config **/
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
}
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -27,6 +27,7 @@ import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { getTableName } from './getTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
@@ -53,6 +54,7 @@ type Args = {
rootRelationsToBuild?: Map<string, string>
rootTableIDColType: string
rootTableName: string
versions: boolean
}
type Result = {
@@ -86,7 +88,9 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
}: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
@@ -217,7 +221,15 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = `enum_${newTableName}_${toSnakeCase(field.name)}`
const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
enumName,
@@ -231,7 +243,14 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = `${newTableName}_${toSnakeCase(field.name)}`
const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -266,6 +285,7 @@ export const traverseFields = ({
disableUnique,
fields: [],
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, selectTableName)
@@ -296,7 +316,13 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -334,6 +360,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
versions,
})
if (subHasManyTextField) {
@@ -356,7 +383,7 @@ export const traverseFields = ({
}
if (hasLocalesTable(field.fields)) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`])
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`])
}
subRelationsToBuild.forEach((val, key) => {
@@ -375,7 +402,13 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
})
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
@@ -416,6 +449,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: blockTableName,
versions,
})
if (subHasManyTextField) {
@@ -439,7 +473,9 @@ export const traverseFields = ({
}
if (hasLocalesTable(block.fields)) {
result._locales = many(adapter.tables[`${blockTableName}_locales`])
result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
)
}
subRelationsToBuild.forEach((val, key) => {
@@ -451,7 +487,7 @@ export const traverseFields = ({
)
adapter.relations[`relations_${blockTableName}`] = blockTableRelations
} else if (process.env.NODE_ENV !== 'production') {
} else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({
block,
localized: field.localized,
@@ -459,7 +495,7 @@ export const traverseFields = ({
table: adapter.tables[blockTableName],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -498,6 +534,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -540,6 +577,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -583,6 +621,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -626,6 +665,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (rowHasLocalizedField) hasLocalizedField = true

View File

@@ -24,11 +24,14 @@ export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
idType?: 'serial' | 'uuid'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
relationshipsSuffix?: string
schemaName?: string
versionsSuffix?: string
}
export type GenericColumn = PgColumn<
@@ -58,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -66,12 +73,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
localesSuffix?: string
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
@@ -82,6 +91,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
}
}
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
@@ -98,9 +108,11 @@ declare module 'payload' {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
@@ -110,5 +122,6 @@ declare module 'payload' {
}
}
tables: Record<string, GenericTable>
versionsSuffix?: string
}
}

View File

@@ -1,11 +1,10 @@
import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +13,10 @@ 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 = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
@@ -49,7 +51,7 @@ export const updateOne: UpdateOne = async function updateOne(
fields: collection.fields,
operation: 'update',
req,
tableName: toSnakeCase(collectionSlug),
tableName,
})
return result

View File

@@ -1,10 +1,9 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobal<T extends TypeWithID>(
@@ -13,7 +12,10 @@ 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 = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({})

View File

@@ -2,11 +2,11 @@ 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.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,7 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({

View File

@@ -2,11 +2,11 @@ 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.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateVersion<T extends TypeWithID>(
@@ -23,7 +23,11 @@ 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 = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -108,6 +108,7 @@ const collectionSchema = joi.object().keys({
joi.boolean(),
),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultSort: joi.string(),
disableDuplicate: joi.bool(),
endpoints: endpointsSchema,

View File

@@ -411,3 +411,10 @@ export type DatabaseAdapterResult<T = BaseDatabaseAdapter> = {
defaultIDType: 'number' | 'text'
init: (args: { payload: Payload }) => T
}
export type DBIdentifierName =
| ((Args: {
/** The name of the parent table when using relational DBs */
tableName?: string
}) => string)
| string

View File

@@ -12,6 +12,7 @@ export type {
CreateMigration,
CreateVersion,
CreateVersionArgs,
DBIdentifierName,
DatabaseAdapterResult as DatabaseAdapterObj,
DeleteMany,
DeleteManyArgs,

View File

@@ -4,6 +4,5 @@ export { deleteCollectionVersions } from '../versions/deleteCollectionVersions.j
export { enforceMaxVersions } from '../versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from '../versions/getLatestCollectionVersion.js'
export { getLatestGlobalVersion } from '../versions/getLatestGlobalVersion.js'
export { getVersionsModelName } from '../versions/getVersionsModelName.js'
export { saveVersion } from '../versions/saveVersion.js'
export type { TypeWithVersion } from '../versions/types.js'

View File

@@ -201,9 +201,11 @@ export const select = baseField.keys({
isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi
.alternatives()
.try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
hasMany: joi.boolean().default(false),
options: joi
.array()
@@ -233,6 +235,7 @@ export const radio = baseField.keys({
layout: joi.string().valid('vertical', 'horizontal'),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
options: joi
.array()
.min(1)
@@ -310,6 +313,7 @@ export const array = baseField.keys({
.default({}),
})
.default({}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
fields: joi.array().items(joi.link('#field')).required(),
interfaceName: joi.string(),
@@ -410,6 +414,7 @@ export const blocks = baseField.keys({
joi.object({
slug: joi.string().required(),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
fields: joi.array().items(joi.link('#field')),
graphQL: joi.object().keys({
singularName: joi.string(),

View File

@@ -17,6 +17,7 @@ import type {
} from '../../admin/types.js'
import type { User } from '../../auth/index.js'
import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { Operation, PayloadRequest, RequestContext, Where } from '../../types/index.js'
import type { ClientFieldConfig } from './client.js'
@@ -68,9 +69,11 @@ export type FieldAccess<T extends TypeWithID = any, P = any, U = any> = (args: {
* The `id` of the current document being read or updated. `id` is undefined during the `create` operation.
*/
id?: number | string
/** The `Express` request object containing the currently authenticated `user` */
req: PayloadRequest<U>
/** The `payload` object to interface with the payload API */
req: PayloadRequest<U>
/**
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
*/
siblingData?: Partial<P>
}) => Promise<boolean> | boolean
@@ -478,6 +481,14 @@ export type SelectField = FieldBase & {
isClearable?: boolean
isSortable?: boolean
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
hasMany?: boolean
options: Option[]
type: 'select'
@@ -584,6 +595,10 @@ export type ArrayField = FieldBase & {
} & Admin['components']
initCollapsed?: boolean
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
fields: Field[]
/** Customize generated GraphQL and Typescript schema names.
* By default it is bound to the collection.
@@ -606,6 +621,14 @@ export type RadioField = FieldBase & {
}
layout?: 'horizontal' | 'vertical'
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
options: Option[]
type: 'radio'
}
@@ -613,6 +636,10 @@ export type RadioField = FieldBase & {
export type Block = {
/** Extension point to add your custom data. */
custom?: Record<string, any>
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
fields: Field[]
/** @deprecated - please migrate to the interfaceName property instead. */
graphQL?: {

View File

@@ -49,6 +49,7 @@ const globalSchema = joi
preview: joi.func(),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
endpoints: endpointsSchema,
fields: joi.array(),
graphQL: joi.alternatives().try(

View File

@@ -16,6 +16,7 @@ import type {
GeneratePreviewURL,
LivePreviewConfig,
} from '../../config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { Field } from '../../fields/config/types.js'
import type { PayloadRequest, RequestContext } from '../../types/index.js'
import type { Where } from '../../types/index.js'
@@ -141,6 +142,10 @@ export type GlobalConfig = {
admin?: GlobalAdminOptions
/** Extension point to add your custom data. */
custom?: Record<string, any>
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
endpoints?: Omit<Endpoint, 'root'>[] | false
fields: Field[]
graphQL?:

View File

@@ -1,6 +0,0 @@
import type { SanitizedCollectionConfig } from '../collections/config/types.ts'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
export const getVersionsModelName = (
entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
): string => `_${entity.slug}_versions`

77
pnpm-lock.yaml generated
View File

@@ -374,9 +374,6 @@ importers:
mongoose:
specifier: 6.12.3
version: 6.12.3
mongoose-aggregate-paginate-v2:
specifier: 1.0.6
version: 1.0.6
mongoose-paginate-v2:
specifier: 1.7.22
version: 1.7.22
@@ -9774,7 +9771,7 @@ packages:
dependencies:
loader-utils: 2.0.4
schema-utils: 3.3.0
webpack: 5.90.3(@swc/core@1.4.2)
webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.19.12)(webpack-cli@5.1.4)
dev: true
/file-type@16.5.4:
@@ -12275,7 +12272,7 @@ packages:
dependencies:
loader-utils: 2.0.4
schema-utils: 3.3.0
webpack: 5.90.3(@swc/core@1.4.2)
webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.19.12)(webpack-cli@5.1.4)
webpack-sources: 1.4.3
dev: true
@@ -12416,11 +12413,6 @@ packages:
'@mongodb-js/saslprep': 1.1.4
dev: true
/mongoose-aggregate-paginate-v2@1.0.6:
resolution: {integrity: sha512-UuALu+mjhQa1K9lMQvjLL3vm3iALvNw8PQNIh2gp1b+tO5hUa0NC0Wf6/8QrT9PSJVTihXaD8hQVy3J4e0jO0Q==}
engines: {node: '>=4.0.0'}
dev: false
/mongoose-paginate-v2@1.7.22:
resolution: {integrity: sha512-xW5GugkE21DJiu9e13EOxKt4ejEKQkRP/S1PkkXRjnk2rRZVKBcld1nPV+VJ/YCPfm8hb3sz9OvI7O38RmixkA==}
engines: {node: '>=4.0.0'}
@@ -15656,31 +15648,6 @@ packages:
webpack: 5.90.3(@swc/core@1.4.2)(esbuild@0.19.12)(webpack-cli@5.1.4)
dev: true
/terser-webpack-plugin@5.3.10(@swc/core@1.4.2)(webpack@5.90.3):
resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
engines: {node: '>= 10.13.0'}
peerDependencies:
'@swc/core': '*'
esbuild: '*'
uglify-js: '*'
webpack: ^5.1.0
peerDependenciesMeta:
'@swc/core':
optional: true
esbuild:
optional: true
uglify-js:
optional: true
dependencies:
'@jridgewell/trace-mapping': 0.3.23
'@swc/core': 1.4.2
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.2
terser: 5.28.1
webpack: 5.90.3(@swc/core@1.4.2)
dev: true
/terser@5.28.1:
resolution: {integrity: sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA==}
engines: {node: '>=10'}
@@ -16440,46 +16407,6 @@ packages:
engines: {node: '>=10.13.0'}
dev: true
/webpack@5.90.3(@swc/core@1.4.2):
resolution: {integrity: sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==}
engines: {node: '>=10.13.0'}
hasBin: true
peerDependencies:
webpack-cli: '*'
peerDependenciesMeta:
webpack-cli:
optional: true
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.5
'@webassemblyjs/ast': 1.11.6
'@webassemblyjs/wasm-edit': 1.11.6
'@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.11.3
acorn-import-assertions: 1.9.0(acorn@8.11.3)
browserslist: 4.23.0
chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.1
es-module-lexer: 1.4.1
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.0
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.10(@swc/core@1.4.2)(webpack@5.90.3)
watchpack: 2.4.0
webpack-sources: 3.2.3
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
dev: true
/webpack@5.90.3(@swc/core@1.4.2)(esbuild@0.19.12)(webpack-cli@5.1.4):
resolution: {integrity: sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==}
engines: {node: '>=10.13.0'}

View File

@@ -63,7 +63,98 @@ export default buildConfigWithDefaults({
singular: 'Relation B',
},
},
{
slug: 'custom-schema',
dbName: 'customs',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
{
name: 'relationship',
type: 'relationship',
hasMany: true,
relationTo: 'relation-a',
},
{
name: 'select',
type: 'select',
dbName: ({ tableName }) => `${tableName}_customSelect`,
enumName: 'selectEnum',
hasMany: true,
options: ['a', 'b', 'c'],
},
{
name: 'radio',
type: 'select',
enumName: 'radioEnum',
options: ['a', 'b', 'c'],
},
{
name: 'array',
type: 'array',
dbName: 'customArrays',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'block',
dbName: 'customBlocks',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
],
},
],
},
],
versions: true,
},
],
globals: [
{
slug: 'global',
// @ts-expect-error
dbName: 'customGlobal',
fields: [
{
name: 'text',
type: 'text',
},
],
versions: true,
},
],
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
onInit: async (payload) => {
await payload.create({
collection: 'users',

View File

@@ -131,6 +131,59 @@ describe('database', () => {
})
})
describe('schema', () => {
it('should use custom dbNames', () => {
expect(payload.db).toBeDefined()
if (payload.db.name === 'mongoose') {
// @ts-expect-error
const db: MongooseAdapter = payload.db
expect(db.collections['custom-schema'].modelName).toStrictEqual('customs')
expect(db.versions['custom-schema'].modelName).toStrictEqual('_customs_versions')
expect(db.versions.global.modelName).toStrictEqual('_customGlobal_versions')
} else {
// @ts-expect-error
const db: PostgresAdapter = payload.db
// collection
expect(db.tables.customs).toBeDefined()
// collection versions
expect(db.tables._customs_v).toBeDefined()
// collection relationships
expect(db.tables.customs_rels).toBeDefined()
// collection localized
expect(db.tables.customs_locales).toBeDefined()
// global
expect(db.tables.customGlobal).toBeDefined()
expect(db.tables._customGlobal_v).toBeDefined()
// select
expect(db.tables.customs_customSelect).toBeDefined()
// array
expect(db.tables.customArrays).toBeDefined()
// array localized
expect(db.tables.customArrays_locales).toBeDefined()
// blocks
expect(db.tables.customBlocks).toBeDefined()
// localized blocks
expect(db.tables.customBlocks_locales).toBeDefined()
// enum names
expect(db.enums.selectEnum).toBeDefined()
expect(db.enums.radioEnum).toBeDefined()
}
})
})
describe('transactions', () => {
describe('local api', () => {
it('should commit multiple operations in isolation', async () => {