@markwharton/liquidplanner 2.4.0 → 2.5.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/README.md +10 -23
- package/dist/client.d.ts +20 -21
- package/dist/client.js +30 -44
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/utils.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ await client.createTimesheetEntry({
|
|
|
44
44
|
|
|
45
45
|
// Query existing entries for a date
|
|
46
46
|
const tsResult = await client.getTimesheetEntries('2026-01-29', assignmentId);
|
|
47
|
-
if (tsResult.ok) console.log(tsResult.data); //
|
|
47
|
+
if (tsResult.ok) console.log(tsResult.data); // LPTimesheetEntry[]
|
|
48
48
|
|
|
49
49
|
// Update an existing entry (accumulate hours)
|
|
50
50
|
const existing = tsResult.data![0];
|
|
@@ -98,25 +98,7 @@ const lateTasks = findInTree(tree, item => item.late === true);
|
|
|
98
98
|
|
|
99
99
|
## Result Pattern
|
|
100
100
|
|
|
101
|
-
All methods return `Result<T>`
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
interface Result<T> {
|
|
105
|
-
ok: boolean;
|
|
106
|
-
data?: T; // present when ok is true
|
|
107
|
-
error?: string; // present when ok is false
|
|
108
|
-
status?: number; // HTTP status code on error
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
const result = await client.getAssignments(memberId);
|
|
114
|
-
if (!result.ok) {
|
|
115
|
-
console.error(result.error, result.status);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const { assignments, treeItemCount } = result.data;
|
|
119
|
-
```
|
|
101
|
+
All methods return `Result<T>` — see [api-core Result Pattern](../../README.md#result-pattern). Always check `ok` before accessing `data`.
|
|
120
102
|
|
|
121
103
|
## API Reference
|
|
122
104
|
|
|
@@ -133,11 +115,16 @@ const { assignments, treeItemCount } = result.data;
|
|
|
133
115
|
| `getChildren(parentId, options?)` | `number, { itemType? }?` | `Result<LPItem[]>` |
|
|
134
116
|
| `getWorkspaceTree()` | — | `Result<LPWorkspaceTree>` |
|
|
135
117
|
| `getAssignments(memberId)` | `number` | `Result<{ assignments: LPAssignment[], treeItemCount: number }>` |
|
|
118
|
+
| `clearCache()` | — | `void` |
|
|
119
|
+
| `invalidateTimesheetCache()` | — | `void` |
|
|
136
120
|
| `invalidateTreeCache()` | — | `void` |
|
|
121
|
+
| `invalidateMemberCache()` | — | `void` |
|
|
122
|
+
| `invalidateItemCache()` | — | `void` |
|
|
123
|
+
| `invalidateCostCodeCache()` | — | `void` |
|
|
137
124
|
| `getCostCodes()` | — | `Result<LPCostCode[]>` |
|
|
138
125
|
| `createTimesheetEntry(entry)` | `LPTimesheetEntry` | `LPSyncResult` |
|
|
139
|
-
| `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `Result<
|
|
140
|
-
| `updateTimesheetEntry(entryId, existing, updates)` | `number,
|
|
126
|
+
| `getTimesheetEntries(date, itemId?)` | `string \| string[], number?` | `Result<LPTimesheetEntry[]>` |
|
|
127
|
+
| `updateTimesheetEntry(entryId, existing, updates)` | `number, LPTimesheetEntry, Partial<LPTimesheetEntry>` | `LPSyncResult` |
|
|
141
128
|
| `upsertTimesheetEntry(entry, options?)` | `LPTimesheetEntry, LPUpsertOptions?` | `LPSyncResult` |
|
|
142
129
|
|
|
143
130
|
### Workflow: `resolveTaskToAssignment`
|
|
@@ -203,7 +190,7 @@ const client = new LPClient({
|
|
|
203
190
|
| Items / ancestors | 5 min |
|
|
204
191
|
| Timesheet entries | 60s |
|
|
205
192
|
|
|
206
|
-
Write operations (`createTimesheetEntry`, `updateTimesheetEntry`) automatically invalidate timesheet cache entries.
|
|
193
|
+
Write operations (`createTimesheetEntry`, `updateTimesheetEntry`) automatically invalidate timesheet cache entries. Use focused invalidation methods (`invalidateTreeCache`, `invalidateMemberCache`, `invalidateItemCache`, `invalidateTimesheetCache`, `invalidateCostCodeCache`) to refresh specific data, or `clearCache()` to clear everything.
|
|
207
194
|
|
|
208
195
|
Failed API results (`ok: false`) are never cached — transient errors won't persist for the full TTL. See the [root README Cache System section](../../README.md#cache-system) for the full cache architecture (layered stores, restricted data handling, request coalescing).
|
|
209
196
|
|
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, LPUpsertOptions, LPAssignment, LPAncestor, LPFindItemsOptions, LPWorkspaceTree } from './types.js';
|
|
11
11
|
/**
|
|
12
12
|
* LiquidPlanner API Client
|
|
13
13
|
*
|
|
@@ -39,11 +39,7 @@ export declare class LPClient {
|
|
|
39
39
|
private readonly cacheTtl;
|
|
40
40
|
private readonly retryConfig?;
|
|
41
41
|
constructor(config: LPConfig);
|
|
42
|
-
/**
|
|
43
|
-
* Route through cache if enabled, otherwise call factory directly.
|
|
44
|
-
* Failed Results (ok === false) are never cached — transient errors
|
|
45
|
-
* shouldn't persist for the full TTL.
|
|
46
|
-
*/
|
|
42
|
+
/** Route through cache if enabled, skipping failed Results. */
|
|
47
43
|
private cached;
|
|
48
44
|
/**
|
|
49
45
|
* Clear all cached API responses.
|
|
@@ -52,21 +48,24 @@ export declare class LPClient {
|
|
|
52
48
|
clearCache(): void;
|
|
53
49
|
/**
|
|
54
50
|
* Invalidate cached timesheet entries only.
|
|
55
|
-
* Use when an external event (e.g., SignalR broadcast) indicates timesheet
|
|
56
|
-
* data changed but the write didn't go through this client instance.
|
|
57
51
|
*/
|
|
58
52
|
invalidateTimesheetCache(): void;
|
|
59
|
-
/**
|
|
60
|
-
* Invalidate cached assignments only.
|
|
61
|
-
* Use when an external event indicates assignment data changed
|
|
62
|
-
* (e.g., after logging time which updates loggedHoursRollup).
|
|
63
|
-
*/
|
|
64
|
-
invalidateAssignmentsCache(): void;
|
|
65
53
|
/**
|
|
66
54
|
* Invalidate cached workspace tree snapshot only.
|
|
67
|
-
* Use when the workspace structure changes (items created, moved, or deleted).
|
|
68
55
|
*/
|
|
69
56
|
invalidateTreeCache(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Invalidate cached workspace members only.
|
|
59
|
+
*/
|
|
60
|
+
invalidateMemberCache(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Invalidate cached items and ancestors only.
|
|
63
|
+
*/
|
|
64
|
+
invalidateItemCache(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Invalidate cached cost codes only.
|
|
67
|
+
*/
|
|
68
|
+
invalidateCostCodeCache(): void;
|
|
70
69
|
/**
|
|
71
70
|
* Make an authenticated request to the LP API
|
|
72
71
|
*
|
|
@@ -88,7 +87,7 @@ export declare class LPClient {
|
|
|
88
87
|
*/
|
|
89
88
|
private fetchAndParseMutation;
|
|
90
89
|
/**
|
|
91
|
-
* Validate the API token
|
|
90
|
+
* Validate the API token
|
|
92
91
|
*/
|
|
93
92
|
validateToken(): Promise<Result<void>>;
|
|
94
93
|
/**
|
|
@@ -96,7 +95,7 @@ export declare class LPClient {
|
|
|
96
95
|
*/
|
|
97
96
|
getWorkspaces(): Promise<Result<LPWorkspace[]>>;
|
|
98
97
|
/**
|
|
99
|
-
* Get all members in the workspace
|
|
98
|
+
* Get all members in the workspace
|
|
100
99
|
*/
|
|
101
100
|
getWorkspaceMembers(): Promise<Result<LPMember[]>>;
|
|
102
101
|
/**
|
|
@@ -122,7 +121,7 @@ export declare class LPClient {
|
|
|
122
121
|
*/
|
|
123
122
|
getItemAncestors(itemId: number): Promise<Result<LPAncestor[]>>;
|
|
124
123
|
/**
|
|
125
|
-
* Find all assignments under a task
|
|
124
|
+
* Find all assignments under a task
|
|
126
125
|
*/
|
|
127
126
|
findAssignments(taskId: number): Promise<Result<LPItem[]>>;
|
|
128
127
|
/**
|
|
@@ -177,7 +176,7 @@ export declare class LPClient {
|
|
|
177
176
|
treeItemCount: number;
|
|
178
177
|
}>>;
|
|
179
178
|
/**
|
|
180
|
-
* Get all cost codes in the workspace
|
|
179
|
+
* Get all cost codes in the workspace
|
|
181
180
|
*/
|
|
182
181
|
getCostCodes(): Promise<Result<LPCostCode[]>>;
|
|
183
182
|
/**
|
|
@@ -201,7 +200,7 @@ export declare class LPClient {
|
|
|
201
200
|
* @param date - Date(s) in YYYY-MM-DD format (string or array)
|
|
202
201
|
* @param itemId - Optional item ID to filter by
|
|
203
202
|
*/
|
|
204
|
-
getTimesheetEntries(date: string | string[], itemId?: number): Promise<Result<
|
|
203
|
+
getTimesheetEntries(date: string | string[], itemId?: number): Promise<Result<LPTimesheetEntry[]>>;
|
|
205
204
|
/**
|
|
206
205
|
* Update an existing timesheet entry
|
|
207
206
|
*
|
|
@@ -221,7 +220,7 @@ export declare class LPClient {
|
|
|
221
220
|
* @param existingEntry - The existing entry (needed because PUT requires all fields)
|
|
222
221
|
* @param updates - Fields to update (merged with existing)
|
|
223
222
|
*/
|
|
224
|
-
updateTimesheetEntry(entryId: number, existingEntry:
|
|
223
|
+
updateTimesheetEntry(entryId: number, existingEntry: LPTimesheetEntry, updates: Partial<LPTimesheetEntry>): Promise<LPSyncResult>;
|
|
225
224
|
/**
|
|
226
225
|
* Create or update a timesheet entry (upsert)
|
|
227
226
|
*
|
package/dist/client.js
CHANGED
|
@@ -10,7 +10,7 @@ import { buildAuthHeader, hoursToMinutes, normalizeItemType, filterIs, filterIsN
|
|
|
10
10
|
import { buildTree, getTreeAncestors } from './tree.js';
|
|
11
11
|
import { parseLPErrorResponse } from './errors.js';
|
|
12
12
|
import { LP_API_BASE } from './constants.js';
|
|
13
|
-
import {
|
|
13
|
+
import { getErrorMessage, fetchWithRetry, ok, okVoid, err, resolveRetryConfig, cachedResult, fetchAndParseResponse, resolveClientCache } from '@markwharton/api-core';
|
|
14
14
|
/** Transform raw API item to LPItem, preserving scheduling and effort fields */
|
|
15
15
|
function transformItem(raw) {
|
|
16
16
|
const item = {
|
|
@@ -114,9 +114,7 @@ export class LPClient {
|
|
|
114
114
|
this.baseUrl = config.baseUrl ?? LP_API_BASE;
|
|
115
115
|
this.onRequest = config.onRequest;
|
|
116
116
|
// Initialize cache if configured
|
|
117
|
-
|
|
118
|
-
this.cache = config.cacheInstance ?? new TTLCache();
|
|
119
|
-
}
|
|
117
|
+
this.cache = resolveClientCache(config);
|
|
120
118
|
this.cacheTtl = {
|
|
121
119
|
membersTtl: config.cache?.membersTtl ?? 300000,
|
|
122
120
|
costCodesTtl: config.cache?.costCodesTtl ?? 300000,
|
|
@@ -127,18 +125,9 @@ export class LPClient {
|
|
|
127
125
|
// Initialize retry config with defaults if provided
|
|
128
126
|
this.retryConfig = resolveRetryConfig(config.retry);
|
|
129
127
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* shouldn't persist for the full TTL.
|
|
134
|
-
*/
|
|
135
|
-
async cached(key, ttlMs, factory, options) {
|
|
136
|
-
if (!this.cache)
|
|
137
|
-
return factory();
|
|
138
|
-
return this.cache.get(key, ttlMs, factory, {
|
|
139
|
-
...options,
|
|
140
|
-
shouldCache: (data) => data.ok !== false,
|
|
141
|
-
});
|
|
128
|
+
/** Route through cache if enabled, skipping failed Results. */
|
|
129
|
+
cached(key, ttlMs, factory, options) {
|
|
130
|
+
return cachedResult(this.cache, key, ttlMs, factory, options);
|
|
142
131
|
}
|
|
143
132
|
/**
|
|
144
133
|
* Clear all cached API responses.
|
|
@@ -149,27 +138,35 @@ export class LPClient {
|
|
|
149
138
|
}
|
|
150
139
|
/**
|
|
151
140
|
* Invalidate cached timesheet entries only.
|
|
152
|
-
* Use when an external event (e.g., SignalR broadcast) indicates timesheet
|
|
153
|
-
* data changed but the write didn't go through this client instance.
|
|
154
141
|
*/
|
|
155
142
|
invalidateTimesheetCache() {
|
|
156
143
|
this.cache?.invalidate('timesheet:');
|
|
157
144
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Invalidate cached assignments only.
|
|
160
|
-
* Use when an external event indicates assignment data changed
|
|
161
|
-
* (e.g., after logging time which updates loggedHoursRollup).
|
|
162
|
-
*/
|
|
163
|
-
invalidateAssignmentsCache() {
|
|
164
|
-
this.cache?.invalidate('assignments');
|
|
165
|
-
}
|
|
166
145
|
/**
|
|
167
146
|
* Invalidate cached workspace tree snapshot only.
|
|
168
|
-
* Use when the workspace structure changes (items created, moved, or deleted).
|
|
169
147
|
*/
|
|
170
148
|
invalidateTreeCache() {
|
|
171
149
|
this.cache?.invalidate('tree');
|
|
172
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Invalidate cached workspace members only.
|
|
153
|
+
*/
|
|
154
|
+
invalidateMemberCache() {
|
|
155
|
+
this.cache?.invalidate('members');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Invalidate cached items and ancestors only.
|
|
159
|
+
*/
|
|
160
|
+
invalidateItemCache() {
|
|
161
|
+
this.cache?.invalidate('item:');
|
|
162
|
+
this.cache?.invalidate('ancestors:');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Invalidate cached cost codes only.
|
|
166
|
+
*/
|
|
167
|
+
invalidateCostCodeCache() {
|
|
168
|
+
this.cache?.invalidate('costcodes');
|
|
169
|
+
}
|
|
173
170
|
/**
|
|
174
171
|
* Make an authenticated request to the LP API
|
|
175
172
|
*
|
|
@@ -208,19 +205,8 @@ export class LPClient {
|
|
|
208
205
|
/**
|
|
209
206
|
* Fetch a URL and parse the response, with standardized error handling.
|
|
210
207
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const response = await this.fetch(url, fetchOptions);
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
const errorText = await response.text();
|
|
216
|
-
const { message, isDuplicate } = parseLPErrorResponse(errorText, response.status);
|
|
217
|
-
return { ok: false, error: message, status: response.status, ...(isDuplicate ? { isDuplicate } : {}) };
|
|
218
|
-
}
|
|
219
|
-
return ok(await parse(response));
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
return err(getErrorMessage(error), 0);
|
|
223
|
-
}
|
|
208
|
+
fetchAndParse(url, parse, fetchOptions) {
|
|
209
|
+
return fetchAndParseResponse(() => this.fetch(url, fetchOptions), parse, parseLPErrorResponse);
|
|
224
210
|
}
|
|
225
211
|
/**
|
|
226
212
|
* Fetch and parse with isDuplicate support for mutation methods.
|
|
@@ -244,7 +230,7 @@ export class LPClient {
|
|
|
244
230
|
// Workspace & Validation
|
|
245
231
|
// ============================================================================
|
|
246
232
|
/**
|
|
247
|
-
* Validate the API token
|
|
233
|
+
* Validate the API token
|
|
248
234
|
*/
|
|
249
235
|
async validateToken() {
|
|
250
236
|
const url = `${this.baseUrl}/workspaces/v1`;
|
|
@@ -276,7 +262,7 @@ export class LPClient {
|
|
|
276
262
|
// Members
|
|
277
263
|
// ============================================================================
|
|
278
264
|
/**
|
|
279
|
-
* Get all members in the workspace
|
|
265
|
+
* Get all members in the workspace
|
|
280
266
|
*/
|
|
281
267
|
async getWorkspaceMembers() {
|
|
282
268
|
return this.cached('members', this.cacheTtl.membersTtl, async () => {
|
|
@@ -359,7 +345,7 @@ export class LPClient {
|
|
|
359
345
|
});
|
|
360
346
|
}
|
|
361
347
|
/**
|
|
362
|
-
* Find all assignments under a task
|
|
348
|
+
* Find all assignments under a task
|
|
363
349
|
*/
|
|
364
350
|
async findAssignments(taskId) {
|
|
365
351
|
// parentId[is]="{taskId}"&itemType[is]="assignments" (LP API uses lowercase plural)
|
|
@@ -523,7 +509,7 @@ export class LPClient {
|
|
|
523
509
|
// Cost Codes
|
|
524
510
|
// ============================================================================
|
|
525
511
|
/**
|
|
526
|
-
* Get all cost codes in the workspace
|
|
512
|
+
* Get all cost codes in the workspace
|
|
527
513
|
*/
|
|
528
514
|
async getCostCodes() {
|
|
529
515
|
return this.cached('costcodes', this.cacheTtl.costCodesTtl, async () => {
|
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,
|
|
33
|
+
export type { LPConfig, LPCacheConfig, LPRetryConfig, LPItemType, LPHierarchyItem, LPItem, LPAncestor, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTaskResolution, LPUpsertOptions, LPAssignment, LPFindItemsOptions, LPTreeNode, LPWorkspaceTree, } from './types.js';
|
|
34
34
|
export type { AccessTier } from './types.js';
|
|
35
35
|
export { METHOD_TIERS } from './types.js';
|
|
36
36
|
export { ok, err, getErrorMessage, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
|
package/dist/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type LPItemType = 'Task' | 'Assignment' | 'Folder' | 'Project' | 'Package
|
|
|
15
15
|
* Used by consumers to store structured hierarchy data from any backend.
|
|
16
16
|
* LP-specific ancestors use the stricter LPAncestor type.
|
|
17
17
|
*/
|
|
18
|
-
export interface
|
|
18
|
+
export interface LPHierarchyItem {
|
|
19
19
|
/** Unique identifier (number for LP, string UUID for other backends) */
|
|
20
20
|
id: number | string;
|
|
21
21
|
/** Display name */
|
|
@@ -217,9 +217,11 @@ export interface LPSyncResult extends Result<number> {
|
|
|
217
217
|
isDuplicate?: boolean;
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
220
|
-
*
|
|
220
|
+
* A timesheet entry (input for create/update, or response from queries)
|
|
221
221
|
*/
|
|
222
222
|
export interface LPTimesheetEntry {
|
|
223
|
+
/** Unique identifier (present in API responses) */
|
|
224
|
+
id?: number;
|
|
223
225
|
/** Date in YYYY-MM-DD format */
|
|
224
226
|
date: string;
|
|
225
227
|
/** Item ID (should be an Assignment ID) */
|
|
@@ -230,14 +232,7 @@ export interface LPTimesheetEntry {
|
|
|
230
232
|
costCodeId?: number;
|
|
231
233
|
/** Optional note/description */
|
|
232
234
|
note?: string;
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* A timesheet entry returned from LP API queries (includes ID)
|
|
236
|
-
*/
|
|
237
|
-
export interface LPTimesheetEntryWithId extends LPTimesheetEntry {
|
|
238
|
-
/** Unique identifier for the entry */
|
|
239
|
-
id: number;
|
|
240
|
-
/** User ID who logged the time */
|
|
235
|
+
/** User ID who logged the time (present in API responses) */
|
|
241
236
|
userId?: number;
|
|
242
237
|
}
|
|
243
238
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -6,36 +6,40 @@ import { getErrorMessage, err } from '@markwharton/api-core';
|
|
|
6
6
|
// ============================================================================
|
|
7
7
|
// LP API Filter Builders
|
|
8
8
|
// ============================================================================
|
|
9
|
+
/** Encode a value for embedding in a pre-encoded LP filter string: %22<encoded-value>%22 */
|
|
10
|
+
function encodeFilterValue(value) {
|
|
11
|
+
return `%22${String(value).replace(/\+/g, '%2B')}%22`;
|
|
12
|
+
}
|
|
9
13
|
/**
|
|
10
14
|
* Build a URL-encoded filter for LP API: field[is]="value"
|
|
11
15
|
*/
|
|
12
16
|
export function filterIs(field, value) {
|
|
13
|
-
return `${field}%5Bis%5D
|
|
17
|
+
return `${field}%5Bis%5D=${encodeFilterValue(value)}`;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Build a URL-encoded filter for LP API: field[is_not]="value"
|
|
17
21
|
*/
|
|
18
22
|
export function filterIsNot(field, value) {
|
|
19
|
-
return `${field}%5Bis_not%5D
|
|
23
|
+
return `${field}%5Bis_not%5D=${encodeFilterValue(value)}`;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
26
|
* Build a URL-encoded filter for LP API: field[in]=["value1","value2"]
|
|
23
27
|
*/
|
|
24
28
|
export function filterIn(field, values) {
|
|
25
|
-
const encoded = values.map(v =>
|
|
29
|
+
const encoded = values.map(v => encodeFilterValue(v)).join(',');
|
|
26
30
|
return `${field}%5Bin%5D=%5B${encoded}%5D`;
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* Build a URL-encoded filter for LP API: field[gt]="value"
|
|
30
34
|
*/
|
|
31
35
|
export function filterGt(field, value) {
|
|
32
|
-
return `${field}%5Bgt%5D
|
|
36
|
+
return `${field}%5Bgt%5D=${encodeFilterValue(value)}`;
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
35
39
|
* Build a URL-encoded filter for LP API: field[lt]="value"
|
|
36
40
|
*/
|
|
37
41
|
export function filterLt(field, value) {
|
|
38
|
-
return `${field}%5Blt%5D
|
|
42
|
+
return `${field}%5Blt%5D=${encodeFilterValue(value)}`;
|
|
39
43
|
}
|
|
40
44
|
/** Normalize YYYY-MM-DD to ISO offset string using local timezone (LP API requires ISO for date filters) */
|
|
41
45
|
function toISODateValue(value) {
|
|
@@ -54,7 +58,7 @@ function toISODateValue(value) {
|
|
|
54
58
|
* Accepts YYYY-MM-DD or full ISO strings.
|
|
55
59
|
*/
|
|
56
60
|
export function filterAfter(field, value) {
|
|
57
|
-
return `${field}%5Bafter%5D
|
|
61
|
+
return `${field}%5Bafter%5D=${encodeFilterValue(toISODateValue(value))}`;
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* Build a URL-encoded filter for LP API: field[before]="value"
|
|
@@ -62,7 +66,7 @@ export function filterAfter(field, value) {
|
|
|
62
66
|
* Accepts YYYY-MM-DD or full ISO strings.
|
|
63
67
|
*/
|
|
64
68
|
export function filterBefore(field, value) {
|
|
65
|
-
return `${field}%5Bbefore%5D
|
|
69
|
+
return `${field}%5Bbefore%5D=${encodeFilterValue(toISODateValue(value))}`;
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* Join multiple filter expressions with &
|