@markwharton/liquidplanner 1.4.0 → 1.5.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 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, LPAncestor } from './types.js';
9
+ import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext, LPAncestor, LPErrorInfo } from './types.js';
10
10
  /**
11
11
  * LiquidPlanner API Client
12
12
  *
@@ -51,21 +51,21 @@ export declare class LPClient {
51
51
  */
52
52
  getWorkspaces(): Promise<{
53
53
  workspaces?: LPWorkspace[];
54
- error?: string;
54
+ error?: LPErrorInfo;
55
55
  }>;
56
56
  /**
57
57
  * Get all members in the workspace (with pagination)
58
58
  */
59
59
  getWorkspaceMembers(): Promise<{
60
60
  members?: LPMember[];
61
- error?: string;
61
+ error?: LPErrorInfo;
62
62
  }>;
63
63
  /**
64
64
  * Get a single item by ID
65
65
  */
66
66
  getItem(itemId: number): Promise<{
67
67
  item?: LPItem;
68
- error?: string;
68
+ error?: LPErrorInfo;
69
69
  }>;
70
70
  /**
71
71
  * Get multiple items by ID in a single request (batch fetch)
@@ -77,7 +77,7 @@ export declare class LPClient {
77
77
  */
78
78
  getItems(itemIds: number[]): Promise<{
79
79
  items?: LPItem[];
80
- error?: string;
80
+ error?: LPErrorInfo;
81
81
  }>;
82
82
  /**
83
83
  * Get the ancestry chain for an item
@@ -89,14 +89,14 @@ export declare class LPClient {
89
89
  */
90
90
  getItemAncestors(itemId: number): Promise<{
91
91
  ancestors?: LPAncestor[];
92
- error?: string;
92
+ error?: LPErrorInfo;
93
93
  }>;
94
94
  /**
95
95
  * Find all assignments under a task (with pagination)
96
96
  */
97
97
  findAssignments(taskId: number): Promise<{
98
98
  assignments?: LPItem[];
99
- error?: string;
99
+ error?: LPErrorInfo;
100
100
  }>;
101
101
  /**
102
102
  * Get all assignments for a specific member
@@ -106,7 +106,7 @@ export declare class LPClient {
106
106
  */
107
107
  getMyAssignments(memberId: number): Promise<{
108
108
  assignments?: LPItem[];
109
- error?: string;
109
+ error?: LPErrorInfo;
110
110
  }>;
111
111
  /**
112
112
  * Get assignments for a member with parent task names resolved
@@ -129,14 +129,14 @@ export declare class LPClient {
129
129
  includeHierarchy?: boolean;
130
130
  }): Promise<{
131
131
  assignments?: LPAssignmentWithContext[];
132
- error?: string;
132
+ error?: LPErrorInfo;
133
133
  }>;
134
134
  /**
135
135
  * Get all cost codes in the workspace (with pagination)
136
136
  */
137
137
  getCostCodes(): Promise<{
138
138
  costCodes?: LPCostCode[];
139
- error?: string;
139
+ error?: LPErrorInfo;
140
140
  }>;
141
141
  /**
142
142
  * Create a timesheet entry (log time)
@@ -158,7 +158,7 @@ export declare class LPClient {
158
158
  */
159
159
  getTimesheetEntries(date: string, itemId?: number): Promise<{
160
160
  entries?: LPTimesheetEntryWithId[];
161
- error?: string;
161
+ error?: LPErrorInfo;
162
162
  }>;
