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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@echoes-io/tracker

v1.2.0

Published

Type-safe SQLite database library for managing hierarchical story content.

Downloads

297

Readme

@echoes-io/tracker

Type-safe SQLite database library for managing hierarchical story content.

npm version License: MIT

Table of Contents

Features

  • Type-safe CRUD operations - Full TypeScript support with Kysely query builder
  • Automatic validation - All inputs validated using Zod schemas
  • Referential integrity - Foreign key constraints with cascade delete
  • SQLite storage - Fast, embedded database with better-sqlite3
  • Automatic migrations - Database schema updates on init
  • Hierarchical navigation - Navigate content using names/numbers, not IDs

Installation

npm install @echoes-io/tracker

Quick Start

import { Tracker } from '@echoes-io/tracker';

// Create tracker instance
const tracker = new Tracker('./echoes.db');

// Initialize database (runs migrations automatically)
await tracker.init();

// Create content
await tracker.createTimeline({
  name: 'my-story',
  description: 'A fantastic adventure'
});

await tracker.createArc({
  timelineName: 'my-story',
  name: 'arc-1',
  number: 1,
  description: 'The beginning'
});

// Query content
const arcs = await tracker.getArcs('my-story');

// Clean up
await tracker.close();

Content Hierarchy

The tracker manages a hierarchical content structure:

Timeline (story universe)
  └─ Arc (story phase)
      └─ Episode (story event)
          └─ Part (optional subdivision)
              └─ Chapter (individual content file)

Example:

  • Timeline: my-fantasy-world
    • Arc: the-beginning (number: 1)
      • Episode: awakening (number: 1)
        • Part: morning (number: 1)
          • Chapter: POV character chapter (number: 1)

API Reference

Tracker Class

Constructor

new Tracker(dbPath?: string)

Creates a new Tracker instance.

  • dbPath: Path to SQLite database file (default: ./tracker.db). Use :memory: for in-memory database.

init(): Promise<void>

Initializes the database schema and runs pending migrations. Must be called before any other operations.

close(): Promise<void>

Closes the database connection. Should be called when done to free resources.

Timeline Methods

createTimeline(data: Timeline): Promise<Timeline>

Creates a new timeline.

await tracker.createTimeline({
  name: 'my-story',
  description: 'A fantastic adventure'
});

getTimelines(): Promise<Timeline[]>

Retrieves all timelines.

getTimeline(name: string): Promise<Timeline | undefined>

Retrieves a specific timeline by name.

updateTimeline(name: string, data: Partial<Timeline>): Promise<Timeline>

Updates a timeline.

deleteTimeline(name: string): Promise<void>

Deletes a timeline and all its related content (cascades).

Arc Methods

createArc(data: Arc): Promise<Arc>

Creates a new arc within a timeline.

await tracker.createArc({
  timelineName: 'my-story',
  name: 'arc-1',
  number: 1,
  description: 'First arc'
});

getArcs(timelineName: string): Promise<Arc[]>

Retrieves all arcs in a timeline, ordered by number.

getArc(timelineName: string, arcName: string): Promise<Arc | undefined>

Retrieves a specific arc.

updateArc(timelineName: string, arcName: string, data: Partial<Arc>): Promise<Arc>

Updates an arc.

deleteArc(timelineName: string, arcName: string): Promise<void>

Deletes an arc and all its related content (cascades).

Episode Methods

createEpisode(data: Episode): Promise<Episode>

Creates a new episode within an arc.

await tracker.createEpisode({
  timelineName: 'my-story',
  arcName: 'arc-1',
  number: 1,
  slug: 'awakening',
  title: 'The Awakening',
  description: 'Hero discovers their power'
});

getEpisodes(timelineName: string, arcName: string): Promise<Episode[]>

Retrieves all episodes in an arc, ordered by number.

getEpisode(timelineName: string, arcName: string, episodeNumber: number): Promise<Episode | undefined>

Retrieves a specific episode.

updateEpisode(timelineName: string, arcName: string, episodeNumber: number, data: Partial<Episode>): Promise<Episode>

Updates an episode.

deleteEpisode(timelineName: string, arcName: string, episodeNumber: number): Promise<void>

