@markwharton/liquidplanner 2.7.2 → 3.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/dist/client.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * @see https://api-docs.liquidplanner.com/
8
8
  */
9
9
  import type { Result } from '@markwharton/api-core';
10
- import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPUpsertOptions, LPAssignment, LPAncestor, LPFindItemsOptions, LPWorkspaceTree } from './types.js';
10
+ import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetOptions, LPUpsertOptions, LPAssignment, LPAncestor, LPFindItemsOptions, LPWorkspaceTree } from './types.js';
11
11
  /**
12
12
  * LiquidPlanner API Client
13
13
  *
@@ -202,9 +202,9 @@ export declare class LPClient {
202
202
  * @see https://api-docs.liquidplanner.com/docs/task-status-1
203
203
  *
204
204
  * @param date - Date(s) in YYYY-MM-DD format (string or array)
205
- * @param itemId - Optional item ID to filter by
205
+ * @param options - Optional filters (itemId, memberId)
206
206
  */
207
- getTimesheetEntries(date: string | string[], itemId?: number): Promise<Result<LPTimesheetEntry[]>>;
207
+ getTimesheetEntries(date: string | string[], options?: LPTimesheetOptions): Promise<Result<LPTimesheetEntry[]>>;
208
208
  /**
209
209
  * Update an existing timesheet entry
210
210
  *
package/dist/client.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @see https://api-docs.liquidplanner.com/
8
8
  */
9
- import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIsNot, filterIn, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
9
+ import { buildAuthHeader, hoursToMinutes, filterIs, filterIsNot, filterIn, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
10
10
  import { buildTree, getTreeAncestors } from './tree.js';
11
11
  import { parseLPErrorResponse } from './errors.js';
12
12
  import { LP_API_BASE } from './constants.js';
@@ -16,7 +16,7 @@ function transformItem(raw) {
16
16
  const item = {
17
17
  id: raw.id,
18
18
  name: raw.name || null,
19
- itemType: normalizeItemType(raw.itemType),
19
+ itemType: raw.itemType,
20
20
  parentId: raw.parentId,
21
21
  costCodeId: raw.costCodeId,
22
22
  userId: raw.userId,
@@ -346,7 +346,7 @@ export class LPClient {
346
346
  return rawData.map((a) => ({
347
347
  id: a.id,
348
348
  name: a.name || null,
349
- itemType: normalizeItemType(a.itemType),
349
+ itemType: a.itemType,
350
350
  })).reverse(); // LP API returns child→root, normalize to root→child
351
351
  }, { description: `Get ancestors for item ${itemId}` });
352
352
  });
