Query DSL

The FLXBL Query DSL is a JSON-based query language that supports MongoDB-style operators and graph traversal. Query complex data without writing SQL or Cypher.

Basic Query Structure

// POST /api/v1/dynamic/Product/query
{
  "where": {
    "price": { "$lte": 100 },
    "inStock": { "$eq": true }
  },
  "orderBy": "price",
  "orderDirection": "ASC",
  "limit": 20
}

Query options:

Option Type Description
where object Filter conditions
traverse array Graph relationship traversal
orderBy string Field to sort by
orderDirection "ASC" | "DESC" Sort direction
limit number Maximum results to return
offset number Number of results to skip
includeCount boolean When true, returns { data: [...], totalCount: number } for pagination

Comparison Operators

// Comparison operators
{
  "where": {
    "price": { "$eq": 99.99 },      // Equal
    "price": { "$neq": 99.99 },     // Not equal
    "price": { "$gt": 50 },         // Greater than
    "price": { "$gte": 50 },        // Greater than or equal
    "price": { "$lt": 100 },        // Less than
    "price": { "$lte": 100 },       // Less than or equal
    "status": { "$in": ["active", "pending"] },     // In array
    "status": { "$notIn": ["archived", "deleted"] } // Not in array
  }
}
Operator Description Example
$eq Equal to { "status": { "$eq": "active" } }
$neq Not equal to { "status": { "$neq": "deleted" } }
$gt Greater than { "price": { "$gt": 50 } }
$gte Greater than or equal { "price": { "$gte": 50 } }
$lt Less than { "price": { "$lt": 100 } }
$lte Less than or equal { "price": { "$lte": 100 } }
$in Value in array { "status": { "$in": ["a", "b"] } }
$notIn Value not in array { "status": { "$notIn": ["x", "y"] } }

Array Operators

For array fields, use $contains to check if the array includes a specific value:

// Array operators (for array fields)
{
  "where": {
    "tags": { "$contains": "featured" }   // Check if array contains value
  }
}
Operator Description Example
$contains Array contains value { "tags": { "$contains": "featured" } }

String Operators

For string fields, FLXBL provides case-insensitive matching operators:

// String operators (case-insensitive)
{
  "where": {
    "name": { "$contains": "widget" },       // Substring match
    "email": { "$startsWith": "admin" },     // Prefix match
    "email": { "$endsWith": "@example.com" } // Suffix match
  }
}
Operator Description Example
$contains Substring match (case-insensitive) { "name": { "$contains": "widget" } }
$startsWith Prefix match (case-insensitive) { "email": { "$startsWith": "admin" } }
$endsWith Suffix match (case-insensitive) { "email": { "$endsWith": "@example.com" } }

Note: The $contains operator works differently based on field type: for array fields, it checks if the array includes the value; for string fields, it performs a case-insensitive substring search.

Null Operators

Check for null or non-null field values:

// Null operators
{
  "where": {
    "deletedAt": { "$isNull": true },   // Field is null
    "assignedTo": { "$isNull": false }  // Field is not null
  }
}
Operator Description Example
$isNull Check if field is null (true) or not null (false) { "deletedAt": { "$isNull": true } }

Logical Operators

Combine multiple conditions with $and and $or:

// Logical operators
{
  "where": {
    "$and": [
      { "price": { "$gte": 10 } },
      { "price": { "$lte": 100 } }
    ]
  }
}

// OR condition
{
  "where": {
    "$or": [
      { "status": { "$eq": "featured" } },
      { "price": { "$lt": 20 } }
    ]
  }
}

// Combining AND and OR
{
  "where": {
    "$and": [
      { "inStock": { "$eq": true } },
      {
        "$or": [
          { "category": { "$eq": "electronics" } },
          { "category": { "$eq": "accessories" } }
        ]
      }
    ]
  }
}

Graph Traversal

The traverse option lets you follow relationships in the graph. This is one of FLXBL's most powerful features. Use it to filter entities based on related data, or to include related entities in your response with include: true.

The examples below show traverse for filtering - finding entities based on their relationships without including the related data in the response:

// Traverse for filtering - find products in Electronics category
// (related entities are NOT included in response)
{
  "where": {
    "name": { "$eq": "Wireless Headphones" }
  },
  "traverse": [{
    "relationship": "BELONGS_TO",
    "direction": "out",
    "where": {
      "name": { "$eq": "Electronics" }
    }
  }]
}

// Multi-hop filtering
{
  "where": {
    "email": { "$eq": "alice@example.com" }
  },
  "traverse": [
    {
      "relationship": "PURCHASED",
      "direction": "out"
    },
    {
      "relationship": "BELONGS_TO",
      "direction": "out",
      "where": {
        "name": { "$eq": "Electronics" }
      }
    }
  ]
}

Traverse options:

Option Type Default Description
relationship string required Name of the relationship to follow
direction "in" | "out" | "both" "out" Direction to traverse
where object - Filter conditions for related entities
include boolean false When true, includes related entities in response (traverse projection)
limit number 100 Maximum related entities to return when include: true
offset number 0 Number of related entities to skip when include: true
orderBy string - Field to sort included related entities by
orderDirection "ASC" | "DESC" "ASC" Sort direction for included related entities
traverse array - Nested traverse steps for multi-hop queries

