@startsimpli/integrations 0.2.31 → 0.2.33

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.
@@ -42,6 +42,7 @@ import {
42
42
  toParty,
43
43
  toSourceDocument,
44
44
  } from './entities';
45
+ import { MAX_ID_LIST, RELATIONSHIP_FILTERS, RESERVED_PARAMS } from './entities';
45
46
  import type { TenantEntity, TenantRelationship } from './entities';
46
47
  import { splitSections, stem } from './text';
47
48
  import type {
@@ -869,11 +870,28 @@ export function createFixtureCorpusApi(options: FixtureCorpusOptions = {}): Corp
869
870
  * search since this was written (bd startsim-768w.38.12) and a retrieval
870
871
  * test must be written against the real semantics, but the suites that
871
872
  * predate it are still testing what they were written to test. `source` and
872
- * `party` are still accepted and ignored, exactly as `EntityViewSet` does;
873
- * • `page_size` is IGNORED (`PageNumberPagination` with no
874
- * `page_size_query_param`), so a caller asking for 200 gets `pageSize`;
875
- * • `/api/v1/relationships/` returns every edge in the org, unfiltered,
876
- * because the tenant has no `?source=` on it either.
873
+ * `party` are accepted and ignored HERE, and this fixture no longer claims
874
+ * that is what `EntityViewSet` does: 768w.38.12 shipped both, along with the
875
+ * `applied_filters` report that names what the server narrowed by. So this
876
+ * stands in for an OLDER fork — which is a useful thing to have a stand-in
877
+ * for, since that is the fork `listSources` refuses to print a count for
878
+ * (bd startsim-jevtu), but it is a lagging stub and not the contract;
879
+ * • `page_size` IS honoured, capped at 200. It was written here as ignored,
880
+ * citing a `PageNumberPagination` with no `page_size_query_param`, and that
881
+ * stopped being true with `TenantPageNumberPagination` (bd
882
+ * startsim-768w.38.12): "a client asking for one row to read a count got
883
+ * fifty, and a client asking for two hundred to walk a graph got fifty and
884
+ * no indication that its request had been overruled". A fixture that keeps
885
+ * ignoring it manufactures evidence for a cap the wire does not impose;
886
+ * • `/api/v1/relationships/` FILTERS — `rel_type`, `source`, `target`,
887
+ * `entity` and the `__in` forms of the last three, bounded at
888
+ * `MAX_ID_LIST` ids and reported in `applied_filters`. This too was written
889
+ * here as "every edge in the org, unfiltered, because the tenant has no
890
+ * `?source=` on it either", and bd startsim-te3r shipped `RelationshipQuery`
891
+ * for exactly the read that comment was excusing. An UNRECOGNISED parameter
892
+ * is still ignored and the page comes back as wide as it was — that part is
893
+ * real, it is what makes an unconfirmed filter dangerous, and it is what
894
+ * `applied_filters` exists to let a client detect.
877
895
  *
878
896
  * Every request is recorded, so "did the browser filter it" and "how many round
879
897
  * trips did one screen cost" are both assertable.
@@ -881,7 +899,7 @@ export function createFixtureCorpusApi(options: FixtureCorpusOptions = {}): Corp
881
899
  export function createFixtureCorpusHttp(options: {
882
900
  entities?: TenantEntity[];
883
901
  relationships?: TenantRelationship[];
884
- /** Rows per page, matching the tenant's own `PAGE_SIZE`. */
902
+ /** Rows per page when the caller does not ask, matching the tenant's `PAGE_SIZE`. */
885
903
  pageSize?: number;
886
904
  /** Endpoints to fail, so the fail-soft paths are exercised. */
887
905
  missing?: string[];
@@ -901,15 +919,90 @@ export function createFixtureCorpusHttp(options: {
901
919
 
902
920
  function slice<T>(rows: T[], params: Record<string, unknown>) {
903
921
  const page = Number(params.page ?? 1) || 1;
904
- const start = (page - 1) * pageSize;
922
+ // `TenantPageNumberPagination.page_size_query_param` is `page_size` and its
923
+ // `max_page_size` is 200 — an OVERRULED request is served at the cap, not
924
+ // refused, which is the behaviour a client that walks a page has to survive.
925
+ const asked = Number(params.page_size);
926
+ const size = Number.isFinite(asked) && asked > 0 ? Math.min(asked, MAX_ID_LIST) : pageSize;
927
+ const start = (page - 1) * size;
905
928
  return {
906
929
  count: rows.length,
907
- next: start + pageSize < rows.length ? `?page=${page + 1}` : null,
930
+ next: start + size < rows.length ? `?page=${page + 1}` : null,
908
931
  previous: page > 1 ? `?page=${page - 1}` : null,
909
- results: rows.slice(start, start + pageSize),
932
+ results: rows.slice(start, start + size),
910
933
  };
911
934
  }
912
935
 
936
+ /**
937
+ * `/api/v1/relationships/`, as `RelationshipQuery.apply` really answers it.
938
+ *
939
+ * THE SHAPE THAT MATTERS IS THE ONE IT REFUSES TO NARROW. A parameter this
940
+ * endpoint does not recognise is swept into `ignored_filters` and the page
941
+ * comes back exactly as wide as it was — so a fixture that returned `[]` for
942
+ * an unsupported filter would let a client pass here and read the whole edge
943
+ * table in production. Every filter below is one the server really applies,
944
+ * and everything else is really ignored.
945
+ */
946
+ function edgeQuery(rows: TenantRelationship[], params: Record<string, unknown>) {
947
+ const applied: string[] = [];
948
+ const ignored: string[] = [];
949
+ const idList = (key: string): Set<string> => {
950
+ const parts = String(params[key]).split(',').map((part) => part.trim()).filter(Boolean);
951
+ // `_uuid_list` refuses a longer list with a 400 rather than truncating it,
952
+ // which is what keeps `__in` from becoming an unpaginated read by another
953
+ // name — and what a client that chunks its ids has to be tested against.
954
+ if (parts.length > MAX_ID_LIST) {
955
+ throw Object.assign(new Error(`${key}: at most ${MAX_ID_LIST} ids`), { status: 400 });
956
+ }
957
+ return new Set(parts);
958
+ };
959
+ const typeOf = (id: unknown): string =>
960
+ String(entities.find((entity) => String(entity.id) === String(id))?.entity_type ?? '');
961
+ let out = rows;
962
+
963
+ for (const [key, field] of [['source', 'source'], ['target', 'target']] as const) {
964
+ if (key in params) {
965
+ const wanted = String(params[key]);
966
+ out = out.filter((edge) => String(edge[field]) === wanted);
967
+ applied.push(key);
968
+ }
969
+ const listed = `${key}__in`;
970
+ if (listed in params) {
971
+ const wanted = idList(listed);
972
+ out = out.filter((edge) => wanted.has(String(edge[field])));
973
+ applied.push(listed);
974
+ }
975
+ }
976
+ if ('entity' in params) {
977
+ const wanted = String(params.entity);
978
+ out = out.filter((edge) => String(edge.source) === wanted || String(edge.target) === wanted);
979
+ applied.push('entity');
980
+ }
981
+ if ('entity__in' in params) {
982
+ const wanted = idList('entity__in');
983
+ out = out.filter((edge) => wanted.has(String(edge.source)) || wanted.has(String(edge.target)));
984
+ applied.push('entity__in');
985
+ }
986
+ if ('rel_type' in params) {
987
+ const wanted = new Set(
988
+ String(params.rel_type).split(',').map((part) => part.trim()).filter(Boolean),
989
+ );
990
+ out = out.filter((edge) => wanted.has(String(edge.rel_type ?? edge.relType ?? '')));
991
+ applied.push('rel_type');
992
+ }
993
+ for (const [key, end] of [['source_type', 'source'], ['target_type', 'target']] as const) {
994
+ if (key in params) {
995
+ const wanted = String(params[key]);
996
+ out = out.filter((edge) => typeOf(edge[end]) === wanted);
997
+ applied.push(key);
998
+ }
999
+ }
1000
+ for (const key of Object.keys(params)) {
1001
+ if (!RESERVED_PARAMS.has(key) && !RELATIONSHIP_FILTERS.has(key)) ignored.push(key);
1002
+ }
1003
+ return { rows: out, applied, ignored };
1004
+ }
1005
+
913
1006
  const http = {
914
1007
  async get<T>(path: string, init?: { params?: Record<string, unknown> }): Promise<T> {
915
1008
  const params = init?.params ?? {};
@@ -920,7 +1013,12 @@ export function createFixtureCorpusHttp(options: {
920
1013
  throw Object.assign(new Error(`${path} responded 404`), { status: 404 });
921
1014
  }
922
1015
  if (path.includes('relationships')) {
923
- return slice(relationships, params) as T;
1016
+ const query = edgeQuery(relationships, params);
1017
+ return {
1018
+ ...slice(query.rows, params),
1019
+ applied_filters: query.applied.slice().sort(),
1020
+ ignored_filters: query.ignored.slice().sort(),
1021
+ } as T;
924
1022
  }
925
1023
  if (path.includes('entities')) {
926
1024
  const id = /entities\/(.+)$/.exec(path)?.[1];
@@ -303,7 +303,16 @@ export async function startExtractionRun(
303
303
  const at = now();
304
304
  const driver = request.driverId ?? options.driverId ?? defaultDriverId();
305
305
 
306
- const existing = await deps.runs.readRun().catch(() => null);
306
+ // NOT `.catch(() => null)`. A store that could not answer is not a store that
307
+ // said "nothing has ever run", and this is the one place where confusing them
308
+ // is expensive: the branch below reads `null` as "start a new sweep", so a
309
+ // transient failure — or a read that could not see far enough (bd
310
+ // startsim-o0o6o.3) — put a SECOND driver on a live cursor, and two model
311
+ // calls over one document produce two items identity resolution cannot see are
312
+ // the same. `readRun` reports absence as `null` and everything else as a
313
+ // throw, precisely so this caller can tell them apart; swallowing the throw
314
+ // here threw that distinction away one line after it was made.
315
+ const existing = await deps.runs.readRun();
307
316
 
308
317
  if (existing && !COMPLETE.has(existing.status) && !request.restart) {
309
318
  if (isLeaseLive(existing, at) && existing.leaseOwner !== driver) {
@@ -172,8 +172,22 @@ export interface MoneyEvent {
172
172
  export interface SourceFreshness {
173
173
  source: SourceKey;
174
174
  label: string;
175
- /** How many documents this source has contributed to the corpus. */
176
- documentCount: number;
175
+ /**
176
+ * How many documents this source has contributed to the corpus — counted OUT
177
+ * OF THE CORPUS, per source, and the only fact on a freshness card that is
178
+ * not central's connection bookkeeping (bd startsim-jevtu). That is what
179
+ * makes it worth its round trip: a source central reports as freshly synced
180
+ * that holds nothing here is the delivery hop failing, and no timestamp on
181
+ * the connection can say so.
182
+ *
183
+ * `null` means NOT COUNTED — the server did not confirm it narrowed by
184
+ * source, so the number it returned describes the whole corpus rather than
185
+ * this card. A screen renders nothing at all for `null`. `0` therefore means
186
+ * exactly what it says, which it did not before: the field was read off a
187
+ * connections payload that has never carried it, so every card everywhere
188
+ * reported `0` by accident.
189
+ */
190
+ documentCount: number | null;
177
191
  /**
178
192
  * THREE FACTS, AND CONFLATING ANY TWO OF THEM IS HOW A FRESHNESS STRIP LIES
179
193
  * (bd startsim-1cow). Central's `SubtreeConnectionsView` computes them