perf(drizzle): single-roundtrip db updates for simple collections (#13186)
Currently, an optimized DB update (simple data => no delete-and-create-row) does the following: 1. sql UPDATE 2. sql SELECT This PR reduces this further to one single DB call for simple collections: 1. sql UPDATE with RETURNING() This only works for simple collections that do not have any fields that need to be fetched from other tables. If a collection has fields like relationship or blocks, we'll need that separate SELECT call to join in the other tables. In 4.0, we can remove all "complex" fields from the jobs collection and replace them with a JSON field to make use of this optimization --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210803039809814
This commit is contained in:
@@ -44,7 +44,7 @@ export const buildFindManyArgs = ({
|
||||
select,
|
||||
tableName,
|
||||
versions,
|
||||
}: BuildFindQueryArgs): Record<string, unknown> => {
|
||||
}: BuildFindQueryArgs): Result => {
|
||||
const result: Result = {
|
||||
extras: {},
|
||||
with: {},
|
||||
@@ -134,5 +134,12 @@ export const buildFindManyArgs = ({
|
||||
result.with._locales = _locales
|
||||
}
|
||||
|
||||
// Delete properties that are empty
|
||||
for (const key of Object.keys(result)) {
|
||||
if (!Object.keys(result[key]).length) {
|
||||
delete result[key]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { SelectedFields } from 'drizzle-orm/sqlite-core'
|
||||
import type { TypeWithID } from 'payload'
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
@@ -53,434 +54,496 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
const drizzle = db as LibSQLDatabase
|
||||
|
||||
await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
// TODO: we can skip fetching idToUpdate here with using the incoming where
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
} else {
|
||||
// Split out the incoming data into the corresponding:
|
||||
// base row, locales, relationships, blocks, and arrays
|
||||
const rowToInsert = transformForWrite({
|
||||
if (ignoreResult) {
|
||||
await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
return ignoreResult === 'idOnly' ? ({ id } as T) : null
|
||||
}
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter,
|
||||
data,
|
||||
enableAtomicWrites: false,
|
||||
depth: 0,
|
||||
fields,
|
||||
path,
|
||||
joinQuery: false,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
// First, we insert the main row
|
||||
try {
|
||||
if (operation === 'update') {
|
||||
const target = upsertTarget || adapter.tables[tableName].id
|
||||
const findManyKeysLength = Object.keys(findManyArgs).length
|
||||
const hasOnlyColumns = Object.keys(findManyArgs.columns || {}).length > 0
|
||||
|
||||
if (id) {
|
||||
rowToInsert.row.id = id
|
||||
;[insertedRow] = await adapter.insert({
|
||||
db,
|
||||
onConflictDoUpdate: { set: rowToInsert.row, target },
|
||||
tableName,
|
||||
values: rowToInsert.row,
|
||||
})
|
||||
} else {
|
||||
;[insertedRow] = await adapter.insert({
|
||||
db,
|
||||
onConflictDoUpdate: { set: rowToInsert.row, target, where },
|
||||
tableName,
|
||||
values: rowToInsert.row,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (adapter.allowIDOnCreate && data.id) {
|
||||
rowToInsert.row.id = data.id
|
||||
if (findManyKeysLength === 0 || hasOnlyColumns) {
|
||||
// Optimization - No need for joins => can simply use returning(). This is optimal for very simple collections
|
||||
// without complex fields that live in separate tables like blocks, arrays, relationships, etc.
|
||||
|
||||
const selectedFields: SelectedFields = {}
|
||||
if (hasOnlyColumns) {
|
||||
for (const [column, enabled] of Object.entries(findManyArgs.columns)) {
|
||||
if (enabled) {
|
||||
selectedFields[column] = adapter.tables[tableName][column]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const docs = await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
.returning(Object.keys(selectedFields).length ? selectedFields : undefined)
|
||||
|
||||
return transform<T>({
|
||||
adapter,
|
||||
config: adapter.payload.config,
|
||||
data: docs[0],
|
||||
fields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
}
|
||||
|
||||
// DB Update that needs the result, potentially with joins => need to update first, then find. returning() does not work with joins.
|
||||
|
||||
await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
|
||||
findManyArgs.where = eq(adapter.tables[tableName].id, insertedRow.id)
|
||||
|
||||
const doc = await db.query[tableName].findFirst(findManyArgs)
|
||||
|
||||
return transform<T>({
|
||||
adapter,
|
||||
config: adapter.payload.config,
|
||||
data: doc,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
}
|
||||
// Split out the incoming data into the corresponding:
|
||||
// base row, locales, relationships, blocks, and arrays
|
||||
const rowToInsert = transformForWrite({
|
||||
adapter,
|
||||
data,
|
||||
enableAtomicWrites: false,
|
||||
fields,
|
||||
path,
|
||||
tableName,
|
||||
})
|
||||
|
||||
// First, we insert the main row
|
||||
try {
|
||||
if (operation === 'update') {
|
||||
const target = upsertTarget || adapter.tables[tableName].id
|
||||
|
||||
if (id) {
|
||||
rowToInsert.row.id = id
|
||||
;[insertedRow] = await adapter.insert({
|
||||
db,
|
||||
onConflictDoUpdate: { set: rowToInsert.row, target },
|
||||
tableName,
|
||||
values: rowToInsert.row,
|
||||
})
|
||||
} else {
|
||||
;[insertedRow] = await adapter.insert({
|
||||
db,
|
||||
onConflictDoUpdate: { set: rowToInsert.row, target, where },
|
||||
tableName,
|
||||
values: rowToInsert.row,
|
||||
})
|
||||
}
|
||||
|
||||
const localesToInsert: Record<string, unknown>[] = []
|
||||
const relationsToInsert: Record<string, unknown>[] = []
|
||||
const textsToInsert: Record<string, unknown>[] = []
|
||||
const numbersToInsert: Record<string, unknown>[] = []
|
||||
const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}
|
||||
const selectsToInsert: { [selectTableName: string]: Record<string, unknown>[] } = {}
|
||||
|
||||
// If there are locale rows with data, add the parent and locale to each
|
||||
if (Object.keys(rowToInsert.locales).length > 0) {
|
||||
Object.entries(rowToInsert.locales).forEach(([locale, localeRow]) => {
|
||||
localeRow._parentID = insertedRow.id
|
||||
localeRow._locale = locale
|
||||
localesToInsert.push(localeRow)
|
||||
})
|
||||
} else {
|
||||
if (adapter.allowIDOnCreate && data.id) {
|
||||
rowToInsert.row.id = data.id
|
||||
}
|
||||
;[insertedRow] = await adapter.insert({
|
||||
db,
|
||||
tableName,
|
||||
values: rowToInsert.row,
|
||||
})
|
||||
}
|
||||
|
||||
// If there are relationships, add parent to each
|
||||
if (rowToInsert.relationships.length > 0) {
|
||||
rowToInsert.relationships.forEach((relation) => {
|
||||
relation.parent = insertedRow.id
|
||||
relationsToInsert.push(relation)
|
||||
})
|
||||
}
|
||||
const localesToInsert: Record<string, unknown>[] = []
|
||||
const relationsToInsert: Record<string, unknown>[] = []
|
||||
const textsToInsert: Record<string, unknown>[] = []
|
||||
const numbersToInsert: Record<string, unknown>[] = []
|
||||
const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}
|
||||
const selectsToInsert: { [selectTableName: string]: Record<string, unknown>[] } = {}
|
||||
|
||||
// If there are texts, add parent to each
|
||||
if (rowToInsert.texts.length > 0) {
|
||||
rowToInsert.texts.forEach((textRow) => {
|
||||
textRow.parent = insertedRow.id
|
||||
textsToInsert.push(textRow)
|
||||
})
|
||||
}
|
||||
// If there are locale rows with data, add the parent and locale to each
|
||||
if (Object.keys(rowToInsert.locales).length > 0) {
|
||||
Object.entries(rowToInsert.locales).forEach(([locale, localeRow]) => {
|
||||
localeRow._parentID = insertedRow.id
|
||||
localeRow._locale = locale
|
||||
localesToInsert.push(localeRow)
|
||||
})
|
||||
}
|
||||
|
||||
// If there are numbers, add parent to each
|
||||
if (rowToInsert.numbers.length > 0) {
|
||||
rowToInsert.numbers.forEach((numberRow) => {
|
||||
numberRow.parent = insertedRow.id
|
||||
numbersToInsert.push(numberRow)
|
||||
})
|
||||
}
|
||||
// If there are relationships, add parent to each
|
||||
if (rowToInsert.relationships.length > 0) {
|
||||
rowToInsert.relationships.forEach((relation) => {
|
||||
relation.parent = insertedRow.id
|
||||
relationsToInsert.push(relation)
|
||||
})
|
||||
}
|
||||
|
||||
// If there are selects, add parent to each, and then
|
||||
// store by table name and rows
|
||||
if (Object.keys(rowToInsert.selects).length > 0) {
|
||||
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows]) => {
|
||||
selectsToInsert[selectTableName] = []
|
||||
// If there are texts, add parent to each
|
||||
if (rowToInsert.texts.length > 0) {
|
||||
rowToInsert.texts.forEach((textRow) => {
|
||||
textRow.parent = insertedRow.id
|
||||
textsToInsert.push(textRow)
|
||||
})
|
||||
}
|
||||
|
||||
selectRows.forEach((row) => {
|
||||
if (typeof row.parent === 'undefined') {
|
||||
row.parent = insertedRow.id
|
||||
}
|
||||
// If there are numbers, add parent to each
|
||||
if (rowToInsert.numbers.length > 0) {
|
||||
rowToInsert.numbers.forEach((numberRow) => {
|
||||
numberRow.parent = insertedRow.id
|
||||
numbersToInsert.push(numberRow)
|
||||
})
|
||||
}
|
||||
|
||||
selectsToInsert[selectTableName].push(row)
|
||||
})
|
||||
})
|
||||
}
|
||||
// If there are selects, add parent to each, and then
|
||||
// store by table name and rows
|
||||
if (Object.keys(rowToInsert.selects).length > 0) {
|
||||
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows]) => {
|
||||
selectsToInsert[selectTableName] = []
|
||||
|
||||
// If there are blocks, add parent to each, and then
|
||||
// store by table name and rows
|
||||
Object.keys(rowToInsert.blocks).forEach((tableName) => {
|
||||
rowToInsert.blocks[tableName].forEach((blockRow) => {
|
||||
blockRow.row._parentID = insertedRow.id
|
||||
if (!blocksToInsert[tableName]) {
|
||||
blocksToInsert[tableName] = []
|
||||
selectRows.forEach((row) => {
|
||||
if (typeof row.parent === 'undefined') {
|
||||
row.parent = insertedRow.id
|
||||
}
|
||||
if (blockRow.row.uuid) {
|
||||
delete blockRow.row.uuid
|
||||
}
|
||||
blocksToInsert[tableName].push(blockRow)
|
||||
|
||||
selectsToInsert[selectTableName].push(row)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT LOCALES
|
||||
// //////////////////////////////////
|
||||
|
||||
if (localesToInsert.length > 0) {
|
||||
const localeTableName = `${tableName}${adapter.localesSuffix}`
|
||||
const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`]
|
||||
|
||||
if (operation === 'update') {
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: localeTableName,
|
||||
where: eq(localeTable._parentID, insertedRow.id),
|
||||
})
|
||||
// If there are blocks, add parent to each, and then
|
||||
// store by table name and rows
|
||||
Object.keys(rowToInsert.blocks).forEach((tableName) => {
|
||||
rowToInsert.blocks[tableName].forEach((blockRow) => {
|
||||
blockRow.row._parentID = insertedRow.id
|
||||
if (!blocksToInsert[tableName]) {
|
||||
blocksToInsert[tableName] = []
|
||||
}
|
||||
if (blockRow.row.uuid) {
|
||||
delete blockRow.row.uuid
|
||||
}
|
||||
blocksToInsert[tableName].push(blockRow)
|
||||
})
|
||||
})
|
||||
|
||||
await adapter.insert({
|
||||
// //////////////////////////////////
|
||||
// INSERT LOCALES
|
||||
// //////////////////////////////////
|
||||
|
||||
if (localesToInsert.length > 0) {
|
||||
const localeTableName = `${tableName}${adapter.localesSuffix}`
|
||||
const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`]
|
||||
|
||||
if (operation === 'update') {
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: localeTableName,
|
||||
values: localesToInsert,
|
||||
where: eq(localeTable._parentID, insertedRow.id),
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT RELATIONSHIPS
|
||||
// //////////////////////////////////
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: localeTableName,
|
||||
values: localesToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
|
||||
// //////////////////////////////////
|
||||
// INSERT RELATIONSHIPS
|
||||
// //////////////////////////////////
|
||||
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...relationsToInsert, ...rowToInsert.relationshipsToDelete],
|
||||
tableName: relationshipsTableName,
|
||||
})
|
||||
}
|
||||
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
|
||||
|
||||
if (relationsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: relationshipsTableName,
|
||||
values: relationsToInsert,
|
||||
})
|
||||
}
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...relationsToInsert, ...rowToInsert.relationshipsToDelete],
|
||||
tableName: relationshipsTableName,
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany TEXTS
|
||||
// //////////////////////////////////
|
||||
if (relationsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: relationshipsTableName,
|
||||
values: relationsToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
const textsTableName = `${tableName}_texts`
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany TEXTS
|
||||
// //////////////////////////////////
|
||||
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...textsToInsert, ...rowToInsert.textsToDelete],
|
||||
tableName: textsTableName,
|
||||
})
|
||||
}
|
||||
const textsTableName = `${tableName}_texts`
|
||||
|
||||
if (textsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: textsTableName,
|
||||
values: textsToInsert,
|
||||
})
|
||||
}
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...textsToInsert, ...rowToInsert.textsToDelete],
|
||||
tableName: textsTableName,
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany NUMBERS
|
||||
// //////////////////////////////////
|
||||
if (textsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: textsTableName,
|
||||
values: textsToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
const numbersTableName = `${tableName}_numbers`
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany NUMBERS
|
||||
// //////////////////////////////////
|
||||
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...numbersToInsert, ...rowToInsert.numbersToDelete],
|
||||
tableName: numbersTableName,
|
||||
})
|
||||
}
|
||||
const numbersTableName = `${tableName}_numbers`
|
||||
|
||||
if (numbersToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: numbersTableName,
|
||||
values: numbersToInsert,
|
||||
})
|
||||
}
|
||||
if (operation === 'update') {
|
||||
await deleteExistingRowsByPath({
|
||||
adapter,
|
||||
db,
|
||||
localeColumnName: 'locale',
|
||||
parentColumnName: 'parent',
|
||||
parentID: insertedRow.id,
|
||||
pathColumnName: 'path',
|
||||
rows: [...numbersToInsert, ...rowToInsert.numbersToDelete],
|
||||
tableName: numbersTableName,
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT BLOCKS
|
||||
// //////////////////////////////////
|
||||
if (numbersToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: numbersTableName,
|
||||
values: numbersToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
|
||||
// //////////////////////////////////
|
||||
// INSERT BLOCKS
|
||||
// //////////////////////////////////
|
||||
|
||||
if (operation === 'update') {
|
||||
for (const tableName of rowToInsert.blocksToDelete) {
|
||||
const blockTable = adapter.tables[tableName]
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName,
|
||||
where: eq(blockTable._parentID, insertedRow.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
|
||||
|
||||
// When versions are enabled, adapter is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
for (const [tableName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
insertedBlockRows[tableName] = await adapter.insert({
|
||||
if (operation === 'update') {
|
||||
for (const tableName of rowToInsert.blocksToDelete) {
|
||||
const blockTable = adapter.tables[tableName]
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName,
|
||||
values: blockRows.map(({ row }) => row),
|
||||
})
|
||||
|
||||
insertedBlockRows[tableName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
arraysBlocksUUIDMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
|
||||
const blockLocaleIndexMap: number[] = []
|
||||
|
||||
const blockLocaleRowsToInsert = blockRows.reduce((acc, blockRow, i) => {
|
||||
if (Object.entries(blockRow.locales).length > 0) {
|
||||
Object.entries(blockRow.locales).forEach(([blockLocale, blockLocaleData]) => {
|
||||
if (Object.keys(blockLocaleData).length > 0) {
|
||||
blockLocaleData._parentID = blockRow.row.id
|
||||
blockLocaleData._locale = blockLocale
|
||||
acc.push(blockLocaleData)
|
||||
blockLocaleIndexMap.push(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (blockLocaleRowsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: `${tableName}${adapter.localesSuffix}`,
|
||||
values: blockLocaleRowsToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
await insertArrays({
|
||||
adapter,
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: insertedBlockRows[tableName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
where: eq(blockTable._parentID, insertedRow.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT ARRAYS RECURSIVELY
|
||||
// //////////////////////////////////
|
||||
// When versions are enabled, adapter is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
if (operation === 'update') {
|
||||
for (const arrayTableName of Object.keys(rowToInsert.arrays)) {
|
||||
await deleteExistingArrayRows({
|
||||
adapter,
|
||||
db,
|
||||
parentID: insertedRow.id,
|
||||
tableName: arrayTableName,
|
||||
for (const [tableName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
insertedBlockRows[tableName] = await adapter.insert({
|
||||
db,
|
||||
tableName,
|
||||
values: blockRows.map(({ row }) => row),
|
||||
})
|
||||
|
||||
insertedBlockRows[tableName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
(typeof row.id === 'string' || typeof row.id === 'number')
|
||||
) {
|
||||
arraysBlocksUUIDMap[row._uuid] = row.id
|
||||
}
|
||||
})
|
||||
|
||||
const blockLocaleIndexMap: number[] = []
|
||||
|
||||
const blockLocaleRowsToInsert = blockRows.reduce((acc, blockRow, i) => {
|
||||
if (Object.entries(blockRow.locales).length > 0) {
|
||||
Object.entries(blockRow.locales).forEach(([blockLocale, blockLocaleData]) => {
|
||||
if (Object.keys(blockLocaleData).length > 0) {
|
||||
blockLocaleData._parentID = blockRow.row.id
|
||||
blockLocaleData._locale = blockLocale
|
||||
acc.push(blockLocaleData)
|
||||
blockLocaleIndexMap.push(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (blockLocaleRowsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: `${tableName}${adapter.localesSuffix}`,
|
||||
values: blockLocaleRowsToInsert,
|
||||
})
|
||||
}
|
||||
|
||||
await insertArrays({
|
||||
adapter,
|
||||
arrays: [rowToInsert.arrays],
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: [insertedRow],
|
||||
parentRows: insertedBlockRows[tableName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany SELECTS
|
||||
// //////////////////////////////////
|
||||
// //////////////////////////////////
|
||||
// INSERT ARRAYS RECURSIVELY
|
||||
// //////////////////////////////////
|
||||
|
||||
for (const [selectTableName, tableRows] of Object.entries(selectsToInsert)) {
|
||||
const selectTable = adapter.tables[selectTableName]
|
||||
if (operation === 'update') {
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: selectTableName,
|
||||
where: eq(selectTable.parent, insertedRow.id),
|
||||
})
|
||||
}
|
||||
if (operation === 'update') {
|
||||
for (const arrayTableName of Object.keys(rowToInsert.arrays)) {
|
||||
await deleteExistingArrayRows({
|
||||
adapter,
|
||||
db,
|
||||
parentID: insertedRow.id,
|
||||
tableName: arrayTableName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
|
||||
tableRows.forEach((row: any) => {
|
||||
if (row.parent in arraysBlocksUUIDMap) {
|
||||
row.parent = arraysBlocksUUIDMap[row.parent]
|
||||
}
|
||||
})
|
||||
}
|
||||
await insertArrays({
|
||||
adapter,
|
||||
arrays: [rowToInsert.arrays],
|
||||
db,
|
||||
parentRows: [insertedRow],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
|
||||
if (tableRows.length) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: selectTableName,
|
||||
values: tableRows,
|
||||
})
|
||||
}
|
||||
// //////////////////////////////////
|
||||
// INSERT hasMany SELECTS
|
||||
// //////////////////////////////////
|
||||
|
||||
for (const [selectTableName, tableRows] of Object.entries(selectsToInsert)) {
|
||||
const selectTable = adapter.tables[selectTableName]
|
||||
if (operation === 'update') {
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: selectTableName,
|
||||
where: eq(selectTable.parent, insertedRow.id),
|
||||
})
|
||||
}
|
||||
|
||||
// //////////////////////////////////
|
||||
// Error Handling
|
||||
// //////////////////////////////////
|
||||
} catch (caughtError) {
|
||||
// Unique constraint violation error
|
||||
// '23505' is the code for PostgreSQL, and 'SQLITE_CONSTRAINT_UNIQUE' is for SQLite
|
||||
|
||||
let error = caughtError
|
||||
if (typeof caughtError === 'object' && 'cause' in caughtError) {
|
||||
error = caughtError.cause
|
||||
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
|
||||
tableRows.forEach((row: any) => {
|
||||
if (row.parent in arraysBlocksUUIDMap) {
|
||||
row.parent = arraysBlocksUUIDMap[row.parent]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
let fieldName: null | string = null
|
||||
// We need to try and find the right constraint for the field but if we can't we fallback to a generic message
|
||||
if (error.code === '23505') {
|
||||
// For PostgreSQL, we can try to extract the field name from the error constraint
|
||||
if (adapter.fieldConstraints?.[tableName]?.[error.constraint]) {
|
||||
fieldName = adapter.fieldConstraints[tableName]?.[error.constraint]
|
||||
} else {
|
||||
const replacement = `${tableName}_`
|
||||
if (tableRows.length) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: selectTableName,
|
||||
values: tableRows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (error.constraint.includes(replacement)) {
|
||||
const replacedConstraint = error.constraint.replace(replacement, '')
|
||||
// //////////////////////////////////
|
||||
// Error Handling
|
||||
// //////////////////////////////////
|
||||
} catch (caughtError) {
|
||||
// Unique constraint violation error
|
||||
// '23505' is the code for PostgreSQL, and 'SQLITE_CONSTRAINT_UNIQUE' is for SQLite
|
||||
|
||||
if (replacedConstraint && adapter.fieldConstraints[tableName]?.[replacedConstraint]) {
|
||||
fieldName = adapter.fieldConstraints[tableName][replacedConstraint]
|
||||
}
|
||||
let error = caughtError
|
||||
if (typeof caughtError === 'object' && 'cause' in caughtError) {
|
||||
error = caughtError.cause
|
||||
}
|
||||
|
||||
if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
let fieldName: null | string = null
|
||||
// We need to try and find the right constraint for the field but if we can't we fallback to a generic message
|
||||
if (error.code === '23505') {
|
||||
// For PostgreSQL, we can try to extract the field name from the error constraint
|
||||
if (adapter.fieldConstraints?.[tableName]?.[error.constraint]) {
|
||||
fieldName = adapter.fieldConstraints[tableName]?.[error.constraint]
|
||||
} else {
|
||||
const replacement = `${tableName}_`
|
||||
|
||||
if (error.constraint.includes(replacement)) {
|
||||
const replacedConstraint = error.constraint.replace(replacement, '')
|
||||
|
||||
if (replacedConstraint && adapter.fieldConstraints[tableName]?.[replacedConstraint]) {
|
||||
fieldName = adapter.fieldConstraints[tableName][replacedConstraint]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fieldName) {
|
||||
// Last case scenario we extract the key and value from the detail on the error
|
||||
const detail = error.detail
|
||||
const regex = /Key \(([^)]+)\)=\(([^)]+)\)/
|
||||
const match: string[] = detail.match(regex)
|
||||
|
||||
if (match && match[1]) {
|
||||
const key = match[1]
|
||||
|
||||
fieldName = key
|
||||
}
|
||||
}
|
||||
} else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
/**
|
||||
* For SQLite, we can try to extract the field name from the error message
|
||||
* The message typically looks like:
|
||||
* "UNIQUE constraint failed: table_name.field_name"
|
||||
*/
|
||||
const regex = /UNIQUE constraint failed: ([^.]+)\.([^.]+)/
|
||||
const match: string[] = error.message.match(regex)
|
||||
|
||||
if (match && match[2]) {
|
||||
if (adapter.fieldConstraints[tableName]) {
|
||||
fieldName = adapter.fieldConstraints[tableName][`${match[2]}_idx`]
|
||||
}
|
||||
|
||||
if (!fieldName) {
|
||||
// Last case scenario we extract the key and value from the detail on the error
|
||||
const detail = error.detail
|
||||
const regex = /Key \(([^)]+)\)=\(([^)]+)\)/
|
||||
const match: string[] = detail.match(regex)
|
||||
|
||||
if (match && match[1]) {
|
||||
const key = match[1]
|
||||
|
||||
fieldName = key
|
||||
}
|
||||
}
|
||||
} else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
/**
|
||||
* For SQLite, we can try to extract the field name from the error message
|
||||
* The message typically looks like:
|
||||
* "UNIQUE constraint failed: table_name.field_name"
|
||||
*/
|
||||
const regex = /UNIQUE constraint failed: ([^.]+)\.([^.]+)/
|
||||
const match: string[] = error.message.match(regex)
|
||||
|
||||
if (match && match[2]) {
|
||||
if (adapter.fieldConstraints[tableName]) {
|
||||
fieldName = adapter.fieldConstraints[tableName][`${match[2]}_idx`]
|
||||
}
|
||||
|
||||
if (!fieldName) {
|
||||
fieldName = match[2]
|
||||
}
|
||||
fieldName = match[2]
|
||||
}
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
{
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
req,
|
||||
},
|
||||
req?.t,
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
{
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
req,
|
||||
},
|
||||
req?.t,
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
test/database/config.postgreslogs.ts
Normal file
19
test/database/config.postgreslogs.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { getConfig } from './getConfig.js'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
export const databaseAdapter = postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
|
||||
},
|
||||
logger: true,
|
||||
})
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
...config,
|
||||
db: databaseAdapter,
|
||||
})
|
||||
@@ -1,933 +1,4 @@
|
||||
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 { randomUUID } from 'crypto'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { seed } from './seed.js'
|
||||
import {
|
||||
customIDsSlug,
|
||||
customSchemaSlug,
|
||||
defaultValuesSlug,
|
||||
errorOnUnnamedFieldsSlug,
|
||||
fakeCustomIDsSlug,
|
||||
fieldsPersistanceSlug,
|
||||
pgMigrationSlug,
|
||||
placesSlug,
|
||||
postsSlug,
|
||||
relationASlug,
|
||||
relationBSlug,
|
||||
relationshipsMigrationSlug,
|
||||
} from './shared.js'
|
||||
import { getConfig } from './getConfig.js'
|
||||
|
||||
const defaultValueField: TextField = {
|
||||
name: 'defaultValue',
|
||||
type: 'text',
|
||||
defaultValue: 'default value from database',
|
||||
}
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'categories',
|
||||
versions: { drafts: true },
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'categories-custom-id',
|
||||
versions: { drafts: true },
|
||||
fields: [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: postsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
// access: { read: () => false },
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories',
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories-custom-id',
|
||||
name: 'categoryCustomID',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-third',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'nested',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-fourth',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'nested',
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'D1',
|
||||
fields: [
|
||||
{
|
||||
name: 'D2',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'D3',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'D4',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
label: 'Collapsible2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Tab1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Collapsible2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Tab1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hasTransaction',
|
||||
type: 'checkbox',
|
||||
hooks: {
|
||||
beforeChange: [({ req }) => !!req.transactionID],
|
||||
},
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'throwAfterChange',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
hooks: {
|
||||
afterChange: [
|
||||
({ value }) => {
|
||||
if (value) {
|
||||
throw new Error('throw after change')
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'arrayWithIDs',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocksWithIDs',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-first',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'group',
|
||||
fields: [{ name: 'text', type: 'text' }],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab',
|
||||
fields: [{ name: 'text', type: 'text' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
beforeOperation: [
|
||||
({ args, operation, req }) => {
|
||||
if (operation === 'update') {
|
||||
const defaultIDType = req.payload.db.defaultIDType
|
||||
|
||||
if (defaultIDType === 'number' && typeof args.id === 'string') {
|
||||
throw new Error('ID was not sanitized to a number properly')
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: errorOnUnnamedFieldsSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'UnnamedTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'groupWithinUnnamedTab',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: defaultValuesSlug,
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'point',
|
||||
type: 'point',
|
||||
defaultValue: [10, 20],
|
||||
},
|
||||
{
|
||||
name: 'escape',
|
||||
type: 'text',
|
||||
defaultValue: "Thanks, we're excited for you to join us.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: relationASlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Relation As',
|
||||
singular: 'Relation A',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: relationBSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Relation Bs',
|
||||
singular: 'Relation B',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: pgMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'relation1',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'myArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation2',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
{
|
||||
name: 'mySubArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation3',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'myGroup',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation4',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'myBlocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'myBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation5',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'relation6',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: customSchemaSlug,
|
||||
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-second',
|
||||
dbName: 'customBlocks',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'localizedText',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: placesSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relations',
|
||||
admin: { useAsTitle: 'postTitle' },
|
||||
access: { read: () => true },
|
||||
fields: [
|
||||
{
|
||||
name: 'postTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'postTitleHidden',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'postCategoryTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.category.title',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryID',
|
||||
type: 'json',
|
||||
virtual: 'post.category.id',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryCustomID',
|
||||
type: 'number',
|
||||
virtual: 'post.categoryCustomID.id',
|
||||
},
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'json',
|
||||
virtual: 'post.id',
|
||||
},
|
||||
{
|
||||
name: 'postLocalized',
|
||||
type: 'text',
|
||||
virtual: 'post.localized',
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'customID',
|
||||
type: 'relationship',
|
||||
relationTo: 'custom-ids',
|
||||
},
|
||||
{
|
||||
name: 'customIDValue',
|
||||
type: 'text',
|
||||
virtual: 'customID.id',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fieldsPersistanceSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
},
|
||||
{
|
||||
name: 'textHooked',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
hooks: { afterRead: [() => 'hooked'] },
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
virtual: true,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinRow',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinCollapsible',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
label: 'Colllapsible',
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'tab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinTabs',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: customIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ value, operation }) => {
|
||||
if (operation === 'create') {
|
||||
return randomUUID()
|
||||
}
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fakeCustomIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'myTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: relationshipsMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'default-values',
|
||||
name: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['default-values'],
|
||||
name: 'relationship_2',
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: 'compound-indexes',
|
||||
fields: [
|
||||
{
|
||||
name: 'one',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'two',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'three',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'four',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
fields: ['one', 'two'],
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
fields: ['three', 'group.four'],
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'aliases',
|
||||
fields: [
|
||||
{
|
||||
name: 'thisIsALongFieldNameThatCanCauseAPostgresErrorEvenThoughWeSetAShorterDBName',
|
||||
dbName: 'shortname',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedArray',
|
||||
type: 'array',
|
||||
dbName: 'short_nested_1',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'blocks-docs',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
localized: true,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'cta',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'testBlocksLocalized',
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'cta',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'testBlocks',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'unique-fields',
|
||||
fields: [
|
||||
{
|
||||
name: 'slugField',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'header',
|
||||
fields: [
|
||||
{
|
||||
name: 'itemsLvl1',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl1',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl2',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl2',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl3',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl3',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl4',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl4',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'global',
|
||||
dbName: 'customGlobal',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: 'global-2',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'global-3',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relation-global',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'postTitle',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'post',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
|
||||
export const postDoc = {
|
||||
title: 'test post',
|
||||
}
|
||||
export default buildConfigWithDefaults(getConfig())
|
||||
|
||||
942
test/database/getConfig.ts
Normal file
942
test/database/getConfig.ts
Normal file
@@ -0,0 +1,942 @@
|
||||
import type { Config, TextField } from 'payload'
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { seed } from './seed.js'
|
||||
import {
|
||||
customIDsSlug,
|
||||
customSchemaSlug,
|
||||
defaultValuesSlug,
|
||||
errorOnUnnamedFieldsSlug,
|
||||
fakeCustomIDsSlug,
|
||||
fieldsPersistanceSlug,
|
||||
pgMigrationSlug,
|
||||
placesSlug,
|
||||
postsSlug,
|
||||
relationASlug,
|
||||
relationBSlug,
|
||||
relationshipsMigrationSlug,
|
||||
} from './shared.js'
|
||||
|
||||
const defaultValueField: TextField = {
|
||||
name: 'defaultValue',
|
||||
type: 'text',
|
||||
defaultValue: 'default value from database',
|
||||
}
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const getConfig: () => Partial<Config> = () => ({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'categories',
|
||||
versions: { drafts: true },
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'simple',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'categories-custom-id',
|
||||
versions: { drafts: true },
|
||||
fields: [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: postsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
// access: { read: () => false },
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories',
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories-custom-id',
|
||||
name: 'categoryCustomID',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-third',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'nested',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-fourth',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'nested',
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'D1',
|
||||
fields: [
|
||||
{
|
||||
name: 'D2',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'D3',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'D4',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
label: 'Collapsible2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Tab1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Collapsible2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: 'Tab1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'hasTransaction',
|
||||
type: 'checkbox',
|
||||
hooks: {
|
||||
beforeChange: [({ req }) => !!req.transactionID],
|
||||
},
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'throwAfterChange',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
hooks: {
|
||||
afterChange: [
|
||||
({ value }) => {
|
||||
if (value) {
|
||||
throw new Error('throw after change')
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'arrayWithIDs',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocksWithIDs',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block-first',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'group',
|
||||
fields: [{ name: 'text', type: 'text' }],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab',
|
||||
fields: [{ name: 'text', type: 'text' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
beforeOperation: [
|
||||
({ args, operation, req }) => {
|
||||
if (operation === 'update') {
|
||||
const defaultIDType = req.payload.db.defaultIDType
|
||||
|
||||
if (defaultIDType === 'number' && typeof args.id === 'string') {
|
||||
throw new Error('ID was not sanitized to a number properly')
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: errorOnUnnamedFieldsSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'UnnamedTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'groupWithinUnnamedTab',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: defaultValuesSlug,
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'point',
|
||||
type: 'point',
|
||||
defaultValue: [10, 20],
|
||||
},
|
||||
{
|
||||
name: 'escape',
|
||||
type: 'text',
|
||||
defaultValue: "Thanks, we're excited for you to join us.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: relationASlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Relation As',
|
||||
singular: 'Relation A',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: relationBSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: 'Relation Bs',
|
||||
singular: 'Relation B',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: pgMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'relation1',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'myArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation2',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
{
|
||||
name: 'mySubArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation3',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'myGroup',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation4',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'myBlocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'myBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation5',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'relation6',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: customSchemaSlug,
|
||||
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-second',
|
||||
dbName: 'customBlocks',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'localizedText',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: placesSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relations',
|
||||
admin: { useAsTitle: 'postTitle' },
|
||||
access: { read: () => true },
|
||||
fields: [
|
||||
{
|
||||
name: 'postTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'postTitleHidden',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'postCategoryTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.category.title',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryID',
|
||||
type: 'json',
|
||||
virtual: 'post.category.id',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryCustomID',
|
||||
type: 'number',
|
||||
virtual: 'post.categoryCustomID.id',
|
||||
},
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'json',
|
||||
virtual: 'post.id',
|
||||
},
|
||||
{
|
||||
name: 'postLocalized',
|
||||
type: 'text',
|
||||
virtual: 'post.localized',
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'customID',
|
||||
type: 'relationship',
|
||||
relationTo: 'custom-ids',
|
||||
},
|
||||
{
|
||||
name: 'customIDValue',
|
||||
type: 'text',
|
||||
virtual: 'customID.id',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fieldsPersistanceSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
},
|
||||
{
|
||||
name: 'textHooked',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
hooks: { afterRead: [() => 'hooked'] },
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
virtual: true,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinRow',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinCollapsible',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
label: 'Colllapsible',
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'tab',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'textWithinTabs',
|
||||
virtual: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: customIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ value, operation }) => {
|
||||
if (operation === 'create') {
|
||||
return randomUUID()
|
||||
}
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fakeCustomIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'myTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: relationshipsMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'default-values',
|
||||
name: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['default-values'],
|
||||
name: 'relationship_2',
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: 'compound-indexes',
|
||||
fields: [
|
||||
{
|
||||
name: 'one',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'two',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'three',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'four',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
fields: ['one', 'two'],
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
fields: ['three', 'group.four'],
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'aliases',
|
||||
fields: [
|
||||
{
|
||||
name: 'thisIsALongFieldNameThatCanCauseAPostgresErrorEvenThoughWeSetAShorterDBName',
|
||||
dbName: 'shortname',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedArray',
|
||||
type: 'array',
|
||||
dbName: 'short_nested_1',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'blocks-docs',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
localized: true,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'cta',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'testBlocksLocalized',
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'cta',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
name: 'testBlocks',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'unique-fields',
|
||||
fields: [
|
||||
{
|
||||
name: 'slugField',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'header',
|
||||
fields: [
|
||||
{
|
||||
name: 'itemsLvl1',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl1',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl2',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl2',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl3',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl3',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'itemsLvl4',
|
||||
type: 'array',
|
||||
dbName: 'header_items_lvl4',
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'global',
|
||||
dbName: 'customGlobal',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: 'global-2',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'global-3',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relation-global',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'postTitle',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'post',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
@@ -68,6 +68,7 @@ export interface Config {
|
||||
blocks: {};
|
||||
collections: {
|
||||
categories: Category;
|
||||
simple: Simple;
|
||||
'categories-custom-id': CategoriesCustomId;
|
||||
posts: Post;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedField;
|
||||
@@ -94,6 +95,7 @@ export interface Config {
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
simple: SimpleSelect<false> | SimpleSelect<true>;
|
||||
'categories-custom-id': CategoriesCustomIdSelect<false> | CategoriesCustomIdSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedFieldsSelect<false> | ErrorOnUnnamedFieldsSelect<true>;
|
||||
@@ -172,6 +174,17 @@ export interface Category {
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "simple".
|
||||
*/
|
||||
export interface Simple {
|
||||
id: string;
|
||||
text?: string | null;
|
||||
number?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories-custom-id".
|
||||
@@ -608,6 +621,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'simple';
|
||||
value: string | Simple;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'categories-custom-id';
|
||||
value: number | CategoriesCustomId;
|
||||
@@ -736,6 +753,16 @@ export interface CategoriesSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "simple_select".
|
||||
*/
|
||||
export interface SimpleSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
number?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories-custom-id_select".
|
||||
|
||||
91
test/database/postgres-logs.int.spec.ts
Normal file
91
test/database/postgres-logs.int.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import assert from 'assert'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const describePostgres = process.env.PAYLOAD_DATABASE?.startsWith('postgres')
|
||||
? describe
|
||||
: describe.skip
|
||||
|
||||
let payload: Payload
|
||||
|
||||
describePostgres('database - postgres logs', () => {
|
||||
beforeAll(async () => {
|
||||
const initialized = await initPayloadInt(
|
||||
dirname,
|
||||
undefined,
|
||||
undefined,
|
||||
'config.postgreslogs.ts',
|
||||
)
|
||||
assert(initialized.payload)
|
||||
assert(initialized.restClient)
|
||||
;({ payload } = initialized)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await payload.destroy()
|
||||
})
|
||||
|
||||
it('ensure simple update uses optimized upsertRow with returning()', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'simple',
|
||||
data: {
|
||||
text: 'Some title',
|
||||
number: 5,
|
||||
},
|
||||
})
|
||||
|
||||
// Count every console log
|
||||
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
const result: any = await payload.db.updateOne({
|
||||
collection: 'simple',
|
||||
id: doc.id,
|
||||
data: {
|
||||
text: 'Updated Title',
|
||||
number: 5,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.text).toEqual('Updated Title')
|
||||
expect(result.number).toEqual(5) // Ensure the update did not reset the number field
|
||||
|
||||
expect(consoleCount).toHaveBeenCalledTimes(1) // Should be 1 single sql call if the optimization is used. If not, this would be 2 calls
|
||||
consoleCount.mockRestore()
|
||||
})
|
||||
|
||||
it('ensure simple update of complex collection uses optimized upsertRow without returning()', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Some title',
|
||||
number: 5,
|
||||
},
|
||||
})
|
||||
|
||||
// Count every console log
|
||||
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
const result: any = await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: doc.id,
|
||||
data: {
|
||||
title: 'Updated Title',
|
||||
number: 5,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.title).toEqual('Updated Title')
|
||||
expect(result.number).toEqual(5) // Ensure the update did not reset the number field
|
||||
|
||||
expect(consoleCount).toHaveBeenCalledTimes(2) // Should be 2 sql call if the optimization is used (update + find). If not, this would be 5 calls
|
||||
consoleCount.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -12,11 +12,11 @@ import { fileURLToPath } from 'url'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const describeToUse = process.env.PAYLOAD_DATABASE?.startsWith('postgres')
|
||||
const describePostgres = process.env.PAYLOAD_DATABASE?.startsWith('postgres')
|
||||
? describe
|
||||
: describe.skip
|
||||
|
||||
describeToUse('postgres vector custom column', () => {
|
||||
describePostgres('postgres vector custom column', () => {
|
||||
const vectorColumnQueryTest = async (vectorType: string) => {
|
||||
const {
|
||||
databaseAdapter,
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { seedDB } from '../helpers/seed.js'
|
||||
import { collectionSlugs } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const _seed = async (_payload: Payload) => {
|
||||
await _payload.create({
|
||||
|
||||
@@ -20,18 +20,3 @@ export const customIDsSlug = 'custom-ids'
|
||||
export const fakeCustomIDsSlug = 'fake-custom-ids'
|
||||
|
||||
export const relationshipsMigrationSlug = 'relationships-migration'
|
||||
|
||||
export const collectionSlugs = [
|
||||
postsSlug,
|
||||
errorOnUnnamedFieldsSlug,
|
||||
defaultValuesSlug,
|
||||
relationASlug,
|
||||
relationBSlug,
|
||||
pgMigrationSlug,
|
||||
customSchemaSlug,
|
||||
placesSlug,
|
||||
fieldsPersistanceSlug,
|
||||
customIDsSlug,
|
||||
fakeCustomIDsSlug,
|
||||
relationshipsMigrationSlug,
|
||||
]
|
||||
|
||||
19
test/select/config.postgreslogs.ts
Normal file
19
test/select/config.postgreslogs.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { getConfig } from './getConfig.js'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
export const databaseAdapter = postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
|
||||
},
|
||||
logger: true,
|
||||
})
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
...config,
|
||||
db: databaseAdapter,
|
||||
})
|
||||
@@ -1,122 +1,4 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import type { Post } from './payload-types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { CustomID } from './collections/CustomID/index.js'
|
||||
import { DeepPostsCollection } from './collections/DeepPosts/index.js'
|
||||
import { ForceSelect } from './collections/ForceSelect/index.js'
|
||||
import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js'
|
||||
import { Pages } from './collections/Pages/index.js'
|
||||
import { Points } from './collections/Points/index.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
import { UsersCollection } from './collections/Users/index.js'
|
||||
import { VersionedPostsCollection } from './collections/VersionedPosts/index.js'
|
||||
import { getConfig } from './getConfig.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
// ...extend config here
|
||||
collections: [
|
||||
PostsCollection,
|
||||
LocalizedPostsCollection,
|
||||
VersionedPostsCollection,
|
||||
DeepPostsCollection,
|
||||
Pages,
|
||||
Points,
|
||||
ForceSelect,
|
||||
{
|
||||
slug: 'upload',
|
||||
fields: [],
|
||||
upload: {
|
||||
staticDir: path.resolve(dirname, 'media'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'rels',
|
||||
fields: [],
|
||||
},
|
||||
CustomID,
|
||||
UsersCollection,
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'global-post',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'force-select-global',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'forceSelected',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'forceSelected',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
forceSelect: { array: { forceSelected: true }, forceSelected: true },
|
||||
} satisfies GlobalConfig<'force-select-global'>,
|
||||
],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
// // Create image
|
||||
// const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||
// const imageFile = await getFileByPath(imageFilePath)
|
||||
|
||||
// await payload.create({
|
||||
// collection: 'media',
|
||||
// data: {},
|
||||
// file: imageFile,
|
||||
// })
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
export default buildConfigWithDefaults(getConfig())
|
||||
|
||||
119
test/select/getConfig.ts
Normal file
119
test/select/getConfig.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { Config, GlobalConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { CustomID } from './collections/CustomID/index.js'
|
||||
import { DeepPostsCollection } from './collections/DeepPosts/index.js'
|
||||
import { ForceSelect } from './collections/ForceSelect/index.js'
|
||||
import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js'
|
||||
import { Pages } from './collections/Pages/index.js'
|
||||
import { Points } from './collections/Points/index.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
import { UsersCollection } from './collections/Users/index.js'
|
||||
import { VersionedPostsCollection } from './collections/VersionedPosts/index.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const getConfig: () => Partial<Config> = () => ({
|
||||
// ...extend config here
|
||||
collections: [
|
||||
PostsCollection,
|
||||
LocalizedPostsCollection,
|
||||
VersionedPostsCollection,
|
||||
DeepPostsCollection,
|
||||
Pages,
|
||||
Points,
|
||||
ForceSelect,
|
||||
{
|
||||
slug: 'upload',
|
||||
fields: [],
|
||||
upload: {
|
||||
staticDir: path.resolve(dirname, 'media'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'rels',
|
||||
fields: [],
|
||||
},
|
||||
CustomID,
|
||||
UsersCollection,
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'global-post',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'force-select-global',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'forceSelected',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'forceSelected',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
forceSelect: { array: { forceSelected: true }, forceSelected: true },
|
||||
} satisfies GlobalConfig<'force-select-global'>,
|
||||
],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
// // Create image
|
||||
// const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||
// const imageFile = await getFileByPath(imageFilePath)
|
||||
|
||||
// await payload.create({
|
||||
// collection: 'media',
|
||||
// data: {},
|
||||
// file: imageFile,
|
||||
// })
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
179
test/select/postgreslogs.int.spec.ts
Normal file
179
test/select/postgreslogs.int.spec.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { assert } from 'ts-essentials'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Point, Post } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const describePostgres = process.env.PAYLOAD_DATABASE === 'postgres' ? describe : describe.skip
|
||||
|
||||
describePostgres('Select - with postgres logs', () => {
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// Boilerplate test setup/teardown
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
beforeAll(async () => {
|
||||
const initialized = await initPayloadInt(
|
||||
dirname,
|
||||
undefined,
|
||||
undefined,
|
||||
'config.postgreslogs.ts',
|
||||
)
|
||||
assert(initialized.payload)
|
||||
assert(initialized.restClient)
|
||||
;({ payload } = initialized)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await payload.destroy()
|
||||
})
|
||||
|
||||
describe('Local API - Base', () => {
|
||||
let post: Post
|
||||
let postId: number | string
|
||||
|
||||
let point: Point
|
||||
let pointId: number | string
|
||||
|
||||
beforeEach(async () => {
|
||||
post = await createPost()
|
||||
postId = post.id
|
||||
|
||||
point = await createPoint()
|
||||
pointId = point.id
|
||||
})
|
||||
|
||||
// Clean up to safely mutate in each test
|
||||
afterEach(async () => {
|
||||
await payload.delete({ id: postId, collection: 'posts' })
|
||||
await payload.delete({ id: pointId, collection: 'points' })
|
||||
})
|
||||
|
||||
describe('Local API - operations', () => {
|
||||
it('ensure optimized db update is still used when using select', async () => {
|
||||
const post = await createPost()
|
||||
|
||||
// Count every console log
|
||||
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
const res = removeEmptyAndUndefined(
|
||||
(await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
text: 'new text',
|
||||
},
|
||||
select: { text: true, number: true },
|
||||
})) as any,
|
||||
)
|
||||
|
||||
expect(consoleCount).toHaveBeenCalledTimes(1) // Should be 1 single sql call if the optimization is used. If not, this would be 2 calls
|
||||
consoleCount.mockRestore()
|
||||
|
||||
expect(res.number).toEqual(1)
|
||||
expect(res.text).toEqual('new text')
|
||||
expect(res.id).toEqual(post.id)
|
||||
expect(Object.keys(res)).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function removeEmptyAndUndefined(obj: any): any {
|
||||
if (Array.isArray(obj)) {
|
||||
const cleanedArray = obj
|
||||
.map(removeEmptyAndUndefined)
|
||||
.filter(
|
||||
(item) =>
|
||||
item !== undefined && !(typeof item === 'object' && Object.keys(item).length === 0),
|
||||
)
|
||||
|
||||
return cleanedArray.length > 0 ? cleanedArray : undefined
|
||||
}
|
||||
|
||||
if (obj !== null && typeof obj === 'object') {
|
||||
const cleanedEntries = Object.entries(obj)
|
||||
.map(([key, value]) => [key, removeEmptyAndUndefined(value)])
|
||||
.filter(
|
||||
([, value]) =>
|
||||
value !== undefined &&
|
||||
!(
|
||||
typeof value === 'object' &&
|
||||
(Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0)
|
||||
),
|
||||
)
|
||||
|
||||
return cleanedEntries.length > 0 ? Object.fromEntries(cleanedEntries) : undefined
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
async function createPost() {
|
||||
const upload = await payload.create({
|
||||
collection: 'upload',
|
||||
data: {},
|
||||
filePath: path.resolve(dirname, 'image.jpg'),
|
||||
})
|
||||
|
||||
const relation = await payload.create({
|
||||
depth: 0,
|
||||
collection: 'rels',
|
||||
data: {},
|
||||
})
|
||||
|
||||
return payload.create({
|
||||
collection: 'posts',
|
||||
depth: 0,
|
||||
data: {
|
||||
number: 1,
|
||||
text: 'text',
|
||||
select: 'a',
|
||||
selectMany: ['a'],
|
||||
group: {
|
||||
number: 1,
|
||||
text: 'text',
|
||||
},
|
||||
hasMany: [relation],
|
||||
hasManyUpload: [upload],
|
||||
hasOne: relation,
|
||||
hasManyPoly: [{ relationTo: 'rels', value: relation }],
|
||||
hasOnePoly: { relationTo: 'rels', value: relation },
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'cta',
|
||||
ctaText: 'cta-text',
|
||||
text: 'text',
|
||||
},
|
||||
{
|
||||
blockType: 'intro',
|
||||
introText: 'intro-text',
|
||||
text: 'text',
|
||||
},
|
||||
],
|
||||
array: [
|
||||
{
|
||||
text: 'text',
|
||||
number: 1,
|
||||
},
|
||||
],
|
||||
tab: {
|
||||
text: 'text',
|
||||
number: 1,
|
||||
},
|
||||
unnamedTabNumber: 2,
|
||||
unnamedTabText: 'text2',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createPoint() {
|
||||
return payload.create({ collection: 'points', data: { text: 'some', point: [10, 20] } })
|
||||
}
|
||||
Reference in New Issue
Block a user