Traverse for Filtering vs Traverse Projection

The traverse option works in two modes depending on whether you set include: true:

  • Traverse for Filtering (default, include: false): Filters root entities based on related entity conditions. The related entities are NOT included in the response - only the root entities that match the traverse conditions are returned.
  • Traverse Projection (include: true): Fetches related entities and nests them in the response. Related entities appear under a field named after the target entity type (pluralized).

Traverse Projection Example

To include related entities in your response, set include: true in your traverse step. You can also paginate and sort the included entities:

// Traverse projection - include related entities in response
// Use include: true to fetch related data
{
  "traverse": [{
    "relationship": "WROTE",
    "direction": "out",
    "include": true,
    "limit": 10,
    "orderBy": "createdAt",
    "orderDirection": "DESC"
  }],
  "limit": 20
}

The response includes related entities nested under the target entity name:

// Response with traverse projection
// Related entities are nested under the target entity name (pluralized)
[
  {
    "id": "user_123",
    "name": "John Doe",
    "email": "john@example.com",
    "posts": [
      {
        "id": "post_456",
        "title": "My First Post",
        "published": true,
        "createdAt": "2024-01-15T10:00:00Z",
        "_relationship": {
          "type": "WROTE",
          "createdAt": "2024-01-15T10:00:00Z"
        }
      },
      {
        "id": "post_789",
        "title": "Another Post",
        "published": true,
        "createdAt": "2024-01-10T10:00:00Z",
        "_relationship": {
          "type": "WROTE",
          "createdAt": "2024-01-10T10:00:00Z"
        }
      }
    ],
    "createdAt": "2024-01-01T00:00:00Z",
    "updatedAt": "2024-01-15T10:00:00Z"
  }
]

Note: Each related entity includes a _relationship object with metadata about the relationship (type, timestamps, and any relationship properties).

Multi-hop Traverse Projection

You can nest traverse steps to fetch multiple levels of related entities:

// Multi-hop traverse projection
// Get users with their posts, and each post with its comments
{
  "traverse": [{
    "relationship": "WROTE",
    "direction": "out",
    "include": true,
    "limit": 5,
    "traverse": [{
      "relationship": "HAS_COMMENT",
      "direction": "out",
      "include": true,
      "limit": 10
    }]
  }],
  "limit": 10
}

Pagination

// Pagination and sorting
{
  "where": {
    "status": { "$eq": "active" }
  },
  "orderBy": "createdAt",
  "orderDirection": "DESC",
  "limit": 10,
  "offset": 20
}

Counting Results

For pagination UIs that need to display total pages or result counts, use includeCount: true to get the total count alongside your paginated results:

// Get total count with paginated results
// Use includeCount: true for pagination UIs that need total pages
{
  "where": {
    "status": { "$eq": "active" }
  },
  "limit": 10,
  "offset": 0,
  "includeCount": true
}

// Response format with includeCount: true
{
  "data": [
    { "id": "1", "name": "Product 1", ... },
    { "id": "2", "name": "Product 2", ... }
  ],
  "totalCount": 157
}

The totalCount represents the total number of entities matching your filter criteria, regardless of limit and offset. This is useful for calculating total pages in pagination UIs.

Complete Example

Here's a complete example that combines multiple operators with graph traversal:

// Complete query example
curl -X POST https://api.flxbl.dev/api/v1/dynamic/Product/query \
  -H "Authorization: Bearer flxbl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "where": {
      "$and": [
        { "price": { "$gte": 10, "$lte": 500 } },
        { "inStock": { "$eq": true } },
        {
          "$or": [
            { "tags": { "$contains": "featured" } },
            { "tags": { "$contains": "sale" } }
          ]
        }
      ]
    },
    "traverse": [{
      "relationship": "BELONGS_TO",
      "direction": "out",
      "where": {
        "slug": { "$in": ["electronics", "accessories"] }
      }
    }],
    "orderBy": "price",
    "orderDirection": "ASC",
    "limit": 20
  }'

TypeScript Client

Use the generated TypeScript client for type-safe queries:

// TypeScript client usage
import { createFlxblClient } from './schemas';

const client = createFlxblClient({
  baseUrl: 'https://api.flxbl.dev',
  apiKey: 'flxbl_your_api_key'
});

// Query with type safety
const products = await client.query('Product', {
  where: {
    price: { $gte: 10, $lte: 100 },
    inStock: { $eq: true }
  },
  traverse: [{
    relationship: 'BELONGS_TO',
    direction: 'out',
    where: { slug: { $eq: 'electronics' } }
  }],
  orderBy: 'price',
  orderDirection: 'ASC',
  limit: 20
});

console.log(products); // Typed as Product[]

Best Practices

  • Limit results: Always set a reasonable limit to avoid returning too much data
  • Optimize traversals: Start with the most selective condition first
  • Cache when possible: Use Redis caching for frequently repeated queries
  • Test with real data: Query performance varies with data size

Next Steps