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

269 lines
8.2 KiB
TypeScript

/* eslint-disable no-param-reassign */
import { AnyPgColumnBuilder, integer, pgEnum, pgTable, serial, uniqueIndex, text, varchar, PgColumn, PgTableExtraConfig, index, numeric, PgColumnHKT, IndexBuilder, PgNumericBuilder, PgVarcharBuilder } from 'drizzle-orm/pg-core';
import { Field } from 'payload/types';
import toSnakeCase from 'to-snake-case';
import { fieldAffectsData } from 'payload/dist/fields/config/types';
import { Relation, relations } from 'drizzle-orm';
import { GenericColumns, PostgresAdapter } from '../types';
import { createIndex } from './createIndex';
import { buildTable } from './build';
import { parentIDColumnMap } from './parentIDColumnMap';
type Args = {
adapter: PostgresAdapter
arrayBlockRelations: Map<string, string>
buildRelationships: boolean
columns: Record<string, AnyPgColumnBuilder>
columnPrefix?: string
fieldPrefix?: string
fields: Field[]
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
localesColumns: Record<string, AnyPgColumnBuilder>
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
tableName: string
relationships: Set<string>
}
export const traverseFields = ({
adapter,
arrayBlockRelations,
buildRelationships,
columnPrefix,
columns,
fieldPrefix,
fields,
indexes,
localesColumns,
localesIndexes,
tableName,
relationships,
}: Args): { hasLocalizedField: boolean } => {
let hasLocalizedField = false;
let parentIDColType = 'integer';
if (columns.id instanceof PgNumericBuilder) parentIDColType = 'numeric';
if (columns.id instanceof PgVarcharBuilder) parentIDColType = 'varchar';
fields.forEach((field) => {
if ('name' in field && field.name === 'id') return;
let columnName: string;
let targetTable = columns;
let targetIndexes = indexes;
if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`;
// If field is localized,
// add the column to the locale table instead of main table
if (field.localized) {
hasLocalizedField = true;
targetTable = localesColumns;
targetIndexes = localesIndexes;
}
if (field.unique || field.index) {
targetIndexes[`${field.name}Idx`] = createIndex({ columnName, name: field.name, unique: field.unique });
}
}
switch (field.type) {
case 'text':
case 'email':
case 'code':
case 'textarea': {
targetTable[`${fieldPrefix || ''}${field.name}`] = varchar(columnName);
break;
}
case 'number': {
targetTable[`${fieldPrefix || ''}${field.name}`] = numeric(columnName);
break;
}
case 'array': {
const baseColumns: Record<string, AnyPgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').references(() => adapter.tables[tableName].id).notNull(),
};
if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = adapter.enums._locales('_locale').notNull();
}
const arrayTableName = `${tableName}_${toSnakeCase(field.name)}`;
const { arrayBlockRelations: subArrayBlockRelations } = buildTable({
adapter,
baseColumns,
fields: field.fields,
tableName: arrayTableName,
});
arrayBlockRelations.set(`${fieldPrefix || ''}${field.name}`, arrayTableName);
const arrayTableRelations = relations(adapter.tables[arrayTableName], ({ many, one }) => {
const result: Record<string, Relation<string>> = {
_parentID: one(adapter.tables[tableName], {
fields: [adapter.tables[arrayTableName]._parentID],
references: [adapter.tables[tableName].id],
}),
};
if (field.localized) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`]);
}
subArrayBlockRelations.forEach((val, key) => {
result[key] = many(adapter.tables[val]);
});
return result;
});
adapter.relations[arrayTableName] = arrayTableRelations;
break;
}
case 'blocks': {
field.blocks.forEach((block) => {
const baseColumns: Record<string, AnyPgColumnBuilder> = {
_order: integer('_order').notNull(),
_path: text('_path').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').references(() => adapter.tables[tableName].id).notNull(),
};
if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = adapter.enums._locales('_locale').notNull();
}
const blockTableName = `${tableName}_${toSnakeCase(block.slug)}`;
if (!adapter.tables[blockTableName]) {
const { arrayBlockRelations: subArrayBlockRelations } = buildTable({
adapter,
baseColumns,
fields: block.fields,
tableName: blockTableName,
});
const blockTableRelations = relations(adapter.tables[blockTableName], ({ many, one }) => {
const result: Record<string, Relation<string>> = {
_parentID: one(adapter.tables[tableName], {
fields: [adapter.tables[blockTableName]._parentID],
references: [adapter.tables[tableName].id],
}),
};
if (field.localized) {
result._locales = many(adapter.tables[`${blockTableName}_locales`]);
}
subArrayBlockRelations.forEach((val, key) => {
result[key] = many(adapter.tables[val]);
});
return result;
});
adapter.relations[blockTableName] = blockTableRelations;
}
arrayBlockRelations.set(`_${fieldPrefix || ''}${field.name}`, blockTableName);
});
break;
}
case 'group': {
// Todo: determine what should happen if groups are set to localized
const { hasLocalizedField: groupHasLocalizedField } = traverseFields({
adapter,
arrayBlockRelations,
buildRelationships,
columnPrefix: `${columnName}_`,
columns,
fieldPrefix: `${fieldPrefix || ''}${field.name}_`,
fields: field.fields,
indexes,
localesColumns,
localesIndexes,
tableName,
relationships,
});
if (groupHasLocalizedField) hasLocalizedField = true;
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
if ('name' in tab) {
const { hasLocalizedField: tabHasLocalizedField } = traverseFields({
adapter,
arrayBlockRelations,
buildRelationships,
columnPrefix: `${columnName}_`,
columns,
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
fields: tab.fields,
indexes,
localesColumns,
localesIndexes,
tableName,
relationships,
});
if (tabHasLocalizedField) hasLocalizedField = true;
} else {
({ hasLocalizedField } = traverseFields({
adapter,
arrayBlockRelations,
buildRelationships,
columns,
fields: tab.fields,
indexes,
localesColumns,
localesIndexes,
tableName,
relationships,
}));
}
});
break;
}
case 'row':
({ hasLocalizedField } = traverseFields({
adapter,
arrayBlockRelations,
buildRelationships,
columns,
fields: field.fields,
indexes,
localesColumns,
localesIndexes,
tableName,
relationships,
}));
break;
case 'relationship':
case 'upload':
if (Array.isArray(field.relationTo)) {
field.relationTo.forEach((relation) => relationships.add(relation));
} else {
relationships.add(field.relationTo);
}
break;
default:
break;
}
});
return { hasLocalizedField };
};