Deletes an episode and all its related content (cascades).

Part Methods

createPart(data: Part): Promise<Part>

Creates a new part within an episode.

await tracker.createPart({
  timelineName: 'my-story',
  arcName: 'arc-1',
  episodeNumber: 1,
  number: 1,
  slug: 'morning',
  title: 'Morning',
  description: 'The day begins'
});

getParts(timelineName: string, arcName: string, episodeNumber: number): Promise<Part[]>

Retrieves all parts in an episode, ordered by number.

getPart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number): Promise<Part | undefined>

Retrieves a specific part.

updatePart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number, data: Partial<Part>): Promise<Part>

Updates a part.

deletePart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number): Promise<void>

Deletes a part and all its related content (cascades).

Chapter Methods

createChapter(data: Chapter): Promise<Chapter>

Creates a new chapter within an episode (and optionally a part).

await tracker.createChapter({
  timelineName: 'my-story',
  arcName: 'arc-1',
  episodeNumber: 1,
  partNumber: 1,
  number: 1,
  pov: 'Alice',
  title: 'A Strange Dream',
  date: new Date('2024-01-01'),
  excerpt: 'Alice woke up in a strange place...',
  location: 'Enchanted Forest',
  outfit: 'Blue dress',  // optional
  kink: 'fantasy',       // optional
  words: 1500,
  characters: 7500,
  charactersNoSpaces: 6000,
  paragraphs: 15,
  sentences: 75,
  readingTimeMinutes: 8
});

getChapters(timelineName: string, arcName: string, episodeNumber: number, partNumber?: number): Promise<Chapter[]>

Retrieves chapters in an episode, optionally filtered by part.

getChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number): Promise<Chapter | undefined>

Retrieves a specific chapter.

updateChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number, data: Partial<Chapter>): Promise<Chapter>

Updates a chapter.

deleteChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number): Promise<void>

Deletes a chapter.

Database Migrations

Migrations run automatically when you call tracker.init().

How It Works

  • Migrations are stored in the migrations/ directory
  • Each migration file follows the pattern XXX_description.ts (e.g., 001_initial.ts)
  • Executed migrations are tracked in the _migrations table
  • Only pending migrations run on initialization
  • Each migration runs in a transaction (atomic)

Creating a Migration

Create a file in migrations/ with the next number:

// migrations/002_add_tags.ts
import type { Kysely } from 'kysely';
import type { Database } from '../lib/database.js';

export async function up(db: Kysely<Database>): Promise<void> {
  await db.schema
    .createTable('tags')
    .addColumn('name', 'text', (col) => col.primaryKey())
    .addColumn('description', 'text')
    .execute();
}

export async function down(db: Kysely<Database>): Promise<void> {
  await db.schema.dropTable('tags').execute();
}

The migration will run automatically the next time tracker.init() is called.

Type Definitions

All types are imported from @echoes-io/models:

import type { 
  Timeline, 
  Arc, 
  Episode, 
  Part, 
  Chapter 
} from '@echoes-io/tracker';

Error Handling

The library throws errors in the following cases:

  • ZodError: Invalid input data (validation failure)
  • Error: Entity not found (e.g., "Timeline not found")
  • Database errors: SQLite constraint violations, connection issues

Always wrap operations in try/catch blocks:

try {
  await tracker.createTimeline({
    name: 'my-story',
    description: 'Test'
  });
} catch (error) {
  if (error instanceof ZodError) {
    console.error('Validation error:', error.errors);
  } else {
    console.error('Database error:', error.message);
  }
}

Development

Prerequisites

  • Node.js >= 20
  • Git

Setup

# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build
npm run build

# Lint
npm run lint

Testing

The library includes comprehensive tests covering all CRUD operations, cascade deletes, error cases, and migrations.

Test structure:

test/
├── connection.test.ts  # Database connection tests
├── index.test.ts       # Module exports tests
├── migrations.test.ts  # Migration system tests
└── tracker.test.ts     # Tracker CRUD operations tests

Current coverage: ~99% statements, 100% functions.

License

MIT


Part of the Echoes project - a multi-POV digital storytelling platform.