← All @molecule/* packages · App templates
@molecule/api-resource-readingsAPI resource · resource-readings · API (Node) · v1.0.1 · Apache-2.0
Generic time-series with on-the-fly aggregation
npm install @molecule/api-resource-readings@molecule/api-resource-readings is an API resource: the routes, validation and storage for resource-readings, built on the database and auth cores so it runs on whichever providers your app has bonded.
import { createReadingsRouter } from '@molecule/api-resource-readings'
// Mount behind your global auth middleware — every route requires a session.
app.use('/readings', createReadingsRouter())
// POST /readings — ingest one reading
// POST /readings/bulk — ingest up to 10 000 readings
// GET /readings?granularity=raw|5min|hour|day&sensor_id=…&metric=…&from=…&to=…Works with: @molecule/api-bonds-default-express, @molecule/api-database, @molecule/api-i18n, @molecule/api-middleware-validation
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
@molecule/api-resource-readings — generic time-series sensor data.
Ingest readings via POST / or POST /bulk, then query raw or
aggregated (5min / hour / day rollups) via GET /?granularity=….
Extracted from the energy-monitoring flagship — the pattern works for any time-series surface (IoT sensors, app metrics, financial tick data, etc.).
import { createReadingsRouter } from '@molecule/api-resource-readings'
// Mount behind your global auth middleware — every route requires a session.
app.use('/readings', createReadingsRouter())
// POST /readings — ingest one reading
// POST /readings/bulk — ingest up to 10 000 readings
// GET /readings?granularity=raw|5min|hour|day&sensor_id=…&metric=…&from=…&to=…
import { ingestReading, listAggregatedReadings } from '@molecule/api-resource-readings'
await ingestReading(userId, { sensor_id: 'meter-1', metric: 'kwh', value: 1.42 })
const hourly = await listAggregatedReadings(userId, { granularity: 'hour', metric: 'kwh' })
resource
npm install @molecule/api-resource-readings @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-middleware-validation express zod
npm install -D @types/express
AggregatedPointA time-bucketed aggregate of sensor readings (min/max/avg/sum/count per bucket).
interface AggregatedPoint {
bucket_at: string
metric: string
sensor_id: string
min: number
max: number
avg: number
sum: number
count: number
}
ReadingPointA single sensor reading data point returned from a query.
interface ReadingPoint {
recorded_at: string
value: number
metric: string
sensor_id: string
unit: string | null
}
ReadingRowRaw database row shape for a single sensor reading record.
interface ReadingRow {
id: string
owner_id: string
sensor_id: string
metric: string
value: number
unit: string | null
recorded_at: string | Date
metadata: Record<string, unknown> | null
}
GranularityTime-bucket granularity options for aggregating sensor readings.
type Granularity = 'raw' | '5min' | 'hour' | 'day'
countReadings(ownerId)Return the total number of readings stored for the given owner.
function countReadings(ownerId: string): Promise<number>
createReadingsRouter()Creates and returns the Express router for the readings resource endpoints.
function createReadingsRouter(): Router
ingestBulk(ownerId, readings)Persist multiple sensor readings in sequence, returning the number successfully inserted.
function ingestBulk(
ownerId: string,
readings: {
sensor_id: string
metric: string
value: number
unit?: string | null
recorded_at?: string
metadata?: Record<string, unknown>
}[],
): Promise<number>
ingestReading(ownerId, data)Persist a single sensor reading row for the given owner.
function ingestReading(
ownerId: string,
data: {
sensor_id: string
metric: string
value: number
unit?: string | null
recorded_at?: string
metadata?: Record<string, unknown>
},
): Promise<ReadingRow>
listAggregatedReadings(ownerId, opts)Group readings into time buckets and aggregate per bucket. Returns
min/max/avg/sum/count per (bucket, metric, sensor_id).
function listAggregatedReadings(
ownerId: string,
opts: {
granularity: Exclude<Granularity, 'raw'>
sensor_id?: string
metric?: string
from?: string
to?: string
limit?: number
},
): Promise<AggregatedPoint[]>
listRawReadings(ownerId, opts?)Fetch raw, unaggregated reading points for an owner, optionally filtered by sensor, metric, and time range.
function listRawReadings(
ownerId: string,
opts?: { sensor_id?: string; metric?: string; from?: string; to?: string; limit?: number },
): Promise<ReadingPoint[]>
GRANULARITIESSupported time-bucket granularities for reading aggregation queries.
const GRANULARITIES: readonly ['raw', '5min', 'hour', 'day']
readingBulkSchemaZod schema for validating a bulk readings creation payload (1–10 000 entries).
const readingBulkSchema: z.ZodObject<
{
readings: z.ZodArray<
z.ZodObject<
{
sensor_id: z.ZodString
metric: z.ZodString
value: z.ZodNumber
unit: z.ZodOptional<z.ZodNullable<z.ZodString>>
recorded_at: z.ZodOptional<z.ZodString>
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
},
z.core.$strip
>
>
},
z.core.$strip
>
readingCreateSchemaZod schema for validating a single reading creation payload.
const readingCreateSchema: z.ZodObject<
{
sensor_id: z.ZodString
metric: z.ZodString
value: z.ZodNumber
unit: z.ZodOptional<z.ZodNullable<z.ZodString>>
recorded_at: z.ZodOptional<z.ZodString>
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
},
z.core.$strip
>
readingQuerySchemaZod schema for validating reading list/query filter parameters.
const readingQuerySchema: z.ZodObject<
{
sensor_id: z.ZodOptional<z.ZodString>
metric: z.ZodOptional<z.ZodString>
from: z.ZodOptional<z.ZodString>
to: z.ZodOptional<z.ZodString>
granularity: z.ZodOptional<z.ZodEnum<{ raw: 'raw'; '5min': '5min'; hour: 'hour'; day: 'day' }>>
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>
},
z.core.$strip
>
Peer dependencies:
@molecule/api-bonds-default-express ^1.0.1@molecule/api-database ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-middleware-validation ^1.0.1express ^5.0.0zod ^4.0.0@molecule/api-bonds-default-express@molecule/api-database@molecule/api-i18n@molecule/api-middleware-validationexpresszodUnlike declarative-route resources, this package ships an Express Router
FACTORY (createReadingsRouter()) — there is no routes /
requestHandlerMap export for mlcl inject; mount the router yourself.
Every route reads the caller via requireUser(res)
(res.locals.session.userId, 401 fail-closed), so it must sit behind the
global auth middleware, and every query/insert is scoped to that owner —
never accept a client-supplied owner id.
Aggregated queries (granularity ≠ raw) run raw SQL using date_trunc,
::int casts, and interval literals — PostgreSQL-only. On the
SQLite/MySQL bonds use granularity=raw (DataStore-based, portable) and
bucket in application code, or supply your own dialect's aggregation.
ingestBulk inserts sequentially (one INSERT per reading, max 10 000 per
request).
Tables: src/__setup__/readings.sql creates readings (owner-scoped via
owner_id). An mlcl-scaffolded API replays __setup__/*.sql automatically
on migrate; anywhere else run it once — nothing at runtime creates them.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is a correctness bug to fix — not a skip: