@markwharton/liquidplanner 1.11.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,42 @@ await client.updateTimesheetEntry(entryId, existingEntry, {
59
59
  hours: existingEntry.hours + 1.5,
60
60
  note: 'Additional work'
61
61
  });
62
+
63
+ // Find items with filters
64
+ const { items } = await client.findItems({
65
+ itemType: 'tasks',
66
+ taskStatusGroupNot: 'done',
67
+ late: true,
68
+ });
69
+
70
+ // Get children of an item
71
+ const { items: children } = await client.getChildren(parentId);
72
+
73
+ // Get workspace tree snapshot (cached, all hierarchy lookups resolved in memory)
74
+ const { tree } = await client.getWorkspaceTree();
75
+
76
+ // Get a member's work with full context from the tree
77
+ const { assignments: myWork, treeItemCount } = await client.getMyWork(memberId);
78
+ // Each assignment includes taskName, projectId, projectName, hierarchyPath, ancestors
79
+ // treeItemCount shows total items loaded (only member's assignments returned downstream)
80
+ ```
81
+
82
+ ### Tree Utilities
83
+
84
+ ```typescript
85
+ import { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree } from '@markwharton/liquidplanner';
86
+
87
+ // Build tree from flat item array
88
+ const tree = buildTree(items);
89
+
90
+ // Get ancestors (root → parent order)
91
+ const ancestors = getTreeAncestors(tree, itemId);
92
+
93
+ // Get formatted path like "Project A › Subfolder B"
94
+ const path = getTreeHierarchyPath(tree, itemId);
95
+
96
+ // Find items matching a predicate
97
+ const lateTasks = findInTree(tree, item => item.late === true);
62
98
  ```
63
99
 
64
100
  ## API Reference
@@ -76,6 +112,11 @@ All methods return `{ data?, error? }` result objects rather than throwing excep
76
112
  | `findAssignments(taskId)` | `number` | `{ assignments?, error? }` |
77
113
  | `getMyAssignments(memberId)` | `number` | `{ assignments?, error? }` |
78
114
  | `getMyAssignmentsWithContext(memberId, options?)` | `number, { includeProject?, includeHierarchy? }` | `{ assignments?, error? }` |
115
+ | `findItems(options)` | `LPFindItemsOptions` | `{ items?, error? }` |
116
+ | `getChildren(parentId, options?)` | `number, { itemType? }?` | `{ items?, error? }` |
117
+ | `getWorkspaceTree()` | — | `{ tree?, error? }` |
118
+ | `getMyWork(memberId)` | `number` | `{ assignments?, treeItemCount?, error? }` |
119
+ | `invalidateTreeCache()` | — | `void` |
79
120
  | `getCostCodes()` | — | `{ costCodes?, error? }` |
80
121
  | `createTimesheetEntry(entry)` | `LPTimesheetEntry` | `LPSyncResult` |
81
122
  | `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `{ entries?, error? }` |
@@ -93,6 +134,28 @@ const resolution = await resolveTaskToAssignment(client, taskId, memberId);
93
134
 
94
135
  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.
95
136
 
137
+ ### Tree Utilities
138
+
139
+ | Function | Description |
140
+ |----------|-------------|
141
+ | `buildTree(items)` | Build a navigable tree from a flat item array |
142
+ | `getTreeAncestors(tree, itemId)` | Get ancestors from root to parent (excludes item) |
143
+ | `getTreeHierarchyPath(tree, itemId)` | Formatted path like "Project A > Subfolder B" |
144
+ | `findInTree(tree, predicate)` | Find all items matching a predicate |
145
+
146
+ ### Filter Utilities
147
+
148
+ | Function | Description |
149
+ |----------|-------------|
150
+ | `filterIs(field, value)` | `field[is]="value"` |
151
+ | `filterIsNot(field, value)` | `field[is_not]="value"` |
152
+ | `filterIn(field, values)` | `field[in]=["v1","v2"]` |
153
+ | `filterGt(field, value)` | `field[gt]="value"` |
154
+ | `filterLt(field, value)` | `field[lt]="value"` |
155
+ | `filterAfter(field, value)` | `field[after]="value"` — accepts YYYY-MM-DD (local timezone) or ISO |
156
+ | `filterBefore(field, value)` | `field[before]="value"` — accepts YYYY-MM-DD (local timezone) or ISO |
157
+ | `joinFilters(...filters)` | Join filter strings with `&` |
158
+
96
159
  ## Configuration
97
160
 
