@markwharton/liquidplanner 3.1.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, LPTimesheetOptions, 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
  */
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
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, LPTimesheetOptions, 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
@@ -283,6 +283,43 @@ export interface LPAssignment extends LPItem {
283
283
  /** Formatted hierarchy path like "Project A › Subfolder B" (undefined if not requested) */
284
284
  hierarchyPath?: string;
285
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
+ }
286
323
  /**
287
324
  * Options for querying items with LP API filters
288
325
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "3.1.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",