Skip to content

Model: recursive condition trees (UnifiedRule.conditions cannot express nested boolean logic) #184

Description

@gfargo

Split out of the #156 roadmap spike (Tier 3: model extension). Hard blocker for AWS WAF.

Problem

UnifiedRule represents conditions as a flat array plus a single top-level connective:

conditions: UnifiedCondition[]
conditionLogic?: 'AND' | 'OR'

Plus a per-condition group?: number (added in #169) giving exactly one level of nesting — AND-within-group, OR-across-groups — which is precisely Vercel's conditionGroup[] model and is expressible in wirefilter.

AWS WAF statements are an arbitrarily recursive tree: AndStatement/OrStatement/NotStatement wrapping leaf matchers (ByteMatchStatement, SqliMatchStatement, GeoMatchStatement, RateBasedStatement, …), nestable to any depth. (A and (B or (C and not D))) has no representation in the current model.

This also caps what WirefilterParser (#178) can accept — it deliberately returns null for an OR nested inside an AND, because there'd be nowhere to put the result. Any Cloudflare rule with genuinely nested boolean logic (hand-authored, or written by another tool) currently falls back to empty conditions.

Proposed approach

Introduce a recursive condition node type alongside the existing flat representation:

type UnifiedConditionNode =
  | { type: 'condition'; condition: UnifiedCondition }
  | { type: 'and'; children: UnifiedConditionNode[] }
  | { type: 'or'; children: UnifiedConditionNode[] }
  | { type: 'not'; child: UnifiedConditionNode }

Backward compatibility is the whole difficulty here. conditions[] + group is in the public schema, in every existing user config, and in every template/example. Options:

  • (a) Add conditionTree?: UnifiedConditionNode as an optional alternative; keep conditions[] as the canonical form for the flat/one-level case and normalize between them internally. Existing configs untouched.
  • (b) Make the tree canonical internally and treat conditions[] purely as a serialization convenience, converting on load/save.

Recommend (a) — (b) touches the diffing/hashing path (CloudflareOptimizer.canonicalizeRule) and the translators for every provider at once, which is a lot of blast radius for a change whose only current beneficiary is a provider that doesn't exist yet.

Downstream work this unblocks/requires

  • ExpressionBuilder.fromUnifiedConditions — emit nested wirefilter
  • WirefilterParser — stop bailing on nested OR-in-AND, return a tree
  • RuleTranslator.unifiedToVercel — must reject (with a clear warning, like it already does for unmappable fields) any tree deeper than Vercel's two-level model rather than silently flattening it
  • Diff/hash canonicalization for tree-shaped rules

Acceptance criteria

  • (A and (B or C)) round-trips through unified → Cloudflare → unified without loss
  • A tree too deep for Vercel produces an explicit unsupported-feature warning, never a silent flatten
  • Existing flat/one-level configs behave identically (full suite green, no schema break)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions