Collection of decorators, utilities and services for building Ts.ED backend applications with Prisma.
npm install @januscaler/tsed-helperNine filter modes translate declarative search payloads into Prisma queries. Each mode behaves differently depending on the field's Prisma scalar type.
| Mode | Description | String | Int/Float/Decimal | DateTime | Boolean | Relation |
|---|---|---|---|---|---|---|
EQ |
Equals / Matches | contains + insensitive |
equals |
Date-only (YYYY-MM-DD) auto-ranges full day; otherwise equals |
equals |
some.id.in (array) or equals (scalar) |
EX |
Excludes / Not match | not.contains |
not.equals |
not.equals |
not.equals |
none.id.in (array) or not.in |
LT |
Less than | — | < |
< Date |
— | — |
LTE |
Less than or equal | — | <= |
<= Date |
— | — |
GT |
Greater than | — | > |
> Date |
— | — |
GTE |
Greater than or equal | — | >= |
>= Date |
— | — |
EM |
Empty / Null | sets field to null |
sets field to null |
sets field to null |
— | — |
NEM |
Not empty / Not null | not: null |
not: null |
not: null |
— | — |
RG |
Range | — | gte + lte |
gte start + < (end+1 day) |
— | — |
// Single filter group (AND within, OR across groups)
{
"filters": [
{ "name": { "mode": "EQ", "value": "john" } }
]
}
// Multiple OR groups
{
"filters": [
{ "name": { "mode": "EQ", "value": "john" }, "age": { "mode": "GT", "value": 18 } },
{ "email": { "mode": "EQ", "value": "admin@acme.io" } }
]
}
// Date-only search (auto-ranges 2024-01-15 → whole day)
{ "createdAt": { "mode": "EQ", "value": "2024-01-15" } }
// Relation filter
{ "roles": { "mode": "EQ", "value": [1, 2], "isRelation": true } }
// Range search
{ "price": { "mode": "RG", "value": [5, 30] } }
{ "createdAt": { "mode": "RG", "value": ["2024-10-13T16:15:57.132Z", "2024-10-20T16:15:57.132Z"] } }Composable decorators and helpers that build Swagger docs for CRUD endpoints from your Prisma schema.
createItem, updateItem, deleteItem, getItem, and getItems wrap Ts.ED HTTP decorators with consistent summaries and response models. getItems emits a typed { items, total } payload.
import { createItem, getItems } from "@januscaler/tsed-helper";
import { UserModel } from "@tsed/prisma";
@Controller("/users")
export class UsersController {
@createItem({ model: UserModel })
async create(@BodyParams() data: any) { return this.service.create(data); }
@getItems({ model: UserModel })
async getAll(@BodyParams(UserSearchParams) params: UserSearchParams) {
return this.service.getAll({ ...params });
}
}makeSearchParamsForPrismaModel('User') inspects your Prisma DMMF to generate search filter examples, orderBy hints, and nested relation field hints for Swagger. The returned class extends BaseSearchParams with fields, orderBy, and filters properties.
import { makeSearchParamsForPrismaModel } from "@januscaler/tsed-helper";
import { UserModel } from "@tsed/prisma";
const BaseUserSearchParams = await makeSearchParamsForPrismaModel(UserModel.name);
export class UserSearchParams extends BaseUserSearchParams {}Extend the generated class to attach validation decorators or expose a named DTO:
@getItems({ model: UserModel })
async getAll(@BodyParams(UserSearchParams) searchParams: UserSearchParams) {
return this.service.getAll({ ...searchParams });
}| Property | Type | Default | Description |
|---|---|---|---|
limit |
number |
10 |
Page size |
offset |
number |
0 |
Pagination offset |
fields |
string[] |
— | Prisma select fields. Supports dot notation for relations (roles.name). Validated against the model — see Field validation |
orderBy |
Record<string, 'asc' | 'desc'> |
{ id: 'asc' } |
Sort specification |
filters |
SearchFilterRecord[] |
— | Array of filter groups, each OR'd together |
countTotal |
boolean |
true |
When false, skips the count query for faster response |
The BaseService<T, M> class in src/baseService.ts wraps a Prisma repository with lifecycle events, relation mapping, computed fields, and search infrastructure.
import { BaseService } from "@januscaler/tsed-helper";
import { PrismaService } from "@tsed/prisma";
@Injectable()
export class UsersService extends BaseService<Prisma.UsersDelegate, UsersModel> {
constructor(prisma: PrismaService) {
super("UsersModel", prisma);
}
}| Parameter | Type | Description |
|---|---|---|
tsedPrismaModelName |
string |
Ts.ED Prisma model name (e.g. "UsersModel") |
prismaService |
PrismaService |
Injected Prisma service instance |
relativePrismaFilePath |
string? |
Override schema path (default "./prisma/schema.prisma") |
RxJS Subject instances broadcast CRUD events for side effects like caching, auditing, or logging:
service.onPostUpdate.subscribe(({ id, result }) => logger.info({ id, result }));
service.onPreCreate.subscribe(({ data }) => validateData(data));| Hook | Emits |
|---|---|
onPreCreate |
{ data } |
onPostCreate |
{ data, result } |
onPreUpdate |
{ id, inputData } |
onPostUpdate |
{ id, inputData, result } |
onPreDelete |
{ id } |
onPostDelete |
{ id, result } |
Creates a record with optional relation operations. Lifecycle hooks fire before and after.
await service.create({ title: "Draft", tags: [1, 2] }, {
relationOperation: "connect",
relationvalueMapper: (field, ids) => ids.map((id) => ({ id })),
});Updates a record, splitting scalar fields from relation fields automatically.
await service.update(42, { title: "Published", tags: [3, 4] }, {
relationOperation: "set",
relationvalueMapper: (field, ids) => ids.map((id) => ({ id })),
});Deletes by id. Fires onPreDelete and onPostDelete.
Returns the record or null if not found.
Batch fetch multiple records by their ids. Optional select for field selection.
const users = await service.getManyByIds([1, 2, 3], { id: true, name: true });Full search with filters, ordering, field selection, and computed field support. Two execution paths:
- Fast path: When no computed fields are active, uses a single Prisma query with aggregate count.
- In-memory path: When computed fields are involved, fetches up to 10,000 records, applies computed filters/sorts in JS, then paginates.
const { items, total } = await service.getAll({
fields: ["name", "email", "roles.name"],
filters: [{ name: { mode: "EQ", value: "john" } }],
orderBy: { name: "asc" },
offset: 0,
limit: 20,
countTotal: true,
});getAll checks every fields[] entry against the model's DMMF before building the select, and throws BadRequest naming the offending entry and listing what is selectable. It rejects an unknown root, an unknown column on a real relation, dotted access through a scalar or Json column, and nesting deeper than one level.
Without this, an unselectable field reaches Prisma, which raises PrismaClientValidationError — not an HttpException, so Ts.ED reports a malformed request as a bare 500 with the reason visible only in the server log.
Computed field names are always allowed (they are stripped or $extends-resolved later and never appear in the DMMF). Validation is skipped entirely when schema metadata has not loaded, so Prisma stays the fallback authority.
Count records matching optional Prisma where clause.
Check if a record with the given id exists. Returns boolean.
Prisma upsert — creates if not found, updates if exists.
The buildPayloadWithRelations helper in src/relationPayload.ts automatically splits incoming data into scalar fields and relation fields, applying Prisma relation operations to each relation key.
Create relation operations: connect (default), connectOrCreate, create, createMany
Update relation operations: set (default), disconnect, delete, connect, disconnectMany, deleteMany, create, createMany, update, updateMany, upsert, upsertMany
type CreateRelationMapper = {
relationOperation?: 'connect' | 'connectOrCreate' | 'create' | 'createMany';
relationvalueMapper?: (fieldName: string, value: any) => any;
};
type UpdateRelationMapper = {
relationOperation?: 'set' | 'disconnect' | 'delete' | 'connect'
| 'disconnectMany' | 'deleteMany' | 'create' | 'createMany'
| 'update' | 'updateMany' | 'upsert' | 'upsertMany';
relationvalueMapper?: (fieldName: string, value: any) => any;
};The default relationvalueMapper wraps array values as [{ id }, ...] and scalar values as { id }.
create, update, and deleteItem accept an optional tx parameter (Prisma interactive transaction client). When provided, the operation runs within that transaction instead of the default repository:
await prisma.$transaction(async (tx) => {
const user = await service.create({ name: "Alice" }, {}, tx);
await service.update(user.id, { verified: true }, {}, tx);
});Define virtual fields computed from Prisma data. The search infrastructure splits filters and orderBy between Prisma-native and computed fields, applying in-memory filtering/sorting/pagination when computed fields are involved.
service.computedFields = [
{
name: "fullName",
needs: ["firstName", "lastName"],
compute: (user) => `${user.firstName} ${user.lastName}`,
type: "string",
filterable: true,
sortable: true,
},
{
name: "age",
needs: ["birthDate"],
compute: (user) => calculateAge(user.birthDate),
type: "number",
filterable: true,
sortable: true,
},
{
name: "roleDisplay",
needs: ["role"],
compute: (user) => user.role?.name ?? "None",
type: "string",
prismaComputed: true, // Prisma $extends already computes this
},
];| Property | Type | Description |
|---|---|---|
name |
string |
Field name exposed in the API |
needs |
string[] |
Prisma fields required for computation (auto-injected into select) |
compute |
(item) => any |
Synchronous or async computation function |
type |
'boolean' | 'number' | 'string' |
Value type for filter coercion |
filterable |
boolean? |
Allow filtering by this field (default false) |
sortable |
boolean? |
Allow sorting by this field (default false) |
prismaComputed |
boolean? |
When true, field is NOT stripped from Prisma select and NOT re-attached after query (Prisma $extends already provides it) |
Use extend() to register Prisma $extends computed fields that Prisma evaluates at query time:
service.extend({
roleDisplay: {
needs: { role: true },
compute: (user) => user.role?.name ?? "None",
},
});The filterMappers object in src/filterMappers.ts maps filter modes to Prisma query builders and is available for direct import:
import { filterMappers, isNumericType } from "@januscaler/tsed-helper";src/prismaMetaMapper.ts parses your Prisma schema once and caches the Data Model Meta Format (DMMF) for fast subsequent access.
PrismaMetaMapper.getTablesInfo()returns a model map with field metadata, unique constraints, and primary keys. Results are cached after the first call.PrismaMetaMapper.getEntityFieldMapping('User')produces a keyed object describing field types, relations, and default values.PrismaMetaMapper.invalidateCache()clears the DMMF cache — useful after schema changes at runtime.PrismaMetaMapper.normalizeEntityName('UsersModel')strips theModelsuffix and pascalCases (returns"Users").- Set
PrismaMetaMapper.relativePrismaFilePathbefore bootstrapping if your schema file lives elsewhere.
PrismaMetaMapper.relativePrismaFilePath = "./apps/api/prisma/schema.prisma";
const usersInfo = await PrismaMetaMapper.getEntity("User");
const emailField = usersInfo.fields.find((field) => field.name === "email");Seed orchestration utilities live in src/seederHelper.ts and help discover, order, and execute seed scripts alongside Prisma relations.
getAllSeeds()globs controllers (default./src/controllers/rest/**/seed.ts) and loads their exports under a keyed entity map.generatePrismaCreateUpdatePayload()transforms plain seed rows into Prismacreatepayloads, automatically resolving relation IDs or eager-connect requests (*).sortEntitiesByDependency()performs a topological sort so dependent seeds run after their prerequisites.
const helper = new SeederHelper();
const seeds = await helper.getAllSeeds();
const order = helper.sortEntitiesByDependency(seeds, ["roles", "users"]);
for (const entity of order) {
const payload = await helper.generatePrismaCreateUpdatePayload({
rowData: seeds[entity].rows,
entity,
prismaService,
});
await prismaService[entity].createMany({ data: payload });
}Common search filter types are defined in src/types.ts and exported through the package entry point.
SearchFilterValuedescribes the dynamic filter payload{ mode, value, isRelation }used in request bodies.SearchFilterRecordmaps entity field names toSearchFilterValuedescriptors and is leveraged byBaseService#getAllwhen building Prisma filters.
const filters: SearchFilterRecord = {
email: { mode: "EQ", value: "admin@acme.io" },
roles: { mode: "EQ", value: [1, 2], isRelation: true },
};The barrel file src/index.ts re-exports all modules:
| Module | Export path | Description |
|---|---|---|
baseCrud |
@januscaler/tsed-helper |
Controller decorators, SearchParams, makeSearchParamsForPrismaModel |
baseService |
@januscaler/tsed-helper |
BaseService class with CRUD, lifecycle, computed fields, search |
prismaMetaMapper |
@januscaler/tsed-helper |
DMMF parser, entity metadata, caching |
seederHelper |
@januscaler/tsed-helper |
Seed discovery, ordering, Prisma payload generation |
types |
@januscaler/tsed-helper |
FilterMode, SearchFilterValue, SearchFilterRecord |
filterMappers |
@januscaler/tsed-helper |
Filter mode → Prisma query mappers, isNumericType helper |
relationPayload |
@januscaler/tsed-helper |
buildPayloadWithRelations for scalar/relation field splitting |
searchHelpers |
@januscaler/tsed-helper |
Search param splitting, computed field pipeline, in-memory sort/filter/paginate |