From 2d0441a72e19297a5ce76b636d55141a15c43321 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 14 Apr 2023 15:52:38 -0400 Subject: [PATCH 1/7] chore: begins work to build queries from fields instead of mongoose schema --- src/auth/getExecuteStaticAccess.ts | 35 +++--- src/auth/strategies/apiKey.ts | 1 - src/collections/buildSchema.ts | 4 +- src/collections/config/types.ts | 3 +- src/collections/initLocal.ts | 8 +- src/collections/operations/delete.ts | 20 ++-- src/collections/operations/deleteByID.ts | 27 +++-- src/collections/operations/find.ts | 25 +++-- src/collections/operations/findByID.ts | 28 ++--- src/collections/operations/findVersionByID.ts | 25 +++-- src/collections/operations/findVersions.ts | 14 ++- src/collections/operations/local/find.ts | 3 - src/collections/operations/restoreVersion.ts | 24 ++-- src/collections/operations/update.ts | 23 ++-- src/collections/operations/updateByID.ts | 24 ++-- src/globals/buildModel.ts | 4 +- src/globals/initLocal.ts | 8 +- src/globals/operations/findOne.ts | 27 +++-- src/globals/operations/findVersionByID.ts | 24 ++-- src/globals/operations/findVersions.ts | 14 ++- src/globals/operations/update.ts | 24 ++-- src/mongoose/buildQuery.ts | 105 +++++++++++++----- src/utilities/flattenWhereConstraints.ts | 28 ++--- src/versions/drafts/queryDrafts.ts | 28 +++-- .../drafts/replaceWithDraftIfAvailable.ts | 36 +++--- 25 files changed, 325 insertions(+), 237 deletions(-) diff --git a/src/auth/getExecuteStaticAccess.ts b/src/auth/getExecuteStaticAccess.ts index cc1071d07d..17deb93a2f 100644 --- a/src/auth/getExecuteStaticAccess.ts +++ b/src/auth/getExecuteStaticAccess.ts @@ -16,26 +16,24 @@ const getExecuteStaticAccess = ({ config, Model }) => async (req: PayloadRequest if (typeof accessResult === 'object') { const filename = decodeURI(req.path).replace(/^\/|\/$/g, ''); - const queryToBuild: { where: Where } = { - where: { - and: [ - { - or: [ - { - filename: { - equals: filename, - }, + const queryToBuild: Where = { + and: [ + { + or: [ + { + filename: { + equals: filename, }, - ], - }, - accessResult, - ], - }, + }, + ], + }, + accessResult, + ], }; if (config.upload.imageSizes) { config.upload.imageSizes.forEach(({ name }) => { - queryToBuild.where.and[0].or.push({ + queryToBuild.and[0].or.push({ [`sizes.${name}.filename`]: { equals: filename, }, @@ -43,7 +41,12 @@ const getExecuteStaticAccess = ({ config, Model }) => async (req: PayloadRequest }); } - const query = await Model.buildQuery(queryToBuild, req.locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess: true, + }); + const doc = await Model.findOne(query); if (!doc) { diff --git a/src/auth/strategies/apiKey.ts b/src/auth/strategies/apiKey.ts index b78ffcf9c5..b2e1a0fe12 100644 --- a/src/auth/strategies/apiKey.ts +++ b/src/auth/strategies/apiKey.ts @@ -45,7 +45,6 @@ export default (payload: Payload, { Model, config }): PassportAPIKey => { }, req: req as PayloadRequest, overrideAccess: true, - queryHiddenFields: true, depth: config.auth.depth, }); diff --git a/src/collections/buildSchema.ts b/src/collections/buildSchema.ts index 36ae7c762a..dd4c11ca84 100644 --- a/src/collections/buildSchema.ts +++ b/src/collections/buildSchema.ts @@ -1,7 +1,7 @@ import paginate from 'mongoose-paginate-v2'; import { Schema } from 'mongoose'; import { SanitizedConfig } from '../config/types'; -import buildQueryPlugin from '../mongoose/buildQuery'; +import getBuildQueryPlugin from '../mongoose/buildQuery'; import buildSchema from '../mongoose/buildSchema'; import { SanitizedCollectionConfig } from './config/types'; @@ -26,7 +26,7 @@ const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: Sa } schema.plugin(paginate, { useEstimatedCount: true }) - .plugin(buildQueryPlugin); + .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug })); return schema; }; diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index e0eff06b47..034b17a34e 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -10,6 +10,7 @@ import { Auth, IncomingAuthType } from '../../auth/types'; import { IncomingUploadType, Upload } from '../../uploads/types'; import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types'; import { Config as GeneratedTypes } from '../../generated-types'; +import { BuildQueryArgs } from '../../mongoose/buildQuery'; type Register = (doc: T, password: string) => T; @@ -19,7 +20,7 @@ interface PassportLocalModel { } export interface CollectionModel extends Model, PaginateModel, AggregatePaginateModel, PassportLocalModel { - buildQuery: (query: unknown, locale: string, queryHiddenFields?: boolean) => Record + buildQuery: (args: BuildQueryArgs) => Promise> } export interface AuthCollectionModel extends CollectionModel { diff --git a/src/collections/initLocal.ts b/src/collections/initLocal.ts index e16542672a..8f5e64e7df 100644 --- a/src/collections/initLocal.ts +++ b/src/collections/initLocal.ts @@ -3,7 +3,7 @@ import paginate from 'mongoose-paginate-v2'; import passportLocalMongoose from 'passport-local-mongoose'; import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'; import { buildVersionCollectionFields } from '../versions/buildCollectionFields'; -import buildQueryPlugin from '../mongoose/buildQuery'; +import getBuildQueryPlugin from '../mongoose/buildQuery'; import buildCollectionSchema from './buildSchema'; import buildSchema from '../mongoose/buildSchema'; import { CollectionModel, SanitizedCollectionConfig } from './config/types'; @@ -62,9 +62,11 @@ export default function initCollectionsLocal(ctx: Payload): void { if (collection.versions) { const versionModelName = getVersionsModelName(collection); + const versionCollectionFields = buildVersionCollectionFields(collection); + const versionSchema = buildSchema( ctx.config, - buildVersionCollectionFields(collection), + versionCollectionFields, { disableUnique: true, draftsEnabled: true, @@ -76,7 +78,7 @@ export default function initCollectionsLocal(ctx: Payload): void { ); versionSchema.plugin(paginate, { useEstimatedCount: true }) - .plugin(buildQueryPlugin); + .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, isVersionsModel: true })); if (collection.versions?.drafts) { versionSchema.plugin(mongooseAggregatePaginate); diff --git a/src/collections/operations/delete.ts b/src/collections/operations/delete.ts index cbaffe42c3..e6a6d8bbf3 100644 --- a/src/collections/operations/delete.ts +++ b/src/collections/operations/delete.ts @@ -74,21 +74,19 @@ async function deleteOperation(inc req, req: { t, - locale, payload, payload: { config, @@ -80,25 +79,25 @@ async function deleteByID(inc // Retrieve document // ///////////////////////////////////// - const queryToBuild: { - where: Where - } = { - where: { - and: [ - { - id: { - equals: id, - }, + const queryToBuild: Where = { + and: [ + { + id: { + equals: id, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + req, + where: queryToBuild, + overrideAccess, + }); const docToDelete = await Model.findOne(query); diff --git a/src/collections/operations/find.ts b/src/collections/operations/find.ts index f09e824a9c..622430b7d1 100644 --- a/src/collections/operations/find.ts +++ b/src/collections/operations/find.ts @@ -24,7 +24,6 @@ export type Arguments = { disableErrors?: boolean pagination?: boolean showHiddenFields?: boolean - queryHiddenFields?: boolean draft?: boolean } @@ -66,7 +65,6 @@ async function find>( overrideAccess, disableErrors, showHiddenFields, - queryHiddenFields, pagination = true, } = args; @@ -74,23 +72,21 @@ async function find>( // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where } = { - where: { - and: [], - }, + let queryToBuild: Where = { + and: [], }; let useEstimatedCount = false; if (where) { - queryToBuild.where = { + queryToBuild = { and: [], ...where, }; if (Array.isArray(where.AND)) { - queryToBuild.where.and = [ - ...queryToBuild.where.and, + queryToBuild.and = [ + ...queryToBuild.and, ...where.AND, ]; } @@ -122,11 +118,15 @@ async function find>( } if (hasWhereAccessResult(accessResult)) { - queryToBuild.where.and.push(accessResult); + queryToBuild.and.push(accessResult); } } - const query = await Model.buildQuery(queryToBuild, locale, queryHiddenFields); + const query = await Model.buildQuery({ + req, + where: queryToBuild, + overrideAccess, + }); // ///////////////////////////////////// // Find @@ -166,7 +166,8 @@ async function find>( result = await queryDrafts({ accessResult, collection, - locale, + req, + overrideAccess, paginationOptions, payload, where, diff --git a/src/collections/operations/findByID.ts b/src/collections/operations/findByID.ts index 25c829e441..78881a9af2 100644 --- a/src/collections/operations/findByID.ts +++ b/src/collections/operations/findByID.ts @@ -50,7 +50,6 @@ async function findByID( req, req: { t, - locale, payload, }, disableErrors, @@ -69,23 +68,25 @@ async function findByID( // If errors are disabled, and access returns false, return null if (accessResult === false) return null; - const queryToBuild: { where: Where } = { - where: { - and: [ - { - _id: { - equals: id, - }, + const queryToBuild: Where = { + and: [ + { + _id: { + equals: id, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResult)) { - queryToBuild.where.and.push(accessResult); + queryToBuild.and.push(accessResult); } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Find by ID @@ -132,7 +133,8 @@ async function findByID( entityType: 'collection', doc: result, accessResult, - locale, + req, + overrideAccess, }); } diff --git a/src/collections/operations/findVersionByID.ts b/src/collections/operations/findVersionByID.ts index 2d313ffda4..45a2a273e7 100644 --- a/src/collections/operations/findVersionByID.ts +++ b/src/collections/operations/findVersionByID.ts @@ -31,7 +31,6 @@ async function findVersionByID = any>(args: Argumen req, req: { t, - locale, payload, }, disableErrors, @@ -57,23 +56,25 @@ async function findVersionByID = any>(args: Argumen const hasWhereAccess = typeof accessResults === 'object'; - const queryToBuild: { where: Where } = { - where: { - and: [ - { - _id: { - equals: id, - }, + const queryToBuild: Where = { + and: [ + { + _id: { + equals: id, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await VersionsModel.buildQuery(queryToBuild, locale); + const query = await VersionsModel.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Find by ID diff --git a/src/collections/operations/findVersions.ts b/src/collections/operations/findVersions.ts index 3b8dabd2e5..a5c92410fd 100644 --- a/src/collections/operations/findVersions.ts +++ b/src/collections/operations/findVersions.ts @@ -49,7 +49,7 @@ async function findVersions>( // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where } = {}; + let queryToBuild: Where = {}; let useEstimatedCount = false; if (where) { @@ -58,7 +58,7 @@ async function findVersions>( if (Array.isArray(where.and)) and = where.and; if (Array.isArray(where.AND)) and = where.AND; - queryToBuild.where = { + queryToBuild = { ...where, and: [ ...and, @@ -75,18 +75,22 @@ async function findVersions>( if (hasWhereAccessResult(accessResults)) { if (!where) { - queryToBuild.where = { + queryToBuild = { and: [ accessResults, ], }; } else { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } } } - const query = await VersionsModel.buildQuery(queryToBuild, locale); + const query = await VersionsModel.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Find diff --git a/src/collections/operations/local/find.ts b/src/collections/operations/local/find.ts index cf2a915a39..bb68d890b4 100644 --- a/src/collections/operations/local/find.ts +++ b/src/collections/operations/local/find.ts @@ -20,7 +20,6 @@ export type Options = { overrideAccess?: boolean disableErrors?: boolean showHiddenFields?: boolean - queryHiddenFields?: boolean pagination?: boolean sort?: string where?: Where @@ -45,7 +44,6 @@ export default async function findLocal(args: Arguments): Prom // Retrieve document // ///////////////////////////////////// - const queryToBuild: { where: Where } = { - where: { - and: [ - { - id: { - equals: parentDocID, - }, + const queryToBuild: Where = { + and: [ + { + id: { + equals: parentDocID, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); const doc = await Model.findOne(query); diff --git a/src/collections/operations/update.ts b/src/collections/operations/update.ts index 60eaa99299..6a798da6e6 100644 --- a/src/collections/operations/update.ts +++ b/src/collections/operations/update.ts @@ -84,21 +84,19 @@ async function update( // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where } = { - where: { - and: [], - }, + let queryToBuild: Where = { + and: [], }; if (where) { - queryToBuild.where = { + queryToBuild = { and: [], ...where, }; if (Array.isArray(where.AND)) { - queryToBuild.where.and = [ - ...queryToBuild.where.and, + queryToBuild.and = [ + ...queryToBuild.and, ...where.AND, ]; } @@ -110,11 +108,15 @@ async function update( accessResult = await executeAccess({ req }, collectionConfig.access.update); if (hasWhereAccessResult(accessResult)) { - queryToBuild.where.and.push(accessResult); + queryToBuild.and.push(accessResult); } } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Retrieve documents @@ -125,7 +127,8 @@ async function update( docs = await queryDrafts({ accessResult, collection, - locale, + req, + overrideAccess, payload, where: query, }); diff --git a/src/collections/operations/updateByID.ts b/src/collections/operations/updateByID.ts index aa29cd9baf..ca8f026254 100644 --- a/src/collections/operations/updateByID.ts +++ b/src/collections/operations/updateByID.ts @@ -96,23 +96,25 @@ async function updateByID( // Retrieve document // ///////////////////////////////////// - const queryToBuild: { where: Where } = { - where: { - and: [ - { - id: { - equals: id, - }, + const queryToBuild: Where = { + and: [ + { + id: { + equals: id, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); const doc = await getLatestCollectionVersion({ payload, diff --git a/src/globals/buildModel.ts b/src/globals/buildModel.ts index e586943963..8c18321d59 100644 --- a/src/globals/buildModel.ts +++ b/src/globals/buildModel.ts @@ -1,14 +1,14 @@ import mongoose from 'mongoose'; import buildSchema from '../mongoose/buildSchema'; import { SanitizedConfig } from '../config/types'; -import buildQueryPlugin from '../mongoose/buildQuery'; +import getBuildQueryPlugin from '../mongoose/buildQuery'; import { GlobalModel } from './config/types'; const buildModel = (config: SanitizedConfig): GlobalModel | null => { if (config.globals && config.globals.length > 0) { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false }); - globalsSchema.plugin(buildQueryPlugin); + globalsSchema.plugin(getBuildQueryPlugin({ isGlobalModel: true })); const Globals = mongoose.model('globals', globalsSchema) as unknown as GlobalModel; diff --git a/src/globals/initLocal.ts b/src/globals/initLocal.ts index cea8748d27..337408a9f2 100644 --- a/src/globals/initLocal.ts +++ b/src/globals/initLocal.ts @@ -1,6 +1,6 @@ import mongoose from 'mongoose'; import paginate from 'mongoose-paginate-v2'; -import buildQueryPlugin from '../mongoose/buildQuery'; +import getBuildQueryPlugin from '../mongoose/buildQuery'; import buildModel from './buildModel'; import { Payload } from '../payload'; import { getVersionsModelName } from '../versions/getVersionsModelName'; @@ -19,9 +19,11 @@ export default function initGlobalsLocal(ctx: Payload): void { if (global.versions) { const versionModelName = getVersionsModelName(global); + const versionGlobalFields = buildVersionGlobalFields(global); + const versionSchema = buildSchema( ctx.config, - buildVersionGlobalFields(global), + versionGlobalFields, { disableUnique: true, draftsEnabled: true, @@ -33,7 +35,7 @@ export default function initGlobalsLocal(ctx: Payload): void { ); versionSchema.plugin(paginate, { useEstimatedCount: true }) - .plugin(buildQueryPlugin); + .plugin(getBuildQueryPlugin({ globalSlug: global.slug, isVersionsModel: true })); ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel; } diff --git a/src/globals/operations/findOne.ts b/src/globals/operations/findOne.ts index 1b3bec22dc..72eaca4e8d 100644 --- a/src/globals/operations/findOne.ts +++ b/src/globals/operations/findOne.ts @@ -40,16 +40,14 @@ async function findOne>(args: Args): Promise>(args: Args): Promise>(args: Args): Promise = any>(args: Argumen const hasWhereAccess = typeof accessResults === 'object'; - const queryToBuild: { where: Where } = { - where: { - and: [ - { - _id: { - equals: id, - }, + const queryToBuild: Where = { + and: [ + { + _id: { + equals: id, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await VersionsModel.buildQuery(queryToBuild, locale); + const query = await VersionsModel.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Find by ID diff --git a/src/globals/operations/findVersions.ts b/src/globals/operations/findVersions.ts index e6185699ab..e245cd7ad2 100644 --- a/src/globals/operations/findVersions.ts +++ b/src/globals/operations/findVersions.ts @@ -47,7 +47,7 @@ async function findVersions>( // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where } = {}; + let queryToBuild: Where = {}; let useEstimatedCount = false; if (where) { @@ -56,7 +56,7 @@ async function findVersions>( if (Array.isArray(where.and)) and = where.and; if (Array.isArray(where.AND)) and = where.AND; - queryToBuild.where = { + queryToBuild = { ...where, and: [ ...and, @@ -73,18 +73,22 @@ async function findVersions>( if (hasWhereAccessResult(accessResults)) { if (!where) { - queryToBuild.where = { + queryToBuild = { and: [ accessResults, ], }; } else { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } } } - const query = await VersionsModel.buildQuery(queryToBuild, locale); + const query = await VersionsModel.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // Find diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index 4d2a7ca728..ee3b2d1ab2 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -62,23 +62,25 @@ async function update( // Retrieve document // ///////////////////////////////////// - const queryToBuild: { where: Where } = { - where: { - and: [ - { - globalType: { - equals: slug, - }, + const queryToBuild: Where = { + and: [ + { + globalType: { + equals: slug, }, - ], - }, + }, + ], }; if (hasWhereAccessResult(accessResults)) { - (queryToBuild.where.and as Where[]).push(accessResults); + queryToBuild.and.push(accessResults); } - const query = await Model.buildQuery(queryToBuild, locale); + const query = await Model.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); // ///////////////////////////////////// // 2. Retrieve document diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index b4d8fb7c1b..da1a2cf778 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -7,6 +7,7 @@ import { CollectionModel } from '../collections/config/types'; import { getSchemaTypeOptions } from './getSchemaTypeOptions'; import { operatorMap } from './operatorMap'; import { sanitizeQueryValue } from './sanitizeFormattedValue'; +import { PayloadRequest, Where } from '../types'; const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near']; @@ -35,11 +36,19 @@ type SearchParam = { } class ParamParser { - locale: string; + collectionSlug?: string - queryHiddenFields: boolean + globalSlug?: string - rawParams: any; + isGlobalModel?: boolean + + isVersionsModel?: boolean + + overrideAccess: boolean + + req: PayloadRequest + + where: Where; model: any; @@ -50,12 +59,25 @@ class ParamParser { sort: boolean; }; - constructor(model, rawParams, locale: string, queryHiddenFields?: boolean) { + constructor({ + req, + collectionSlug, + globalSlug, + isGlobalModel, + isVersionsModel, + model, + where, + overrideAccess, + }) { + this.req = req; + this.collectionSlug = collectionSlug; + this.globalSlug = globalSlug; + this.isGlobalModel = isGlobalModel; + this.isVersionsModel = isVersionsModel; this.parse = this.parse.bind(this); this.model = model; - this.rawParams = rawParams; - this.locale = locale; - this.queryHiddenFields = queryHiddenFields; + this.where = where; + this.overrideAccess = overrideAccess; this.query = { searchParams: {}, sort: false, @@ -65,14 +87,8 @@ class ParamParser { // Entry point to the ParamParser class async parse(): Promise { - if (typeof this.rawParams === 'object') { - for (const key of Object.keys(this.rawParams)) { - if (key === 'where') { - this.query.searchParams = await this.parsePathOrRelation(this.rawParams.where); - } else if (key === 'sort') { - this.query.sort = this.rawParams[key]; - } - } + if (typeof this.where === 'object') { + this.query.searchParams = await this.parsePathOrRelation(this.where); return this.query; } return {}; @@ -174,7 +190,7 @@ class ParamParser { return true; } - const localePath = `${currentPath}.${this.locale}`; + const localePath = `${currentPath}.${this.req.locale}`; const localizedSchemaType = schema.path(localePath); if (localizedSchemaType || operator === 'near') { @@ -231,7 +247,7 @@ class ParamParser { const schemaOptions = getSchemaTypeOptions(schemaType); const formattedValue = sanitizeQueryValue(schemaType, path, operator, val); - if (!this.queryHiddenFields && (['salt', 'hash'].includes(path) || schemaType?.options?.hidden)) { + if (!this.overrideAccess && (['salt', 'hash'].includes(path) || schemaType?.options?.hidden)) { return undefined; } @@ -258,7 +274,9 @@ class ParamParser { [operator]: val, }, }, - }, this.locale); + req: this.req, + overrideAccess: this.overrideAccess, + }); const result = await SubModel.find(subQuery, subQueryOptions); @@ -360,15 +378,46 @@ class ParamParser { return undefined; } } + +type GetBuildQueryPluginArgs = { + collectionSlug?: string + globalSlug?: string + isGlobalModel?: boolean + isVersionsModel?: boolean +} + +export type BuildQueryArgs = { + req: PayloadRequest + where: Where + overrideAccess: boolean +} + // This plugin asynchronously builds a list of Mongoose query constraints // which can then be used in subsequent Mongoose queries. -function buildQueryPlugin(schema) { - const modifiedSchema = schema; - async function buildQuery(rawParams, locale, queryHiddenFields = false) { - const paramParser = new ParamParser(this, rawParams, locale, queryHiddenFields); - const params = await paramParser.parse(); - return params.searchParams; - } - modifiedSchema.statics.buildQuery = buildQuery; -} -export default buildQueryPlugin; +const getBuildQueryPlugin = ({ + collectionSlug, + globalSlug, + isGlobalModel, + isVersionsModel, +}: GetBuildQueryPluginArgs) => { + return function buildQueryPlugin(schema) { + const modifiedSchema = schema; + async function buildQuery({ req, where, overrideAccess = false }: BuildQueryArgs) { + const paramParser = new ParamParser({ + req, + collectionSlug, + globalSlug, + isGlobalModel, + isVersionsModel, + model: this, + where, + overrideAccess, + }); + const params = await paramParser.parse(); + return params.searchParams; + } + modifiedSchema.statics.buildQuery = buildQuery; + }; +}; + +export default getBuildQueryPlugin; diff --git a/src/utilities/flattenWhereConstraints.ts b/src/utilities/flattenWhereConstraints.ts index 0a5bf5c259..9fbded0af4 100644 --- a/src/utilities/flattenWhereConstraints.ts +++ b/src/utilities/flattenWhereConstraints.ts @@ -1,18 +1,18 @@ -import { WhereField } from '../types'; +import { WhereField, Where } from '../types'; -const flattenWhereConstraints = (query): WhereField[] => { - if (!query.where && !query.and && !query.or) { - return Object.keys(query).map((key) => query[key]); +// Take a where query and flatten it to all top-level operators +const flattenWhereConstraints = (query: Where): WhereField[] => Object.entries(query).reduce((flattenedConstraints, [key, val]) => { + if ((key === 'and' || key === 'or') && Array.isArray(val)) { + return [ + ...flattenedConstraints, + ...val.map((subVal) => flattenWhereConstraints(subVal)), + ]; } - if (query.where) { - const whereResult = flattenWhereConstraints(query.where); - return Object.keys(whereResult).map((key) => whereResult[key]); - } - const nested = [...query.or || [], ...query.and || []]; - if (nested.length > 0) { - return nested.flatMap((nest) => flattenWhereConstraints(nest)); - } - return query; -}; + + return [ + ...flattenedConstraints, + val, + ]; +}, []); export default flattenWhereConstraints; diff --git a/src/versions/drafts/queryDrafts.ts b/src/versions/drafts/queryDrafts.ts index 72b87cc4b7..421437132f 100644 --- a/src/versions/drafts/queryDrafts.ts +++ b/src/versions/drafts/queryDrafts.ts @@ -1,6 +1,6 @@ import { PaginateOptions } from 'mongoose'; import { AccessResult } from '../../config/types'; -import { Where } from '../../types'; +import { PayloadRequest, Where } from '../../types'; import { Payload } from '../../payload'; import { PaginatedDocs } from '../../mongoose/types'; import { Collection, CollectionModel, TypeWithID } from '../../collections/config/types'; @@ -17,7 +17,8 @@ type AggregateVersion = { type Args = { accessResult: AccessResult collection: Collection - locale: string + req: PayloadRequest + overrideAccess: boolean paginationOptions?: PaginateOptions payload: Payload where: Where @@ -26,7 +27,8 @@ type Args = { export const queryDrafts = async ({ accessResult, collection, - locale, + req, + overrideAccess, payload, paginationOptions, where: incomingWhere, @@ -35,21 +37,23 @@ export const queryDrafts = async ({ const where = appendVersionToQueryKey(incomingWhere || {}); - const versionQueryToBuild: { where: Where } = { - where: { - ...where, - and: [ - ...where?.and || [], - ], - }, + const versionQueryToBuild: Where = { + ...where, + and: [ + ...where?.and || [], + ], }; if (hasWhereAccessResult(accessResult)) { const versionAccessResult = appendVersionToQueryKey(accessResult); - versionQueryToBuild.where.and.push(versionAccessResult); + versionQueryToBuild.and.push(versionAccessResult); } - const versionQuery = await VersionModel.buildQuery(versionQueryToBuild, locale); + const versionQuery = await VersionModel.buildQuery({ + where: versionQueryToBuild, + req, + overrideAccess, + }); const aggregate = VersionModel.aggregate>([ // Sort so that newest are first diff --git a/src/versions/drafts/replaceWithDraftIfAvailable.ts b/src/versions/drafts/replaceWithDraftIfAvailable.ts index 861bb0d4af..d367fe6150 100644 --- a/src/versions/drafts/replaceWithDraftIfAvailable.ts +++ b/src/versions/drafts/replaceWithDraftIfAvailable.ts @@ -1,5 +1,5 @@ import { Payload } from '../../payload'; -import { docHasTimestamps, Where } from '../../types'; +import { docHasTimestamps, PayloadRequest, Where } from '../../types'; import { hasWhereAccessResult } from '../../auth'; import { AccessResult } from '../../config/types'; import { CollectionModel, SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types'; @@ -12,7 +12,8 @@ type Arguments = { entity: SanitizedCollectionConfig | SanitizedGlobalConfig entityType: 'collection' | 'global' doc: T - locale: string + req: PayloadRequest + overrideAccess: boolean accessResult: AccessResult } @@ -21,25 +22,24 @@ const replaceWithDraftIfAvailable = async ({ entity, entityType, doc, - locale, + req, + overrideAccess, accessResult, }: Arguments): Promise => { const VersionModel = payload.versions[entity.slug] as CollectionModel; - const queryToBuild: { where: Where } = { - where: { - and: [ - { - 'version._status': { - equals: 'draft', - }, + const queryToBuild: Where = { + and: [ + { + 'version._status': { + equals: 'draft', }, - ], - }, + }, + ], }; if (entityType === 'collection') { - queryToBuild.where.and.push({ + queryToBuild.and.push({ parent: { equals: doc.id, }, @@ -47,7 +47,7 @@ const replaceWithDraftIfAvailable = async ({ } if (docHasTimestamps(doc)) { - queryToBuild.where.and.push({ + queryToBuild.and.push({ updatedAt: { greater_than: doc.updatedAt, }, @@ -56,10 +56,14 @@ const replaceWithDraftIfAvailable = async ({ if (hasWhereAccessResult(accessResult)) { const versionAccessResult = appendVersionToQueryKey(accessResult); - queryToBuild.where.and.push(versionAccessResult); + queryToBuild.and.push(versionAccessResult); } - const query = await VersionModel.buildQuery(queryToBuild, locale); + const query = await VersionModel.buildQuery({ + where: queryToBuild, + req, + overrideAccess, + }); let draft = await VersionModel.findOne(query, {}, { lean: true, From d187b809d7b0b37465b15d0b4b17702be521f630 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 17 Apr 2023 16:08:44 -0400 Subject: [PATCH 2/7] feat: refactors buildQuery to rely on fields instead of mongoose --- src/collections/initLocal.ts | 2 +- src/globals/buildModel.ts | 2 +- src/globals/initLocal.ts | 2 +- src/globals/operations/findOne.ts | 1 + src/globals/operations/findVersionByID.ts | 2 +- src/globals/operations/findVersions.ts | 1 + src/globals/operations/update.ts | 2 +- src/mongoose/buildQuery.ts | 541 +++++++++++------- ...ormattedValue.ts => sanitizeQueryValue.ts} | 67 ++- src/utilities/getEntityPolicies.ts | 57 +- .../drafts/replaceWithDraftIfAvailable.ts | 1 + test/globals/config.ts | 36 ++ test/globals/int.spec.ts | 25 +- test/localization/int.spec.ts | 4 - 14 files changed, 487 insertions(+), 256 deletions(-) rename src/mongoose/{sanitizeFormattedValue.ts => sanitizeQueryValue.ts} (74%) diff --git a/src/collections/initLocal.ts b/src/collections/initLocal.ts index 8f5e64e7df..8b1eae234f 100644 --- a/src/collections/initLocal.ts +++ b/src/collections/initLocal.ts @@ -78,7 +78,7 @@ export default function initCollectionsLocal(ctx: Payload): void { ); versionSchema.plugin(paginate, { useEstimatedCount: true }) - .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, isVersionsModel: true })); + .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug, versionsFields: versionCollectionFields })); if (collection.versions?.drafts) { versionSchema.plugin(mongooseAggregatePaginate); diff --git a/src/globals/buildModel.ts b/src/globals/buildModel.ts index 8c18321d59..b7d2fc6863 100644 --- a/src/globals/buildModel.ts +++ b/src/globals/buildModel.ts @@ -8,7 +8,7 @@ const buildModel = (config: SanitizedConfig): GlobalModel | null => { if (config.globals && config.globals.length > 0) { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false }); - globalsSchema.plugin(getBuildQueryPlugin({ isGlobalModel: true })); + globalsSchema.plugin(getBuildQueryPlugin()); const Globals = mongoose.model('globals', globalsSchema) as unknown as GlobalModel; diff --git a/src/globals/initLocal.ts b/src/globals/initLocal.ts index 337408a9f2..0728c552b9 100644 --- a/src/globals/initLocal.ts +++ b/src/globals/initLocal.ts @@ -35,7 +35,7 @@ export default function initGlobalsLocal(ctx: Payload): void { ); versionSchema.plugin(paginate, { useEstimatedCount: true }) - .plugin(getBuildQueryPlugin({ globalSlug: global.slug, isVersionsModel: true })); + .plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields })); ctx.versions[global.slug] = mongoose.model(versionModelName, versionSchema) as CollectionModel; } diff --git a/src/globals/operations/findOne.ts b/src/globals/operations/findOne.ts index 72eaca4e8d..b3e7974a04 100644 --- a/src/globals/operations/findOne.ts +++ b/src/globals/operations/findOne.ts @@ -64,6 +64,7 @@ async function findOne>(args: Args): Promise = any>(args: Argumen req: { t, payload, - locale, }, disableErrors, currentDepth, @@ -68,6 +67,7 @@ async function findVersionByID = any>(args: Argumen where: queryToBuild, req, overrideAccess, + globalSlug: globalConfig.slug, }); // ///////////////////////////////////// diff --git a/src/globals/operations/findVersions.ts b/src/globals/operations/findVersions.ts index e245cd7ad2..5ddceaea57 100644 --- a/src/globals/operations/findVersions.ts +++ b/src/globals/operations/findVersions.ts @@ -88,6 +88,7 @@ async function findVersions>( where: queryToBuild, req, overrideAccess, + globalSlug: globalConfig.slug, }); // ///////////////////////////////////// diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index ee3b2d1ab2..349944052f 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -33,7 +33,6 @@ async function update( slug, req, req: { - locale, payload, payload: { globals: { @@ -80,6 +79,7 @@ async function update( where: queryToBuild, req, overrideAccess, + globalSlug: slug, }); // ///////////////////////////////////// diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index da1a2cf778..86d51d67b4 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -1,13 +1,17 @@ +/* eslint-disable no-continue */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ import deepmerge from 'deepmerge'; -import mongoose, { FilterQuery } from 'mongoose'; +import { 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'; +import { sanitizeQueryValue } from './sanitizeQueryValue'; import { PayloadRequest, Where } from '../types'; +import { Field, FieldAffectingData, TabAsField, UIField, fieldAffectsData } from '../fields/config/types'; +import { CollectionPermission, FieldPermissions, GlobalPermission } from '../auth'; +import flattenFields from '../utilities/flattenTopLevelFields'; +import { getEntityPolicies } from '../utilities/getEntityPolicies'; +import { SanitizedConfig } from '../config/types'; const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near']; @@ -16,18 +20,15 @@ const subQueryOptions = { lean: true, }; -type ParseType = { - searchParams?: - { - [key: string]: any; - }; - sort?: boolean; -}; - type PathToQuery = { complete: boolean + collectionSlug?: string path: string - Model: CollectionModel + field: Field | TabAsField + fields?: (FieldAffectingData | UIField | TabAsField)[] + fieldPolicies?: { + [field: string]: FieldPermissions + } } type SearchParam = { @@ -35,15 +36,23 @@ type SearchParam = { value: unknown, } -class ParamParser { +type ParamParserArgs = { + req: PayloadRequest + collectionSlug?: string + globalSlug?: string + versionsFields?: Field[] + model: any + where: Where + overrideAccess?: boolean +} + +type QueryError = { path: string } + +export class ParamParser { collectionSlug?: string globalSlug?: string - isGlobalModel?: boolean - - isVersionsModel?: boolean - overrideAccess: boolean req: PayloadRequest @@ -52,49 +61,68 @@ class ParamParser { model: any; - query: { - searchParams: { - [key: string]: any; + fields: Field[] + + localizationConfig: SanitizedConfig['localization'] + + policies: { + collections?: { + [collectionSlug: string]: CollectionPermission; }; - sort: boolean; - }; + globals?: { + [globalSlug: string]: GlobalPermission; + }; + } + + errors: QueryError[] constructor({ req, collectionSlug, globalSlug, - isGlobalModel, - isVersionsModel, + versionsFields, model, where, overrideAccess, - }) { + }: ParamParserArgs) { this.req = req; this.collectionSlug = collectionSlug; this.globalSlug = globalSlug; - this.isGlobalModel = isGlobalModel; - this.isVersionsModel = isVersionsModel; this.parse = this.parse.bind(this); this.model = model; this.where = where; this.overrideAccess = overrideAccess; - this.query = { - searchParams: {}, - sort: false, + this.localizationConfig = req.payload.config.localization; + this.policies = { + collections: {}, + globals: {}, }; + this.errors = []; + + // Get entity fields + if (globalSlug) { + const globalConfig = req.payload.globals.config.find(({ slug }) => slug === globalSlug); + this.fields = versionsFields || globalConfig.fields; + } + + if (collectionSlug) { + const collectionConfig = req.payload.collections[collectionSlug].config; + this.fields = versionsFields || collectionConfig.fields; + } } // Entry point to the ParamParser class - async parse(): Promise { + async parse(): Promise> { if (typeof this.where === 'object') { - this.query.searchParams = await this.parsePathOrRelation(this.where); - return this.query; + const query = await this.parsePathOrRelation(this.where); + return query; } + return {}; } - async parsePathOrRelation(object) { + async parsePathOrRelation(object: Where): Promise> { let result = {} as FilterQuery; // We need to determine if the whereKey is an AND, OR, or a schema path for (const relationOrPath of Object.keys(object)) { @@ -114,7 +142,12 @@ class ParamParser { if (typeof pathOperators === 'object') { for (const operator of Object.keys(pathOperators)) { if (validOperators.includes(operator)) { - const searchParam = await this.buildSearchParam(this.model.schema, relationOrPath, pathOperators[operator], operator); + const searchParam = await this.buildSearchParam({ + fields: this.fields, + incomingPath: relationOrPath, + val: pathOperators[operator], + operator, + }); if (searchParam?.value && searchParam?.path) { result = { @@ -148,123 +181,85 @@ class ParamParser { return completedConditions; } - // Build up an array of auto-localized paths to search on - // Multiple paths may be possible if searching on properties of relationship fields - - getLocalizedPaths(Model: CollectionModel, incomingPath: string, operator): PathToQuery[] { - const { schema } = Model; - const pathSegments = incomingPath.split('.'); - - let paths: PathToQuery[] = [ - { - path: '', - complete: false, - Model, - }, - ]; - - pathSegments.every((segment, i) => { - const lastIncompletePath = paths.find(({ complete }) => !complete); - const { path } = lastIncompletePath; - - const currentPath = path ? `${path}.${segment}` : segment; - const currentSchemaType = schema.path(currentPath); - const currentSchemaPathType = schema.pathType(currentPath); - - if (currentSchemaPathType === 'nested') { - lastIncompletePath.path = currentPath; - return true; - } - - const upcomingSegment = pathSegments[i + 1]; - - if (currentSchemaType && currentSchemaPathType !== 'adhocOrUndefined') { - const currentSchemaTypeOptions = getSchemaTypeOptions(currentSchemaType); - - if (currentSchemaTypeOptions.localized) { - const upcomingLocalizedPath = `${currentPath}.${upcomingSegment}`; - const upcomingSchemaTypeWithLocale = schema.path(upcomingLocalizedPath); - - if (upcomingSchemaTypeWithLocale) { - lastIncompletePath.path = currentPath; - return true; - } - - const localePath = `${currentPath}.${this.req.locale}`; - const localizedSchemaType = schema.path(localePath); - - if (localizedSchemaType || operator === 'near') { - lastIncompletePath.path = localePath; - return true; - } - } - - lastIncompletePath.path = currentPath; - return true; - } - - 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 false; - } - } - - if (operator === 'near' || currentSchemaPathType === 'adhocOrUndefined') { - lastIncompletePath.path = currentPath; - } - - return true; - }); - - return paths; - } - // Convert the Payload key / value / operator into a MongoDB query - async buildSearchParam(schema, incomingPath, val, operator): Promise { + async buildSearchParam({ + fields, + incomingPath, + val, + operator, + }: { + fields: Field[], + incomingPath: string, + val: unknown, + operator: string + }): 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; + let paths: PathToQuery[] = []; + + let hasCustomID = false; + + if (sanitizedPath === '_id') { + const customIDfield = this.req.payload.collections[this.collectionSlug]?.config.fields.find((field) => fieldAffectsData(field) && field.name === 'id'); + + let idFieldType: 'text' | 'number' = 'text'; + + if (customIDfield) { + if (customIDfield?.type === 'text' || customIDfield?.type === 'number') { + idFieldType = customIDfield.type; + } + + hasCustomID = true; + } + + paths.push({ + path: '_id', + field: { + name: 'id', + type: idFieldType, + }, + complete: true, + collectionSlug: this.collectionSlug, + }); + } else { + paths = await this.getLocalizedPaths({ + collectionSlug: this.collectionSlug, + globalSlug: this.globalSlug, + fields, + incomingPath: sanitizedPath, + }); + } + + const [{ path, field }] = paths; if (path) { - const schemaType = schema.path(path); - const schemaOptions = getSchemaTypeOptions(schemaType); - const formattedValue = sanitizeQueryValue(schemaType, path, operator, val); - - if (!this.overrideAccess && (['salt', 'hash'].includes(path) || schemaType?.options?.hidden)) { - return undefined; - } + const formattedValue = sanitizeQueryValue({ + ctx: this, + field, + path, + operator, + val, + hasCustomID, + }); // If there are multiple collections to search through, // Recursively build up a list of query constraints - if (collectionPaths.length > 1) { + if (paths.length > 1) { // Remove top collection and reverse array // to work backwards from top - const collectionPathsToSearch = collectionPaths.slice(1).reverse(); + const pathsToQuery = paths.slice(1).reverse(); const initialRelationshipQuery = { value: {}, } as SearchParam; - const relationshipQuery = await collectionPathsToSearch.reduce(async (priorQuery, { Model: SubModel, path: subPath }, i) => { + const relationshipQuery = await pathsToQuery.reduce(async (priorQuery, { path: subPath, collectionSlug }, i) => { const priorQueryResult = await priorQuery; + const SubModel = this.req.payload.collections[collectionSlug].Model; + // On the "deepest" collection, // Search on the value passed through the query if (i === 0) { @@ -282,9 +277,9 @@ class ParamParser { const $in = result.map((doc) => doc._id.toString()); - if (collectionPathsToSearch.length === 1) return { path, value: { $in } }; + if (pathsToQuery.length === 1) return { path, value: { $in } }; - const nextSubPath = collectionPathsToSearch[i + 1].path; + const nextSubPath = pathsToQuery[i + 1].path; return { value: { [nextSubPath]: { $in } }, @@ -298,7 +293,7 @@ class ParamParser { // If it is the last recursion // then pass through the search param - if (i + 1 === collectionPathsToSearch.length) { + if (i + 1 === pathsToQuery.length) { return { path, value: { $in } }; } @@ -315,51 +310,6 @@ class ParamParser { 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 === 'number' || (typeof formattedValue === 'string' && mongoose.Types.ObjectId.isValid(formattedValue))) { - query.$or.push({ - [path]: { - [operatorKey]: formattedValue.toString(), - }, - }); - } - - if (typeof formattedValue === 'string') { - if (!Number.isNaN(formattedValue)) { - query.$or.push({ - [path]: { - [operatorKey]: parseFloat(formattedValue), - }, - }); - } - } - } - - // 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) { @@ -377,44 +327,247 @@ class ParamParser { } return undefined; } + + // Build up an array of auto-localized paths to search on + // Multiple paths may be possible if searching on properties of relationship fields + + async getLocalizedPaths({ + collectionSlug, + globalSlug, + fields, + incomingPath, + }: { + collectionSlug?: string + globalSlug?: string + fields: Field[] + incomingPath: string + }): Promise { + const pathSegments = incomingPath.split('.'); + + let paths: PathToQuery[] = [ + { + path: '', + complete: false, + field: undefined, + fields: flattenFields(fields, false), + fieldPolicies: undefined, + collectionSlug, + }, + ]; + + if (!this.overrideAccess) { + if (collectionSlug) { + const collection = { ...this.req.payload.collections[collectionSlug].config }; + collection.fields = fields; + + if (!this.policies.collections[collectionSlug]) { + const [policy, promises] = getEntityPolicies({ + req: this.req, + entity: collection, + operations: ['read'], + type: 'collection', + }); + + await Promise.all(promises); + this.policies.collections[collectionSlug] = policy; + } + + paths[0].fieldPolicies = this.policies.collections[collectionSlug].fields; + + if (['salt', 'hash'].includes(incomingPath) && collection.auth && !collection.auth?.disableLocalStrategy) { + this.errors.push({ path: incomingPath }); + return []; + } + } + + if (globalSlug) { + if (!this.policies.globals[globalSlug]) { + const global = { ...this.req.payload.globals.config.find(({ slug }) => slug === globalSlug) }; + global.fields = fields; + + const [policy, promises] = getEntityPolicies({ + req: this.req, + entity: global, + operations: ['read'], + type: 'global', + }); + + await Promise.all(promises); + this.policies.globals[globalSlug] = policy; + } + + paths[0].fieldPolicies = this.policies.globals[globalSlug].fields; + } + } + + // Use a 'some' so that we can bail out + // if a relationship query is found + // or if Rich Text / JSON + + let done = false; + + for (let i = 0; i < pathSegments.length; i += 1) { + if (done) continue; + + const segment = pathSegments[i]; + + const lastIncompletePath = paths.find(({ complete }) => !complete); + + if (lastIncompletePath) { + const { path } = lastIncompletePath; + let currentPath = path ? `${path}.${segment}` : segment; + + const matchedField = lastIncompletePath.fields.find((field) => fieldAffectsData(field) && field.name === segment); + lastIncompletePath.field = matchedField; + + if (currentPath === 'globalType' && this.globalSlug) { + lastIncompletePath.path = currentPath; + lastIncompletePath.complete = true; + lastIncompletePath.field = { + name: 'globalType', + type: 'text', + }; + + done = true; + continue; + } + + if (matchedField) { + if (!this.overrideAccess) { + const fieldAccess = lastIncompletePath.fieldPolicies[matchedField.name].read.permission; + + if (!fieldAccess || ('hidden' in matchedField && matchedField.hidden)) { + this.errors.push({ path: currentPath }); + done = true; + continue; + } + } + + const nextSegment = pathSegments[i + 1]; + const nextSegmentIsLocale = this.localizationConfig && this.localizationConfig.locales.includes(nextSegment); + + if (nextSegmentIsLocale) { + // Skip the next iteration, because it's a locale + i += 1; + currentPath = `${currentPath}.${nextSegment}`; + } else if ('localized' in matchedField && matchedField.localized) { + currentPath = `${currentPath}.${this.req.locale}`; + } + + switch (matchedField.type) { + case 'blocks': + case 'richText': + case 'json': { + const upcomingSegments = pathSegments.slice(i + 1).join('.'); + lastIncompletePath.complete = true; + lastIncompletePath.path = upcomingSegments ? `${currentPath}.${upcomingSegments}` : currentPath; + done = true; + continue; + } + + case 'relationship': + case 'upload': { + // If this is a polymorphic relation, + // We only support querying directly (no nested querying) + if (typeof matchedField.relationTo !== 'string') { + const lastSegmentIsValid = ['value', 'relationTo'].includes(pathSegments[pathSegments.length - 1]); + + if (lastSegmentIsValid) { + lastIncompletePath.complete = true; + lastIncompletePath.path = pathSegments.join('.'); + } else { + this.errors.push({ path: currentPath }); + done = true; + continue; + } + } else { + lastIncompletePath.complete = true; + lastIncompletePath.collectionSlug = matchedField.relationTo; + lastIncompletePath.path = currentPath; + + const nestedPathToQuery = pathSegments.slice(nextSegmentIsLocale ? i + 2 : i + 1).join('.'); + + if (nestedPathToQuery) { + const relatedCollection = this.req.payload.collections[matchedField.relationTo as string].config; + + const remainingPaths = await this.getLocalizedPaths({ + collectionSlug: relatedCollection.slug, + fields: relatedCollection.fields, + incomingPath: nestedPathToQuery, + }); + + paths = [ + ...paths, + ...remainingPaths, + ]; + } + + done = true; + continue; + } + + break; + } + + default: { + if ('fields' in lastIncompletePath.field) { + lastIncompletePath.fields = flattenFields(lastIncompletePath.field.fields, false); + } + + if (!this.overrideAccess && 'fields' in lastIncompletePath.fieldPolicies[lastIncompletePath.field.name]) { + lastIncompletePath.fieldPolicies = lastIncompletePath.fieldPolicies[lastIncompletePath.field.name].fields; + } + + if (i + 1 === pathSegments.length) lastIncompletePath.complete = true; + lastIncompletePath.path = currentPath; + continue; + } + } + } else { + this.errors.push({ path: currentPath }); + done = true; + continue; + } + } + } + + return paths; + } } type GetBuildQueryPluginArgs = { collectionSlug?: string - globalSlug?: string - isGlobalModel?: boolean - isVersionsModel?: boolean + versionsFields?: Field[] } export type BuildQueryArgs = { req: PayloadRequest where: Where overrideAccess: boolean + globalSlug?: string } // This plugin asynchronously builds a list of Mongoose query constraints // which can then be used in subsequent Mongoose queries. const getBuildQueryPlugin = ({ collectionSlug, - globalSlug, - isGlobalModel, - isVersionsModel, -}: GetBuildQueryPluginArgs) => { + versionsFields, +}: GetBuildQueryPluginArgs = {}) => { return function buildQueryPlugin(schema) { const modifiedSchema = schema; - async function buildQuery({ req, where, overrideAccess = false }: BuildQueryArgs) { + async function buildQuery({ req, where, overrideAccess = false, globalSlug }: BuildQueryArgs): Promise> { const paramParser = new ParamParser({ req, collectionSlug, globalSlug, - isGlobalModel, - isVersionsModel, + versionsFields, model: this, where, overrideAccess, }); - const params = await paramParser.parse(); - return params.searchParams; + const result = await paramParser.parse(); + // TODO: throw errors here + return result; } modifiedSchema.statics.buildQuery = buildQuery; }; diff --git a/src/mongoose/sanitizeFormattedValue.ts b/src/mongoose/sanitizeQueryValue.ts similarity index 74% rename from src/mongoose/sanitizeFormattedValue.ts rename to src/mongoose/sanitizeQueryValue.ts index d538a472e2..845ac4505f 100644 --- a/src/mongoose/sanitizeFormattedValue.ts +++ b/src/mongoose/sanitizeQueryValue.ts @@ -1,27 +1,40 @@ -import mongoose, { SchemaType } from 'mongoose'; +import mongoose from 'mongoose'; import { createArrayFromCommaDelineated } from './createArrayFromCommaDelineated'; -import { getSchemaTypeOptions } from './getSchemaTypeOptions'; import wordBoundariesRegex from '../utilities/wordBoundariesRegex'; +import { Field, TabAsField } from '../fields/config/types'; +import { ParamParser } from './buildQuery'; -export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operator: string, val: any): unknown => { +type SanitizeQueryValueArgs = { + ctx: ParamParser, + field: Field | TabAsField, + path: string, + operator: string, + val: any + hasCustomID: boolean +} + +export const sanitizeQueryValue = ({ ctx, field, path, operator, val, hasCustomID }: SanitizeQueryValueArgs): unknown => { let formattedValue = val; - const schemaOptions = getSchemaTypeOptions(schemaType); // Disregard invalid _ids if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) { - if (schemaType?.instance === 'ObjectID') { + if (!hasCustomID) { const isValid = mongoose.Types.ObjectId.isValid(val); + formattedValue = new mongoose.Types.ObjectId(val); + if (!isValid) { + ctx.errors.push({ path }); return undefined; } } - if (schemaType?.instance === 'Number') { + if (field.type === 'number') { const parsedNumber = parseFloat(val); if (Number.isNaN(parsedNumber)) { + ctx.errors.push({ path }); return undefined; } } @@ -29,17 +42,34 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato // Cast incoming values as proper searchable types - if (schemaType?.instance === 'Boolean' && typeof val === 'string') { + if (field.type === 'checkbox' && typeof val === 'string') { if (val.toLowerCase() === 'true') formattedValue = true; if (val.toLowerCase() === 'false') formattedValue = false; } - if (schemaType?.instance === 'Number' && typeof val === 'string') { + if (field.type === 'number' && typeof val === 'string') { formattedValue = Number(val); } - if ((schemaOptions?.ref || schemaOptions?.refPath) && val === 'null') { - formattedValue = null; + if (['relationship', 'upload'].includes(field.type) && val === 'null') { + if (val === 'null') { + formattedValue = null; + } + + if (operator === 'in' && 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, + ]; + }, []); + } } // Set up specific formatting necessary by operators @@ -74,23 +104,6 @@ export const sanitizeQueryValue = (schemaType: SchemaType, path: string, operato formattedValue = createArrayFromCommaDelineated(formattedValue); } - if (schemaOptions && (schemaOptions.ref || schemaOptions.refPath) && operator === 'in') { - 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 (path !== '_id') { if (operator === 'contains') { formattedValue = { $regex: formattedValue, $options: 'i' }; diff --git a/src/utilities/getEntityPolicies.ts b/src/utilities/getEntityPolicies.ts index 52b465b460..9aeae2ef49 100644 --- a/src/utilities/getEntityPolicies.ts +++ b/src/utilities/getEntityPolicies.ts @@ -1,23 +1,20 @@ import { Access } from '../config/types'; -import { AllOperations, Where, Document } from '../types'; +import { AllOperations, Document, Where } from '../types'; import { FieldAccess, tabHasName } from '../fields/config/types'; -import type { CollectionConfig } from '../collections/config/types'; -import type { GlobalConfig } from '../globals/config/types'; +import type { SanitizedCollectionConfig } from '../collections/config/types'; +import { TypeWithID } from '../collections/config/types'; +import type { SanitizedGlobalConfig } from '../globals/config/types'; import type { PayloadRequest } from '../express/types'; import type { CollectionPermission, GlobalPermission } from '../auth/types'; -import { TypeWithID } from '../collections/config/types'; -type Args = ({ +type Args = { req: PayloadRequest operations: AllOperations[] id?: string -} & ({ - type: 'collection' - entity: CollectionConfig -} | { - type: 'global' - entity: GlobalConfig -})) + type: 'collection' | 'global' + entity: SanitizedCollectionConfig | SanitizedGlobalConfig +} + type ReturnType = T['type'] extends 'global' ? [GlobalPermission, Promise[]] : [CollectionPermission, Promise[]] type CreateAccessPromise = (args: { @@ -111,14 +108,15 @@ export function getEntityPolicies(args: T): ReturnType { } }; - const executeFieldPolicies = ({ - policiesObj = {}, + const executeFieldPolicies = async ({ + policiesObj, fields, operation, + entityAccessPromise, }) => { - const mutablePolicies = policiesObj; + const mutablePolicies = policiesObj.fields; - fields.forEach((field) => { + fields.forEach(async (field) => { if (field.name) { if (!mutablePolicies[field.name]) mutablePolicies[field.name] = {}; @@ -131,17 +129,19 @@ export function getEntityPolicies(args: T): ReturnType { accessLevel: 'field', })); } else { + if (entityAccessPromise) await entityAccessPromise; mutablePolicies[field.name][operation] = { - permission: isLoggedIn, + permission: policiesObj[operation]?.permission, }; } if (field.fields) { if (!mutablePolicies[field.name].fields) mutablePolicies[field.name].fields = {}; executeFieldPolicies({ - policiesObj: mutablePolicies[field.name].fields, + policiesObj: mutablePolicies[field.name], fields: field.fields, operation, + entityAccessPromise, }); } } else if (field.fields) { @@ -149,6 +149,7 @@ export function getEntityPolicies(args: T): ReturnType { policiesObj: mutablePolicies, fields: field.fields, operation, + entityAccessPromise, }); } else if (field.type === 'tabs') { field.tabs.forEach((tab) => { @@ -158,12 +159,14 @@ export function getEntityPolicies(args: T): ReturnType { policiesObj: mutablePolicies[tab.name].fields, fields: tab.fields, operation, + entityAccessPromise, }); } else { executeFieldPolicies({ policiesObj: mutablePolicies, fields: tab.fields, operation, + entityAccessPromise, }); } }); @@ -172,24 +175,28 @@ export function getEntityPolicies(args: T): ReturnType { }; operations.forEach((operation) => { - executeFieldPolicies({ - policiesObj: policies.fields, - fields: entity.fields, - operation, - }); + let entityAccessPromise: Promise; if (typeof entity.access[operation] === 'function') { - promises.push(createAccessPromise({ + entityAccessPromise = createAccessPromise({ policiesObj: policies, access: entity.access[operation], operation, accessLevel: 'entity', - })); + }); + promises.push(entityAccessPromise); } else { policies[operation] = { permission: isLoggedIn, }; } + + executeFieldPolicies({ + policiesObj: policies, + fields: entity.fields, + operation, + entityAccessPromise, + }); }); return [policies, promises] as ReturnType; diff --git a/src/versions/drafts/replaceWithDraftIfAvailable.ts b/src/versions/drafts/replaceWithDraftIfAvailable.ts index d367fe6150..f1531f4239 100644 --- a/src/versions/drafts/replaceWithDraftIfAvailable.ts +++ b/src/versions/drafts/replaceWithDraftIfAvailable.ts @@ -63,6 +63,7 @@ const replaceWithDraftIfAvailable = async ({ where: queryToBuild, req, overrideAccess, + globalSlug: entityType === 'global' ? entity.slug : undefined, }); let draft = await VersionModel.findOne(query, {}, { diff --git a/test/globals/config.ts b/test/globals/config.ts index 4ea54823a5..da911f86eb 100644 --- a/test/globals/config.ts +++ b/test/globals/config.ts @@ -4,6 +4,8 @@ import { buildConfig } from '../buildConfig'; export const slug = 'global'; export const arraySlug = 'array'; +export const accessControlSlug = 'access-control'; + export const englishLocale = 'en'; export const spanishLocale = 'es'; @@ -51,6 +53,33 @@ export default buildConfig({ }, ], }, + { + slug: accessControlSlug, + access: { + read: ({ req: { user } }) => { + if (user) { + return true; + } + + return { + enabled: { + equals: true, + }, + }; + }, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'enabled', + type: 'checkbox', + }, + ], + }, ], onInit: async (payload) => { await payload.create({ @@ -60,5 +89,12 @@ export default buildConfig({ password: devUser.password, }, }); + + await payload.updateGlobal({ + slug: accessControlSlug, + data: { + title: 'hello', + }, + }); }, }); diff --git a/test/globals/int.spec.ts b/test/globals/int.spec.ts index 7bd009e208..8500a41d02 100644 --- a/test/globals/int.spec.ts +++ b/test/globals/int.spec.ts @@ -1,6 +1,6 @@ import { GraphQLClient } from 'graphql-request'; import { initPayloadTest } from '../helpers/configHelpers'; -import configPromise, { arraySlug, englishLocale, slug, spanishLocale } from './config'; +import configPromise, { accessControlSlug, arraySlug, englishLocale, slug, spanishLocale } from './config'; import payload from '../../src'; import { RESTClient } from '../helpers/rest'; @@ -144,6 +144,29 @@ describe('globals', () => { expect(en).toMatchObject(localized.en); expect(es).toMatchObject(localized.es); }); + + it('should respect valid access query constraint', async () => { + const emptyGlobal = await payload.findGlobal({ + slug: accessControlSlug, + overrideAccess: false, + }); + + expect(Object.keys(emptyGlobal)).toHaveLength(0); + + await payload.updateGlobal({ + slug: accessControlSlug, + data: { + enabled: true, + }, + }); + + const hasAccess = await payload.findGlobal({ + slug: accessControlSlug, + overrideAccess: false, + }); + + expect(hasAccess.title).toBeDefined(); + }); }); describe('graphql', () => { diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 508c25302a..751971ef3b 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -5,11 +5,7 @@ import payload from '../../src'; import type { LocalizedPost, WithLocalizedRelationship, - LocalizedRequired, - RelationshipLocalized, - GlobalArray, } from './payload-types'; -import type { LocalizedPostAllLocale } from './config'; import configPromise, { relationshipLocalizedSlug, slug, withLocalizedRelSlug, withRequiredLocalizedFields } from './config'; import { defaultLocale, From 995054d46bc8c4c5f02c78b1c4d0d6155d449b75 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 17 Apr 2023 16:25:07 -0400 Subject: [PATCH 3/7] chore: adds tests to validate queries --- src/errors/QueryError.ts | 16 ++ src/mongoose/buildQuery.ts | 11 +- test/collections-rest/config.ts | 48 +++- test/collections-rest/int.spec.ts | 382 ++++++++++++++++++++++++------ test/helpers/rest.ts | 14 +- 5 files changed, 383 insertions(+), 88 deletions(-) create mode 100644 src/errors/QueryError.ts diff --git a/src/errors/QueryError.ts b/src/errors/QueryError.ts new file mode 100644 index 0000000000..6d254267a3 --- /dev/null +++ b/src/errors/QueryError.ts @@ -0,0 +1,16 @@ +import httpStatus from 'http-status'; +import type { TFunction } from 'i18next'; +import APIError from './APIError'; + +class QueryError extends APIError { + constructor(results: { path: string }[], t?: TFunction) { + const message = t ? t('error:unspecific', { count: results.length }) : `The following path${results.length === 1 ? '' : 's'} cannot be queried:`; + super( + `${message} ${results.map((err) => err.path).join(', ')}`, + httpStatus.BAD_REQUEST, + results, + ); + } +} + +export default QueryError; diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 86d51d67b4..531a26971d 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -12,6 +12,7 @@ import { CollectionPermission, FieldPermissions, GlobalPermission } from '../aut import flattenFields from '../utilities/flattenTopLevelFields'; import { getEntityPolicies } from '../utilities/getEntityPolicies'; import { SanitizedConfig } from '../config/types'; +import QueryError from '../errors/QueryError'; const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near']; @@ -46,8 +47,6 @@ type ParamParserArgs = { overrideAccess?: boolean } -type QueryError = { path: string } - export class ParamParser { collectionSlug?: string @@ -74,7 +73,7 @@ export class ParamParser { }; } - errors: QueryError[] + errors: { path: string }[] constructor({ req, @@ -566,7 +565,11 @@ const getBuildQueryPlugin = ({ overrideAccess, }); const result = await paramParser.parse(); - // TODO: throw errors here + + if (this.errors.length > 0) { + throw new QueryError(this.errors); + } + return result; } modifiedSchema.statics.buildQuery = buildQuery; diff --git a/test/collections-rest/config.ts b/test/collections-rest/config.ts index cb60c5382c..0ece14ea68 100644 --- a/test/collections-rest/config.ts +++ b/test/collections-rest/config.ts @@ -35,6 +35,29 @@ export const customIdNumberSlug = 'custom-id-number'; export const errorOnHookSlug = 'error-on-hooks'; export default buildConfig({ + endpoints: [ + { + path: '/send-test-email', + method: 'get', + handler: async (req, res) => { + await req.payload.sendEmail({ + from: 'dev@payloadcms.com', + to: devUser.email, + subject: 'Test Email', + html: 'This is a test email.', + // to recreate a failing email transport, add the following credentials + // to the `email` property of `payload.init()` in `../dev.ts` + // the app should fail to send the email, but the error should be handled without crashing the app + // transportOptions: { + // host: 'smtp.ethereal.email', + // port: 587, + // }, + }); + + res.status(200).send('Email sent'); + }, + }, + ], collections: [ { slug, @@ -78,6 +101,13 @@ export default buildConfig({ relationTo: [relationSlug, 'dummy'], hasMany: true, }, + { + name: 'restrictedField', + type: 'text', + access: { + read: () => false, + }, + }, ], }, { @@ -91,7 +121,23 @@ export default buildConfig({ ], }, collectionWithName(relationSlug), - collectionWithName('dummy'), + { + slug: 'dummy', + access: openAccess, + fields: [ + { + type: 'text', + name: 'title', + }, + { + type: 'text', + name: 'name', + access: { + read: () => false, + }, + }, + ], + }, { slug: customIdSlug, access: openAccess, diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index 119f7b3fe6..b9803520c9 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import { randomBytes } from 'crypto'; import { initPayloadTest } from '../helpers/configHelpers'; import type { Relation } from './config'; -import config, { customIdNumberSlug, customIdSlug, slug, relationSlug, pointSlug, errorOnHookSlug } from './config'; +import config, { customIdNumberSlug, customIdSlug, errorOnHookSlug, pointSlug, relationSlug, slug } from './config'; import payload from '../../src'; import { RESTClient } from '../helpers/rest'; import type { ErrorOnHook, Post } from './payload-types'; @@ -49,10 +49,16 @@ describe('collections-rest', () => { }); it('should update existing', async () => { - const { id, description } = await createPost({ description: 'desc' }); + const { + id, + description, + } = await createPost({ description: 'desc' }); const updatedTitle = 'updated-title'; - const { status, doc: updated } = await client.update({ + const { + status, + doc: updated, + } = await client.update({ id, data: { title: updatedTitle }, }); @@ -62,98 +68,305 @@ describe('collections-rest', () => { expect(updated.description).toEqual(description); // Check was not modified }); - it('should bulk update', async () => { - await mapAsync([...Array(11)], async (_, i) => { - await createPost({ description: `desc ${i}` }); + describe('Bulk operations', () => { + it('should bulk update', async () => { + await mapAsync([...Array(11)], async (_, i) => { + await createPost({ description: `desc ${i}` }); + }); + + const description = 'updated'; + const { + status, + docs, + } = await client.updateMany({ + where: { title: { equals: 'title' } }, + data: { description }, + }); + + expect(status).toEqual(200); + expect(docs[0].title).toEqual('title'); // Check was not modified + expect(docs[0].description).toEqual(description); + expect(docs.pop().description).toEqual(description); }); - const description = 'updated'; - const { status, docs } = await client.updateMany({ - query: { title: { equals: 'title' } }, - data: { description }, + it('should not bulk update with a bad query', async () => { + await mapAsync([...Array(2)], async (_, i) => { + await createPost({ description: `desc ${i}` }); + }); + + const description = 'updated'; + + const { status, docs: noDocs, errors } = await client.updateMany({ + where: { missing: { equals: 'title' } }, + data: { description }, + }); + + expect(status).toEqual(400); + expect(noDocs).toBeUndefined(); + expect(errors).toHaveLength(1); + + const { docs } = await payload.find({ + collection: slug, + }); + + expect(docs[0].description).not.toEqual(description); + expect(docs.pop().description).not.toEqual(description); }); - expect(status).toEqual(200); - expect(docs[0].title).toEqual('title'); // Check was not modified - expect(docs[0].description).toEqual(description); - expect(docs.pop().description).toEqual(description); - }); + it('should not bulk update with a bad relationship query', async () => { + await mapAsync([...Array(2)], async (_, i) => { + await createPost({ description: `desc ${i}` }); + }); - it('should return formatted errors for bulk updates', async () => { - const text = 'bulk-update-test-errors'; - const errorDoc = await payload.create({ - collection: errorOnHookSlug, - data: { - text, - errorBeforeChange: true, - }, - }); - const successDoc = await payload.create({ - collection: errorOnHookSlug, - data: { - text, - errorBeforeChange: false, - }, + const description = 'updated'; + const { status: relationFieldStatus, docs: relationFieldDocs, errors: relationFieldErrors } = await client.updateMany({ + where: { 'relationField.missing': { equals: 'title' } }, + data: { description }, + }); + + console.log({ relationFieldStatus, relationFieldDocs, relationFieldErrors }); + + const { status: relationMultiRelationToStatus } = await client.updateMany({ + where: { 'relationMultiRelationTo.missing': { equals: 'title' } }, + data: { description }, + }); + + const { docs } = await payload.find({ + collection: slug, + }); + + expect(relationFieldStatus).toEqual(400); + expect(relationMultiRelationToStatus).toEqual(400); + expect(docs[0].description).not.toEqual(description); + expect(docs.pop().description).not.toEqual(description); }); - const update = 'update'; + it('should not bulk update with a read restricted field query', async () => { + const { id } = await payload.create({ + collection: slug, + data: { + restrictedField: 'restricted', + }, + }); - const result = await client.updateMany({ - slug: errorOnHookSlug, - query: { text: { equals: text } }, - data: { text: update }, + const description = 'description'; + const { status } = await client.updateMany({ + query: { restrictedField: { equals: 'restricted' } }, + data: { description }, + }); + + const doc = await payload.findByID({ + collection: slug, + id, + }); + + expect(status).toEqual(400); + expect(doc.description).toBeUndefined(); }); - expect(result.status).toEqual(400); - expect(result.docs).toHaveLength(1); - expect(result.docs[0].id).toEqual(successDoc.id); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].message).toBeDefined(); - expect(result.errors[0].id).toEqual(errorDoc.id); - expect(result.docs[0].text).toEqual(update); - }); + it('should bulk update with a relationship field that exists in one collection and not another', async () => { + const relationOne = await payload.create({ + collection: 'dummy', + data: { + title: 'title', + }, + }); + const relationTwo = await payload.create({ + collection: 'relation', + data: { + name: 'name', + }, + }); - it('should bulk delete', async () => { - const count = 11; - await mapAsync([...Array(count)], async (_, i) => { - await createPost({ description: `desc ${i}` }); + const description = 'desc'; + const relationPost = await payload.create({ + collection: slug, + data: { + description, + relationMultiRelationTo: { + value: relationTwo.id, + relationTo: 'relation', + }, + }, + }); + + const relationToDummyPost = await payload.create({ + collection: slug, + data: { + description, + relationMultiRelationTo: { + value: relationOne.id, + relationTo: 'dummy', + }, + }, + }); + + const updatedDescription = 'updated'; + + const { status: relationMultiRelationToStatus, docs: updated } = await client.updateMany({ + where: { + 'relationMultiRelationTo.title': { + equals: relationOne.title, + }, + }, + data: { description: updatedDescription }, + }); + + const updatedDoc = await payload.findByID({ + collection: slug, + id: relationToDummyPost.id, + }); + + const otherDoc = await payload.findByID({ + collection: slug, + id: relationPost.id, + }); + + expect(relationMultiRelationToStatus).toEqual(200); + expect(updated).toHaveLength(1); + expect(updated[0].id).toEqual(relationToDummyPost.id); + expect(updatedDoc.description).toEqual(updatedDescription); + expect(otherDoc.description).toEqual(description); }); - const { status, docs } = await client.deleteMany({ - query: { title: { eq: 'title' } }, + it('should bulk update with a relationship field that exists in one collection and is restricted in another', async () => { + const name = 'name'; + const relationOne = await payload.create({ + collection: 'dummy', + data: { + title: 'title', + name, // read access: () => false + }, + }); + const relationTwo = await payload.create({ + collection: 'relation', + data: { + name, + }, + }); + + const description = 'desc'; + const relationPost = await payload.create({ + collection: slug, + data: { + description, + relationMultiRelationTo: { + value: relationTwo.id, + relationTo: 'relation', + }, + }, + }); + + const relationToDummyPost = await payload.create({ + collection: slug, + data: { + description, + relationMultiRelationTo: { + value: relationOne.id, + relationTo: 'dummy', + }, + }, + }); + + const updatedDescription = 'updated'; + + const { status } = await client.updateMany({ + where: { 'relationMultiRelationTo.name': { equals: name } }, + data: { description: updatedDescription }, + }); + + + const updatedDoc = await payload.findByID({ + collection: slug, + id: relationPost.id, + }); + + const otherDoc = await payload.findByID({ + collection: slug, + id: relationToDummyPost.id, + }); + + expect(status).toEqual(200); + expect(updatedDoc.description).toEqual(updatedDescription); + expect(otherDoc.description).toEqual(description); }); - expect(status).toEqual(200); - expect(docs[0].title).toEqual('title'); // Check was not modified - expect(docs).toHaveLength(count); - }); + it('should return formatted errors for bulk updates', async () => { + const text = 'bulk-update-test-errors'; + const errorDoc = await payload.create({ + collection: errorOnHookSlug, + data: { + text, + errorBeforeChange: true, + }, + }); + const successDoc = await payload.create({ + collection: errorOnHookSlug, + data: { + text, + errorBeforeChange: false, + }, + }); - it('should return formatted errors for bulk deletes', async () => { - await payload.create({ - collection: errorOnHookSlug, - data: { - text: 'test', - errorAfterDelete: true, - }, - }); - await payload.create({ - collection: errorOnHookSlug, - data: { - text: 'test', - errorAfterDelete: false, - }, + const update = 'update'; + + const result = await client.updateMany({ + slug: errorOnHookSlug, + where: { text: { equals: text } }, + data: { text: update }, + }); + + expect(result.status).toEqual(400); + expect(result.docs).toHaveLength(1); + expect(result.docs[0].id).toEqual(successDoc.id); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].message).toBeDefined(); + expect(result.errors[0].id).toEqual(errorDoc.id); + expect(result.docs[0].text).toEqual(update); }); - const result = await client.deleteMany({ - slug: errorOnHookSlug, - query: { text: { equals: 'test' } }, + it('should bulk delete', async () => { + const count = 11; + await mapAsync([...Array(count)], async (_, i) => { + await createPost({ description: `desc ${i}` }); + }); + + const { status, docs } = await client.deleteMany({ + where: { title: { eq: 'title' } }, + }); + + expect(status).toEqual(200); + expect(docs[0].title).toEqual('title'); // Check was not modified + expect(docs).toHaveLength(count); }); - expect(result.status).toEqual(400); - expect(result.docs).toHaveLength(1); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].message).toBeDefined(); - expect(result.errors[0].id).toBeDefined(); + it('should return formatted errors for bulk deletes', async () => { + await payload.create({ + collection: errorOnHookSlug, + data: { + text: 'test', + errorAfterDelete: true, + }, + }); + await payload.create({ + collection: errorOnHookSlug, + data: { + text: 'test', + errorAfterDelete: false, + }, + }); + + const result = await client.deleteMany({ + slug: errorOnHookSlug, + where: { text: { equals: 'test' } }, + }); + + expect(result.status).toEqual(400); + expect(result.docs).toHaveLength(1); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].message).toBeDefined(); + expect(result.errors[0].id).toBeDefined(); + }); }); describe('Custom ID', () => { @@ -342,7 +555,24 @@ describe('collections-rest', () => { expect(result.totalDocs).toEqual(1); }); - it.todo('nested by property value'); + it('nested by property value', async () => { + const post1 = await createPost({ + relationMultiRelationTo: { relationTo: relationSlug, value: relation.id }, + }); + await createPost(); + + const { status, result } = await client.find({ + query: { + 'relationMultiRelationTo.value.name': { + equals: relation.name, + }, + }, + }); + + expect(status).toEqual(200); + expect(result.docs).toEqual([post1]); + expect(result.totalDocs).toEqual(1); + }); }); describe('relationTo multi hasMany', () => { diff --git a/test/helpers/rest.ts b/test/helpers/rest.ts index 36867a93b8..b8dfba3267 100644 --- a/test/helpers/rest.ts +++ b/test/helpers/rest.ts @@ -57,7 +57,7 @@ type UpdateManyArgs = { slug?: string; data: Partial; auth?: boolean; - query: any; + where: any; }; type DeleteArgs = { @@ -69,7 +69,7 @@ type DeleteArgs = { type DeleteManyArgs = { slug?: string; auth?: boolean; - query: any; + where: any; }; type FindGlobalArgs = { @@ -92,7 +92,7 @@ type DocResponse = { type DocsResponse = { status: number; docs: T[]; - errors?: { name: string, message: string, data: any, id: string | number}[] + errors?: { name: string, message: string, data: any, id: string | number }[] }; const headers = { @@ -205,9 +205,9 @@ export class RESTClient { } async updateMany(args: UpdateManyArgs): Promise> { - const { slug, data, query } = args; + const { slug, data, where } = args; const formattedQs = qs.stringify({ - ...(query ? { where: query } : {}), + ...(where ? { where } : {}), }, { addQueryPrefix: true, }); @@ -225,9 +225,9 @@ export class RESTClient { } async deleteMany(args: DeleteManyArgs): Promise> { - const { slug, query } = args; + const { slug, where } = args; const formattedQs = qs.stringify({ - ...(query ? { where: query } : {}), + ...(where ? { where } : {}), }, { addQueryPrefix: true, }); From 959a5d78c7b6b1690f226250a232dd4b052819fe Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Mon, 17 Apr 2023 16:43:00 -0400 Subject: [PATCH 4/7] chore: test coverage queries --- src/mongoose/buildQuery.ts | 6 +- test/collections-rest/int.spec.ts | 156 +----------------------------- 2 files changed, 8 insertions(+), 154 deletions(-) diff --git a/src/mongoose/buildQuery.ts b/src/mongoose/buildQuery.ts index 531a26971d..9c2f87b9e5 100644 --- a/src/mongoose/buildQuery.ts +++ b/src/mongoose/buildQuery.ts @@ -7,7 +7,7 @@ import { combineMerge } from '../utilities/combineMerge'; import { operatorMap } from './operatorMap'; import { sanitizeQueryValue } from './sanitizeQueryValue'; import { PayloadRequest, Where } from '../types'; -import { Field, FieldAffectingData, TabAsField, UIField, fieldAffectsData } from '../fields/config/types'; +import { Field, FieldAffectingData, fieldAffectsData, TabAsField, UIField } from '../fields/config/types'; import { CollectionPermission, FieldPermissions, GlobalPermission } from '../auth'; import flattenFields from '../utilities/flattenTopLevelFields'; import { getEntityPolicies } from '../utilities/getEntityPolicies'; @@ -566,8 +566,8 @@ const getBuildQueryPlugin = ({ }); const result = await paramParser.parse(); - if (this.errors.length > 0) { - throw new QueryError(this.errors); + if (paramParser.errors.length > 0) { + throw new QueryError(paramParser.errors); } return result; diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index b9803520c9..54058b259c 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -124,8 +124,6 @@ describe('collections-rest', () => { data: { description }, }); - console.log({ relationFieldStatus, relationFieldDocs, relationFieldErrors }); - const { status: relationMultiRelationToStatus } = await client.updateMany({ where: { 'relationMultiRelationTo.missing': { equals: 'title' } }, data: { description }, @@ -150,8 +148,8 @@ describe('collections-rest', () => { }); const description = 'description'; - const { status } = await client.updateMany({ - query: { restrictedField: { equals: 'restricted' } }, + const result = await client.updateMany({ + where: { restrictedField: { equals: 'restricted' } }, data: { description }, }); @@ -160,137 +158,12 @@ describe('collections-rest', () => { id, }); - expect(status).toEqual(400); + expect(result.status).toEqual(400); + expect(result.errors).toHaveLength(1); + expect(result.errors[0].message).toEqual('The following path cannot be queried: restrictedField'); expect(doc.description).toBeUndefined(); }); - it('should bulk update with a relationship field that exists in one collection and not another', async () => { - const relationOne = await payload.create({ - collection: 'dummy', - data: { - title: 'title', - }, - }); - const relationTwo = await payload.create({ - collection: 'relation', - data: { - name: 'name', - }, - }); - - const description = 'desc'; - const relationPost = await payload.create({ - collection: slug, - data: { - description, - relationMultiRelationTo: { - value: relationTwo.id, - relationTo: 'relation', - }, - }, - }); - - const relationToDummyPost = await payload.create({ - collection: slug, - data: { - description, - relationMultiRelationTo: { - value: relationOne.id, - relationTo: 'dummy', - }, - }, - }); - - const updatedDescription = 'updated'; - - const { status: relationMultiRelationToStatus, docs: updated } = await client.updateMany({ - where: { - 'relationMultiRelationTo.title': { - equals: relationOne.title, - }, - }, - data: { description: updatedDescription }, - }); - - const updatedDoc = await payload.findByID({ - collection: slug, - id: relationToDummyPost.id, - }); - - const otherDoc = await payload.findByID({ - collection: slug, - id: relationPost.id, - }); - - expect(relationMultiRelationToStatus).toEqual(200); - expect(updated).toHaveLength(1); - expect(updated[0].id).toEqual(relationToDummyPost.id); - expect(updatedDoc.description).toEqual(updatedDescription); - expect(otherDoc.description).toEqual(description); - }); - - it('should bulk update with a relationship field that exists in one collection and is restricted in another', async () => { - const name = 'name'; - const relationOne = await payload.create({ - collection: 'dummy', - data: { - title: 'title', - name, // read access: () => false - }, - }); - const relationTwo = await payload.create({ - collection: 'relation', - data: { - name, - }, - }); - - const description = 'desc'; - const relationPost = await payload.create({ - collection: slug, - data: { - description, - relationMultiRelationTo: { - value: relationTwo.id, - relationTo: 'relation', - }, - }, - }); - - const relationToDummyPost = await payload.create({ - collection: slug, - data: { - description, - relationMultiRelationTo: { - value: relationOne.id, - relationTo: 'dummy', - }, - }, - }); - - const updatedDescription = 'updated'; - - const { status } = await client.updateMany({ - where: { 'relationMultiRelationTo.name': { equals: name } }, - data: { description: updatedDescription }, - }); - - - const updatedDoc = await payload.findByID({ - collection: slug, - id: relationPost.id, - }); - - const otherDoc = await payload.findByID({ - collection: slug, - id: relationToDummyPost.id, - }); - - expect(status).toEqual(200); - expect(updatedDoc.description).toEqual(updatedDescription); - expect(otherDoc.description).toEqual(description); - }); - it('should return formatted errors for bulk updates', async () => { const text = 'bulk-update-test-errors'; const errorDoc = await payload.create({ @@ -554,25 +427,6 @@ describe('collections-rest', () => { expect(result.docs).toEqual([post1]); expect(result.totalDocs).toEqual(1); }); - - it('nested by property value', async () => { - const post1 = await createPost({ - relationMultiRelationTo: { relationTo: relationSlug, value: relation.id }, - }); - await createPost(); - - const { status, result } = await client.find({ - query: { - 'relationMultiRelationTo.value.name': { - equals: relation.name, - }, - }, - }); - - expect(status).toEqual(200); - expect(result.docs).toEqual([post1]); - expect(result.totalDocs).toEqual(1); - }); }); describe('relationTo multi hasMany', () => { From 1a681dd97bb5a8b9999cc19d61941490c3cf3403 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 17 Apr 2023 16:46:08 -0400 Subject: [PATCH 5/7] chore: revises outdated tests --- test/uploads/int.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/uploads/int.spec.ts b/test/uploads/int.spec.ts index ec52be6532..6387028882 100644 --- a/test/uploads/int.spec.ts +++ b/test/uploads/int.spec.ts @@ -198,7 +198,7 @@ describe('Collections - Uploads', () => { const { status } = await client.updateMany({ // id: mediaDoc.id, - query: { + where: { id: { equals: mediaDoc.id }, }, file: true, @@ -419,7 +419,7 @@ describe('Collections - Uploads', () => { const { errors } = await client.deleteMany({ slug: mediaSlug, - query: { + where: { id: { equals: doc.id }, }, auth: true, From 086feafcb7d73128c741320a23ad9049a897ce9f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 17 Apr 2023 16:56:38 -0400 Subject: [PATCH 6/7] chore: cleans duplicative logic --- src/mongoose/sanitizeQueryValue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mongoose/sanitizeQueryValue.ts b/src/mongoose/sanitizeQueryValue.ts index 845ac4505f..5b5e88df45 100644 --- a/src/mongoose/sanitizeQueryValue.ts +++ b/src/mongoose/sanitizeQueryValue.ts @@ -51,7 +51,7 @@ export const sanitizeQueryValue = ({ ctx, field, path, operator, val, hasCustomI formattedValue = Number(val); } - if (['relationship', 'upload'].includes(field.type) && val === 'null') { + if (['relationship', 'upload'].includes(field.type)) { if (val === 'null') { formattedValue = null; } From 3f9bbe90bd460ce75a661bcb00e1510680837676 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 17 Apr 2023 19:37:22 -0400 Subject: [PATCH 7/7] fix: incorrect version defaults --- .../utilities/DocumentInfo/index.tsx | 23 ++++++++++++------- src/collections/config/sanitize.ts | 9 ++++---- src/globals/config/sanitize.ts | 9 ++++---- src/utilities/getEntityPolicies.ts | 6 ++--- src/versions/defaults.ts | 17 -------------- test/access-control/e2e.spec.ts | 2 +- 6 files changed, 29 insertions(+), 37 deletions(-) delete mode 100644 src/versions/defaults.ts diff --git a/src/admin/components/utilities/DocumentInfo/index.tsx b/src/admin/components/utilities/DocumentInfo/index.tsx index 8e99ca5895..c50cc75886 100644 --- a/src/admin/components/utilities/DocumentInfo/index.tsx +++ b/src/admin/components/utilities/DocumentInfo/index.tsx @@ -58,6 +58,7 @@ export const DocumentInfoProvider: React.FC = ({ const getVersions = useCallback(async () => { let versionFetchURL; let publishedFetchURL; + let draftsEnabled = false; let shouldFetchVersions = false; let unpublishedVersionJSON = null; let versionJSON = null; @@ -93,12 +94,14 @@ export const DocumentInfoProvider: React.FC = ({ }; if (global) { + draftsEnabled = Boolean(global?.versions?.drafts); shouldFetchVersions = Boolean(global?.versions); versionFetchURL = `${baseURL}/globals/${global.slug}/versions`; publishedFetchURL = `${baseURL}/globals/${global.slug}?${qs.stringify(publishedVersionParams)}`; } if (collection) { + draftsEnabled = Boolean(collection?.versions?.drafts); shouldFetchVersions = Boolean(collection?.versions); versionFetchURL = `${baseURL}/${collection.slug}/versions`; @@ -122,15 +125,19 @@ export const DocumentInfoProvider: React.FC = ({ } if (shouldFetch) { - let publishedJSON = await fetch(publishedFetchURL, { - credentials: 'include', - headers: { - 'Accept-Language': i18n.language, - }, - }).then((res) => res.json()); + let publishedJSON; - if (collection) { - publishedJSON = publishedJSON?.docs?.[0]; + if (draftsEnabled) { + publishedJSON = await fetch(publishedFetchURL, { + credentials: 'include', + headers: { + 'Accept-Language': i18n.language, + }, + }).then((res) => res.json()); + + if (collection) { + publishedJSON = publishedJSON?.docs?.[0]; + } } if (shouldFetchVersions) { diff --git a/src/collections/config/sanitize.ts b/src/collections/config/sanitize.ts index c635e3fc27..fbf18f4ec1 100644 --- a/src/collections/config/sanitize.ts +++ b/src/collections/config/sanitize.ts @@ -10,7 +10,6 @@ import getBaseUploadFields from '../../uploads/getBaseFields'; import { formatLabels } from '../../utilities/formatLabels'; import { defaults, authDefaults } from './defaults'; import { Config } from '../../config/types'; -import { versionCollectionDefaults } from '../../versions/defaults'; import baseVersionFields from '../../versions/baseFields'; import TimestampsRequired from '../../errors/TimestampsRequired'; import mergeBaseFields from '../../fields/mergeBaseFields'; @@ -40,12 +39,14 @@ const sanitizeCollection = (config: Config, collection: CollectionConfig): Sanit }; } - if (sanitized.versions.drafts.autosave === true) sanitized.versions.drafts.autosave = {}; + if (sanitized.versions.drafts.autosave === true) { + sanitized.versions.drafts.autosave = { + interval: 2000, + }; + } sanitized.fields = mergeBaseFields(sanitized.fields, baseVersionFields); } - - sanitized.versions = merge(versionCollectionDefaults, sanitized.versions); } if (sanitized.upload) { diff --git a/src/globals/config/sanitize.ts b/src/globals/config/sanitize.ts index bd70787a50..321f79530f 100644 --- a/src/globals/config/sanitize.ts +++ b/src/globals/config/sanitize.ts @@ -6,7 +6,6 @@ import { GlobalConfig, SanitizedGlobalConfig } from './types'; import defaultAccess from '../../auth/defaultAccess'; import baseVersionFields from '../../versions/baseFields'; import mergeBaseFields from '../../fields/mergeBaseFields'; -import { versionGlobalDefaults } from '../../versions/defaults'; const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[]): SanitizedGlobalConfig[] => { const sanitizedGlobals = globals.map((global) => { @@ -42,12 +41,14 @@ const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[ }; } - if (sanitizedGlobal.versions.drafts.autosave === true) sanitizedGlobal.versions.drafts.autosave = {}; + if (sanitizedGlobal.versions.drafts.autosave === true) { + sanitizedGlobal.versions.drafts.autosave = { + interval: 2000, + }; + } sanitizedGlobal.fields = mergeBaseFields(sanitizedGlobal.fields, baseVersionFields); } - - sanitizedGlobal.versions = merge(versionGlobalDefaults, sanitizedGlobal.versions); } // ///////////////////////////////// diff --git a/src/utilities/getEntityPolicies.ts b/src/utilities/getEntityPolicies.ts index 9aeae2ef49..ae1bf6f10d 100644 --- a/src/utilities/getEntityPolicies.ts +++ b/src/utilities/getEntityPolicies.ts @@ -146,7 +146,7 @@ export function getEntityPolicies(args: T): ReturnType { } } else if (field.fields) { executeFieldPolicies({ - policiesObj: mutablePolicies, + policiesObj, fields: field.fields, operation, entityAccessPromise, @@ -156,14 +156,14 @@ export function getEntityPolicies(args: T): ReturnType { if (tabHasName(tab)) { if (!mutablePolicies[tab.name]) mutablePolicies[tab.name] = { fields: {} }; executeFieldPolicies({ - policiesObj: mutablePolicies[tab.name].fields, + policiesObj: mutablePolicies[tab.name], fields: tab.fields, operation, entityAccessPromise, }); } else { executeFieldPolicies({ - policiesObj: mutablePolicies, + policiesObj, fields: tab.fields, operation, entityAccessPromise, diff --git a/src/versions/defaults.ts b/src/versions/defaults.ts deleted file mode 100644 index 7825e65491..0000000000 --- a/src/versions/defaults.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IncomingCollectionVersions, IncomingGlobalVersions } from './types'; - -export const versionCollectionDefaults: IncomingCollectionVersions = { - drafts: { - autosave: { - interval: 2000, // in milliseconds - }, - }, -}; - -export const versionGlobalDefaults: IncomingGlobalVersions = { - drafts: { - autosave: { - interval: 2000, // in milliseconds - }, - }, -}; diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index 62e5c9529b..bc9a204522 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -219,7 +219,7 @@ describe('access control', () => { async function createDoc(data: any): Promise<{ id: string }> { return payload.create({ - collection: docLevelAccessSlug, + collection: slug, data, }); }