@markwharton/liquidplanner 3.0.0 → 3.2.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, LPAncestor, LPFindItemsOptions, LPWorkspaceTree, LPAssignmentsResult } from './types.js';
11
11
  /**
12
12
  * LiquidPlanner API Client
13
13
  *
@@ -166,19 +166,19 @@ export declare class LPClient {
166
166
  */
167
167
  getWorkspaceTree(): Promise<Result<LPWorkspaceTree>>;
168
168
  /**
169
- * Get a member's active work with full context from the workspace tree
169
+ * Get assignments with full context from the workspace tree
170
+ *
171
+ * Returns a normalized result with shared lookup tables for ancestor items
172
+ * and custom field values, reducing payload size for large workspaces.
170
173
  *
171
174
  * Uses the workspace tree snapshot (cached) to resolve hierarchy locally,
172
175
  * eliminating N+1 ancestor request patterns.
173
176
  *
174
177
  * API calls: 0 (if tree cached) or 1-3 (cold load)
175
178
  *
176
- * @param memberId - The member ID to get assignments for
179
+ * @param memberId - Filter to a specific member's assignments. When omitted, returns all members' assignments.
177
180
  */
178
- getAssignments(memberId: number): Promise<Result<{
179
- assignments: LPAssignment[];
180
- treeItemCount: number;
181
- }>>;
181
+ getAssignments(memberId?: number): Promise<Result<LPAssignmentsResult>>;
182
182
  /**
183
183
  * Get all cost codes in the workspace
184
184
  */
@@ -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
@@ -473,31 +473,62 @@ export class LPClient {
473
473
  });
474
474
  }
475
475
  /**
476
- * Get a member's active work with full context from the workspace tree
476
+ * Get assignments with full context from the workspace tree
477
+ *
478
+ * Returns a normalized result with shared lookup tables for ancestor items
479
+ * and custom field values, reducing payload size for large workspaces.
477
480
  *
478
481
  * Uses the workspace tree snapshot (cached) to resolve hierarchy locally,
479
482
  * eliminating N+1 ancestor request patterns.
480
483
  *
481
484
  * API calls: 0 (if tree cached) or 1-3 (cold load)
482
485
  *
483
- * @param memberId - The member ID to get assignments for
486
+ * @param memberId - Filter to a specific member's assignments. When omitted, returns all members' assignments.
484
487
  */
