@markwharton/liquidplanner 1.4.1 → 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
  /**
@@ -408,13 +408,13 @@ export class LPClient {
408
408
  if (!response.ok) {
409
409
  const errorText = await response.text();
410
410
  const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
411
- return { success: false, error: message, isDuplicate };
411
+ return { success: false, error: message, statusCode: response.status, isDuplicate };
412
412
  }
413
413
  const result = await response.json();
414
414
  return { success: true, entryId: result.id };
415
415
  }
416
416
  catch (error) {
417
- return { success: false, error: getErrorMessage(error) };
417
+ return { success: false, error: getErrorMessage(error), statusCode: 0 };
418
418
  }
419
419
  }
420
420
  /**
@@ -488,12 +488,12 @@ export class LPClient {
488
488
  if (!response.ok) {
489
489
  const errorText = await response.text();
490
490
  const { message } = parseLPErrorResponse(errorText, response.status);
491
- return { success: false, error: message };
491
+ return { success: false, error: message, statusCode: response.status };
492
492
  }
493
493
  return { success: true, entryId };
494
494
  }
495
495
  catch (error) {
496
- return { success: false, error: getErrorMessage(error) };
496
+ return { success: false, error: getErrorMessage(error), statusCode: 0 };
497
497
  }
498
498
  }
499
499
  /**
@@ -535,7 +535,7 @@ export class LPClient {
535
535
  // Fetch existing entries for this date/item first
536
536
  const { entries, error: fetchError } = await this.getTimesheetEntries(entry.date, entry.itemId);
537
537
  if (fetchError) {
538
- return { success: false, error: fetchError };
538
+ return { success: false, error: fetchError.message, statusCode: fetchError.statusCode };
539
539
  }
540
540
  // Find matching entry
541
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.1",
3
+ "version": "1.5.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",