payloadcms/payload · critical · APIError
Operator handler "${handler.name}" cannot define both "build
Error message
Operator handler "${handler.name}" cannot define both "build" and "transformOperands". Use "transformOperands" to adjust operands, or "build" to replace the comparison entirely. What it means
Thrown at adapter boot by validateOperatorHandlers when a custom DrizzleOperatorHandler defines both a 'build' and a 'transformOperands' function. These two are mutually exclusive: 'transformOperands' adjusts the operands before the default comparison runs, while 'build' replaces the comparison entirely. Allowing both would make execution order ambiguous, so the config is rejected before any request is served.
Source
Thrown at packages/drizzle/src/queries/validateOperatorHandlers.ts:25
): handler is DrizzleOperatorReplacementHandler => typeof handler.build === 'function'
const fieldTypesOverlap = (
a: DrizzleOperatorReplacementHandler,
b: DrizzleOperatorReplacementHandler,
): boolean =>
!a.fieldTypes ||
!b.fieldTypes ||
a.fieldTypes.some((fieldType) => b.fieldTypes.includes(fieldType))
/**
* Validates operator-handler configuration before any request runs: rejects a handler that
* defines both `build` and `transformOperands`, and rejects two replacement handlers whose
* `operators` and `fieldTypes` both overlap for the same resolved operator.
*/
export const validateOperatorHandlers = (operatorHandlers: DrizzleOperatorHandler[]): void => {
for (const handler of operatorHandlers) {
if (typeof handler.build === 'function' && typeof handler.transformOperands === 'function') {
throw new APIError(
`Operator handler "${handler.name}" cannot define both "build" and "transformOperands". Use "transformOperands" to adjust operands, or "build" to replace the comparison entirely.`,
)
}
}
const replacementHandlers = operatorHandlers.filter(isReplacementHandler)
for (let i = 0; i < replacementHandlers.length; i++) {
for (let j = i + 1; j < replacementHandlers.length; j++) {
const handlerA = replacementHandlers[i]
const handlerB = replacementHandlers[j]
const sharedOperators = handlerA.operators.filter((operator) =>
handlerB.operators.includes(operator),
)
if (!sharedOperators.length) {
continueView on GitHub (pinned to 00c58b35c0)
Solutions
- Decide whether you want to adjust operands (keep transformOperands, drop build) or fully replace the SQL (keep build, drop transformOperands).
- Run the adapter's operator-handler validation in a unit test (validateOperatorHandlers.spec.ts pattern) so misconfiguration fails at test time, not boot.
- Check the DrizzleOperatorHandler type definition to confirm only one of the two keys is allowed for your use case.
Example fix
// before
const handler = {
name: 'my-like',
operators: ['like'],
build: (opts) => sql`...`,
transformOperands: (operands) => operands.map(...),
}
// after
const handler = {
name: 'my-like',
operators: ['like'],
transformOperands: (operands) => operands.map(...),
} Defensive patterns
Strategy: validation
Validate before calling
function assertHandlerConfig(handler) {
if (typeof handler.build === 'function' && typeof handler.transformOperands === 'function') {
throw new Error(`Handler ${handler.name} must not define both build and transformOperands`)
}
} Type guard
const hasBuildAndTransform = (h) => typeof h.build === 'function' && typeof h.transformOperands === 'function'
Prevention
- Run validateOperatorHandlers in a unit test for your operatorHandlers array before wiring the adapter.
- Treat the DrizzleOperatorHandler type as authoritative: never include both keys.
When it happens
Trigger: Registering a custom operator handler (via operatorHandlers on the Drizzle adapter) whose object literal includes both `build` and `transformOperands` keys as functions.
Common situations: Copying a handler as a starting point and adding the other key without removing the original; upgrading the adapter across a version that introduced transformOperands and not pruning the old build fn.
Related errors
- Operator handlers "${handlerA.name}" and "${handlerB.name}"
- Exceeded file size limit. Limit: ${bytesToMB(filesizeLimit).
- Invalid template given
- Invalid database type given. Valid types are: ${Object.value
- No version or tag "${version}" found for package: ${packageN
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/f7a238e124e0e162.
Report an issue: GitHub.