@markwharton/liquidplanner 1.11.0 → 2.0.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 +65 -1
- package/dist/client.d.ts +74 -45
- package/dist/client.js +285 -105
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -6
- package/dist/index.d.ts +9 -5
- package/dist/index.js +9 -4
- package/dist/tree.d.ts +32 -0
- package/dist/tree.js +86 -0
- package/dist/types.d.ts +89 -38
- package/dist/utils.d.ts +31 -5
- package/dist/utils.js +56 -5
- package/dist/workflows.js +8 -6
- package/package.json +2 -2
- package/dist/cache.d.ts +0 -43
- package/dist/cache.js +0 -78
package/dist/index.d.ts
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* const client = new LPClient({ apiToken: 'xxx', workspaceId: 123 });
|
|
11
11
|
*
|
|
12
12
|
* // Validate credentials
|
|
13
|
-
* await client.validateToken();
|
|
13
|
+
* const result = await client.validateToken();
|
|
14
|
+
* if (!result.ok) { console.error(result.error); }
|
|
14
15
|
*
|
|
15
16
|
* // Get workspaces
|
|
16
|
-
* const
|
|
17
|
+
* const wsResult = await client.getWorkspaces();
|
|
18
|
+
* const workspaces = wsResult.data;
|
|
17
19
|
*
|
|
18
20
|
* // Resolve task to assignment
|
|
19
21
|
* const resolution = await resolveTaskToAssignment(client, taskId, memberId);
|
|
@@ -28,10 +30,12 @@
|
|
|
28
30
|
*/
|
|
29
31
|
export { LPClient } from './client.js';
|
|
30
32
|
export { resolveTaskToAssignment } from './workflows.js';
|
|
31
|
-
export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, HierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution,
|
|
32
|
-
export {
|
|
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
|
+
export { ok, err, getErrorMessage } from '@markwharton/api-core';
|
|
35
|
+
export type { Result, RetryConfig } from '@markwharton/api-core';
|
|
36
|
+
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
|
|
33
37
|
export type { PaginateOptions } from './utils.js';
|
|
34
|
-
export {
|
|
38
|
+
export { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree, } from './tree.js';
|
|
35
39
|
export { LP_API_BASE } from './constants.js';
|
|
36
40
|
export { LPError, parseLPErrorResponse } from './errors.js';
|
|
37
41
|
export type { LPParsedError } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* const client = new LPClient({ apiToken: 'xxx', workspaceId: 123 });
|
|
11
11
|
*
|
|
12
12
|
* // Validate credentials
|
|
13
|
-
* await client.validateToken();
|
|
13
|
+
* const result = await client.validateToken();
|
|
14
|
+
* if (!result.ok) { console.error(result.error); }
|
|
14
15
|
*
|
|
15
16
|
* // Get workspaces
|
|
16
|
-
* const
|
|
17
|
+
* const wsResult = await client.getWorkspaces();
|
|
18
|
+
* const workspaces = wsResult.data;
|
|
17
19
|
*
|
|
18
20
|
* // Resolve task to assignment
|
|
19
21
|
* const resolution = await resolveTaskToAssignment(client, taskId, memberId);
|
|
@@ -30,9 +32,12 @@
|
|
|
30
32
|
export { LPClient } from './client.js';
|
|
31
33
|
// Workflows
|
|
32
34
|
export { resolveTaskToAssignment } from './workflows.js';
|
|
35
|
+
// Re-export Result from api-core
|
|
36
|
+
export { ok, err, getErrorMessage } from '@markwharton/api-core';
|
|
33
37
|
// Utilities
|
|
34
|
-
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
35
|
-
|
|
38
|
+
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
|
|
39
|
+
// Tree utilities
|
|
40
|
+
export { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree, } from './tree.js';
|
|
36
41
|
// Constants
|
|
37
42
|
export { LP_API_BASE } from './constants.js';
|
|
38
43
|
// Errors
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree Building and Query Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that operate on LPWorkspaceTree — zero API calls.
|
|
5
|
+
* Used by LPClient.getWorkspaceTree() for building and by consumers for querying.
|
|
6
|
+
*/
|
|
7
|
+
import type { LPItem, LPAncestor, LPWorkspaceTree } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build a workspace tree from a flat array of items
|
|
10
|
+
*
|
|
11
|
+
* Assembles items into a tree using parentId relationships.
|
|
12
|
+
* Items whose parent is not in the array become root nodes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildTree(items: LPItem[]): LPWorkspaceTree;
|
|
15
|
+
/**
|
|
16
|
+
* Get ancestors of an item from root to parent
|
|
17
|
+
*
|
|
18
|
+
* Walks up the tree via parentId, collecting ancestors in root→child order.
|
|
19
|
+
* Same order as LPClient.getItemAncestors() — excludes the item itself.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTreeAncestors(tree: LPWorkspaceTree, itemId: number): LPAncestor[];
|
|
22
|
+
/**
|
|
23
|
+
* Build a formatted hierarchy path for an item
|
|
24
|
+
*
|
|
25
|
+
* Returns a string like "Project A › Subfolder B" from Project and Folder ancestors.
|
|
26
|
+
* Excludes system containers (Package, WorkspaceRoot) and Tasks.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getTreeHierarchyPath(tree: LPWorkspaceTree, itemId: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Find all items in the tree matching a predicate
|
|
31
|
+
*/
|
|
32
|
+
export declare function findInTree(tree: LPWorkspaceTree, predicate: (item: LPItem) => boolean): LPItem[];
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree Building and Query Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that operate on LPWorkspaceTree — zero API calls.
|
|
5
|
+
* Used by LPClient.getWorkspaceTree() for building and by consumers for querying.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Build a workspace tree from a flat array of items
|
|
9
|
+
*
|
|
10
|
+
* Assembles items into a tree using parentId relationships.
|
|
11
|
+
* Items whose parent is not in the array become root nodes.
|
|
12
|
+
*/
|
|
13
|
+
export function buildTree(items) {
|
|
14
|
+
// Create tree nodes with empty children arrays
|
|
15
|
+
const byId = new Map();
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
byId.set(item.id, { ...item, children: [] });
|
|
18
|
+
}
|
|
19
|
+
// Wire parent-child relationships
|
|
20
|
+
const roots = [];
|
|
21
|
+
for (const node of byId.values()) {
|
|
22
|
+
if (node.parentId !== undefined && byId.has(node.parentId)) {
|
|
23
|
+
byId.get(node.parentId).children.push(node);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
roots.push(node);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
roots,
|
|
31
|
+
byId,
|
|
32
|
+
fetchedAt: Date.now(),
|
|
33
|
+
itemCount: items.length,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get ancestors of an item from root to parent
|
|
38
|
+
*
|
|
39
|
+
* Walks up the tree via parentId, collecting ancestors in root→child order.
|
|
40
|
+
* Same order as LPClient.getItemAncestors() — excludes the item itself.
|
|
41
|
+
*/
|
|
42
|
+
export function getTreeAncestors(tree, itemId) {
|
|
43
|
+
const ancestors = [];
|
|
44
|
+
let current = tree.byId.get(itemId);
|
|
45
|
+
if (!current)
|
|
46
|
+
return ancestors;
|
|
47
|
+
// Walk up to root
|
|
48
|
+
while (current.parentId !== undefined) {
|
|
49
|
+
const parent = tree.byId.get(current.parentId);
|
|
50
|
+
if (!parent)
|
|
51
|
+
break;
|
|
52
|
+
ancestors.push({
|
|
53
|
+
id: parent.id,
|
|
54
|
+
name: parent.name,
|
|
55
|
+
itemType: parent.itemType,
|
|
56
|
+
});
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
// Reverse to root→child order (we collected child→root)
|
|
60
|
+
return ancestors.reverse();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a formatted hierarchy path for an item
|
|
64
|
+
*
|
|
65
|
+
* Returns a string like "Project A › Subfolder B" from Project and Folder ancestors.
|
|
66
|
+
* Excludes system containers (Package, WorkspaceRoot) and Tasks.
|
|
67
|
+
*/
|
|
68
|
+
export function getTreeHierarchyPath(tree, itemId) {
|
|
69
|
+
const ancestors = getTreeAncestors(tree, itemId);
|
|
70
|
+
const hierarchyAncestors = ancestors.filter(a => a.itemType === 'Project' || a.itemType === 'Folder');
|
|
71
|
+
return hierarchyAncestors
|
|
72
|
+
.map(a => a.name ?? `[${a.id}]`)
|
|
73
|
+
.join(' › ');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Find all items in the tree matching a predicate
|
|
77
|
+
*/
|
|
78
|
+
export function findInTree(tree, predicate) {
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const node of tree.byId.values()) {
|
|
81
|
+
if (predicate(node)) {
|
|
82
|
+
results.push(node);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +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
8
|
/**
|
|
8
9
|
* LiquidPlanner item types in the hierarchy
|
|
9
10
|
*/
|
|
@@ -91,6 +92,24 @@ export interface LPItem {
|
|
|
91
92
|
folderStatus?: string;
|
|
92
93
|
/** Priority ordering (global priority array from LP) */
|
|
93
94
|
globalPriority?: string[];
|
|
95
|
+
/** Item color */
|
|
96
|
+
color?: string;
|
|
97
|
+
/** Work type */
|
|
98
|
+
workType?: string;
|
|
99
|
+
/** Item description/notes */
|
|
100
|
+
description?: string;
|
|
101
|
+
/** Custom field values with inheritance info */
|
|
102
|
+
customFieldValues?: Record<string, unknown>;
|
|
103
|
+
/** When the item was created (ISO string) */
|
|
104
|
+
createdAt?: string;
|
|
105
|
+
/** When the item was last updated (ISO string) */
|
|
106
|
+
updatedAt?: string;
|
|
107
|
+
/** Hours clipped by work limit */
|
|
108
|
+
clippedHours?: number;
|
|
109
|
+
/** Work limit in hours */
|
|
110
|
+
workLimitHours?: number;
|
|
111
|
+
/** Whether work limit is at risk */
|
|
112
|
+
workLimitRisk?: boolean;
|
|
94
113
|
}
|
|
95
114
|
/**
|
|
96
115
|
* A cost code from LiquidPlanner
|
|
@@ -168,22 +187,15 @@ export interface LPCacheConfig {
|
|
|
168
187
|
assignmentsTtl?: number;
|
|
169
188
|
/** TTL for items and ancestors (default: 300000 = 5 min) */
|
|
170
189
|
itemsTtl?: number;
|
|
190
|
+
/** TTL for workspace tree snapshot (default: 600000 = 10 min) */
|
|
191
|
+
treeTtl?: number;
|
|
171
192
|
}
|
|
172
193
|
/**
|
|
173
194
|
* Retry configuration for LPClient
|
|
174
195
|
*
|
|
175
|
-
*
|
|
176
|
-
* (HTTP 429 Too Many Requests, 503 Service Unavailable).
|
|
177
|
-
* Uses exponential backoff with optional Retry-After header support.
|
|
196
|
+
* @deprecated Use RetryConfig from @markwharton/api-core instead
|
|
178
197
|
*/
|
|
179
|
-
export
|
|
180
|
-
/** Maximum number of retry attempts (default: 3) */
|
|
181
|
-
maxRetries?: number;
|
|
182
|
-
/** Initial delay in milliseconds before first retry (default: 1000) */
|
|
183
|
-
initialDelayMs?: number;
|
|
184
|
-
/** Maximum delay cap in milliseconds (default: 10000) */
|
|
185
|
-
maxDelayMs?: number;
|
|
186
|
-
}
|
|
198
|
+
export type LPRetryConfig = RetryConfig;
|
|
187
199
|
/**
|
|
188
200
|
* LiquidPlanner configuration for API access
|
|
189
201
|
*/
|
|
@@ -207,16 +219,10 @@ export interface LPConfig {
|
|
|
207
219
|
}
|
|
208
220
|
/**
|
|
209
221
|
* Result of a timesheet sync operation
|
|
222
|
+
*
|
|
223
|
+
* Extends Result<number> where data is the entry ID.
|
|
210
224
|
*/
|
|
211
|
-
export interface LPSyncResult {
|
|
212
|
-
/** Whether the operation succeeded */
|
|
213
|
-
success: boolean;
|
|
214
|
-
/** ID of the created entry (if successful) */
|
|
215
|
-
entryId?: number;
|
|
216
|
-
/** Error message (if failed) */
|
|
217
|
-
error?: string;
|
|
218
|
-
/** HTTP status code (if failed) - useful for detecting rate limits (429) */
|
|
219
|
-
statusCode?: number;
|
|
225
|
+
export interface LPSyncResult extends Result<number> {
|
|
220
226
|
/** Whether the error was due to a duplicate entry */
|
|
221
227
|
isDuplicate?: boolean;
|
|
222
228
|
}
|
|
@@ -244,15 +250,6 @@ export interface LPTimesheetEntryWithId extends LPTimesheetEntry {
|
|
|
244
250
|
/** User ID who logged the time */
|
|
245
251
|
userId?: number;
|
|
246
252
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Generic result wrapper for LP operations
|
|
249
|
-
*/
|
|
250
|
-
export interface LPResult<T> {
|
|
251
|
-
/** The data if successful */
|
|
252
|
-
data?: T;
|
|
253
|
-
/** Error message if failed */
|
|
254
|
-
error?: string;
|
|
255
|
-
}
|
|
256
253
|
/**
|
|
257
254
|
* Options for upsert timesheet entry operation
|
|
258
255
|
*/
|
|
@@ -283,15 +280,69 @@ export interface LPAssignmentWithContext extends LPItem {
|
|
|
283
280
|
hierarchyPath?: string;
|
|
284
281
|
}
|
|
285
282
|
/**
|
|
286
|
-
*
|
|
283
|
+
* Options for querying items with LP API filters
|
|
287
284
|
*
|
|
288
|
-
*
|
|
285
|
+
* Maps to LP API filter parameters on the items endpoint.
|
|
286
|
+
* All fields are optional — only specified fields generate filters.
|
|
289
287
|
*/
|
|
290
|
-
export interface
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
|
|
288
|
+
export interface LPFindItemsOptions {
|
|
289
|
+
/** Filter by parent item ID (parentId[is]) */
|
|
290
|
+
parentId?: number;
|
|
291
|
+
/** Filter by item type — use LP API values: 'tasks', 'assignments', 'packages', 'projects', 'folders' */
|
|
292
|
+
itemType?: string;
|
|
293
|
+
/** Filter by task status group: 'scheduled', 'unscheduled', 'done' (taskStatusGroup[is]) */
|
|
294
|
+
taskStatusGroup?: string;
|
|
295
|
+
/** Exclude a task status group (taskStatusGroup[is_not]) */
|
|
296
|
+
taskStatusGroupNot?: string;
|
|
297
|
+
/** Filter by specific task status ID (taskStatusId[is]) */
|
|
298
|
+
taskStatusId?: number;
|
|
299
|
+
/** Filter by package collection: 'scheduled', 'backlog', 'archived', 'templates' (packageStatus[is]) */
|
|
300
|
+
packageStatus?: string;
|
|
301
|
+
/** Filter by folder status: 'active', 'onHold', 'done' (folderStatus[is]) */
|
|
302
|
+
folderStatus?: string;
|
|
303
|
+
/** Filter by scheduling priority (scheduleDirective[is]) */
|
|
304
|
+
scheduleDirective?: string;
|
|
305
|
+
/** Items with expected start after this date (expectedStart[after]) */
|
|
306
|
+
expectedStartAfter?: string;
|
|
307
|
+
/** Items with expected finish before this date (expectedFinish[before]) */
|
|
308
|
+
expectedFinishBefore?: string;
|
|
309
|
+
/** Items with target start after this date (targetStart[after]) */
|
|
310
|
+
targetStartAfter?: string;
|
|
311
|
+
/** Items with target finish before this date (targetFinish[before]) */
|
|
312
|
+
targetFinishBefore?: string;
|
|
313
|
+
/** Items completed after this date (doneDate[after]) */
|
|
314
|
+
doneDateAfter?: string;
|
|
315
|
+
/** Filter for late items (late[is]) */
|
|
316
|
+
late?: boolean;
|
|
317
|
+
/** Filter for items at work limit risk (workLimitRisk[is]) */
|
|
318
|
+
workLimitRisk?: boolean;
|
|
319
|
+
/** Filter for items with files (hasFiles[is]) */
|
|
320
|
+
hasFiles?: boolean;
|
|
321
|
+
/** Filter by item name (name[is]) */
|
|
322
|
+
name?: string;
|
|
323
|
+
/** Filter by custom field values (customFieldValues.<key>[is]) */
|
|
324
|
+
customFieldValues?: Record<string, string>;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A node in the workspace tree, extending LPItem with children
|
|
328
|
+
*/
|
|
329
|
+
export interface LPTreeNode extends LPItem {
|
|
330
|
+
/** Direct children of this node */
|
|
331
|
+
children: LPTreeNode[];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Workspace tree snapshot with lookup indices
|
|
335
|
+
*
|
|
336
|
+
* Built from a single fetch of all active items, this structure
|
|
337
|
+
* enables zero-API-call hierarchy lookups via the `byId` index.
|
|
338
|
+
*/
|
|
339
|
+
export interface LPWorkspaceTree {
|
|
340
|
+
/** Root nodes (typically packages) */
|
|
341
|
+
roots: LPTreeNode[];
|
|
342
|
+
/** Fast lookup: item ID → tree node */
|
|
343
|
+
byId: Map<number, LPTreeNode>;
|
|
344
|
+
/** Timestamp when snapshot was taken (Date.now()) */
|
|
345
|
+
fetchedAt: number;
|
|
346
|
+
/** Total number of items in the tree */
|
|
347
|
+
itemCount: number;
|
|
297
348
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LiquidPlanner Utility Functions
|
|
3
3
|
*/
|
|
4
|
-
import type { LPItemType
|
|
4
|
+
import type { LPItemType } from './types.js';
|
|
5
|
+
import type { Result } from '@markwharton/api-core';
|
|
5
6
|
/**
|
|
6
7
|
* Build a URL-encoded filter for LP API: field[is]="value"
|
|
7
8
|
*/
|
|
8
9
|
export declare function filterIs(field: string, value: string | number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Build a URL-encoded filter for LP API: field[is_not]="value"
|
|
12
|
+
*/
|
|
13
|
+
export declare function filterIsNot(field: string, value: string | number): string;
|
|
9
14
|
/**
|
|
10
15
|
* Build a URL-encoded filter for LP API: field[in]=["value1","value2"]
|
|
11
16
|
*/
|
|
12
17
|
export declare function filterIn(field: string, values: (string | number)[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* Build a URL-encoded filter for LP API: field[gt]="value"
|
|
20
|
+
*/
|
|
21
|
+
export declare function filterGt(field: string, value: string | number): string;
|
|
22
|
+
/**
|
|
23
|
+
* Build a URL-encoded filter for LP API: field[lt]="value"
|
|
24
|
+
*/
|
|
25
|
+
export declare function filterLt(field: string, value: string | number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build a URL-encoded filter for LP API: field[after]="value"
|
|
28
|
+
*
|
|
29
|
+
* Accepts YYYY-MM-DD or full ISO strings.
|
|
30
|
+
*/
|
|
31
|
+
export declare function filterAfter(field: string, value: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Build a URL-encoded filter for LP API: field[before]="value"
|
|
34
|
+
*
|
|
35
|
+
* Accepts YYYY-MM-DD or full ISO strings.
|
|
36
|
+
*/
|
|
37
|
+
export declare function filterBefore(field: string, value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Join multiple filter expressions with &
|
|
40
|
+
*/
|
|
41
|
+
export declare function joinFilters(...filters: string[]): string;
|
|
13
42
|
/**
|
|
14
43
|
* Options for paginated fetch
|
|
15
44
|
*/
|
|
@@ -28,10 +57,7 @@ export interface PaginateOptions<TRaw, TResult> {
|
|
|
28
57
|
*
|
|
29
58
|
* Handles the continuation token pattern used by LP API.
|
|
30
59
|
*/
|
|
31
|
-
export declare function paginatedFetch<TRaw, TResult>(options: PaginateOptions<TRaw, TResult>): Promise<
|
|
32
|
-
results?: TResult[];
|
|
33
|
-
error?: LPErrorInfo;
|
|
34
|
-
}>;
|
|
60
|
+
export declare function paginatedFetch<TRaw, TResult>(options: PaginateOptions<TRaw, TResult>): Promise<Result<TResult[]>>;
|
|
35
61
|
/**
|
|
36
62
|
* Convert decimal hours to minutes
|
|
37
63
|
*
|
package/dist/utils.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* LiquidPlanner Utility Functions
|
|
3
3
|
*/
|
|
4
4
|
import { parseLPErrorResponse } from './errors.js';
|
|
5
|
-
import { getErrorMessage } from '@markwharton/api-core';
|
|
5
|
+
import { getErrorMessage, err } from '@markwharton/api-core';
|
|
6
6
|
// ============================================================================
|
|
7
7
|
// LP API Filter Builders
|
|
8
8
|
// ============================================================================
|
|
@@ -12,6 +12,12 @@ import { getErrorMessage } from '@markwharton/api-core';
|
|
|
12
12
|
export function filterIs(field, value) {
|
|
13
13
|
return `${field}%5Bis%5D=%22${value}%22`;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a URL-encoded filter for LP API: field[is_not]="value"
|
|
17
|
+
*/
|
|
18
|
+
export function filterIsNot(field, value) {
|
|
19
|
+
return `${field}%5Bis_not%5D=%22${value}%22`;
|
|
20
|
+
}
|
|
15
21
|
/**
|
|
16
22
|
* Build a URL-encoded filter for LP API: field[in]=["value1","value2"]
|
|
17
23
|
*/
|
|
@@ -19,6 +25,51 @@ export function filterIn(field, values) {
|
|
|
19
25
|
const encoded = values.map(v => `%22${v}%22`).join(',');
|
|
20
26
|
return `${field}%5Bin%5D=%5B${encoded}%5D`;
|
|
21
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a URL-encoded filter for LP API: field[gt]="value"
|
|
30
|
+
*/
|
|
31
|
+
export function filterGt(field, value) {
|
|
32
|
+
return `${field}%5Bgt%5D=%22${value}%22`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a URL-encoded filter for LP API: field[lt]="value"
|
|
36
|
+
*/
|
|
37
|
+
export function filterLt(field, value) {
|
|
38
|
+
return `${field}%5Blt%5D=%22${value}%22`;
|
|
39
|
+
}
|
|
40
|
+
/** Normalize YYYY-MM-DD to ISO offset string using local timezone (LP API requires ISO for date filters) */
|
|
41
|
+
function toISODateValue(value) {
|
|
42
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
43
|
+
return value;
|
|
44
|
+
const d = new Date(`${value}T00:00:00`);
|
|
45
|
+
const offset = -d.getTimezoneOffset();
|
|
46
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
47
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0');
|
|
48
|
+
const minutes = String(Math.abs(offset) % 60).padStart(2, '0');
|
|
49
|
+
return `${value}T00:00:00${sign}${hours}:${minutes}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build a URL-encoded filter for LP API: field[after]="value"
|
|
53
|
+
*
|
|
54
|
+
* Accepts YYYY-MM-DD or full ISO strings.
|
|
55
|
+
*/
|
|
56
|
+
export function filterAfter(field, value) {
|
|
57
|
+
return `${field}%5Bafter%5D=%22${toISODateValue(value)}%22`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a URL-encoded filter for LP API: field[before]="value"
|
|
61
|
+
*
|
|
62
|
+
* Accepts YYYY-MM-DD or full ISO strings.
|
|
63
|
+
*/
|
|
64
|
+
export function filterBefore(field, value) {
|
|
65
|
+
return `${field}%5Bbefore%5D=%22${toISODateValue(value)}%22`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Join multiple filter expressions with &
|
|
69
|
+
*/
|
|
70
|
+
export function joinFilters(...filters) {
|
|
71
|
+
return filters.join('&');
|
|
72
|
+
}
|
|
22
73
|
/**
|
|
23
74
|
* Generic pagination helper for LP API endpoints
|
|
24
75
|
*
|
|
@@ -37,8 +88,8 @@ export async function paginatedFetch(options) {
|
|
|
37
88
|
const response = await fetchFn(url);
|
|
38
89
|
if (!response.ok) {
|
|
39
90
|
const errorText = await response.text();
|
|
40
|
-
const { message
|
|
41
|
-
return
|
|
91
|
+
const { message } = parseLPErrorResponse(errorText, response.status);
|
|
92
|
+
return err(message, response.status);
|
|
42
93
|
}
|
|
43
94
|
const result = await response.json();
|
|
44
95
|
const rawData = result.data || [];
|
|
@@ -47,11 +98,11 @@ export async function paginatedFetch(options) {
|
|
|
47
98
|
allResults.push(...pageResults);
|
|
48
99
|
continuationToken = result.continuationToken;
|
|
49
100
|
} while (continuationToken);
|
|
50
|
-
return {
|
|
101
|
+
return { ok: true, data: allResults };
|
|
51
102
|
}
|
|
52
103
|
catch (error) {
|
|
53
104
|
// Network errors or JSON parse errors don't have HTTP status codes
|
|
54
|
-
return
|
|
105
|
+
return err(getErrorMessage(error), 0);
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
/**
|
package/dist/workflows.js
CHANGED
|
@@ -37,14 +37,15 @@
|
|
|
37
37
|
*/
|
|
38
38
|
export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
39
39
|
// Step 1: Fetch the item
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
40
|
+
const itemResult = await client.getItem(itemId);
|
|
41
|
+
if (!itemResult.ok || !itemResult.data) {
|
|
42
42
|
return {
|
|
43
43
|
inputItem: { id: itemId, name: null, itemType: 'Task' },
|
|
44
44
|
assignmentId: 0,
|
|
45
|
-
error:
|
|
45
|
+
error: itemResult.error || 'Item not found',
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
const item = itemResult.data;
|
|
48
49
|
// Step 2: Check item type and resolve accordingly
|
|
49
50
|
switch (item.itemType) {
|
|
50
51
|
case 'Assignment':
|
|
@@ -57,14 +58,15 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
|
|
|
57
58
|
};
|
|
58
59
|
case 'Task': {
|
|
59
60
|
// Find assignments under this task
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
61
|
+
const assignResult = await client.findAssignments(item.id);
|
|
62
|
+
if (!assignResult.ok) {
|
|
62
63
|
return {
|
|
63
64
|
inputItem: item,
|
|
64
65
|
assignmentId: 0,
|
|
65
|
-
error: `Failed to find assignments: ${
|
|
66
|
+
error: `Failed to find assignments: ${assignResult.error}`,
|
|
66
67
|
};
|
|
67
68
|
}
|
|
69
|
+
const assignments = assignResult.data;
|
|
68
70
|
if (!assignments || assignments.length === 0) {
|
|
69
71
|
return {
|
|
70
72
|
inputItem: item,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markwharton/liquidplanner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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.1.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^20.10.0",
|
package/dist/cache.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple in-memory TTL cache with request coalescing
|
|
3
|
-
*
|
|
4
|
-
* Provides per-instance memoization for LPClient API responses.
|
|
5
|
-
* In serverless environments (Azure Functions, Static Web Apps),
|
|
6
|
-
* module-level state persists across warm invocations within the
|
|
7
|
-
* same instance — this cache leverages that behavior.
|
|
8
|
-
*
|
|
9
|
-
* Request coalescing: when multiple concurrent callers request the
|
|
10
|
-
* same expired key, only one factory call is made. All callers
|
|
11
|
-
* receive the same resolved value (or the same rejection).
|
|
12
|
-
*
|
|
13
|
-
* Not a distributed cache: each instance has its own cache.
|
|
14
|
-
* Cold starts and instance recycling naturally clear stale data.
|
|
15
|
-
*/
|
|
16
|
-
export declare class TTLCache {
|
|
17
|
-
private store;
|
|
18
|
-
private inflight;
|
|
19
|
-
/**
|
|
20
|
-
* Get a cached value, or call the factory to populate it.
|
|
21
|
-
*
|
|
22
|
-
* If a factory call is already in progress for this key,
|
|
23
|
-
* returns the existing promise instead of starting a duplicate.
|
|
24
|
-
*
|
|
25
|
-
* @param key - Cache key
|
|
26
|
-
* @param ttlMs - Time-to-live in milliseconds
|
|
27
|
-
* @param factory - Async function to produce the value on cache miss
|
|
28
|
-
*/
|
|
29
|
-
get<T>(key: string, ttlMs: number, factory: () => Promise<T>): Promise<T>;
|
|
30
|
-
/**
|
|
31
|
-
* Invalidate cache entries matching a key prefix.
|
|
32
|
-
*
|
|
33
|
-
* Also cancels any in-flight requests for matching keys,
|
|
34
|
-
* so subsequent calls will start fresh factory invocations.
|
|
35
|
-
*
|
|
36
|
-
* Example: invalidate('timesheet:') clears all timesheet entries.
|
|
37
|
-
*/
|
|
38
|
-
invalidate(prefix: string): void;
|
|
39
|
-
/**
|
|
40
|
-
* Clear all cached data and in-flight requests.
|
|
41
|
-
*/
|
|
42
|
-
clear(): void;
|
|
43
|
-
}
|