@markwharton/liquidplanner 2.2.0 → 2.4.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
@@ -27,22 +27,6 @@ if (wsResult.ok) console.log(wsResult.data); // LPWorkspace[]
27
27
  const membersResult = await client.getWorkspaceMembers();
28
28
  if (membersResult.ok) console.log(membersResult.data); // LPMember[]
29
29
 
30
- // Get user's assignments (for task picker)
31
- const assignResult = await client.getMyAssignments(memberId);
32
- if (assignResult.ok) console.log(assignResult.data); // LPItem[]
33
-
34
- // Get assignments with parent task/project names resolved
35
- const ctxResult = await client.getMyAssignmentsWithContext(memberId, {
36
- includeProject: true // optional: also fetch project names
37
- });
38
- if (ctxResult.ok) console.log(ctxResult.data); // LPAssignmentWithContext[]
39
-
40
- // Get assignments with full hierarchy path
41
- const hierResult = await client.getMyAssignmentsWithContext(memberId, {
42
- includeHierarchy: true // includes ancestors and hierarchyPath
43
- });
44
- if (hierResult.ok) console.log(hierResult.data); // LPAssignmentWithContext[]
45
-
46
30
  // Get item ancestors (hierarchy chain)
47
31
  const ancResult = await client.getItemAncestors(itemId);
48
32
  if (ancResult.ok) console.log(ancResult.data); // LPAncestor[]
@@ -85,8 +69,8 @@ if (childResult.ok) console.log(childResult.data); // LPItem[]
85
69
  const treeResult = await client.getWorkspaceTree();
86
70
  if (treeResult.ok) console.log(treeResult.data); // LPWorkspaceTree
87
71
 
