Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/queries/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,34 +63,34 @@ The following operators are available for use in queries:
field immensely. [More details](../database/indexes).
</Banner>

### And / Or Logic
### Logical Operators (And / Or / Not)

In addition to defining simple queries, you can join multiple queries together using AND / OR logic. These can be nested as deeply as you need to create complex queries.
In addition to defining simple queries, you can join multiple queries together using AND / OR / NOT logic. These can be nested as deeply as you need to create complex queries.

To join queries, use the `and` or `or` keys in your query object:
To join queries, use the `and`, `or`, or `not` keys in your query object:

```ts
import type { Where } from 'payload'

const query: Where = {
or: [
// highlight-line
{
color: {
equals: 'mint',
},
},
{
and: [
// highlight-line
{
color: {
equals: 'white',
},
},
{
featured: {
equals: false,
not: {
featured: {
equals: true,
},
},
},
],
Expand All @@ -99,7 +99,7 @@ const query: Where = {
}
```

Written in plain English, if the above query were passed to a `find` operation, it would translate to finding posts where either the `color` is `mint` OR the `color` is `white` AND `featured` is set to false.
Written in plain English, if the above query were passed to a `find` operation, it would translate to finding posts where either the `color` is `mint` OR the `color` is `white` AND `featured` is NOT set to true.

### Nested properties

Expand Down
17 changes: 16 additions & 1 deletion packages/db-mongodb/src/queries/parseParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ export async function parseParams({
// We need to determine if the whereKey is an AND, OR, or a schema path
for (const relationOrPath of Object.keys(where)) {
const condition = where[relationOrPath]
let conditionOperator: '$and' | '$or' | null = null
let conditionOperator: '$and' | '$not' | '$or' | null = null
if (relationOrPath.toLowerCase() === 'and') {
conditionOperator = '$and'
} else if (relationOrPath.toLowerCase() === 'or') {
conditionOperator = '$or'
} else if (relationOrPath.toLowerCase() === 'not') {
conditionOperator = '$not'
}
if (Array.isArray(condition)) {
const builtConditions = await buildAndOrConditions({
Expand All @@ -49,6 +51,19 @@ export async function parseParams({
if (builtConditions.length > 0 && conditionOperator !== null) {
result[conditionOperator] = builtConditions
}
} else if (conditionOperator === '$not' && typeof condition === 'object') {
const builtCondition = await parseParams({
collectionSlug,
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition as Where,
})
if (Object.keys(builtCondition).length > 0) {
result.$nor = [builtCondition]
}
} else {
// It's a path - and there can be multiple comparisons on a single path.
// For example - title like 'test' and title not equal to 'tester'
Expand Down
25 changes: 22 additions & 3 deletions packages/drizzle/src/queries/parseParams.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SQL, Table } from 'drizzle-orm'
import type { FlattenedField, Operator, Sort, Where } from 'payload'

import { and, getTableName, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
import { and, getTableName, isNotNull, isNull, ne, not, notInArray, or, sql } from 'drizzle-orm'
import { PgUUID } from 'drizzle-orm/pg-core'
import { APIError, QueryError } from 'payload'
import { validOperatorSet } from 'payload/shared'
Expand Down Expand Up @@ -54,11 +54,13 @@ export function parseParams({
for (const relationOrPath of Object.keys(where)) {
if (relationOrPath) {
const condition = where[relationOrPath]
let conditionOperator: typeof and | typeof or
let conditionOperator: typeof and | typeof not | typeof or
if (relationOrPath.toLowerCase() === 'and') {
conditionOperator = and
} else if (relationOrPath.toLowerCase() === 'or') {
conditionOperator = or
} else if (relationOrPath.toLowerCase() === 'not') {
conditionOperator = not
}
if (Array.isArray(condition)) {
const builtConditions = buildAndOrConditions({
Expand All @@ -75,7 +77,24 @@ export function parseParams({
where: condition,
})
if (builtConditions.length > 0) {
result = conditionOperator(...builtConditions)
result = (conditionOperator as typeof and | typeof or)(...builtConditions)
}
} else if (conditionOperator === not && typeof condition === 'object') {
const builtCondition = parseParams({
adapter,
aliasTable,
context,
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
where: condition as Where,
})
if (builtCondition) {
result = not(builtCondition)
}
} else {
// It's a path - and there can be multiple comparisons on a single path.
Expand Down
9 changes: 9 additions & 0 deletions packages/graphql/src/schema/buildWhereInputType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ export const buildWhereInputType = ({ name, fields, parentName }: Args): GraphQL
}),
),
},
NOT: {
type: new GraphQLInputObjectType({
name: `${fieldName}_where_not`,
fields: () => ({
...fieldTypes,
...recursiveFields,
}),
}),
},
OR: {
type: new GraphQLList(
new GraphQLInputObjectType({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export async function validateQueryPaths({
const promises: Promise<void>[] = []
for (const path in where) {
const constraint = where[path]
const lowercasePath = path.toLowerCase()

if ((path === 'and' || path === 'or') && Array.isArray(constraint)) {
if ((lowercasePath === 'and' || lowercasePath === 'or') && Array.isArray(constraint)) {
for (const item of constraint) {
if (collectionConfig) {
promises.push(
Expand Down Expand Up @@ -80,6 +81,38 @@ export async function validateQueryPaths({
)
}
}
} else if (
lowercasePath === 'not' &&
typeof constraint === 'object' &&
!Array.isArray(constraint)
) {
if (collectionConfig) {
promises.push(
validateQueryPaths({
collectionConfig,
errors,
overrideAccess,
policies,
polymorphicJoin,
req,
versionFields,
where: constraint as Where,
}),
)
} else {
promises.push(
validateQueryPaths({
errors,
globalConfig,
overrideAccess,
policies,
polymorphicJoin,
req,
versionFields,
where: constraint as Where,
}),
)
}
} else if (!Array.isArray(constraint)) {
for (const operator in constraint) {
const val = constraint[operator as keyof typeof constraint]
Expand Down
5 changes: 5 additions & 0 deletions packages/payload/src/database/sanitizeWhereQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export const sanitizeWhereQuery = ({
continue
}

if (key.toLowerCase() === 'not' && typeof value === 'object' && !Array.isArray(value)) {
sanitizeWhereQuery({ fields, payload, where: value as Where })
continue
}

const paths = key.split('.')
let pathHasChanged = false

Expand Down
4 changes: 3 additions & 1 deletion packages/payload/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,12 @@ export type WhereField = {
}

export type Where = {
[key: string]: Where[] | WhereField
[key: string]: Where | Where[] | WhereField
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
and?: Where[]
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
not?: Where
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change this to _not in case if someone has field not defined so it won't be breaking for them. We explicitly say that fields that start with _ are internal

// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
or?: Where[]
}

Expand Down
60 changes: 60 additions & 0 deletions test/db-not/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import path from 'path'
import { fileURLToPath } from 'url'

import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export const postsSlug = 'posts'
export const categoriesSlug = 'categories'

export default buildConfigWithDefaults({
collections: [
{
slug: postsSlug,
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'description',
type: 'text',
},
{
name: 'category',
type: 'relationship',
relationTo: categoriesSlug,
},
],
},
{
slug: categoriesSlug,
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
],
},
],
graphQL: {
schemaOutputFile: path.resolve(dirname, 'schema.graphql'),
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
Loading
Loading