@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 +9 -9
- package/dist/client.js +64 -13
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +46 -0
- package/package.json +1 -1
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,
|
|
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
|
|
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 -
|
|
179
|
+
* @param memberId - Filter to a specific member's assignments. When omitted, returns all members' assignments.
|
|
177
180
|
*/
|
|
178
|
-
getAssignments(memberId
|
|
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
|
|
205
|
+
* @param options - Optional filters (itemId, memberId)
|
|
206
206
|
*/
|
|
207
|
-
getTimesheetEntries(date: string | string[],
|
|
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
|
|
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 -
|
|
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
|
|
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'
|
|
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
|
-
|
|
498
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
620
|
+
* @param options - Optional filters (itemId, memberId)
|
|
580
621
|
*/
|
|
581
|
-
async getTimesheetEntries(date,
|
|
622
|
+
async getTimesheetEntries(date, options) {
|
|
582
623
|
const dates = Array.isArray(date) ? date : [date];
|
|
583
624
|
const sortedKey = [...dates].sort().join(',');
|
|
584
|
-
const
|
|
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
|
*
|