Skip to content

Commit 695ef32

Browse files
authored
feat(db-*): add defaultValues to database schemas (#7368)
## Description Prior to this change, the `defaultValue` for fields have only been used in the application layer of Payload. With this change, you get the added benefit of having the database columns get the default also. This is especially helpful when adding new columns to postgres with existing data to avoid needing to write complex migrations. In MongoDB this change applies the default to the Mongoose model which is useful when calling payload.db.create() directly. This only works for statically defined values. 🙏 A big thanks to @r1tsuu for the feature and implementation idea as I lifted some code from PR #6983 - [x] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository. ## Type of change - [x] New feature (non-breaking change which adds functionality) - [x] This change requires a documentation update ## Checklist: - [x] I have added tests that prove my fix is effective or that my feature works - [x] Existing test suite passes locally with my changes - [x] I have made corresponding changes to the documentation
1 parent b5b2bb1 commit 695ef32

File tree

24 files changed

+175
-49
lines changed

24 files changed

+175
-49
lines changed

docs/fields/overview.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ export const MyField: Field = {
205205
}
206206
```
207207

208-
Default values can be defined as a static string or a function that returns a string. Functions are called with the following arguments:
208+
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.
209+
210+
Functions can be written to make use of the following argument properties:
209211

210212
- `user` - the authenticated user object
211213
- `locale` - the currently selected locale string

packages/db-mongodb/src/models/buildSchema.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,19 @@ type FieldSchemaGenerator = (
5252
buildSchemaOptions: BuildSchemaOptions,
5353
) => void
5454

55+
/**
56+
* get a field's defaultValue only if defined and not dynamic so that it can be set on the field schema
57+
* @param field
58+
*/
59+
const formatDefaultValue = (field: FieldAffectingData) =>
60+
typeof field.defaultValue !== 'undefined' && typeof field.defaultValue !== 'function'
61+
? field.defaultValue
62+
: undefined
63+
5564
const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => {
5665
const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions
5766
const schema: SchemaTypeOptions<unknown> = {
67+
default: formatDefaultValue(field),
5868
index: field.index || (!disableUnique && field.unique) || indexSortableFields || false,
5969
required: false,
6070
unique: (!disableUnique && field.unique) || false,
@@ -159,7 +169,6 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
159169
},
160170
}),
161171
],
162-
default: undefined,
163172
}
164173

165174
schema.add({
@@ -174,7 +183,6 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
174183
): void => {
175184
const fieldSchema = {
176185
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
177-
default: undefined,
178186
}
179187

180188
schema.add({
@@ -339,7 +347,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
339347
},
340348
coordinates: {
341349
type: [Number],
342-
default: field.defaultValue || undefined,
350+
default: formatDefaultValue(field),
343351
required: false,
344352
},
345353
}
@@ -420,7 +428,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
420428

421429
return {
422430
...locales,
423-
[locale]: field.hasMany ? { type: [localeSchema], default: undefined } : localeSchema,
431+
[locale]: field.hasMany
432+
? { type: [localeSchema], default: formatDefaultValue(field) }
433+
: localeSchema,
424434
}
425435
}, {}),
426436
localized: true,
@@ -440,7 +450,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
440450
if (field.hasMany) {
441451
schemaToReturn = {
442452
type: [schemaToReturn],
443-
default: undefined,
453+
default: formatDefaultValue(field),
444454
}
445455
}
446456
} else {
@@ -453,7 +463,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
453463
if (field.hasMany) {
454464
schemaToReturn = {
455465
type: [schemaToReturn],
456-
default: undefined,
466+
default: formatDefaultValue(field),
457467
}
458468
}
459469
}

packages/db-postgres/src/schema/build.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-param-reassign */
21
import type { Relation } from 'drizzle-orm'
32
import type {
43
ForeignKeyBuilder,

packages/db-postgres/src/schema/traverseFields.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-param-reassign */
21
import type { Relation } from 'drizzle-orm'
32
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
43
import type { Field, TabAsField } from 'payload'
@@ -35,6 +34,7 @@ import { buildTable } from './build.js'
3534
import { createIndex } from './createIndex.js'
3635
import { idToUUID } from './idToUUID.js'
3736
import { parentIDColumnMap } from './parentIDColumnMap.js'
37+
import { withDefault } from './withDefault.js'
3838

3939
type Args = {
4040
adapter: PostgresAdapter
@@ -170,14 +170,14 @@ export const traverseFields = ({
170170
)
171171
}
172172
} else {
173-
targetTable[fieldName] = varchar(columnName)
173+
targetTable[fieldName] = withDefault(varchar(columnName), field)
174174
}
175175
break
176176
}
177177
case 'email':
178178
case 'code':
179179
case 'textarea': {
180-
targetTable[fieldName] = varchar(columnName)
180+
targetTable[fieldName] = withDefault(varchar(columnName), field)
181181
break
182182
}
183183

@@ -199,23 +199,26 @@ export const traverseFields = ({
199199
)
200200
}
201201
} else {
202-
targetTable[fieldName] = numeric(columnName)
202+
targetTable[fieldName] = withDefault(numeric(columnName), field)
203203
}
204204
break
205205
}
206206

207207
case 'richText':
208208
case 'json': {
209-
targetTable[fieldName] = jsonb(columnName)
209+
targetTable[fieldName] = withDefault(jsonb(columnName), field)
210210
break
211211
}
212212

213213
case 'date': {
214-
targetTable[fieldName] = timestamp(columnName, {
215-
mode: 'string',
216-
precision: 3,
217-
withTimezone: true,
218-
})
214+
targetTable[fieldName] = withDefault(
215+
timestamp(columnName, {
216+
mode: 'string',
217+
precision: 3,
218+
withTimezone: true,
219+
}),
220+
field,
221+
)
219222
break
220223
}
221224

@@ -311,13 +314,13 @@ export const traverseFields = ({
311314
}),
312315
)
313316
} else {
314-
targetTable[fieldName] = adapter.enums[enumName](fieldName)
317+
targetTable[fieldName] = withDefault(adapter.enums[enumName](fieldName), field)
315318
}
316319
break
317320
}
318321

319322
case 'checkbox': {
320-
targetTable[fieldName] = boolean(columnName)
323+
targetTable[fieldName] = withDefault(boolean(columnName), field)
321324
break
322325
}
323326

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { PgColumnBuilder } from 'drizzle-orm/pg-core'
2+
import type { FieldAffectingData } from 'payload'
3+
4+
export const withDefault = (
5+
column: PgColumnBuilder,
6+
field: FieldAffectingData,
7+
): PgColumnBuilder => {
8+
if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function')
9+
return column
10+
11+
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
12+
const escapedString = field.defaultValue.replaceAll("'", "''")
13+
return column.default(escapedString)
14+
}
15+
16+
return column.default(field.defaultValue)
17+
}

packages/db-sqlite/src/schema/traverseFields.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable no-param-reassign */
21
import type { Relation } from 'drizzle-orm'
32
import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
43
import type { Field, TabAsField } from 'payload'
@@ -30,6 +29,7 @@ import { buildTable } from './build.js'
3029
import { createIndex } from './createIndex.js'
3130
import { getIDColumn } from './getIDColumn.js'
3231
import { idToUUID } from './idToUUID.js'
32+
import { withDefault } from './withDefault.js'
3333

3434
type Args = {
3535
adapter: SQLiteAdapter
@@ -166,14 +166,14 @@ export const traverseFields = ({
166166
)
167167
}
168168
} else {
169-
targetTable[fieldName] = text(columnName)
169+
targetTable[fieldName] = withDefault(text(columnName), field)
170170
}
171171
break
172172
}
173173
case 'email':
174174
case 'code':
175175
case 'textarea': {
176-
targetTable[fieldName] = text(columnName)
176+
targetTable[fieldName] = withDefault(text(columnName), field)
177177
break
178178
}
179179

@@ -195,19 +195,19 @@ export const traverseFields = ({
195195
)
196196
}
197197
} else {
198-
targetTable[fieldName] = numeric(columnName)
198+
targetTable[fieldName] = withDefault(numeric(columnName), field)
199199
}
200200
break
201201
}
202202

203203
case 'richText':
204204
case 'json': {
205-
targetTable[fieldName] = text(columnName, { mode: 'json' })
205+
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
206206
break
207207
}
208208

209209
case 'date': {
210-
targetTable[fieldName] = text(columnName)
210+
targetTable[fieldName] = withDefault(text(columnName), field)
211211
break
212212
}
213213

@@ -295,13 +295,13 @@ export const traverseFields = ({
295295
}),
296296
)
297297
} else {
298-
targetTable[fieldName] = text(fieldName, { enum: options })
298+
targetTable[fieldName] = withDefault(text(fieldName, { enum: options }), field)
299299
}
300300
break
301301
}
302302

303303
case 'checkbox': {
304-
targetTable[fieldName] = integer(columnName, { mode: 'boolean' })
304+
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
305305
break
306306
}
307307

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
2+
import type { FieldAffectingData } from 'payload'
3+
4+
export const withDefault = (
5+
column: SQLiteColumnBuilder,
6+
field: FieldAffectingData,
7+
): SQLiteColumnBuilder => {
8+
if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function')
9+
return column
10+
11+
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
12+
const escapedString = field.defaultValue.replaceAll("'", "''")
13+
return column.default(escapedString)
14+
}
15+
16+
return column.default(field.defaultValue)
17+
}

packages/drizzle/src/count.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { Count } from 'payload'
2-
import type { SanitizedCollectionConfig } from 'payload'
1+
import type { Count , SanitizedCollectionConfig } from 'payload'
32

43
import toSnakeCase from 'to-snake-case'
54

@@ -15,7 +14,7 @@ export const count: Count = async function count(
1514

1615
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
1716

18-
const db = this.sessions[await req.transactionID]?.db || this.drizzle
17+
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
1918

2019
const { joins, where } = await buildQuery({
2120
adapter: this,

packages/drizzle/src/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const create: Create = async function create(
1010
this: DrizzleAdapter,
1111
{ collection: collectionSlug, data, req },
1212
) {
13-
const db = this.sessions[await req.transactionID]?.db || this.drizzle
13+
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
1414
const collection = this.payload.collections[collectionSlug].config
1515

1616
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))

packages/drizzle/src/createGlobal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
1010
this: DrizzleAdapter,
1111
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
1212
): Promise<T> {
13-
const db = this.sessions[await req.transactionID]?.db || this.drizzle
13+
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
1414
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
1515

1616
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))

0 commit comments

Comments
 (0)