@bitbeater/ssr
v2.1.2
Published
Tiny TypeScript typings to describe search filters, selected fields, ordering, and pagination for repositories. It’s runtime-agnostic (ORM/DB/framework independent) and focuses on strong compile-time safety for your query shape.
Readme
@bitbeater/ssr — Type-safe search, fields, order, and pagination
Tiny TypeScript typings to describe search filters, selected fields, ordering, and pagination for repositories. It’s runtime-agnostic (ORM/DB/framework independent) and focuses on strong compile-time safety for your query shape.
Key ideas:
- Search: deep, type-safe filters with operators (equal, like, greater, lesser, between, not)
- Fields: precise field selection, including nested objects and arrays
- Order: multi-field ordering with direction, nulls handling, and priority
- Pagination: page + pageSize with optional search/order/fields
- Repository: a minimal interface to wire your data source
Install
npm i @bitbeater/ssrQuick start
import {
Fields,
Order,
PaginatedResult,
PaginatedSearch,
Repository,
Search,
} from '@bitbeater/ssr';Person model used in examples
Includes number, string, object, date, array, and boolean types.
type Contact = {
type: 'email' | 'phone';
value: string;
verified: boolean;
};
type Person = {
id: number; // number
name: string; // string
age: number; // number
isActive: boolean; // boolean
createdAt: Date; // date
tags: string[]; // array of scalar
address: { // object
city: string;
zip: number;
};
contacts: Contact[]; // array of objects
notes?: string | null; // nullable field
};Search — type-safe filters
Operator shape for scalar fields:
- equal: T | T[]
- like: T
- range: { greater: T; lesser: T }
- not?: boolean (negate any of the above)
You can pass either a raw scalar (shorthand for equal) or a Find object with one operator. Nested objects and arrays are supported recursively.
import { Search } from '@bitbeater/ssr';
const search: Search<Person> = {
// scalars
name: { like: 'ali' },
age: { greater: 18 },
isActive: true, // shorthand for { equal: true }
createdAt: { greater: new Date('2024-01-01'), lesser: new Date('2024-12-31') },
// array of scalars -> filter by an element match
tags: { like: 'script' },
// or shorthand equal
// tags: 'typescript',
// nested object
address: {
city: { like: 'York' },
},
// array of objects -> filter by fields on the element type
contacts: {
verified: { equal: true },
type: { equal: ['email', 'phone'] },
},
};Notes:
- Arrays use the element type for filtering (semantics like “any element matches” are up to your implementation).
- Functions are excluded by design and cannot be filtered.
- Nullable fields are treated as their non-nullable type for filtering (i.e.,
string | nullis filtered asstring). If you need explicit null checks, model them in your repository implementation or extend the types to support it.
Fields — shape the returned data
Rules:
- true includes the field
- object selects nested fields recursively
- arrays accept either true (whole element) or a nested object describing the element shape
- functions are excluded
import { Fields } from '@bitbeater/ssr';
const fields: Fields<Person> = {
id: true,
name: true,
address: { city: true },
tags: true, // include array values
contacts: { type: true, verified: true },
// Omitting a key excludes it (unless the root is set to true)
};Order — multi-field ordering
Each ordered field accepts a strategy:
- direction: 'ASC' | 'DESC'
- nulls?: 'FIRST' | 'LAST'
- priority?: number (lower number = higher precedence)
import { Order } from '@bitbeater/ssr';
const order: Order<Person> = {
createdAt: { direction: 'DESC', nulls: 'LAST', priority: 1 },
name: { direction: 'ASC', priority: 2 },
// order within arrays-of-objects is expressed over the element type
contacts: { verified: { direction: 'DESC', priority: 1 } },
};PaginatedSearch and PaginatedResult
import { PaginatedSearch, PaginatedResult } from '@bitbeater/ssr';
const query: PaginatedSearch<Person> = {
page: 0, // default 0
pageSize: 20, // omit to fetch until the end
fields, // optional: return all when omitted/empty
order, // optional
search, // optional: match-all when omitted
};
// Your data source returns this shape
const result: PaginatedResult<Person> = {
page: 0,
pageSize: 20,
totalCount: 2,
items: [/* persons */],
};Repository interface
Bring your own persistence. These types describe a minimal repo surface.
import { Repository } from '@bitbeater/ssr';
const peopleRepo: Repository<Person> = {
save: async (people) => people, // create/update
search: async (q) => ({ page: q?.page ?? 0, pageSize: q?.pageSize, totalCount: 0, items: [] }),
remove: async (s) => [],
};Type-only package
This library exports TypeScript types with no runtime. Use them to type your query-building and repository contracts in any stack (SQL, NoSQL, REST, GraphQL, ORM, etc.).
API surface
- Fields: describe selected fields
- Search: describe filters by field with operators
- Order: describe ordering with direction/nulls/priority
- PaginatedSearch: page, pageSize, order, fields, search
- PaginatedResult: page, pageSize, totalCount, items
- Repository: save, search, remove function types
Query Builder
A simple SQLite query builder implementation is included to demonstrate how to use the types in practice. It supports basic filtering, field selection, ordering, and pagination.
import { buildQueryString } from '@bitbeater/ssr/query_builder/sqlite3';
import { Condition, OrderDirection } from '@bitbeater/ssr';
import { Metadata } from '@bitbeater/ssr';
import Database from 'better-sqlite3';
const db = new Database(':memory:');
const sql_schema = `
CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT,
parentId INTEGER,
addressId INTEGER,
FOREIGN KEY(parentId) REFERENCES person(id),
FOREIGN KEY(addressId) REFERENCES address(id)
);
CREATE TABLE bio (
id INTEGER PRIMARY KEY,
personId INTEGER UNIQUE,
height INTEGER,
eyeColor TEXT,
FOREIGN KEY(personId) REFERENCES person(id)
);
CREATE TABLE address (
id INTEGER PRIMARY KEY,
city TEXT,
zip INTEGER
);
CREATE TABLE tag (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE person_tags (
personId INTEGER,
tagId INTEGER,
FOREIGN KEY(personId) REFERENCES person(id),
FOREIGN KEY(tagId) REFERENCES tag(id)
);`
db.exec(sql_schema);
type Tag = {
id: number;
name: string
};
type Bio = {
height: number;
eyeColor: string;
};
type Address = {
city: string;
zip: number;
};
type Person = {
id: number;
name: string;
bio: Bio;
address: Address;
tags: Tag[];
children: Person[];
parent: Person;
};
const tagMetadata: Metadata<Tag> = {
tableName: 'tag',
id: 'id',
name: 'name',
}
const bioMetadata: Metadata<Bio> = {
tableName: 'bio',
height: 'height',
eyeColor: 'eyeColor',
}
const addressMetadata: Metadata<Address> = {
tableName: 'address',
city: 'city',
zip: 'zip',
}
const personMetadata: Metadata<Person> = {
tableName: 'person',
id: 'id',
name: 'name',
bio: {
targetRefKey: 'id',
sourceForeignkey: 'personId',
targetMetadata: bioMetadata
},
address: {
sourceForeignkey: 'addressId',
targetRefKey: 'id',
targetMetadata: addressMetadata
},
tags: {
bridgeTable: 'person_tags',
sourceRefKey: 'id',
targetRefKey: 'id',
bridgeSourceForeignKey: 'personId',
bridgeTargetForeignKey: 'tagId',
targetMetadata: tagMetadata
}
}
personMetadata.children = {
sourceRefKey: 'id',
targetForeignKey: 'parentId',
targetMetadata: personMetadata
};
personMetadata.parent = {
sourceForeignkey: 'parentId',
targetRefKey: 'id',
targetMetadata: personMetadata
};
// Example usage of buildQueryString
// equality condition
const [equalityQuery, queryParams] = buildQueryString<Person>({ search: { name: 'John' } }, personMetadata);
// equalityQuery: "SELECT person.* FROM person WHERE person.name = ?"
// queryParams: ['John']
console.log(equalityQuery, queryParams);
// range condition
const [rangeQuery, rangeParams] = buildQueryString<Person>({ search: { id: { $_gt: 5, $_lt: 10 } } }, personMetadata);
// rangeQuery: "SELECT person.* FROM person WHERE person.id > ? AND person.id < ?"
// rangeParams: [5, 10]
console.log(rangeQuery, rangeParams);
// like condition
const [likeQuery, likeParams] = buildQueryString<Person>({ search: { name: { [Condition.LIKE]: 'Jo%' } } }, personMetadata);
// likeQuery: "SELECT person.* FROM person WHERE person.name LIKE ?"
// likeParams: ['Jo%']
console.log(likeQuery, likeParams);
// combined conditions with ordering and pagination
const [complexQuery, complexParams] = buildQueryString<Person>({
search: {
name: { [Condition.LIKE]: 'Jo%' },
id: { [Condition.GREATER]: 5 }
},
order: { name: { direction: OrderDirection.ASC } },
page: 0,
pageSize: 10
}, personMetadata);
// complexQuery: "SELECT person.* FROM person WHERE person.name LIKE ? AND person.id > ? ORDER BY person.name ASC LIMIT ? OFFSET ?"
// complexParams: ['Jo%', 5, 10, 0]
console.log(complexQuery, complexParams);
let res = db.prepare(equalityQuery).run(queryParams);
res = db.prepare(rangeQuery).run(rangeParams);
res = db.prepare(likeQuery).run(likeParams);
res = db.prepare(complexQuery).run(complexParams);License
ISC
