dyno-cql
v1.2.3
Published
A TypeScript library for building CQL queries
Readme
Dyno CQL
Build type-safe OGC Common Query Language (CQL) filter expressions with a fluent TypeScript API.
Why Dyno CQL?
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
Installation
npm install dyno-cqlQuick Start
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)Type Safety
Define interfaces for your data to get full type safety on attribute names and value types
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
// Type-safe query builder
const productQuery = queryBuilder<Product>()
.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
)
)Comparison Operators
Standard value comparisons for filtering your data.
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 NULLText Operators
String matching for search functionality.
import { like, contains } from 'dyno-cql';
// Prefix matching
like("name", "A") // → name LIKE 'A%'
// Substring search
contains("description", "important") // → description LIKE '%important%'Logical Operators
Combine multiple conditions to build complex filters.
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')Spatial Operators
Filter geospatial data using GeoJSON geometries. Input uses GeoJSON, output converts to WKT (Well-Known Text) format.
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))Available spatial operators
| 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 |
Temporal Operators
Filter data by time and date relationships. Supports ISO 8601 timestamps, Date objects, and intervals.
Simple temporal filtering
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'))Point-in-time operators
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'))Interval operators
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'))Boundary operators
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'))Advanced Usage
Reusable conditions
Build conditions once, use them everywhere.
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();Clone and modify queries
Start with a base query and create variations.
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)));URL-safe output
Generate encoded strings ready for URL parameters.
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
Comparison Operators
eq(attr, value)- Equal tone(attr, value)- Not equal tolt(attr, value)- Less thanlte(attr, value)- Less than or equal togt(attr, value)- Greater thangte(attr, value)- Greater than or equal tobetween(attr, min, max)- Between two valuesisNull(attr)- Is nullisNotNull(attr)- Is not null
Text Operators
like(attr, value)- Prefix match (value%)contains(attr, value)- Substring match (%value%)
Logical Operators
and(...conditions)- All conditions must matchor(...conditions)- Any condition must matchnot(condition)- Negate a condition
Spatial Operators
intersects(attr, geometry)- Geometries intersectdisjoint(attr, geometry)- Geometries are disjointspatialContains(attr, geometry)- Contains geometrywithin(attr, geometry)- Within geometrytouches(attr, geometry)- Touches geometryoverlaps(attr, geometry)- Overlaps geometrycrosses(attr, geometry)- Crosses geometryspatialEquals(attr, geometry)- Spatially equal
Temporal Operators
Simple CQL:
anyinteracts(attr, temporal)- Any temporal interaction
Enhanced operators:
after(attr, timestamp)- After timestampbefore(attr, timestamp)- Before timestamptequals(attr, timestamp)- Temporally equalduring(attr, interval)- During intervaltoverlaps(attr, interval)- Overlaps intervaloverlappedby(attr, interval)- Overlapped by intervaltcontains(attr, temporal)- Contains temporalbegins(attr, timestamp)- Begins at timestampbegunby(attr, timestamp)- Begun by timestampends(attr, timestamp)- Ends at timestampendedby(attr, timestamp)- Ended by timestampmeets(attr, interval)- Meets intervalmetby(attr, interval)- Met by intervaltintersects(attr, temporal)- Temporally intersects
License
MIT
