@markwharton/liquidplanner 2.3.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 +5 -24
- package/dist/client.d.ts +5 -34
- package/dist/client.js +4 -145
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +1 -3
- package/dist/types.js +1 -3
- package/package.json +1 -1
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
|
|
89
|
-
const workResult = await client.
|
|
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.
|
|
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
|
-
| `
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
173
|
+
* @param memberId - The member ID to get assignments for
|
|
203
174
|
*/
|
|
204
|
-
|
|
205
|
-
assignments:
|
|
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,
|
|
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
|
|
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
|
|
490
|
+
* @param memberId - The member ID to get assignments for
|
|
632
491
|
*/
|
|
633
|
-
async
|
|
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,7 @@
|
|
|
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,
|
|
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
34
|
export type { AccessTier } from './types.js';
|
|
35
35
|
export { METHOD_TIERS } from './types.js';
|
|
36
36
|
export { ok, err, getErrorMessage, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
|
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) */
|
|
@@ -259,7 +257,7 @@ export interface LPUpsertOptions {
|
|
|
259
257
|
* Extends LPItem with additional fields for the parent task name,
|
|
260
258
|
* grandparent project name, and cost code name.
|
|
261
259
|
*/
|
|
262
|
-
export interface
|
|
260
|
+
export interface LPAssignment extends LPItem {
|
|
263
261
|
/** Parent task name (null if not found) */
|
|
264
262
|
taskName?: string | null;
|
|
265
263
|
/** Grandparent project ID (undefined if not requested/found) */
|
package/dist/types.js
CHANGED
|
@@ -17,12 +17,10 @@ export const METHOD_TIERS = {
|
|
|
17
17
|
getItems: 'standard',
|
|
18
18
|
getItemAncestors: 'standard',
|
|
19
19
|
findAssignments: 'standard',
|
|
20
|
-
getMyAssignments: 'standard',
|
|
21
|
-
getMyAssignmentsWithContext: 'standard',
|
|
22
20
|
findItems: 'standard',
|
|
23
21
|
getChildren: 'standard',
|
|
24
22
|
getWorkspaceTree: 'standard',
|
|
25
|
-
|
|
23
|
+
getAssignments: 'standard',
|
|
26
24
|
getCostCodes: 'standard',
|
|
27
25
|
createTimesheetEntry: 'standard',
|
|
28
26
|
getTimesheetEntries: 'standard',
|