Package Exports
- @eurika/prisma-charts
- @eurika/prisma-charts/condition
- @eurika/prisma-charts/pieStats
- @eurika/prisma-charts/timeline
- @eurika/prisma-charts/types
Readme
prisma-toolkit
Utility helpers for building chart-ready queries with Prisma — pie/donut distributions, time-series timelines, and reusable filter builders. Designed for admin panels (react-admin, Refine, etc.) and dashboard APIs backed by PostgreSQL.
Install
npm install prisma-toolkit
# or
pnpm add prisma-toolkitRequires Prisma >= 5.0.0 as a peer dependency.
Modules
| Module | Purpose |
|---|---|
| condition | Convert a generic filter input (search, date range, enabled) into a Prisma where clause |
| pieStats | Build pie/donut chart data — count or aggregate (sum/avg/min/max) across named slices |
| timeline | Build parametrized SQL for time-bucketed aggregations (PostgreSQL date_trunc) |
Condition — Generic Filter Builder
Converts a common FilterInput shape into a Prisma where object.
import { buildFilterWhere, mergeWhere } from "prisma-toolkit";
// From your GraphQL / REST filter input
const filter = { query: "john", createdFrom: new Date("2024-01-01"), isEnabled: true };
const where = buildFilterWhere(filter, {
queryFields: ["name", "email"], // OR-search across these fields
enabledField: "isEnabled", // defaults to "isEnabled"
createdAtField: "createdAt", // defaults to "createdAt"
});
const users = await prisma.user.findMany({ where });Merging with scope
const tenantScope = { tenantId: ctx.auth.tenantId };
const merged = mergeWhere(tenantScope, where);
// → { AND: [{ tenantId: "..." }, { ...filterWhere }] }
const users = await prisma.user.findMany({ where: merged });PieStats — Pie / Donut Chart Data
Build distribution data by running parallel count or aggregate queries per slice.
Count-based pie (e.g., Enabled vs Disabled)
import {
andWhere,
buildPieGlobalWhere,
type PieFilterOptions,
type PieSlice,
} from "prisma-toolkit";
import { Prisma } from "@prisma/client";
// 1. Define global filters
const filterOpts: PieFilterOptions<Prisma.UserWhereInput> = {
baseWhere: { tenantId: ctx.auth.tenantId },
query: args.filter?.query ?? null,
searchFields: ["name", "email"],
audited: {
createdFrom: args.filter?.createdFrom,
createdTo: args.filter?.createdTo,
dateField: "createdAt",
},
};
const globalWhere = buildPieGlobalWhere<Prisma.UserWhereInput>(filterOpts);
// 2. Define slices
const slices: PieSlice<Prisma.UserWhereInput>[] = [
{ key: "Enabled", where: { isEnabled: true } },
{ key: "Disabled", where: { isEnabled: false } },
];
// 3. Run in parallel
const data = await Promise.all(
slices.map(async (slice) => {
const where = andWhere(globalWhere, slice.where);
const value = await prisma.user.count({ where: where ?? undefined });
return { key: slice.key, value };
}),
);
// → [{ key: "Enabled", value: 142 }, { key: "Disabled", value: 23 }]Aggregate-based pie (e.g., Revenue by status)
import {
buildAggregateArgs,
readAggregateValue,
andWhere,
buildPieGlobalWhere,
} from "prisma-toolkit";
const globalWhere = buildPieGlobalWhere<Prisma.OrderWhereInput>(filterOpts);
const slices = [
{ key: "Completed", where: { status: "COMPLETED" } },
{ key: "Pending", where: { status: "PENDING" } },
];
const data = await Promise.all(
slices.map(async (slice) => {
const where = andWhere(globalWhere, slice.where);
const aggArgs = buildAggregateArgs("sum", "totalAmount", where);
const agg = await prisma.order.aggregate(aggArgs);
const value = readAggregateValue("sum", "totalAmount", agg);
return { key: slice.key, value };
}),
);
// → [{ key: "Completed", value: 54320.50 }, { key: "Pending", value: 12100.00 }]Custom search builder (nested relations)
const filterOpts: PieFilterOptions<Prisma.UserWhereInput> = {
query: args.filter?.query ?? null,
searchBuilder: (q) => ({
identities: {
some: { normalizedValue: { contains: q, mode: "insensitive" } },
},
}),
};Timeline — Time-Series Aggregation
Builds parametrized PostgreSQL SQL for date_trunc-based time bucketing. Returns { sql, params } for use with prisma.$queryRawUnsafe.
Basic usage
import { buildTimelineQuery, mapTimelineRowsToPoints } from "prisma-toolkit";
const query = buildTimelineQuery({
table: "users",
dateColumn: "created_at",
op: "count",
unit: "month", // "hour" | "day" | "week" | "month" | "year"
tz: "America/New_York",
createdFrom: new Date("2024-01-01"),
createdTo: new Date("2024-12-31"),
});
const rows = await prisma.$queryRawUnsafe(query.sql, ...query.params);
const points = mapTimelineRowsToPoints(rows);
// → [{ x: "2024-01-01T05:00:00.000Z", y: 42 }, { x: "2024-02-01T05:00:00.000Z", y: 58 }, ...]With tenant scoping (customWhere)
const query = buildTimelineQuery({
table: "public.users",
dateColumn: "created_at",
op: "count",
unit: "day",
createdFrom: args.filter?.createdFrom,
createdTo: args.filter?.createdTo,
isEnabled: args.filter?.isEnabled,
customWhere: tenantId
? [{ sql: `"tenant_id" = $1`, params: [tenantId] }]
: [],
});
const rows = await prisma.$queryRawUnsafe(query.sql, ...query.params);
const points = mapTimelineRowsToPoints(rows);
return points.map((p) => ({ x: new Date(p.x), y: p.y }));Aggregate timeline (e.g., revenue over time)
const query = buildTimelineQuery({
table: "orders",
dateColumn: "completed_at",
op: "sum",
valueColumn: "total_amount",
unit: "month",
});API Reference
condition.ts
| Export | Description |
|---|---|
buildFilterWhere(filter, options) |
Converts CommonFilterInput → Prisma where clause |
mergeWhere(base, extra) |
AND-merges two where objects; handles undefined |
CommonFilterInput |
{ query?, createdFrom?, createdTo?, isEnabled? } |
FilterOptions |
{ enabledField?, createdAtField?, queryFields? } |
pieStats.ts
| Export | Description |
|---|---|
andWhere(...clauses) |
AND-combine where fragments, skipping falsy |
buildPieGlobalWhere(opts) |
Combine base + query + audited + enableable filters |
buildPieQueryWhere(opts) |
Build text search OR clause |
buildPieAuditedWhere(opts) |
Build date range clause |
buildPieEnableableWhere(opts) |
Build boolean flag clause |
buildAggregateArgs(op, field, where) |
Build Prisma .aggregate() args |
readAggregateValue(op, field, agg) |
Extract number from aggregate result |
timeline.ts
| Export | Description |
|---|---|
buildTimelineQuery(opts) |
Build parametrized SQL for time-bucketed aggregation |
mapTimelineRowsToPoints(rows) |
Normalize raw DB rows to { x, y } chart points |
aggregateExpr(op, col?) |
Generate SQL aggregate expression |
quoteIdentChain(input) |
Validate & quote SQL identifiers |
types.ts
| Export | Description |
|---|---|
AggregateOp |
"count" | "sum" | "avg" | "min" | "max" |
NumericAggregateOp |
AggregateOp excluding "count" |
TimeUnit |
"hour" | "day" | "week" | "month" | "year" |
Security
- SQL identifiers are validated and quoted via
quoteIdentChain(rejects anything outside[A-Za-z0-9_.]) - Values are always passed as parametrized
$Nbindings — never interpolated - Timezone strings are validated against an IANA pattern before use
- Pie helpers produce Prisma
whereobjects — Prisma handles parameterization
License
MIT