dyno-cql
v1.5.0
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?
Building complex CQL queries by concatenating strings is a headache. Dyno CQL provides a better way to generate CQL filters:
- Type safety: Catch typos and invalid types at compile time.
- Fluent builder: Chain conditions together without worrying about brackets or operator precedence.
- Automatic escaping: Values are safely formatted and escaped.
- Standards compliant: Outputs valid OGC CQL2 ready for your 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.isIn("id", ["p1", "p2"]), // ✓ Valid list values
// op.eq("invalid", "val") // ✗ Compile error
)
)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 valuesisIn(attr, values)- Value is in listisNotIn(attr, values)- Value is not in listisNull(attr)- Is nullisNotNull(attr)- Is not null
Text Operators
like(attr, value)- Match a caller-supplied wildcard patterncontains(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
Comparison Operators
Standard value comparisons for filtering your data.
import { eq, ne, lt, lte, gt, gte, between, isIn, isNotIn, 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
// List membership
isIn("investigation_type", ["CPT", "OTHER"]) // → investigation_type IN ('CPT', 'OTHER')
isNotIn("status", ["DELETED", "ARCHIVED"]) // → status NOT IN ('DELETED', 'ARCHIVED')
// 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';
// Wildcard 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
The anyinteracts operator is the broadest temporal filter. It evaluates to true if the property has any interaction or overlap (e.g., contains, intersects, touches, or equals) with the provided time. It supports ISO 8601 strings, Date objects, and time intervals.
import { anyinteracts } from 'dyno-cql';
// Match a specific timestamp (string or Date)
anyinteracts("eventDate", "2023-01-01T00:00:00Z")
// → ANYINTERACTS(eventDate, TIMESTAMP('2023-01-01T00:00:00Z'))
// Match a time interval (object with start and end)
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}`);License
MIT
