@object-ui/data-objectstack 15.0.0 → 16.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "15.0.0",
3
+ "version": "16.1.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,9 +22,9 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@objectstack/client": "^14.6.0",
26
- "@object-ui/core": "15.0.0",
27
- "@object-ui/types": "15.0.0"
25
+ "@objectstack/client": "^16.0.0-rc.0",
26
+ "@object-ui/core": "16.1.0",
27
+ "@object-ui/types": "16.1.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
@@ -23,7 +23,9 @@ function csvResponse(body = 'ID,Name\n1,Acme') {
23
23
 
24
24
  describe('ObjectStackAdapter.exportDownload', () => {
25
25
  it('GETs the /export route with format, fields, orderby, header and limit', async () => {
26
- const fetchImpl = vi.fn(async () => csvResponse());
26
+ // Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
27
+ // than an empty tuple (the zero-arg impl would otherwise infer `never`).
28
+ const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
27
29
  const ds = makeDS(fetchImpl);
28
30
 
29
31
  const blob = await ds.exportDownload('task', {
@@ -50,7 +52,9 @@ describe('ObjectStackAdapter.exportDownload', () => {
50
52
  });
51
53
 
52
54
  it('defaults to csv and omits optional params when not provided', async () => {
53
- const fetchImpl = vi.fn(async () => csvResponse());
55
+ // Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
56
+ // than an empty tuple (the zero-arg impl would otherwise infer `never`).
57
+ const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
54
58
  const ds = makeDS(fetchImpl);
55
59
 
56
60
  await ds.exportDownload('task', {});
@@ -65,7 +69,9 @@ describe('ObjectStackAdapter.exportDownload', () => {
65
69
  });
66
70
 
67
71
  it('serializes the filter to a JSON AST query param', async () => {
68
- const fetchImpl = vi.fn(async () => csvResponse());
72
+ // Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
73
+ // than an empty tuple (the zero-arg impl would otherwise infer `never`).
74
+ const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
69
75
  const ds = makeDS(fetchImpl);
70
76
 
71
77
  await ds.exportDownload('task', { filter: [['status', '=', 'open']] });
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
10
10
  import type {
11
11
  DataSource,
12
+ BatchTransactionOperation,
12
13
  MutationEvent,
13
14
  QueryParams,
14
15
  QueryResult,
@@ -23,8 +24,13 @@ import type {
23
24
  ImportJobUndoResult,
24
25
  ListImportJobsOptions,
25
26
  } from '@object-ui/types';
26
- import { convertFiltersToAST, normalizeSchemaReferenceKeys } from '@object-ui/core';
27
+ import {
28
+ convertFiltersToAST,
29
+ emulateBatchTransaction,
30
+ normalizeSchemaReferenceKeys,
31
+ } from '@object-ui/core';
27
32
  import { MetadataCache } from './cache/MetadataCache';
33
+ import { MetadataClient } from './metadata-client';
28
34
  import {
29
35
  ObjectStackError,
30
36
  MetadataNotFoundError,
@@ -173,6 +179,44 @@ export function clearSharedDiscoveryCache(): void {
173
179
  discoveryCache.clear();
174
180
  }
175
181
 
182
+ /**
183
+ * Read the cross-object atomic-batch capability from a `discovery` document
184
+ * (framework #3298 / objectui #2693). The server advertises it hierarchically
185
+ * under `capabilities.transactionalBatch.enabled`; the published
186
+ * `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
187
+ * boolean` form and normalizes the two — mirror that here so the adapter reads
188
+ * the same bit regardless of which shape reaches it.
189
+ *
190
+ * Returns:
191
+ * - `true` — the backend GUARANTEES an atomic `/batch` (declared === enforced,
192
+ * i.e. the route is mounted AND the runtime can honour a transaction): the
193
+ * client may drop its non-atomic fallback and treat any batch failure as a
194
+ * real error.
195
+ * - `false` — the backend explicitly does NOT (route absent, or a runtime that
196
+ * can't open a transaction).
197
+ * - `undefined` — the capability is absent, i.e. the backend predates #3298;
198
+ * the caller must keep the legacy runtime-probe fallback (we can't tell
199
+ * whether `/batch` exists without trying it).
200
+ */
201
+ export function readTransactionalBatchCapability(
202
+ discovery: unknown,
203
+ ): boolean | undefined {
204
+ const caps = (discovery as { capabilities?: unknown } | null | undefined)?.capabilities;
205
+ if (!caps || typeof caps !== 'object') return undefined;
206
+ const value = (caps as Record<string, unknown>).transactionalBatch;
207
+ // Flat form: `{ transactionalBatch: true }`.
208
+ if (typeof value === 'boolean') return value;
209
+ // Hierarchical form: `{ transactionalBatch: { enabled: true } }`.
210
+ if (
211
+ value &&
212
+ typeof value === 'object' &&
213
+ typeof (value as { enabled?: unknown }).enabled === 'boolean'
214
+ ) {
215
+ return (value as { enabled: boolean }).enabled;
216
+ }
217
+ return undefined;
218
+ }
219
+
176
220
  /**
177
221
  * Detect "missing resource" errors regardless of where they originate.
178
222
  *
@@ -404,6 +448,21 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
404
448
  // so optional collections like sys_presence don't hammer the server with
405
449
  // failing requests on every record open / panel render.
406
450
  private missingResources = new Set<string>();
451
+ // Set once the server has told us it can't do a cross-object transactional
452
+ // batch (the client SDK's data.batchTransaction threw HTTP 404/405/501). After
453
+ // that, batchTransaction skips the SDK call and serves every call via the
454
+ // client-side emulation — the non-atomic fallback lives HERE, isolated to the
455
+ // one adapter that has to cope with a backend lacking server atomicity (#2679).
456
+ private batchUnsupported = false;
457
+ // The server's declared cross-object atomic-batch capability, read from
458
+ // discovery at connect() (framework #3298 / objectui #2693). `true` → the
459
+ // backend GUARANTEES an atomic `/batch`, so batchTransaction trusts it and
460
+ // never degrades to the non-atomic emulation (any failure surfaces as a real
461
+ // error). `false` or `undefined` (capability absent → backend predates #3298)
462
+ // → keep the legacy runtime-probe + emulation fallback so a save is still
463
+ // possible; dropping it there would turn "saves, less safe" into "no save
464
+ // path" on older backends (#2679 compatibility constraint).
465
+ private atomicBatchCapability: boolean | undefined;
407
466
  // Subscribers registered via onMutation(). Emitted after each successful
408
467
  // create/update/delete so data-bound views (ListView, ObjectView, kanban,
409
468
  // calendar) auto-refresh — the interface ListView relies on to reflect
@@ -479,6 +538,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
479
538
  // helpers continue to work without a redundant fetch.
480
539
  (this.client as unknown as { discoveryInfo?: unknown }).discoveryInfo = data;
481
540
 
541
+ // Record the declared cross-object atomic-batch capability (#3298) so
542
+ // batchTransaction can decide declaratively at call time whether it may
543
+ // trust server atomicity instead of runtime-probing 404/405/501.
544
+ this.atomicBatchCapability = readTransactionalBatchCapability(data);
545
+
482
546
  this.connected = true;
483
547
  this.reconnectAttempts = 0;
484
548
  this.setConnectionState('connected');
@@ -927,42 +991,119 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
927
991
  * @returns Promise resolving to array of results
928
992
  */
929
993
  /**
930
- * Cross-object transactional batch (ObjectStack #1604). Runs the operations
931
- * in ONE server transaction — commit all or roll back all. A field value of
932
- * `{ $ref: <earlier op index> }` resolves to that op's created id, so a child
933
- * can reference its parent created earlier in the same batch (master-detail).
994
+ * Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
995
+ * Runs the operations in ONE server transaction — commit all or roll back
996
+ * all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
997
+ * created id, so a child can reference its parent created earlier in the same
998
+ * batch (master-detail).
999
+ *
1000
+ * Transport: the published `@objectstack/client` SDK method
1001
+ * `data.batchTransaction` (framework #3271; shipped since client v16, our
1002
+ * dependency floor). Per AGENTS.md §7 data always flows through the client —
1003
+ * never a hand-rolled `fetch('/api/v1/batch')`.
1004
+ *
1005
+ * Fallback decision — declarative capability negotiation (framework #3298 /
1006
+ * objectui #2693). At connect() we read `capabilities.transactionalBatch`
1007
+ * from discovery:
1008
+ * - Declared `true` → the backend GUARANTEES atomicity (declared ===
1009
+ * enforced). We TRUST it: any batch failure — including 404/405/501 —
1010
+ * surfaces as a real error. No non-atomic client-side compensation. This
1011
+ * is the path modern backends take.
1012
+ * - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
1013
+ * server atomicity, so we keep the legacy behaviour: on 404/405 (no
1014
+ * endpoint) or 501 (runtime without transactions) degrade to the
1015
+ * client-side, NON-atomic {@link emulateBatchTransaction} so a save is
1016
+ * still possible. Removing that here would regress older backends from
1017
+ * "saves, less safe" to "no save path" (#2679 compatibility constraint).
1018
+ * The non-atomic fallback stays isolated to THIS adapter.
934
1019
  */
935
1020
  async batchTransaction(
936
- operations: Array<{ object: string; action?: 'create' | 'update' | 'delete'; data?: any; id?: string }>,
1021
+ operations: BatchTransactionOperation[],
937
1022
  ): Promise<{ results: any[] }> {
938
- const url = `${this.baseUrl}/api/v1/batch`;
939
- const response = await this.fetchImpl(url, {
940
- method: 'POST',
941
- headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
942
- // Send the session cookie too: in the browser console auth is cookie-based
943
- // (no bearer `token`), so without `credentials: 'include'` this raw fetch
944
- // is unauthenticated — every master-detail batch save would 401. Bearer
945
- // (server-to-server) and cookie (console) auth now both work.
946
- credentials: 'include',
947
- body: JSON.stringify({ operations }),
948
- });
949
- if (!response.ok) {
950
- const error = await response.json().catch(() => ({ message: response.statusText }));
951
- throw new ObjectStackError(
952
- error.error || error.message || `Batch failed with status ${response.status}`,
953
- 'BATCH_ERROR',
954
- response.status,
1023
+ // Ensure discovery (and thus the #3298 capability) is loaded so the
1024
+ // decision below is declarative, not "fire a batch and read the status".
1025
+ await this.connect();
1026
+
1027
+ // When the backend declares atomic batch support we never degrade: a
1028
+ // failure is a real error, not a cue to fall back. Otherwise (declared
1029
+ // false, or capability absent on a pre-#3298 backend) the emulation
1030
+ // fallback below stays active.
1031
+ const guaranteed = this.atomicBatchCapability === true;
1032
+
1033
+ // Already degraded on a non-declaring backend — skip the SDK call.
1034
+ // (Unreachable once `guaranteed`: that path never sets `batchUnsupported`.)
1035
+ if (!guaranteed && this.batchUnsupported) {
1036
+ return emulateBatchTransaction(this, operations);
1037
+ }
1038
+
1039
+ try {
1040
+ // Typed SDK method — guaranteed present by the `@objectstack/client@^16`
1041
+ // dependency floor (framework #3271). No hand-rolled POST /api/v1/batch.
1042
+ const payload = await this.client.data.batchTransaction(operations);
1043
+ this.emitBatchMutations(operations, payload?.results);
1044
+ return payload;
1045
+ } catch (err) {
1046
+ // On a non-declaring backend, endpoint missing (404/405) or a runtime that
1047
+ // can't do transactions (the framework rest-server answers 501
1048
+ // "Transactional batch not supported by this runtime") → degrade to the
1049
+ // non-atomic client emulation so the save still goes through. When the
1050
+ // backend DECLARED support (`guaranteed`), even these are hard errors — a
1051
+ // server that advertised the capability must honour it. Every other status
1052
+ // (400 validation, 401/403 auth, 409 conflict, 500 fault) is a real error
1053
+ // the caller must see — never silently retried.
1054
+ const status = this.errorStatusOf(err);
1055
+ if (!guaranteed && this.batchStatusUnsupported(status)) {
1056
+ return this.fallbackToEmulation(operations, status);
1057
+ }
1058
+ throw err;
1059
+ }
1060
+ }
1061
+
1062
+ /** True for statuses that mean "this backend can't do a transactional batch". */
1063
+ private batchStatusUnsupported(status: number | undefined): boolean {
1064
+ return status === 404 || status === 405 || status === 501;
1065
+ }
1066
+
1067
+ /** Best-effort HTTP status extraction from a thrown SDK/client error. */
1068
+ private errorStatusOf(err: unknown): number | undefined {
1069
+ if (!err || typeof err !== 'object') return undefined;
1070
+ const e = err as Record<string, unknown>;
1071
+ const s = e.httpStatus ?? e.status ?? e.statusCode;
1072
+ return typeof s === 'number' ? s : undefined;
1073
+ }
1074
+
1075
+ /** Mark the endpoint unsupported (warn once) and serve via emulation. */
1076
+ private fallbackToEmulation(
1077
+ operations: BatchTransactionOperation[],
1078
+ status: number | undefined,
1079
+ ): Promise<{ results: any[] }> {
1080
+ if (!this.batchUnsupported) {
1081
+ this.batchUnsupported = true;
1082
+ console.warn(
1083
+ `ObjectStackAdapter: POST /api/v1/batch unavailable (HTTP ${status ?? '?'}) — ` +
1084
+ 'falling back to non-atomic client-side batch emulation. Cross-object ' +
1085
+ 'saves on this backend are best-effort, not transactional.',
955
1086
  );
956
1087
  }
957
- const payload: { results: any[] } = await response.json();
958
- // The whole transaction committed — emit one MutationEvent per operation so
959
- // the invalidation bus (#2269) sees writes that went through /batch exactly
960
- // like single create/update/delete calls (master-detail ModalForm saves
961
- // otherwise leave related lists and count badges stale, #2582). `results`
962
- // is index-aligned with `operations`; creates take id/record from the
963
- // server echo. A failed batch rolled back entirely and throws above, so
964
- // nothing is emitted for it.
965
- const results = Array.isArray(payload?.results) ? payload.results : [];
1088
+ return emulateBatchTransaction(this, operations);
1089
+ }
1090
+
1091
+ /**
1092
+ * Emit one MutationEvent per committed operation so the invalidation bus
1093
+ * (#2269) sees writes that went through /batch exactly like single
1094
+ * create/update/delete calls — master-detail ModalForm saves otherwise leave
1095
+ * related lists and count badges stale (#2582). `results` is index-aligned
1096
+ * with `operations`; creates take id/record from the server echo.
1097
+ *
1098
+ * Only called on the server-committed paths. The emulation branch drives the
1099
+ * adapter's own create/update/delete primitives, which already emit — so it
1100
+ * must NOT be routed through here, or events would double-fire.
1101
+ */
1102
+ private emitBatchMutations(
1103
+ operations: BatchTransactionOperation[],
1104
+ rawResults: unknown,
1105
+ ): void {
1106
+ const results = Array.isArray(rawResults) ? rawResults : [];
966
1107
  operations.forEach((op, i) => {
967
1108
  const action = op.action ?? 'create';
968
1109
  const echo = results[i];
@@ -974,7 +1115,6 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
974
1115
  this.emitMutation({ type: 'delete', resource: op.object, id: op.id });
975
1116
  }
976
1117
  });
977
- return payload;
978
1118
  }
979
1119
 
980
1120
  async bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]> {
@@ -1930,32 +2070,25 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1930
2070
  config: Record<string, any>
1931
2071
  ): Promise<Record<string, any> | void> {
1932
2072
  await this.connect();
1933
- try {
1934
- // ADR-0005 metadata customization overlay: persist views under
1935
- // `type='view'` (NOT `type=<objectName>` — that was a pre-overlay
1936
- // misuse that hit `/api/v1/meta/<objectName>/<viewId>`, which the
1937
- // server never wired). The view's `data.object` field is what
1938
- // associates it back to the object on read.
1939
- const merged = { ...(config || {}), object: (config as any)?.object || objectName, name: viewId };
1940
- const result: any = await this.client.meta.saveItem(
1941
- 'view',
1942
- viewId,
1943
- merged
1944
- );
1945
- // Invalidate cached read so next getView reflects the change
1946
- const cacheKey = `view:${objectName}:${viewId}`;
1947
- this.metadataCache.invalidate?.(cacheKey);
1948
- // Also invalidate the batch override map so listViewOverrides re-fetches
1949
- this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1950
- this.metadataCache.invalidate?.(`views:${objectName}`);
1951
- if (result && result.item) return result.item;
1952
- return result ?? undefined;
1953
- } catch (err) {
1954
- // Surface the error so the caller can decide whether to toast/log;
1955
- // we don't swallow it here because persistence failures are
1956
- // operationally meaningful (unlike read fallbacks).
1957
- throw err;
1958
- }
2073
+ // ADR-0005 metadata customization overlay: persist views under
2074
+ // `type='view'` (NOT `type=<objectName>` — that was a pre-overlay
2075
+ // misuse that hit `/api/v1/meta/<objectName>/<viewId>`, which the
2076
+ // server never wired). The view's `data.object` field is what
2077
+ // associates it back to the object on read.
2078
+ const merged = { ...(config || {}), object: (config as any)?.object || objectName, name: viewId };
2079
+ const result: any = await this.client.meta.saveItem(
2080
+ 'view',
2081
+ viewId,
2082
+ merged
2083
+ );
2084
+ // Invalidate cached read so next getView reflects the change
2085
+ const cacheKey = `view:${objectName}:${viewId}`;
2086
+ this.metadataCache.invalidate?.(cacheKey);
2087
+ // Also invalidate the batch override map so listViewOverrides re-fetches
2088
+ this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
2089
+ this.metadataCache.invalidate?.(`views:${objectName}`);
2090
+ if (result && result.item) return result.item;
2091
+ return result ?? undefined;
1959
2092
  }
1960
2093
 
1961
2094
  /**
@@ -1968,13 +2101,31 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1968
2101
  * Filters by `data.object === objectName` (or top-level `object`)
1969
2102
  * client-side because the metadata index is name-only, not field-typed.
1970
2103
  */
1971
- async listViews(objectName: string): Promise<any[]> {
2104
+ async listViews(
2105
+ objectName: string,
2106
+ options?: { previewDrafts?: boolean },
2107
+ ): Promise<any[]> {
1972
2108
  await this.connect();
1973
2109
  try {
1974
- const result: any = await this.client.meta.getItems('view');
1975
- const items: any[] = Array.isArray(result?.items)
1976
- ? result.items
1977
- : Array.isArray(result) ? result : [];
2110
+ let items: any[];
2111
+ if (options?.previewDrafts) {
2112
+ // ADR-0037 + #2767 (P2/P3): a SINGLE `?preview=draft` request already
2113
+ // returns the active+draft OVERLAID list — draft wins by name,
2114
+ // draft-only views surface, each draft tagged `_draft: true`. So we
2115
+ // REPLACE the published list wholesale (never fetch both and append,
2116
+ // which double-lists a draft that edits a published view). Route it
2117
+ // through `MetadataClient` rather than a hand-rolled fetch so the
2118
+ // metadata route + any environment scoping stay in one place.
2119
+ const draftItems = await this.metadataClient()
2120
+ .withPreviewDrafts(true)
2121
+ .list<any>('view');
2122
+ items = Array.isArray(draftItems) ? draftItems : [];
2123
+ } else {
2124
+ const result: any = await this.client.meta.getItems('view');
2125
+ items = Array.isArray(result?.items)
2126
+ ? result.items
2127
+ : Array.isArray(result) ? result : [];
2128
+ }
1978
2129
  // This feeds the list-view switcher (ViewTabBar), so it must return
1979
2130
  // LIST-family views only. The backend now exposes each view as an
1980
2131
  // independent ViewItem carrying a `viewKind` discriminant (ADR-0017);
@@ -1993,6 +2144,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1993
2144
  return !(viewKind && FORM_FAMILY.has(viewKind));
1994
2145
  }).map((v: any) => {
1995
2146
  const spec = v.list ?? v;
2147
+ // Preserve the draft provenance flag so the switcher can badge an
2148
+ // unpublished view (ADR-0037). The overlay tags the item, not its
2149
+ // nested spec, so read from either.
2150
+ const isDraft = v._draft === true || spec?._draft === true;
1996
2151
  // Canonical ViewItem (ADR-0017) carries its body under `config`;
1997
2152
  // the display `type` (grid/kanban/gallery/…) lives at `config.type`,
1998
2153
  // and only the list/form *family* sits at the top level (`viewKind`).
@@ -2008,9 +2163,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2008
2163
  name: spec.name ?? spec.config.name,
2009
2164
  label: spec.label ?? spec.config.label,
2010
2165
  isDefault: !!spec.isDefault,
2166
+ ...(isDraft ? { _draft: true } : {}),
2011
2167
  };
2012
2168
  }
2013
- return spec;
2169
+ return isDraft ? { ...spec, _draft: true } : spec;
2014
2170
  });
2015
2171
  } catch (err) {
2016
2172
  console.warn('[OBJECTSTACKDataSource] listViews failed:', err);
@@ -2018,6 +2174,20 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2018
2174
  }
2019
2175
  }
2020
2176
 
2177
+ /**
2178
+ * Build a {@link MetadataClient} bound to this adapter's server + auth. Used
2179
+ * by draft-aware reads (`listViews({ previewDrafts })`) so the `/meta` route,
2180
+ * `?preview=draft` flag, and environment scoping live in the SDK rather than
2181
+ * being hand-assembled at each call site (#2767 P3).
2182
+ */
2183
+ private metadataClient(): MetadataClient {
2184
+ return new MetadataClient({
2185
+ baseUrl: this.baseUrl,
2186
+ fetch: this.fetchImpl,
2187
+ ...(this.token ? { headers: { Authorization: `Bearer ${this.token}` } } : {}),
2188
+ });
2189
+ }
2190
+
2021
2191
  /**
2022
2192
  * List registered import `mapping` artifacts targeting a given object
2023
2193
  * (framework #2611). Reads the `mapping` metadata kind via the overlay API
@@ -2075,14 +2245,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2075
2245
  object: spec?.object || objectName,
2076
2246
  data: spec?.data || { provider: 'object', object: objectName },
2077
2247
  };
2078
- try {
2079
- const result: any = await this.client.meta.saveItem('view', name, fullSpec);
2080
- this.metadataCache.invalidate?.(`views:${objectName}`);
2081
- if (result && result.item) return result.item;
2082
- return fullSpec;
2083
- } catch (err) {
2084
- throw err;
2085
- }
2248
+ const result: any = await this.client.meta.saveItem('view', name, fullSpec);
2249
+ this.metadataCache.invalidate?.(`views:${objectName}`);
2250
+ if (result && result.item) return result.item;
2251
+ return fullSpec;
2086
2252
  }
2087
2253
 
2088
2254
  /**
@@ -2112,15 +2278,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2112
2278
  name: viewName,
2113
2279
  object: current?.object || (current as any)?.data?.object || objectName,
2114
2280
  };
2115
- try {
2116
- const result: any = await this.client.meta.saveItem('view', viewName, merged);
2117
- this.metadataCache.invalidate?.(`views:${objectName}`);
2118
- this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
2119
- if (result && result.item) return result.item;
2120
- return merged;
2121
- } catch (err) {
2122
- throw err;
2123
- }
2281
+ const result: any = await this.client.meta.saveItem('view', viewName, merged);
2282
+ this.metadataCache.invalidate?.(`views:${objectName}`);
2283
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
2284
+ if (result && result.item) return result.item;
2285
+ return merged;
2124
2286
  }
2125
2287
 
2126
2288
  /**
@@ -2133,14 +2295,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2133
2295
  viewName: string,
2134
2296
  ): Promise<{ deleted: boolean }> {
2135
2297
  await this.connect();
2136
- try {
2137
- const result: any = await this.client.meta.deleteItem('view', viewName);
2138
- this.metadataCache.invalidate?.(`views:${objectName}`);
2139
- this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
2140
- return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
2141
- } catch (err) {
2142
- throw err;
2143
- }
2298
+ const result: any = await this.client.meta.deleteItem('view', viewName);
2299
+ this.metadataCache.invalidate?.(`views:${objectName}`);
2300
+ this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
2301
+ return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
2144
2302
  }
2145
2303
 
2146
2304
 
@@ -2207,21 +2365,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2207
2365
  schema: Record<string, any>
2208
2366
  ): Promise<Record<string, any> | void> {
2209
2367
  await this.connect();
2210
- try {
2211
- const result: any = await this.client.meta.saveItem(
2212
- 'dashboard',
2213
- dashboardName,
2214
- schema
2215
- );
2216
- // Invalidate dashboards list and any cached dashboard read so the
2217
- // next render reflects the change.
2218
- this.metadataCache.invalidate?.('dashboards');
2219
- this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
2220
- if (result && result.item) return result.item;
2221
- return result ?? undefined;
2222
- } catch (err) {
2223
- throw err;
2224
- }
2368
+ const result: any = await this.client.meta.saveItem(
2369
+ 'dashboard',
2370
+ dashboardName,
2371
+ schema
2372
+ );
2373
+ // Invalidate dashboards list and any cached dashboard read so the
2374
+ // next render reflects the change.
2375
+ this.metadataCache.invalidate?.('dashboards');
2376
+ this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
2377
+ if (result && result.item) return result.item;
2378
+ return result ?? undefined;
2225
2379
  }
2226
2380
 
2227
2381
  /**
@@ -143,6 +143,10 @@ export class IntegrationManager {
143
143
  }
144
144
  } catch (e) {
145
145
  if (e instanceof TypeError) {
146
+ // A malformed URL surfaces as its own message; we can't attach `e`
147
+ // as the `Error` `cause` because this package targets ES2020, whose
148
+ // lib types the 1-arg `Error` constructor only. Scoped disable.
149
+ // eslint-disable-next-line preserve-caught-error
146
150
  throw new Error(`Invalid webhook URL: ${url}`);
147
151
  }
148
152
  throw e;
@@ -168,6 +172,10 @@ export class IntegrationManager {
168
172
  }
169
173
  } catch (e) {
170
174
  if (e instanceof TypeError) {
175
+ // A malformed URL surfaces as its own message; we can't attach `e`
176
+ // as the `Error` `cause` because this package targets ES2020, whose
177
+ // lib types the 1-arg `Error` constructor only. Scoped disable.
178
+ // eslint-disable-next-line preserve-caught-error
171
179
  throw new Error(`Invalid Slack webhook URL: ${url}`);
172
180
  }
173
181
  throw e;