98
161
  ```typescript
@@ -114,13 +177,14 @@ const client = new LPClient({
114
177
 
115
178
  | Cache Key | Default TTL |
116
179
  |-----------|-------------|
180
+ | Workspace tree | 10 min |
117
181
  | Workspace members | 5 min |
118
182
  | Cost codes | 5 min |
119
183
  | Items / ancestors | 5 min |
120
184
  | Assignments | 2 min |
121
185
  | Timesheet entries | 60s |
122
186
 
123
- Write operations (`createTimesheetEntry`, `updateTimesheetEntry`) automatically invalidate timesheet cache entries. Call `client.clearCache()` to manually clear all cached data.
187
+ Write operations (`createTimesheetEntry`, `updateTimesheetEntry`) automatically invalidate timesheet cache entries. Call `client.invalidateTreeCache()` to refresh the workspace tree, or `client.clearCache()` to clear all cached data.
124
188
 
125
189
  ### Retry
126
190
 
package/dist/client.d.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * @see https://api-docs.liquidplanner.com/
8
8
  */
9
- import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext, LPAncestor, LPErrorInfo } from './types.js';
9
+ import type { Result } from '@markwharton/api-core';
10
+ import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext, LPAncestor, LPFindItemsOptions, LPWorkspaceTree } from './types.js';
10
11
  /**
11
12
  * LiquidPlanner API Client
12
13
  *
@@ -59,6 +60,11 @@ export declare class LPClient {
59
60
  * (e.g., after logging time which updates loggedHoursRollup).
60
61
  */
61
62
  invalidateAssignmentsCache(): void;
63
+ /**
64
+ * Invalidate cached workspace tree snapshot only.
65
+ * Use when the workspace structure changes (items created, moved, or deleted).
66
+ */
67
+ invalidateTreeCache(): void;
62
68
  /**
63
69
  * Make an authenticated request to the LP API
64
70
  *
@@ -75,34 +81,26 @@ export declare class LPClient {
75
81
  * Fetch a URL and parse the response, with standardized error handling.
76
82
  */
77
83
  private fetchAndParse;
84
+ /**
85
+ * Fetch and parse with isDuplicate support for mutation methods.
86
+ */
87
+ private fetchAndParseMutation;
78
88
  /**
79
89
  * Validate the API token by listing workspaces
80
90
  */
81
- validateToken(): Promise<{
82
- valid: boolean;
83
- error?: string;
84
- }>;
91
+ validateToken(): Promise<Result<void>>;
85
92
  /**
86
93
  * Get all workspaces accessible to the API token
87
94
  */
88
- getWorkspaces(): Promise<{
89
- workspaces?: LPWorkspace[];
90
- error?: LPErrorInfo;
91
- }>;
95
+ getWorkspaces(): Promise<Result<LPWorkspace[]>>;
92
96
  /**
93
97
  * Get all members in the workspace (with pagination)
94
98
  */
95
- getWorkspaceMembers(): Promise<{
96
- members?: LPMember[];
97
- error?: LPErrorInfo;
98
- }>;
99
+ getWorkspaceMembers(): Promise<Result<LPMember[]>>;
99
100
  /**
100
101
  * Get a single item by ID
101
102
  */
102
- getItem(itemId: number): Promise<{
103
- item?: LPItem;
104
- error?: LPErrorInfo;
105
- }>;
103
+ getItem(itemId: number): Promise<Result<LPItem>>;
106
104
  /**
107
105
  * Get multiple items by ID in a single request (batch fetch)
108
106
  *
@@ -111,10 +109,7 @@ export declare class LPClient {
111
109
  *
112
110
  * @param itemIds - Array of item IDs to fetch
113
111
  */
114
- getItems(itemIds: number[]): Promise<{
115
- items?: LPItem[];
116
- error?: LPErrorInfo;
117
- }>;
112
+ getItems(itemIds: number[]): Promise<Result<LPItem[]>>;
118
113
  /**
119
114
  * Get the ancestry chain for an item
120
115
  *
@@ -123,27 +118,18 @@ export declare class LPClient {
123
118
  *
124
119
  * @param itemId - The item ID to get ancestors for
125
120
  */
126
- getItemAncestors(itemId: number): Promise<{
127
- ancestors?: LPAncestor[];
128
- error?: LPErrorInfo;
129
- }>;
121
+ getItemAncestors(itemId: number): Promise<Result<LPAncestor[]>>;
130
122
  /**
131
123
  * Find all assignments under a task (with pagination)
132
124
  */
133
- findAssignments(taskId: number): Promise<{
134
- assignments?: LPItem[];
135
- error?: LPErrorInfo;
136
- }>;
125
+ findAssignments(taskId: number): Promise<Result<LPItem[]>>;
137
126
  /**
138
127
  * Get all assignments for a specific member
139
128
  *
140
129
  * This enables PWA apps to show a task picker populated from LP directly.
141
130
  * Note: userId is not a supported filter field in the LP API, so we filter client-side.
142
131
  */
143
- getMyAssignments(memberId: number): Promise<{
144
- assignments?: LPItem[];
145
- error?: LPErrorInfo;
146
- }>;
132
+ getMyAssignments(memberId: number): Promise<Result<LPItem[]>>;
147
133
  /**
148
134
  * Get assignments for a member with parent task names resolved
149
135
  *
@@ -163,17 +149,63 @@ export declare class LPClient {
163
149
  getMyAssignmentsWithContext(memberId: number, options?: {
164
150
  includeProject?: boolean;
165
151
  includeHierarchy?: boolean;
166
- }): Promise<{
167
- assignments?: LPAssignmentWithContext[];
168
- error?: LPErrorInfo;
169
- }>;
152
+ }): Promise<Result<LPAssignmentWithContext[]>>;
153
+ /**
154
+ * Query items with LP API filters
155
+ *
156
+ * General-purpose method that exposes the full filtering capabilities of
157
+ * the LP items endpoint. Not cached — use higher-level methods like
158
+ * getWorkspaceTree() for cached access.
159
+ *
160
+ * @see https://api-docs.liquidplanner.com/docs/plan-items
161
+ *
162
+ * @param options - Filter options (all optional, combined with AND)
163
+ */
164
+ findItems(options: LPFindItemsOptions): Promise<Result<LPItem[]>>;
165
+ /**
166
+ * Get direct children of an item
167
+ *
168
+ * Uses parentId[is] filter to fetch immediate descendants.
169
+ * Optionally filter by item type.
170
+ *
171
+ * @param parentId - The parent item ID
172
+ * @param options - Optional item type filter
173
+ */
174
+ getChildren(parentId: number, options?: {
175
+ itemType?: string;
176
+ }): Promise<Result<LPItem[]>>;
177
+ /**
178
+ * Fetch a snapshot of the active workspace tree
179
+ *
180
+ * Fetches all workspace items in paginated API calls, then builds a
181
+ * navigable tree in memory from parentId relationships.
182
+ *
183
+ * The result is cached with treeTtl (default: 10 minutes).
184
+ * Use invalidateTreeCache() to force a refresh.
185
+ *
186
+ * After the initial fetch, all hierarchy queries (ancestors, paths, assignments
187
+ * with context) can be answered from the cached tree with zero API calls.
188
+ */
189
+ getWorkspaceTree(): Promise<Result<LPWorkspaceTree>>;
190
+ /**
191
+ * Get a member's active work with full context from the workspace tree
192
+ *
193
+ * This is the optimized alternative to getMyAssignmentsWithContext().
194
+ * Uses the workspace tree snapshot (cached) to resolve hierarchy locally,
195
+ * eliminating the N+1 ancestor request pattern.
196
+ *
197
+ * API calls: 0 (if tree cached) or 1-3 (cold load)
198
+ *
199
+ * @param memberId - The member ID to get work for
200
+ */
201
+ getMyWork(memberId: number): Promise<Result<{
202
+ assignments: LPAssignmentWithContext[];
203
+ treeItemCount: number;
204
+ }>>;
170
205
  /**
171
206
  * Get all cost codes in the workspace (with pagination)
172
207
  */
173
- getCostCodes(): Promise<{
174
- costCodes?: LPCostCode[];
175
- error?: LPErrorInfo;
176
- }>;
208
+ getCostCodes(): Promise<Result<LPCostCode[]>>;
177
209
  /**
178
210
  * Create a timesheet entry (log time)
179
211
  *
@@ -195,10 +227,7 @@ export declare class LPClient {
195
227
  * @param date - Date(s) in YYYY-MM-DD format (string or array)
196
228
  * @param itemId - Optional item ID to filter by
197
229
  */
198
- getTimesheetEntries(date: string | string[], itemId?: number): Promise<{
199
- entries?: LPTimesheetEntryWithId[];
200
- error?: LPErrorInfo;
201
- }>;
230
+ getTimesheetEntries(date: string | string[], itemId?: number): Promise<Result<LPTimesheetEntryWithId[]>>;
202
231
  /**
203
232
  * Update an existing timesheet entry
204
233
  *