163
163
  /**
164
164
  * Update an existing timesheet entry
package/dist/client.js CHANGED
@@ -96,8 +96,8 @@ export class LPClient {
96
96
  const response = await this.fetch(url);
97
97
  if (!response.ok) {
98
98
  const errorText = await response.text();
99
- const { message } = parseLPErrorResponse(errorText, response.status);
100
- return { error: message };
99
+ const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
100
+ return { error: { message, statusCode: response.status, isDuplicate } };
101
101
  }
102
102
  const result = await response.json();
103
103
  const workspaces = (result.data || []).map(ws => ({
@@ -107,7 +107,7 @@ export class LPClient {
107
107
  return { workspaces };
108
108
  }
109
109
  catch (error) {
110
- return { error: getErrorMessage(error) };
110
+ return { error: { message: getErrorMessage(error), statusCode: 0 } };
111
111
  }
112
112
  }
113
113
  // ============================================================================
@@ -144,17 +144,17 @@ export class LPClient {
144
144
  const response = await this.fetch(url);
145
145
  if (!response.ok) {
146
146
  const errorText = await response.text();
147
- const { message } = parseLPErrorResponse(errorText, response.status);
148
- return { error: message };
147
+ const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
148
+ return { error: { message, statusCode: response.status, isDuplicate } };
149
149
  }
150
150
  const result = await response.json();
151
151
  if (!result.data || result.data.length === 0) {
152
- return { error: `Item ${itemId} not found` };
152
+ return { error: { message: `Item ${itemId} not found`, statusCode: 404 } };
153
153
  }
154
154
  return { item: transformItem(result.data[0]) };
155
155
  }
156
156
  catch (error) {
157
- return { error: getErrorMessage(error) };
157
+ return { error: { message: getErrorMessage(error), statusCode: 0 } };
158
158
  }
159
159
  }
160
160
  /**
@@ -192,8 +192,8 @@ export class LPClient {
192
192
  });
193
193
  if (!response.ok) {
194
194
  const errorText = await response.text();
195
- const { message } = parseLPErrorResponse(errorText, response.status);
196
- return { error: message };
195
+ const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
196
+ return { error: { message, statusCode: response.status, isDuplicate } };
197
197
  }
198
198
  const json = (await response.json());
199
199
  // Handle both { data: [...] } and direct array responses
@@ -206,7 +206,7 @@ export class LPClient {
206
206
  return { ancestors };
207
207
  }
208
208
  catch (error) {
209
- return { error: getErrorMessage(error) };
209
+ return { error: { message: getErrorMessage(error), statusCode: 0 } };
210
210
  }
211
211
  }
212
212
  /**
@@ -276,9 +276,13 @@ export class LPClient {
276
276
  }
277
277
  }
278
278
  const ancestorResults = await Promise.all([...assignmentsByParent.entries()].map(async ([parentId, assignment]) => {
279
- const { ancestors } = await this.getItemAncestors(assignment.id);
280
- return { parentId, ancestors };
279
+ const { ancestors, error } = await this.getItemAncestors(assignment.id);
280
+ return { parentId, ancestors, error };
281
281
  }));
282
+ const firstError = ancestorResults.find(r => r.error);
283
+ if (firstError) {
284
+ return { error: firstError.error };
285
+ }
282
286
  for (const { parentId, ancestors } of ancestorResults) {
283
287
  ancestorMap.set(parentId, ancestors);
284
288
  }
@@ -404,13 +408,13 @@ export class LPClient {
404
408
  if (!response.ok) {
405
409
  const errorText = await response.text();
406
410
  const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
407
- return { success: false, error: message, isDuplicate };
411
+ return { success: false, error: message, statusCode: response.status, isDuplicate };
408
412
  }
409
413
  const result = await response.json();
410
414
  return { success: true, entryId: result.id };
411
415
  }
412
416
  catch (error) {
413
- return { success: false, error: getErrorMessage(error) };
417
+ return { success: false, error: getErrorMessage(error), statusCode: 0 };
414
418
  }
415
419
  }
416
420
  /**
@@ -484,12 +488,12 @@ export class LPClient {
484
488
  if (!response.ok) {
485
489
  const errorText = await response.text();
486
490
  const { message } = parseLPErrorResponse(errorText, response.status);
487
- return { success: false, error: message };
491
+ return { success: false, error: message, statusCode: response.status };
488
492
  }
489
493
  return { success: true, entryId };
490
494
  }
491
495
  catch (error) {
492
- return { success: false, error: getErrorMessage(error) };
496
+ return { success: false, error: getErrorMessage(error), statusCode: 0 };
493
497
  }
494
498
  }
495
499
  /**
@@ -531,7 +535,7 @@ export class LPClient {
531
535
  // Fetch existing entries for this date/item first
532
536
  const { entries, error: fetchError } = await this.getTimesheetEntries(entry.date, entry.itemId);
533
537
  if (fetchError) {
534
- return { success: false, error: fetchError };
538
+ return { success: false, error: fetchError.message, statusCode: fetchError.statusCode };
535
539
  }
536
540
  // Find matching entry
537
541
  // If no costCodeId specified, match any entry (LP uses assignment's default)
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, LPAncestor, 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, LPErrorInfo, } 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
@@ -119,6 +119,8 @@ export interface LPSyncResult {
119
119
  entryId?: number;
120
120
  /** Error message (if failed) */
