@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/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 { workspaces } = await client.getWorkspaces();
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, LPResult, LPUpsertOptions, LPAssignmentWithContext, LPErrorInfo, } from './types.js';
32
- export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
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 { getErrorMessage } from '@markwharton/api-core';
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 { workspaces } = await client.getWorkspaces();
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
- export { getErrorMessage } from '@markwharton/api-core';
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
- * Controls automatic retry behavior for transient failures
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 interface LPRetryConfig {
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
- * Structured error information from LP API
283
+ * Options for querying items with LP API filters
287
284
  *
288
- * Preserves HTTP status code for proper error handling (e.g., 429 rate limits).
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 LPErrorInfo {
291
- /** Human-readable error message */
292
- message: string;
293
- /** HTTP status code from the response */
294
- statusCode: number;
295
- /** Whether this error indicates a duplicate entry */
296
- isDuplicate?: boolean;
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, LPErrorInfo } from './types.js';
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, isDuplicate } = parseLPErrorResponse(errorText, response.status);
41
- return { error: { message, statusCode: response.status, isDuplicate } };
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 { results: allResults };
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 { error: { message: getErrorMessage(error), statusCode: 0 } };
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 { item, error: fetchError } = await client.getItem(itemId);
41
- if (fetchError || !item) {
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: fetchError?.message || 'Item not found',
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 { assignments, error: assignmentError } = await client.findAssignments(item.id);
61
- if (assignmentError) {
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: ${assignmentError.message}`,
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": "1.11.0",
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.0.0"
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
- }