A TypeScript library for building CQL queries
npm install dyno-cqlBuild type-safe OGC Common Query Language (CQL) filter expressions with a fluent TypeScript API.


Working with geospatial and temporal data shouldn't mean wrestling with raw CQL strings. Dyno CQL gives you:
- Full TypeScript support - Catch errors at compile time, not runtime
- Fluent chaining - Build complex filters that read like sentences
- Zero string concatenation - No more manual escaping or formatting
- OGC CQL2 compliant - Works with modern geospatial APIs
``bash`
npm install dyno-cql
`typescript
import { queryBuilder, eq, gt, and } from 'dyno-cql';
// Simple filter
const simple = queryBuilder()
.filter(eq("status", "ACTIVE"))
.toCQL();
// → status = 'ACTIVE'
// Complex filter
const complex = queryBuilder()
.filter(
and(
eq("status", "ACTIVE"),
gt("age", 18)
)
)
.toCQL();
// → (status = 'ACTIVE' AND age > 18)
`
Define interfaces for your data to get full type safety on attribute names and value types
`typescript
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
// Type-safe query builder
const productQuery = queryBuilder
.filter((op) =>
op.and(
// the attribute name and value types are checked
op.eq("name", "Laptop"), // ✓ Valid attribute
op.gte("price", 500), // ✓ Valid type (number)
op.eq("inStock", true), // ✓ Valid type (boolean)
// op.eq("invalid", "val") // ✗ Compile error
)
)
`
Standard value comparisons for filtering your data.
`typescript
import { eq, ne, lt, lte, gt, gte, between, isNull, isNotNull } from 'dyno-cql';
// Equality
eq("status", "ACTIVE") // → status = 'ACTIVE'
ne("status", "DELETED") // → status <> 'DELETED'
// Numeric comparisons
lt("age", 18) // → age < 18
lte("score", 100) // → score <= 100
gt("price", 50) // → price > 50
gte("quantity", 5) // → quantity >= 5
// Range queries
between("age", 18, 65) // → age BETWEEN 18 AND 65
// Null checks
isNull("deletedAt") // → deletedAt IS NULL
isNotNull("email") // → email IS NOT NULL
`
String matching for search functionality.
`typescript
import { like, contains } from 'dyno-cql';
// Prefix matching
like("name", "A") // → name LIKE 'A%'
// Substring search
contains("description", "important") // → description LIKE '%important%'
`
Combine multiple conditions to build complex filters.
`typescript
import { and, or, not, eq, gt } from 'dyno-cql';
// AND - all conditions must match
and(
eq("status", "ACTIVE"),
gt("age", 18)
)
// → (status = 'ACTIVE' AND age > 18)
// OR - any condition can match
or(
eq("status", "PENDING"),
eq("status", "PROCESSING")
)
// → (status = 'PENDING' OR status = 'PROCESSING')
// NOT - negate a condition
not(eq("status", "DELETED"))
// → NOT (status = 'DELETED')
`
Filter geospatial data using GeoJSON geometries. Input uses GeoJSON, output converts to WKT (Well-Known Text) format.
`typescript
import {
intersects,
disjoint,
spatialContains,
within,
touches,
overlaps,
crosses,
spatialEquals
} from 'dyno-cql';
// Point geometry
const point = { type: "Point", coordinates: [0, 0] };
intersects("geometry", point)
// → INTERSECTS(geometry, POINT(0 0))
// Polygon geometry
const polygon = {
type: "Polygon",
coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]
};
within("geometry", polygon)
// → WITHIN(geometry, POLYGON((0 0, 1 0, 1 1, 0 1, 0 0)))
// Line geometry
const line = {
type: "LineString",
coordinates: [[0, 0], [1, 1]]
};
crosses("geometry", line)
// → CROSSES(geometry, LINESTRING(0 0, 1 1))
`
| Operator | Description |
|----------|-------------|
| intersects | Geometries share any space |disjoint
| | Geometries share no space |spatialContains
| | First geometry contains the second |within
| | First geometry is within the second |touches
| | Geometries touch at boundary only |overlaps
| | Geometries overlap but neither contains the other |crosses
| | Geometries cross each other |spatialEquals
| | Geometries are spatially equal |
Filter data by time and date relationships. Supports ISO 8601 timestamps, Date objects, and intervals.
`typescript
import { anyinteracts } from 'dyno-cql';
// Match a specific timestamp
anyinteracts("eventDate", "2023-01-01T00:00:00Z")
// → ANYINTERACTS(eventDate, TIMESTAMP('2023-01-01T00:00:00Z'))
// Match a time interval
anyinteracts("eventDate", { start: "2023-01-01", end: "2023-12-31" })
// → ANYINTERACTS(eventDate, INTERVAL('2023-01-01', '2023-12-31'))
`
`typescript
import { after, before, tequals } from 'dyno-cql';
// After a timestamp
after("eventDate", "2023-01-01T00:00:00Z")
// → AFTER(eventDate, TIMESTAMP('2023-01-01T00:00:00Z'))
// Before a timestamp
before("eventDate", "2023-12-31T23:59:59Z")
// → BEFORE(eventDate, TIMESTAMP('2023-12-31T23:59:59Z'))
// Equal to a timestamp
tequals("eventDate", "2023-06-15T12:00:00Z")
// → TEQUALS(eventDate, TIMESTAMP('2023-06-15T12:00:00Z'))
// Works with Date objects too
const date = new Date("2023-06-15");
after("eventDate", date)
// → AFTER(eventDate, TIMESTAMP('2023-06-15T00:00:00.000Z'))
`
`typescript
import { during, toverlaps, overlappedby, tcontains } from 'dyno-cql';
// Event occurs during an interval
during("eventDate", { start: "2023-01-01", end: "2023-12-31" })
// → DURING(eventDate, INTERVAL('2023-01-01', '2023-12-31'))
// Event period overlaps an interval
toverlaps("eventPeriod", { start: "2023-06-01", end: "2023-12-31" })
// → TOVERLAPS(eventPeriod, INTERVAL('2023-06-01', '2023-12-31'))
// Event period is overlapped by an interval
overlappedby("eventPeriod", { start: "2022-06-01", end: "2023-06-30" })
// → OVERLAPPEDBY(eventPeriod, INTERVAL('2022-06-01', '2023-06-30'))
// Event period contains a timestamp
tcontains("eventPeriod", "2023-06-15T12:00:00Z")
// → TCONTAINS(eventPeriod, TIMESTAMP('2023-06-15T12:00:00Z'))
`
`typescript
import { begins, begunby, ends, endedby, meets, metby, tintersects } from 'dyno-cql';
// Period begins at timestamp
begins("eventPeriod", "2023-01-01T00:00:00Z")
// → BEGINS(eventPeriod, TIMESTAMP('2023-01-01T00:00:00Z'))
// Period is begun by timestamp
begunby("eventPeriod", "2023-01-01T00:00:00Z")
// → BEGUNBY(eventPeriod, TIMESTAMP('2023-01-01T00:00:00Z'))
// Period ends at timestamp
ends("eventPeriod", "2023-12-31T23:59:59Z")
// → ENDS(eventPeriod, TIMESTAMP('2023-12-31T23:59:59Z'))
// Period is ended by timestamp
endedby("eventPeriod", "2023-12-31T23:59:59Z")
// → ENDEDBY(eventPeriod, TIMESTAMP('2023-12-31T23:59:59Z'))
// Periods meet (one ends when other begins)
meets("eventPeriod", { start: "2023-07-01", end: "2023-12-31" })
// → MEETS(eventPeriod, INTERVAL('2023-07-01', '2023-12-31'))
// Periods are met by (other ends when one begins)
metby("eventPeriod", { start: "2022-07-01", end: "2023-01-01" })
// → METBY(eventPeriod, INTERVAL('2022-07-01', '2023-01-01'))
// Temporal intersection
tintersects("eventDate", "2023-06-15T12:00:00Z")
// → TINTERSECTS(eventDate, TIMESTAMP('2023-06-15T12:00:00Z'))
`
Build conditions once, use them everywhere.
`typescript
import { queryBuilder, eq, and, gt, ne } from 'dyno-cql';
// Define reusable conditions
const isActive = eq("status", "ACTIVE");
const isAdult = gt("age", 18);
const notDeleted = ne("deleted", true);
// Combine them
const standardFilters = and(isActive, notDeleted);
// Use in multiple queries
const query1 = queryBuilder()
.filter(and(standardFilters, eq("type", "premium")))
.toCQL();
const query2 = queryBuilder()
.filter(and(standardFilters, isAdult))
.toCQL();
`
Start with a base query and create variations.
`typescript
import { queryBuilder, eq, and } from 'dyno-cql';
// Base query
const baseQuery = queryBuilder()
.filter(eq("type", "product"));
// Create variations
const activeProducts = baseQuery.clone()
.filter(and(eq("type", "product"), eq("status", "ACTIVE")));
const archivedProducts = baseQuery.clone()
.filter(and(eq("type", "product"), eq("archived", true)));
`
Generate encoded strings ready for URL parameters.
`typescript
import { queryBuilder, and, eq, contains } from 'dyno-cql';
const query = queryBuilder()
.filter(
and(
eq("name", "John & Jane"),
contains("description", "100% satisfaction")
)
)
.toCQLUrlSafe();
// Use directly in fetch
fetch(/api/products?filter=${query});`API Reference
- Equal to
- ne(attr, value) - Not equal to
- lt(attr, value) - Less than
- lte(attr, value) - Less than or equal to
- gt(attr, value) - Greater than
- gte(attr, value) - Greater than or equal to
- between(attr, min, max) - Between two values
- isNull(attr) - Is null
- isNotNull(attr) - Is not null$3
- like(attr, value) - Prefix match (value%)
- contains(attr, value) - Substring match (%value%)$3
- and(...conditions) - All conditions must match
- or(...conditions) - Any condition must match
- not(condition) - Negate a condition$3
- intersects(attr, geometry) - Geometries intersect
- disjoint(attr, geometry) - Geometries are disjoint
- spatialContains(attr, geometry) - Contains geometry
- within(attr, geometry) - Within geometry
- touches(attr, geometry) - Touches geometry
- overlaps(attr, geometry) - Overlaps geometry
- crosses(attr, geometry) - Crosses geometry
- spatialEquals(attr, geometry) - Spatially equal$3
Simple CQL:
-
anyinteracts(attr, temporal) - Any temporal interactionEnhanced operators:
-
after(attr, timestamp) - After timestamp
- before(attr, timestamp) - Before timestamp
- tequals(attr, timestamp) - Temporally equal
- during(attr, interval) - During interval
- toverlaps(attr, interval) - Overlaps interval
- overlappedby(attr, interval) - Overlapped by interval
- tcontains(attr, temporal) - Contains temporal
- begins(attr, timestamp) - Begins at timestamp
- begunby(attr, timestamp) - Begun by timestamp
- ends(attr, timestamp) - Ends at timestamp
- endedby(attr, timestamp) - Ended by timestamp
- meets(attr, interval) - Meets interval
- metby(attr, interval) - Met by interval
- tintersects(attr, temporal)` - Temporally intersectsMIT