From a0b38f68322cd7a39ca6ae08e6ffb7f57aa62171 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Oct 2021 11:33:56 -0400 Subject: [PATCH 01/25] fix: ensures tooltips in email fields are positioned properly --- src/admin/components/forms/field-types/Email/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/admin/components/forms/field-types/Email/index.scss b/src/admin/components/forms/field-types/Email/index.scss index 0f72f9022e..1d8bfced47 100644 --- a/src/admin/components/forms/field-types/Email/index.scss +++ b/src/admin/components/forms/field-types/Email/index.scss @@ -2,6 +2,7 @@ .field-type.email { margin-bottom: $baseline; + position: relative; input { @include formInput; From b4c15ed3f3649ea6d157987571874fb8486ab3cb Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Oct 2021 11:34:23 -0400 Subject: [PATCH 02/25] fix: #348, relationship options appearing twice in admin ui --- .../forms/field-types/Relationship/index.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index e2323d9232..ce02b26486 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -201,15 +201,6 @@ const Relationship: React.FC = (props) => { } }, [addOptions, api, errorLoading, serverURL]); - // /////////////////////////// - // Get first results - // /////////////////////////// - - useEffect(() => { - getResults(); - setHasLoadedFirstOptions(true); - }, [addOptions, api, required, relationTo, serverURL, getResults]); - // /////////////////////////// // Get results when search input changes // /////////////////////////// @@ -220,6 +211,7 @@ const Relationship: React.FC = (props) => { required, }); + setHasLoadedFirstOptions(true); setLastLoadedPage(1); setLastFullyLoadedRelation(-1); getResults({ search: debouncedSearch }); From a870cc70361a0987db13d5651d3878b8ecc4c23c Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Oct 2021 11:37:09 -0400 Subject: [PATCH 03/25] chore(release): v0.12.3 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ffb5d4ed..a7722ad0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.12.3](https://github.com/payloadcms/payload/compare/v0.12.2...v0.12.3) (2021-10-23) + + +### Bug Fixes + +* [#348](https://github.com/payloadcms/payload/issues/348), relationship options appearing twice in admin ui ([b4c15ed](https://github.com/payloadcms/payload/commit/b4c15ed3f3649ea6d157987571874fb8486ab3cb)) +* ensures tooltips in email fields are positioned properly ([a0b38f6](https://github.com/payloadcms/payload/commit/a0b38f68322cd7a39ca6ae08e6ffb7f57aa62171)) + ## [0.12.2](https://github.com/payloadcms/payload/compare/v0.12.1...v0.12.2) (2021-10-21) diff --git a/package.json b/package.json index 31e47208e4..0ded1f2af5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "0.12.2", + "version": "0.12.3", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "SEE LICENSE IN license.md", "author": { From 4c8574784995b1cb1f939648f4d2158286089b3d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 Oct 2021 00:49:27 -0400 Subject: [PATCH 04/25] feat: improves querying logic --- demo/payload.config.ts | 2 +- package.json | 2 +- .../graphql/resolvers/resolvers.spec.js | 6 +- src/collections/requestHandlers/find.ts | 12 +- src/collections/tests/collections.spec.js | 22 +- src/collections/tests/hooks.spec.js | 10 +- src/index.ts | 2 + src/mongoose/buildQuery.ts | 378 +++++++++++------- src/mongoose/buildSchema.ts | 32 +- src/mongoose/connect.ts | 7 +- .../createArrayFromCommaDelineated.ts | 7 + src/mongoose/getSchemaTypeOptions.ts | 9 + src/mongoose/operatorMap.ts | 13 + src/mongoose/sanitizeFormattedValue.ts | 109 +++++ src/utilities/combineMerge.ts | 4 +- yarn.lock | 28 +- 16 files changed, 470 insertions(+), 173 deletions(-) create mode 100644 src/mongoose/createArrayFromCommaDelineated.ts create mode 100644 src/mongoose/getSchemaTypeOptions.ts create mode 100644 src/mongoose/operatorMap.ts create mode 100644 src/mongoose/sanitizeFormattedValue.ts diff --git a/demo/payload.config.ts b/demo/payload.config.ts index 78174d766f..ddddca5107 100644 --- a/demo/payload.config.ts +++ b/demo/payload.config.ts @@ -45,7 +45,7 @@ export default buildConfig({ // // ogImage: '/static/find-image-here.jpg', // // favicon: '/img/whatever.png', // }, - disable: false, + // disable: true, scss: path.resolve(__dirname, './client/scss/overrides.scss'), components: { // Nav: () => ( diff --git a/package.json b/package.json index 0ded1f2af5..3a1efddc51 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "mini-css-extract-plugin": "1.3.3", "minimist": "^1.2.0", "mkdirp": "^1.0.4", - "mongoose": "^5.8.9", + "mongoose": "^6.0.12", "mongoose-paginate-v2": "^1.3.6", "node-sass": "^6.0.1", "nodemailer": "^6.4.2", diff --git a/src/collections/graphql/resolvers/resolvers.spec.js b/src/collections/graphql/resolvers/resolvers.spec.js index 1fa99237d0..6ae36448f6 100644 --- a/src/collections/graphql/resolvers/resolvers.spec.js +++ b/src/collections/graphql/resolvers/resolvers.spec.js @@ -68,7 +68,7 @@ describe('GrahpQL Resolvers', () => { describe('Read', () => { it('should be able to read localized post', async () => { - const title = 'gql read'; + const title = 'gql read 1'; const description = 'description'; // language=graphQL @@ -99,7 +99,7 @@ describe('GrahpQL Resolvers', () => { }); it('should query exists - true', async () => { - const title = 'gql read'; + const title = 'gql read 2'; const description = 'description'; const summary = 'summary'; @@ -136,7 +136,7 @@ describe('GrahpQL Resolvers', () => { }); it('should query exists - false', async () => { - const title = 'gql read'; + const title = 'gql read 3'; const description = 'description'; // language=graphQL diff --git a/src/collections/requestHandlers/find.ts b/src/collections/requestHandlers/find.ts index 85e1e5f8c9..f5d23d29b2 100644 --- a/src/collections/requestHandlers/find.ts +++ b/src/collections/requestHandlers/find.ts @@ -5,11 +5,21 @@ import { PaginatedDocs } from '../config/types'; export default async function find(req: PayloadRequest, res: Response, next: NextFunction): Promise | void> { try { + let page; + + if (typeof req.query.page === 'string') { + const parsedPage = parseInt(req.query.page, 10); + + if (!Number.isNaN(parsedPage)) { + page = parsedPage; + } + } + const options = { req, collection: req.collection, where: req.query.where, - page: req.query.page, + page, limit: req.query.limit, sort: req.query.sort, depth: req.query.depth, diff --git a/src/collections/tests/collections.spec.js b/src/collections/tests/collections.spec.js index ddd235d3ef..47da21d231 100644 --- a/src/collections/tests/collections.spec.js +++ b/src/collections/tests/collections.spec.js @@ -103,7 +103,7 @@ describe('Collections - REST', () => { it('should allow updating an existing post', async () => { const createResponse = await fetch(`${url}/api/localized-posts`, { body: JSON.stringify({ - title: 'title', + title: 'newTitle', description: 'original description', richText: [{ children: [{ text: 'english' }], @@ -132,7 +132,7 @@ describe('Collections - REST', () => { ]; const response = await fetch(`${url}/api/localized-posts/${id}`, { body: JSON.stringify({ - title: 'title', + title: 'newTitle', description: updatedDesc, richText: updatedRichText, nonLocalizedArray: updatedNonLocalizedArray, @@ -161,7 +161,7 @@ describe('Collections - REST', () => { it('should allow a Spanish locale to be added to an existing post', async () => { const response = await fetch(`${url}/api/localized-posts/${localizedPostID}?locale=es`, { body: JSON.stringify({ - title: 'title', + title: 'title in spanish', description: spanishPostDesc, priority: 1, nonLocalizedGroup: { @@ -251,7 +251,7 @@ describe('Collections - REST', () => { it('should allow querying by id', async () => { const response = await fetch(`${url}/api/localized-posts`, { body: JSON.stringify({ - title: 'title', + title: 'another title', description: 'description', priority: 1, }), @@ -275,9 +275,12 @@ describe('Collections - REST', () => { const desc = 'query test'; const response = await fetch(`${url}/api/localized-posts`, { body: JSON.stringify({ - title: 'title', + title: 'unique title here', description: desc, priority: 1, + nonLocalizedGroup: { + text: 'sample content', + }, }), headers, method: 'post', @@ -290,6 +293,13 @@ describe('Collections - REST', () => { expect(getResponse.status).toBe(200); expect(data.docs[0].description).toBe(desc); expect(data.docs).toHaveLength(1); + + const getResponse2 = await fetch(`${url}/api/localized-posts?where[nonLocalizedGroup.text][equals]=sample content`); + const data2 = await getResponse2.json(); + + expect(getResponse2.status).toBe(200); + expect(data2.docs[0].description).toBe(desc); + expect(data2.docs).toHaveLength(1); }); it('should allow querying with OR', async () => { @@ -366,7 +376,7 @@ describe('Collections - REST', () => { it('should allow a post to be deleted', async () => { const response = await fetch(`${url}/api/localized-posts`, { body: JSON.stringify({ - title: 'title', + title: 'title to be deleted', description: englishPostDesc, priority: 1, }), diff --git a/src/collections/tests/hooks.spec.js b/src/collections/tests/hooks.spec.js index 34dc4d6bf9..f09a4724f1 100644 --- a/src/collections/tests/hooks.spec.js +++ b/src/collections/tests/hooks.spec.js @@ -37,7 +37,7 @@ describe('Collections - REST', () => { it('beforeChange', async () => { const response = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ - title: 'title', + title: 'title 1', description: 'Original', priority: 1, }), @@ -55,7 +55,7 @@ describe('Collections - REST', () => { it('beforeDelete', async () => { const createResponse = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ - title: 'title', + title: 'title 2', description: 'Original', priority: 1, }), @@ -84,7 +84,7 @@ describe('Collections - REST', () => { it('afterRead', async () => { const response = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ - title: 'title', + title: 'title 3', description: 'afterRead', priority: 1, }), @@ -104,7 +104,7 @@ describe('Collections - REST', () => { it('afterChange', async () => { const createResponse = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ - title: 'title', + title: 'title 4', description: 'Original', priority: 1, }), @@ -133,7 +133,7 @@ describe('Collections - REST', () => { it('afterDelete', async () => { const createResponse = await fetch(`${url}/api/hooks`, { body: JSON.stringify({ - title: 'title', + title: 'title 5', description: 'Original', priority: 1, }), diff --git a/src/index.ts b/src/index.ts index a7ed922f3a..c471f89a5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -148,6 +148,8 @@ export class Payload { initPreferences(this); // Connect to database + + connectMongoose(this.mongoURL, options.mongoOptions, options.local); // If not initializing locally, set up HTTP routing diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 0a948ebb58..908a015037 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -1,21 +1,19 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ +import deepmerge from 'deepmerge'; import mongoose, { FilterQuery } from 'mongoose'; +import { combineMerge } from '../utilities/combineMerge'; +import { CollectionModel } from '../collections/config/types'; +import { getSchemaTypeOptions } from './getSchemaTypeOptions'; +import { operatorMap } from './operatorMap'; +import { sanitizeQueryValue } from './sanitizeFormattedValue'; const validOperators = ['like', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near']; -function addSearchParam(key, value, searchParams) { - return { - ...searchParams, - [key]: value, - }; -} -function convertArrayFromCommaDelineated(input) { - if (Array.isArray(input)) return input; - if (input.indexOf(',') > -1) { - return input.split(','); - } - return [input]; -} + +const subQueryOptions = { + limit: 50, + lean: true, +}; type ParseType = { searchParams?: @@ -25,6 +23,17 @@ type ParseType = { sort?: boolean; }; +type PathToQuery = { + complete: boolean + path: string + Model: CollectionModel +} + +type SearchParam = { + path?: string, + value: unknown, +} + class ParamParser { locale: string; @@ -50,10 +59,6 @@ class ParamParser { }; } - getLocalizedKey(key: string, schemaObject) { - return `${key}${(schemaObject && schemaObject.localized) ? `.${this.locale}` : ''}`; - } - // Entry point to the ParamParser class async parse(): Promise { @@ -91,10 +96,17 @@ class ParamParser { for (const operator of Object.keys(pathOperators)) { if (validOperators.includes(operator)) { const searchParam = await this.buildSearchParam(this.model.schema, relationOrPath, pathOperators[operator], operator); - if (Array.isArray(searchParam)) { - const [key, value] = searchParam; - result = addSearchParam(key, value, result); + + if ('path' in searchParam) { + result = { + ...result, + [searchParam.path]: searchParam.value, + }; + } else if (typeof searchParam.value === 'object') { + result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); } + + return result; } } } @@ -117,140 +129,224 @@ class ParamParser { return completedConditions; } - // Checks to see - async buildSearchParam(schema, key, val, operator) { - let schemaObject = schema.obj[key]; - const sanitizedKey = key.replace(/__/gi, '.'); - let localizedKey = this.getLocalizedKey(sanitizedKey, schemaObject); + // Build up an array of auto-localized paths to search on + // Multiple paths may be possible if searching on properties of relationship fields - if (key === '_id' || key === 'id') { - localizedKey = '_id'; - schemaObject = schema.paths._id; + getLocalizedPaths(Model: CollectionModel, incomingPath: string, operator): PathToQuery[] { + const { schema } = Model; + const pathSegments = incomingPath.split('.'); - if (schemaObject.instance === 'ObjectID') { - const isValid = mongoose.Types.ObjectId.isValid(val); - if (!isValid) { - return undefined; - } - } + let paths: PathToQuery[] = [ + { + path: '', + complete: false, + Model, + }, + ]; - if (schemaObject.instance === 'Number') { - const parsedNumber = parseFloat(val); + pathSegments.forEach((segment, i) => { + const lastIncompletePath = paths.find(({ complete }) => !complete); + const { path } = lastIncompletePath; - if (Number.isNaN(parsedNumber)) { - return undefined; - } - } - } + const currentPath = path ? `${path}.${segment}` : segment; + const currentSchemaType = schema.path(currentPath); - if (key.includes('.') || key.includes('__')) { - const paths = key.split('.'); - schemaObject = schema.obj[paths[0]]; - const localizedPath = this.getLocalizedKey(paths[0], schemaObject); - const path = schema.paths[localizedPath]; - // If the schema object has a dot, split on the dot - // Check the path of the first index of the newly split array - // If it's an array OR an ObjectID, we need to recurse - if (path) { - // If the path is an ObjectId with a direct ref, - // Grab it - let { ref } = path.options; - // If the path is an Array, grab the ref of the first index type - if (path.instance === 'Array') { - ref = path.options && path.options.type && path.options.type[0].ref; - } - // ////////////////////////////////////////////////////////////////////////// - // TODO: - // - // Need to handle relationships that have more than one type. - // Right now, this code only handles one ref. But there could be a - // refPath as well, which could allow for a relation to multiple types. - // In that case, we would need to get the allowed referenced models - // and run the subModel query on each - building up a list of $in IDs. - // ////////////////////////////////////////////////////////////////////////// - if (ref) { - const subModel = mongoose.model(ref); - let subQuery = {}; - const localizedSubKey = this.getLocalizedKey(paths[1], subModel.schema.obj[paths[1]]); - const [searchParamKey, searchParamValue] = await this.buildSearchParam(subModel.schema, localizedSubKey, val, operator); - subQuery = addSearchParam(searchParamKey, searchParamValue, subQuery); - const matchingSubDocuments = await subModel.find(subQuery); - return [localizedPath, { - $in: matchingSubDocuments.map((subDoc) => subDoc.id), - }]; - } - } - } - let formattedValue = val; + if (currentSchemaType) { + const currentSchemaTypeOptions = getSchemaTypeOptions(currentSchemaType); - const schemaObjectType = schemaObject?.localized ? schemaObject?.type[this.locale].type : schemaObject?.type; + if (currentSchemaTypeOptions.localized) { + const upcomingSegment = pathSegments[i + 1]; + const upcomingPath = `${currentPath}.${upcomingSegment}`; + const upcomingSchemaType = schema.path(upcomingPath); - if (schemaObject && schemaObjectType === Boolean && typeof val === 'string') { - if (val.toLowerCase() === 'true') formattedValue = true; - if (val.toLowerCase() === 'false') formattedValue = false; - } - - if (schemaObject && schemaObjectType === Number && typeof val === 'string') { - formattedValue = Number(val); - } - - if (schemaObject && schemaObject.ref && val === 'null') { - formattedValue = null; - } - - if (operator && validOperators.includes(operator)) { - switch (operator) { - case 'greater_than_equal': - formattedValue = { $gte: formattedValue }; - break; - case 'less_than_equal': - formattedValue = { $lte: formattedValue }; - break; - case 'less_than': - formattedValue = { $lt: formattedValue }; - break; - case 'greater_than': - formattedValue = { $gt: formattedValue }; - break; - case 'in': - case 'all': - formattedValue = { [`$${operator}`]: convertArrayFromCommaDelineated(formattedValue) }; - break; - case 'not_in': - formattedValue = { $nin: convertArrayFromCommaDelineated(formattedValue) }; - break; - case 'not_equals': - formattedValue = { $ne: formattedValue }; - break; - case 'like': - if (localizedKey !== '_id') { - formattedValue = { $regex: formattedValue, $options: '-i' }; + if (upcomingSchemaType) { + lastIncompletePath.path = currentPath; + return; } - break; - case 'exists': - formattedValue = { $exists: (formattedValue === 'true' || formattedValue === true) }; - break; - case 'near': - // eslint-disable-next-line no-case-declarations - const [x, y, maxDistance, minDistance] = convertArrayFromCommaDelineated(formattedValue); - if (!x || !y || (!maxDistance && !minDistance)) { - formattedValue = undefined; - break; + + const localePath = `${currentPath}.${this.locale}`; + const localizedSchemaType = schema.path(localePath); + + if (localizedSchemaType || operator === 'near') { + lastIncompletePath.path = localePath; + return; } - formattedValue = { - $near: { - $geometry: { type: 'Point', coordinates: [parseFloat(x), parseFloat(y)] }, + } + + lastIncompletePath.path = currentPath; + return; + } + + const priorSchemaType = schema.path(path); + + if (priorSchemaType) { + const priorSchemaTypeOptions = getSchemaTypeOptions(priorSchemaType); + if (typeof priorSchemaTypeOptions.ref === 'string') { + const RefModel = mongoose.model(priorSchemaTypeOptions.ref) as any; + + lastIncompletePath.complete = true; + + const remainingPath = pathSegments.slice(i).join('.'); + + paths = [ + ...paths, + ...this.getLocalizedPaths(RefModel, remainingPath, operator), + ]; + return; + } + } + + if (operator === 'near') { + lastIncompletePath.path = currentPath; + } + }); + + return paths; + } + + // Convert the Payload key / value / operator into a MongoDB query + async buildSearchParam(schema, incomingPath, val, operator): Promise { + // Replace GraphQL nested field double underscore formatting + let sanitizedPath = incomingPath.replace(/__/gi, '.'); + if (sanitizedPath === 'id') sanitizedPath = '_id'; + + const collectionPaths = this.getLocalizedPaths(this.model, sanitizedPath, operator); + const [{ path }] = collectionPaths; + + if (path) { + const schemaType = schema.path(path); + const schemaOptions = getSchemaTypeOptions(schemaType); + const formattedValue = sanitizeQueryValue(schemaType, path, operator, val); + + // If there are multiple collections to search through, + // Recursively build up a list of query constraints + if (collectionPaths.length > 1) { + // Remove top collection and reverse array + // to work backwards from top + const collectionPathsToSearch = collectionPaths.slice(1).reverse(); + + const initialRelationshipQuery = { + value: {}, + } as SearchParam; + + const relationshipQuery = await collectionPathsToSearch.reduce(async (priorQuery, { Model: SubModel, path: subPath }, i) => { + const priorQueryResult = await priorQuery; + + // On the "deepest" collection, + // Search on the value passed through the query + if (i === 0) { + const subQuery = await SubModel.buildQuery({ + where: { + [subPath]: { + [operator]: val, + }, + }, + }, this.locale); + + const result = await SubModel.find(subQuery, subQueryOptions); + + const $in = result.map((doc) => doc._id.toString()); + + if (collectionPathsToSearch.length === 1) return { path, value: { $in } }; + + return { + value: { _id: { $in } }, + }; + } + + const subQuery = priorQueryResult.value; + const result = await SubModel.find(subQuery, subQueryOptions); + + const $in = result.map((doc) => doc._id.toString()); + + // If it is the last recursion + // then pass through the search param + if (i + 1 === collectionPathsToSearch.length) { + return { path, value: { $in } }; + } + + return { + value: { + _id: { $in }, }, }; - if (maxDistance) formattedValue.$near.$maxDistance = parseFloat(maxDistance); - if (minDistance) formattedValue.$near.$minDistance = parseFloat(minDistance); - break; - default: - break; + }, Promise.resolve(initialRelationshipQuery)); + + return relationshipQuery; + } + + if (operator && validOperators.includes(operator)) { + const operatorKey = operatorMap[operator]; + + let overrideQuery = false; + let query; + + + // If there is a ref, this is a relationship or upload field + // IDs can be either string, number, or ObjectID + // So we need to build an `or` query for all these types + if (schemaOptions && (schemaOptions.ref || schemaOptions.refPath)) { + overrideQuery = true; + + query = { + $or: [ + { + [path]: { + [operatorKey]: formattedValue, + }, + }, + ], + }; + + if (typeof formattedValue.toString === 'function') { + query.$or.push({ + [path]: { + [operatorKey]: formattedValue.toString(), + }, + }); + } + + if (typeof formattedValue === 'string') { + query.$or.push({ + [path]: { + [operatorKey]: formattedValue, + }, + }); + + const parsedNumber = parseFloat(formattedValue); + + if (!Number.isNaN(parsedNumber)) { + query.$or.push({ + [path]: { + [operatorKey]: parsedNumber, + }, + }); + } + } + } + + // If forced query + if (overrideQuery) { + return { + value: query, + }; + } + + // Some operators like 'near' need to define a full query + // so if there is no operator key, just return the value + if (!operatorKey) { + return { + value: formattedValue, + }; + } + + return { + path, + value: { [operatorKey]: formattedValue }, + }; } } - - return [localizedKey, formattedValue]; + return undefined; } } // This plugin asynchronously builds a list of Mongoose query constraints diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index 3d966766c8..e8b2288c81 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -1,10 +1,30 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable class-methods-use-this */ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable no-use-before-define */ -import { Schema, SchemaDefinition, SchemaOptions } from 'mongoose'; +import mongoose, { Schema, SchemaDefinition, SchemaOptions } from 'mongoose'; import { SanitizedConfig } from '../config/types'; import { ArrayField, Block, BlockField, CheckboxField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, fieldIsPresentationalOnly, NonPresentationalField } from '../fields/config/types'; import sortableFieldTypes from '../fields/sortableFieldTypes'; +class PayloadID extends mongoose.SchemaType { + constructor(key, options) { + super(key, options, 'PayloadID'); + } + + cast(val) { + const number = Number(val); + if (!Number.isNaN(number)) return number; + + if (mongoose.Types.ObjectId.isValid(val)) return new mongoose.Types.ObjectId(val); + + return val; + } +} + +// @ts-ignore +mongoose.Schema.Types.PayloadID = PayloadID; + type BuildSchemaOptions = { options?: SchemaOptions allowIDField?: boolean @@ -227,7 +247,7 @@ const fieldToSchemaMap = { upload: (field: UploadField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, + type: PayloadID, ref: field.relationTo, }; @@ -248,14 +268,14 @@ const fieldToSchemaMap = { if (hasManyRelations) { localeSchema._id = false; localeSchema.value = { - type: Schema.Types.Mixed, + type: PayloadID, refPath: `${field.name}.${locale}.relationTo`, }; localeSchema.relationTo = { type: String, enum: field.relationTo }; } else { localeSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, + type: PayloadID, ref: field.relationTo, }; } @@ -270,7 +290,7 @@ const fieldToSchemaMap = { } else if (hasManyRelations) { schemaToReturn._id = false; schemaToReturn.value = { - type: Schema.Types.Mixed, + type: PayloadID, refPath: `${field.name}.relationTo`, }; schemaToReturn.relationTo = { type: String, enum: field.relationTo }; @@ -279,7 +299,7 @@ const fieldToSchemaMap = { } else { schemaToReturn = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, + type: PayloadID, ref: field.relationTo, }; diff --git a/src/mongoose/connect.ts b/src/mongoose/connect.ts index 1d1a31c649..15bdd5a924 100644 --- a/src/mongoose/connect.ts +++ b/src/mongoose/connect.ts @@ -1,19 +1,16 @@ -import mongoose, { ConnectionOptions } from 'mongoose'; +import mongoose, { ConnectOptions } from 'mongoose'; import Logger from '../utilities/logger'; import { connection } from './testCredentials'; const logger = Logger(); -const connectMongoose = async (url: string, options: ConnectionOptions, local: boolean): Promise => { +const connectMongoose = async (url: string, options: ConnectOptions, local: boolean): Promise => { let urlToConnect = url; let successfulConnectionMessage = 'Connected to Mongo server successfully!'; const connectionOptions = { ...options, useNewUrlParser: true, - useUnifiedTopology: true, - useCreateIndex: true, autoIndex: true, - useFindAndModify: false, }; if (process.env.NODE_ENV === 'test') { diff --git a/src/mongoose/createArrayFromCommaDelineated.ts b/src/mongoose/createArrayFromCommaDelineated.ts new file mode 100644 index 0000000000..90949cf754 --- /dev/null +++ b/src/mongoose/createArrayFromCommaDelineated.ts @@ -0,0 +1,7 @@ +export function createArrayFromCommaDelineated(input: string): string[] { + if (Array.isArray(input)) return input; + if (input.indexOf(',') > -1) { + return input.split(','); + } + return [input]; +} diff --git a/src/mongoose/getSchemaTypeOptions.ts b/src/mongoose/getSchemaTypeOptions.ts new file mode 100644 index 0000000000..07723a6e30 --- /dev/null +++ b/src/mongoose/getSchemaTypeOptions.ts @@ -0,0 +1,9 @@ +import { SchemaType, SchemaTypeOptions } from 'mongoose'; + +export const getSchemaTypeOptions = (schemaType: SchemaType): SchemaTypeOptions<{ localized: boolean }> => { + if (schemaType?.instance === 'Array') { + return schemaType.options.type[0]; + } + + return schemaType?.options; +}; diff --git a/src/mongoose/operatorMap.ts b/src/mongoose/operatorMap.ts new file mode 100644 index 0000000000..e5282461b1 --- /dev/null +++ b/src/mongoose/operatorMap.ts @@ -0,0 +1,13 @@ +export const operatorMap = { + greater_than_equal: '$gte', + less_than_equal: '$lte', + less_than: '$lt', + greater_than: '$gt', + in: '$in', + all: '$all', + not_in: '$nin', + not_equals: '$ne', + exists: '$exists', + equals: '$eq', + near: '$near', +}; diff --git a/src/mongoose/sanitizeFormattedValue.ts b/src/mongoose/sanitizeFormattedValue.ts new file mode 100644 index 0000000000..9c1ffba315 --- /dev/null +++ b/src/mongoose/sanitizeFormattedValue.ts @@ -0,0 +1,109 @@ +import mongoose, { SchemaType } from 'mongoose'; +import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated'; +import { getSchemaTypeOptions } from './getSchemaTypeOptions'; + +export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operator: string, val: any): unknown => { + let formattedValue = val; + const schemaOptions = getSchemaTypeOptions(schemaType); + + // Disregard invalid _ids + + if (path === '_id' && typeof val === 'string') { + if (schemaType?.instance === 'ObjectID') { + const isValid = mongoose.Types.ObjectId.isValid(val); + + if (!isValid) { + return undefined; + } + } + + if (schemaType?.instance === 'Number') { + const parsedNumber = parseFloat(val); + + if (Number.isNaN(parsedNumber)) { + return undefined; + } + } + } + + // Cast incoming values as proper searchable types + + if (schemaType?.instance === 'Boolean' && typeof val === 'string') { + if (val.toLowerCase() === 'true') formattedValue = true; + if (val.toLowerCase() === 'false') formattedValue = false; + } + + if (schemaType?.instance === 'Number' && typeof val === 'string') { + formattedValue = Number(val); + } + + if ((schemaOptions?.ref || schemaOptions?.refPath) && val === 'null') { + formattedValue = null; + } + + // Set up specific formatting necessary by operators + + if (operator === 'near') { + let x; + let y; + let maxDistance; + let minDistance; + + if (Array.isArray(formattedValue)) { + [x, y, maxDistance, minDistance] = formattedValue; + } + + if (typeof formattedValue === 'string') { + [x, y, maxDistance, minDistance] = createArrayFromCommaDelineated(formattedValue); + } + + if (!x || !y || (!maxDistance && !minDistance)) { + formattedValue = undefined; + } else { + formattedValue = { + $geometry: { type: 'Point', coordinates: [parseFloat(x), parseFloat(y)] }, + }; + + if (maxDistance) formattedValue.$maxDistance = parseFloat(maxDistance); + if (minDistance) formattedValue.$minDistance = parseFloat(minDistance); + } + } + + if (['all', 'not_in'].includes(operator) && typeof formattedValue === 'string') { + formattedValue = createArrayFromCommaDelineated(formattedValue); + } + + if (schemaOptions && (schemaOptions.ref || schemaOptions.refPath)) { + if (operator === 'in') { + if (typeof formattedValue === 'string') formattedValue = createArrayFromCommaDelineated(formattedValue); + if (Array.isArray(formattedValue)) { + formattedValue = formattedValue.reduce((formattedValues, inVal) => { + const newValues = [inVal]; + if (mongoose.Types.ObjectId.isValid(inVal)) newValues.push(new mongoose.Types.ObjectId(inVal)); + + const parsedNumber = parseFloat(inVal); + if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber); + + return [ + ...formattedValues, + ...newValues, + ]; + }, []); + } + } + + if (typeof formattedValue === 'string' && mongoose.Types.ObjectId.isValid(formattedValue)) { + formattedValue = new mongoose.Types.ObjectId(formattedValue); + } + } + + if (operator === 'like' && path !== '_id') { + formattedValue = { $regex: formattedValue, $options: '-i' }; + } + + if (operator === 'exists') { + formattedValue = (formattedValue === 'true' || formattedValue === true); + } + + return formattedValue; +}; diff --git a/src/utilities/combineMerge.ts b/src/utilities/combineMerge.ts index 1075e1525e..ffdbd1c728 100644 --- a/src/utilities/combineMerge.ts +++ b/src/utilities/combineMerge.ts @@ -1,6 +1,6 @@ import merge from 'deepmerge'; -const combineMerge = (target, source, options) => { +export const combineMerge = (target, source, options) => { const destination = target.slice(); source.forEach((item, index) => { @@ -14,5 +14,3 @@ const combineMerge = (target, source, options) => { }); return destination; }; - -export default combineMerge; diff --git a/yarn.lock b/yarn.lock index c525089132..8a290dc178 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8603,6 +8603,17 @@ mongodb@4.1.2: optionalDependencies: saslprep "^1.0.3" +mongodb@4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz#8bf24d782ba3f3833201f4e60b0307d87980ba71" + integrity sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q== + dependencies: + bson "^4.5.2" + denque "^2.0.1" + mongodb-connection-string-url "^2.0.0" + optionalDependencies: + saslprep "^1.0.3" + mongoose-legacy-pluralize@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" @@ -8613,7 +8624,7 @@ mongoose-paginate-v2@^1.3.6: resolved "https://registry.npmjs.org/mongoose-paginate-v2/-/mongoose-paginate-v2-1.4.2.tgz#482cea73f4a0bd49131ef9004694c73cae93b73e" integrity sha512-b7FBe7fasJazhlTIfMSPyIST04yvobASXKmUt3ducnvYThulCyGSPhPcSYOzXcPmqBR4tpMnafIJxv+8GjM+UA== -mongoose@5.*, mongoose@^5.8.9: +mongoose@5.*: version "5.13.11" resolved "https://registry.npmjs.org/mongoose/-/mongoose-5.13.11.tgz#4f6fc4959310d0c31606fdec19b6f25f8ebbb50a" integrity sha512-hVHm864eBpaCr0W6CF7qTmC62dmepnT+A6ZnqKPU9asw7jjYHBcBxPgCnrGBMUACbZtzidO6HmB4J3F1YU2rdg== @@ -8633,6 +8644,21 @@ mongoose@5.*, mongoose@^5.8.9: sift "13.5.2" sliced "1.0.1" +mongoose@^6.0.12: + version "6.0.12" + resolved "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz#a22727d52ca9e9ce3996330b895afe5cde75af3c" + integrity sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw== + dependencies: + bson "^4.2.2" + kareem "2.3.2" + mongodb "4.1.3" + mpath "0.8.4" + mquery "4.0.0" + ms "2.1.2" + regexp-clone "1.0.0" + sift "13.5.2" + sliced "1.0.1" + mongoose@^6.0.8: version "6.0.10" resolved "https://registry.npmjs.org/mongoose/-/mongoose-6.0.10.tgz#c22ac914afccf648778edfe11954f4aa0425a105" From 6b149843522a6221b452a689cf6f09cc98191056 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 Oct 2021 13:08:42 -0400 Subject: [PATCH 05/25] chore: removes non-functional buildQuery code --- src/mongoose/buildQuery.ts | 14 +----------- src/mongoose/buildSchema.ts | 30 ++++++-------------------- src/mongoose/sanitizeFormattedValue.ts | 4 ---- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 908a015037..a9704f10a6 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -289,13 +289,7 @@ class ParamParser { overrideQuery = true; query = { - $or: [ - { - [path]: { - [operatorKey]: formattedValue, - }, - }, - ], + $or: [], }; if (typeof formattedValue.toString === 'function') { @@ -307,12 +301,6 @@ class ParamParser { } if (typeof formattedValue === 'string') { - query.$or.push({ - [path]: { - [operatorKey]: formattedValue, - }, - }); - const parsedNumber = parseFloat(formattedValue); if (!Number.isNaN(parsedNumber)) { diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index e8b2288c81..e1b0af2428 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -2,29 +2,11 @@ /* eslint-disable class-methods-use-this */ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable no-use-before-define */ -import mongoose, { Schema, SchemaDefinition, SchemaOptions } from 'mongoose'; +import { Schema, SchemaDefinition, SchemaOptions } from 'mongoose'; import { SanitizedConfig } from '../config/types'; import { ArrayField, Block, BlockField, CheckboxField, CodeField, DateField, EmailField, Field, fieldAffectsData, GroupField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TextareaField, TextField, UploadField, fieldIsPresentationalOnly, NonPresentationalField } from '../fields/config/types'; import sortableFieldTypes from '../fields/sortableFieldTypes'; -class PayloadID extends mongoose.SchemaType { - constructor(key, options) { - super(key, options, 'PayloadID'); - } - - cast(val) { - const number = Number(val); - if (!Number.isNaN(number)) return number; - - if (mongoose.Types.ObjectId.isValid(val)) return new mongoose.Types.ObjectId(val); - - return val; - } -} - -// @ts-ignore -mongoose.Schema.Types.PayloadID = PayloadID; - type BuildSchemaOptions = { options?: SchemaOptions allowIDField?: boolean @@ -247,7 +229,7 @@ const fieldToSchemaMap = { upload: (field: UploadField, fields: SchemaDefinition, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): SchemaDefinition => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: PayloadID, + type: Schema.Types.Mixed, ref: field.relationTo, }; @@ -268,14 +250,14 @@ const fieldToSchemaMap = { if (hasManyRelations) { localeSchema._id = false; localeSchema.value = { - type: PayloadID, + type: Schema.Types.Mixed, refPath: `${field.name}.${locale}.relationTo`, }; localeSchema.relationTo = { type: String, enum: field.relationTo }; } else { localeSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: PayloadID, + type: Schema.Types.Mixed, ref: field.relationTo, }; } @@ -290,7 +272,7 @@ const fieldToSchemaMap = { } else if (hasManyRelations) { schemaToReturn._id = false; schemaToReturn.value = { - type: PayloadID, + type: Schema.Types.Mixed, refPath: `${field.name}.relationTo`, }; schemaToReturn.relationTo = { type: String, enum: field.relationTo }; @@ -299,7 +281,7 @@ const fieldToSchemaMap = { } else { schemaToReturn = { ...formatBaseSchema(field, buildSchemaOptions), - type: PayloadID, + type: Schema.Types.Mixed, ref: field.relationTo, }; diff --git a/src/mongoose/sanitizeFormattedValue.ts b/src/mongoose/sanitizeFormattedValue.ts index 9c1ffba315..034e15d710 100644 --- a/src/mongoose/sanitizeFormattedValue.ts +++ b/src/mongoose/sanitizeFormattedValue.ts @@ -91,10 +91,6 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato }, []); } } - - if (typeof formattedValue === 'string' && mongoose.Types.ObjectId.isValid(formattedValue)) { - formattedValue = new mongoose.Types.ObjectId(formattedValue); - } } if (operator === 'like' && path !== '_id') { From 57c0346a00286a3df695ea46e5c2630494183b5b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 14:34:46 -0400 Subject: [PATCH 06/25] fix: ensures relationship field search can return more than 10 options --- .../forms/field-types/Relationship/index.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index ce02b26486..fcedde1c73 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -85,13 +85,12 @@ const Relationship: React.FC = (props) => { lastFullyLoadedRelation: lastFullyLoadedRelationArg, lastLoadedPage: lastLoadedPageArg, search: searchArg, - } = { - lastFullyLoadedRelation: -1, - lastLoadedPage: 1, - search: '', }) => { + let lastLoadedPageToUse = typeof lastLoadedPageArg !== 'undefined' ? lastLoadedPageArg : 1; + const lastFullyLoadedRelationToUse = typeof lastFullyLoadedRelationArg !== 'undefined' ? lastFullyLoadedRelationArg : -1; + const relations = Array.isArray(relationTo) ? relationTo : [relationTo]; - const relationsToFetch = lastFullyLoadedRelationArg === -1 ? relations : relations.slice(lastFullyLoadedRelationArg); + const relationsToFetch = lastFullyLoadedRelationToUse === -1 ? relations : relations.slice(lastFullyLoadedRelationToUse + 1); let resultsFetched = 0; @@ -104,7 +103,7 @@ const Relationship: React.FC = (props) => { const fieldToSearch = collection?.admin?.useAsTitle || 'id'; const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : ''; - const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageArg}&depth=0${searchParam}`); + const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`); if (response.ok) { const data: PaginatedDocs = await response.json(); @@ -115,6 +114,12 @@ const Relationship: React.FC = (props) => { if (!data.nextPage) { setLastFullyLoadedRelation(relations.indexOf(relation)); + + // If there are more relations to search, need to reset lastLoadedPage to 1 + // both locally within function and state + if (relations.indexOf(relation) + 1 < relations.length) { + lastLoadedPageToUse = 1; + } } } } else { @@ -303,7 +308,7 @@ const Relationship: React.FC = (props) => { } } : undefined} onMenuScrollToBottom={() => { - getResults({ lastFullyLoadedRelation: lastFullyLoadedRelation + 1, lastLoadedPage }); + getResults({ lastFullyLoadedRelation, lastLoadedPage: lastLoadedPage + 1 }); }} value={valueToRender} showError={showError} From 40b33d9f5e99285cb0de148dbe059259817fcad8 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 17:07:05 -0400 Subject: [PATCH 07/25] fix: bug with relationship cell when no doc is available --- .../collections/List/Cell/field-types/Relationship/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/admin/components/views/collections/List/Cell/field-types/Relationship/index.tsx b/src/admin/components/views/collections/List/Cell/field-types/Relationship/index.tsx index 750caf5394..5e581217d5 100644 --- a/src/admin/components/views/collections/List/Cell/field-types/Relationship/index.tsx +++ b/src/admin/components/views/collections/List/Cell/field-types/Relationship/index.tsx @@ -31,7 +31,7 @@ const RelationshipCell = (props) => { const doc = hasManyRelations ? cellData.value : cellData; const collection = collections.find((coll) => coll.slug === relation); - if (collection) { + if (collection && doc) { const useAsTitle = collection.admin.useAsTitle ? collection.admin.useAsTitle : 'id'; setData(doc[useAsTitle]); From 37b21b07628e892e85c2cf979d9e2c8af0d291f7 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 17:07:42 -0400 Subject: [PATCH 08/25] fix: ensures tquerying by relationship subpaths works --- src/mongoose/buildQuery.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index a9704f10a6..af28e4d757 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ import deepmerge from 'deepmerge'; -import mongoose, { FilterQuery } from 'mongoose'; +import mongoose, { FilterQuery, SchemaType } from 'mongoose'; import { combineMerge } from '../utilities/combineMerge'; import { CollectionModel } from '../collections/config/types'; import { getSchemaTypeOptions } from './getSchemaTypeOptions'; @@ -97,12 +97,12 @@ class ParamParser { if (validOperators.includes(operator)) { const searchParam = await this.buildSearchParam(this.model.schema, relationOrPath, pathOperators[operator], operator); - if ('path' in searchParam) { + if (searchParam && 'path' in searchParam) { result = { ...result, [searchParam.path]: searchParam.value, }; - } else if (typeof searchParam.value === 'object') { + } else if (typeof searchParam?.value === 'object') { result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); } @@ -149,9 +149,12 @@ class ParamParser { const { path } = lastIncompletePath; const currentPath = path ? `${path}.${segment}` : segment; - const currentSchemaType = schema.path(currentPath); + const currentSchemaType: SchemaType & { path: string } = schema.path(currentPath); - if (currentSchemaType) { + // If we find a schema type, and it matches the exact current path + // NOTE - not a sub-path. Some schema types like `mixed` will return anything + // nested within. Need to make sure that schema type path matches exactly + if (currentSchemaType && (currentSchemaType.path === currentPath || currentSchemaType.instance === 'Embedded')) { const currentSchemaTypeOptions = getSchemaTypeOptions(currentSchemaType); if (currentSchemaTypeOptions.localized) { @@ -281,7 +284,6 @@ class ParamParser { let overrideQuery = false; let query; - // If there is a ref, this is a relationship or upload field // IDs can be either string, number, or ObjectID // So we need to build an `or` query for all these types From 0eceb8d76c7ebb531fdbec088eb87ba55b589c01 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 17:09:02 -0400 Subject: [PATCH 09/25] chore: improves demo collections for testing --- demo/collections/RelationshipA.ts | 1 - demo/collections/RelationshipB.ts | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/demo/collections/RelationshipA.ts b/demo/collections/RelationshipA.ts index 8cd3da09b6..4c288fbb11 100644 --- a/demo/collections/RelationshipA.ts +++ b/demo/collections/RelationshipA.ts @@ -15,7 +15,6 @@ const RelationshipA: CollectionConfig = { label: 'Post', type: 'relationship', relationTo: 'relationship-b', - localized: true, }, { name: 'LocalizedPost', diff --git a/demo/collections/RelationshipB.ts b/demo/collections/RelationshipB.ts index 5adab1b05d..bb8ab298e4 100644 --- a/demo/collections/RelationshipB.ts +++ b/demo/collections/RelationshipB.ts @@ -5,11 +5,18 @@ const RelationshipB: CollectionConfig = { access: { read: () => true, }, + admin: { + useAsTitle: 'title', + }, labels: { singular: 'Relationship B', plural: 'Relationship B', }, fields: [ + { + name: 'title', + type: 'text', + }, { name: 'post', label: 'Post', From 94c2b8d80b046c067057d4ad089ed6a2edd656cf Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 17:10:11 -0400 Subject: [PATCH 10/25] fix: #351 --- src/auth/operations/logout.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/auth/operations/logout.ts b/src/auth/operations/logout.ts index 838d551055..bffeaf8715 100644 --- a/src/auth/operations/logout.ts +++ b/src/auth/operations/logout.ts @@ -28,8 +28,11 @@ async function logout(args: Arguments): Promise { httpOnly: true, secure: collectionConfig.auth.cookies.secure, sameSite: collectionConfig.auth.cookies.sameSite, + domain: undefined, }; + if (collectionConfig.auth.cookies.domain) cookieOptions.domain = collectionConfig.auth.cookies.domain; + res.clearCookie(`${config.cookiePrefix}-token`, cookieOptions); return 'Logged out successfully.'; From 056f078615b922a4d7a78af3143e769713f49886 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 17:12:08 -0400 Subject: [PATCH 11/25] chore: release beta --- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7722ad0ed..bab1b60ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [0.12.5-beta.0](https://github.com/payloadcms/payload/compare/v0.12.3...v0.12.5-beta.0) (2021-11-01) + + +### Bug Fixes + +* [#351](https://github.com/payloadcms/payload/issues/351) ([94c2b8d](https://github.com/payloadcms/payload/commit/94c2b8d80b046c067057d4ad089ed6a2edd656cf)) +* bug with relationship cell when no doc is available ([40b33d9](https://github.com/payloadcms/payload/commit/40b33d9f5e99285cb0de148dbe059259817fcad8)) +* ensures relationship field search can return more than 10 options ([57c0346](https://github.com/payloadcms/payload/commit/57c0346a00286a3df695ea46e5c2630494183b5b)) +* ensures querying by relationship subpaths works ([37b21b0](https://github.com/payloadcms/payload/commit/37b21b07628e892e85c2cf979d9e2c8af0d291f7)) + + +### Features + +* improves querying logic ([4c85747](https://github.com/payloadcms/payload/commit/4c8574784995b1cb1f939648f4d2158286089b3d)) + + ## [0.12.3](https://github.com/payloadcms/payload/compare/v0.12.2...v0.12.3) (2021-10-23) diff --git a/package.json b/package.json index 3a1efddc51..075d322a28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "0.12.3", + "version": "0.12.5-beta.0", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "SEE LICENSE IN license.md", "author": { From 20d4e72a951dfcbf1cc301d0938e5095932436b9 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 19:05:09 -0400 Subject: [PATCH 12/25] fix: ensures 'like' query param remains functional in all cases --- src/mongoose/buildQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index af28e4d757..5df43b78bc 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -326,6 +326,7 @@ class ParamParser { // so if there is no operator key, just return the value if (!operatorKey) { return { + path, value: formattedValue, }; } From abf61d0734c09fd0fc5c5b827cb0631e11701f71 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 19:41:22 -0400 Subject: [PATCH 13/25] fix: ensures richtext links retain proper formatting --- .../forms/field-types/RichText/elements/link/index.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/admin/components/forms/field-types/RichText/elements/link/index.scss b/src/admin/components/forms/field-types/RichText/elements/link/index.scss index efd6b1f577..e4df9a4683 100644 --- a/src/admin/components/forms/field-types/RichText/elements/link/index.scss +++ b/src/admin/components/forms/field-types/RichText/elements/link/index.scss @@ -23,6 +23,11 @@ .rich-text-link__button { @extend %btn-reset; + font-size: inherit; + font-weight: inherit; + color: inherit; + letter-spacing: inherit; + line-height: inherit; position: relative; z-index: 2; text-decoration: underline; From bee18a5e99da83f0ae95635b42d2d1e9cd8f025c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 22:30:16 -0400 Subject: [PATCH 14/25] chore: adds buildQuery testing --- src/mongoose/buildQuery.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 5df43b78bc..81afd02765 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ import deepmerge from 'deepmerge'; -import mongoose, { FilterQuery, SchemaType } from 'mongoose'; +import mongoose, { FilterQuery } from 'mongoose'; import { combineMerge } from '../utilities/combineMerge'; import { CollectionModel } from '../collections/config/types'; import { getSchemaTypeOptions } from './getSchemaTypeOptions'; @@ -97,7 +97,7 @@ class ParamParser { if (validOperators.includes(operator)) { const searchParam = await this.buildSearchParam(this.model.schema, relationOrPath, pathOperators[operator], operator); - if (searchParam && 'path' in searchParam) { + if (searchParam?.value && searchParam?.path) { result = { ...result, [searchParam.path]: searchParam.value, @@ -149,12 +149,10 @@ class ParamParser { const { path } = lastIncompletePath; const currentPath = path ? `${path}.${segment}` : segment; - const currentSchemaType: SchemaType & { path: string } = schema.path(currentPath); + const currentSchemaType = schema.path(currentPath); + const currentSchemaPathType = schema.pathType(currentPath); - // If we find a schema type, and it matches the exact current path - // NOTE - not a sub-path. Some schema types like `mixed` will return anything - // nested within. Need to make sure that schema type path matches exactly - if (currentSchemaType && (currentSchemaType.path === currentPath || currentSchemaType.instance === 'Embedded')) { + if (currentSchemaType && currentSchemaPathType !== 'adhocOrUndefined') { const currentSchemaTypeOptions = getSchemaTypeOptions(currentSchemaType); if (currentSchemaTypeOptions.localized) { @@ -291,10 +289,16 @@ class ParamParser { overrideQuery = true; query = { - $or: [], + $or: [ + { + [path]: { + [operatorKey]: formattedValue, + }, + }, + ], }; - if (typeof formattedValue.toString === 'function') { + if (typeof formattedValue === 'number' || (typeof formattedValue === 'string' && mongoose.Types.ObjectId.isValid(formattedValue))) { query.$or.push({ [path]: { [operatorKey]: formattedValue.toString(), From 733716934224bb785aecd927318ff0eb64cb6cb2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 22:30:35 -0400 Subject: [PATCH 15/25] chore: adds query testing --- src/collections/tests/collections.spec.js | 100 +++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/collections/tests/collections.spec.js b/src/collections/tests/collections.spec.js index 47da21d231..5caa239b9a 100644 --- a/src/collections/tests/collections.spec.js +++ b/src/collections/tests/collections.spec.js @@ -10,6 +10,7 @@ let token = null; let headers = null; let localizedPostID; +const localizedPostTitle = 'title'; const englishPostDesc = 'english description'; const spanishPostDesc = 'spanish description'; @@ -44,7 +45,7 @@ describe('Collections - REST', () => { beforeAll(async () => { response = await fetch(`${url}/api/localized-posts`, { body: JSON.stringify({ - title: 'title', + title: localizedPostTitle, description: englishPostDesc, priority: 1, nonLocalizedGroup: { @@ -370,6 +371,103 @@ describe('Collections - REST', () => { expect(data.docs).toHaveLength(1); expect(data.docs[0].title).toBe(title1); }); + + it('should allow querying by a non-localized nested relationship property', async () => { + const relationshipBTitle = 'test'; + const relationshipBRes = await fetch(`${url}/api/relationship-b?depth=0`, { + body: JSON.stringify({ + title: relationshipBTitle, + }), + headers, + method: 'post', + }); + + const relationshipBData = await relationshipBRes.json(); + + const res = await fetch(`${url}/api/relationship-a?depth=0`, { + body: JSON.stringify({ + post: relationshipBData.doc.id, + }), + headers, + method: 'post', + }); + + const additionalRelationshipARes = await fetch(`${url}/api/relationship-a?depth=0`, { + body: JSON.stringify({ + postLocalizedMultiple: [{ + relationTo: 'localized-posts', + value: localizedPostID, + }], + }), + headers, + method: 'post', + }); + + const relationshipAData = await res.json(); + + expect(res.status).toBe(201); + expect(additionalRelationshipARes.status).toBe(201); + expect(relationshipAData.doc.post).toBe(relationshipBData.doc.id); + + const queryRes = await fetch(`${url}/api/relationship-a?where[post.title][equals]=${relationshipBTitle}`); + const data = await queryRes.json(); + + expect(data.docs).toHaveLength(1); + }); + + it('should allow querying by a localized nested relationship property', async () => { + const res = await fetch(`${url}/api/relationship-a`, { + body: JSON.stringify({ + LocalizedPost: [localizedPostID], + }), + headers, + method: 'post', + }); + + expect(res.status).toBe(201); + + const queryRes1 = await fetch(`${url}/api/relationship-a?where[LocalizedPost.title][in]=${localizedPostTitle}`); + const data1 = await queryRes1.json(); + + expect(data1.docs).toHaveLength(1); + + const queryRes2 = await fetch(`${url}/api/relationship-a?where[LocalizedPost.en.title][in]=${localizedPostTitle}`); + const data2 = await queryRes2.json(); + + expect(queryRes2.status).toBe(200); + expect(data2.docs).toHaveLength(1); + }); + + it('should allow querying by a field within a group', async () => { + const text = 'laiwjefliajwe'; + + await fetch(`${url}/api/localized-posts`, { + body: JSON.stringify({ + title: 'super great title', + description: 'desc', + priority: 1, + nonLocalizedGroup: { + text, + }, + localizedGroup: { + text, + }, + }), + headers, + method: 'post', + }); + + const queryRes1 = await fetch(`${url}/api/localized-posts?where[nonLocalizedGroup.text][equals]=${text}`); + const data1 = await queryRes1.json(); + + expect(data1.docs).toHaveLength(1); + + const queryRes2 = await fetch(`${url}/api/localized-posts?where[localizedGroup.text][equals]=${text}`); + const data2 = await queryRes2.json(); + + expect(queryRes2.status).toBe(200); + expect(data2.docs).toHaveLength(1); + }); }); describe('Delete', () => { From 8987ce1f69717be9847dc9b5933fb1c27ccdb4e8 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 22:31:32 -0400 Subject: [PATCH 16/25] chore: scaffolds relationship filter --- .../Condition/Relationship/index.scss | 5 +++++ .../Condition/Relationship/index.tsx | 22 +++++++++++++++++++ .../Condition/Relationship/types.ts | 4 ++++ .../elements/WhereBuilder/Condition/index.tsx | 3 +++ .../elements/WhereBuilder/field-types.tsx | 2 +- 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/admin/components/elements/WhereBuilder/Condition/Relationship/index.scss create mode 100644 src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx create mode 100644 src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.scss b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.scss new file mode 100644 index 0000000000..a8e4cbf5f7 --- /dev/null +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.scss @@ -0,0 +1,5 @@ +@import '../../../../../scss/styles.scss'; + +.condition-value-relationship { + @include formInput; +} diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx new file mode 100644 index 0000000000..c4a6e3b50b --- /dev/null +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Props } from './types'; + +import './index.scss'; + +const baseClass = 'condition-value-relationship'; + +const RelationshipField: React.FC = (props) => { + const { onChange, value } = props; + console.log(props); + return ( + onChange(e.target.value)} + value={value} + /> + ); +}; + +export default RelationshipField; diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts new file mode 100644 index 0000000000..b2abc4c5cd --- /dev/null +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts @@ -0,0 +1,4 @@ +export type Props = { + onChange: (val: string) => void, + value: string, +} diff --git a/src/admin/components/elements/WhereBuilder/Condition/index.tsx b/src/admin/components/elements/WhereBuilder/Condition/index.tsx index 4cb96043b5..8763f979ca 100644 --- a/src/admin/components/elements/WhereBuilder/Condition/index.tsx +++ b/src/admin/components/elements/WhereBuilder/Condition/index.tsx @@ -6,6 +6,7 @@ import Button from '../../Button'; import Date from './Date'; import Number from './Number'; import Text from './Text'; +import Relationship from './Relationship'; import useDebounce from '../../../../hooks/useDebounce'; import { FieldCondition } from '../types'; @@ -15,6 +16,7 @@ const valueFields = { Date, Number, Text, + Relationship, }; const baseClass = 'condition'; @@ -93,6 +95,7 @@ const Condition: React.FC = (props) => { DefaultComponent={ValueComponent} componentProps={{ ...activeField?.props, + operator: operatorValue, value: internalValue, onChange: setInternalValue, }} diff --git a/src/admin/components/elements/WhereBuilder/field-types.tsx b/src/admin/components/elements/WhereBuilder/field-types.tsx index 6839f7829e..92c6b6c52f 100644 --- a/src/admin/components/elements/WhereBuilder/field-types.tsx +++ b/src/admin/components/elements/WhereBuilder/field-types.tsx @@ -100,7 +100,7 @@ const fieldTypeConditions = { operators: [...base], }, relationship: { - component: 'Text', + component: 'Relationship', operators: [...base], }, select: { From e06df905c51360a2fafd1e805ce74d0f8fd61621 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 22:33:31 -0400 Subject: [PATCH 17/25] chore: beta release --- CHANGELOG.md | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab1b60ad3..18f2112f88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ -## [0.12.5-beta.0](https://github.com/payloadcms/payload/compare/v0.12.3...v0.12.5-beta.0) (2021-11-01) +## [0.12.6-beta.0](https://github.com/payloadcms/payload/compare/v0.12.3...v0.12.6-beta.0) (2021-11-02) ### Bug Fixes * [#351](https://github.com/payloadcms/payload/issues/351) ([94c2b8d](https://github.com/payloadcms/payload/commit/94c2b8d80b046c067057d4ad089ed6a2edd656cf)) * bug with relationship cell when no doc is available ([40b33d9](https://github.com/payloadcms/payload/commit/40b33d9f5e99285cb0de148dbe059259817fcad8)) +* ensures 'like' query param remains functional in all cases ([20d4e72](https://github.com/payloadcms/payload/commit/20d4e72a951dfcbf1cc301d0938e5095932436b9)) * ensures relationship field search can return more than 10 options ([57c0346](https://github.com/payloadcms/payload/commit/57c0346a00286a3df695ea46e5c2630494183b5b)) -* ensures querying by relationship subpaths works ([37b21b0](https://github.com/payloadcms/payload/commit/37b21b07628e892e85c2cf979d9e2c8af0d291f7)) +* ensures richtext links retain proper formatting ([abf61d0](https://github.com/payloadcms/payload/commit/abf61d0734c09fd0fc5c5b827cb0631e11701f71)) ### Features diff --git a/package.json b/package.json index 075d322a28..245c163279 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "0.12.5-beta.0", + "version": "0.12.6-beta.0", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "SEE LICENSE IN license.md", "author": { From 463c4e60de8e647fca6268b826d826f9c6e45412 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 23:11:03 -0400 Subject: [PATCH 18/25] feat: adds relationship filter field --- .../components/elements/ReactSelect/index.tsx | 2 + .../components/elements/ReactSelect/types.ts | 1 + .../Condition/Relationship/index.scss | 8 +- .../Condition/Relationship/index.tsx | 267 +++++++++++++++++- .../Condition/Relationship/optionsReducer.ts | 92 ++++++ .../Condition/Relationship/types.ts | 34 ++- 6 files changed, 390 insertions(+), 14 deletions(-) create mode 100644 src/admin/components/elements/WhereBuilder/Condition/Relationship/optionsReducer.ts diff --git a/src/admin/components/elements/ReactSelect/index.tsx b/src/admin/components/elements/ReactSelect/index.tsx index b872c354b0..92c0de3e87 100644 --- a/src/admin/components/elements/ReactSelect/index.tsx +++ b/src/admin/components/elements/ReactSelect/index.tsx @@ -13,6 +13,7 @@ const ReactSelect: React.FC = (props) => { onChange, value, disabled = false, + placeholder, } = props; const classes = [ @@ -23,6 +24,7 @@ const ReactSelect: React.FC = (props) => { return ( onChange(e.target.value)} - value={value} - /> +
+ {!errorLoading && ( + { + if (hasMany) { + onChange(selected ? selected.map((option) => { + if (hasMultipleRelations) { + return { + relationTo: option.relationTo, + value: option.value, + }; + } + + return option.value; + }) : null); + } else if (hasMultipleRelations) { + onChange({ + relationTo: selected.relationTo, + value: selected.value, + }); + } else { + onChange(selected.value); + } + }} + onMenuScrollToBottom={() => { + getResults({ lastFullyLoadedRelation, lastLoadedPage: lastLoadedPage + 1 }); + }} + value={valueToRender} + options={options} + isMulti={hasMany} + /> + )} + {errorLoading && ( +
+ {errorLoading} +
+ )} +
); }; diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/optionsReducer.ts b/src/admin/components/elements/WhereBuilder/Condition/Relationship/optionsReducer.ts new file mode 100644 index 0000000000..8ea2dac561 --- /dev/null +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/optionsReducer.ts @@ -0,0 +1,92 @@ +import { Option, Action } from './types'; + +const reduceToIDs = (options) => options.reduce((ids, option) => { + if (option.options) { + return [ + ...ids, + ...reduceToIDs(option.options), + ]; + } + + return [ + ...ids, + option.id, + ]; +}, []); + +const optionsReducer = (state: Option[], action: Action): Option[] => { + switch (action.type) { + case 'CLEAR': { + return action.required ? [] : [{ value: 'null', label: 'None' }]; + } + + case 'ADD': { + const { hasMultipleRelations, collection, relation, data } = action; + + const labelKey = collection.admin.useAsTitle || 'id'; + + const loadedIDs = reduceToIDs(state); + + if (!hasMultipleRelations) { + return [ + ...state, + ...data.docs.reduce((docs, doc) => { + if (loadedIDs.indexOf(doc.id) === -1) { + loadedIDs.push(doc.id); + return [ + ...docs, + { + label: doc[labelKey], + value: doc.id, + }, + ]; + } + return docs; + }, []), + ]; + } + + const newOptions = [...state]; + const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural); + + const newSubOptions = data.docs.reduce((docs, doc) => { + if (loadedIDs.indexOf(doc.id) === -1) { + loadedIDs.push(doc.id); + + return [ + ...docs, + { + label: doc[labelKey], + relationTo: relation, + value: doc.id, + }, + ]; + } + + return docs; + }, []); + + if (optionsToAddTo) { + optionsToAddTo.options = [ + ...optionsToAddTo.options, + ...newSubOptions, + ]; + } else { + newOptions.push({ + label: collection.labels.plural, + options: newSubOptions, + value: undefined, + }); + } + + return newOptions; + } + + + default: { + return state; + } + } +}; + +export default optionsReducer; diff --git a/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts index b2abc4c5cd..6a3a46e069 100644 --- a/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts +++ b/src/admin/components/elements/WhereBuilder/Condition/Relationship/types.ts @@ -1,4 +1,34 @@ +import { RelationshipField } from '../../../../../../fields/config/types'; +import { PaginatedDocs, SanitizedCollectionConfig } from '../../../../../../collections/config/types'; + export type Props = { - onChange: (val: string) => void, - value: string, + onChange: (val: unknown) => void, + value: unknown, +} & RelationshipField + +export type Option = { + label: string + value: string + relationTo?: string + options?: Option[] +} + +type CLEAR = { + type: 'CLEAR' + required: boolean +} + +type ADD = { + type: 'ADD' + data: PaginatedDocs + relation: string + hasMultipleRelations: boolean + collection: SanitizedCollectionConfig +} + +export type Action = CLEAR | ADD + +export type ValueWithRelation = { + relationTo: string + value: string } From 72fc413764c6c42ba64a45f01d99b68ad3bd46c4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Nov 2021 23:11:54 -0400 Subject: [PATCH 19/25] fix: ensures buildQuery works with fields as well as simultaneous or / and --- src/mongoose/buildQuery.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 81afd02765..e828220581 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -105,8 +105,6 @@ class ParamParser { } else if (typeof searchParam?.value === 'object') { result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); } - - return result; } } } From f67286be7b618d21eb1529566e1f380c0033a18f Mon Sep 17 00:00:00 2001 From: Don Stephan Date: Thu, 4 Nov 2021 20:13:17 -0500 Subject: [PATCH 20/25] Typo in options label for select field --- docs/fields/select.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fields/select.mdx b/docs/fields/select.mdx index 08577ac24e..8d1d8c0a99 100644 --- a/docs/fields/select.mdx +++ b/docs/fields/select.mdx @@ -15,7 +15,7 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co | Option | Description | | ---------------- | ----------- | | **`name`** * | To be used as the property name when stored and retrieved from the database. | -| **`options`** * | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `option` string and a `value` string. | +| **`options`** * | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. | | **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. | | **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | From e7b1adf4eda3c7ea2bac49c69aaf2db999956451 Mon Sep 17 00:00:00 2001 From: Don Stephan Date: Thu, 4 Nov 2021 20:28:32 -0500 Subject: [PATCH 21/25] Better english --- docs/fields/select.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fields/select.mdx b/docs/fields/select.mdx index 8d1d8c0a99..3f5483b034 100644 --- a/docs/fields/select.mdx +++ b/docs/fields/select.mdx @@ -15,7 +15,7 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co | Option | Description | | ---------------- | ----------- | | **`name`** * | To be used as the property name when stored and retrieved from the database. | -| **`options`** * | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. | +| **`options`** * | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. | | **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. | | **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | From 291c193ad4a9ec8ce9310cc63c714eba10eca102 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Wed, 10 Nov 2021 11:12:52 -0500 Subject: [PATCH 22/25] fix: updates field description type to include react nodes --- .../forms/FieldDescription/types.ts | 4 +-- .../forms/field-types/Array/types.ts | 3 +- .../forms/field-types/Blocks/types.ts | 3 +- .../forms/field-types/Password/types.ts | 3 +- src/fields/config/types.ts | 35 +++++++++---------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/admin/components/forms/FieldDescription/types.ts b/src/admin/components/forms/FieldDescription/types.ts index 98e8c0bdd7..ee90d42ee0 100644 --- a/src/admin/components/forms/FieldDescription/types.ts +++ b/src/admin/components/forms/FieldDescription/types.ts @@ -2,9 +2,9 @@ import React from 'react'; export type DescriptionFunction = (value: unknown) => string -export type DescriptionComponent = React.ComponentType<{value: unknown}> +export type DescriptionComponent = React.ComponentType<{ value: unknown }> -type Description = string | DescriptionFunction | DescriptionComponent +export type Description = string | DescriptionFunction | DescriptionComponent export type Props = { description?: Description diff --git a/src/admin/components/forms/field-types/Array/types.ts b/src/admin/components/forms/field-types/Array/types.ts index 067f45c811..007dbbbf4c 100644 --- a/src/admin/components/forms/field-types/Array/types.ts +++ b/src/admin/components/forms/field-types/Array/types.ts @@ -1,7 +1,8 @@ import { Data } from '../../Form/types'; -import { ArrayField, Labels, Field, Description } from '../../../../../fields/config/types'; +import { ArrayField, Labels, Field } from '../../../../../fields/config/types'; import { FieldTypes } from '..'; import { FieldPermissions } from '../../../../../auth/types'; +import { Description } from '../../FieldDescription/types'; export type Props = Omit & { path?: string diff --git a/src/admin/components/forms/field-types/Blocks/types.ts b/src/admin/components/forms/field-types/Blocks/types.ts index e12683df3a..ee687dee7b 100644 --- a/src/admin/components/forms/field-types/Blocks/types.ts +++ b/src/admin/components/forms/field-types/Blocks/types.ts @@ -1,7 +1,8 @@ import { Data } from '../../Form/types'; -import { BlockField, Labels, Block, Description } from '../../../../../fields/config/types'; +import { BlockField, Labels, Block } from '../../../../../fields/config/types'; import { FieldTypes } from '..'; import { FieldPermissions } from '../../../../../auth/types'; +import { Description } from '../../FieldDescription/types'; export type Props = Omit & { path?: string diff --git a/src/admin/components/forms/field-types/Password/types.ts b/src/admin/components/forms/field-types/Password/types.ts index 214b322c52..3814c63928 100644 --- a/src/admin/components/forms/field-types/Password/types.ts +++ b/src/admin/components/forms/field-types/Password/types.ts @@ -1,5 +1,6 @@ import React from 'react'; -import { Description, Validate } from '../../../../../fields/config/types'; +import { Validate } from '../../../../../fields/config/types'; +import { Description } from '../../FieldDescription/types'; export type Props = { autoComplete?: string diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index 91c4b4218d..801d69793f 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -4,6 +4,7 @@ import { Editor } from 'slate'; import { PayloadRequest } from '../../express/types'; import { Document } from '../../types'; import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types'; +import { Description } from '../../admin/components/forms/FieldDescription/types'; export type FieldHook = (args: { value?: unknown, @@ -40,8 +41,6 @@ type Admin = { hidden?: boolean } -export type Description = string | ((value: Record) => string); - export type Labels = { singular: string; plural: string; @@ -302,22 +301,22 @@ export type FieldAffectingData = | PointField export type NonPresentationalField = TextField -| NumberField -| EmailField -| TextareaField -| CheckboxField -| DateField -| BlockField -| GroupField -| RadioField -| RelationshipField -| ArrayField -| RichTextField -| SelectField -| UploadField -| CodeField -| PointField -| RowField; + | NumberField + | EmailField + | TextareaField + | CheckboxField + | DateField + | BlockField + | GroupField + | RadioField + | RelationshipField + | ArrayField + | RichTextField + | SelectField + | UploadField + | CodeField + | PointField + | RowField; export type FieldWithPath = Field & { path?: string From b2c5b7e5752e829c7a53c054decceb43ec33065e Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Nov 2021 17:25:59 -0500 Subject: [PATCH 23/25] feat: ensures update hooks have access to full original docs even in spite of access control --- src/collections/operations/update.ts | 2 +- src/globals/operations/update.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collections/operations/update.ts b/src/collections/operations/update.ts index 85e4843981..9a140e0f3a 100644 --- a/src/collections/operations/update.ts +++ b/src/collections/operations/update.ts @@ -113,7 +113,7 @@ async function update(incomingArgs: Arguments): Promise { data: docWithLocales, hook: 'afterRead', operation: 'update', - overrideAccess, + overrideAccess: true, flattenLocales: true, showHiddenFields, }); diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index 87a04e610a..4fe7664b09 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -44,7 +44,7 @@ async function update(args) { data: globalJSON, hook: 'afterRead', operation: 'update', - overrideAccess, + overrideAccess: true, flattenLocales: true, showHiddenFields, }); From 40899c211b410168280b3ccd724ee77b1c50eb78 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 10 Nov 2021 17:30:31 -0500 Subject: [PATCH 24/25] chore: beta release --- CHANGELOG.md | 8 +++----- package.json | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f2112f88..b37733886c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,18 @@ -## [0.12.6-beta.0](https://github.com/payloadcms/payload/compare/v0.12.3...v0.12.6-beta.0) (2021-11-02) +## [0.12.8-beta.0](https://github.com/payloadcms/payload/compare/v0.12.3...v0.12.8-beta.0) (2021-11-10) ### Bug Fixes * [#351](https://github.com/payloadcms/payload/issues/351) ([94c2b8d](https://github.com/payloadcms/payload/commit/94c2b8d80b046c067057d4ad089ed6a2edd656cf)) * bug with relationship cell when no doc is available ([40b33d9](https://github.com/payloadcms/payload/commit/40b33d9f5e99285cb0de148dbe059259817fcad8)) -* ensures 'like' query param remains functional in all cases ([20d4e72](https://github.com/payloadcms/payload/commit/20d4e72a951dfcbf1cc301d0938e5095932436b9)) -* ensures relationship field search can return more than 10 options ([57c0346](https://github.com/payloadcms/payload/commit/57c0346a00286a3df695ea46e5c2630494183b5b)) * ensures richtext links retain proper formatting ([abf61d0](https://github.com/payloadcms/payload/commit/abf61d0734c09fd0fc5c5b827cb0631e11701f71)) - ### Features +* adds relationship filter field ([463c4e6](https://github.com/payloadcms/payload/commit/463c4e60de8e647fca6268b826d826f9c6e45412)) +* ensures update hooks have access to full original docs even in spite of access control ([b2c5b7e](https://github.com/payloadcms/payload/commit/b2c5b7e5752e829c7a53c054decceb43ec33065e)) * improves querying logic ([4c85747](https://github.com/payloadcms/payload/commit/4c8574784995b1cb1f939648f4d2158286089b3d)) - ## [0.12.3](https://github.com/payloadcms/payload/compare/v0.12.2...v0.12.3) (2021-10-23) diff --git a/package.json b/package.json index 245c163279..776927a577 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "0.12.6-beta.0", + "version": "0.12.8-beta.0", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "SEE LICENSE IN license.md", "author": { From b2fe27dda503b30ab10c3f32f5eb53b69ce7205b Mon Sep 17 00:00:00 2001 From: Tejas Ahluwalia <39881648+tejasahluwalia@users.noreply.github.com> Date: Sun, 21 Nov 2021 00:05:10 +0530 Subject: [PATCH 25/25] Include 'WebP' as image type This is called in the useThumbail hook. Adding webp support for thumbnails in the admin Thumbnail component. --- src/uploads/isImage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uploads/isImage.ts b/src/uploads/isImage.ts index 4728e6d58d..f91314031c 100644 --- a/src/uploads/isImage.ts +++ b/src/uploads/isImage.ts @@ -1,3 +1,3 @@ export default function isImage(mimeType: string): boolean { - return ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].indexOf(mimeType) > -1; + return ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'].indexOf(mimeType) > -1; }