Files
payload/packages/db-sqlite/src/createJSONQuery/index.ts
Sasha b26a73be4a fix: querying hasMany: true select fields inside polymorphic joins (#13334)
This PR fixes queries like this:

```ts
const findFolder = await payload.find({
  collection: 'payload-folders',
  where: {
    id: {
      equals: folderDoc.id,
    },
  },
  joins: {
    documentsAndFolders: {
      limit: 100_000,
      sort: 'name',
      where: {
        and: [
          {
            relationTo: {
              equals: 'payload-folders',
            },
          },
          {
            folderType: {
              in: ['folderPoly1'], // previously this didn't work
            },
          },
        ],
      },
    },
  },
})
```

Additionally, this PR potentially fixes querying JSON fields by the top
level path, for example if your JSON field has a value like: `[1, 2]`,
previously `where: { json: { equals: 1 } }` didn't work, however with a
value like `{ nested: [1, 2] }` and a query `where: { 'json.nested': {
equals: 1 } }`it did.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-07-30 15:30:20 -04:00

110 lines
2.7 KiB
TypeScript

import type { CreateJSONQueryArgs } from '@payloadcms/drizzle/types'
type FromArrayArgs = {
isRoot?: true
operator: string
pathSegments: string[]
table: string
treatAsArray?: string[]
value: boolean | number | string
}
const fromArray = ({
isRoot,
operator,
pathSegments,
table,
treatAsArray,
value,
}: FromArrayArgs) => {
const newPathSegments = pathSegments.slice(1)
const alias = `${pathSegments[isRoot ? 0 : 1]}_alias_${newPathSegments.length}`
return `EXISTS (
SELECT 1
FROM json_each(${table}.${pathSegments[0]}) AS ${alias}
WHERE ${createJSONQuery({
operator,
pathSegments: newPathSegments,
table: alias,
treatAsArray,
value,
})}
)`
}
type CreateConstraintArgs = {
alias?: string
operator: string
pathSegments: string[]
treatAsArray?: string[]
value: boolean | number | string
}
const createConstraint = ({
alias,
operator,
pathSegments,
value,
}: CreateConstraintArgs): string => {
const newAlias = `${pathSegments[0]}_alias_${pathSegments.length - 1}`
let formattedValue = value
let formattedOperator = operator
if (['contains', 'like'].includes(operator)) {
formattedOperator = 'like'
formattedValue = `%${value}%`
} else if (['not_like', 'notlike'].includes(operator)) {
formattedOperator = 'not like'
formattedValue = `%${value}%`
} else if (operator === 'equals') {
formattedOperator = '='
}
if (pathSegments.length === 1) {
return `EXISTS (SELECT 1 FROM json_each("${pathSegments[0]}") AS ${newAlias} WHERE ${newAlias}.value ${formattedOperator} '${formattedValue}')`
}
return `EXISTS (
SELECT 1
FROM json_each(${alias}.value -> '${pathSegments[0]}') AS ${newAlias}
WHERE COALESCE(${newAlias}.value ->> '${pathSegments[1]}', '') ${formattedOperator} '${formattedValue}'
)`
}
export const createJSONQuery = ({
column,
operator,
pathSegments,
rawColumn,
table,
treatAsArray,
treatRootAsArray,
value,
}: CreateJSONQueryArgs): string => {
if ((operator === 'in' || operator === 'not_in') && Array.isArray(value)) {
let sql = ''
for (const [i, v] of value.entries()) {
sql = `${sql}${createJSONQuery({ column, operator: operator === 'in' ? 'equals' : 'not_equals', pathSegments, rawColumn, table, treatAsArray, treatRootAsArray, value: v })} ${i === value.length - 1 ? '' : ` ${operator === 'in' ? 'OR' : 'AND'} `}`
}
return sql
}
if (treatAsArray?.includes(pathSegments[1]!) && table) {
return fromArray({
operator,
pathSegments,
table,
treatAsArray,
value: value as CreateConstraintArgs['value'],
})
}
return createConstraint({
alias: table,
operator,
pathSegments,
treatAsArray,
value: value as CreateConstraintArgs['value'],
})
}