@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 +68 -33
- package/dist/client.js +3 -9
- package/dist/errors.d.ts +4 -5
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +2 -12
- package/package.json +2 -2
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
|
|
23
|
+
const wsResult = await client.getWorkspaces();
|
|
24
|
+
if (wsResult.ok) console.log(wsResult.data); // LPWorkspace[]
|
|
23
25
|
|
|
24
26
|
// Get workspace members
|
|
25
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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()` | — | `
|
|
107
|
-
| `getWorkspaces()` | — | `
|
|
108
|
-
| `getWorkspaceMembers()` | — | `
|
|
109
|
-
| `getItem(itemId)` | `number` | `
|
|
110
|
-
| `getItems(itemIds)` | `number[]` | `
|
|
111
|
-
| `getItemAncestors(itemId)` | `number` | `
|
|
112
|
-
| `findAssignments(taskId)` | `number` | `
|
|
113
|
-
| `getMyAssignments(memberId)` | `number` | `
|
|
114
|
-
| `getMyAssignmentsWithContext(memberId, options?)` | `number, { includeProject?, includeHierarchy? }` | `
|
|
115
|
-
| `findItems(options)` | `LPFindItemsOptions` | `
|
|
116
|
-
| `getChildren(parentId, options?)` | `number, { itemType? }?` | `
|
|
117
|
-
| `getWorkspaceTree()` | — | `
|
|
118
|
-
| `getMyWork(memberId)` | `number` | `{ assignments
|
|
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()` | — | `
|
|
155
|
+
| `getCostCodes()` | — | `Result<LPCostCode[]>` |
|
|
121
156
|
| `createTimesheetEntry(entry)` | `LPTimesheetEntry` | `LPSyncResult` |
|
|
122
|
-
| `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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.
|
|
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.
|
|
19
|
+
"@markwharton/api-core": "^1.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.10.0",
|