fix(drizzle): equals polymorphic querying with object notation (#8316)

Previously, this wasn't valid in Postgres / SQLite:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    polymorphic: {
      equals: {
        relationTo: 'movies',
        value: movie.id,
      },
    },
  },
})
```

Now it works and actually in more performant way than this:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    and: [
      {
        'polymorphic.relationTo': {
          equals: 'movies',
        },
      },
      {
        'polymorphic.value': {
          equals: 'movies',
        },
      },
    ],
  },
})
``` 

Why? Because with the object notation, the output SQL is: `movies_id =
1` - checks exactly 1 column in the `*_rels` table, while with the
separate query by `relationTo` and `value` we need to check against
_each_ possible relationship collection with OR.
This commit is contained in:
Sasha
2024-09-20 18:18:13 +03:00
committed by GitHub
parent 2596b85027
commit 265d7fa0e2
6 changed files with 121 additions and 12 deletions

View File

@@ -12,6 +12,7 @@ import { validate as uuidValidate } from 'uuid'
import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases } from './buildQuery.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
import { getTableAlias } from './getTableAlias.js'
type Constraint = {
@@ -603,6 +604,19 @@ export const getTableColumnFromPath = ({
},
table: aliasRelationshipTable,
}
} else if (isPolymorphicRelationship(value)) {
const { relationTo } = value
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
return {
constraints,
field,
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
table: aliasRelationshipTable,
}
} else {
throw new APIError('Not supported')
}

View File

@@ -6,6 +6,9 @@ import { validate as uuidValidate } from 'uuid'
import type { DrizzleAdapter } from '../types.js'
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
type SanitizeQueryValueArgs = {
adapter: DrizzleAdapter
columns?: {
@@ -107,17 +110,28 @@ export const sanitizeQueryValue = ({
// convert the value to the idType of the relationship
let idType: 'number' | 'text'
if (typeof field.relationTo === 'string') {
const collection = adapter.payload.collections[field.relationTo]
const mixedType: 'number' | 'serial' | 'text' | 'uuid' =
collection.customIDType || adapter.idType
const typeMap: Record<string, 'number' | 'text'> = {
number: 'number',
serial: 'number',
text: 'text',
uuid: 'text',
}
idType = typeMap[mixedType]
idType = getCollectionIdType({
adapter,
collection: adapter.payload.collections[field.relationTo],
})
} else {
if (isPolymorphicRelationship(val)) {
if (operator !== 'equals') {
throw new APIError(
`Only 'equals' operator is supported for polymorphic relationship object notation. Given - ${operator}`,
)
}
idType = getCollectionIdType({
adapter,
collection: adapter.payload.collections[val.relationTo],
})
return {
operator,
value: idType === 'number' ? Number(val.value) : String(val.value),
}
}
formattedColumns = columns
.map(({ idType, rawColumn }) => {
let formattedValue: number | number[] | string | string[]

View File

@@ -0,0 +1,20 @@
import type { Collection } from 'payload'
import type { DrizzleAdapter } from '../types.js'
const typeMap: Record<string, 'number' | 'text'> = {
number: 'number',
serial: 'number',
text: 'text',
uuid: 'text',
}
export const getCollectionIdType = ({
adapter,
collection,
}: {
adapter: DrizzleAdapter
collection: Collection
}) => {
return collection.customIDType ?? typeMap[adapter.idType]
}

View File

@@ -0,0 +1,16 @@
import type { CollectionSlug } from 'payload'
export const isPolymorphicRelationship = (
value: unknown,
): value is {
relationTo: CollectionSlug
value: number | string
} => {
return (
value &&
typeof value === 'object' &&
'relationTo' in value &&
typeof value.relationTo === 'string' &&
'value' in value
)
}

View File

@@ -186,12 +186,12 @@ export interface PayloadLockedDocument {
relationTo: 'users';
value: string | User;
} | null);
editedAt?: string | null;
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
editedAt?: string | null;
updatedAt: string;
createdAt: string;
}

View File

@@ -1,6 +1,6 @@
import type { Payload, PayloadRequest } from 'payload'
import { randomBytes } from 'crypto'
import { randomBytes, randomUUID } from 'crypto'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -1168,6 +1168,51 @@ describe('Relationships', () => {
expect(queryOne.docs).toHaveLength(1)
expect(queryTwo.docs).toHaveLength(1)
})
it('should allow querying on polymorphic relationships with an object syntax', async () => {
const movie = await payload.create({
collection: 'movies',
data: {
name: 'Pulp Fiction 2',
},
})
await payload.create({
collection: polymorphicRelationshipsSlug,
data: {
polymorphic: {
relationTo: 'movies',
value: movie.id,
},
},
})
const res = await payload.find({
collection: 'polymorphic-relationships',
where: {
polymorphic: {
equals: {
relationTo: 'movies',
value: movie.id,
},
},
},
})
expect(res.docs).toHaveLength(1)
const res_2 = await payload.find({
collection: 'polymorphic-relationships',
where: {
polymorphic: {
equals: {
relationTo: 'movies',
value: payload.db.idType === 'uuid' ? randomUUID() : 99,
},
},
},
})
expect(res_2.docs).toHaveLength(0)
})
})
})