JSPM

@eurika/prisma-charts

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 9
  • Score
    100M100P100Q47152F
  • License MIT

Utility helpers for building chart-ready queries with Prisma — pie distributions, time-series timelines, and reusable filter builders for admin panels and dashboards.

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-toolkit

Requires 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 $N bindings — never interpolated
  • Timezone strings are validated against an IANA pattern before use
  • Pie helpers produce Prisma where objects — Prisma handles parameterization

License

MIT