@markwharton/liquidplanner 1.1.0 → 1.3.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 +30 -1
- package/dist/client.js +78 -4
- package/dist/constants.d.ts +0 -4
- package/dist/constants.js +0 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +22 -2
- package/dist/workflows.js +4 -5
- 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 } from './types.js';
|
|
9
|
+
import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext } from './types.js';
|
|
10
10
|
/**
|
|
11
11
|
* LiquidPlanner API Client
|
|
12
12
|
*
|
|
@@ -33,6 +33,7 @@ export declare class LPClient {
|
|
|
33
33
|
private readonly apiToken;
|
|
34
34
|
private readonly workspaceId;
|
|
35
35
|
private readonly baseUrl;
|
|
36
|
+
private readonly onRequest?;
|
|
36
37
|
constructor(config: LPConfig);
|
|
37
38
|
/**
|
|
38
39
|
* Make an authenticated request to the LP API
|
|
@@ -66,6 +67,18 @@ export declare class LPClient {
|
|
|
66
67
|
item?: LPItem;
|
|
67
68
|
error?: string;
|
|
68
69
|
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Get multiple items by ID in a single request (batch fetch)
|
|
72
|
+
*
|
|
73
|
+
* Uses the id[in] filter to fetch multiple items efficiently.
|
|
74
|
+
* This reduces N individual requests to a single paginated request.
|
|
75
|
+
*
|
|
76
|
+
* @param itemIds - Array of item IDs to fetch
|
|
77
|
+
*/
|
|
78
|
+
getItems(itemIds: number[]): Promise<{
|
|
79
|
+
items?: LPItem[];
|
|
80
|
+
error?: string;
|
|
81
|
+
}>;
|
|
69
82
|
/**
|
|
70
83
|
* Find all assignments under a task (with pagination)
|
|
71
84
|
*/
|
|
@@ -83,6 +96,22 @@ export declare class LPClient {
|
|
|
83
96
|
assignments?: LPItem[];
|
|
84
97
|
error?: string;
|
|
85
98
|
}>;
|
|
99
|
+
/**
|
|
100
|
+
* Get assignments for a member with parent task names resolved
|
|
101
|
+
*
|
|
102
|
+
* This is a convenience method that fetches assignments and enriches
|
|
103
|
+
* them with parent task names using batch fetching (3 requests max instead of N+1).
|
|
104
|
+
*
|
|
105
|
+
* @param memberId - The member ID to get assignments for
|
|
106
|
+
* @param options - Options for including additional context
|
|
107
|
+
* @param options.includeProject - If true, also fetch grandparent project names
|
|
108
|
+
*/
|
|
109
|
+
getMyAssignmentsWithContext(memberId: number, options?: {
|
|
110
|
+
includeProject?: boolean;
|
|
111
|
+
}): Promise<{
|
|
112
|
+
assignments?: LPAssignmentWithContext[];
|
|
113
|
+
error?: string;
|
|
114
|
+
}>;
|
|
86
115
|
/**
|
|
87
116
|
* Get all cost codes in the workspace (with pagination)
|
|
88
117
|
*/
|
package/dist/client.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
10
10
|
import { parseLPErrorResponse, getErrorMessage } from './errors.js';
|
|
11
|
-
import { LP_API_BASE
|
|
11
|
+
import { LP_API_BASE } from './constants.js';
|
|
12
12
|
/** Transform raw API item to LPItem */
|
|
13
13
|
function transformItem(raw) {
|
|
14
14
|
return {
|
|
15
15
|
id: raw.id,
|
|
16
|
-
name: raw.name ||
|
|
16
|
+
name: raw.name || null,
|
|
17
17
|
itemType: normalizeItemType(raw.itemType),
|
|
18
18
|
parentId: raw.parentId,
|
|
19
19
|
costCodeId: raw.costCodeId,
|
|
@@ -47,12 +47,15 @@ export class LPClient {
|
|
|
47
47
|
this.apiToken = config.apiToken;
|
|
48
48
|
this.workspaceId = config.workspaceId;
|
|
49
49
|
this.baseUrl = config.baseUrl ?? LP_API_BASE;
|
|
50
|
+
this.onRequest = config.onRequest;
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Make an authenticated request to the LP API
|
|
53
54
|
*/
|
|
54
55
|
async fetch(url, options = {}) {
|
|
55
|
-
const { method = 'GET', body } = options;
|
|
56
|
+
const { method = 'GET', body, description } = options;
|
|
57
|
+
// Notify listener of request (for debugging)
|
|
58
|
+
this.onRequest?.({ method, url, description });
|
|
56
59
|
return fetch(url, {
|
|
57
60
|
method,
|
|
58
61
|
headers: {
|
|
@@ -114,7 +117,7 @@ export class LPClient {
|
|
|
114
117
|
* Get all members in the workspace (with pagination)
|
|
115
118
|
*/
|
|
116
119
|
async getWorkspaceMembers() {
|
|
117
|
-
const baseUrl = `${this.baseUrl}/
|
|
120
|
+
const baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/users/v1`;
|
|
118
121
|
const { results, error } = await paginatedFetch({
|
|
119
122
|
fetchFn: (url) => this.fetch(url),
|
|
120
123
|
baseUrl,
|
|
@@ -154,6 +157,25 @@ export class LPClient {
|
|
|
154
157
|
return { error: getErrorMessage(error) };
|
|
155
158
|
}
|
|
156
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Get multiple items by ID in a single request (batch fetch)
|
|
162
|
+
*
|
|
163
|
+
* Uses the id[in] filter to fetch multiple items efficiently.
|
|
164
|
+
* This reduces N individual requests to a single paginated request.
|
|
165
|
+
*
|
|
166
|
+
* @param itemIds - Array of item IDs to fetch
|
|
167
|
+
*/
|
|
168
|
+
async getItems(itemIds) {
|
|
169
|
+
if (itemIds.length === 0)
|
|
170
|
+
return { items: [] };
|
|
171
|
+
const baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/items/v1?${filterIn('id', itemIds)}`;
|
|
172
|
+
const { results, error } = await paginatedFetch({
|
|
173
|
+
fetchFn: (url) => this.fetch(url),
|
|
174
|
+
baseUrl,
|
|
175
|
+
transform: (data) => data.map(transformItem),
|
|
176
|
+
});
|
|
177
|
+
return error ? { error } : { items: results };
|
|
178
|
+
}
|
|
157
179
|
/**
|
|
158
180
|
* Find all assignments under a task (with pagination)
|
|
159
181
|
*/
|
|
@@ -183,6 +205,58 @@ export class LPClient {
|
|
|
183
205
|
});
|
|
184
206
|
return error ? { error } : { assignments: results };
|
|
185
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Get assignments for a member with parent task names resolved
|
|
210
|
+
*
|
|
211
|
+
* This is a convenience method that fetches assignments and enriches
|
|
212
|
+
* them with parent task names using batch fetching (3 requests max instead of N+1).
|
|
213
|
+
*
|
|
214
|
+
* @param memberId - The member ID to get assignments for
|
|
215
|
+
* @param options - Options for including additional context
|
|
216
|
+
* @param options.includeProject - If true, also fetch grandparent project names
|
|
217
|
+
*/
|
|
218
|
+
async getMyAssignmentsWithContext(memberId, options) {
|
|
219
|
+
// 1. Get raw assignments
|
|
220
|
+
const { assignments, error } = await this.getMyAssignments(memberId);
|
|
221
|
+
if (error || !assignments)
|
|
222
|
+
return { error };
|
|
223
|
+
if (assignments.length === 0)
|
|
224
|
+
return { assignments: [] };
|
|
225
|
+
// 2. Extract unique parent IDs (tasks)
|
|
226
|
+
const taskIds = [...new Set(assignments.map(a => a.parentId).filter((id) => id !== undefined))];
|
|
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
|
|
236
|
+
let projectMap = new Map();
|
|
237
|
+
if (options?.includeProject) {
|
|
238
|
+
const projectIds = [...new Set([...taskMap.values()].map(t => t.parentId).filter((id) => id !== undefined))];
|
|
239
|
+
const { items: projects, error: projectError } = await this.getItems(projectIds);
|
|
240
|
+
if (projectError)
|
|
241
|
+
return { error: projectError };
|
|
242
|
+
for (const project of projects || []) {
|
|
243
|
+
projectMap.set(project.id, project);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 5. Merge context into assignments
|
|
247
|
+
return {
|
|
248
|
+
assignments: assignments.map(a => {
|
|
249
|
+
const task = a.parentId ? taskMap.get(a.parentId) : undefined;
|
|
250
|
+
const project = task?.parentId ? projectMap.get(task.parentId) : undefined;
|
|
251
|
+
return {
|
|
252
|
+
...a,
|
|
253
|
+
taskName: task?.name ?? null,
|
|
254
|
+
projectId: project?.id,
|
|
255
|
+
projectName: project?.name ?? null,
|
|
256
|
+
};
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
186
260
|
// ============================================================================
|
|
187
261
|
// Cost Codes
|
|
188
262
|
// ============================================================================
|
package/dist/constants.d.ts
CHANGED
|
@@ -5,7 +5,3 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Default LP API base URL */
|
|
7
7
|
export declare const LP_API_BASE = "https://next.liquidplanner.com/api";
|
|
8
|
-
/** Default name for items when not available */
|
|
9
|
-
export declare const DEFAULT_ITEM_NAME = "-";
|
|
10
|
-
/** Default name for assignments when not available */
|
|
11
|
-
export declare const DEFAULT_ASSIGNMENT_NAME = "-";
|
package/dist/constants.js
CHANGED
|
@@ -5,7 +5,3 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Default LP API base URL */
|
|
7
7
|
export const LP_API_BASE = 'https://next.liquidplanner.com/api';
|
|
8
|
-
/** Default name for items when not available */
|
|
9
|
-
export const DEFAULT_ITEM_NAME = '-';
|
|
10
|
-
/** Default name for assignments when not available */
|
|
11
|
-
export const DEFAULT_ASSIGNMENT_NAME = '-';
|
package/dist/index.d.ts
CHANGED
|
@@ -28,9 +28,9 @@
|
|
|
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, } from './types.js';
|
|
31
|
+
export type { LPConfig, LPItemType, LPItem, 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
|
-
export { LP_API_BASE
|
|
34
|
+
export { LP_API_BASE } from './constants.js';
|
|
35
35
|
export { LPError, parseLPErrorResponse, getErrorMessage } from './errors.js';
|
|
36
36
|
export type { LPParsedError } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,6 @@ export { resolveTaskToAssignment } from './workflows.js';
|
|
|
33
33
|
// Utilities
|
|
34
34
|
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
35
35
|
// Constants
|
|
36
|
-
export { LP_API_BASE
|
|
36
|
+
export { LP_API_BASE } from './constants.js';
|
|
37
37
|
// Errors
|
|
38
38
|
export { LPError, parseLPErrorResponse, getErrorMessage } from './errors.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export type LPItemType = 'Task' | 'Assignment' | 'Folder' | 'Milestone' | 'Event
|
|
|
14
14
|
export interface LPItem {
|
|
15
15
|
/** Unique identifier */
|
|
16
16
|
id: number;
|
|
17
|
-
/** Display name */
|
|
18
|
-
name: string;
|
|
17
|
+
/** Display name (null if not set) */
|
|
18
|
+
name: string | null;
|
|
19
19
|
/** Type of item in LP hierarchy */
|
|
20
20
|
itemType: LPItemType;
|
|
21
21
|
/** Parent item ID (e.g., Assignment's parent is a Task) */
|
|
@@ -89,6 +89,12 @@ export interface LPConfig {
|
|
|
89
89
|
apiToken: string;
|
|
90
90
|
/** Base URL for LP API (defaults to https://next.liquidplanner.com/api) */
|
|
91
91
|
baseUrl?: string;
|
|
92
|
+
/** Optional callback invoked on each API request (for debugging/logging) */
|
|
93
|
+
onRequest?: (info: {
|
|
94
|
+
method: string;
|
|
95
|
+
url: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
}) => void;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* Result of a timesheet sync operation
|
|
@@ -147,3 +153,17 @@ export interface LPUpsertOptions {
|
|
|
147
153
|
*/
|
|
148
154
|
accumulate?: boolean;
|
|
149
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Assignment with resolved parent context
|
|
158
|
+
*
|
|
159
|
+
* Extends LPItem with additional fields for the parent task name,
|
|
160
|
+
* grandparent project name, and cost code name.
|
|
161
|
+
*/
|
|
162
|
+
export interface LPAssignmentWithContext extends LPItem {
|
|
163
|
+
/** Parent task name (null if not found) */
|
|
164
|
+
taskName?: string | null;
|
|
165
|
+
/** Grandparent project ID (undefined if not requested/found) */
|
|
166
|
+
projectId?: number;
|
|
167
|
+
/** Grandparent project name (null if not requested/found) */
|
|
168
|
+
projectName?: string | null;
|
|
169
|
+
}
|
package/dist/workflows.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Higher-level functions that combine multiple API calls to accomplish
|
|
5
5
|
* common tasks, like resolving a Task ID to the correct Assignment ID.
|
|
6
6
|
*/
|
|
7
|
-
import { DEFAULT_ITEM_NAME, DEFAULT_ASSIGNMENT_NAME } from './constants.js';
|
|
8
7
|
/**
|
|
9
8
|
* Resolve an item ID to the correct Assignment ID for logging time
|
|
10
9
|
*
|
|
@@ -41,7 +40,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
|
41
40
|
const { item, error: fetchError } = await client.getItem(itemId);
|
|
42
41
|
if (fetchError || !item) {
|
|
43
42
|
return {
|
|
44
|
-
inputItem: { id: itemId, name:
|
|
43
|
+
inputItem: { id: itemId, name: null, itemType: 'Task' },
|
|
45
44
|
assignmentId: 0,
|
|
46
45
|
error: fetchError || 'Item not found',
|
|
47
46
|
};
|
|
@@ -53,7 +52,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
|
53
52
|
return {
|
|
54
53
|
inputItem: item,
|
|
55
54
|
assignmentId: item.id,
|
|
56
|
-
assignmentName: item.name
|
|
55
|
+
assignmentName: item.name ?? undefined,
|
|
57
56
|
assignmentUserId: item.userId,
|
|
58
57
|
};
|
|
59
58
|
case 'Task': {
|
|
@@ -78,7 +77,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
|
78
77
|
return {
|
|
79
78
|
inputItem: item,
|
|
80
79
|
assignmentId: assignments[0].id,
|
|
81
|
-
assignmentName: assignments[0].name
|
|
80
|
+
assignmentName: assignments[0].name ?? undefined,
|
|
82
81
|
assignmentUserId: assignments[0].userId,
|
|
83
82
|
};
|
|
84
83
|
}
|
|
@@ -90,7 +89,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
|
90
89
|
return {
|
|
91
90
|
inputItem: item,
|
|
92
91
|
assignmentId: myAssignments[0].id,
|
|
93
|
-
assignmentName: myAssignments[0].name
|
|
92
|
+
assignmentName: myAssignments[0].name ?? undefined,
|
|
94
93
|
assignmentUserId: myAssignments[0].userId,
|
|
95
94
|
};
|
|
96
95
|
}
|