@markwharton/liquidplanner 1.2.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 +14 -1
- package/dist/client.js +42 -17
- package/dist/constants.d.ts +0 -4
- package/dist/constants.js +0 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +14 -6
- package/dist/workflows.js +4 -5
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -87,7 +100,7 @@ export declare class LPClient {
|
|
|
87
100
|
* Get assignments for a member with parent task names resolved
|
|
88
101
|
*
|
|
89
102
|
* This is a convenience method that fetches assignments and enriches
|
|
90
|
-
* them with parent task names
|
|
103
|
+
* them with parent task names using batch fetching (3 requests max instead of N+1).
|
|
91
104
|
*
|
|
92
105
|
* @param memberId - The member ID to get assignments for
|
|
93
106
|
* @param options - Options for including additional context
|
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
|
*/
|
|
@@ -187,7 +209,7 @@ export class LPClient {
|
|
|
187
209
|
* Get assignments for a member with parent task names resolved
|
|
188
210
|
*
|
|
189
211
|
* This is a convenience method that fetches assignments and enriches
|
|
190
|
-
* them with parent task names
|
|
212
|
+
* them with parent task names using batch fetching (3 requests max instead of N+1).
|
|
191
213
|
*
|
|
192
214
|
* @param memberId - The member ID to get assignments for
|
|
193
215
|
* @param options - Options for including additional context
|
|
@@ -202,21 +224,23 @@ export class LPClient {
|
|
|
202
224
|
return { assignments: [] };
|
|
203
225
|
// 2. Extract unique parent IDs (tasks)
|
|
204
226
|
const taskIds = [...new Set(assignments.map(a => a.parentId).filter((id) => id !== undefined))];
|
|
205
|
-
// 3. Batch fetch all parent tasks
|
|
206
|
-
const
|
|
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 };
|
|
207
231
|
const taskMap = new Map();
|
|
208
|
-
for (const
|
|
209
|
-
|
|
210
|
-
taskMap.set(result.item.id, result.item);
|
|
232
|
+
for (const task of tasks || []) {
|
|
233
|
+
taskMap.set(task.id, task);
|
|
211
234
|
}
|
|
212
|
-
// 4. Optionally fetch grandparent projects
|
|
235
|
+
// 4. Optionally batch fetch grandparent projects in a single request
|
|
213
236
|
let projectMap = new Map();
|
|
214
237
|
if (options?.includeProject) {
|
|
215
238
|
const projectIds = [...new Set([...taskMap.values()].map(t => t.parentId).filter((id) => id !== undefined))];
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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);
|
|
220
244
|
}
|
|
221
245
|
}
|
|
222
246
|
// 5. Merge context into assignments
|
|
@@ -226,8 +250,9 @@ export class LPClient {
|
|
|
226
250
|
const project = task?.parentId ? projectMap.get(task.parentId) : undefined;
|
|
227
251
|
return {
|
|
228
252
|
...a,
|
|
229
|
-
taskName: task?.name ??
|
|
230
|
-
|
|
253
|
+
taskName: task?.name ?? null,
|
|
254
|
+
projectId: project?.id,
|
|
255
|
+
projectName: project?.name ?? null,
|
|
231
256
|
};
|
|
232
257
|
}),
|
|
233
258
|
};
|
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
|
@@ -31,6 +31,6 @@ export { resolveTaskToAssignment } from './workflows.js';
|
|
|
31
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
|
|
@@ -154,8 +160,10 @@ export interface LPUpsertOptions {
|
|
|
154
160
|
* grandparent project name, and cost code name.
|
|
155
161
|
*/
|
|
156
162
|
export interface LPAssignmentWithContext extends LPItem {
|
|
157
|
-
/** Parent task name */
|
|
158
|
-
taskName?: string;
|
|
159
|
-
/** Grandparent project
|
|
160
|
-
|
|
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;
|
|
161
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
|
}
|