npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-resource-journal-entry

v1.0.1

Published

Owner-scoped journal entry resource with optional at-rest encryption, mood linkage, daily streaks, and JSON/CSV/TXT export. Extracted from mental-health-journal flagship.

Readme

@molecule/api-resource-journal-entry

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.ts JSDoc, not this file.

@molecule/api-resource-journal-entry — owner-scoped diary entries with optional at-rest encryption, mood-score linkage, daily-write streaks, and JSON/CSV/TXT export.

Extracted from the mental-health-journal flagship. When @molecule/api-encryption is bonded, entry bodies are stored encrypted-at-rest; otherwise the plaintext column is used (the read path falls back automatically so key rotation never bricks reads).

Quick Start

import { createJournalEntryRouter } from '@molecule/api-resource-journal-entry'
import express from 'express'

const app = express()
app.use('/api/journal', createJournalEntryRouter())

Type

resource

Installation

npm install @molecule/api-resource-journal-entry @molecule/api-bonds-default-express @molecule/api-database @molecule/api-encryption @molecule/api-i18n @molecule/api-logger @molecule/api-middleware-validation express zod
npm install -D @types/express

API

Interfaces

CreateEntryInput

Input fields accepted when creating a new journal entry.

interface CreateEntryInput {
  mood?: MoodLevel
  title?: string
  body: string
  tags?: string[]
  prompt_id?: string | null
  written_at?: string | Date
}

ExportRecord

Flattened, export-friendly representation of a single journal entry.

interface ExportRecord {
  id: string
  date: string
  title: string
  mood: MoodLevel
  tags: string[]
  word_count: number
  body: string
}

JournalEntryRow

Row shape persisted in journal_entries.

interface JournalEntryRow {
  id: string
  user_id: string
  written_at: string | Date
  title: string | null
  body: string | null
  body_encrypted: string | null
  body_iv: string | null
  mood_id: string | null
  prompt_id: string | null
  activity_ids: unknown
  tags: unknown
  word_count: number
  ai_summary: string | null
  ai_themes: unknown
  is_private: boolean
  created_at: string | Date
}

MoodEntryRow

Row shape persisted in mood_entries.

interface MoodEntryRow {
  id: string
  user_id: string
  recorded_at: string | Date
  score: number
  energy: number | null
  anxiety: number | null
  label: string | null
  activities: unknown
  notes: string | null
  journal_entry_id: string | null
  created_at: string | Date
}

PublicJournalEntry

Public-facing decrypted entry (returned by the service / routes).

interface PublicJournalEntry {
  id: string
  date: string
  title: string
  preview: string
  body: string
  mood: MoodLevel
  tags: string[]
  word_count: number
  ai_summary: string | null
  ai_themes: string[]
  prompt: string | null
}

UpdateEntryInput

Fields that can be patched on an existing journal entry.

interface UpdateEntryInput {
  mood?: MoodLevel
  title?: string
  body?: string
  tags?: string[]
}

Types

MoodLevel

Discrete mood-level labels mapped to a 1..5 score.

type MoodLevel = 'radiant' | 'good' | 'neutral' | 'low' | 'struggling'

Functions

computeStreak(userId)

Daily-write streak: consecutive distinct UTC days ending today/yesterday.

function computeStreak(userId: string): Promise<number>

createEntryForOwner(userId, input)

Create a journal entry and, if mood is provided, upsert today's mood_entries row + link it to the journal entry.

function createEntryForOwner(
  userId: string,
  input: CreateEntryInput,
): Promise<PublicJournalEntry | null>

createJournalEntryRouter()

Build the journal-entry router.

function createJournalEntryRouter(): Router

decryptIfNeeded(row)

Decrypt the entry body if encryption is bonded; otherwise return plaintext.

function decryptIfNeeded(row: JournalEntryRow): Promise<string>

deleteEntryForOwner(userId, id)

Owner-scoped delete — true if it deleted, false if not owned / missing.

function deleteEntryForOwner(userId: string, id: string): Promise<boolean>

exportEntries(userId, max?)

Serialize all entries for an owner into an export-friendly array.

function exportEntries(userId: string, max?: number): Promise<ExportRecord[]>

formatExport(records, format)

Format export records as JSON / CSV / TXT.

function formatExport(
  records: ExportRecord[],
  format: 'json' | 'csv' | 'txt',
): { contentType: string; body: string }

getEntryForOwner(userId, id)

Owner-scoped read — returns null when missing or not owned.

function getEntryForOwner(userId: string, id: string): Promise<PublicJournalEntry | null>

levelByScore(score)