@@ -489,11 +489,11 @@ export class LPClient {
489
489
  const tree = treeResult.data;
490
490
  const assignments = [];
491
491
  for (const node of tree.byId.values()) {
492
- if (node.itemType !== 'Assignment' || node.userId !== memberId)
492
+ if (node.itemType !== 'assignments' || node.userId !== memberId)
493
493
  continue;
494
494
  const ancestors = getTreeAncestors(tree, node.id);
495
- const taskAncestor = ancestors.find(a => a.itemType === 'Task');
496
- const hierarchyAncestors = ancestors.filter(a => a.itemType === 'Project' || a.itemType === 'Folder');
495
+ const taskAncestor = ancestors.find(a => a.itemType === 'tasks');
496
+ const hierarchyAncestors = ancestors.filter(a => a.itemType === 'projects' || a.itemType === 'folders');
497
497
  const { children: _, ...itemFields } = node;
498
498
  const result = { ...itemFields };
499
499
  result.taskName = taskAncestor?.name ?? null;
@@ -576,12 +576,18 @@ export class LPClient {
576
576
  * @see https://api-docs.liquidplanner.com/docs/task-status-1
577
577
  *
578
578
  * @param date - Date(s) in YYYY-MM-DD format (string or array)
579
- * @param itemId - Optional item ID to filter by
579
+ * @param options - Optional filters (itemId, memberId)
580
580
  */
581
- async getTimesheetEntries(date, itemId) {
581
+ async getTimesheetEntries(date, options) {
582
582
  const dates = Array.isArray(date) ? date : [date];
583
583
  const sortedKey = [...dates].sort().join(',');
584
- const cacheKey = itemId ? `timesheet:${sortedKey}:${itemId}` : `timesheet:${sortedKey}`;
584
+ const { itemId, memberId } = options || {};
585
+ // Build cache key from all filter dimensions
586
+ let cacheKey = `timesheet:${sortedKey}`;
587
+ if (itemId)
588
+ cacheKey += `:item=${itemId}`;
589
+ if (memberId)
590
+ cacheKey += `:member=${memberId}`;
585
591
  return this.cached(cacheKey, this.cacheTtl.timesheetTtl, async () => {
586
592
  // Build query with date[in] filter (supports multiple dates)
587
593
  let baseUrl = this.workspaceUrl(`logged-time-entries/v1?${filterIn('date', dates)}`);
@@ -589,6 +595,10 @@ export class LPClient {
589
595
  if (itemId) {
590
596
  baseUrl += `&${filterIs('itemId', itemId)}`;
591
597
  }
598
+ // Optional filter by memberId (server-side user filtering)
599
+ if (memberId) {
600
+ baseUrl += `&${filterIs('userId', memberId)}`;
601
+ }
592
602
  return paginatedFetch({
593
603
  fetchFn: (url) => this.fetch(url),
594
604
  baseUrl,
@@ -679,7 +689,7 @@ export class LPClient {
679
689
  async upsertTimesheetEntry(entry, options = {}) {
680
690
  const { accumulate = true } = options;
681
691
  // Fetch existing entries for this date/item first
682
- const fetchResult = await this.getTimesheetEntries(entry.date, entry.itemId);
692
+ const fetchResult = await this.getTimesheetEntries(entry.date, { itemId: entry.itemId });
683
693
  if (!fetchResult.ok) {
684
694
  return { ok: false, error: fetchResult.error, status: fetchResult.status };
685
695
  }
package/dist/index.d.ts CHANGED
@@ -30,12 +30,12 @@
30
30
  */
31
31
  export { LPClient } from './client.js';
32
32
  export { resolveTaskToAssignment } from './workflows.js';
33
- export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, LPHierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTaskResolution, LPUpsertOptions, LPAssignment, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
33
+ export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, LPUserType, LPHierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTaskResolution, LPTimesheetOptions, LPUpsertOptions, LPAssignment, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
34
34
  export type { AccessTier } from './types.js';
35
35
  export { METHOD_TIERS, ENTITIES } from './types.js';
36
36
  export { ok, err, getErrorMessage, normalizeEnum, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
37
37
  export type { Result, RetryConfig, OnRequestCallback, ClientConfig, Cache, CacheStore, CacheGetOptions } from '@markwharton/api-core';
38
- export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
38
+ export { hoursToMinutes, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
39
39
  export type { PaginateOptions } from './utils.js';
40
40
  export { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree, } from './tree.js';
41
41
  export { LP_API_BASE } from './constants.js';
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ export { METHOD_TIERS, ENTITIES } from './types.js';
36
36
  // Re-exported from @markwharton/api-core
37
37
  export { ok, err, getErrorMessage, normalizeEnum, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
38
38
  // Utilities
39
- export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
39
+ export { hoursToMinutes, buildAuthHeader, filterIs, filterIsNot, filterIn, filterGt, filterLt, filterAfter, filterBefore, joinFilters, paginatedFetch, } from './utils.js';
40
40
  // Tree utilities
41
41
  export { buildTree, getTreeAncestors, getTreeHierarchyPath, findInTree, } from './tree.js';
42
42
  // Constants
package/dist/tree.d.ts CHANGED
@@ -22,8 +22,8 @@ export declare function getTreeAncestors(tree: LPWorkspaceTree, itemId: number):
22
22
  /**
23
23
  * Build a formatted hierarchy path for an item
24
24
  *
25
- * Returns a string like "Project A › Subfolder B" from Project and Folder ancestors.
26
- * Excludes system containers (Package, WorkspaceRoot) and Tasks.
25
+ * Returns a string like "Project A › Subfolder B" from projects and folders ancestors.
26
+ * Excludes system containers (packages, workspaceRoots) and tasks.
27
27
  */
28
28
  export declare function getTreeHierarchyPath(tree: LPWorkspaceTree, itemId: number): string;
29
29
  /**
package/dist/tree.js CHANGED
@@ -62,12 +62,12 @@ export function getTreeAncestors(tree, itemId) {
62
62
  /**
63
63
  * Build a formatted hierarchy path for an item
64
64
  *
65
- * Returns a string like "Project A › Subfolder B" from Project and Folder ancestors.
66
- * Excludes system containers (Package, WorkspaceRoot) and Tasks.
65
+ * Returns a string like "Project A › Subfolder B" from projects and folders ancestors.
66
+ * Excludes system containers (packages, workspaceRoots) and tasks.
67
67
  */
68
68
  export function getTreeHierarchyPath(tree, itemId) {
69
69
  const ancestors = getTreeAncestors(tree, itemId);
70
- const hierarchyAncestors = ancestors.filter(a => a.itemType === 'Project' || a.itemType === 'Folder');
70
+ const hierarchyAncestors = ancestors.filter(a => a.itemType === 'projects' || a.itemType === 'folders');
71
71
  return hierarchyAncestors
72
72
  .map(a => a.name ?? `[${a.id}]`)
73
73
  .join(' › ');
package/dist/types.d.ts CHANGED
@@ -8,7 +8,7 @@ import type { Result, RetryConfig, ClientConfig, EntityDef } from '@markwharton/
8
8
  /**
9
9
  * LiquidPlanner item types in the hierarchy
10
10
  */
11
- export type LPItemType = 'Task' | 'Assignment' | 'Folder' | 'Project' | 'Package' | 'WorkspaceRoot' | 'Milestone' | 'Event';
11
+ export type LPItemType = 'workspaceRoots' | 'packages' | 'projects' | 'folders' | 'tasks' | 'assignments' | 'custom';
12
12
  /**
13
13
  * A generic hierarchy item for representing project/task paths.
14
14
  *
@@ -131,6 +131,8 @@ export interface LPWorkspace {
131
131
  /** Workspace name */
132
132
  name: string;
133
133
  }
134
+ /** LiquidPlanner user type */
135
+ export type LPUserType = 'member' | 'resource' | 'placeholder';
134
136
  /**
135
137
  * A workspace member from LiquidPlanner
136
138
  *
@@ -154,7 +156,7 @@ export interface LPMember {
154
156
  /** Last name */
155
157
  lastName: string;
156
158
  /** User type (member, resource, or placeholder) */
157
- userType: 'member' | 'resource' | 'placeholder';
159
+ userType: LPUserType;
158
160
  }
159
161
  /**
160
162
  * Result of resolving an LP Item ID to the correct Assignment ID for logging time
@@ -243,6 +245,15 @@ export interface LPTimesheetEntry {
243
245
  /** User ID who logged the time (present in API responses) */
244
246
  userId?: number;
245
247
  }
248
+ /**
249
+ * Options for querying timesheet entries
250
+ */
251
+ export interface LPTimesheetOptions {
252
+ /** Filter by assignment/item ID (itemId[is]) */
253
+ itemId?: number;
254
+ /** Filter by member ID (userId[is]) — server-side user filtering */
255
+ memberId?: number;
256
+ }
246
257
  /**
247
258
  * Options for upsert timesheet entry operation
248
259
  */
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * LiquidPlanner Utility Functions
3
3
  */
4
- import type { LPItemType } from './types.js';
5
4
  import type { Result } from '@markwharton/api-core';
6
5
  /**
7
6
  * Build a URL-encoded filter for LP API: field[is]="value"
@@ -67,13 +66,6 @@ export declare function paginatedFetch<TRaw, TResult>(options: PaginateOptions<T
67
66
  * @throws Error if hours is not a valid non-negative number
68
67
  */
69
68
  export declare function hoursToMinutes(hours: number): number;
70
- /**
71
- * Normalize LP API itemType values to internal format
72
- *
73
- * LP API returns lowercase plural (e.g., "tasks", "assignments")
74
- * but our code uses PascalCase singular (e.g., "Task", "Assignment")
75
- */
76
- export declare function normalizeItemType(apiItemType: string): LPItemType;
77
69
  /**
78
70
  * Build the Authorization header for LP API requests
79
71
  */
package/dist/utils.js CHANGED
@@ -124,35 +124,6 @@ export function hoursToMinutes(hours) {
124
124
  }
125
125
  return Math.round(hours * 60);
126
126
  }
127
- /**
128
- * Normalize LP API itemType values to internal format
129
- *
130
- * LP API returns lowercase plural (e.g., "tasks", "assignments")
131
- * but our code uses PascalCase singular (e.g., "Task", "Assignment")
132
- */
133
- export function normalizeItemType(apiItemType) {
134
- const mapping = {
135
- // LP API lowercase plural format
136
- 'tasks': 'Task',
137
- 'assignments': 'Assignment',
138
- 'folders': 'Folder',
139
- 'projects': 'Project',
140
- 'packages': 'Package',
141
- 'workspaceRoots': 'WorkspaceRoot',
142
- 'milestones': 'Milestone',
143
- 'events': 'Event',
144
- // Already-normalized values (for safety)
145
- 'Task': 'Task',
146
- 'Assignment': 'Assignment',
147
- 'Folder': 'Folder',
148
- 'Project': 'Project',
149
- 'Package': 'Package',
150
- 'WorkspaceRoot': 'WorkspaceRoot',
151
- 'Milestone': 'Milestone',
152
- 'Event': 'Event',
153
- };
154
- return mapping[apiItemType] || apiItemType;
155
- }
156
127
  /**
157
128
  * Build the Authorization header for LP API requests
158
129
  */
package/dist/workflows.js CHANGED
@@ -40,7 +40,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
40
40
  const itemResult = await client.getItem(itemId);
41
41
  if (!itemResult.ok || !itemResult.data) {
42
42
  return {
43
- inputItem: { id: itemId, name: null, itemType: 'Task' },
43
+ inputItem: { id: itemId, name: null, itemType: 'tasks' },
44
44
  assignmentId: 0,
45
45
  error: itemResult.error || 'Item not found',
46
46
  };
@@ -48,7 +48,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
48
48
  const item = itemResult.data;
49
49
  // Step 2: Check item type and resolve accordingly
50
50
  switch (item.itemType) {
51
- case 'Assignment':
51
+ case 'assignments':
52
52
  // Already an assignment, use it directly
53
53
  return {
54
54
  inputItem: item,
@@ -56,7 +56,7 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
56
56
  assignmentName: item.name ?? undefined,
57
57
  assignmentUserId: item.userId,
58
58
  };
59
- case 'Task': {
59
+ case 'tasks': {
60
60
  // Find assignments under this task
61
61
  const assignResult = await client.findAssignments(item.id);
62
62
  if (!assignResult.ok) {
@@ -119,19 +119,11 @@ export async function resolveTaskToAssignment(client, itemId, lpMemberId) {
119
119
  multipleAssignments: assignments,
120
120
  };
121
121
  }
122
- case 'Folder':
123
- case 'Milestone':
124
- case 'Event':
125
- return {
126
- inputItem: item,
127
- assignmentId: 0,
128
- error: `Cannot log time to a ${item.itemType}. Enter a Task or Assignment ID.`,
129
- };
130
122
  default:
131
123
  return {
132
124
  inputItem: item,
133
125
  assignmentId: 0,
134
- error: `Unsupported item type: ${item.itemType}`,
126
+ error: `Cannot log time to item type '${item.itemType}'. Provide a task or assignment ID.`,
135
127
  };
136
128
  }
137
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "2.7.2",
3
+ "version": "3.1.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",