@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/liquidplannerQuick 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 timeResolves 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