Round + clamp a raw score and return its discrete label.

function levelByScore(score: number | null | undefined): MoodLevel

listEntriesForOwner(userId, limit?)

List the most-recent entries for an owner.

function listEntriesForOwner(userId: string, limit?: number): Promise<PublicJournalEntry[]>

normalizeScore(score)

Normalize a 1..5 score to 0..1 (useful for sparkline charts).

function normalizeScore(score: number | null | undefined): number

shapeEntry(entry, body)

Shape a row into the public-facing representation.

function shapeEntry(entry: JournalEntryRow, body: string): Promise<PublicJournalEntry>

updateEntryForOwner(userId, id, input)

Owner-scoped patch update — returns null when missing or not owned.

function updateEntryForOwner(
  userId: string,
  id: string,
  input: UpdateEntryInput,
): Promise<PublicJournalEntry | null>

Constants

createEntrySchema

Validator for creating a new journal entry.

const createEntrySchema: z.ZodObject<
  {
    mood: z.ZodOptional<
      z.ZodEnum<{
        radiant: 'radiant'
        good: 'good'
        neutral: 'neutral'
        low: 'low'
        struggling: 'struggling'
      }>
    >
    title: z.ZodOptional<z.ZodString>
    body: z.ZodString
    tags: z.ZodOptional<z.ZodArray<z.ZodString>>
    prompt_id: z.ZodOptional<z.ZodString>
    written_at: z.ZodOptional<z.ZodString>
  },
  z.core.$strip
>

moodLevelSchema

Mood level enum used by journal-entry payloads.

const moodLevelSchema: z.ZodEnum<{
  radiant: 'radiant'
  good: 'good'
  neutral: 'neutral'
  low: 'low'
  struggling: 'struggling'
}>

SCORE_BY_LEVEL

Maps each MoodLevel label to its corresponding numeric score (1..5).

const SCORE_BY_LEVEL: Record<MoodLevel, number>

updateEntrySchema

Validator for updating an existing journal entry.

const updateEntrySchema: z.ZodObject<
  {
    mood: z.ZodOptional<
      z.ZodOptional<
        z.ZodEnum<{
          radiant: 'radiant'
          good: 'good'
          neutral: 'neutral'
          low: 'low'
          struggling: 'struggling'
        }>
      >
    >
    title: z.ZodOptional<z.ZodOptional<z.ZodString>>
    tags: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>
    prompt_id: z.ZodOptional<z.ZodOptional<z.ZodString>>
    written_at: z.ZodOptional<z.ZodOptional<z.ZodString>>
    body: z.ZodOptional<z.ZodString>
  },
  z.core.$strip
>

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-bonds-default-express ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-database ^1.0.1
  • @molecule/api-encryption ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-middleware-validation ^1.0.1
  • express ^5.0.0
  • zod ^4.0.0

Runtime Dependencies

  • @molecule/api-bonds-default-express
  • @molecule/api-database
  • @molecule/api-encryption
  • @molecule/api-i18n
  • @molecule/api-logger
  • @molecule/api-middleware-validation
  • express
  • zod

Schema lives in __setup__/journal_entries.sql — two tables: journal_entries + mood_entries. Mood rows are upserted per (user, day) so multiple entries in a day share one mood row.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual journal screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. This is a PRIVATE journal, so privacy is the defining requirement:

  • [ ] Writing an entry persists its real fields — title, body, mood (radiant/good/neutral/low/struggling), tags, and date (written_at) — and the entry then appears in the author's journal list dated correctly (the list is newest-first by written_at) with a word_count matching the body.
  • [ ] Editing an entry (title / body / mood / tags) and deleting one both reflect in the UI immediately AND survive a reload — the change persisted server-side, not just local state.
  • [ ] If the UI filters or searches the list (by tag, mood, or date), only matching entries come back, and the mood shown per entry matches the mood saved for that day (mood rows upsert once per user per day).
  • [ ] If a write streak or count is shown, it equals the number of consecutive distinct days the author actually wrote, counted on the UTC day boundary and ending today or yesterday — add or remove entries across a day boundary and confirm it recomputes; a JSON/CSV/TXT export downloads only the author's own entries.
  • [ ] PRIVACY / AUTHORIZATION — entries are visible ONLY to their author. There is no sharing and no cross-user read path: signed in as a second user, requesting another user's entry id returns 404 (not-owned and truly missing are indistinguishable, so existence is never leaked), and the list and export return only your own rows — no admin or global view surfaces another user's entries. The owner is always the session user, never a userId taken from the request body, and journal content is never written to logs in the clear.