485
488
  async getAssignments(memberId) {
486
489
  const treeResult = await this.getWorkspaceTree();
487
490
  if (!treeResult.ok)
488
491
  return err(treeResult.error, treeResult.status);
489
492
  const tree = treeResult.data;
490
- const assignments = [];
493
+ const items = {};
494
+ const cfvMap = new Map();
495
+ const cfvArray = [];
496
+ const memberMap = new Map();
491
497
  for (const node of tree.byId.values()) {
492
- if (node.itemType !== 'assignments' || node.userId !== memberId)
498
+ if (node.itemType !== 'assignments')
499
+ continue;
500
+ if (memberId !== undefined && node.userId !== memberId)
493
501
  continue;
494
502
  const ancestors = getTreeAncestors(tree, node.id);
495
503
  const taskAncestor = ancestors.find(a => a.itemType === 'tasks');
496
504
  const hierarchyAncestors = ancestors.filter(a => a.itemType === 'projects' || a.itemType === 'folders');
497
- const { children: _, ...itemFields } = node;
498
- const result = { ...itemFields };
505
+ // Collect ancestor items into shared lookup
506
+ for (const anc of ancestors) {
507
+ if (!items[anc.id]) {
508
+ const treeNode = tree.byId.get(anc.id);
509
+ items[anc.id] = {
510
+ id: anc.id,
511
+ name: anc.name,
512
+ itemType: anc.itemType,
513
+ parentId: treeNode?.parentId,
514
+ };
515
+ }
516
+ }
517
+ // Dedup customFieldValues
518
+ const { children: _, customFieldValues, ...itemFields } = node;
519
+ let customFieldValuesIndex;
520
+ if (customFieldValues) {
521
+ const key = JSON.stringify(customFieldValues);
522
+ let idx = cfvMap.get(key);
523
+ if (idx === undefined) {
524
+ idx = cfvArray.length;
525
+ cfvArray.push(customFieldValues);
526
+ cfvMap.set(key, idx);
527
+ }
528
+ customFieldValuesIndex = idx;
529
+ }
530
+ const result = { ...itemFields, customFieldValuesIndex };
499
531
  result.taskName = taskAncestor?.name ?? null;
500
- result.ancestors = ancestors;
501
532
  if (hierarchyAncestors.length > 0) {
502
533
  result.projectId = hierarchyAncestors[0].id;
503
534
  result.projectName = hierarchyAncestors[0].name;
@@ -508,9 +539,19 @@ export class LPClient {
508
539
  else {
509
540
  result.projectName = null;
510
541
  }
511
- assignments.push(result);
542
+ // Group by member
543
+ const uid = node.userId ?? 0;
544
+ let memberAssignments = memberMap.get(uid);
545
+ if (!memberAssignments) {
546
+ memberAssignments = [];
547
+ memberMap.set(uid, memberAssignments);
548
+ }
549
+ memberAssignments.push(result);
512
550
  }
513
- return ok({ assignments, treeItemCount: tree.itemCount });
551
+ const members = [...memberMap.entries()]
552
+ .sort(([a], [b]) => a - b)
553
+ .map(([mid, assignments]) => ({ memberId: mid, assignments }));
554
+ return ok({ members, items, customFieldValues: cfvArray, treeItemCount: tree.itemCount });
514
555
  }
515
556
  // ============================================================================
516
557
  // Cost Codes
@@ -576,12 +617,18 @@ export class LPClient {
576
617
  * @see https://api-docs.liquidplanner.com/docs/task-status-1
577
618
  *
578
619
  * @param date - Date(s) in YYYY-MM-DD format (string or array)
579
- * @param itemId - Optional item ID to filter by
620
+ * @param options - Optional filters (itemId, memberId)
580
621
  */
581
- async getTimesheetEntries(date, itemId) {
622
+ async getTimesheetEntries(date, options) {
582
623
  const dates = Array.isArray(date) ? date : [date];
583
624
  const sortedKey = [...dates].sort().join(',');
584
- const cacheKey = itemId ? `timesheet:${sortedKey}:${itemId}` : `timesheet:${sortedKey}`;
625
+ const { itemId, memberId } = options || {};
626
+ // Build cache key from all filter dimensions
627
+ let cacheKey = `timesheet:${sortedKey}`;
628
+ if (itemId)
629
+ cacheKey += `:item=${itemId}`;
630
+ if (memberId)
631
+ cacheKey += `:member=${memberId}`;
585
632
  return this.cached(cacheKey, this.cacheTtl.timesheetTtl, async () => {
586
633
  // Build query with date[in] filter (supports multiple dates)
587
634
  let baseUrl = this.workspaceUrl(`logged-time-entries/v1?${filterIn('date', dates)}`);
@@ -589,6 +636,10 @@ export class LPClient {
589
636
  if (itemId) {
590
637
  baseUrl += `&${filterIs('itemId', itemId)}`;
591
638
  }
639
+ // Optional filter by memberId (server-side user filtering)
640
+ if (memberId) {
641
+ baseUrl += `&${filterIs('userId', memberId)}`;
642
+ }
592
643
  return paginatedFetch({
593
644
  fetchFn: (url) => this.fetch(url),
594
645
  baseUrl,
@@ -679,7 +730,7 @@ export class LPClient {
679
730
  async upsertTimesheetEntry(entry, options = {}) {
680
731
  const { accumulate = true } = options;
681
732
  // Fetch existing entries for this date/item first
682
- const fetchResult = await this.getTimesheetEntries(entry.date, entry.itemId);
733
+ const fetchResult = await this.getTimesheetEntries(entry.date, { itemId: entry.itemId });
683
734
  if (!fetchResult.ok) {
684
735
  return { ok: false, error: fetchResult.error, status: fetchResult.status };
685
736
  }
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  */
31
31
  export { LPClient } from './client.js';
32
32
  export { resolveTaskToAssignment } from './workflows.js';
33
- export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, LPUserType, 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, LPItemRef, LPNormalizedAssignment, LPMemberAssignments, LPAssignmentsResult, 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';
package/dist/types.d.ts CHANGED
@@ -245,6 +245,15 @@ export interface LPTimesheetEntry {
245
245
  /** User ID who logged the time (present in API responses) */
246
246
  userId?: number;
247
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
+ }
248
257
  /**
249
258
  * Options for upsert timesheet entry operation
250
259
  */
@@ -274,6 +283,43 @@ export interface LPAssignment extends LPItem {
274
283
  /** Formatted hierarchy path like "Project A › Subfolder B" (undefined if not requested) */
275
284
  hierarchyPath?: string;
276
285
  }
286
+ /**
287
+ * Item reference in the shared items lookup.
288
+ * Extends LPAncestor with parentId for ancestor chain reconstruction.
289
+ */
290
+ export interface LPItemRef extends LPAncestor {
291
+ parentId?: number;
292
+ }
293
+ /**
294
+ * Assignment in the normalized result.
295
+ * Ancestors are reconstructable from the items lookup via parentId chain.
296
+ * Custom field values are referenced by index into the shared customFieldValues array.
297
+ */
298
+ export type LPNormalizedAssignment = Omit<LPAssignment, 'ancestors' | 'customFieldValues'> & {
299
+ /** Index into the top-level customFieldValues array. Undefined when no custom field values. */
300
+ customFieldValuesIndex?: number;
301
+ };
302
+ /**
303
+ * One member's assignments in the normalized result
304
+ */
305
+ export interface LPMemberAssignments {
306
+ memberId: number;
307
+ assignments: LPNormalizedAssignment[];
308
+ }
309
+ /**
310
+ * Normalized assignments result with shared lookup tables.
311
+ * Deduplicates ancestor items and custom field values to reduce payload size.
312
+ */
313
+ export interface LPAssignmentsResult {
314
+ /** Assignments grouped by member. One entry when memberId specified, all members when omitted. */
315
+ members: LPMemberAssignments[];
316
+ /** Shared items lookup — walk parentId chains to reconstruct ancestor paths */
317
+ items: Record<number, LPItemRef>;
318
+ /** Deduplicated custom field value sets — indexed by assignment's customFieldValuesIndex */
319
+ customFieldValues: Record<string, unknown>[];
320
+ /** Total items in the workspace tree snapshot */
321
+ treeItemCount: number;
322
+ }
277
323
  /**
278
324
  * Options for querying items with LP API filters
279
325
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",