feat: supports collection compound indexes (#2529)

Co-authored-by: Perry Li <yuanping.li@moblab.com>
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Perry
2023-04-26 23:11:01 +08:00
committed by GitHub
parent bf6522898d
commit 85b3d579d3
8 changed files with 112 additions and 11 deletions

View File

@@ -16,6 +16,7 @@ It's often best practice to write your Collections in separate files and then im
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`indexes`** * | Array of database indexes to create, including compound indexes that have multiple fields. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
@@ -52,7 +53,7 @@ const Orders: CollectionConfig = {
relationTo: 'customers',
required: true,
}
]
],
};
```

View File

@@ -24,7 +24,11 @@ const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: Sa
schema.index({ updatedAt: 1 });
schema.index({ createdAt: 1 });
}
if (collection.indexes) {
collection.indexes.forEach((index) => {
schema.index(index.fields, index.options);
});
}
schema.plugin(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }));

View File

@@ -72,6 +72,12 @@ const collectionSchema = joi.object().keys({
hideAPIURL: joi.bool(),
}),
fields: joi.array(),
indexes: joi.array().items(
joi.object().keys({
fields: joi.object().required(),
options: joi.object(),
}),
),
hooks: joi.object({
beforeOperation: joi.array().items(joi.func()),
beforeValidate: joi.array().items(joi.func()),

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DeepRequired } from 'ts-essentials';
import { AggregatePaginateModel, Model, PaginateModel } from 'mongoose';
import { AggregatePaginateModel, IndexDefinition, IndexOptions, Model, PaginateModel } from 'mongoose';
import { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from 'graphql';
import { Response } from 'express';
import { Access, Endpoint, EntityDescription, GeneratePreviewURL } from '../../config/types';
@@ -220,8 +220,8 @@ export type CollectionConfig = {
plural?: Record<string, string> | string;
};
/**
* Default field to sort by in collection list view
*/
* Default field to sort by in collection list view
*/
defaultSort?: string;
/**
* GraphQL configuration
@@ -240,6 +240,10 @@ export type CollectionConfig = {
interface?: string
}
fields: Field[];
/**
* Array of database indexes to create, including compound indexes that have multiple fields
*/
indexes?: TypeOfIndex[];
/**
* Collection admin options
*/
@@ -352,3 +356,8 @@ export type TypeWithTimestamps = {
updatedAt: string
[key: string]: unknown
}
export type TypeOfIndex = {
fields: IndexDefinition
options?: IndexOptions
}

View File

@@ -77,6 +77,17 @@ export default function initCollectionsLocal(ctx: Payload): void {
},
);
if (collection.indexes) {
collection.indexes.forEach((index) => {
// prefix 'version.' to each field in the index
const versionIndex = { fields: {}, options: index.options };
Object.entries(index.fields).forEach(([key, value]) => {
versionIndex.fields[`version.${key}`] = value;
});
versionSchema.index(versionIndex.fields, versionIndex.options);
});
}
versionSchema.plugin(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, versionsFields: versionCollectionFields }));

View File

@@ -11,11 +11,15 @@ const beforeDuplicate: BeforeDuplicate<IndexedField> = ({ data }) => {
},
collapsibleTextUnique: data.collapsibleTextUnique ? `${data.collapsibleTextUnique}-copy` : '',
collapsibleLocalizedUnique: data.collapsibleLocalizedUnique ? `${data.collapsibleLocalizedUnique}-copy` : '',
partOne: data.partOne ? `${data.partOne}-copy` : '',
partTwo: data.partTwo ? `${data.partTwo}-copy` : '',
};
};
const IndexedFields: CollectionConfig = {
slug: 'indexed-fields',
// used to assert that versions also get indexes
versions: true,
admin: {
hooks: {
beforeDuplicate,
@@ -71,6 +75,20 @@ const IndexedFields: CollectionConfig = {
},
],
},
{
name: 'partOne',
type: 'text',
},
{
name: 'partTwo',
type: 'text',
},
],
indexes: [
{
fields: { partOne: 1, partTwo: 1 },
options: { unique: true, name: 'compound-index', sparse: true },
},
],
};

View File

@@ -4,8 +4,8 @@ import { RESTClient } from '../helpers/rest';
import configPromise from '../uploads/config';
import payload from '../../src';
import { pointDoc } from './collections/Point';
import { arrayFieldsSlug, arrayDefaultValue, arrayDoc } from './collections/Array';
import { groupFieldsSlug, groupDefaultChild, groupDefaultValue, groupDoc } from './collections/Group';
import { arrayDefaultValue, arrayDoc, arrayFieldsSlug } from './collections/Array';
import { groupDefaultChild, groupDefaultValue, groupDoc, groupFieldsSlug } from './collections/Group';
import { defaultText } from './collections/Text';
import { blocksFieldSeedData } from './collections/Blocks';
import { localizedTextValue, namedTabDefaultValue, namedTabText, tabsDoc, tabsSlug } from './collections/Tabs';
@@ -162,9 +162,6 @@ describe('Fields', () => {
const options: Record<string, IndexOptions> = {};
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.forEach((index) => {
@@ -200,7 +197,10 @@ describe('Fields', () => {
expect(definitions.collapsibleTextUnique).toEqual(1);
expect(options.collapsibleTextUnique).toMatchObject({ unique: true });
});
it('should have unique compound indexes', () => {
expect(definitions.partOne).toEqual(1);
expect(options.partOne).toMatchObject({ unique: true, name: 'compound-index', sparse: true });
});
it('should throw validation error saving on unique fields', async () => {
const data = {
text: 'a',
@@ -218,6 +218,56 @@ describe('Fields', () => {
return result.error;
}).toBeDefined();
});
it('should throw validation error saving on unique combined fields', async () => {
await payload.delete({ collection: 'indexed-fields', where: {} });
const data1 = {
text: 'a',
uniqueText: 'a',
partOne: 'u',
partTwo: 'u',
};
const data2 = {
text: 'b',
uniqueText: 'b',
partOne: 'u',
partTwo: 'u',
};
await payload.create({
collection: 'indexed-fields',
data: data1,
});
expect(async () => {
const result = await payload.create({
collection: 'indexed-fields',
data: data2,
});
return result.error;
}).toBeDefined();
});
});
describe('version indexes', () => {
let indexes;
const definitions: Record<string, IndexDirection> = {};
const options: Record<string, IndexOptions> = {};
beforeAll(() => {
indexes = payload.versions['indexed-fields'].schema.indexes() as [Record<string, IndexDirection>, IndexOptions];
indexes.forEach((index) => {
const field = Object.keys(index[0])[0];
definitions[field] = index[0][field];
// eslint-disable-next-line prefer-destructuring
options[field] = index[1];
});
});
it('should have versions indexes', () => {
expect(definitions['version.text']).toEqual(1);
});
it('should have version indexes from collection indexes', () => {
expect(definitions['version.partOne']).toEqual(1);
expect(options['version.partOne']).toMatchObject({ unique: true, name: 'compound-index', sparse: true });
});
});
describe('point', () => {

View File

@@ -328,6 +328,8 @@ export interface IndexedField {
};
collapsibleLocalizedUnique?: string;
collapsibleTextUnique?: string;
partOne?: string;
partTwo?: string;
createdAt: string;
updatedAt: string;
}