diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index cfc0d2138c..d544debe61 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -57,9 +57,9 @@ export const Posts: CollectionConfig = { The following options are available: -| Option | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | +| Option | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | | `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). | | `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). | | `custom` | Extension point for adding custom data (e.g. for plugins) | @@ -67,17 +67,18 @@ The following options are available: | `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. | | `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. | | `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). | -| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). | +| `fields` * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). | | `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) | | `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). | | `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | | `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). | -| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. | +| `slug` * | Unique, URL-friendly string that will act as an identifier for this Collection. | | `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | | `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. | | `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). | | `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). | +| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. | _* An asterisk denotes that a property is required._ diff --git a/packages/db-mongodb/src/init.ts b/packages/db-mongodb/src/init.ts index f477d65920..01b306a8e9 100644 --- a/packages/db-mongodb/src/init.ts +++ b/packages/db-mongodb/src/init.ts @@ -3,7 +3,11 @@ import type { Init, SanitizedCollectionConfig } from 'payload' import mongoose from 'mongoose' import paginate from 'mongoose-paginate-v2' -import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' +import { + buildVersionCollectionFields, + buildVersionCompoundIndexes, + buildVersionGlobalFields, +} from 'payload' import type { MongooseAdapter } from './index.js' import type { CollectionModel, GlobalModel } from './types.js' @@ -36,6 +40,7 @@ export const init: Init = function init(this: MongooseAdapter) { }, ...schemaOptions, }, + compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }), configFields: versionCollectionFields, payload: this.payload, }) diff --git a/packages/db-mongodb/src/models/buildCollectionSchema.ts b/packages/db-mongodb/src/models/buildCollectionSchema.ts index 8c4070f865..01eb24acd1 100644 --- a/packages/db-mongodb/src/models/buildCollectionSchema.ts +++ b/packages/db-mongodb/src/models/buildCollectionSchema.ts @@ -23,6 +23,7 @@ export const buildCollectionSchema = ( ...schemaOptions, }, }, + compoundIndexes: collection.sanitizedIndexes, configFields: collection.fields, payload, }) diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index bd350ca171..4cd833ccec 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -2,7 +2,6 @@ import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mon import mongoose from 'mongoose' import { - APIError, type ArrayField, type BlocksField, type CheckboxField, @@ -22,6 +21,7 @@ import { type RelationshipField, type RichTextField, type RowField, + type SanitizedCompoundIndex, type SanitizedLocalizationConfig, type SelectField, type Tab, @@ -128,6 +128,7 @@ const localizeSchema = ( export const buildSchema = (args: { buildSchemaOptions: BuildSchemaOptions + compoundIndexes?: SanitizedCompoundIndex[] configFields: Field[] parentIsLocalized?: boolean payload: Payload @@ -166,6 +167,26 @@ export const buildSchema = (args: { } }) + if (args.compoundIndexes) { + for (const index of args.compoundIndexes) { + const indexDefinition: Record = {} + + for (const field of index.fields) { + if (field.pathHasLocalized && payload.config.localization) { + for (const locale of payload.config.localization.locales) { + indexDefinition[field.localizedPath.replace('', locale.code)] = 1 + } + } else { + indexDefinition[field.path] = 1 + } + } + + schema.index(indexDefinition, { + unique: args.buildSchemaOptions.disableUnique ? false : index.unique, + }) + } + } + return schema } diff --git a/packages/drizzle/src/schema/build.ts b/packages/drizzle/src/schema/build.ts index 045c7fd251..746ad3aada 100644 --- a/packages/drizzle/src/schema/build.ts +++ b/packages/drizzle/src/schema/build.ts @@ -1,5 +1,6 @@ -import type { FlattenedField } from 'payload' +import type { FlattenedField, SanitizedCompoundIndex } from 'payload' +import { InvalidConfiguration } from 'payload' import toSnakeCase from 'to-snake-case' import type { @@ -33,6 +34,7 @@ type Args = { baseIndexes?: Record buildNumbers?: boolean buildRelationships?: boolean + compoundIndexes?: SanitizedCompoundIndex[] disableNotNull: boolean disableRelsTableUnique?: boolean disableUnique: boolean @@ -68,6 +70,7 @@ export const buildTable = ({ baseColumns = {}, baseForeignKeys = {}, baseIndexes = {}, + compoundIndexes, disableNotNull, disableRelsTableUnique = false, disableUnique = false, @@ -268,6 +271,61 @@ export const buildTable = ({ adapter.rawRelations[localeTableName] = localeRelations } + if (compoundIndexes) { + for (const index of compoundIndexes) { + let someLocalized: boolean | null = null + const columns: string[] = [] + + const getTableToUse = () => { + if (someLocalized) { + return localesTable + } + + return table + } + + for (const { path, pathHasLocalized } of index.fields) { + if (someLocalized === null) { + someLocalized = pathHasLocalized + } + + if (someLocalized !== pathHasLocalized) { + throw new InvalidConfiguration( + `Compound indexes within localized and non localized fields are not supported in SQL. Expected ${path} to be ${someLocalized ? 'non' : ''} localized.`, + ) + } + + const columnPath = path.replaceAll('.', '_') + + if (!getTableToUse().columns[columnPath]) { + throw new InvalidConfiguration( + `Column ${columnPath} for compound index on ${path} was not found in the ${getTableToUse().name} table.`, + ) + } + + columns.push(columnPath) + } + + if (someLocalized) { + columns.push('_locale') + } + + let name = columns.join('_') + // truncate against the limit, buildIndexName will handle collisions + if (name.length > 63) { + name = 'compound_index' + } + + const indexName = buildIndexName({ name, adapter }) + + getTableToUse().indexes[indexName] = { + name: indexName, + on: columns, + unique: disableUnique ? false : index.unique, + } + } + } + if (isRoot) { if (hasManyTextField) { const textsTableName = `${rootTableName}_texts` diff --git a/packages/drizzle/src/schema/buildRawSchema.ts b/packages/drizzle/src/schema/buildRawSchema.ts index 101118f182..a669753235 100644 --- a/packages/drizzle/src/schema/buildRawSchema.ts +++ b/packages/drizzle/src/schema/buildRawSchema.ts @@ -1,4 +1,8 @@ -import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' +import { + buildVersionCollectionFields, + buildVersionCompoundIndexes, + buildVersionGlobalFields, +} from 'payload' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter, RawIndex, SetColumnID } from '../types.js' @@ -52,6 +56,7 @@ export const buildRawSchema = ({ buildTable({ adapter, + compoundIndexes: collection.sanitizedIndexes, disableNotNull: !!collection?.versions?.drafts, disableUnique: false, fields: collection.flattenedFields, @@ -70,6 +75,7 @@ export const buildRawSchema = ({ buildTable({ adapter, + compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }), disableNotNull: !!collection.versions?.drafts, disableUnique: true, fields: versionFields, diff --git a/packages/drizzle/src/schema/traverseFields.ts b/packages/drizzle/src/schema/traverseFields.ts index d5243366a5..f82bd34335 100644 --- a/packages/drizzle/src/schema/traverseFields.ts +++ b/packages/drizzle/src/schema/traverseFields.ts @@ -1,4 +1,4 @@ -import type { FlattenedField } from 'payload' +import type { CompoundIndex, FlattenedField } from 'payload' import { InvalidConfiguration } from 'payload' import { diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index 9170a0234d..5894670860 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -17,7 +17,15 @@ import { createClientFields } from '../../fields/config/client.js' export type ServerOnlyCollectionProperties = keyof Pick< SanitizedCollectionConfig, - 'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins' | 'polymorphicJoins' + | 'access' + | 'custom' + | 'endpoints' + | 'flattenedFields' + | 'hooks' + | 'indexes' + | 'joins' + | 'polymorphicJoins' + | 'sanitizedIndexes' > export type ServerOnlyCollectionAdminProperties = keyof Pick< @@ -70,6 +78,8 @@ const serverOnlyCollectionProperties: Partial[] 'joins', 'polymorphicJoins', 'flattenedFields', + 'indexes', + 'sanitizedIndexes', // `upload` // `admin` // are all handled separately diff --git a/packages/payload/src/collections/config/defaults.ts b/packages/payload/src/collections/config/defaults.ts index 1c44245bae..3211688320 100644 --- a/packages/payload/src/collections/config/defaults.ts +++ b/packages/payload/src/collections/config/defaults.ts @@ -48,6 +48,7 @@ export const defaults: Partial = { me: [], refresh: [], }, + indexes: [], timestamps: true, upload: false, versions: false, diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts index 401f0f6c63..c97ad6d57e 100644 --- a/packages/payload/src/collections/config/sanitize.ts +++ b/packages/payload/src/collections/config/sanitize.ts @@ -2,6 +2,7 @@ import type { Config, SanitizedConfig } from '../../config/types.js' import type { CollectionConfig, + CompoundIndex, SanitizedCollectionConfig, SanitizedJoin, SanitizedJoins, @@ -26,6 +27,7 @@ import { addDefaultsToLoginWithUsernameConfig, } from './defaults.js' import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js' +import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js' import { validateUseAsTitle } from './useAsTitle.js' export const sanitizeCollection = async ( @@ -241,5 +243,10 @@ export const sanitizeCollection = async ( sanitizedConfig.flattenedFields = flattenAllFields({ fields: sanitizedConfig.fields }) + sanitizedConfig.sanitizedIndexes = sanitizeCompoundIndexes({ + fields: sanitizedConfig.flattenedFields, + indexes: sanitizedConfig.indexes, + }) + return sanitizedConfig } diff --git a/packages/payload/src/collections/config/sanitizeCompoundIndexes.ts b/packages/payload/src/collections/config/sanitizeCompoundIndexes.ts new file mode 100644 index 0000000000..38f83fc49d --- /dev/null +++ b/packages/payload/src/collections/config/sanitizeCompoundIndexes.ts @@ -0,0 +1,40 @@ +import type { FlattenedField } from '../../fields/config/types.js' +import type { CompoundIndex, SanitizedCompoundIndex } from './types.js' + +import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js' +import { getFieldByPath } from '../../utilities/getFieldByPath.js' + +export const sanitizeCompoundIndexes = ({ + fields, + indexes, +}: { + fields: FlattenedField[] + indexes: CompoundIndex[] +}): SanitizedCompoundIndex[] => { + const sanitizedCompoundIndexes: SanitizedCompoundIndex[] = [] + + for (const index of indexes) { + const sanitized: SanitizedCompoundIndex = { fields: [], unique: index.unique ?? false } + for (const path of index.fields) { + const result = getFieldByPath({ fields, path }) + + if (!result) { + throw new InvalidConfiguration(`Field ${path} was not found`) + } + + const { field, localizedPath, pathHasLocalized } = result + + if (['array', 'blocks', 'group', 'tab'].includes(field.type)) { + throw new InvalidConfiguration( + `Compound index on ${field.type} cannot be set. Path: ${localizedPath}`, + ) + } + + sanitized.fields.push({ field, localizedPath, path, pathHasLocalized }) + } + + sanitizedCompoundIndexes.push(sanitized) + } + + return sanitizedCompoundIndexes +} diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index af92b83724..7221eea5f7 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -464,6 +464,16 @@ export type CollectionConfig = { */ refresh?: RefreshHook[] } + /** + * Define compound indexes for this collection. + * This can be used to either speed up querying/sorting by 2 or more fields at the same time or + * to ensure uniqueness between several fields. + * Specify field paths + * @example + * [{ unique: true, fields: ['title', 'group.name'] }] + * @default [] + */ + indexes?: CompoundIndex[] /** * Label configuration */ @@ -542,7 +552,6 @@ export interface SanitizedCollectionConfig auth: Auth endpoints: Endpoint[] | false fields: Field[] - /** * Fields in the database schema structure * Rows / collapsible / tabs w/o name `fields` merged to top, UIs are excluded @@ -559,6 +568,8 @@ export interface SanitizedCollectionConfig */ polymorphicJoins: SanitizedJoin[] + sanitizedIndexes: SanitizedCompoundIndex[] + slug: CollectionSlug upload: SanitizedUploadConfig versions: SanitizedCollectionVersions @@ -602,3 +613,18 @@ export type TypeWithTimestamps = { id: number | string updatedAt: string } + +export type CompoundIndex = { + fields: string[] + unique?: boolean +} + +export type SanitizedCompoundIndex = { + fields: { + field: FlattenedField + localizedPath: string + path: string + pathHasLocalized: boolean + }[] + unique: boolean +} diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index a657cddaae..349befae35 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1058,6 +1058,8 @@ export type { TypeWithID, TypeWithTimestamps, } from './collections/config/types.js' +export type { CompoundIndex } from './collections/config/types.js' +export type { SanitizedCompoundIndex } from './collections/config/types.js' export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js' export { countOperation } from './collections/operations/count.js' export { createOperation } from './collections/operations/create.js' @@ -1072,6 +1074,7 @@ export { findVersionsOperation } from './collections/operations/findVersions.js' export { restoreVersionOperation } from './collections/operations/restoreVersion.js' export { updateOperation } from './collections/operations/update.js' export { updateByIDOperation } from './collections/operations/updateByID.js' + export { buildConfig } from './config/build.js' export { type ClientConfig, @@ -1080,7 +1083,6 @@ export { serverOnlyConfigProperties, type UnsanitizedClientConfig, } from './config/client.js' - export { defaults } from './config/defaults.js' export { sanitizeConfig } from './config/sanitize.js' export type * from './config/types.js' @@ -1193,10 +1195,11 @@ export { ValidationError, ValidationErrorName, } from './errors/index.js' + export type { ValidationFieldError } from './errors/index.js' export { baseBlockFields } from './fields/baseFields/baseBlockFields.js' - export { baseIDField } from './fields/baseFields/baseIDField.js' + export { createClientField, createClientFields, @@ -1308,12 +1311,12 @@ export type { ValueWithRelation, } from './fields/config/types.js' export { getDefaultValue } from './fields/getDefaultValue.js' - export { traverseFields as afterChangeTraverseFields } from './fields/hooks/afterChange/traverseFields.js' export { promise as afterReadPromise } from './fields/hooks/afterRead/promise.js' export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterRead/traverseFields.js' export { traverseFields as beforeChangeTraverseFields } from './fields/hooks/beforeChange/traverseFields.js' export { traverseFields as beforeValidateTraverseFields } from './fields/hooks/beforeValidate/traverseFields.js' + export { default as sortableFieldTypes } from './fields/sortableFieldTypes.js' export { validations } from './fields/validations.js' @@ -1348,6 +1351,7 @@ export type { UploadFieldValidation, UsernameFieldValidation, } from './fields/validations.js' + export { type ClientGlobalConfig, createClientGlobalConfig, @@ -1367,9 +1371,7 @@ export type { GlobalConfig, SanitizedGlobalConfig, } from './globals/config/types.js' - export { docAccessOperation as docAccessOperationGlobal } from './globals/operations/docAccess.js' - export { findOneOperation } from './globals/operations/findOne.js' export { findVersionByIDOperation as findVersionByIDOperationGlobal } from './globals/operations/findVersionByID.js' export { findVersionsOperation as findVersionsOperationGlobal } from './globals/operations/findVersions.js' @@ -1414,9 +1416,9 @@ export { importHandlerPath } from './queues/operations/runJobs/runJob/importHand export { getLocalI18n } from './translations/getLocalI18n.js' export * from './types/index.js' export { getFileByPath } from './uploads/getFileByPath.js' + export type * from './uploads/types.js' export { addDataAndFileToRequest } from './utilities/addDataAndFileToRequest.js' - export { addLocalesToRequestFromData, sanitizeLocales } from './utilities/addLocalesToRequest.js' export { commitTransaction } from './utilities/commitTransaction.js' export { @@ -1478,6 +1480,7 @@ export { traverseFields } from './utilities/traverseFields.js' export type { TraverseFieldsCallback } from './utilities/traverseFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' export { buildVersionGlobalFields } from './versions/buildGlobalFields.js' +export { buildVersionCompoundIndexes } from './versions/buildVersionCompoundIndexes.js' export { versionDefaults } from './versions/defaults.js' export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js' export { enforceMaxVersions } from './versions/enforceMaxVersions.js' diff --git a/packages/payload/src/utilities/getFieldByPath.ts b/packages/payload/src/utilities/getFieldByPath.ts new file mode 100644 index 0000000000..7760eacebf --- /dev/null +++ b/packages/payload/src/utilities/getFieldByPath.ts @@ -0,0 +1,70 @@ +import type { FlattenedField } from '../fields/config/types.js' + +/** + * Get the field from by its path. + * Can accept nested paths, e.g: group.title, array.group.title + * If there were any localized on the path, pathHasLocalized will be true and localizedPath will look like: + * group..title // group is localized here + */ +export const getFieldByPath = ({ + fields, + localizedPath = '', + path, +}: { + fields: FlattenedField[] + localizedPath?: string + path: string +}): { + field: FlattenedField + localizedPath: string + pathHasLocalized: boolean +} | null => { + let currentFields: FlattenedField[] = fields + + let currentField: FlattenedField | null = null + + const segments = path.split('.') + + let pathHasLocalized = false + + while (segments.length > 0) { + const segment = segments.shift() + localizedPath = `${localizedPath ? `${localizedPath}.` : ''}${segment}` + const field = currentFields.find((each) => each.name === segment) + + if (!field) { + return null + } + + if (field.localized) { + pathHasLocalized = true + localizedPath = `${localizedPath}.` + } + + if ('flattenedFields' in field) { + currentFields = field.flattenedFields + } + + if ('blocks' in field) { + for (const block of field.blocks) { + const maybeField = getFieldByPath({ + fields: block.flattenedFields, + localizedPath, + path: [...segments].join('.'), + }) + + if (maybeField) { + return maybeField + } + } + } + + currentField = field + } + + if (!currentField) { + return null + } + + return { field: currentField, localizedPath, pathHasLocalized } +} diff --git a/packages/payload/src/versions/buildVersionCompoundIndexes.ts b/packages/payload/src/versions/buildVersionCompoundIndexes.ts new file mode 100644 index 0000000000..3a5ccdfb7b --- /dev/null +++ b/packages/payload/src/versions/buildVersionCompoundIndexes.ts @@ -0,0 +1,17 @@ +import type { SanitizedCompoundIndex } from '../collections/config/types.js' + +export const buildVersionCompoundIndexes = ({ + indexes, +}: { + indexes: SanitizedCompoundIndex[] +}): SanitizedCompoundIndex[] => { + return indexes.map((each) => ({ + fields: each.fields.map(({ field, localizedPath, path, pathHasLocalized }) => ({ + field, + localizedPath: `version.${localizedPath}`, + path: `version.${path}`, + pathHasLocalized, + })), + unique: false, + })) +} diff --git a/test/database/config.ts b/test/database/config.ts index 14a086186c..7e372c6f6f 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -4,10 +4,9 @@ const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import type { TextField } from 'payload' -import { v4 as uuid } from 'uuid' +import { randomUUID } from 'crypto' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' -import { devUser } from '../credentials.js' import { seed } from './seed.js' import { customIDsSlug, @@ -501,7 +500,7 @@ export default buildConfigWithDefaults({ beforeChange: [ ({ value, operation }) => { if (operation === 'create') { - return uuid() + return randomUUID() } return value }, @@ -564,6 +563,43 @@ export default buildConfigWithDefaults({ ], 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, + }, + ], + }, ], globals: [ { diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 9b76303088..1509b5f980 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -7,6 +7,7 @@ import { migrateRelationshipsV2_V3, migrateVersionsV1_V2, } from '@payloadcms/db-mongodb/migration-utils' +import { randomUUID } from 'crypto' import { type Table } from 'drizzle-orm' import * as drizzlePg from 'drizzle-orm/pg-core' import * as drizzleSqlite from 'drizzle-orm/sqlite-core' @@ -305,6 +306,68 @@ describe('database', () => { }) }) + describe('Compound Indexes', () => { + beforeEach(async () => { + await payload.delete({ collection: 'compound-indexes', where: {} }) + }) + + it('top level: should throw a unique error', async () => { + await payload.create({ + collection: 'compound-indexes', + data: { three: randomUUID(), one: '1', two: '2' }, + }) + + // does not fail + await payload.create({ + collection: 'compound-indexes', + data: { three: randomUUID(), one: '1', two: '3' }, + }) + // does not fail + await payload.create({ + collection: 'compound-indexes', + data: { three: randomUUID(), one: '-1', two: '2' }, + }) + + // fails + await expect( + payload.create({ + collection: 'compound-indexes', + data: { three: randomUUID(), one: '1', two: '2' }, + }), + ).rejects.toBeTruthy() + }) + + it('combine group and top level: should throw a unique error', async () => { + await payload.create({ + collection: 'compound-indexes', + data: { + one: randomUUID(), + three: '3', + group: { four: '4' }, + }, + }) + + // does not fail + await payload.create({ + collection: 'compound-indexes', + data: { one: randomUUID(), three: '3', group: { four: '5' } }, + }) + // does not fail + await payload.create({ + collection: 'compound-indexes', + data: { one: randomUUID(), three: '4', group: { four: '4' } }, + }) + + // fails + await expect( + payload.create({ + collection: 'compound-indexes', + data: { one: randomUUID(), three: '3', group: { four: '4' } }, + }), + ).rejects.toBeTruthy() + }) + }) + describe('migrations', () => { let ranFreshTest = false diff --git a/test/database/payload-types.ts b/test/database/payload-types.ts index d2f2334eaa..a80a82aa00 100644 --- a/test/database/payload-types.ts +++ b/test/database/payload-types.ts @@ -78,6 +78,7 @@ export interface Config { 'custom-ids': CustomId; 'fake-custom-ids': FakeCustomId; 'relationships-migration': RelationshipsMigration; + 'compound-indexes': CompoundIndex; users: User; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -97,6 +98,7 @@ export interface Config { 'custom-ids': CustomIdsSelect | CustomIdsSelect; 'fake-custom-ids': FakeCustomIdsSelect | FakeCustomIdsSelect; 'relationships-migration': RelationshipsMigrationSelect | RelationshipsMigrationSelect; + 'compound-indexes': CompoundIndexesSelect | CompoundIndexesSelect; users: UsersSelect | UsersSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; @@ -400,6 +402,21 @@ export interface RelationshipsMigration { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "compound-indexes". + */ +export interface CompoundIndex { + id: string; + one?: string | null; + two?: string | null; + three?: string | null; + group?: { + four?: string | null; + }; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users". @@ -472,6 +489,10 @@ export interface PayloadLockedDocument { relationTo: 'relationships-migration'; value: string | RelationshipsMigration; } | null) + | ({ + relationTo: 'compound-indexes'; + value: string | CompoundIndex; + } | null) | ({ relationTo: 'users'; value: string | User; @@ -755,6 +776,22 @@ export interface RelationshipsMigrationSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "compound-indexes_select". + */ +export interface CompoundIndexesSelect { + one?: T; + two?: T; + three?: T; + group?: + | T + | { + four?: T; + }; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "users_select".