@markwharton/liquidplanner 2.0.0 → 2.1.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/README.md CHANGED
@@ -16,29 +16,36 @@ import { LPClient, resolveTaskToAssignment } from '@markwharton/liquidplanner';
16
16
  const client = new LPClient({ apiToken: 'xxx', workspaceId: 123 });
17
17
 
18
18
  // Validate credentials
19
- await client.validateToken();
19
+ const validation = await client.validateToken();
20
+ if (!validation.ok) throw new Error(validation.error);
20
21
 
21
22
  // Get workspaces
22
- const { workspaces } = await client.getWorkspaces();
23
+ const wsResult = await client.getWorkspaces();
24
+ if (wsResult.ok) console.log(wsResult.data); // LPWorkspace[]
23
25
 
24
26
  // Get workspace members
25
- const { members } = await client.getWorkspaceMembers();
27
+ const membersResult = await client.getWorkspaceMembers();
28
+ if (membersResult.ok) console.log(membersResult.data); // LPMember[]
26
29
 
27
30
  // Get user's assignments (for task picker)
28
- const { assignments } = await client.getMyAssignments(memberId);
31
+ const assignResult = await client.getMyAssignments(memberId);
32
+ if (assignResult.ok) console.log(assignResult.data); // LPItem[]
29
33
 
30
34
  // Get assignments with parent task/project names resolved
