@startsimpli/integrations 0.2.9 → 0.2.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/integrations",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -367,6 +367,29 @@ describe('listCommitments speaks the tenant data API’s own filter language', (
367
367
  expect(page.appliedFilters).not.toContain('owner');
368
368
  });
369
369
 
370
+ it('believes the server through the CAMELCASE client too', async () => {
371
+ // `@startsimpli/api` converts every response key snake_case -> camelCase, so
372
+ // in a browser the tenant's `applied_filters` arrives as `appliedFilters`.
373
+ // Reading only one spelling put "this workspace's data API did not filter by
374
+ // status" on a deployed screen while the server was demonstrably filtering
375
+ // by status. Same lesson as accepting `document_id` in a citation.
376
+ const seen: Array<Record<string, unknown>> = [];
377
+ const api = createCorpusApi({
378
+ http: {
379
+ async get<T>(_path: string, options?: { params?: Record<string, unknown> }): Promise<T> {
380
+ seen.push(options?.params ?? {});
381
+ return { count: 0, next: null, previous: null, results: [],
382
+ appliedFilters: ['attr.status__in'], ignoredFilters: [] } as T;
383
+ },
384
+ },
385
+ scope: { orgId: 'tandem' },
386
+ supportedFilters: [],
387
+ });
388
+ const page = await api.listCommitments({ status: ['open', 'blocked'] });
389
+ expect(page.appliedFilters).toEqual(['status']);
390
+ expect(seen[0]['attr.status__in']).toBe('open,blocked');
391
+ });
392
+
370
393
  it('falls back to the declared list on a server that does not report', async () => {
371
394
  const { api } = client(undefined);
372
395
  const page = await api.listCommitments({ status: 'open' });
@@ -125,7 +125,22 @@ interface DrfPage<T> {
125
125
  */
126
126
  interface ServerFilterReport {
127
127
  applied_filters?: string[];
128
+ /**
129
+ * BOTH SPELLINGS, and this is not defensive padding — it is the difference
130
+ * between the banner telling the truth and telling a lie.
131
+ *
132
+ * `@startsimpli/api`'s fetch wrapper converts every response key from
133
+ * snake_case to camelCase by default (`transformKeys`), so the tenant's
134
+ * `applied_filters` reaches this adapter as `appliedFilters` in the browser
135
+ * and as `applied_filters` from a plain server-side `fetch`. Reading only one
136
+ * made the deployed Tracker report "this workspace's data API did not filter
137
+ * by status" while the server was, verifiably, filtering by status. Same
138
+ * lesson as `resolveCitations` accepting `document_id`: the tolerance belongs
139
+ * at the seam.
140
+ */
141
+ appliedFilters?: string[];
128
142
  ignored_filters?: string[];
143
+ ignoredFilters?: string[];
129
144
  search_backend?: string;
130
145
  }
131
146
 
@@ -170,6 +185,22 @@ const FILTER_PARAMS: Array<[keyof CommitmentQuery, string[]]> = [
170
185
  ['search', ['search']],
171
186
  ];
172
187
 
188
+ /** Which server parameter carries which `DocumentQuery` key. */
189
+ const DOCUMENT_FILTER_PARAMS: Array<[keyof DocumentQuery, string[]]> = [
190
+ ['search', ['search']],
191
+ ['source', ['source', 'attr.source']],
192
+ ['partyId', ['party', 'related_to']],
193
+ ['occurredAfter', ['occurred_after', 'attr.occurred_at__gte']],
194
+ ['occurredBefore', ['occurred_before', 'attr.occurred_at__lte']],
195
+ ];
196
+
197
+ function reportedDocumentFilters(query: DocumentQuery, applied: string[]): string[] {
198
+ const server = new Set(applied);
199
+ return DOCUMENT_FILTER_PARAMS.filter(
200
+ ([key, names]) => Boolean(query[key]) && names.some((name) => server.has(name)),
201
+ ).map(([key]) => key);
202
+ }
203
+
173
204
  /** Translate the server's report back into the composer's vocabulary. */
174
205
  function reportedFilters(query: CommitmentQuery, applied: string[]): string[] {
175
206
  const server = new Set(applied);
@@ -227,15 +258,20 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
227
258
  query: DocumentQuery,
228
259
  map: (entity: TenantEntity) => T,
229
260
  ): Promise<CorpusPage<T>> {
230
- const response = await http.get<DrfPage<TenantEntity>>(entities, {
261
+ const response = await http.get<DrfPage<TenantEntity> & ServerFilterReport>(entities, {
231
262
  params: params(query, type),
232
263
  });
264
+ // The SERVER's own report wins here too — the Library was reading a
265
+ // hardcoded list while the tenant had learned to filter.
266
+ const reported = response.applied_filters ?? response.appliedFilters;
233
267
  return {
234
268
  count: response.count ?? response.results?.length ?? 0,
235
269
  next: response.next ?? null,
236
270
  previous: response.previous ?? null,
237
271
  results: (response.results ?? []).map(map),
238
- appliedFilters: appliedFilters(query, supportedFilters),
272
+ appliedFilters: reported
273
+ ? reportedDocumentFilters(query, reported)
274
+ : appliedFilters(query, supportedFilters as string[]),
239
275
  };
240
276
  }
241
277
 
@@ -344,6 +380,7 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
344
380
  async listCommitments(query: CommitmentQuery = {}): Promise<CorpusPage<Commitment>> {
345
381
  const params = commitmentParams(query, types.commitment);
346
382
  const response = await http.get<DrfPage<TenantEntity> & ServerFilterReport>(entities, { params });
383
+ const reported = response.applied_filters ?? response.appliedFilters;
347
384
  return {
348
385
  count: response.count ?? response.results?.length ?? 0,
349
386
  next: response.next ?? null,
@@ -355,8 +392,8 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
355
392
  // do — and cannot claim a gap that has since been closed. The static
356
393
  // `supportedFilters` list is only the fallback for a deployment old
357
394
  // enough not to report.
358
- appliedFilters: response.applied_filters
359
- ? reportedFilters(query, response.applied_filters)
395
+ appliedFilters: reported
396
+ ? reportedFilters(query, reported)
360
397
  : appliedFilters(query as DocumentQuery, supportedFilters as string[]),
361
398
  };
362
399
  },