88
- // Get a member's work with full context from the tree
89
- const workResult = await client.getMyWork(memberId);
72
+ // Get a member's assignments with full context from the tree
73
+ const workResult = await client.getAssignments(memberId);
90
74
  if (workResult.ok) {
91
75
  const { assignments, treeItemCount } = workResult.data;
92
76
  // Each assignment includes taskName, projectId, projectName, hierarchyPath, ancestors
@@ -126,7 +110,7 @@ interface Result<T> {
126
110
  ```
127
111
 
128
112
  ```typescript
129
- const result = await client.getMyWork(memberId);
113
+ const result = await client.getAssignments(memberId);
130
114
  if (!result.ok) {
131
115
  console.error(result.error, result.status);
132
116
  return;
@@ -145,12 +129,10 @@ const { assignments, treeItemCount } = result.data;
145
129
  | `getItems(itemIds)` | `number[]` | `Result<LPItem[]>` |
146
130
  | `getItemAncestors(itemId)` | `number` | `Result<LPAncestor[]>` |
147
131
  | `findAssignments(taskId)` | `number` | `Result<LPItem[]>` |
148
- | `getMyAssignments(memberId)` | `number` | `Result<LPItem[]>` |
149
- | `getMyAssignmentsWithContext(memberId, options?)` | `number, { includeProject?, includeHierarchy? }` | `Result<LPAssignmentWithContext[]>` |
150
132
  | `findItems(options)` | `LPFindItemsOptions` | `Result<LPItem[]>` |
151
133
  | `getChildren(parentId, options?)` | `number, { itemType? }?` | `Result<LPItem[]>` |
152
134
  | `getWorkspaceTree()` | — | `Result<LPWorkspaceTree>` |
153
- | `getMyWork(memberId)` | `number` | `Result<{ assignments: LPAssignmentWithContext[], treeItemCount: number }>` |
135
+ | `getAssignments(memberId)` | `number` | `Result<{ assignments: LPAssignment[], treeItemCount: number }>` |
154
136
  | `invalidateTreeCache()` | — | `void` |
155
137
  | `getCostCodes()` | — | `Result<LPCostCode[]>` |
156
138
  | `createTimesheetEntry(entry)` | `LPTimesheetEntry` | `LPSyncResult` |
@@ -219,12 +201,11 @@ const client = new LPClient({
219
201
  | Workspace members | 5 min |
220
202
  | Cost codes | 5 min |
221
203
  | Items / ancestors | 5 min |
222
- | Assignments | 2 min |
223
204
  | Timesheet entries | 60s |
224
205
 
225
206
  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.
226
207
 
227
- Failed API results (`ok: false`) are never cached — transient errors won't persist for the full TTL. See the [root README Cache System section](../../README.md#cache-system) for the full cache architecture (layered stores, PII handling, request coalescing).
208
+ Failed API results (`ok: false`) are never cached — transient errors won't persist for the full TTL. See the [root README Cache System section](../../README.md#cache-system) for the full cache architecture (layered stores, restricted data handling, request coalescing).
228
209
 
229
210
  ### Retry
230
211
 
package/dist/client.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * @see https://api-docs.liquidplanner.com/
8
8
  */
9
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
+ import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignment, LPAncestor, LPFindItemsOptions, LPWorkspaceTree } from './types.js';
11
11
  /**
12
12
  * LiquidPlanner API Client
13
13
  *
@@ -125,34 +125,6 @@ export declare class LPClient {
125
125
  * Find all assignments under a task (with pagination)
126
126
  */
127
127
  findAssignments(taskId: number): Promise<Result<LPItem[]>>;
128
- /**
129
- * Get all assignments for a specific member
130
- *
131
- * This enables PWA apps to show a task picker populated from LP directly.
132
- * Note: userId is not a supported filter field in the LP API, so we filter client-side.
133
- * The full unfiltered dataset is cached once; all memberId queries share the same cache entry.
134
- */
135
- getMyAssignments(memberId: number): Promise<Result<LPItem[]>>;
136
- /**
137
- * Get assignments for a member with parent task names resolved
138
- *
139
- * This is a convenience method that fetches assignments and enriches
140
- * them with parent task names using batch fetching.
141
- *
142
- * Request counts:
143
- * - Default: 2 requests (assignments + tasks)
144
- * - includeProject: 3 requests (assignments + tasks + projects)
145
- * - includeHierarchy: 1 + N requests (assignments + N ancestors calls)
146
- *
147
- * @param memberId - The member ID to get assignments for
148
- * @param options - Options for including additional context
149
- * @param options.includeProject - If true, also fetch grandparent project names
150
- * @param options.includeHierarchy - If true, fetch full ancestry and build hierarchy path
151
- */
152
- getMyAssignmentsWithContext(memberId: number, options?: {
153
- includeProject?: boolean;
154
- includeHierarchy?: boolean;
155
- }): Promise<Result<LPAssignmentWithContext[]>>;
156
128
  /**
157
129
  * Query items with LP API filters
158
130
  *
@@ -193,16 +165,15 @@ export declare class LPClient {
193
165
  /**
194
166
  * Get a member's active work with full context from the workspace tree
195
167
  *
196
- * This is the optimized alternative to getMyAssignmentsWithContext().
197
168
  * Uses the workspace tree snapshot (cached) to resolve hierarchy locally,
198
- * eliminating the N+1 ancestor request pattern.
169
+ * eliminating N+1 ancestor request patterns.
199
170
  *
200
171
  * API calls: 0 (if tree cached) or 1-3 (cold load)
201
172
  *
202
- * @param memberId - The member ID to get work for
173
+ * @param memberId - The member ID to get assignments for
203
174
  */
204
- getMyWork(memberId: number): Promise<Result<{
205
- assignments: LPAssignmentWithContext[];
175
+ getAssignments(memberId: number): Promise<Result<{
176
+ assignments: LPAssignment[];
206
177
  treeItemCount: number;
207
178
  }>>;
208
179
  /**
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIsN
10
10
  import { buildTree, getTreeAncestors } from './tree.js';
11
11
  import { parseLPErrorResponse } from './errors.js';
12
12
  import { LP_API_BASE } from './constants.js';
13
- import { TTLCache, batchMap, getErrorMessage, fetchWithRetry, ok, okVoid, err, resolveRetryConfig } from '@markwharton/api-core';
13
+ import { TTLCache, getErrorMessage, fetchWithRetry, ok, okVoid, err, resolveRetryConfig } from '@markwharton/api-core';
14
14
  /** Transform raw API item to LPItem, preserving scheduling and effort fields */
15
15
  function transformItem(raw) {
16
16
  const item = {
@@ -121,7 +121,6 @@ export class LPClient {
121
121
  membersTtl: config.cache?.membersTtl ?? 300000,
122
122
  costCodesTtl: config.cache?.costCodesTtl ?? 300000,
123
123
  timesheetTtl: config.cache?.timesheetTtl ?? 60000,
124
- assignmentsTtl: config.cache?.assignmentsTtl ?? 120000,
125
124
  itemsTtl: config.cache?.itemsTtl ?? 300000,
126
125
  treeTtl: config.cache?.treeTtl ?? 600000,
127
126
  };
@@ -371,145 +370,6 @@ export class LPClient {
371
370
  transform: (data) => data.map(transformItem),
372
371
  });
373
372
  }
374
- /**
375
- * Get all assignments for a specific member
376
- *
377
- * This enables PWA apps to show a task picker populated from LP directly.
378
- * Note: userId is not a supported filter field in the LP API, so we filter client-side.
379
- * The full unfiltered dataset is cached once; all memberId queries share the same cache entry.
380
- */
381
- async getMyAssignments(memberId) {
382
- const result = await this.cached('assignments', this.cacheTtl.assignmentsTtl, async () => {
383
- const baseUrl = this.workspaceUrl(`items/v1?${filterIs('itemType', 'assignments')}`);
384
- return paginatedFetch({
385
- fetchFn: (url) => this.fetch(url),
386
- baseUrl,
387
- transform: (data) => data.map(transformItem),
388
- });
389
- });
390
- if (!result.ok)
391
- return result;
392
- return ok(result.data.filter(item => item.userId === memberId));
393
- }
394
- /**
395
- * Get assignments for a member with parent task names resolved
396
- *
397
- * This is a convenience method that fetches assignments and enriches
398
- * them with parent task names using batch fetching.
399
- *
400
- * Request counts:
401
- * - Default: 2 requests (assignments + tasks)
402
- * - includeProject: 3 requests (assignments + tasks + projects)
403
- * - includeHierarchy: 1 + N requests (assignments + N ancestors calls)
404
- *
405
- * @param memberId - The member ID to get assignments for
406
- * @param options - Options for including additional context
407
- * @param options.includeProject - If true, also fetch grandparent project names
408
- * @param options.includeHierarchy - If true, fetch full ancestry and build hierarchy path
409
- */
410
- async getMyAssignmentsWithContext(memberId, options) {
411
- // 1. Get raw assignments
412
- const assignResult = await this.getMyAssignments(memberId);
413
- if (!assignResult.ok)
414
- return assignResult;
415
- const assignments = assignResult.data;
416
- if (assignments.length === 0)
417
- return ok([]);
418
- // 2. Handle based on options
419
- let taskMap = new Map();
420
- let projectMap = new Map();
421
- let ancestorMap = new Map();
422
- if (options?.includeHierarchy) {
423
- // Optimized path: fetch ancestors for assignments (includes task info)
424
- // This avoids the separate task batch fetch since task is first ancestor
425
- // Deduplicate by parentId - only one ancestors call per unique parent task
426
- const assignmentsByParent = new Map();
427
- for (const a of assignments) {
428
- if (a.parentId && !assignmentsByParent.has(a.parentId)) {
429
- assignmentsByParent.set(a.parentId, a);
430
- }
431
- }
432
- const parentEntries = [...assignmentsByParent.entries()];
433
- const ancestorResults = await batchMap(parentEntries, 5, async ([parentId, assignment]) => {
434
- const result = await this.getItemAncestors(assignment.id);
435
- return { parentId, ancestors: result.data, error: result.ok ? undefined : result };
436
- });
437
- const firstError = ancestorResults.find(r => r.error);
438
- if (firstError) {
439
- return firstError.error;
440
- }
441
- for (const { parentId, ancestors } of ancestorResults) {
442
- ancestorMap.set(parentId, ancestors);
443
- }
444
- }
445
- else {
446
- // Original path: batch fetch tasks first
447
- const taskIds = [...new Set(assignments.map(a => a.parentId).filter((id) => id !== undefined))];
448
- const taskResult = await this.getItems(taskIds);
449
- if (!taskResult.ok)
450
- return taskResult;
451
- for (const task of taskResult.data || []) {
452
- taskMap.set(task.id, task);
453
- }
454
- if (options?.includeProject) {
455
- // Also fetch grandparent projects
456
- const projectIds = [...new Set([...taskMap.values()].map(t => t.parentId).filter((id) => id !== undefined))];
457
- const projectResult = await this.getItems(projectIds);
458
- if (!projectResult.ok)
459
- return projectResult;
460
- for (const project of projectResult.data || []) {
461
- projectMap.set(project.id, project);
462
- }
463
- }
464
- }
465
- // 3. Merge context into assignments
466
- return ok(assignments.map(a => {
467
- const result = { ...a };
468
- if (options?.includeHierarchy && a.parentId) {
469
- // Full hierarchy mode - extract task name from ancestors
470
- const ancestors = ancestorMap.get(a.parentId);
471
- result.ancestors = ancestors;
472
- if (ancestors && ancestors.length > 0) {
473
- // Extract task name from first Task ancestor
474
- const taskAncestor = ancestors.find(anc => anc.itemType === 'Task');
475
- result.taskName = taskAncestor?.name ?? null;
476
- // Build hierarchyPath from Project and Folder ancestors
477
- // Exclude system containers (Package, WorkspaceRoot) and Tasks
478
- const hierarchyAncestors = ancestors
479
- .filter(anc => anc.itemType === 'Project' || anc.itemType === 'Folder');
480
- if (hierarchyAncestors.length > 0) {
481
- result.hierarchyPath = hierarchyAncestors
482
- .map(anc => anc.name ?? `[${anc.id}]`)
483
- .join(' › ');
484
- // Set projectId/projectName from root (first in reversed array)
485
- result.projectId = hierarchyAncestors[0].id;
486
- result.projectName = hierarchyAncestors[0].name;
487
- }
488
- else {
489
- result.projectName = null;
490
- }
491
- }
492
- else {
493
- result.taskName = null;
494
- result.projectName = null;
495
- }
496
- }
497
- else {
498
- // Original path - use taskMap
499
- const task = a.parentId ? taskMap.get(a.parentId) : undefined;
500
- result.taskName = task?.name ?? null;
501
- if (options?.includeProject) {
502
- const project = task?.parentId ? projectMap.get(task.parentId) : undefined;
503
- result.projectId = project?.id;
504
- result.projectName = project?.name ?? null;
505
- }
506
- else {
507
- result.projectName = null;
508
- }
509
- }
510
- return result;
511
- }));
512
- }
513
373
  // ============================================================================
514
374
  // Item Queries (Rich Filtering)
515
375
  // ============================================================================
@@ -622,15 +482,14 @@ export class LPClient {
622
482
  /**
623
483
  * Get a member's active work with full context from the workspace tree
624
484
  *
625
- * This is the optimized alternative to getMyAssignmentsWithContext().
626
485
  * Uses the workspace tree snapshot (cached) to resolve hierarchy locally,
627
- * eliminating the N+1 ancestor request pattern.
486
+ * eliminating N+1 ancestor request patterns.
628
487
  *
629
488
  * API calls: 0 (if tree cached) or 1-3 (cold load)
630
489
  *
631
- * @param memberId - The member ID to get work for
490
+ * @param memberId - The member ID to get assignments for
632
491
  */
633
- async getMyWork(memberId) {
492
+ async getAssignments(memberId) {
634
493
  const treeResult = await this.getWorkspaceTree();
635
494
  if (!treeResult.ok)
636
495
  return err(treeResult.error, treeResult.status);
package/dist/index.d.ts CHANGED
@@ -30,7 +30,9 @@
30
30
  */
31
31
  export { LPClient } from './client.js';
32
32
  export { resolveTaskToAssignment } from './workflows.js';
33
- export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, HierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPUpsertOptions, LPAssignmentWithContext, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
33
+ export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, HierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPUpsertOptions, LPAssignment, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
34
+ export type { AccessTier } from './types.js';
35
+ export { METHOD_TIERS } from './types.js';
34
36
  export { ok, err, getErrorMessage, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
35
37
  export type { Result, RetryConfig, OnRequestCallback, ClientConfig, Cache, CacheStore, CacheGetOptions } from '@markwharton/api-core';
36
38
  export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@
32
32
  export { LPClient } from './client.js';
33
33
  // Workflows
34
34
  export { resolveTaskToAssignment } from './workflows.js';
35
+ export { METHOD_TIERS } from './types.js';
35
36
  // Re-exported from @markwharton/api-core
36
37
  export { ok, err, getErrorMessage, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
37
38
  // Utilities
package/dist/types.d.ts CHANGED
@@ -183,8 +183,6 @@ export interface LPCacheConfig {
183
183
  costCodesTtl?: number;
184
184
  /** TTL for timesheet entries (default: 60000 = 60s) */
185
185
  timesheetTtl?: number;
186
- /** TTL for user assignments (default: 120000 = 2 min) */
187
- assignmentsTtl?: number;
188
186
  /** TTL for items and ancestors (default: 300000 = 5 min) */
189
187
  itemsTtl?: number;
190
188
  /** TTL for workspace tree snapshot (default: 600000 = 10 min) */
@@ -206,6 +204,8 @@ export interface LPConfig extends ClientConfig {
206
204
  apiToken: string;
207
205
  /** Enable caching with optional TTL overrides. Omit to disable caching. */
208
206
  cache?: LPCacheConfig;
207
+ /** Whether to persist restricted-tier data in persistent cache stores (default: true). Set false to keep restricted data in memory only. */
208
+ persistRestricted?: boolean;
209
209
  }
210
210
  /**
211
211
  * Result of a timesheet sync operation
@@ -257,7 +257,7 @@ export interface LPUpsertOptions {
257
257
  * Extends LPItem with additional fields for the parent task name,
258
258
  * grandparent project name, and cost code name.
259
259
  */
260
- export interface LPAssignmentWithContext extends LPItem {
260
+ export interface LPAssignment extends LPItem {
261
261
  /** Parent task name (null if not found) */
262
262
  taskName?: string | null;
263
263
  /** Grandparent project ID (undefined if not requested/found) */
@@ -336,3 +336,12 @@ export interface LPWorkspaceTree {
336
336
  /** Total number of items in the tree */
337
337
  itemCount: number;
338
338
  }
339
+ /** Access tier for method-level authorization */
340
+ export type AccessTier = 'standard' | 'restricted';
341
+ /**
342
+ * Access tier for each data method.
343
+ *
344
+ * All LP methods are standard tier — LP data is workspace-scoped, not employee-scoped.
345
+ * Utility methods (validate, clear, invalidate) are not included.
346
+ */
347
+ export declare const METHOD_TIERS: Record<string, AccessTier>;
package/dist/types.js CHANGED
@@ -4,4 +4,26 @@
4
4
  * These types define the data structures used when interacting with
5
5
  * the LiquidPlanner API.
6
6
  */
7
- export {};
7
+ /**
8
+ * Access tier for each data method.
9
+ *
10
+ * All LP methods are standard tier — LP data is workspace-scoped, not employee-scoped.
11
+ * Utility methods (validate, clear, invalidate) are not included.
12
+ */
13
+ export const METHOD_TIERS = {
14
+ getWorkspaces: 'standard',
15
+ getWorkspaceMembers: 'standard',
16
+ getItem: 'standard',
17
+ getItems: 'standard',
18
+ getItemAncestors: 'standard',
19
+ findAssignments: 'standard',
20
+ findItems: 'standard',
21
+ getChildren: 'standard',
22
+ getWorkspaceTree: 'standard',
23
+ getAssignments: 'standard',
24
+ getCostCodes: 'standard',
25
+ createTimesheetEntry: 'standard',
26
+ getTimesheetEntries: 'standard',
27
+ updateTimesheetEntry: 'standard',
28
+ upsertTimesheetEntry: 'standard',
29
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",