diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index e694d47ed..ae1647d0c 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -369,7 +369,8 @@ export type FieldAffectingData = | CodeField | PointField -export type NonPresentationalField = TextField +export type NonPresentationalField = + TextField | NumberField | EmailField | TextareaField diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index c5eed1f4f..402fee44c 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -14,7 +14,7 @@ export type BuildSchemaOptions = { global?: boolean } -type FieldSchemaGenerator = (field: Field, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => void; +type FieldSchemaGenerator = (field: Field, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]) => void; type Index = { index: IndexDefinition @@ -44,13 +44,11 @@ const localizeSchema = (field: NonPresentationalField, schema, localization) => return schema; }; -const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchemaOptions: BuildSchemaOptions = {}): Schema => { +const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchemaOptions: BuildSchemaOptions = {}, indexes: Index[] = []): Schema => { const { allowIDField, options } = buildSchemaOptions; let fields = {}; - let schemaFields = configFields; - const indexFields: Index[] = []; if (!allowIDField) { const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id'); @@ -69,103 +67,84 @@ const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchema const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]; if (addFieldSchema) { - addFieldSchema(field, schema, config, buildSchemaOptions); - } - - // geospatial field index must be created after the schema is created - if (fieldIndexMap[field.type]) { - indexFields.push(...fieldIndexMap[field.type](field, config)); - } - - if (config.indexSortableFields && !buildSchemaOptions.global && !field.index && !field.hidden && sortableFieldTypes.indexOf(field.type) > -1 && fieldAffectsData(field)) { - indexFields.push({ index: { [field.name]: 1 } }); - } else if (field.unique && fieldAffectsData(field)) { - indexFields.push({ index: { [field.name]: 1 }, options: { unique: !buildSchemaOptions.disableUnique, sparse: field.localized || false } }); - } else if (field.index && fieldAffectsData(field)) { - indexFields.push({ index: { [field.name]: 1 } }); + addFieldSchema(field, schema, config, buildSchemaOptions, indexes); } } }); if (buildSchemaOptions?.options?.timestamps) { - indexFields.push({ index: { createdAt: 1 } }); - indexFields.push({ index: { updatedAt: 1 } }); + indexes.push({ index: { createdAt: 1 } }); + indexes.push({ index: { updatedAt: 1 } }); } - indexFields.forEach((indexField) => { + // mongoose on mongoDB 5 or 6 need to call this to make the index in the database, schema indexes alone are not enough + indexes.forEach((indexField) => { schema.index(indexField.index, indexField.options); }); return schema; }; -const fieldIndexMap = { - point: (field: PointField, config: SanitizedConfig) => { - let direction: boolean | '2dsphere'; - const options: IndexOptions = { - unique: field.unique || false, - sparse: (field.localized && field.unique) || false, - }; - if (field.index === true || field.index === undefined) { - direction = '2dsphere'; - } - if (field.localized && config.localization) { - return config.localization.locales.map((locale) => ({ - index: { [`${field.name}.${locale}`]: direction }, - options, - })); - } - if (field.unique) { - options.unique = true; - } - return [{ index: { [field.name]: direction }, options }]; - }, +const addFieldIndex = (field: NonPresentationalField, indexFields: Index[], config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => { + if (config.indexSortableFields && !buildSchemaOptions.global && !field.index && !field.hidden && sortableFieldTypes.indexOf(field.type) > -1 && fieldAffectsData(field)) { + indexFields.push({ index: { [field.name]: 1 } }); + } else if (field.unique && fieldAffectsData(field)) { + indexFields.push({ index: { [field.name]: 1 }, options: { unique: !buildSchemaOptions.disableUnique, sparse: field.localized || false } }); + } else if (field.index && fieldAffectsData(field)) { + indexFields.push({ index: { [field.name]: 1 } }); + } }; -const fieldToSchemaMap = { - number: (field: NumberField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { +const fieldToSchemaMap: Record = { + number: (field: NumberField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Number }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - text: (field: TextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + text: (field: TextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - email: (field: EmailField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + email: (field: EmailField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - textarea: (field: TextareaField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + textarea: (field: TextareaField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - richText: (field: RichTextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + richText: (field: RichTextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - code: (field: CodeField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + code: (field: CodeField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - point: (field: PointField, schema: Schema, config: SanitizedConfig): void => { + point: (field: PointField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { type: { type: String, @@ -173,8 +152,8 @@ const fieldToSchemaMap = { }, coordinates: { type: [Number], - sparse: field.unique && field.localized, - unique: field.unique || false, + sparse: (buildSchemaOptions.disableUnique && field.unique) && field.localized, + unique: (buildSchemaOptions.disableUnique && field.unique) || false, required: false, default: field.defaultValue || undefined, }, @@ -183,8 +162,30 @@ const fieldToSchemaMap = { schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + + // creates geospatial 2dsphere index by default + let direction; + const options: IndexOptions = { + unique: field.unique || false, + sparse: (field.localized && field.unique) || false, + }; + if (field.index === true || field.index === undefined) { + direction = '2dsphere'; + } + if (field.localized && config.localization) { + indexes.push( + ...config.localization.locales.map((locale) => ({ + index: { [`${field.name}.${locale}`]: direction }, + options, + })), + ); + } + if (field.unique) { + options.unique = true; + } + indexes.push({ index: { [field.name]: direction }, options }); }, - radio: (field: RadioField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + radio: (field: RadioField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String, @@ -197,22 +198,25 @@ const fieldToSchemaMap = { schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - checkbox: (field: CheckboxField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + checkbox: (field: CheckboxField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - date: (field: DateField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + date: (field: DateField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - upload: (field: UploadField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + upload: (field: UploadField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed, @@ -222,8 +226,9 @@ const fieldToSchemaMap = { schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - relationship: (field: RelationshipField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => { + relationship: (field: RelationshipField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]) => { const hasManyRelations = Array.isArray(field.relationTo); let schemaToReturn: { [key: string]: any } = {}; @@ -276,51 +281,57 @@ const fieldToSchemaMap = { schema.add({ [field.name]: schemaToReturn, }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - row: (field: RowField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + row: (field: RowField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { field.fields.forEach((subField: Field) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); + addFieldSchema(subField, schema, config, buildSchemaOptions, indexes); } }); }, - collapsible: (field: CollapsibleField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + collapsible: (field: CollapsibleField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { field.fields.forEach((subField: Field) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); + addFieldSchema(subField, schema, config, buildSchemaOptions, indexes); } }); }, - tabs: (field: TabsField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + tabs: (field: TabsField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { field.tabs.forEach((tab) => { tab.fields.forEach((subField: Field) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); + addFieldSchema(subField, schema, config, buildSchemaOptions, indexes); } }); }); }, - array: (field: ArrayField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => { + array: (field: ArrayField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]) => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: [buildSchema(config, field.fields, { - options: { _id: false, id: false }, - allowIDField: true, - disableUnique: buildSchemaOptions.disableUnique, - })], + type: [buildSchema( + config, + field.fields, + { + options: { _id: false, id: false }, + allowIDField: true, + disableUnique: buildSchemaOptions.disableUnique, + }, + indexes, + )], }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); }, - group: (field: GroupField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + group: (field: GroupField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { let { required } = field; if (field?.admin?.condition || field?.localized || field?.access?.create) required = false; @@ -329,20 +340,25 @@ const fieldToSchemaMap = { const baseSchema = { ...formattedBaseSchema, required: required && field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized && !subField?.admin?.condition && !subField?.access?.create)), - type: buildSchema(config, field.fields, { - options: { - _id: false, - id: false, + type: buildSchema( + config, + field.fields, + { + options: { + _id: false, + id: false, + }, + disableUnique: buildSchemaOptions.disableUnique, }, - disableUnique: buildSchemaOptions.disableUnique, - }), + indexes, + ), }; schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), }); }, - select: (field: SelectField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + select: (field: SelectField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String, @@ -356,8 +372,9 @@ const fieldToSchemaMap = { schema.add({ [field.name]: field.hasMany ? [schemaToReturn] : schemaToReturn, }); + addFieldIndex(field, indexes, config, buildSchemaOptions); }, - blocks: (field: BlockField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + blocks: (field: BlockField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => { const fieldSchema = [new Schema({}, { _id: false, discriminatorKey: 'blockType' })]; let schemaToReturn; @@ -380,7 +397,7 @@ const fieldToSchemaMap = { blockItem.fields.forEach((blockField) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]; if (addFieldSchema) { - addFieldSchema(blockField, blockSchema, config, buildSchemaOptions); + addFieldSchema(blockField, blockSchema, config, buildSchemaOptions, indexes); } }); diff --git a/test/fields/collections/Indexed/index.ts b/test/fields/collections/Indexed/index.ts index 08d91f267..139c28d01 100644 --- a/test/fields/collections/Indexed/index.ts +++ b/test/fields/collections/Indexed/index.ts @@ -34,6 +34,24 @@ const IndexedFields: CollectionConfig = { }, ], }, + { + type: 'collapsible', + label: 'Collapsible', + fields: [ + { + name: 'collapsibleLocalizedUnique', + type: 'text', + unique: true, + localized: true, + }, + { + name: 'collapsibleTextUnique', + type: 'text', + label: 'collapsibleTextUnique', + unique: true, + }, + ], + }, ], }; diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 660b33bd4..bf72d4a45 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -118,6 +118,9 @@ describe('Fields', () => { const options: Record = {}; beforeAll(() => { + // mongoose model schema indexes do not always create indexes in the actual database + // see: https://github.com/payloadcms/payload/issues/571 + indexes = payload.collections['indexed-fields'].Model.schema.indexes() as [Record, IndexOptions]; indexes.forEach((index) => { @@ -147,6 +150,12 @@ describe('Fields', () => { expect(definitions['group.localizedUnique.es']).toEqual(1); expect(options['group.localizedUnique.es']).toMatchObject({ unique: true, sparse: true }); }); + it('should have unique indexes in a collapsible', () => { + expect(definitions['collapsibleLocalizedUnique.en']).toEqual(1); + expect(options['collapsibleLocalizedUnique.en']).toMatchObject({ unique: true, sparse: true }); + expect(definitions.collapsibleTextUnique).toEqual(1); + expect(options.collapsibleTextUnique).toMatchObject({ unique: true }); + }); }); describe('point', () => {