fix(drizzle): indexes / unique with relationships (#8432)
Fixes https://github.com/payloadcms/payload/issues/8413 and https://github.com/payloadcms/payload/issues/6460 - Builds indexes for relationships by default in the SQL schema - Fixes `unique: true` handling with Postgres / SQLite for every type of relationships (non-polymorphic. hasMany, polymorphic, polymorphic hasMany) _note_: disables unique for nested to arrays / blocks relationships in the `_rels` table. - adds tests 2.0 PR tha ports only indexes creation https://github.com/payloadcms/payload/pull/8446, because `unique: true` could be a breaking change if someone has incosistent unique data in the database.
This commit is contained in:
@@ -24,6 +24,7 @@ import toSnakeCase from 'to-snake-case'
|
|||||||
|
|
||||||
import type { GenericColumns, GenericTable, IDType, SQLiteAdapter } from '../types.js'
|
import type { GenericColumns, GenericTable, IDType, SQLiteAdapter } from '../types.js'
|
||||||
|
|
||||||
|
import { createIndex } from './createIndex.js'
|
||||||
import { getIDColumn } from './getIDColumn.js'
|
import { getIDColumn } from './getIDColumn.js'
|
||||||
import { setColumnID } from './setColumnID.js'
|
import { setColumnID } from './setColumnID.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
@@ -56,6 +57,7 @@ type Args = {
|
|||||||
buildNumbers?: boolean
|
buildNumbers?: boolean
|
||||||
buildRelationships?: boolean
|
buildRelationships?: boolean
|
||||||
disableNotNull: boolean
|
disableNotNull: boolean
|
||||||
|
disableRelsTableUnique?: boolean
|
||||||
disableUnique: boolean
|
disableUnique: boolean
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
@@ -64,6 +66,7 @@ type Args = {
|
|||||||
rootRelationsToBuild?: RelationMap
|
rootRelationsToBuild?: RelationMap
|
||||||
rootTableIDColType?: IDType
|
rootTableIDColType?: IDType
|
||||||
rootTableName?: string
|
rootTableName?: string
|
||||||
|
rootUniqueRelationships?: Set<string>
|
||||||
tableName: string
|
tableName: string
|
||||||
timestamps?: boolean
|
timestamps?: boolean
|
||||||
versions: boolean
|
versions: boolean
|
||||||
@@ -88,6 +91,7 @@ export const buildTable = ({
|
|||||||
baseColumns = {},
|
baseColumns = {},
|
||||||
baseExtraConfig = {},
|
baseExtraConfig = {},
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique,
|
||||||
disableUnique = false,
|
disableUnique = false,
|
||||||
fields,
|
fields,
|
||||||
joins,
|
joins,
|
||||||
@@ -96,6 +100,7 @@ export const buildTable = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName: incomingRootTableName,
|
rootTableName: incomingRootTableName,
|
||||||
|
rootUniqueRelationships,
|
||||||
tableName,
|
tableName,
|
||||||
timestamps,
|
timestamps,
|
||||||
versions,
|
versions,
|
||||||
@@ -114,6 +119,7 @@ export const buildTable = ({
|
|||||||
|
|
||||||
// Relationships to the base collection
|
// Relationships to the base collection
|
||||||
const relationships: Set<string> = rootRelationships || new Set()
|
const relationships: Set<string> = rootRelationships || new Set()
|
||||||
|
const uniqueRelationships: Set<string> = rootUniqueRelationships || new Set()
|
||||||
|
|
||||||
let relationshipsTable: GenericTable | SQLiteTableWithColumns<any>
|
let relationshipsTable: GenericTable | SQLiteTableWithColumns<any>
|
||||||
|
|
||||||
@@ -133,6 +139,7 @@ export const buildTable = ({
|
|||||||
adapter,
|
adapter,
|
||||||
columns,
|
columns,
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields,
|
fields,
|
||||||
indexes,
|
indexes,
|
||||||
@@ -147,6 +154,7 @@ export const buildTable = ({
|
|||||||
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
|
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
|
||||||
rootTableIDColType: rootTableIDColType || idColType,
|
rootTableIDColType: rootTableIDColType || idColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -393,7 +401,9 @@ export const buildTable = ({
|
|||||||
colType = 'text'
|
colType = 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
relationshipColumns[`${relationTo}ID`] = getIDColumn({
|
const colName = `${relationTo}ID`
|
||||||
|
|
||||||
|
relationshipColumns[colName] = getIDColumn({
|
||||||
name: `${formattedRelationTo}_id`,
|
name: `${formattedRelationTo}_id`,
|
||||||
type: colType,
|
type: colType,
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
@@ -402,9 +412,27 @@ export const buildTable = ({
|
|||||||
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
||||||
foreignKey({
|
foreignKey({
|
||||||
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
||||||
columns: [cols[`${relationTo}ID`]],
|
columns: [cols[colName]],
|
||||||
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
||||||
}).onDelete('cascade')
|
}).onDelete('cascade')
|
||||||
|
|
||||||
|
const indexName = [colName]
|
||||||
|
|
||||||
|
const unique = !disableUnique && uniqueRelationships.has(relationTo)
|
||||||
|
|
||||||
|
if (unique) {
|
||||||
|
indexName.push('path')
|
||||||
|
}
|
||||||
|
if (hasLocalizedRelationshipField) {
|
||||||
|
indexName.push('locale')
|
||||||
|
}
|
||||||
|
|
||||||
|
relationExtraConfig[`${relationTo}IdIdx`] = createIndex({
|
||||||
|
name: indexName,
|
||||||
|
columnName: `${formattedRelationTo}_id`,
|
||||||
|
tableName: relationshipsTableName,
|
||||||
|
unique,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
relationshipsTable = sqliteTable(relationshipsTableName, relationshipColumns, (cols) => {
|
relationshipsTable = sqliteTable(relationshipsTableName, relationshipColumns, (cols) => {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Args = {
|
|||||||
columnPrefix?: string
|
columnPrefix?: string
|
||||||
columns: Record<string, SQLiteColumnBuilder>
|
columns: Record<string, SQLiteColumnBuilder>
|
||||||
disableNotNull: boolean
|
disableNotNull: boolean
|
||||||
|
disableRelsTableUnique?: boolean
|
||||||
disableUnique?: boolean
|
disableUnique?: boolean
|
||||||
fieldPrefix?: string
|
fieldPrefix?: string
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
@@ -52,6 +53,7 @@ type Args = {
|
|||||||
rootRelationsToBuild?: RelationMap
|
rootRelationsToBuild?: RelationMap
|
||||||
rootTableIDColType: IDType
|
rootTableIDColType: IDType
|
||||||
rootTableName: string
|
rootTableName: string
|
||||||
|
uniqueRelationships: Set<string>
|
||||||
versions: boolean
|
versions: boolean
|
||||||
/**
|
/**
|
||||||
* Tracks whether or not this table is built
|
* Tracks whether or not this table is built
|
||||||
@@ -74,6 +76,7 @@ export const traverseFields = ({
|
|||||||
columnPrefix,
|
columnPrefix,
|
||||||
columns,
|
columns,
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique,
|
||||||
disableUnique = false,
|
disableUnique = false,
|
||||||
fieldPrefix,
|
fieldPrefix,
|
||||||
fields,
|
fields,
|
||||||
@@ -90,6 +93,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
}: Args): Result => {
|
}: Args): Result => {
|
||||||
@@ -147,9 +151,10 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(field.unique || field.index) &&
|
(field.unique || field.index || ['relationship', 'upload'].includes(field.type)) &&
|
||||||
!['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) &&
|
!['array', 'blocks', 'group', 'point'].includes(field.type) &&
|
||||||
!('hasMany' in field && field.hasMany === true)
|
!('hasMany' in field && field.hasMany === true) &&
|
||||||
|
!('relationTo' in field && Array.isArray(field.relationTo))
|
||||||
) {
|
) {
|
||||||
const unique = disableUnique !== true && field.unique
|
const unique = disableUnique !== true && field.unique
|
||||||
if (unique) {
|
if (unique) {
|
||||||
@@ -401,12 +406,14 @@ export const traverseFields = ({
|
|||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
tableName: arrayTableName,
|
tableName: arrayTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -540,12 +547,14 @@ export const traverseFields = ({
|
|||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
tableName: blockTableName,
|
tableName: blockTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -664,6 +673,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -719,6 +729,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||||
})
|
})
|
||||||
@@ -775,6 +786,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -831,6 +843,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -859,9 +872,17 @@ export const traverseFields = ({
|
|||||||
case 'relationship':
|
case 'relationship':
|
||||||
case 'upload':
|
case 'upload':
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
field.relationTo.forEach((relation) => {
|
||||||
|
relationships.add(relation)
|
||||||
|
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||||
|
uniqueRelationships.add(relation)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else if (field.hasMany) {
|
} else if (field.hasMany) {
|
||||||
relationships.add(field.relationTo)
|
relationships.add(field.relationTo)
|
||||||
|
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||||
|
uniqueRelationships.add(field.relationTo)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||||
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import type {
|
|||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
|
|
||||||
import { createTableName } from '../../createTableName.js'
|
import { createTableName } from '../../createTableName.js'
|
||||||
|
import { createIndex } from './createIndex.js'
|
||||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||||
import { setColumnID } from './setColumnID.js'
|
import { setColumnID } from './setColumnID.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
@@ -45,6 +46,7 @@ type Args = {
|
|||||||
buildNumbers?: boolean
|
buildNumbers?: boolean
|
||||||
buildRelationships?: boolean
|
buildRelationships?: boolean
|
||||||
disableNotNull: boolean
|
disableNotNull: boolean
|
||||||
|
disableRelsTableUnique?: boolean
|
||||||
disableUnique: boolean
|
disableUnique: boolean
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
@@ -52,6 +54,7 @@ type Args = {
|
|||||||
rootRelationsToBuild?: RelationMap
|
rootRelationsToBuild?: RelationMap
|
||||||
rootTableIDColType?: string
|
rootTableIDColType?: string
|
||||||
rootTableName?: string
|
rootTableName?: string
|
||||||
|
rootUniqueRelationships?: Set<string>
|
||||||
tableName: string
|
tableName: string
|
||||||
timestamps?: boolean
|
timestamps?: boolean
|
||||||
versions: boolean
|
versions: boolean
|
||||||
@@ -76,6 +79,7 @@ export const buildTable = ({
|
|||||||
baseColumns = {},
|
baseColumns = {},
|
||||||
baseExtraConfig = {},
|
baseExtraConfig = {},
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique = false,
|
||||||
disableUnique = false,
|
disableUnique = false,
|
||||||
fields,
|
fields,
|
||||||
joins,
|
joins,
|
||||||
@@ -83,6 +87,7 @@ export const buildTable = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName: incomingRootTableName,
|
rootTableName: incomingRootTableName,
|
||||||
|
rootUniqueRelationships,
|
||||||
tableName,
|
tableName,
|
||||||
timestamps,
|
timestamps,
|
||||||
versions,
|
versions,
|
||||||
@@ -102,6 +107,9 @@ export const buildTable = ({
|
|||||||
// Relationships to the base collection
|
// Relationships to the base collection
|
||||||
const relationships: Set<string> = rootRelationships || new Set()
|
const relationships: Set<string> = rootRelationships || new Set()
|
||||||
|
|
||||||
|
// Unique relationships to the base collection
|
||||||
|
const uniqueRelationships: Set<string> = rootUniqueRelationships || new Set()
|
||||||
|
|
||||||
let relationshipsTable: GenericTable | PgTableWithColumns<any>
|
let relationshipsTable: GenericTable | PgTableWithColumns<any>
|
||||||
|
|
||||||
// Drizzle relations
|
// Drizzle relations
|
||||||
@@ -120,6 +128,7 @@ export const buildTable = ({
|
|||||||
adapter,
|
adapter,
|
||||||
columns,
|
columns,
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields,
|
fields,
|
||||||
indexes,
|
indexes,
|
||||||
@@ -133,6 +142,7 @@ export const buildTable = ({
|
|||||||
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
|
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
|
||||||
rootTableIDColType: rootTableIDColType || idColType,
|
rootTableIDColType: rootTableIDColType || idColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -368,16 +378,34 @@ export const buildTable = ({
|
|||||||
colType = 'varchar'
|
colType = 'varchar'
|
||||||
}
|
}
|
||||||
|
|
||||||
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
|
const colName = `${relationTo}ID`
|
||||||
`${formattedRelationTo}_id`,
|
|
||||||
)
|
relationshipColumns[colName] = parentIDColumnMap[colType](`${formattedRelationTo}_id`)
|
||||||
|
|
||||||
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
||||||
foreignKey({
|
foreignKey({
|
||||||
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
||||||
columns: [cols[`${relationTo}ID`]],
|
columns: [cols[colName]],
|
||||||
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
||||||
}).onDelete('cascade')
|
}).onDelete('cascade')
|
||||||
|
|
||||||
|
const indexName = [colName]
|
||||||
|
|
||||||
|
const unique = !disableUnique && uniqueRelationships.has(relationTo)
|
||||||
|
|
||||||
|
if (unique) {
|
||||||
|
indexName.push('path')
|
||||||
|
}
|
||||||
|
if (hasLocalizedRelationshipField) {
|
||||||
|
indexName.push('locale')
|
||||||
|
}
|
||||||
|
|
||||||
|
relationExtraConfig[`${relationTo}IdIdx`] = createIndex({
|
||||||
|
name: indexName,
|
||||||
|
columnName: `${formattedRelationTo}_id`,
|
||||||
|
tableName: relationshipsTableName,
|
||||||
|
unique,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
relationshipsTable = adapter.pgSchema.table(
|
relationshipsTable = adapter.pgSchema.table(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Args = {
|
|||||||
columnPrefix?: string
|
columnPrefix?: string
|
||||||
columns: Record<string, PgColumnBuilder>
|
columns: Record<string, PgColumnBuilder>
|
||||||
disableNotNull: boolean
|
disableNotNull: boolean
|
||||||
|
disableRelsTableUnique?: boolean
|
||||||
disableUnique?: boolean
|
disableUnique?: boolean
|
||||||
fieldPrefix?: string
|
fieldPrefix?: string
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
@@ -58,6 +59,7 @@ type Args = {
|
|||||||
rootRelationsToBuild?: RelationMap
|
rootRelationsToBuild?: RelationMap
|
||||||
rootTableIDColType: string
|
rootTableIDColType: string
|
||||||
rootTableName: string
|
rootTableName: string
|
||||||
|
uniqueRelationships: Set<string>
|
||||||
versions: boolean
|
versions: boolean
|
||||||
/**
|
/**
|
||||||
* Tracks whether or not this table is built
|
* Tracks whether or not this table is built
|
||||||
@@ -80,6 +82,7 @@ export const traverseFields = ({
|
|||||||
columnPrefix,
|
columnPrefix,
|
||||||
columns,
|
columns,
|
||||||
disableNotNull,
|
disableNotNull,
|
||||||
|
disableRelsTableUnique,
|
||||||
disableUnique = false,
|
disableUnique = false,
|
||||||
fieldPrefix,
|
fieldPrefix,
|
||||||
fields,
|
fields,
|
||||||
@@ -95,6 +98,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
}: Args): Result => {
|
}: Args): Result => {
|
||||||
@@ -152,9 +156,10 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(field.unique || field.index) &&
|
(field.unique || field.index || ['relationship', 'upload'].includes(field.type)) &&
|
||||||
!['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) &&
|
!['array', 'blocks', 'group', 'point'].includes(field.type) &&
|
||||||
!('hasMany' in field && field.hasMany === true)
|
!('hasMany' in field && field.hasMany === true) &&
|
||||||
|
!('relationTo' in field && Array.isArray(field.relationTo))
|
||||||
) {
|
) {
|
||||||
const unique = disableUnique !== true && field.unique
|
const unique = disableUnique !== true && field.unique
|
||||||
if (unique) {
|
if (unique) {
|
||||||
@@ -412,12 +417,14 @@ export const traverseFields = ({
|
|||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
tableName: arrayTableName,
|
tableName: arrayTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -547,12 +554,14 @@ export const traverseFields = ({
|
|||||||
baseColumns,
|
baseColumns,
|
||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
|
disableRelsTableUnique: true,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
rootUniqueRelationships: uniqueRelationships,
|
||||||
tableName: blockTableName,
|
tableName: blockTableName,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: isLocalized,
|
withinLocalizedArrayOrBlock: isLocalized,
|
||||||
@@ -670,6 +679,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -724,6 +734,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||||
})
|
})
|
||||||
@@ -779,6 +790,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -834,6 +846,7 @@ export const traverseFields = ({
|
|||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
rootTableName,
|
rootTableName,
|
||||||
|
uniqueRelationships,
|
||||||
versions,
|
versions,
|
||||||
withinLocalizedArrayOrBlock,
|
withinLocalizedArrayOrBlock,
|
||||||
})
|
})
|
||||||
@@ -862,9 +875,17 @@ export const traverseFields = ({
|
|||||||
case 'relationship':
|
case 'relationship':
|
||||||
case 'upload':
|
case 'upload':
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
field.relationTo.forEach((relation) => {
|
||||||
|
relationships.add(relation)
|
||||||
|
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||||
|
uniqueRelationships.add(relation)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else if (field.hasMany) {
|
} else if (field.hasMany) {
|
||||||
relationships.add(field.relationTo)
|
relationships.add(field.relationTo)
|
||||||
|
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||||
|
uniqueRelationships.add(field.relationTo)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||||
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
||||||
|
|||||||
@@ -16,6 +16,52 @@ const IndexedFields: CollectionConfig = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'uniqueRelationship',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'text-fields',
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniqueHasManyRelationship',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'text-fields',
|
||||||
|
unique: true,
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniqueHasManyRelationship_2',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'text-fields',
|
||||||
|
hasMany: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniquePolymorphicRelationship',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['text-fields'],
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniquePolymorphicRelationship_2',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['text-fields'],
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniqueHasManyPolymorphicRelationship',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['text-fields'],
|
||||||
|
unique: true,
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uniqueHasManyPolymorphicRelationship_2',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['text-fields'],
|
||||||
|
unique: true,
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'uniqueRequiredText',
|
name: 'uniqueRequiredText',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { MongooseAdapter } from '@payloadcms/db-mongodb'
|
import type { MongooseAdapter } from '@payloadcms/db-mongodb'
|
||||||
import type { IndexDirection, IndexOptions } from 'mongoose'
|
import type { IndexDirection, IndexOptions } from 'mongoose'
|
||||||
import type { PaginatedDocs, Payload } from 'payload'
|
|
||||||
|
|
||||||
import { reload } from '@payloadcms/next/utilities'
|
import { reload } from '@payloadcms/next/utilities'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { type PaginatedDocs, type Payload, ValidationError } from 'payload'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
@@ -1035,6 +1035,218 @@ describe('Fields', () => {
|
|||||||
}).toBeDefined()
|
}).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should throw validation error saving on unique relationship fields hasMany: false non polymorphic', async () => {
|
||||||
|
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
|
||||||
|
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '1',
|
||||||
|
text: '2',
|
||||||
|
uniqueRequiredText: '3',
|
||||||
|
uniqueRelationship: textDoc.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '20' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
payload.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '4',
|
||||||
|
text: '5',
|
||||||
|
uniqueRequiredText: '10',
|
||||||
|
uniqueRelationship: textDoc.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw validation error saving on unique relationship fields hasMany: true', async () => {
|
||||||
|
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
|
||||||
|
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '1',
|
||||||
|
text: '2',
|
||||||
|
uniqueRequiredText: '3',
|
||||||
|
uniqueHasManyRelationship: [textDoc.id],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '40' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should allow the same relationship on a diferrent field!
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '31',
|
||||||
|
text: '24',
|
||||||
|
uniqueRequiredText: '55',
|
||||||
|
uniqueHasManyRelationship_2: [textDoc.id],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '30' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
payload.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '4',
|
||||||
|
text: '5',
|
||||||
|
uniqueRequiredText: '10',
|
||||||
|
uniqueHasManyRelationship: [textDoc.id],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw validation error saving on unique relationship fields polymorphic', async () => {
|
||||||
|
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
|
||||||
|
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '1',
|
||||||
|
text: '2',
|
||||||
|
uniqueRequiredText: '3',
|
||||||
|
uniquePolymorphicRelationship: { relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '20' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should allow the same relationship on a diferrent field!
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '31',
|
||||||
|
text: '24',
|
||||||
|
uniqueRequiredText: '55',
|
||||||
|
uniquePolymorphicRelationship_2: { relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '100' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
payload.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '4',
|
||||||
|
text: '5',
|
||||||
|
uniqueRequiredText: '10',
|
||||||
|
uniquePolymorphicRelationship: { relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw validation error saving on unique relationship fields polymorphic hasMany: true', async () => {
|
||||||
|
const textDoc = await payload.create({ collection: 'text-fields', data: { text: 'asd' } })
|
||||||
|
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '1',
|
||||||
|
text: '2',
|
||||||
|
uniqueRequiredText: '3',
|
||||||
|
uniqueHasManyPolymorphicRelationship: [
|
||||||
|
{ relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '100' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should allow the same relationship on a diferrent field!
|
||||||
|
await payload
|
||||||
|
.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '31',
|
||||||
|
text: '24',
|
||||||
|
uniqueRequiredText: '55',
|
||||||
|
uniqueHasManyPolymorphicRelationship_2: [
|
||||||
|
{ relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Skip mongodb uniuqe error because it threats localizedUniqueRequriedText.es as undefined
|
||||||
|
.then((doc) =>
|
||||||
|
payload.update({
|
||||||
|
locale: 'es',
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: { localizedUniqueRequiredText: '300' },
|
||||||
|
id: doc.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
payload.create({
|
||||||
|
collection: 'indexed-fields',
|
||||||
|
data: {
|
||||||
|
localizedUniqueRequiredText: '4',
|
||||||
|
text: '5',
|
||||||
|
uniqueRequiredText: '10',
|
||||||
|
uniqueHasManyPolymorphicRelationship: [
|
||||||
|
{ relationTo: 'text-fields', value: textDoc.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should not throw validation error saving multiple null values for unique fields', async () => {
|
it('should not throw validation error saving multiple null values for unique fields', async () => {
|
||||||
const data = {
|
const data = {
|
||||||
localizedUniqueRequiredText: 'en1',
|
localizedUniqueRequiredText: 'en1',
|
||||||
|
|||||||
@@ -1020,6 +1020,29 @@ export interface IndexedField {
|
|||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
uniqueText?: string | null;
|
uniqueText?: string | null;
|
||||||
|
uniqueRelationship?: (string | null) | TextField;
|
||||||
|
uniqueHasManyRelationship?: (string | TextField)[] | null;
|
||||||
|
uniqueHasManyRelationship_2?: (string | TextField)[] | null;
|
||||||
|
uniquePolymorphicRelationship?: {
|
||||||
|
relationTo: 'text-fields';
|
||||||
|
value: string | TextField;
|
||||||
|
} | null;
|
||||||
|
uniquePolymorphicRelationship_2?: {
|
||||||
|
relationTo: 'text-fields';
|
||||||
|
value: string | TextField;
|
||||||
|
} | null;
|
||||||
|
uniqueHasManyPolymorphicRelationship?:
|
||||||
|
| {
|
||||||
|
relationTo: 'text-fields';
|
||||||
|
value: string | TextField;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
uniqueHasManyPolymorphicRelationship_2?:
|
||||||
|
| {
|
||||||
|
relationTo: 'text-fields';
|
||||||
|
value: string | TextField;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
uniqueRequiredText: string;
|
uniqueRequiredText: string;
|
||||||
localizedUniqueRequiredText: string;
|
localizedUniqueRequiredText: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user