121
121
  error?: string;
122
+ /** HTTP status code (if failed) - useful for detecting rate limits (429) */
123
+ statusCode?: number;
122
124
  /** Whether the error was due to a duplicate entry */
123
125
  isDuplicate?: boolean;
124
126
  }
@@ -184,3 +186,16 @@ export interface LPAssignmentWithContext extends LPItem {
184
186
  /** Formatted hierarchy path like "Project A › Subfolder B" (undefined if not requested) */
185
187
  hierarchyPath?: string;
186
188
  }
189
+ /**
190
+ * Structured error information from LP API
191
+ *
192
+ * Preserves HTTP status code for proper error handling (e.g., 429 rate limits).
193
+ */
194
+ export interface LPErrorInfo {
195
+ /** Human-readable error message */
196
+ message: string;
197
+ /** HTTP status code from the response */
198
+ statusCode: number;
199
+ /** Whether this error indicates a duplicate entry */
200
+ isDuplicate?: boolean;
201
+ }
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LiquidPlanner Utility Functions
3
3
  */
4
- import type { LPItemType } from './types.js';
4
+ import type { LPItemType, LPErrorInfo } from './types.js';
5
5
  /**
6
6
  * Build a URL-encoded filter for LP API: field[is]="value"
7
7
  */
@@ -30,7 +30,7 @@ export interface PaginateOptions<TRaw, TResult> {
30
30
  */
31
31
  export declare function paginatedFetch<TRaw, TResult>(options: PaginateOptions<TRaw, TResult>): Promise<{
32
32
  results?: TResult[];
33
- error?: string;
33
+ error?: LPErrorInfo;
34
34
  }>;
35
35
  /**
36
36
  * Convert decimal hours to minutes
package/dist/utils.js CHANGED
@@ -36,8 +36,8 @@ export async function paginatedFetch(options) {
36
36
  const response = await fetchFn(url);
37
37
  if (!response.ok) {
38
38
  const errorText = await response.text();
39
- const { message } = parseLPErrorResponse(errorText, response.status);
40
- return { error: message };
39
+ const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
40
+ return { error: { message, statusCode: response.status, isDuplicate } };
41
41
  }
42
42
  const result = await response.json();
43
43
  const rawData = result.data || [];
@@ -49,7 +49,8 @@ export async function paginatedFetch(options) {
49
49
  return { results: allResults };
50
50
  }
51
51
  catch (error) {
52
- return { error: getErrorMessage(error) };
52
+ // Network errors or JSON parse errors don't have HTTP status codes
53
+ return { error: { message: getErrorMessage(error), statusCode: 0 } };
53
54
  }
54
55
  }
55
56
  /**
package/dist/workflows.js CHANGED
@@ -42,7 +42,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
42
42
  return {
43
43
  inputItem: { id: itemId, name: null, itemType: 'Task' },
44
44
  assignmentId: 0,
45
- error: fetchError || 'Item not found',
45
+ error: fetchError?.message || 'Item not found',
46
46
  };
47
47
  }
48
48
  // Step 2: Check item type and resolve accordingly
@@ -62,7 +62,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
62
62
  return {
63
63
  inputItem: item,
64
64
  assignmentId: 0,
65
- error: `Failed to find assignments: ${assignmentError}`,
65
+ error: `Failed to find assignments: ${assignmentError.message}`,
66
66
  };
67
67
  }
68
68
  if (!assignments || assignments.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",