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) {
        continue

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Decide whether you want to adjust operands (keep transformOperands, drop build) or fully replace the SQL (keep build, drop transformOperands).
  2. Run the adapter's operator-handler validation in a unit test (validateOperatorHandlers.spec.ts pattern) so misconfiguration fails at test time, not boot.
  3. 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

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


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/f7a238e124e0e162. Report an issue: GitHub.