@markwharton/liquidplanner 1.5.0 → 1.6.1
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/cache.d.ts +32 -0
- package/dist/cache.js +50 -0
- package/dist/client.d.ts +24 -4
- package/dist/client.js +147 -89
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +25 -0
- package/package.json +1 -1
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory TTL cache
|
|
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
|
+
* Not a distributed cache: each instance has its own cache.
|
|
10
|
+
* Cold starts and instance recycling naturally clear stale data.
|
|
11
|
+
*/
|
|
12
|
+
export declare class TTLCache {
|
|
13
|
+
private store;
|
|
14
|
+
/**
|
|
15
|
+
* Get a cached value, or call the factory to populate it.
|
|
16
|
+
*
|
|
17
|
+
* @param key - Cache key
|
|
18
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
19
|
+
* @param factory - Async function to produce the value on cache miss
|
|
20
|
+
*/
|
|
21
|
+
get<T>(key: string, ttlMs: number, factory: () => Promise<T>): Promise<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Invalidate cache entries matching a key prefix.
|
|
24
|
+
*
|
|
25
|
+
* Example: invalidate('timesheet:') clears all timesheet entries.
|
|
26
|
+
*/
|
|
27
|
+
invalidate(prefix: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Clear all cached data.
|
|
30
|
+
*/
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory TTL cache
|
|
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
|
+
* Not a distributed cache: each instance has its own cache.
|
|
10
|
+
* Cold starts and instance recycling naturally clear stale data.
|
|
11
|
+
*/
|
|
12
|
+
export class TTLCache {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.store = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get a cached value, or call the factory to populate it.
|
|
18
|
+
*
|
|
19
|
+
* @param key - Cache key
|
|
20
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
21
|
+
* @param factory - Async function to produce the value on cache miss
|
|
22
|
+
*/
|
|
23
|
+
async get(key, ttlMs, factory) {
|
|
24
|
+
const existing = this.store.get(key);
|
|
25
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
26
|
+
return existing.data;
|
|
27
|
+
}
|
|
28
|
+
const data = await factory();
|
|
29
|
+
this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Invalidate cache entries matching a key prefix.
|
|
34
|
+
*
|
|
35
|
+
* Example: invalidate('timesheet:') clears all timesheet entries.
|
|
36
|
+
*/
|
|
37
|
+
invalidate(prefix) {
|
|
38
|
+
for (const key of this.store.keys()) {
|
|
39
|
+
if (key.startsWith(prefix)) {
|
|
40
|
+
this.store.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clear all cached data.
|
|
46
|
+
*/
|
|
47
|
+
clear() {
|
|
48
|
+
this.store.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -34,7 +34,24 @@ export declare class LPClient {
|
|
|
34
34
|
private readonly workspaceId;
|
|
35
35
|
private readonly baseUrl;
|
|
36
36
|
private readonly onRequest?;
|
|
37
|
+
private readonly cache?;
|
|
38
|
+
private readonly cacheTtl;
|
|
37
39
|
constructor(config: LPConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Route through cache if enabled, otherwise call factory directly.
|
|
42
|
+
*/
|
|
43
|
+
private cached;
|
|
44
|
+
/**
|
|
45
|
+
* Clear all cached API responses.
|
|
46
|
+
* Useful after external changes that the cache wouldn't know about.
|
|
47
|
+
*/
|
|
48
|
+
clearCache(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Invalidate cached timesheet entries only.
|
|
51
|
+
* Use when an external event (e.g., SignalR broadcast) indicates timesheet
|
|
52
|
+
* data changed but the write didn't go through this client instance.
|
|
53
|
+
*/
|
|
54
|
+
invalidateTimesheetCache(): void;
|
|
38
55
|
/**
|
|
39
56
|
* Make an authenticated request to the LP API
|
|
40
57
|
*/
|
|
@@ -146,17 +163,20 @@ export declare class LPClient {
|
|
|
146
163
|
*/
|
|
147
164
|
createTimesheetEntry(entry: LPTimesheetEntry): Promise<LPSyncResult>;
|
|
148
165
|
/**
|
|
149
|
-
* Get timesheet entries for
|
|
166
|
+
* Get timesheet entries for one or more dates
|
|
150
167
|
*
|
|
151
168
|
* Uses the Logged Time Entries API to query existing entries.
|
|
152
|
-
*
|
|
169
|
+
* Supports both single date and multi-date queries using LP's date[in] filter.
|
|
170
|
+
*
|
|
171
|
+
* Multi-date queries reduce API calls — e.g., fetching a full week's entries
|
|
172
|
+
* in a single request instead of 7 separate calls.
|
|
153
173
|
*
|
|
154
174
|
* @see https://api-docs.liquidplanner.com/docs/task-status-1
|
|
155
175
|
*
|
|
156
|
-
* @param date - Date in YYYY-MM-DD format
|
|
176
|
+
* @param date - Date(s) in YYYY-MM-DD format (string or array)
|
|
157
177
|
* @param itemId - Optional item ID to filter by
|
|
158
178
|
*/
|
|
159
|
-
getTimesheetEntries(date: string, itemId?: number): Promise<{
|
|
179
|
+
getTimesheetEntries(date: string | string[], itemId?: number): Promise<{
|
|
160
180
|
entries?: LPTimesheetEntryWithId[];
|
|
161
181
|
error?: LPErrorInfo;
|
|
162
182
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
10
10
|
import { parseLPErrorResponse, getErrorMessage } from './errors.js';
|
|
11
11
|
import { LP_API_BASE } from './constants.js';
|
|
12
|
+
import { TTLCache } from './cache.js';
|
|
12
13
|
/** Transform raw API item to LPItem */
|
|
13
14
|
function transformItem(raw) {
|
|
14
15
|
return {
|
|
@@ -48,6 +49,41 @@ export class LPClient {
|
|
|
48
49
|
this.workspaceId = config.workspaceId;
|
|
49
50
|
this.baseUrl = config.baseUrl ?? LP_API_BASE;
|
|
50
51
|
this.onRequest = config.onRequest;
|
|
52
|
+
// Initialize cache if configured
|
|
53
|
+
if (config.cache) {
|
|
54
|
+
this.cache = new TTLCache();
|
|
55
|
+
}
|
|
56
|
+
this.cacheTtl = {
|
|
57
|
+
membersTtl: config.cache?.membersTtl ?? 300000,
|
|
58
|
+
costCodesTtl: config.cache?.costCodesTtl ?? 300000,
|
|
59
|
+
timesheetTtl: config.cache?.timesheetTtl ?? 60000,
|
|
60
|
+
assignmentsTtl: config.cache?.assignmentsTtl ?? 120000,
|
|
61
|
+
itemsTtl: config.cache?.itemsTtl ?? 300000,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Route through cache if enabled, otherwise call factory directly.
|
|
66
|
+
*/
|
|
67
|
+
async cached(key, ttlMs, factory) {
|
|
68
|
+
if (this.cache) {
|
|
69
|
+
return this.cache.get(key, ttlMs, factory);
|
|
70
|
+
}
|
|
71
|
+
return factory();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear all cached API responses.
|
|
75
|
+
* Useful after external changes that the cache wouldn't know about.
|
|
76
|
+
*/
|
|
77
|
+
clearCache() {
|
|
78
|
+
this.cache?.clear();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Invalidate cached timesheet entries only.
|
|
82
|
+
* Use when an external event (e.g., SignalR broadcast) indicates timesheet
|
|
83
|
+
* data changed but the write didn't go through this client instance.
|
|
84
|
+
*/
|
|
85
|
+
invalidateTimesheetCache() {
|
|
86
|
+
this.cache?.invalidate('timesheet:');
|
|
51
87
|
}
|
|
52
88
|
/**
|
|
53
89
|
* Make an authenticated request to the LP API
|
|
@@ -117,20 +153,22 @@ export class LPClient {
|
|
|
117
153
|
* Get all members in the workspace (with pagination)
|
|
118
154
|
*/
|
|
119
155
|
async getWorkspaceMembers() {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
return this.cached('members', this.cacheTtl.membersTtl, async () => {
|
|
157
|
+
const baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/users/v1`;
|
|
158
|
+
const { results, error } = await paginatedFetch({
|
|
159
|
+
fetchFn: (url) => this.fetch(url),
|
|
160
|
+
baseUrl,
|
|
161
|
+
transform: (data) => data.map(m => ({
|
|
162
|
+
id: m.id,
|
|
163
|
+
username: m.username,
|
|
164
|
+
email: m.email,
|
|
165
|
+
firstName: m.firstName,
|
|
166
|
+
lastName: m.lastName,
|
|
167
|
+
userType: m.userType,
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
return error ? { error } : { members: results };
|
|
132
171
|
});
|
|
133
|
-
return error ? { error } : { members: results };
|
|
134
172
|
}
|
|
135
173
|
// ============================================================================
|
|
136
174
|
// Items
|
|
@@ -139,23 +177,25 @@ export class LPClient {
|
|
|
139
177
|
* Get a single item by ID
|
|
140
178
|
*/
|
|
141
179
|
async getItem(itemId) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
return this.cached(`item:${itemId}`, this.cacheTtl.itemsTtl, async () => {
|
|
181
|
+
const url = `${this.baseUrl}/workspaces/${this.workspaceId}/items/v1?${filterIs('id', itemId)}`;
|
|
182
|
+
try {
|
|
183
|
+
const response = await this.fetch(url);
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const errorText = await response.text();
|
|
186
|
+
const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
|
|
187
|
+
return { error: { message, statusCode: response.status, isDuplicate } };
|
|
188
|
+
}
|
|
189
|
+
const result = await response.json();
|
|
190
|
+
if (!result.data || result.data.length === 0) {
|
|
191
|
+
return { error: { message: `Item ${itemId} not found`, statusCode: 404 } };
|
|
192
|
+
}
|
|
193
|
+
return { item: transformItem(result.data[0]) };
|
|
149
194
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return { error: { message: `Item ${itemId} not found`, statusCode: 404 } };
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
153
197
|
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
158
|
-
}
|
|
198
|
+
});
|
|
159
199
|
}
|
|
160
200
|
/**
|
|
161
201
|
* Get multiple items by ID in a single request (batch fetch)
|
|
@@ -185,29 +225,31 @@ export class LPClient {
|
|
|
185
225
|
* @param itemId - The item ID to get ancestors for
|
|
186
226
|
*/
|
|
187
227
|
async getItemAncestors(itemId) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
return this.cached(`ancestors:${itemId}`, this.cacheTtl.itemsTtl, async () => {
|
|
229
|
+
const url = `${this.baseUrl}/workspaces/${this.workspaceId}/items/v1/${itemId}/ancestors`;
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.fetch(url, {
|
|
232
|
+
description: `Get ancestors for item ${itemId}`,
|
|
233
|
+
});
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const errorText = await response.text();
|
|
236
|
+
const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
|
|
237
|
+
return { error: { message, statusCode: response.status, isDuplicate } };
|
|
238
|
+
}
|
|
239
|
+
const json = (await response.json());
|
|
240
|
+
// Handle both { data: [...] } and direct array responses
|
|
241
|
+
const rawData = Array.isArray(json) ? json : (json.data || []);
|
|
242
|
+
const ancestors = rawData.map((a) => ({
|
|
243
|
+
id: a.id,
|
|
244
|
+
name: a.name || null,
|
|
245
|
+
itemType: normalizeItemType(a.itemType),
|
|
246
|
+
}));
|
|
247
|
+
return { ancestors };
|
|
197
248
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
id: a.id,
|
|
203
|
-
name: a.name || null,
|
|
204
|
-
itemType: normalizeItemType(a.itemType),
|
|
205
|
-
}));
|
|
206
|
-
return { ancestors };
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
210
|
-
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
251
|
+
}
|
|
252
|
+
});
|
|
211
253
|
}
|
|
212
254
|
/**
|
|
213
255
|
* Find all assignments under a task (with pagination)
|
|
@@ -229,14 +271,16 @@ export class LPClient {
|
|
|
229
271
|
* Note: userId is not a supported filter field in the LP API, so we filter client-side.
|
|
230
272
|
*/
|
|
231
273
|
async getMyAssignments(memberId) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
return this.cached(`assignments:${memberId}`, this.cacheTtl.assignmentsTtl, async () => {
|
|
275
|
+
const baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/items/v1?${filterIs('itemType', 'assignments')}`;
|
|
276
|
+
const { results, error } = await paginatedFetch({
|
|
277
|
+
fetchFn: (url) => this.fetch(url),
|
|
278
|
+
baseUrl,
|
|
279
|
+
filter: (data) => data.filter(item => item.userId === memberId),
|
|
280
|
+
transform: (data) => data.map(transformItem),
|
|
281
|
+
});
|
|
282
|
+
return error ? { error } : { assignments: results };
|
|
238
283
|
});
|
|
239
|
-
return error ? { error } : { assignments: results };
|
|
240
284
|
}
|
|
241
285
|
/**
|
|
242
286
|
* Get assignments for a member with parent task names resolved
|
|
@@ -365,17 +409,19 @@ export class LPClient {
|
|
|
365
409
|
* Get all cost codes in the workspace (with pagination)
|
|
366
410
|
*/
|
|
367
411
|
async getCostCodes() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
412
|
+
return this.cached('costcodes', this.cacheTtl.costCodesTtl, async () => {
|
|
413
|
+
const baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/cost-codes/v1`;
|
|
414
|
+
const { results, error } = await paginatedFetch({
|
|
415
|
+
fetchFn: (url) => this.fetch(url),
|
|
416
|
+
baseUrl,
|
|
417
|
+
transform: (data) => data.map(cc => ({
|
|
418
|
+
id: cc.id,
|
|
419
|
+
name: cc.name,
|
|
420
|
+
billable: cc.billable,
|
|
421
|
+
})),
|
|
422
|
+
});
|
|
423
|
+
return error ? { error } : { costCodes: results };
|
|
377
424
|
});
|
|
378
|
-
return error ? { error } : { costCodes: results };
|
|
379
425
|
}
|
|
380
426
|
// ============================================================================
|
|
381
427
|
// Timesheet
|
|
@@ -410,6 +456,8 @@ export class LPClient {
|
|
|
410
456
|
const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
|
|
411
457
|
return { success: false, error: message, statusCode: response.status, isDuplicate };
|
|
412
458
|
}
|
|
459
|
+
// Invalidate cached timesheet entries for this date
|
|
460
|
+
this.cache?.invalidate(`timesheet:`);
|
|
413
461
|
const result = await response.json();
|
|
414
462
|
return { success: true, entryId: result.id };
|
|
415
463
|
}
|
|
@@ -418,37 +466,45 @@ export class LPClient {
|
|
|
418
466
|
}
|
|
419
467
|
}
|
|
420
468
|
/**
|
|
421
|
-
* Get timesheet entries for
|
|
469
|
+
* Get timesheet entries for one or more dates
|
|
422
470
|
*
|
|
423
471
|
* Uses the Logged Time Entries API to query existing entries.
|
|
424
|
-
*
|
|
472
|
+
* Supports both single date and multi-date queries using LP's date[in] filter.
|
|
473
|
+
*
|
|
474
|
+
* Multi-date queries reduce API calls — e.g., fetching a full week's entries
|
|
475
|
+
* in a single request instead of 7 separate calls.
|
|
425
476
|
*
|
|
426
477
|
* @see https://api-docs.liquidplanner.com/docs/task-status-1
|
|
427
478
|
*
|
|
428
|
-
* @param date - Date in YYYY-MM-DD format
|
|
479
|
+
* @param date - Date(s) in YYYY-MM-DD format (string or array)
|
|
429
480
|
* @param itemId - Optional item ID to filter by
|
|
430
481
|
*/
|
|
431
482
|
async getTimesheetEntries(date, itemId) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
483
|
+
const dates = Array.isArray(date) ? date : [date];
|
|
484
|
+
const sortedKey = [...dates].sort().join(',');
|
|
485
|
+
const cacheKey = itemId ? `timesheet:${sortedKey}:${itemId}` : `timesheet:${sortedKey}`;
|
|
486
|
+
return this.cached(cacheKey, this.cacheTtl.timesheetTtl, async () => {
|
|
487
|
+
// Build query with date[in] filter (supports multiple dates)
|
|
488
|
+
let baseUrl = `${this.baseUrl}/workspaces/${this.workspaceId}/logged-time-entries/v1?${filterIn('date', dates)}`;
|
|
489
|
+
// Optional filter by itemId
|
|
490
|
+
if (itemId) {
|
|
491
|
+
baseUrl += `&${filterIs('itemId', itemId)}`;
|
|
492
|
+
}
|
|
493
|
+
const { results, error } = await paginatedFetch({
|
|
494
|
+
fetchFn: (url) => this.fetch(url),
|
|
495
|
+
baseUrl,
|
|
496
|
+
transform: (data) => data.map(entry => ({
|
|
497
|
+
id: entry.id,
|
|
498
|
+
date: entry.date,
|
|
499
|
+
itemId: entry.itemId,
|
|
500
|
+
hours: entry.loggedEntriesInMinutes / 60,
|
|
501
|
+
costCodeId: entry.costCodeId,
|
|
502
|
+
note: entry.note,
|
|
503
|
+
userId: entry.userId,
|
|
504
|
+
})),
|
|
505
|
+
});
|
|
506
|
+
return error ? { error } : { entries: results };
|
|
450
507
|
});
|
|
451
|
-
return error ? { error } : { entries: results };
|
|
452
508
|
}
|
|
453
509
|
/**
|
|
454
510
|
* Update an existing timesheet entry
|
|
@@ -490,6 +546,8 @@ export class LPClient {
|
|
|
490
546
|
const { message } = parseLPErrorResponse(errorText, response.status);
|
|
491
547
|
return { success: false, error: message, statusCode: response.status };
|
|
492
548
|
}
|
|
549
|
+
// Invalidate cached timesheet entries
|
|
550
|
+
this.cache?.invalidate(`timesheet:`);
|
|
493
551
|
return { success: true, entryId };
|
|
494
552
|
}
|
|
495
553
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export { LPClient } from './client.js';
|
|
30
30
|
export { resolveTaskToAssignment } from './workflows.js';
|
|
31
|
-
export type { LPConfig, LPItemType, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, LPUpsertOptions, LPAssignmentWithContext, LPErrorInfo, } from './types.js';
|
|
31
|
+
export type { LPConfig, LPCacheConfig, LPItemType, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, LPUpsertOptions, LPAssignmentWithContext, LPErrorInfo, } from './types.js';
|
|
32
32
|
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
33
33
|
export type { PaginateOptions } from './utils.js';
|
|
34
34
|
export { LP_API_BASE } from './constants.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -92,6 +92,29 @@ export interface LPTaskResolution {
|
|
|
92
92
|
/** Error message if resolution failed */
|
|
93
93
|
error?: string;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Cache configuration for LPClient
|
|
97
|
+
*
|
|
98
|
+
* When provided to LPConfig, enables in-memory TTL caching of API responses.
|
|
99
|
+
* All TTL values are in milliseconds. Omit individual TTLs to use defaults.
|
|
100
|
+
*
|
|
101
|
+
* Caching uses a simple in-memory Map that persists across warm invocations
|
|
102
|
+
* in serverless environments (Azure Functions, Static Web Apps). Each instance
|
|
103
|
+
* has its own independent cache — this is per-instance memoization, not a
|
|
104
|
+
* distributed cache. Cold starts and instance recycling naturally clear data.
|
|
105
|
+
*/
|
|
106
|
+
export interface LPCacheConfig {
|
|
107
|
+
/** TTL for workspace members (default: 300000 = 5 min) */
|
|
108
|
+
membersTtl?: number;
|
|
109
|
+
/** TTL for cost codes (default: 300000 = 5 min) */
|
|
110
|
+
costCodesTtl?: number;
|
|
111
|
+
/** TTL for timesheet entries (default: 60000 = 60s) */
|
|
112
|
+
timesheetTtl?: number;
|
|
113
|
+
/** TTL for user assignments (default: 120000 = 2 min) */
|
|
114
|
+
assignmentsTtl?: number;
|
|
115
|
+
/** TTL for items and ancestors (default: 300000 = 5 min) */
|
|
116
|
+
itemsTtl?: number;
|
|
117
|
+
}
|
|
95
118
|
/**
|
|
96
119
|
* LiquidPlanner configuration for API access
|
|
97
120
|
*/
|
|
@@ -108,6 +131,8 @@ export interface LPConfig {
|
|
|
108
131
|
url: string;
|
|
109
132
|
description?: string;
|
|
110
133
|
}) => void;
|
|
134
|
+
/** Enable caching with optional TTL overrides. Omit to disable caching. */
|
|
135
|
+
cache?: LPCacheConfig;
|
|
111
136
|
}
|
|
112
137
|
/**
|
|
113
138
|
* Result of a timesheet sync operation
|