A fluent query builder for SODA (Socrata Open Data API) v3, providing an ORM-like interface for constructing SoQL queries in Node.js.
- π― Fluent Query Builder - Chain methods to build queries naturally
- π Type-Safe - Full TypeScript support with comprehensive type definitions
- π SODA v3 Compatible - Built specifically for the SODA v3 API
- π¦ Zero Dependencies - Uses native Node.js
fetch(Node 18+) - π¨ Flexible - Use standalone query builder or integrated client
npm install soda3-query
# or
pnpm add soda3-query
# or
yarn add soda3-queryπ¦ Package: npmjs.com/package/soda3-query
import { SodaClient } from 'soda3-query';
// Initialize the client
const client = new SodaClient({
domain: 'data.city.gov',
appToken: 'your-app-token', // Optional but recommended
});
// Execute a query
const results = await client
.query('resource-id')
.select(['name', 'age', 'city'])
.where('age', '>', 30)
.where('city', '=', 'New York')
.orderBy('age', 'desc')
.limit(100)
.offset(0)
.execute();
console.log(results.data);The main client for interacting with SODA v3 APIs.
new SodaClient(options: SodaClientOptions)Options:
domain(required): The SODA domain (e.g.,'data.city.gov')appToken(optional): Your SODA application token for higher rate limitstimeout(optional): Request timeout in milliseconds
Creates a query builder bound to this client for the specified resource.
Executes a query directly with raw query parameters.
Retrieves metadata about a resource.
The fluent query builder for constructing SoQL queries.
Specifies which fields to retrieve. Supports:
- Single field:
select('name') - Multiple fields:
select(['name', 'age', 'city']) - Wildcard:
select('*') - Aggregate functions:
select(['category', 'count(*)', 'sum(amount)']) - Aliases:
select('name as full_name')
Adds a WHERE condition (defaults to AND). Supports operators:
=,!=,<,>,<=,>=LIKE- Pattern matchingIN- Value in arrayIS NULL,IS NOT NULL- Null checks
Explicitly adds an AND condition.
Adds an OR condition.
Adds a raw SQL condition for complex queries.
Adds an ORDER BY clause. Default direction is 'asc'.
Adds a GROUP BY clause.
Adds a HAVING clause for post-aggregation filtering.
Sets the LIMIT for pagination.
Sets the OFFSET for pagination.
Builds and returns the SoQL query parameters object without executing.
Executes the query and returns the results. Requires the builder to be bound to a client.
const results = await client
.query('resource-id')
.select('*')
.where('status', '=', 'active')
.limit(10)
.execute();const results = await client
.query('resource-id')
.select(['name', 'age', 'city'])
.where('age', '>', 30)
.where('city', '=', 'New York')
.orWhere('status', '=', 'vip')
.orderBy('age', 'desc')
.execute();const results = await client
.query('resource-id')
.select(['category', 'count(*)', 'sum(amount)'])
.groupBy('category')
.having('count(*)', '>', 10)
.orderBy('category')
.execute();const results = await client
.query('resource-id')
.select('*')
.where('status', 'IN', ['active', 'pending', 'completed'])
.execute();// Automatically converts = null to IS NULL
const results = await client
.query('resource-id')
.select('*')
.where('deleted_at', '=', null)
.execute();
// Or explicitly
const results = await client
.query('resource-id')
.select('*')
.where('email', 'IS NOT NULL', null)
.execute();const results = await client
.query('resource-id')
.select('*')
.where('name', 'LIKE', 'John%')
.execute();const results = await client
.query('resource-id')
.select('*')
.where('status', '=', 'active')
.andWhereRaw('EXTRACT(YEAR FROM created_at) = 2024')
.execute();You can also use the query builder without a client to generate query parameters:
import { QueryBuilder } from 'soda3-query';
const query = new QueryBuilder()
.select(['name', 'age'])
.where('age', '>', 30)
.orderBy('name')
.limit(100)
.build();
// Returns: {
// $select: 'name,age',
// $where: 'age > 30',
// $order: 'name ASC',
// $limit: 100
// }// First page
const page1 = await client.query('resource-id').select('*').limit(20).offset(0).execute();
// Second page
const page2 = await client.query('resource-id').select('*').limit(20).offset(20).execute();const metadata = await client.getMetadata('resource-id');
console.log(metadata.columns); // Array of column definitionsThe client throws errors for various scenarios:
try {
const results = await client.query('resource-id').select('*').execute();
} catch (error) {
if (error instanceof Error) {
console.error('Query failed:', error.message);
// Handle 4xx, 5xx, network errors, timeouts, etc.
}
}Full TypeScript support is included. You can type your query results:
interface User {
id: number;
name: string;
email: string;
}
const results = await client
.query<User>('users-resource-id')
.select(['id', 'name', 'email'])
.where('active', '=', true)
.execute();
// results.data is typed as User[]| Operator | Description | Example |
|---|---|---|
= |
Equal | .where('status', '=', 'active') |
!= |
Not equal | .where('status', '!=', 'inactive') |
< |
Less than | .where('age', '<', 18) |
> |
Greater than | .where('age', '>', 65) |
<= |
Less than or equal | .where('age', '<=', 18) |
>= |
Greater than or equal | .where('age', '>=', 18) |
LIKE |
Pattern match | .where('name', 'LIKE', 'John%') |
IN |
Value in array | .where('status', 'IN', ['a', 'b']) |
IS NULL |
Null check | .where('deleted_at', 'IS NULL', null) |
IS NOT NULL |
Not null check | .where('email', 'IS NOT NULL', null) |
- Node.js 18 or higher (uses native
fetch)
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run tests with coverage in watch mode
npm run test:coverage:watch
# Run live API integration tests (requires internet connection)
npm run test:live-integration
# Type check
npm run typecheck
# Lint
npm run lint
# Format code
npm run format
# Build
npm run buildThe project includes comprehensive test coverage reporting. Coverage reports are generated in multiple formats:
- Text: Displayed in terminal
- HTML: Interactive report in
coverage/index.html - LCOV: For CI/CD integration (e.g., Codecov)
- JSON: Machine-readable format
Coverage Thresholds:
- Lines: 80%
- Functions: 80%
- Branches: 75%
- Statements: 80%
View the HTML coverage report:
npm run test:coverage
open coverage/index.htmlThe test suite includes optional live integration tests that query real SODA v3 endpoints from multiple cities' open data portals. These tests validate that the query builder produces correct SoQL that the API accepts and returns expected results across different SODA deployments.
Test Coverage:
- San Francisco (
data.sfgov.org) - Comprehensive tests using the 311 Cases dataset - Multi-City Tests - Basic compatibility tests across:
- San Francisco (
data.sfgov.org) - Chicago (
data.cityofchicago.org) - Seattle (
data.seattle.gov) - Los Angeles (
data.lacity.org) - Dallas (
www.dallasopendata.com) - Austin (
data.austintexas.gov) - New York City (
data.cityofnewyork.us)
- San Francisco (
To run live integration tests:
# Using the npm script
npm run test:live-integration
# Or manually with environment variable
ENABLE_REAL_API_TESTS=true npm testCI/CD Integration:
The repository includes GitHub Actions workflows that follow best practices for running live tests:
- On Pull Requests: Only fast unit tests, linting, and type checking run (no live tests)
- On Merges to Main: Live integration tests run automatically after code is merged
- Scheduled: Optional daily scheduled runs to catch external API changes/regressions
This approach balances:
- Fast feedback on PRs (unit tests complete in seconds)
- Comprehensive validation after merge (live tests ensure real-world compatibility)
- Early detection of external API changes (scheduled runs)
Note:
- Live integration tests require an internet connection
- Tests may be slower than mocked tests (~10-15 seconds for full suite)
- Tests are subject to API rate limits
- Tests are skipped by default in CI/CD pipelines (opt-in via environment variable)
- The tests use public datasets which don't require authentication
- Multi-city tests validate compatibility across different SODA versions and configurations
ISC
Contributions are welcome! Please feel free to submit a Pull Request.
This package follows Semantic Versioning. For details on the release process, see RELEASING.md.
Quick Release:
- Use the "Prepare Release" GitHub Action workflow
- Review and merge the release PR
- Create and push a version tag (e.g.,
v1.2.3) - The release workflow automatically publishes to npm
- π¦ npm Package
- π GitHub Repository
- π SODA API Documentation