fix: create indexes in nested fields

This commit is contained in:
Dan Ribbens
2022-08-12 11:57:36 -04:00
parent d0da3d7962
commit f615abc9b1
4 changed files with 125 additions and 80 deletions

View File

@@ -369,7 +369,8 @@ export type FieldAffectingData =
| CodeField | CodeField
| PointField | PointField
export type NonPresentationalField = TextField export type NonPresentationalField =
TextField
| NumberField | NumberField
| EmailField | EmailField
| TextareaField | TextareaField

View File

@@ -14,7 +14,7 @@ export type BuildSchemaOptions = {
global?: boolean 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 = { type Index = {
index: IndexDefinition index: IndexDefinition
@@ -44,13 +44,11 @@ const localizeSchema = (field: NonPresentationalField, schema, localization) =>
return schema; 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; const { allowIDField, options } = buildSchemaOptions;
let fields = {}; let fields = {};
let schemaFields = configFields; let schemaFields = configFields;
const indexFields: Index[] = [];
if (!allowIDField) { if (!allowIDField) {
const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id'); 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]; const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type];
if (addFieldSchema) { if (addFieldSchema) {
addFieldSchema(field, schema, config, buildSchemaOptions); addFieldSchema(field, schema, config, buildSchemaOptions, indexes);
}
// 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 } });
} }
} }
}); });
if (buildSchemaOptions?.options?.timestamps) { if (buildSchemaOptions?.options?.timestamps) {
indexFields.push({ index: { createdAt: 1 } }); indexes.push({ index: { createdAt: 1 } });
indexFields.push({ index: { updatedAt: 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); schema.index(indexField.index, indexField.options);
}); });
return schema; return schema;
}; };
const fieldIndexMap = { const addFieldIndex = (field: NonPresentationalField, indexFields: Index[], config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => {
point: (field: PointField, config: SanitizedConfig) => { if (config.indexSortableFields && !buildSchemaOptions.global && !field.index && !field.hidden && sortableFieldTypes.indexOf(field.type) > -1 && fieldAffectsData(field)) {
let direction: boolean | '2dsphere'; indexFields.push({ index: { [field.name]: 1 } });
const options: IndexOptions = { } else if (field.unique && fieldAffectsData(field)) {
unique: field.unique || false, indexFields.push({ index: { [field.name]: 1 }, options: { unique: !buildSchemaOptions.disableUnique, sparse: field.localized || false } });
sparse: (field.localized && field.unique) || false, } else if (field.index && fieldAffectsData(field)) {
}; indexFields.push({ index: { [field.name]: 1 } });
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 fieldToSchemaMap = { const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
number: (field: NumberField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { number: (field: NumberField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions, indexes: Index[]): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Number }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Number };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 = { const baseSchema = {
type: { type: {
type: String, type: String,
@@ -173,8 +152,8 @@ const fieldToSchemaMap = {
}, },
coordinates: { coordinates: {
type: [Number], type: [Number],
sparse: field.unique && field.localized, sparse: (buildSchemaOptions.disableUnique && field.unique) && field.localized,
unique: field.unique || false, unique: (buildSchemaOptions.disableUnique && field.unique) || false,
required: false, required: false,
default: field.defaultValue || undefined, default: field.defaultValue || undefined,
}, },
@@ -183,8 +162,30 @@ const fieldToSchemaMap = {
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 = { const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions), ...formatBaseSchema(field, buildSchemaOptions),
type: String, type: String,
@@ -197,22 +198,25 @@ const fieldToSchemaMap = {
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 }; const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 = { const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions), ...formatBaseSchema(field, buildSchemaOptions),
type: Schema.Types.Mixed, type: Schema.Types.Mixed,
@@ -222,8 +226,9 @@ const fieldToSchemaMap = {
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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); const hasManyRelations = Array.isArray(field.relationTo);
let schemaToReturn: { [key: string]: any } = {}; let schemaToReturn: { [key: string]: any } = {};
@@ -276,51 +281,57 @@ const fieldToSchemaMap = {
schema.add({ schema.add({
[field.name]: schemaToReturn, [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) => { field.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type];
if (addFieldSchema) { 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) => { field.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type];
if (addFieldSchema) { 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) => { field.tabs.forEach((tab) => {
tab.fields.forEach((subField: Field) => { tab.fields.forEach((subField: Field) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type];
if (addFieldSchema) { 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 = { const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions), ...formatBaseSchema(field, buildSchemaOptions),
type: [buildSchema(config, field.fields, { type: [buildSchema(
options: { _id: false, id: false }, config,
allowIDField: true, field.fields,
disableUnique: buildSchemaOptions.disableUnique, {
})], options: { _id: false, id: false },
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
},
indexes,
)],
}; };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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; let { required } = field;
if (field?.admin?.condition || field?.localized || field?.access?.create) required = false; if (field?.admin?.condition || field?.localized || field?.access?.create) required = false;
@@ -329,20 +340,25 @@ const fieldToSchemaMap = {
const baseSchema = { const baseSchema = {
...formattedBaseSchema, ...formattedBaseSchema,
required: required && field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized && !subField?.admin?.condition && !subField?.access?.create)), required: required && field.fields.some((subField) => (!fieldIsPresentationalOnly(subField) && subField.required && !subField.localized && !subField?.admin?.condition && !subField?.access?.create)),
type: buildSchema(config, field.fields, { type: buildSchema(
options: { config,
_id: false, field.fields,
id: false, {
options: {
_id: false,
id: false,
},
disableUnique: buildSchemaOptions.disableUnique,
}, },
disableUnique: buildSchemaOptions.disableUnique, indexes,
}), ),
}; };
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [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 = { const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions), ...formatBaseSchema(field, buildSchemaOptions),
type: String, type: String,
@@ -356,8 +372,9 @@ const fieldToSchemaMap = {
schema.add({ schema.add({
[field.name]: field.hasMany ? [schemaToReturn] : schemaToReturn, [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' })]; const fieldSchema = [new Schema({}, { _id: false, discriminatorKey: 'blockType' })];
let schemaToReturn; let schemaToReturn;
@@ -380,7 +397,7 @@ const fieldToSchemaMap = {
blockItem.fields.forEach((blockField) => { blockItem.fields.forEach((blockField) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]; const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type];
if (addFieldSchema) { if (addFieldSchema) {
addFieldSchema(blockField, blockSchema, config, buildSchemaOptions); addFieldSchema(blockField, blockSchema, config, buildSchemaOptions, indexes);
} }
}); });

View File

@@ -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,
},
],
},
], ],
}; };

View File

@@ -118,6 +118,9 @@ describe('Fields', () => {
const options: Record<string, IndexOptions> = {}; const options: Record<string, IndexOptions> = {};
beforeAll(() => { 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<string, IndexDirection>, IndexOptions]; indexes = payload.collections['indexed-fields'].Model.schema.indexes() as [Record<string, IndexDirection>, IndexOptions];
indexes.forEach((index) => { indexes.forEach((index) => {
@@ -147,6 +150,12 @@ describe('Fields', () => {
expect(definitions['group.localizedUnique.es']).toEqual(1); expect(definitions['group.localizedUnique.es']).toEqual(1);
expect(options['group.localizedUnique.es']).toMatchObject({ unique: true, sparse: true }); 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', () => { describe('point', () => {