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

@markwharton/liquidplanner

v3.2.3

Published

LiquidPlanner API client for timesheet integration

Readme

@markwharton/liquidplanner

LiquidPlanner API client for timesheet integration.

Install

npm install @markwharton/liquidplanner

Quick Start

import { LPClient, resolveTaskToAssignment } from '@markwharton/liquidplanner';

const client = new LPClient({ apiToken: 'xxx', workspaceId: 123 });

// Validate credentials
const validation = await client.validateToken();
if (!validation.ok) throw new Error(validation.error);

// Get workspaces
const wsResult = await client.getWorkspaces();
if (wsResult.ok) console.log(wsResult.data); // LPWorkspace[]

// Get workspace members
const membersResult = await client.getWorkspaceMembers();
if (membersResult.ok) console.log(membersResult.data); // LPMember[]

// Get item ancestors (hierarchy chain)
const ancResult = await client.getItemAncestors(itemId);
if (ancResult.ok) console.log(ancResult.data); // LPAncestor[]

// Resolve task to assignment
const resolution = await resolveTaskToAssignment(client, taskId, memberId);

// Log time
await client.createTimesheetEntry({
  date: '2026-01-29',
  itemId: resolution.assignmentId,
  hours: 2.5,
  note: 'Working on feature'
});

// Query existing entries for a date
const tsResult = await client.getTimesheetEntries('2026-01-29');
if (tsResult.ok) console.log(tsResult.data); // LPTimesheetEntries (grouped by member)

// Update an existing entry (accumulate hours)
const existing = tsResult.data!.members[0].entries[0];
await client.updateTimesheetEntry(existing.id, existing, {
  hours: existing.hours + 1.5,
  note: 'Additional work'
});

// Find items with filters
const lateResult = await client.findItems({
  itemType: 'tasks',
  taskStatusGroupNot: 'done',
  late: true,
});
if (lateResult.ok) console.log(lateResult.data); // LPItem[]

// Get children of an item
const childResult = await client.getChildren(parentId);
if (childResult.ok) console.log(childResult.data); // LPItem[]

// Get workspace tree snapshot (cached, all hierarchy lookups resolved in memory)
const treeResult = await client.getWorkspaceTree();
if (treeResult.ok) console.log(treeResult.data); // LPWorkspaceTree

// Get a member's assignments with full context from the tree
const workResult = await client.getAssignments(memberId);
if (workResult.ok) {
  const { assignments, treeItemCount } = workResult.data;
  // Each assignment includes taskName, projectId, projectName, hierarchyPath, ancestors
  // treeItemCount shows total items loaded (only member's assignments returned downstream)
}

Tree Utilities

import { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree } from '@markwharton/liquidplanner';

// Build tree from flat item array
const tree = buildTree(items);

// Get ancestors (root → parent order)
const ancestors = getTreeAncestors(tree, itemId);

// Get formatted path like "Project A › Subfolder B"
const path = getTreeHierarchyPath(tree, itemId);

// Find items matching a predicate
const lateTasks = findInTree(tree, item => item.late === true);

Result Pattern

All methods return Result<T> — see api-core Result Pattern. Always check ok before accessing data.

API Reference

| Method | Parameters | Returns | |--------|-----------|---------| | validateToken() | — | Result<void> | | getWorkspaces() | — | Result<LPWorkspace[]> | | getWorkspaceMembers() | — | Result<LPMember[]> | | getItem(itemId) | number | Result<LPItem> | | getItems(itemIds) | number[] | Result<LPItem[]> | | getItemAncestors(itemId) | number | Result<LPAncestor[]> | | findAssignments(taskId) | number | Result<LPItem[]> | | findItems(options) | LPFindItemsOptions | Result<LPItem[]> | | getChildren(parentId, options?) | number, { itemType? }? | Result<LPItem[]> | | getWorkspaceTree() | — | Result<LPWorkspaceTree> | | getAssignments(memberId?) | number? | Result<LPAssignments> | | clearCache() | — | void | | invalidateTimesheetCache() | — | void | | invalidateTreeCache() | — | void | | invalidateMemberCache() | — | void | | invalidateItemCache() | — | void | | invalidateCostCodeCache() | — | void | | getCostCodes() | — | Result<LPCostCode[]> | | createTimesheetEntry(entry) | LPTimesheetEntry | LPSyncResult | | getTimesheetEntries(date, options?) | string \| string[], LPTimesheetOptions? | Result<LPTimesheetEntries> | | updateTimesheetEntry(entryId, existing, updates) | number, LPTimesheetEntry, Partial<LPTimesheetEntry> | LPSyncResult | | upsertTimesheetEntry(entry, options?) | LPTimesheetEntry, LPUpsertOptions? | LPSyncResult | | getWeeklySummary(dates, options?) | string[], LPWeeklySummaryOptions? | Result<LPWeeklySummary> |

