Files
payload/packages/db-postgres/src/transform/read/traverseFields.ts
2023-10-18 16:53:26 -04:00

462 lines
13 KiB
TypeScript

/* eslint-disable no-param-reassign */
import type { SanitizedConfig } from 'payload/config'
import type { Field, TabAsField } from 'payload/types'
import { fieldAffectsData } from 'payload/types'
import type { BlocksMap } from '../../utilities/createBlocksMap'
import { transformHasManyNumber } from './hasManyNumber'
import { transformRelationship } from './relationship'
type TraverseFieldsArgs = {
/**
* Pre-formatted blocks map
*/
blocks: BlocksMap
/**
* The full Payload config
*/
config: SanitizedConfig
/**
* The data reference to be mutated within this recursive function
*/
dataRef: Record<string, unknown>
/**
* Data that needs to be removed from the result after all fields have populated
*/
deletions: (() => void)[]
/**
* Column prefix can be built up by group and named tab fields
*/
fieldPrefix: string
/**
* An array of Payload fields to traverse
*/
fields: (Field | TabAsField)[]
/**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
numbers: Record<string, Record<string, unknown>[]>
/**
* The current field path (in dot notation), used to merge in relationships
*/
path: string
/**
* All related documents, as returned by Drizzle, keyed on an object by field path
*/
relationships: Record<string, Record<string, unknown>[]>
/**
* Data structure representing the nearest table from db
*/
table: Record<string, unknown>
}
// Traverse fields recursively, transforming data
// for each field type into required Payload shape
export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields,
numbers,
path,
relationships,
table,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
const formatted = fields.reduce((result, field) => {
if (field.type === 'tabs') {
traverseFields({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
numbers,
path,
relationships,
table,
})
}
if (
field.type === 'collapsible' ||
field.type === 'row' ||
(field.type === 'tab' && !('name' in field))
) {
traverseFields({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields: field.fields,
numbers,
path,
relationships,
table,
})
}
if (fieldAffectsData(field)) {
const fieldName = `${fieldPrefix || ''}${field.name}`
const fieldData = table[fieldName]
if (field.type === 'array') {
if (Array.isArray(fieldData)) {
if (field.localized) {
result[field.name] = fieldData.reduce((arrayResult, row) => {
if (typeof row._locale === 'string') {
if (!arrayResult[row._locale]) arrayResult[row._locale] = []
const locale = row._locale
const data = {}
delete row._locale
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
const rowResult = traverseFields<T>({
blocks,
config,
dataRef: data,
deletions,
fieldPrefix: '',
fields: field.fields,
numbers,
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
})
arrayResult[locale].push(rowResult)
}
return arrayResult
}, {})
} else {
result[field.name] = fieldData.map((row, i) => {
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
return traverseFields<T>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: field.fields,
numbers,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
})
})
}
}
return result
}
if (field.type === 'blocks') {
const blockFieldPath = `${sanitizedPath}${field.name}`
if (Array.isArray(blocks[blockFieldPath])) {
if (field.localized) {
result[field.name] = {}
blocks[blockFieldPath].forEach((row) => {
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
if (typeof row._locale === 'string') {
if (!result[field.name][row._locale]) result[field.name][row._locale] = []
result[field.name][row._locale].push(row)
delete row._locale
}
})
Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => {
result[field.name][locale] = localizedBlocks.map((row) => {
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
const blockResult = traverseFields<T>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
})
delete blockResult._order
return blockResult
}
return {}
})
})
} else {
result[field.name] = blocks[blockFieldPath].map((row, i) => {
delete row._order
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
return traverseFields<T>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
})
}
return {}
})
}
}
return result
}
if (field.type === 'relationship' || field.type === 'upload') {
const relationPathMatch = relationships[`${sanitizedPath}${field.name}`]
if (!relationPathMatch) {
if ('hasMany' in field && field.hasMany) {
if (field.localized && config.localization && config.localization.locales) {
result[field.name] = {
[config.localization.defaultLocale]: [],
}
} else {
result[field.name] = []
}
}
return result
}
if (field.localized) {
result[field.name] = {}
const relationsByLocale: Record<string, Record<string, unknown>[]> = {}
relationPathMatch.forEach((row) => {
if (typeof row.locale === 'string') {
if (!relationsByLocale[row.locale]) relationsByLocale[row.locale] = []
relationsByLocale[row.locale].push(row)
}
})
Object.entries(relationsByLocale).forEach(([locale, relations]) => {
transformRelationship({
field,
locale,
ref: result,
relations,
})
})
} else {
transformRelationship({
field,
ref: result,
relations: relationPathMatch,
})
}
return result
}
if (field.type === 'number' && field.hasMany) {
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
if (!numberPathMatch) return result
if (field.localized) {
result[field.name] = {}
const numbersByLocale: Record<string, Record<string, unknown>[]> = {}
numberPathMatch.forEach((row) => {
if (typeof row.locale === 'string') {
if (!numbersByLocale[row.locale]) numbersByLocale[row.locale] = []
numbersByLocale[row.locale].push(row)
}
})
Object.entries(numbersByLocale).forEach(([locale, numbers]) => {
transformHasManyNumber({
field,
locale,
numberRows: numbers,
ref: result,
})
})
} else {
transformHasManyNumber({
field,
numberRows: numberPathMatch,
ref: result,
})
}
return result
}
if (field.type === 'select' && field.hasMany) {
if (Array.isArray(fieldData)) {
if (field.localized) {
result[field.name] = fieldData.reduce((selectResult, row) => {
if (typeof row.locale === 'string') {
if (!selectResult[row.locale]) selectResult[row.locale] = []
selectResult[row.locale].push(row.value)
}
return selectResult
}, {})
} else {
result[field.name] = fieldData.map(({ value }) => value)
}
}
return result
}
const localizedFieldData = {}
const valuesToTransform: {
ref: Record<string, unknown>
table: Record<string, unknown>
}[] = []
if (field.localized && Array.isArray(table._locales)) {
table._locales.forEach((localeRow) => {
valuesToTransform.push({ ref: localizedFieldData, table: localeRow })
})
} else {
valuesToTransform.push({ ref: result, table })
}
valuesToTransform.forEach(({ ref, table }) => {
const fieldData = table[`${fieldPrefix || ''}${field.name}`]
const locale = table?._locale
switch (field.type) {
case 'tab':
case 'group': {
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
if (field.localized) {
if (typeof locale === 'string' && !ref[locale]) {
ref[locale] = {}
delete table._locale
}
Object.entries(ref).forEach(([groupLocale, groupLocaleData]) => {
ref[groupLocale] = traverseFields<Record<string, unknown>>({
blocks,
config,
dataRef: groupLocaleData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
table,
})
})
} else {
const groupData = {}
ref[field.name] = traverseFields<Record<string, unknown>>({
blocks,
config,
dataRef: groupData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
table,
})
}
break
}
case 'number': {
let val = fieldData
if (typeof fieldData === 'string') {
val = Number.parseFloat(fieldData)
}
if (typeof locale === 'string') {
ref[locale] = val
} else {
result[field.name] = val
}
break
}
case 'date': {
let val = fieldData
if (typeof fieldData === 'string') {
val = new Date(fieldData).toISOString()
}
if (typeof locale === 'string') {
ref[locale] = val
} else {
result[field.name] = val
}
break
}
default: {
if (typeof locale === 'string') {
ref[locale] = fieldData
} else {
result[field.name] = fieldData
}
break
}
}
})
if (Object.keys(localizedFieldData).length > 0) {
result[field.name] = localizedFieldData
}
return result
}
return result
}, dataRef)
if (Array.isArray(table._locales)) {
deletions.push(() => delete table._locales)
}
return formatted as T
}