@mohasinac/sievejs
v1.0.0
Published
Database-level filtering, sorting, and pagination middleware inspired by Sieve
Maintainers
Readme
SieveJS
SieveJS is a JavaScript-first, ESM-only filtering, sorting, and pagination library inspired by Sieve for .NET.
It applies query logic at the database/query-builder layer (Knex/Mongoose/Prisma/SQL Server/Firebase/Couchbase), instead of in-memory filtering by default.
Install
npm install @mohasinac/sievejsQuick start (Express)
import express from "express";
import {
createKnexAdapter,
createSieveMiddleware,
SieveProcessorBase,
} from "@mohasinac/sievejs";
const processor = new SieveProcessorBase({
adapter: createKnexAdapter(),
autoLoadConfig: true, // reads sieve.config.json / .js / .mjs / .cjs
});
const app = express();
app.get(
"/posts",
createSieveMiddleware({
processor,
queryFactory: (req) => req.db("posts"),
}),
async (req, res, next) => {
try {
const rows = await req.sieveQuery;
res.json(rows);
} catch (error) {
next(error);
}
},
);Versioning
- Package:
@mohasinac/sievejs - Current version:
1.0.0 - Version policy: SemVer
MAJOR: breaking API changesMINOR: backward-compatible featuresPATCH: backward-compatible fixes
- Note: while in
0.x, minor versions may include changes that would otherwise be major in1.x.
Features
- Sieve DSL parsing (
filters,sorts,page,pageSize) - Adapter-based query translation (Knex, Mongoose, Prisma, SQL Server, Firebase, Couchbase)
- Metadata/attribute-style mapping (
Sieve,SieveAttribute,buildFieldsFromClass) - Typed exceptions (
SieveException,SieveMethodNotFoundException,SieveIncompatibleMethodException) - Integration pattern for Express, Next, and custom frameworks
- Pipe helper for non-middleware pipelines
Usage (Express)
In this example, we use posts to show filtering/sorting/pagination.
1. Create and configure a processor
import { createKnexAdapter, SieveProcessorBase } from "@mohasinac/sievejs";
const processor = new SieveProcessorBase({
adapter: createKnexAdapter(),
autoLoadConfig: true, // reads sieve.config.json / .js / .mjs / .cjs
});2. Define which fields are filterable/sortable
Option A: Config file (sieve.config.json)
{
"options": {
"caseSensitive": false,
"defaultPageSize": 20,
"maxPageSize": 100,
"throwExceptions": true
},
"fields": {
"title": { "canFilter": true, "canSort": true },
"created": {
"path": "created_at",
"canFilter": true,
"canSort": true
}
}
}Option B: Attributes/metadata in code
import { Sieve, buildFieldsFromClass } from "@mohasinac/sievejs";
class Post {}
Sieve({ canFilter: true, canSort: true })(Post.prototype, "title");
Sieve({ canFilter: true, canSort: true, name: "created" })(
Post.prototype,
"createdAt",
);
const fields = buildFieldsFromClass(Post);In plain JavaScript, decorators are applied as function calls. In TypeScript/Babel you can use decorator syntax if enabled.
3. Apply Sieve in your route
import express from "express";
import {
createSieveMiddleware,
createKnexAdapter,
SieveProcessorBase,
} from "@mohasinac/sievejs";
const app = express();
const processor = new SieveProcessorBase({
adapter: createKnexAdapter(),
autoLoadConfig: true,
});
app.get(
"/posts",
createSieveMiddleware({
processor,
queryFactory: (req) => req.db("posts"),
}),
async (req, res, next) => {
try {
const rows = await req.sieveQuery;
res.json(rows);
} catch (error) {
next(error);
}
},
);4. Send a request
GET /posts?sorts=likeCount,commentCount,-created&filters=likeCount>10,title@=awesome&page=1&pageSize=10Add custom sort/filter methods
Custom methods can be provided via processor configuration:
import { createSieveProcessor, createPrismaAdapter } from "@mohasinac/sievejs";
const processor = createSieveProcessor({
adapter: createPrismaAdapter(),
fields: {
title: { canFilter: true, canSort: true },
created: { canFilter: true, canSort: true },
},
customFilters: {
isNew(query, operator, values) {
return {
...query,
where: {
...(query.where ?? {}),
createdAt: { gte: new Date(values[0]) },
},
};
},
},
customSorts: {
popularity(query, useThenBy, descending) {
const orderBy = Array.isArray(query.orderBy) ? query.orderBy : [];
const dir = descending ? "desc" : "asc";
return {
...query,
orderBy: [
...orderBy,
{ likeCount: dir },
{ commentCount: dir },
],
};
},
},
});Configure Sieve
Processor options
caseSensitive: property-name case sensitivitydefaultPageSize: default page size when none providedmaxPageSize: upper bound for requested page sizethrowExceptions: throw typed errors instead of silent fallbackignoreNullsOnNotEqual: null behavior for!=
Configure via constructor
const processor = new SieveProcessorBase({
autoLoadConfig: false,
adapter: createKnexAdapter(),
options: {
caseSensitive: false,
defaultPageSize: 20,
maxPageSize: 100,
throwExceptions: true,
ignoreNullsOnNotEqual: true,
},
fields: {
title: { canFilter: true, canSort: true },
created: { path: "created_at", canFilter: true, canSort: true },
},
});Send a request (DSL reference)
Query parameters
sorts: comma-delimited ordered list of fields (-prefix means descending)filters: comma-delimited list of{Name}{Operator}{Value}page: page numberpageSize: page size
OR logic
- OR fields:
(likeCount|commentCount)>10 - OR values:
title@=new|hot
Escaping
- Escaped comma:
title@=some\,title - Escaped pipe:
title@=some\|title - Literal
nullstring:title==\null
Operators
| Operator | Meaning |
| -------- | ------------------------------------ |
| == | Equals |
| != | Not equals |
| > | Greater than |
| < | Less than |
| >= | Greater than or equal |
| <= | Less than or equal |
| @= | Contains |
| _= | Starts with |
| _-= | Ends with |
| !@= | Does not contain |
| !_= | Does not start with |
| !_-= | Does not end with |
| @=* | Case-insensitive contains |
| _=* | Case-insensitive starts with |
| _-=* | Case-insensitive ends with |
| ==* | Case-insensitive equals |
| !=* | Case-insensitive not equals |
| !@=* | Case-insensitive does not contain |
| !_=* | Case-insensitive does not start with |
| !_-=* | Case-insensitive does not end with |
Nested objects/paths
Use mapped paths for nested fields. Example:
const processor = new SieveProcessorBase({
adapter: createPrismaAdapter(),
fields: {
creatorName: { path: "creator.name", canFilter: true, canSort: true },
},
});Then query with:
GET /posts?filters=creatorName==mohasinCreating your own DSL/model binding
You can map any request shape to Sieve model format (filters, sorts, page, pageSize) in integrations:
import { createSieveIntegration } from "@mohasinac/sievejs";
const runSieve = createSieveIntegration({
processor,
requestModel: ({ request }) => ({
filters: request.body?.where,
sorts: request.body?.order,
page: request.body?.p,
pageSize: request.body?.size,
}),
queryFactory: ({ request }) => request.db("posts"),
execute: ({ query }) => query,
});Handle Sieve exceptions
With throwExceptions: true, typed errors include:
SieveMethodNotFoundExceptionSieveIncompatibleMethodExceptionSieveException(wrapper for unexpected runtime errors)
Express error middleware example:
import {
SieveException,
SieveIncompatibleMethodException,
SieveMethodNotFoundException,
} from "@mohasinac/sievejs";
app.use((error, req, res, next) => {
if (
error instanceof SieveMethodNotFoundException ||
error instanceof SieveIncompatibleMethodException ||
error instanceof SieveException
) {
return res.status(400).json({
type: error.name,
message: error.message,
});
}
next(error);
});Integrations
Express helper
import { createExpressSieveMiddleware } from "@mohasinac/sievejs/integrations";Next App Router helper
import { createNextRouteHandler } from "@mohasinac/sievejs/integrations";Adapters
Prisma
const processor = new SieveProcessorBase({
adapter: createPrismaAdapter(),
fields: {
title: { canFilter: true, canSort: true },
},
});
const args = processor.apply(req.query, {});
const rows = await prisma.post.findMany(args);SQL Server
const plan = processor.apply(req.query, {
whereClauses: [],
orderByClauses: [],
params: [],
});
const { text, params } = buildSqlServerQuery(plan, {
table: "dbo.Posts",
columns: "*",
});Firebase
const processor = new SieveProcessorBase({
adapter: createFirebaseAdapter(),
fields: {
tags: { canFilter: true, canSort: false },
created: { canFilter: true, canSort: true },
},
});
const query = processor.apply(req.query, db.collection("posts"));
const snapshot = await query.get();Firebase limitations:
containsmaps toarray-containsstartsWithuses range query (>= value,<= value + "\uf8ff")endsWith, case-insensitive operators, and negated string operators are not supported by Firestore queries
Couchbase
const plan = processor.apply(req.query, {
whereClauses: [],
orderByClauses: [],
parameters: {},
parameterIndex: 0,
});
const { statement, parameters } = buildCouchbaseQuery(plan, {
bucket: "blog",
scope: "inventory",
collection: "posts",
});Pipes
import { createSievePipe } from "@mohasinac/sievejs";
const sievePipe = createSievePipe({ processor });
const query = sievePipe(req.query, db("posts"));Module API cheat sheet
Use direct imports when you only need specific modules:
- Core:
import { createSieveProcessor, createSieveMiddleware, createSievePipe } from "@mohasinac/sievejs" - Attributes + metadata:
import { Sieve, SieveAttribute, buildFieldsFromClass, getSieveMetadata } from "@mohasinac/sievejs/attributes" - Exceptions:
import { SieveException } from "@mohasinac/sievejs/exceptions" - Extensions:
import { getMethodExt } from "@mohasinac/sievejs/extensions" - Models:
import { SieveModel } from "@mohasinac/sievejs/models" - Services:
import { SieveProcessorBase } from "@mohasinac/sievejs/services" - Pipes:
import { createSievePipe } from "@mohasinac/sievejs/pipes" - Integrations:
import { createExpressSieveMiddleware, createNextRouteHandler } from "@mohasinac/sievejs/integrations" - Adapters:
import { createKnexAdapter } from "@mohasinac/sievejs/adapters/knex"import { createMongooseAdapter } from "@mohasinac/sievejs/adapters/mongoose"import { createPrismaAdapter } from "@mohasinac/sievejs/adapters/prisma"import { createSqlServerAdapter, buildSqlServerQuery } from "@mohasinac/sievejs/adapters/sqlserver"import { createFirebaseAdapter } from "@mohasinac/sievejs/adapters/firebase"import { createCouchbaseAdapter, buildCouchbaseQuery } from "@mohasinac/sievejs/adapters/couchbase"
Upgrading
Migrating from .NET Sieve to SieveJS
- Replace attribute annotations with JS metadata (
Sieve(...)) orfieldsconfig. - Replace
IQueryablepipeline with adapter-native query objects (Knex,Prisma args, etc.). - Replace ASP.NET model binding with integration
requestModelmapping. - Keep filtering/sorting/pagination at query-builder/database layer.
Updating within SieveJS
- Follow SemVer tags/releases.
- Read release notes before upgrading minor versions while in
0.x. - Re-run
npm testandnpm pack --dry-runafter upgrades.
Included samples
See sample/ for working examples:
sample/server.js(Express +createExpressSieveMiddleware)sample/next-route.example.js(Next Route Handler +createNextRouteHandler+ Firebase adapter)sample/advanced-modules.example.js(metadata, attributes, extensions, exceptions, models, pipes, services)sample/sieve.config.json
License & Contributing
- License: MIT (
LICENSE) - Contributions are welcome via issues and pull requests.