Workflow: resolveTaskToAssignment

import { resolveTaskToAssignment } from '@markwharton/liquidplanner';

const resolution = await resolveTaskToAssignment(client, taskId, memberId);
// resolution.assignmentId - the assignment ID to use for logging time

Resolves a Task ID to the correct Assignment ID for time logging. Handles cases where a task has multiple assignments by filtering on member ID.

Tree Utilities

| Function | Description | |----------|-------------| | buildTree(items) | Build a navigable tree from a flat item array | | getTreeAncestors(tree, itemId) | Get ancestors from root to parent (excludes item) | | getTreeHierarchyPath(tree, itemId) | Formatted path like "Project A > Subfolder B" | | findInTree(tree, predicate) | Find all items matching a predicate |

Filter Utilities

| Function | Description | |----------|-------------| | filterIs(field, value) | field[is]="value" | | filterIsNot(field, value) | field[is_not]="value" | | filterIn(field, values) | field[in]=["v1","v2"] | | filterGt(field, value) | field[gt]="value" | | filterLt(field, value) | field[lt]="value" | | filterAfter(field, value) | field[after]="value" — accepts YYYY-MM-DD (local timezone) or ISO | | filterBefore(field, value) | field[before]="value" — accepts YYYY-MM-DD (local timezone) or ISO | | joinFilters(...filters) | Join filter strings with & |

Configuration

const client = new LPClient({
  apiToken: 'your-token',      // Required: Bearer token
  workspaceId: 12345,          // Required: workspace ID
  baseUrl: '...',              // Optional: override API base URL
  onRequest: ({ method, url, description }) => { ... },  // Optional: debug callback
  cache: {},                   // Optional: enable TTL caching (defaults below)
  cacheInstance: cache,        // Optional: custom cache backend (e.g., LayeredCache)
  retry: {                     // Optional: retry on 429/503
    maxRetries: 3,
    initialDelayMs: 1000,
    maxDelayMs: 10000,
  },
});

LPConfig extends ClientConfig from api-core, which provides the baseUrl, onRequest, retry, and cacheInstance fields. apiToken, workspaceId, and cache are LP-specific. Pass cacheInstance to use a custom cache backend (e.g., LayeredCache with persistent stores); otherwise cache: {} creates an in-memory TTLCache.

Cache TTLs

| Cache Key | Default TTL | |-----------|-------------| | Workspace tree | 10 min | | Workspace members | 5 min | | Cost codes | 5 min | | Items / ancestors | 5 min | | Timesheet entries | 60s |

Write operations (createTimesheetEntry, updateTimesheetEntry) automatically invalidate timesheet cache entries. Use focused invalidation methods (invalidateTreeCache, invalidateMemberCache, invalidateItemCache, invalidateTimesheetCache, invalidateCostCodeCache) to refresh specific data, or clearCache() to clear everything.

Failed API results (ok: false) are never cached — transient errors won't persist for the full TTL. See the root README Cache System section for the full cache architecture (layered stores, restricted data handling, request coalescing).

Retry

Automatically retries on HTTP 429 (Too Many Requests) and 503 (Service Unavailable) with exponential backoff. Respects the Retry-After header when present.

Architecture

See ARCHITECTURE.md for design decisions, implementation patterns, and known limitations.

License

MIT