@markwharton/liquidplanner 1.3.0 → 1.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/dist/client.d.ts +21 -2
- package/dist/client.js +125 -28
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/utils.js +6 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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 } from './types.js';
|
|
9
|
+
import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext, LPAncestor } from './types.js';
|
|
10
10
|
/**
|
|
11
11
|
* LiquidPlanner API Client
|
|
12
12
|
*
|
|
@@ -79,6 +79,18 @@ export declare class LPClient {
|
|
|
79
79
|
items?: LPItem[];
|
|
80
80
|
error?: string;
|
|
81
81
|
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Get the ancestry chain for an item
|
|
84
|
+
*
|
|
85
|
+
* Returns ancestors from root to immediate parent (excludes the item itself).
|
|
86
|
+
* Uses the items/{itemId}/ancestors endpoint.
|
|
87
|
+
*
|
|
88
|
+
* @param itemId - The item ID to get ancestors for
|
|
89
|
+
*/
|
|
90
|
+
getItemAncestors(itemId: number): Promise<{
|
|
91
|
+
ancestors?: LPAncestor[];
|
|
92
|
+
error?: string;
|
|
93
|
+
}>;
|
|
82
94
|
/**
|
|
83
95
|
* Find all assignments under a task (with pagination)
|
|
84
96
|
*/
|
|
@@ -100,14 +112,21 @@ export declare class LPClient {
|
|
|
100
112
|
* Get assignments for a member with parent task names resolved
|
|
101
113
|
*
|
|
102
114
|
* This is a convenience method that fetches assignments and enriches
|
|
103
|
-
* them with parent task names using batch fetching
|
|
115
|
+
* them with parent task names using batch fetching.
|
|
116
|
+
*
|
|
117
|
+
* Request counts:
|
|
118
|
+
* - Default: 2 requests (assignments + tasks)
|
|
119
|
+
* - includeProject: 3 requests (assignments + tasks + projects)
|
|
120
|
+
* - includeHierarchy: 1 + N requests (assignments + N ancestors calls)
|
|
104
121
|
*
|
|
105
122
|
* @param memberId - The member ID to get assignments for
|
|
106
123
|
* @param options - Options for including additional context
|
|
107
124
|
* @param options.includeProject - If true, also fetch grandparent project names
|
|
125
|
+
* @param options.includeHierarchy - If true, fetch full ancestry and build hierarchy path
|
|
108
126
|
*/
|
|
109
127
|
getMyAssignmentsWithContext(memberId: number, options?: {
|
|
110
128
|
includeProject?: boolean;
|
|
129
|
+
includeHierarchy?: boolean;
|
|
111
130
|
}): Promise<{
|
|
112
131
|
assignments?: LPAssignmentWithContext[];
|
|
113
132
|
error?: string;
|
package/dist/client.js
CHANGED
|
@@ -176,6 +176,39 @@ export class LPClient {
|
|
|
176
176
|
});
|
|
177
177
|
return error ? { error } : { items: results };
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the ancestry chain for an item
|
|
181
|
+
*
|
|
182
|
+
* Returns ancestors from root to immediate parent (excludes the item itself).
|
|
183
|
+
* Uses the items/{itemId}/ancestors endpoint.
|
|
184
|
+
*
|
|
185
|
+
* @param itemId - The item ID to get ancestors for
|
|
186
|
+
*/
|
|
187
|
+
async getItemAncestors(itemId) {
|
|
188
|
+
const url = `${this.baseUrl}/workspaces/${this.workspaceId}/items/v1/${itemId}/ancestors`;
|
|
189
|
+
try {
|
|
190
|
+
const response = await this.fetch(url, {
|
|
191
|
+
description: `Get ancestors for item ${itemId}`,
|
|
192
|
+
});
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const errorText = await response.text();
|
|
195
|
+
const { message } = parseLPErrorResponse(errorText, response.status);
|
|
196
|
+
return { error: message };
|
|
197
|
+
}
|
|
198
|
+
const json = (await response.json());
|
|
199
|
+
// Handle both { data: [...] } and direct array responses
|
|
200
|
+
const rawData = Array.isArray(json) ? json : (json.data || []);
|
|
201
|
+
const ancestors = rawData.map((a) => ({
|
|
202
|
+
id: a.id,
|
|
203
|
+
name: a.name || null,
|
|
204
|
+
itemType: normalizeItemType(a.itemType),
|
|
205
|
+
}));
|
|
206
|
+
return { ancestors };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return { error: getErrorMessage(error) };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
179
212
|
/**
|
|
180
213
|
* Find all assignments under a task (with pagination)
|
|
181
214
|
*/
|
|
@@ -209,11 +242,17 @@ export class LPClient {
|
|
|
209
242
|
* Get assignments for a member with parent task names resolved
|
|
210
243
|
*
|
|
211
244
|
* This is a convenience method that fetches assignments and enriches
|
|
212
|
-
* them with parent task names using batch fetching
|
|
245
|
+
* them with parent task names using batch fetching.
|
|
246
|
+
*
|
|
247
|
+
* Request counts:
|
|
248
|
+
* - Default: 2 requests (assignments + tasks)
|
|
249
|
+
* - includeProject: 3 requests (assignments + tasks + projects)
|
|
250
|
+
* - includeHierarchy: 1 + N requests (assignments + N ancestors calls)
|
|
213
251
|
*
|
|
214
252
|
* @param memberId - The member ID to get assignments for
|
|
215
253
|
* @param options - Options for including additional context
|
|
216
254
|
* @param options.includeProject - If true, also fetch grandparent project names
|
|
255
|
+
* @param options.includeHierarchy - If true, fetch full ancestry and build hierarchy path
|
|
217
256
|
*/
|
|
218
257
|
async getMyAssignmentsWithContext(memberId, options) {
|
|
219
258
|
// 1. Get raw assignments
|
|
@@ -222,38 +261,96 @@ export class LPClient {
|
|
|
222
261
|
return { error };
|
|
223
262
|
if (assignments.length === 0)
|
|
224
263
|
return { assignments: [] };
|
|
225
|
-
// 2.
|
|
226
|
-
|
|
227
|
-
// 3. Batch fetch all parent tasks in a single request
|
|
228
|
-
const { items: tasks, error: taskError } = await this.getItems(taskIds);
|
|
229
|
-
if (taskError)
|
|
230
|
-
return { error: taskError };
|
|
231
|
-
const taskMap = new Map();
|
|
232
|
-
for (const task of tasks || []) {
|
|
233
|
-
taskMap.set(task.id, task);
|
|
234
|
-
}
|
|
235
|
-
// 4. Optionally batch fetch grandparent projects in a single request
|
|
264
|
+
// 2. Handle based on options
|
|
265
|
+
let taskMap = new Map();
|
|
236
266
|
let projectMap = new Map();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
267
|
+
let ancestorMap = new Map();
|
|
268
|
+
if (options?.includeHierarchy) {
|
|
269
|
+
// Optimized path: fetch ancestors for assignments (includes task info)
|
|
270
|
+
// This avoids the separate task batch fetch since task is first ancestor
|
|
271
|
+
// Deduplicate by parentId - only one ancestors call per unique parent task
|
|
272
|
+
const assignmentsByParent = new Map();
|
|
273
|
+
for (const a of assignments) {
|
|
274
|
+
if (a.parentId && !assignmentsByParent.has(a.parentId)) {
|
|
275
|
+
assignmentsByParent.set(a.parentId, a);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const ancestorResults = await Promise.all([...assignmentsByParent.entries()].map(async ([parentId, assignment]) => {
|
|
279
|
+
const { ancestors } = await this.getItemAncestors(assignment.id);
|
|
280
|
+
return { parentId, ancestors };
|
|
281
|
+
}));
|
|
282
|
+
for (const { parentId, ancestors } of ancestorResults) {
|
|
283
|
+
ancestorMap.set(parentId, ancestors);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Original path: batch fetch tasks first
|
|
288
|
+
const taskIds = [...new Set(assignments.map(a => a.parentId).filter((id) => id !== undefined))];
|
|
289
|
+
const { items: tasks, error: taskError } = await this.getItems(taskIds);
|
|
290
|
+
if (taskError)
|
|
291
|
+
return { error: taskError };
|
|
292
|
+
for (const task of tasks || []) {
|
|
293
|
+
taskMap.set(task.id, task);
|
|
294
|
+
}
|
|
295
|
+
if (options?.includeProject) {
|
|
296
|
+
// Also fetch grandparent projects
|
|
297
|
+
const projectIds = [...new Set([...taskMap.values()].map(t => t.parentId).filter((id) => id !== undefined))];
|
|
298
|
+
const { items: projects, error: projectError } = await this.getItems(projectIds);
|
|
299
|
+
if (projectError)
|
|
300
|
+
return { error: projectError };
|
|
301
|
+
for (const project of projects || []) {
|
|
302
|
+
projectMap.set(project.id, project);
|
|
303
|
+
}
|
|
244
304
|
}
|
|
245
305
|
}
|
|
246
|
-
//
|
|
306
|
+
// 3. Merge context into assignments
|
|
247
307
|
return {
|
|
248
308
|
assignments: assignments.map(a => {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
309
|
+
const result = { ...a };
|
|
310
|
+
if (options?.includeHierarchy && a.parentId) {
|
|
311
|
+
// Full hierarchy mode - extract task name from ancestors
|
|
312
|
+
const ancestors = ancestorMap.get(a.parentId);
|
|
313
|
+
result.ancestors = ancestors;
|
|
314
|
+
if (ancestors && ancestors.length > 0) {
|
|
315
|
+
// Extract task name from first Task ancestor
|
|
316
|
+
const taskAncestor = ancestors.find(anc => anc.itemType === 'Task');
|
|
317
|
+
result.taskName = taskAncestor?.name ?? null;
|
|
318
|
+
// Build hierarchyPath from Project and Folder ancestors
|
|
319
|
+
// Exclude system containers (Package, WorkspaceRoot) and Tasks
|
|
320
|
+
const hierarchyAncestors = ancestors
|
|
321
|
+
.filter(anc => anc.itemType === 'Project' || anc.itemType === 'Folder')
|
|
322
|
+
.reverse(); // LP returns child→root, we want root→child
|
|
323
|
+
if (hierarchyAncestors.length > 0) {
|
|
324
|
+
result.hierarchyPath = hierarchyAncestors
|
|
325
|
+
.map(anc => anc.name ?? `[${anc.id}]`)
|
|
326
|
+
.join(' › ');
|
|
327
|
+
// Set projectId/projectName from root (first in reversed array)
|
|
328
|
+
result.projectId = hierarchyAncestors[0].id;
|
|
329
|
+
result.projectName = hierarchyAncestors[0].name;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
result.projectName = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
result.taskName = null;
|
|
337
|
+
result.projectName = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// Original path - use taskMap
|
|
342
|
+
const task = a.parentId ? taskMap.get(a.parentId) : undefined;
|
|
343
|
+
result.taskName = task?.name ?? null;
|
|
344
|
+
if (options?.includeProject) {
|
|
345
|
+
const project = task?.parentId ? projectMap.get(task.parentId) : undefined;
|
|
346
|
+
result.projectId = project?.id;
|
|
347
|
+
result.projectName = project?.name ?? null;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
result.projectName = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
257
354
|
}),
|
|
258
355
|
};
|
|
259
356
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export { LPClient } from './client.js';
|
|
30
30
|
export { resolveTaskToAssignment } from './workflows.js';
|
|
31
|
-
export type { LPConfig, LPItemType, LPItem, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, LPUpsertOptions, LPAssignmentWithContext, } from './types.js';
|
|
31
|
+
export type { LPConfig, LPItemType, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, LPUpsertOptions, LPAssignmentWithContext, } from './types.js';
|
|
32
32
|
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
33
33
|
export type { PaginateOptions } from './utils.js';
|
|
34
34
|
export { LP_API_BASE } from './constants.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,20 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* LiquidPlanner item types in the hierarchy
|
|
9
9
|
*/
|
|
10
|
-
export type LPItemType = 'Task' | 'Assignment' | 'Folder' | 'Milestone' | 'Event';
|
|
10
|
+
export type LPItemType = 'Task' | 'Assignment' | 'Folder' | 'Project' | 'Package' | 'WorkspaceRoot' | 'Milestone' | 'Event';
|
|
11
|
+
/**
|
|
12
|
+
* An ancestor item in the hierarchy chain
|
|
13
|
+
*
|
|
14
|
+
* Returned by getItemAncestors() in order from root to immediate parent.
|
|
15
|
+
*/
|
|
16
|
+
export interface LPAncestor {
|
|
17
|
+
/** Unique identifier */
|
|
18
|
+
id: number;
|
|
19
|
+
/** Display name (null if not set) */
|
|
20
|
+
name: string | null;
|
|
21
|
+
/** Type of item in LP hierarchy */
|
|
22
|
+
itemType: LPItemType;
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* An item from LiquidPlanner (Task, Assignment, Folder, etc.)
|
|
13
26
|
*/
|
|
@@ -166,4 +179,8 @@ export interface LPAssignmentWithContext extends LPItem {
|
|
|
166
179
|
projectId?: number;
|
|
167
180
|
/** Grandparent project name (null if not requested/found) */
|
|
168
181
|
projectName?: string | null;
|
|
182
|
+
/** Full ancestry from root to parent task (undefined if not requested) */
|
|
183
|
+
ancestors?: LPAncestor[];
|
|
184
|
+
/** Formatted hierarchy path like "Project A › Subfolder B" (undefined if not requested) */
|
|
185
|
+
hierarchyPath?: string;
|
|
169
186
|
}
|
package/dist/utils.js
CHANGED
|
@@ -80,12 +80,18 @@ export function normalizeItemType(apiItemType) {
|
|
|
80
80
|
'tasks': 'Task',
|
|
81
81
|
'assignments': 'Assignment',
|
|
82
82
|
'folders': 'Folder',
|
|
83
|
+
'projects': 'Project',
|
|
84
|
+
'packages': 'Package',
|
|
85
|
+
'workspaceRoots': 'WorkspaceRoot',
|
|
83
86
|
'milestones': 'Milestone',
|
|
84
87
|
'events': 'Event',
|
|
85
88
|
// Already-normalized values (for safety)
|
|
86
89
|
'Task': 'Task',
|
|
87
90
|
'Assignment': 'Assignment',
|
|
88
91
|
'Folder': 'Folder',
|
|
92
|
+
'Project': 'Project',
|
|
93
|
+
'Package': 'Package',
|
|
94
|
+
'WorkspaceRoot': 'WorkspaceRoot',
|
|
89
95
|
'Milestone': 'Milestone',
|
|
90
96
|
'Event': 'Event',
|
|
91
97
|
};
|