diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index bf02bbd50..fcd346505 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -205,7 +205,9 @@ export const MyField: Field = { } ``` -Default values can be defined as a static string or a function that returns a string. Functions are called with the following arguments: +Default values can be defined as a static value or a function that returns a value. When a `defaultValue` is defined statically, Payload's DB adapters will apply it to the database schema or models. + +Functions can be written to make use of the following argument properties: - `user` - the authenticated user object - `locale` - the currently selected locale string diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index 54388ecb9..f713f096a 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -52,9 +52,19 @@ type FieldSchemaGenerator = ( buildSchemaOptions: BuildSchemaOptions, ) => void +/** + * get a field's defaultValue only if defined and not dynamic so that it can be set on the field schema + * @param field + */ +const formatDefaultValue = (field: FieldAffectingData) => + typeof field.defaultValue !== 'undefined' && typeof field.defaultValue !== 'function' + ? field.defaultValue + : undefined + const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => { const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions const schema: SchemaTypeOptions = { + default: formatDefaultValue(field), index: field.index || (!disableUnique && field.unique) || indexSortableFields || false, required: false, unique: (!disableUnique && field.unique) || false, @@ -159,7 +169,6 @@ const fieldToSchemaMap: Record = { }, }), ], - default: undefined, } schema.add({ @@ -174,7 +183,6 @@ const fieldToSchemaMap: Record = { ): void => { const fieldSchema = { type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })], - default: undefined, } schema.add({ @@ -339,7 +347,7 @@ const fieldToSchemaMap: Record = { }, coordinates: { type: [Number], - default: field.defaultValue || undefined, + default: formatDefaultValue(field), required: false, }, } @@ -420,7 +428,9 @@ const fieldToSchemaMap: Record = { return { ...locales, - [locale]: field.hasMany ? { type: [localeSchema], default: undefined } : localeSchema, + [locale]: field.hasMany + ? { type: [localeSchema], default: formatDefaultValue(field) } + : localeSchema, } }, {}), localized: true, @@ -440,7 +450,7 @@ const fieldToSchemaMap: Record = { if (field.hasMany) { schemaToReturn = { type: [schemaToReturn], - default: undefined, + default: formatDefaultValue(field), } } } else { @@ -453,7 +463,7 @@ const fieldToSchemaMap: Record = { if (field.hasMany) { schemaToReturn = { type: [schemaToReturn], - default: undefined, + default: formatDefaultValue(field), } } } diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts index f8391af76..0e814abff 100644 --- a/packages/db-postgres/src/schema/build.ts +++ b/packages/db-postgres/src/schema/build.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import type { Relation } from 'drizzle-orm' import type { ForeignKeyBuilder, diff --git a/packages/db-postgres/src/schema/traverseFields.ts b/packages/db-postgres/src/schema/traverseFields.ts index be46f665c..5d014495d 100644 --- a/packages/db-postgres/src/schema/traverseFields.ts +++ b/packages/db-postgres/src/schema/traverseFields.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import type { Relation } from 'drizzle-orm' import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core' import type { Field, TabAsField } from 'payload' @@ -35,6 +34,7 @@ import { buildTable } from './build.js' import { createIndex } from './createIndex.js' import { idToUUID } from './idToUUID.js' import { parentIDColumnMap } from './parentIDColumnMap.js' +import { withDefault } from './withDefault.js' type Args = { adapter: PostgresAdapter @@ -170,14 +170,14 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = varchar(columnName) + targetTable[fieldName] = withDefault(varchar(columnName), field) } break } case 'email': case 'code': case 'textarea': { - targetTable[fieldName] = varchar(columnName) + targetTable[fieldName] = withDefault(varchar(columnName), field) break } @@ -199,23 +199,26 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = numeric(columnName) + targetTable[fieldName] = withDefault(numeric(columnName), field) } break } case 'richText': case 'json': { - targetTable[fieldName] = jsonb(columnName) + targetTable[fieldName] = withDefault(jsonb(columnName), field) break } case 'date': { - targetTable[fieldName] = timestamp(columnName, { - mode: 'string', - precision: 3, - withTimezone: true, - }) + targetTable[fieldName] = withDefault( + timestamp(columnName, { + mode: 'string', + precision: 3, + withTimezone: true, + }), + field, + ) break } @@ -311,13 +314,13 @@ export const traverseFields = ({ }), ) } else { - targetTable[fieldName] = adapter.enums[enumName](fieldName) + targetTable[fieldName] = withDefault(adapter.enums[enumName](fieldName), field) } break } case 'checkbox': { - targetTable[fieldName] = boolean(columnName) + targetTable[fieldName] = withDefault(boolean(columnName), field) break } diff --git a/packages/db-postgres/src/schema/withDefault.ts b/packages/db-postgres/src/schema/withDefault.ts new file mode 100644 index 000000000..1214c52e3 --- /dev/null +++ b/packages/db-postgres/src/schema/withDefault.ts @@ -0,0 +1,17 @@ +import type { PgColumnBuilder } from 'drizzle-orm/pg-core' +import type { FieldAffectingData } from 'payload' + +export const withDefault = ( + column: PgColumnBuilder, + field: FieldAffectingData, +): PgColumnBuilder => { + if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function') + return column + + if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) { + const escapedString = field.defaultValue.replaceAll("'", "''") + return column.default(escapedString) + } + + return column.default(field.defaultValue) +} diff --git a/packages/db-sqlite/src/schema/traverseFields.ts b/packages/db-sqlite/src/schema/traverseFields.ts index 2f4f95524..5f6ff8841 100644 --- a/packages/db-sqlite/src/schema/traverseFields.ts +++ b/packages/db-sqlite/src/schema/traverseFields.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import type { Relation } from 'drizzle-orm' import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' import type { Field, TabAsField } from 'payload' @@ -30,6 +29,7 @@ import { buildTable } from './build.js' import { createIndex } from './createIndex.js' import { getIDColumn } from './getIDColumn.js' import { idToUUID } from './idToUUID.js' +import { withDefault } from './withDefault.js' type Args = { adapter: SQLiteAdapter @@ -166,14 +166,14 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = text(columnName) + targetTable[fieldName] = withDefault(text(columnName), field) } break } case 'email': case 'code': case 'textarea': { - targetTable[fieldName] = text(columnName) + targetTable[fieldName] = withDefault(text(columnName), field) break } @@ -195,19 +195,19 @@ export const traverseFields = ({ ) } } else { - targetTable[fieldName] = numeric(columnName) + targetTable[fieldName] = withDefault(numeric(columnName), field) } break } case 'richText': case 'json': { - targetTable[fieldName] = text(columnName, { mode: 'json' }) + targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field) break } case 'date': { - targetTable[fieldName] = text(columnName) + targetTable[fieldName] = withDefault(text(columnName), field) break } @@ -295,13 +295,13 @@ export const traverseFields = ({ }), ) } else { - targetTable[fieldName] = text(fieldName, { enum: options }) + targetTable[fieldName] = withDefault(text(fieldName, { enum: options }), field) } break } case 'checkbox': { - targetTable[fieldName] = integer(columnName, { mode: 'boolean' }) + targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field) break } diff --git a/packages/db-sqlite/src/schema/withDefault.ts b/packages/db-sqlite/src/schema/withDefault.ts new file mode 100644 index 000000000..beb974e04 --- /dev/null +++ b/packages/db-sqlite/src/schema/withDefault.ts @@ -0,0 +1,17 @@ +import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core' +import type { FieldAffectingData } from 'payload' + +export const withDefault = ( + column: SQLiteColumnBuilder, + field: FieldAffectingData, +): SQLiteColumnBuilder => { + if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function') + return column + + if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) { + const escapedString = field.defaultValue.replaceAll("'", "''") + return column.default(escapedString) + } + + return column.default(field.defaultValue) +} diff --git a/packages/drizzle/src/count.ts b/packages/drizzle/src/count.ts index 10d080d87..c2048d261 100644 --- a/packages/drizzle/src/count.ts +++ b/packages/drizzle/src/count.ts @@ -1,5 +1,4 @@ -import type { Count } from 'payload' -import type { SanitizedCollectionConfig } from 'payload' +import type { Count , SanitizedCollectionConfig } from 'payload' import toSnakeCase from 'to-snake-case' @@ -15,7 +14,7 @@ export const count: Count = async function count( const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug)) - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const { joins, where } = await buildQuery({ adapter: this, diff --git a/packages/drizzle/src/create.ts b/packages/drizzle/src/create.ts index 07048a28e..7ab4679ae 100644 --- a/packages/drizzle/src/create.ts +++ b/packages/drizzle/src/create.ts @@ -10,7 +10,7 @@ export const create: Create = async function create( this: DrizzleAdapter, { collection: collectionSlug, data, req }, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) diff --git a/packages/drizzle/src/createGlobal.ts b/packages/drizzle/src/createGlobal.ts index da3710ff4..18b7dc7b8 100644 --- a/packages/drizzle/src/createGlobal.ts +++ b/packages/drizzle/src/createGlobal.ts @@ -10,7 +10,7 @@ export async function createGlobal>( this: DrizzleAdapter, { slug, data, req = {} as PayloadRequest }: CreateGlobalArgs, ): Promise { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug)) diff --git a/packages/drizzle/src/createGlobalVersion.ts b/packages/drizzle/src/createGlobalVersion.ts index a82b8559f..810be5af6 100644 --- a/packages/drizzle/src/createGlobalVersion.ts +++ b/packages/drizzle/src/createGlobalVersion.ts @@ -12,7 +12,7 @@ export async function createGlobalVersion( this: DrizzleAdapter, { autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug) const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`) diff --git a/packages/drizzle/src/createVersion.ts b/packages/drizzle/src/createVersion.ts index afa12721d..3e0c8c15b 100644 --- a/packages/drizzle/src/createVersion.ts +++ b/packages/drizzle/src/createVersion.ts @@ -18,7 +18,7 @@ export async function createVersion( versionData, }: CreateVersionArgs, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const defaultTableName = toSnakeCase(collection.slug) diff --git a/packages/drizzle/src/deleteMany.ts b/packages/drizzle/src/deleteMany.ts index 389f8bc30..7d0637fe3 100644 --- a/packages/drizzle/src/deleteMany.ts +++ b/packages/drizzle/src/deleteMany.ts @@ -11,7 +11,7 @@ export const deleteMany: DeleteMany = async function deleteMany( this: DrizzleAdapter, { collection, req = {} as PayloadRequest, where }, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collectionConfig = this.payload.collections[collection].config const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug)) diff --git a/packages/drizzle/src/deleteOne.ts b/packages/drizzle/src/deleteOne.ts index daee86249..8a73eabf1 100644 --- a/packages/drizzle/src/deleteOne.ts +++ b/packages/drizzle/src/deleteOne.ts @@ -14,7 +14,7 @@ export const deleteOne: DeleteOne = async function deleteOne( this: DrizzleAdapter, { collection: collectionSlug, req = {} as PayloadRequest, where: whereArg }, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) diff --git a/packages/drizzle/src/deleteVersions.ts b/packages/drizzle/src/deleteVersions.ts index 9d8320a74..69504bc23 100644 --- a/packages/drizzle/src/deleteVersions.ts +++ b/packages/drizzle/src/deleteVersions.ts @@ -12,7 +12,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion( this: DrizzleAdapter, { collection, locale, req = {} as PayloadRequest, where: where }, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const tableName = this.tableNameMap.get( diff --git a/packages/drizzle/src/update.ts b/packages/drizzle/src/update.ts index a3a8849a2..1e9e506c8 100644 --- a/packages/drizzle/src/update.ts +++ b/packages/drizzle/src/update.ts @@ -12,7 +12,7 @@ export const updateOne: UpdateOne = async function updateOne( this: DrizzleAdapter, { id, collection: collectionSlug, data, draft, locale, req, where: whereArg }, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) const whereToUse = whereArg || { id: { equals: id } } diff --git a/packages/drizzle/src/updateGlobal.ts b/packages/drizzle/src/updateGlobal.ts index 974c1b13c..ccb13d47a 100644 --- a/packages/drizzle/src/updateGlobal.ts +++ b/packages/drizzle/src/updateGlobal.ts @@ -10,7 +10,7 @@ export async function updateGlobal>( this: DrizzleAdapter, { slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs, ): Promise { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug)) diff --git a/packages/drizzle/src/updateGlobalVersion.ts b/packages/drizzle/src/updateGlobalVersion.ts index 39e097bd6..ff6352f21 100644 --- a/packages/drizzle/src/updateGlobalVersion.ts +++ b/packages/drizzle/src/updateGlobalVersion.ts @@ -25,7 +25,7 @@ export async function updateGlobalVersion( where: whereArg, }: UpdateGlobalVersionArgs, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find( ({ slug }) => slug === global, ) diff --git a/packages/drizzle/src/updateVersion.ts b/packages/drizzle/src/updateVersion.ts index 2a0f0f483..5f922ec0a 100644 --- a/packages/drizzle/src/updateVersion.ts +++ b/packages/drizzle/src/updateVersion.ts @@ -25,7 +25,7 @@ export async function updateVersion( where: whereArg, }: UpdateVersionArgs, ) { - const db = this.sessions[await req.transactionID]?.db || this.drizzle + const db = this.sessions[await req?.transactionID]?.db || this.drizzle const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const whereToUse = whereArg || { id: { equals: id } } const tableName = this.tableNameMap.get( diff --git a/test/database/config.ts b/test/database/config.ts index 2685a1f84..a6c6d5631 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -2,9 +2,17 @@ import { fileURLToPath } from 'node:url' import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) +import type { TextField } from 'payload' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' +const defaultValueField: TextField = { + name: 'defaultValue', + type: 'text', + defaultValue: 'default value from database', +} + export default buildConfigWithDefaults({ collections: [ { @@ -46,6 +54,40 @@ export default buildConfigWithDefaults({ ], }, }, + { + slug: 'default-values', + fields: [ + { + name: 'title', + type: 'text', + }, + defaultValueField, + { + name: 'array', + type: 'array', + // default array with one object to test subfield defaultValue properties for Mongoose + defaultValue: [{}], + fields: [defaultValueField], + }, + { + name: 'group', + type: 'group', + // we need to have to use as default in order to have subfield defaultValue properties directly for Mongoose + defaultValue: {}, + fields: [defaultValueField], + }, + { + name: 'select', + type: 'select', + defaultValue: 'default', + options: [ + { value: 'option0', label: 'Option 0' }, + { value: 'option1', label: 'Option 1' }, + { value: 'default', label: 'Default' }, + ], + }, + ], + }, { slug: 'relation-a', fields: [ diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 2c969f16b..a976034a5 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -89,11 +89,10 @@ describe('database', () => { }) it('should allow createdAt to be set in create', async () => { - const createdAt = new Date('2021-01-01T00:00:00.000Z') + const createdAt = new Date('2021-01-01T00:00:00.000Z').toISOString() const result = await payload.create({ collection: 'posts', data: { - // TODO: createdAt should be optional on RequiredDataFromCollectionSlug createdAt, title: 'hello', }, @@ -104,8 +103,8 @@ describe('database', () => { collection: 'posts', }) - expect(result.createdAt).toStrictEqual(createdAt.toISOString()) - expect(doc.createdAt).toStrictEqual(createdAt.toISOString()) + expect(result.createdAt).toStrictEqual(createdAt) + expect(doc.createdAt).toStrictEqual(createdAt) }) it('updatedAt cannot be set in create', async () => { @@ -461,4 +460,24 @@ describe('database', () => { }) }) }) + + describe('defaultValue', () => { + it('should set default value from db.create', async () => { + // call the db adapter create directly to bypass Payload's default value assignment + const result = await payload.db.create({ + collection: 'default-values', + data: { + // for drizzle DBs, we need to pass an array of objects to test subfields + array: [{ id: 1 }], + title: 'hello', + }, + req: undefined, + }) + + expect(result.defaultValue).toStrictEqual('default value from database') + expect(result.array[0].defaultValue).toStrictEqual('default value from database') + expect(result.group.defaultValue).toStrictEqual('default value from database') + expect(result.select).toStrictEqual('default') + }) + }) }) diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index a013bab6f..969eb7668 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -76,17 +76,26 @@ describe('fields', () => { // TODO - This test is flaky. Rarely, but sometimes it randomly fails. test('should display unique constraint error in ui', async () => { const uniqueText = 'uniqueText' - await payload.create({ + const doc = await payload.create({ collection: 'indexed-fields', data: { group: { unique: uniqueText, }, + localizedUniqueRequiredText: 'text', text: 'text', uniqueRequiredText: 'text', uniqueText, }, }) + await payload.update({ + id: doc.id, + collection: 'indexed-fields', + data: { + localizedUniqueRequiredText: 'es text', + }, + locale: 'es', + }) await page.goto(url.create) await page.waitForURL(url.create) diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 1691ead07..1de61d01e 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -700,10 +700,19 @@ describe('Fields', () => { uniqueRequiredText: 'a', // uniqueText omitted on purpose } - await payload.create({ + const doc = await payload.create({ collection: 'indexed-fields', data, }) + // Update spanish so we do not run into the unique constraint for other locales + await payload.update({ + id: doc.id, + collection: 'indexed-fields', + data: { + localizedUniqueRequiredText: 'es1', + }, + locale: 'es', + }) data.uniqueRequiredText = 'b' const result = await payload.create({ collection: 'indexed-fields', diff --git a/test/live-preview/fields/link.ts b/test/live-preview/fields/link.ts index aa7d00aa7..15bfbaf3f 100644 --- a/test/live-preview/fields/link.ts +++ b/test/live-preview/fields/link.ts @@ -137,7 +137,7 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = linkResult.fields.push({ name: 'appearance', type: 'select', - defaultValue: 'default', + defaultValue: appearanceOptionsToUse[0].value, options: appearanceOptionsToUse, admin: { description: 'Choose how the link should be rendered.',