31
- const { assignments: enriched } = await client.getMyAssignmentsWithContext(memberId, {
35
+ const ctxResult = await client.getMyAssignmentsWithContext(memberId, {
32
36
  includeProject: true // optional: also fetch project names
33
37
  });
38
+ if (ctxResult.ok) console.log(ctxResult.data); // LPAssignmentWithContext[]
34
39
 
35
40
  // Get assignments with full hierarchy path
36
- const { assignments: withHierarchy } = await client.getMyAssignmentsWithContext(memberId, {
41
+ const hierResult = await client.getMyAssignmentsWithContext(memberId, {
37
42
  includeHierarchy: true // includes ancestors and hierarchyPath
38
43
  });
44
+ if (hierResult.ok) console.log(hierResult.data); // LPAssignmentWithContext[]
39
45
 
40
46
  // Get item ancestors (hierarchy chain)
41
- const { ancestors } = await client.getItemAncestors(itemId);
47
+ const ancResult = await client.getItemAncestors(itemId);
48
+ if (ancResult.ok) console.log(ancResult.data); // LPAncestor[]
42
49
 
43
50
  // Resolve task to assignment
44
51
  const resolution = await resolveTaskToAssignment(client, taskId, memberId);
@@ -52,31 +59,39 @@ await client.createTimesheetEntry({
52
59
  });
53
60
 
54
61
  // Query existing entries for a date
55
- const { entries } = await client.getTimesheetEntries('2026-01-29', assignmentId);
62
+ const tsResult = await client.getTimesheetEntries('2026-01-29', assignmentId);
63
+ if (tsResult.ok) console.log(tsResult.data); // LPTimesheetEntryWithId[]
56
64
 
57
65
  // Update an existing entry (accumulate hours)
58
- await client.updateTimesheetEntry(entryId, existingEntry, {
59
- hours: existingEntry.hours + 1.5,
66
+ const existing = tsResult.data![0];
67
+ await client.updateTimesheetEntry(existing.id, existing, {
68
+ hours: existing.hours + 1.5,
60
69
  note: 'Additional work'
61
70
  });
62
71
 
63
72
  // Find items with filters
64
- const { items } = await client.findItems({
73
+ const lateResult = await client.findItems({
65
74
  itemType: 'tasks',
66
75
  taskStatusGroupNot: 'done',
67
76
  late: true,
68
77
  });
78
+ if (lateResult.ok) console.log(lateResult.data); // LPItem[]
69
79
 
70
80
  // Get children of an item
71
- const { items: children } = await client.getChildren(parentId);
81
+ const childResult = await client.getChildren(parentId);
82
+ if (childResult.ok) console.log(childResult.data); // LPItem[]
72
83
 
73
84
  // Get workspace tree snapshot (cached, all hierarchy lookups resolved in memory)
74
- const { tree } = await client.getWorkspaceTree();
85
+ const treeResult = await client.getWorkspaceTree();
86
+ if (treeResult.ok) console.log(treeResult.data); // LPWorkspaceTree
75
87
 
76
88
  // Get a member's work with full context from the tree
77
- const { assignments: myWork, treeItemCount } = await client.getMyWork(memberId);
78
- // Each assignment includes taskName, projectId, projectName, hierarchyPath, ancestors
79
- // treeItemCount shows total items loaded (only member's assignments returned downstream)
89
+ const workResult = await client.getMyWork(memberId);
90
+ if (workResult.ok) {
91
+ const { assignments, treeItemCount } = workResult.data;
92
+ // Each assignment includes taskName, projectId, projectName, hierarchyPath, ancestors
93
+ // treeItemCount shows total items loaded (only member's assignments returned downstream)
94
+ }
80
95
  ```
81
96
 
82
97
  ### Tree Utilities
@@ -97,29 +112,49 @@ const path = getTreeHierarchyPath(tree, itemId);
97
112
  const lateTasks = findInTree(tree, item => item.late === true);
98
113
  ```
99
114
 
100
- ## API Reference
115
+ ## Result Pattern
116
+
117
+ All methods return `Result<T>` objects rather than throwing exceptions. Always check `ok` before accessing `data`:
118
+
119
+ ```typescript
120
+ interface Result<T> {
121
+ ok: boolean;
122
+ data?: T; // present when ok is true
123
+ error?: string; // present when ok is false
124
+ status?: number; // HTTP status code on error
125
+ }
126
+ ```
101
127
 
102
- All methods return `{ data?, error? }` result objects rather than throwing exceptions.
128
+ ```typescript
129
+ const result = await client.getMyWork(memberId);
130
+ if (!result.ok) {
131
+ console.error(result.error, result.status);
132
+ return;
133
+ }
134
+ const { assignments, treeItemCount } = result.data;
135
+ ```
136
+
137
+ ## API Reference
103
138
 
104
139
  | Method | Parameters | Returns |
105
140
  |--------|-----------|---------|
106
- | `validateToken()` | — | `{ valid, error? }` |
107
- | `getWorkspaces()` | — | `{ workspaces?, error? }` |
108
- | `getWorkspaceMembers()` | — | `{ members?, error? }` |
109
- | `getItem(itemId)` | `number` | `{ item?, error? }` |
110
- | `getItems(itemIds)` | `number[]` | `{ items?, error? }` |
111
- | `getItemAncestors(itemId)` | `number` | `{ ancestors?, error? }` |
112
- | `findAssignments(taskId)` | `number` | `{ assignments?, error? }` |
113
- | `getMyAssignments(memberId)` | `number` | `{ assignments?, error? }` |
114
- | `getMyAssignmentsWithContext(memberId, options?)` | `number, { includeProject?, includeHierarchy? }` | `{ assignments?, error? }` |
115
- | `findItems(options)` | `LPFindItemsOptions` | `{ items?, error? }` |
116
- | `getChildren(parentId, options?)` | `number, { itemType? }?` | `{ items?, error? }` |
117
- | `getWorkspaceTree()` | — | `{ tree?, error? }` |
118
- | `getMyWork(memberId)` | `number` | `{ assignments?, treeItemCount?, error? }` |
141
+ | `validateToken()` | — | `Result<void>` |
142
+ | `getWorkspaces()` | — | `Result<LPWorkspace[]>` |
143
+ | `getWorkspaceMembers()` | — | `Result<LPMember[]>` |
144
+ | `getItem(itemId)` | `number` | `Result<LPItem>` |
145
+ | `getItems(itemIds)` | `number[]` | `Result<LPItem[]>` |
146
+ | `getItemAncestors(itemId)` | `number` | `Result<LPAncestor[]>` |
147
+ | `findAssignments(taskId)` | `number` | `Result<LPItem[]>` |
148
+ | `getMyAssignments(memberId)` | `number` | `Result<LPItem[]>` |
149
+ | `getMyAssignmentsWithContext(memberId, options?)` | `number, { includeProject?, includeHierarchy? }` | `Result<LPAssignmentWithContext[]>` |
150
+ | `findItems(options)` | `LPFindItemsOptions` | `Result<LPItem[]>` |
151
+ | `getChildren(parentId, options?)` | `number, { itemType? }?` | `Result<LPItem[]>` |
152
+ | `getWorkspaceTree()` | — | `Result<LPWorkspaceTree>` |
153
+ | `getMyWork(memberId)` | `number` | `Result<{ assignments: LPAssignmentWithContext[], treeItemCount: number }>` |
119
154
  | `invalidateTreeCache()` | — | `void` |
120
- | `getCostCodes()` | — | `{ costCodes?, error? }` |
155
+ | `getCostCodes()` | — | `Result<LPCostCode[]>` |
121
156
  | `createTimesheetEntry(entry)` | `LPTimesheetEntry` | `LPSyncResult` |
122
- | `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `{ entries?, error? }` |
157
+ | `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `Result<LPTimesheetEntryWithId[]>` |
123
158
  | `updateTimesheetEntry(entryId, existing, updates)` | `number, LPTimesheetEntryWithId, Partial<LPTimesheetEntry>` | `LPSyncResult` |
124
159
  | `upsertTimesheetEntry(entry, options?)` | `LPTimesheetEntry, LPUpsertOptions?` | `LPSyncResult` |
125
160
 
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIsN
10
10
  import { buildTree, getTreeAncestors } from './tree.js';
11
11
  import { parseLPErrorResponse } from './errors.js';
12
12
  import { LP_API_BASE } from './constants.js';
13
- import { TTLCache, batchMap, getErrorMessage, fetchWithRetry, ok, err } from '@markwharton/api-core';
13
+ import { TTLCache, batchMap, getErrorMessage, fetchWithRetry, ok, err, resolveRetryConfig } from '@markwharton/api-core';
14
14
  /** Transform raw API item to LPItem, preserving scheduling and effort fields */
15
15
  function transformItem(raw) {
16
16
  const item = {
@@ -126,13 +126,7 @@ export class LPClient {
126
126
  treeTtl: config.cache?.treeTtl ?? 600000,
127
127
  };
128
128
  // Initialize retry config with defaults if provided
129
- if (config.retry) {
130
- this.retryConfig = {
131
- maxRetries: config.retry.maxRetries ?? 3,
132
- initialDelayMs: config.retry.initialDelayMs ?? 1000,
133
- maxDelayMs: config.retry.maxDelayMs ?? 10000,
134
- };
135
- }
129
+ this.retryConfig = resolveRetryConfig(config.retry);
136
130
  }
137
131
  /**
138
132
  * Route through cache if enabled, otherwise call factory directly.
@@ -262,7 +256,7 @@ export class LPClient {
262
256
  return err(`Unexpected response: HTTP ${response.status}`, response.status);
263
257
  }
264
258
  catch (error) {
265
- return err(getErrorMessage(error) || 'Connection failed');
259
+ return err(getErrorMessage(error, 'Connection failed'));
266
260
  }
267
261
  }
268
262
  /**
package/dist/errors.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * - {"message":"..."} or {"error":"..."}
7
7
  * - Plain text
8
8
  */
9
+ import { ApiError } from '@markwharton/api-core';
9
10
  /**
10
11
  * Parsed LP error response
11
12
  */
@@ -25,14 +26,12 @@ export interface LPParsedError {
25
26
  export declare function parseLPErrorResponse(errorText: string, statusCode: number): LPParsedError;
26
27
  /**
27
28
  * Custom error class for LP API errors
29
+ *
30
+ * Extends ApiError from api-core with LP-specific isDuplicate flag.
28
31
  */
29
- export declare class LPError extends Error {
30
- /** HTTP status code */
31
- status: number;
32
+ export declare class LPError extends ApiError {
32
33
  /** Whether this is a duplicate entry error */
33
34
  isDuplicate: boolean;
34
- /** Raw error response */
35
- rawResponse?: string;
36
35
  constructor(message: string, status: number, options?: {
37
36
  isDuplicate?: boolean;
38
37
  rawResponse?: string;
package/dist/errors.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * - {"message":"..."} or {"error":"..."}
7
7
  * - Plain text
8
8
  */
9
- import { parseJsonErrorResponse } from '@markwharton/api-core';
9
+ import { ApiError, parseJsonErrorResponse } from '@markwharton/api-core';
10
10
  /** Error code for duplicate entry errors */
11
11
  const LP_ERROR_CODE_DUPLICATE = 'duplicate_value';
12
12
  /**
@@ -36,14 +36,14 @@ export function parseLPErrorResponse(errorText, statusCode) {
36
36
  }
37
37
  /**
38
38
  * Custom error class for LP API errors
39
+ *
40
+ * Extends ApiError from api-core with LP-specific isDuplicate flag.
39
41
  */
40
- export class LPError extends Error {
42
+ export class LPError extends ApiError {
41
43
  constructor(message, status, options) {
42
- super(message);
44
+ super(message, status, options);
43
45
  this.name = 'LPError';
44
- this.status = status;
45
46
  this.isDuplicate = options?.isDuplicate ?? false;
46
- this.rawResponse = options?.rawResponse;
47
47
  }
48
48
  /**
49
49
  * Create an LPError from an API response
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@ export { LPClient } from './client.js';
32
32
  export { resolveTaskToAssignment } from './workflows.js';
33
33
  export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, HierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPUpsertOptions, LPAssignmentWithContext, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
34
34
  export { ok, err, getErrorMessage } from '@markwharton/api-core';
35
- export type { Result, RetryConfig } from '@markwharton/api-core';
35
+ export type { Result, RetryConfig, OnRequestCallback, BaseClientConfig } from '@markwharton/api-core';
36
36
  export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
37
37
  export type { PaginateOptions } from './utils.js';
38
38
  export { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree, } from './tree.js';
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@
32
32
  export { LPClient } from './client.js';
33
33
  // Workflows
34
34
  export { resolveTaskToAssignment } from './workflows.js';
35
- // Re-export Result from api-core
35
+ // Re-exported from @markwharton/api-core
36
36
  export { ok, err, getErrorMessage } from '@markwharton/api-core';
37
37
  // Utilities
38
38
  export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
package/dist/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * These types define the data structures used when interacting with
5
5
  * the LiquidPlanner API.
6
6
  */
7
- import type { Result, RetryConfig } from '@markwharton/api-core';
7
+ import type { Result, RetryConfig, BaseClientConfig } from '@markwharton/api-core';
8
8
  /**
9
9
  * LiquidPlanner item types in the hierarchy
10
10
  */
@@ -199,23 +199,13 @@ export type LPRetryConfig = RetryConfig;
199
199
  /**
200
200
  * LiquidPlanner configuration for API access
201
201
  */
202
- export interface LPConfig {
202
+ export interface LPConfig extends BaseClientConfig {
203
203
  /** Workspace ID to operate on */
204
204
  workspaceId: number;
205
205
  /** API token for authentication */
206
206
  apiToken: string;
207
- /** Base URL for LP API (defaults to https://next.liquidplanner.com/api) */
208
- baseUrl?: string;
209
- /** Optional callback invoked on each API request (for debugging/logging) */
210
- onRequest?: (info: {
211
- method: string;
212
- url: string;
213
- description?: string;
214
- }) => void;
215
207
  /** Enable caching with optional TTL overrides. Omit to disable caching. */
216
208
  cache?: LPCacheConfig;
217
- /** Retry configuration for transient failures (429, 503). Omit to disable retry. */
218
- retry?: LPRetryConfig;
219
209
  }
220
210
  /**
221
211
  * Result of a timesheet sync operation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "dependencies": {
19
- "@markwharton/api-core": "^1.1.0"
19
+ "@markwharton/api-core": "^1.2.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.10.0",