@object-ui/data-objectstack 4.0.7 → 4.0.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 4.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@4.0.9
8
+ - @object-ui/core@4.0.9
9
+
10
+ ## 4.0.8
11
+
12
+ ### Patch Changes
13
+
14
+ - @object-ui/types@4.0.8
15
+ - @object-ui/core@4.0.8
16
+
3
17
  ## 4.0.7
4
18
 
5
19
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  createObjectStackAdapter: () => createObjectStackAdapter,
40
40
  generateContractManifest: () => generateContractManifest,
41
41
  getSharedDiscovery: () => getSharedDiscovery,
42
+ is404Error: () => is404Error,
42
43
  isErrorType: () => isErrorType,
43
44
  isObjectStackError: () => isObjectStackError,
44
45
  snapToGrid: () => snapToGrid,
@@ -895,6 +896,44 @@ async function getSharedDiscovery(baseUrl, fetcher) {
895
896
  function clearSharedDiscoveryCache() {
896
897
  discoveryCache.clear();
897
898
  }
899
+ function is404Error(error) {
900
+ if (!error || typeof error !== "object") return false;
901
+ const err = error;
902
+ if (err.httpStatus === 404 || err.status === 404 || err.statusCode === 404) {
903
+ return true;
904
+ }
905
+ const code = typeof err.code === "string" ? err.code : "";
906
+ return code === "object_not_found" || code === "record_not_found";
907
+ }
908
+ function createQuietHttpLogger() {
909
+ const isExpected404 = (meta) => {
910
+ if (!meta || typeof meta !== "object") return false;
911
+ if (meta.status === 404 || meta.statusCode === 404) return true;
912
+ const errBody = meta.error;
913
+ if (errBody && typeof errBody === "object") {
914
+ const code = errBody.code;
915
+ if (code === "object_not_found" || code === "record_not_found") return true;
916
+ }
917
+ return false;
918
+ };
919
+ const logger = {
920
+ debug: (message, meta) => console.debug(message, meta ?? ""),
921
+ info: (message, meta) => console.info(message, meta ?? ""),
922
+ warn: (message, meta) => console.warn(message, meta ?? ""),
923
+ error: (message, error, meta) => {
924
+ if (isExpected404(meta)) {
925
+ console.debug(`[ObjectStack] ${message} (suppressed expected 404)`, meta);
926
+ return;
927
+ }
928
+ console.error(message, error ?? "", meta ?? "");
929
+ },
930
+ fatal: (message, error, meta) => console.error(message, error ?? "", meta ?? ""),
931
+ log: (message, ...args) => console.log(message, ...args),
932
+ child: () => logger,
933
+ withTrace: () => logger
934
+ };
935
+ return logger;
936
+ }
898
937
  function stableStringify(value) {
899
938
  if (value === null || typeof value !== "object") return JSON.stringify(value);
900
939
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
@@ -923,7 +962,7 @@ var ObjectStackAdapter = class {
923
962
  // multiple sibling components requesting the same dataset on first paint)
924
963
  // into a single network round trip.
925
964
  __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
926
- this.client = new import_client.ObjectStackClient(config);
965
+ this.client = new import_client.ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
927
966
  this.metadataCache = new MetadataCache(config.cache);
928
967
  this.autoReconnect = config.autoReconnect ?? true;
929
968
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
@@ -1074,11 +1113,12 @@ var ObjectStackAdapter = class {
1074
1113
  return this.normalizeQueryResult(result, params);
1075
1114
  })();
1076
1115
  this.inflightFinds.set(key, promise);
1077
- promise.finally(() => {
1116
+ const cleanup = () => {
1078
1117
  if (this.inflightFinds.get(key) === promise) {
1079
1118
  this.inflightFinds.delete(key);
1080
1119
  }
1081
- });
1120
+ };
1121
+ promise.then(cleanup, cleanup);
1082
1122
  return promise;
1083
1123
  }
1084
1124
  /**
@@ -1101,7 +1141,7 @@ var ObjectStackAdapter = class {
1101
1141
  const records = resultObj.records || resultObj.value || [];
1102
1142
  return records[0] || null;
1103
1143
  } catch (error) {
1104
- if (error?.status === 404) {
1144
+ if (is404Error(error)) {
1105
1145
  return null;
1106
1146
  }
1107
1147
  }
@@ -1110,7 +1150,7 @@ var ObjectStackAdapter = class {
1110
1150
  const result = await this.client.data.get(resource, String(id));
1111
1151
  return result.record;
1112
1152
  } catch (error) {
1113
- if (error?.status === 404) {
1153
+ if (is404Error(error)) {
1114
1154
  return null;
1115
1155
  }
1116
1156
  throw error;
@@ -1434,7 +1474,7 @@ var ObjectStackAdapter = class {
1434
1474
  return schema;
1435
1475
  } catch (error) {
1436
1476
  const errorObj = error;
1437
- if (errorObj?.status === 404 || errorObj?.statusCode === 404) {
1477
+ if (is404Error(errorObj)) {
1438
1478
  throw new MetadataNotFoundError(objectName, { originalError: error });
1439
1479
  }
1440
1480
  if (error instanceof ObjectStackError) {
@@ -1466,6 +1506,43 @@ var ObjectStackAdapter = class {
1466
1506
  return null;
1467
1507
  }
1468
1508
  }
1509
+ /**
1510
+ * Batch-fetch all persisted view overrides for an object.
1511
+ *
1512
+ * Per-view runtime overrides (density, column widths, sort, …) are
1513
+ * stored in the metadata registry under key `<objectName>/<viewName>`.
1514
+ * Loading them per-view fires N HTTP GETs that return 404 for views
1515
+ * the user has never customized — generates console noise on every
1516
+ * page load. This batch method performs a single
1517
+ * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
1518
+ * returns a `{viewName: override}` map. Returns an empty map if the
1519
+ * server doesn't expose the listing or returns no items.
1520
+ *
1521
+ * Result is cached identically to {@link getView}; saving a view via
1522
+ * {@link updateViewConfig} invalidates the cache.
1523
+ *
1524
+ * @param objectName - Object name (e.g. 'lead')
1525
+ * @returns Map keyed by view name with the persisted override config
1526
+ */
1527
+ async listViewOverrides(objectName) {
1528
+ await this.connect();
1529
+ try {
1530
+ const cacheKey = `view-overrides:${objectName}`;
1531
+ return await this.metadataCache.get(cacheKey, async () => {
1532
+ const result = await this.client.meta.getItems(objectName);
1533
+ const items = Array.isArray(result?.items) ? result.items : [];
1534
+ const out = {};
1535
+ for (const it of items) {
1536
+ if (!it || typeof it !== "object") continue;
1537
+ const key = it.name ?? it.id ?? it._name;
1538
+ if (typeof key === "string" && key) out[key] = it;
1539
+ }
1540
+ return out;
1541
+ });
1542
+ } catch {
1543
+ return {};
1544
+ }
1545
+ }
1469
1546
  /**
1470
1547
  * Get a view definition for an object.
1471
1548
  * Attempts to fetch from the server metadata API.
@@ -1481,7 +1558,7 @@ var ObjectStackAdapter = class {
1481
1558
  try {
1482
1559
  const cacheKey = `view:${objectName}:${viewId}`;
1483
1560
  return await this.metadataCache.get(cacheKey, async () => {
1484
- const result = await this.client.meta.getItem(objectName, `views/${viewId}`);
1561
+ const result = await this.client.meta.getItem(objectName, viewId);
1485
1562
  if (result && result.item) return result.item;
1486
1563
  return result ?? null;
1487
1564
  });
@@ -1489,6 +1566,40 @@ var ObjectStackAdapter = class {
1489
1566
  return null;
1490
1567
  }
1491
1568
  }
1569
+ /**
1570
+ * Persist a view definition for an object.
1571
+ *
1572
+ * Symmetric counterpart to {@link getView}: writes the view to the
1573
+ * server metadata store via `client.meta.saveItem`, then invalidates
1574
+ * the matching cache entry so the next {@link getView} reflects the
1575
+ * new payload. Returns the persisted item when the server echoes it,
1576
+ * otherwise undefined.
1577
+ *
1578
+ * Used by ObjectView for "live" toolbar persistence (density,
1579
+ * column widths, sort, etc.) and by the View Config Panel for
1580
+ * explicit saves.
1581
+ *
1582
+ * @param objectName - Object name (e.g. 'lead')
1583
+ * @param viewId - View identifier (e.g. 'all_leads')
1584
+ * @param config - Full view definition to persist
1585
+ */
1586
+ async updateViewConfig(objectName, viewId, config) {
1587
+ await this.connect();
1588
+ try {
1589
+ const result = await this.client.meta.saveItem(
1590
+ objectName,
1591
+ viewId,
1592
+ config
1593
+ );
1594
+ const cacheKey = `view:${objectName}:${viewId}`;
1595
+ this.metadataCache.invalidate?.(cacheKey);
1596
+ this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1597
+ if (result && result.item) return result.item;
1598
+ return result ?? void 0;
1599
+ } catch (err) {
1600
+ throw err;
1601
+ }
1602
+ }
1492
1603
  /**
1493
1604
  * Get an application definition by name or ID.
1494
1605
  * Attempts to fetch from the server metadata API.
@@ -1529,6 +1640,34 @@ var ObjectStackAdapter = class {
1529
1640
  return null;
1530
1641
  }
1531
1642
  }
1643
+ /**
1644
+ * Update (upsert) a dashboard definition.
1645
+ *
1646
+ * Dashboards are control-plane metadata, not data records. Persist via
1647
+ * `client.meta.saveItem('dashboard', name, schema)` which routes to
1648
+ * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the
1649
+ * relevant metadata cache entry so the next dashboard read reflects
1650
+ * the new payload.
1651
+ *
1652
+ * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard')
1653
+ * @param schema - Full dashboard schema (widgets, layout, etc.)
1654
+ */
1655
+ async updateDashboard(dashboardName, schema) {
1656
+ await this.connect();
1657
+ try {
1658
+ const result = await this.client.meta.saveItem(
1659
+ "dashboard",
1660
+ dashboardName,
1661
+ schema
1662
+ );
1663
+ this.metadataCache.invalidate?.("dashboards");
1664
+ this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
1665
+ if (result && result.item) return result.item;
1666
+ return result ?? void 0;
1667
+ } catch (err) {
1668
+ throw err;
1669
+ }
1670
+ }
1532
1671
  /**
1533
1672
  * Perform server-side aggregation via the ObjectStack analytics API.
1534
1673
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
@@ -1547,7 +1686,7 @@ var ObjectStackAdapter = class {
1547
1686
  dimensions: params.groupBy && params.groupBy !== "_all" ? [params.groupBy] : []
1548
1687
  };
1549
1688
  if (params.filter) {
1550
- payload.filters = params.filter;
1689
+ payload.where = params.filter;
1551
1690
  }
1552
1691
  const data = await this.client.analytics.query(payload);
1553
1692
  const rawRows = Array.isArray(data) ? data : data?.rows && Array.isArray(data.rows) ? data.rows : data?.data && Array.isArray(data.data) ? data.data : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows : data?.results && Array.isArray(data.results) ? data.results : [];
@@ -1558,7 +1697,7 @@ var ObjectStackAdapter = class {
1558
1697
  return true;
1559
1698
  });
1560
1699
  if (measureMissing) {
1561
- const result = await this.find(resource);
1700
+ const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
1562
1701
  const records = result.data || [];
1563
1702
  if (records.length === 0) return [];
1564
1703
  return this.aggregateClientSide(records, params);
@@ -1572,7 +1711,7 @@ var ObjectStackAdapter = class {
1572
1711
  return mapped;
1573
1712
  });
1574
1713
  } catch {
1575
- const result = await this.find(resource);
1714
+ const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
1576
1715
  const records = result.data || [];
1577
1716
  if (records.length === 0) return [];
1578
1717
  return this.aggregateClientSide(records, params);
@@ -1771,6 +1910,7 @@ function createObjectStackAdapter(config) {
1771
1910
  createObjectStackAdapter,
1772
1911
  generateContractManifest,
1773
1912
  getSharedDiscovery,
1913
+ is404Error,
1774
1914
  isErrorType,
1775
1915
  isObjectStackError,
1776
1916
  snapToGrid,
package/dist/index.d.cts CHANGED
@@ -648,6 +648,16 @@ declare function calculateAutoLayout(items: Array<{
648
648
  declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unknown>): Promise<unknown>;
649
649
  /** Test/dev helper to drop the cache (e.g. on logout or origin change). */
650
650
  declare function clearSharedDiscoveryCache(): void;
651
+ /**
652
+ * Detect "missing resource" errors regardless of where they originate.
653
+ *
654
+ * The ObjectStack client decorates thrown errors with `httpStatus` (and a
655
+ * machine-readable `code` such as `object_not_found`/`record_not_found`),
656
+ * while raw `fetch()` callers may surface `status` or `statusCode`. Treat
657
+ * any of these as a 404 so callers can degrade gracefully instead of
658
+ * tripping on the property-name mismatch.
659
+ */
660
+ declare function is404Error(error: unknown): boolean;
651
661
  /**
652
662
  * Connection state for monitoring
653
663
  */
@@ -838,6 +848,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
838
848
  * @returns Promise resolving to discovery data, or null if not connected
839
849
  */
840
850
  getDiscovery(): Promise<unknown | null>;
851
+ /**
852
+ * Batch-fetch all persisted view overrides for an object.
853
+ *
854
+ * Per-view runtime overrides (density, column widths, sort, …) are
855
+ * stored in the metadata registry under key `<objectName>/<viewName>`.
856
+ * Loading them per-view fires N HTTP GETs that return 404 for views
857
+ * the user has never customized — generates console noise on every
858
+ * page load. This batch method performs a single
859
+ * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
860
+ * returns a `{viewName: override}` map. Returns an empty map if the
861
+ * server doesn't expose the listing or returns no items.
862
+ *
863
+ * Result is cached identically to {@link getView}; saving a view via
864
+ * {@link updateViewConfig} invalidates the cache.
865
+ *
866
+ * @param objectName - Object name (e.g. 'lead')
867
+ * @returns Map keyed by view name with the persisted override config
868
+ */
869
+ listViewOverrides(objectName: string): Promise<Record<string, any>>;
841
870
  /**
842
871
  * Get a view definition for an object.
843
872
  * Attempts to fetch from the server metadata API.
@@ -849,6 +878,24 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
849
878
  * @returns Promise resolving to the view definition or null
850
879
  */
851
880
  getView(objectName: string, viewId: string): Promise<unknown | null>;
881
+ /**
882
+ * Persist a view definition for an object.
883
+ *
884
+ * Symmetric counterpart to {@link getView}: writes the view to the
885
+ * server metadata store via `client.meta.saveItem`, then invalidates
886
+ * the matching cache entry so the next {@link getView} reflects the
887
+ * new payload. Returns the persisted item when the server echoes it,
888
+ * otherwise undefined.
889
+ *
890
+ * Used by ObjectView for "live" toolbar persistence (density,
891
+ * column widths, sort, etc.) and by the View Config Panel for
892
+ * explicit saves.
893
+ *
894
+ * @param objectName - Object name (e.g. 'lead')
895
+ * @param viewId - View identifier (e.g. 'all_leads')
896
+ * @param config - Full view definition to persist
897
+ */
898
+ updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
852
899
  /**
853
900
  * Get an application definition by name or ID.
854
901
  * Attempts to fetch from the server metadata API.
@@ -865,6 +912,19 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
865
912
  * Returns null if the server doesn't support page metadata.
866
913
  */
867
914
  getPage(pageId: string): Promise<unknown | null>;
915
+ /**
916
+ * Update (upsert) a dashboard definition.
917
+ *
918
+ * Dashboards are control-plane metadata, not data records. Persist via
919
+ * `client.meta.saveItem('dashboard', name, schema)` which routes to
920
+ * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the
921
+ * relevant metadata cache entry so the next dashboard read reflects
922
+ * the new payload.
923
+ *
924
+ * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard')
925
+ * @param schema - Full dashboard schema (widgets, layout, etc.)
926
+ */
927
+ updateDashboard(dashboardName: string, schema: Record<string, any>): Promise<Record<string, any> | void>;
868
928
  /**
869
929
  * Perform server-side aggregation via the ObjectStack analytics API.
870
930
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
@@ -966,4 +1026,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
966
1026
  reconnectDelay?: number;
967
1027
  }): DataSource<T>;
968
1028
 
969
- export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MetadataNotFoundError, ObjectStackAdapter, ObjectStackError, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, generateContractManifest, getSharedDiscovery, isErrorType, isObjectStackError, snapToGrid, validatePluginContract };
1029
+ export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MetadataNotFoundError, ObjectStackAdapter, ObjectStackError, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, generateContractManifest, getSharedDiscovery, is404Error, isErrorType, isObjectStackError, snapToGrid, validatePluginContract };
package/dist/index.d.ts CHANGED
@@ -648,6 +648,16 @@ declare function calculateAutoLayout(items: Array<{
648
648
  declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unknown>): Promise<unknown>;
649
649
  /** Test/dev helper to drop the cache (e.g. on logout or origin change). */
650
650
  declare function clearSharedDiscoveryCache(): void;
651
+ /**
652
+ * Detect "missing resource" errors regardless of where they originate.
653
+ *
654
+ * The ObjectStack client decorates thrown errors with `httpStatus` (and a
655
+ * machine-readable `code` such as `object_not_found`/`record_not_found`),
656
+ * while raw `fetch()` callers may surface `status` or `statusCode`. Treat
657
+ * any of these as a 404 so callers can degrade gracefully instead of
658
+ * tripping on the property-name mismatch.
659
+ */
660
+ declare function is404Error(error: unknown): boolean;
651
661
  /**
652
662
  * Connection state for monitoring
653
663
  */
@@ -838,6 +848,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
838
848
  * @returns Promise resolving to discovery data, or null if not connected
839
849
  */
840
850
  getDiscovery(): Promise<unknown | null>;
851
+ /**
852
+ * Batch-fetch all persisted view overrides for an object.
853
+ *
854
+ * Per-view runtime overrides (density, column widths, sort, …) are
855
+ * stored in the metadata registry under key `<objectName>/<viewName>`.
856
+ * Loading them per-view fires N HTTP GETs that return 404 for views
857
+ * the user has never customized — generates console noise on every
858
+ * page load. This batch method performs a single
859
+ * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
860
+ * returns a `{viewName: override}` map. Returns an empty map if the
861
+ * server doesn't expose the listing or returns no items.
862
+ *
863
+ * Result is cached identically to {@link getView}; saving a view via
864
+ * {@link updateViewConfig} invalidates the cache.
865
+ *
866
+ * @param objectName - Object name (e.g. 'lead')
867
+ * @returns Map keyed by view name with the persisted override config
868
+ */
869
+ listViewOverrides(objectName: string): Promise<Record<string, any>>;
841
870
  /**
842
871
  * Get a view definition for an object.
843
872
  * Attempts to fetch from the server metadata API.
@@ -849,6 +878,24 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
849
878
  * @returns Promise resolving to the view definition or null
850
879
  */
851
880
  getView(objectName: string, viewId: string): Promise<unknown | null>;
881
+ /**
882
+ * Persist a view definition for an object.
883
+ *
884
+ * Symmetric counterpart to {@link getView}: writes the view to the
885
+ * server metadata store via `client.meta.saveItem`, then invalidates
886
+ * the matching cache entry so the next {@link getView} reflects the
887
+ * new payload. Returns the persisted item when the server echoes it,
888
+ * otherwise undefined.
889
+ *
890
+ * Used by ObjectView for "live" toolbar persistence (density,
891
+ * column widths, sort, etc.) and by the View Config Panel for
892
+ * explicit saves.
893
+ *
894
+ * @param objectName - Object name (e.g. 'lead')
895
+ * @param viewId - View identifier (e.g. 'all_leads')
896
+ * @param config - Full view definition to persist
897
+ */
898
+ updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
852
899
  /**
853
900
  * Get an application definition by name or ID.
854
901
  * Attempts to fetch from the server metadata API.
@@ -865,6 +912,19 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
865
912
  * Returns null if the server doesn't support page metadata.
866
913
  */
867
914
  getPage(pageId: string): Promise<unknown | null>;
915
+ /**
916
+ * Update (upsert) a dashboard definition.
917
+ *
918
+ * Dashboards are control-plane metadata, not data records. Persist via
919
+ * `client.meta.saveItem('dashboard', name, schema)` which routes to
920
+ * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the
921
+ * relevant metadata cache entry so the next dashboard read reflects
922
+ * the new payload.
923
+ *
924
+ * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard')
925
+ * @param schema - Full dashboard schema (widgets, layout, etc.)
926
+ */
927
+ updateDashboard(dashboardName: string, schema: Record<string, any>): Promise<Record<string, any> | void>;
868
928
  /**
869
929
  * Perform server-side aggregation via the ObjectStack analytics API.
870
930
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
@@ -966,4 +1026,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
966
1026
  reconnectDelay?: number;
967
1027
  }): DataSource<T>;
968
1028
 
969
- export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MetadataNotFoundError, ObjectStackAdapter, ObjectStackError, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, generateContractManifest, getSharedDiscovery, isErrorType, isObjectStackError, snapToGrid, validatePluginContract };
1029
+ export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, MetadataNotFoundError, ObjectStackAdapter, ObjectStackError, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, generateContractManifest, getSharedDiscovery, is404Error, isErrorType, isObjectStackError, snapToGrid, validatePluginContract };
package/dist/index.js CHANGED
@@ -853,6 +853,44 @@ async function getSharedDiscovery(baseUrl, fetcher) {
853
853
  function clearSharedDiscoveryCache() {
854
854
  discoveryCache.clear();
855
855
  }
856
+ function is404Error(error) {
857
+ if (!error || typeof error !== "object") return false;
858
+ const err = error;
859
+ if (err.httpStatus === 404 || err.status === 404 || err.statusCode === 404) {
860
+ return true;
861
+ }
862
+ const code = typeof err.code === "string" ? err.code : "";
863
+ return code === "object_not_found" || code === "record_not_found";
864
+ }
865
+ function createQuietHttpLogger() {
866
+ const isExpected404 = (meta) => {
867
+ if (!meta || typeof meta !== "object") return false;
868
+ if (meta.status === 404 || meta.statusCode === 404) return true;
869
+ const errBody = meta.error;
870
+ if (errBody && typeof errBody === "object") {
871
+ const code = errBody.code;
872
+ if (code === "object_not_found" || code === "record_not_found") return true;
873
+ }
874
+ return false;
875
+ };
876
+ const logger = {
877
+ debug: (message, meta) => console.debug(message, meta ?? ""),
878
+ info: (message, meta) => console.info(message, meta ?? ""),
879
+ warn: (message, meta) => console.warn(message, meta ?? ""),
880
+ error: (message, error, meta) => {
881
+ if (isExpected404(meta)) {
882
+ console.debug(`[ObjectStack] ${message} (suppressed expected 404)`, meta);
883
+ return;
884
+ }
885
+ console.error(message, error ?? "", meta ?? "");
886
+ },
887
+ fatal: (message, error, meta) => console.error(message, error ?? "", meta ?? ""),
888
+ log: (message, ...args) => console.log(message, ...args),
889
+ child: () => logger,
890
+ withTrace: () => logger
891
+ };
892
+ return logger;
893
+ }
856
894
  function stableStringify(value) {
857
895
  if (value === null || typeof value !== "object") return JSON.stringify(value);
858
896
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
@@ -881,7 +919,7 @@ var ObjectStackAdapter = class {
881
919
  // multiple sibling components requesting the same dataset on first paint)
882
920
  // into a single network round trip.
883
921
  __publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
884
- this.client = new ObjectStackClient(config);
922
+ this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
885
923
  this.metadataCache = new MetadataCache(config.cache);
886
924
  this.autoReconnect = config.autoReconnect ?? true;
887
925
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
@@ -1032,11 +1070,12 @@ var ObjectStackAdapter = class {
1032
1070
  return this.normalizeQueryResult(result, params);
1033
1071
  })();
1034
1072
  this.inflightFinds.set(key, promise);
1035
- promise.finally(() => {
1073
+ const cleanup = () => {
1036
1074
  if (this.inflightFinds.get(key) === promise) {
1037
1075
  this.inflightFinds.delete(key);
1038
1076
  }
1039
- });
1077
+ };
1078
+ promise.then(cleanup, cleanup);
1040
1079
  return promise;
1041
1080
  }
1042
1081
  /**
@@ -1059,7 +1098,7 @@ var ObjectStackAdapter = class {
1059
1098
  const records = resultObj.records || resultObj.value || [];
1060
1099
  return records[0] || null;
1061
1100
  } catch (error) {
1062
- if (error?.status === 404) {
1101
+ if (is404Error(error)) {
1063
1102
  return null;
1064
1103
  }
1065
1104
  }
@@ -1068,7 +1107,7 @@ var ObjectStackAdapter = class {
1068
1107
  const result = await this.client.data.get(resource, String(id));
1069
1108
  return result.record;
1070
1109
  } catch (error) {
1071
- if (error?.status === 404) {
1110
+ if (is404Error(error)) {
1072
1111
  return null;
1073
1112
  }
1074
1113
  throw error;
@@ -1392,7 +1431,7 @@ var ObjectStackAdapter = class {
1392
1431
  return schema;
1393
1432
  } catch (error) {
1394
1433
  const errorObj = error;
1395
- if (errorObj?.status === 404 || errorObj?.statusCode === 404) {
1434
+ if (is404Error(errorObj)) {
1396
1435
  throw new MetadataNotFoundError(objectName, { originalError: error });
1397
1436
  }
1398
1437
  if (error instanceof ObjectStackError) {
@@ -1424,6 +1463,43 @@ var ObjectStackAdapter = class {
1424
1463
  return null;
1425
1464
  }
1426
1465
  }
1466
+ /**
1467
+ * Batch-fetch all persisted view overrides for an object.
1468
+ *
1469
+ * Per-view runtime overrides (density, column widths, sort, …) are
1470
+ * stored in the metadata registry under key `<objectName>/<viewName>`.
1471
+ * Loading them per-view fires N HTTP GETs that return 404 for views
1472
+ * the user has never customized — generates console noise on every
1473
+ * page load. This batch method performs a single
1474
+ * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
1475
+ * returns a `{viewName: override}` map. Returns an empty map if the
1476
+ * server doesn't expose the listing or returns no items.
1477
+ *
1478
+ * Result is cached identically to {@link getView}; saving a view via
1479
+ * {@link updateViewConfig} invalidates the cache.
1480
+ *
1481
+ * @param objectName - Object name (e.g. 'lead')
1482
+ * @returns Map keyed by view name with the persisted override config
1483
+ */
1484
+ async listViewOverrides(objectName) {
1485
+ await this.connect();
1486
+ try {
1487
+ const cacheKey = `view-overrides:${objectName}`;
1488
+ return await this.metadataCache.get(cacheKey, async () => {
1489
+ const result = await this.client.meta.getItems(objectName);
1490
+ const items = Array.isArray(result?.items) ? result.items : [];
1491
+ const out = {};
1492
+ for (const it of items) {
1493
+ if (!it || typeof it !== "object") continue;
1494
+ const key = it.name ?? it.id ?? it._name;
1495
+ if (typeof key === "string" && key) out[key] = it;
1496
+ }
1497
+ return out;
1498
+ });
1499
+ } catch {
1500
+ return {};
1501
+ }
1502
+ }
1427
1503
  /**
1428
1504
  * Get a view definition for an object.
1429
1505
  * Attempts to fetch from the server metadata API.
@@ -1439,7 +1515,7 @@ var ObjectStackAdapter = class {
1439
1515
  try {
1440
1516
  const cacheKey = `view:${objectName}:${viewId}`;
1441
1517
  return await this.metadataCache.get(cacheKey, async () => {
1442
- const result = await this.client.meta.getItem(objectName, `views/${viewId}`);
1518
+ const result = await this.client.meta.getItem(objectName, viewId);
1443
1519
  if (result && result.item) return result.item;
1444
1520
  return result ?? null;
1445
1521
  });
@@ -1447,6 +1523,40 @@ var ObjectStackAdapter = class {
1447
1523
  return null;
1448
1524
  }
1449
1525
  }
1526
+ /**
1527
+ * Persist a view definition for an object.
1528
+ *
1529
+ * Symmetric counterpart to {@link getView}: writes the view to the
1530
+ * server metadata store via `client.meta.saveItem`, then invalidates
1531
+ * the matching cache entry so the next {@link getView} reflects the
1532
+ * new payload. Returns the persisted item when the server echoes it,
1533
+ * otherwise undefined.
1534
+ *
1535
+ * Used by ObjectView for "live" toolbar persistence (density,
1536
+ * column widths, sort, etc.) and by the View Config Panel for
1537
+ * explicit saves.
1538
+ *
1539
+ * @param objectName - Object name (e.g. 'lead')
1540
+ * @param viewId - View identifier (e.g. 'all_leads')
1541
+ * @param config - Full view definition to persist
1542
+ */
1543
+ async updateViewConfig(objectName, viewId, config) {
1544
+ await this.connect();
1545
+ try {
1546
+ const result = await this.client.meta.saveItem(
1547
+ objectName,
1548
+ viewId,
1549
+ config
1550
+ );
1551
+ const cacheKey = `view:${objectName}:${viewId}`;
1552
+ this.metadataCache.invalidate?.(cacheKey);
1553
+ this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1554
+ if (result && result.item) return result.item;
1555
+ return result ?? void 0;
1556
+ } catch (err) {
1557
+ throw err;
1558
+ }
1559
+ }
1450
1560
  /**
1451
1561
  * Get an application definition by name or ID.
1452
1562
  * Attempts to fetch from the server metadata API.
@@ -1487,6 +1597,34 @@ var ObjectStackAdapter = class {
1487
1597
  return null;
1488
1598
  }
1489
1599
  }
1600
+ /**
1601
+ * Update (upsert) a dashboard definition.
1602
+ *
1603
+ * Dashboards are control-plane metadata, not data records. Persist via
1604
+ * `client.meta.saveItem('dashboard', name, schema)` which routes to
1605
+ * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the
1606
+ * relevant metadata cache entry so the next dashboard read reflects
1607
+ * the new payload.
1608
+ *
1609
+ * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard')
1610
+ * @param schema - Full dashboard schema (widgets, layout, etc.)
1611
+ */
1612
+ async updateDashboard(dashboardName, schema) {
1613
+ await this.connect();
1614
+ try {
1615
+ const result = await this.client.meta.saveItem(
1616
+ "dashboard",
1617
+ dashboardName,
1618
+ schema
1619
+ );
1620
+ this.metadataCache.invalidate?.("dashboards");
1621
+ this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
1622
+ if (result && result.item) return result.item;
1623
+ return result ?? void 0;
1624
+ } catch (err) {
1625
+ throw err;
1626
+ }
1627
+ }
1490
1628
  /**
1491
1629
  * Perform server-side aggregation via the ObjectStack analytics API.
1492
1630
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
@@ -1505,7 +1643,7 @@ var ObjectStackAdapter = class {
1505
1643
  dimensions: params.groupBy && params.groupBy !== "_all" ? [params.groupBy] : []
1506
1644
  };
1507
1645
  if (params.filter) {
1508
- payload.filters = params.filter;
1646
+ payload.where = params.filter;
1509
1647
  }
1510
1648
  const data = await this.client.analytics.query(payload);
1511
1649
  const rawRows = Array.isArray(data) ? data : data?.rows && Array.isArray(data.rows) ? data.rows : data?.data && Array.isArray(data.data) ? data.data : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows : data?.results && Array.isArray(data.results) ? data.results : [];
@@ -1516,7 +1654,7 @@ var ObjectStackAdapter = class {
1516
1654
  return true;
1517
1655
  });
1518
1656
  if (measureMissing) {
1519
- const result = await this.find(resource);
1657
+ const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
1520
1658
  const records = result.data || [];
1521
1659
  if (records.length === 0) return [];
1522
1660
  return this.aggregateClientSide(records, params);
@@ -1530,7 +1668,7 @@ var ObjectStackAdapter = class {
1530
1668
  return mapped;
1531
1669
  });
1532
1670
  } catch {
1533
- const result = await this.find(resource);
1671
+ const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
1534
1672
  const records = result.data || [];
1535
1673
  if (records.length === 0) return [];
1536
1674
  return this.aggregateClientSide(records, params);
@@ -1728,6 +1866,7 @@ export {
1728
1866
  createObjectStackAdapter,
1729
1867
  generateContractManifest,
1730
1868
  getSharedDiscovery,
1869
+ is404Error,
1731
1870
  isErrorType,
1732
1871
  isObjectStackError,
1733
1872
  snapToGrid,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,14 +22,14 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@objectstack/client": "^4.0.4",
26
- "@object-ui/core": "4.0.7",
27
- "@object-ui/types": "4.0.7"
25
+ "@objectstack/client": "^4.0.5",
26
+ "@object-ui/core": "4.0.9",
27
+ "@object-ui/types": "4.0.9"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
31
31
  "typescript": "^6.0.3",
32
- "vitest": "^4.1.5"
32
+ "vitest": "^4.1.6"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
package/src/index.ts CHANGED
@@ -110,6 +110,70 @@ export function clearSharedDiscoveryCache(): void {
110
110
  discoveryCache.clear();
111
111
  }
112
112
 
113
+ /**
114
+ * Detect "missing resource" errors regardless of where they originate.
115
+ *
116
+ * The ObjectStack client decorates thrown errors with `httpStatus` (and a
117
+ * machine-readable `code` such as `object_not_found`/`record_not_found`),
118
+ * while raw `fetch()` callers may surface `status` or `statusCode`. Treat
119
+ * any of these as a 404 so callers can degrade gracefully instead of
120
+ * tripping on the property-name mismatch.
121
+ */
122
+ export function is404Error(error: unknown): boolean {
123
+ if (!error || typeof error !== 'object') return false;
124
+ const err = error as Record<string, unknown>;
125
+ if (err.httpStatus === 404 || err.status === 404 || err.statusCode === 404) {
126
+ return true;
127
+ }
128
+ const code = typeof err.code === 'string' ? err.code : '';
129
+ return code === 'object_not_found' || code === 'record_not_found';
130
+ }
131
+
132
+ /**
133
+ * Build a Logger compatible with @objectstack/client that demotes expected
134
+ * 404 noise to console.debug. The client logs every non-2xx response with
135
+ * `logger.error("HTTP request failed", undefined, { status, error })`, but
136
+ * 404s on optional collections (sys_presence, sys_activity, …) are part of
137
+ * normal degraded operation when those plugins aren't installed on the
138
+ * server — they should not surface as errors in the browser DevTools.
139
+ *
140
+ * Returned object is loosely typed because the spec's Logger interface lives
141
+ * in a transitive package; using `any` keeps us decoupled.
142
+ */
143
+ function createQuietHttpLogger(): any {
144
+ const isExpected404 = (meta?: Record<string, any>): boolean => {
145
+ if (!meta || typeof meta !== 'object') return false;
146
+ if (meta.status === 404 || meta.statusCode === 404) return true;
147
+ const errBody = meta.error;
148
+ if (errBody && typeof errBody === 'object') {
149
+ const code = (errBody as Record<string, unknown>).code;
150
+ if (code === 'object_not_found' || code === 'record_not_found') return true;
151
+ }
152
+ return false;
153
+ };
154
+ const logger: any = {
155
+ debug: (message: string, meta?: Record<string, any>) =>
156
+ console.debug(message, meta ?? ''),
157
+ info: (message: string, meta?: Record<string, any>) =>
158
+ console.info(message, meta ?? ''),
159
+ warn: (message: string, meta?: Record<string, any>) =>
160
+ console.warn(message, meta ?? ''),
161
+ error: (message: string, error?: Error, meta?: Record<string, any>) => {
162
+ if (isExpected404(meta)) {
163
+ console.debug(`[ObjectStack] ${message} (suppressed expected 404)`, meta);
164
+ return;
165
+ }
166
+ console.error(message, error ?? '', meta ?? '');
167
+ },
168
+ fatal: (message: string, error?: Error, meta?: Record<string, any>) =>
169
+ console.error(message, error ?? '', meta ?? ''),
170
+ log: (message: string, ...args: any[]) => console.log(message, ...args),
171
+ child: () => logger,
172
+ withTrace: () => logger,
173
+ };
174
+ return logger;
175
+ }
176
+
113
177
  /**
114
178
  * Connection state for monitoring
115
179
  */
@@ -224,7 +288,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
224
288
  maxReconnectAttempts?: number;
225
289
  reconnectDelay?: number;
226
290
  }) {
227
- this.client = new ObjectStackClient(config);
291
+ // Inject a quiet logger that demotes expected 404s ("HTTP request failed"
292
+ // from probing optional collections like sys_presence/sys_activity) to
293
+ // debug() so they don't pollute the browser console. Other log levels are
294
+ // forwarded to the standard console.
295
+ this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
228
296
  this.metadataCache = new MetadataCache(config.cache);
229
297
  this.autoReconnect = config.autoReconnect ?? true;
230
298
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
@@ -426,13 +494,19 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
426
494
  })();
427
495
 
428
496
  this.inflightFinds.set(key, promise);
429
- promise.finally(() => {
497
+ // Use `.then(cleanup, cleanup)` instead of `.finally(cleanup)`. `.finally`
498
+ // returns a new chained promise that re-raises the rejection, and because
499
+ // we don't return that chain, Node/browsers see it as an unhandled
500
+ // rejection — flooding DevTools when callers handle the original `promise`
501
+ // via `.catch()` (e.g. AppHeader probing optional sys_presence/sys_activity).
502
+ const cleanup = () => {
430
503
  // Only clear if the entry still points at this promise; a later call
431
504
  // that started after settle may have already replaced it.
432
505
  if (this.inflightFinds.get(key) === promise) {
433
506
  this.inflightFinds.delete(key);
434
507
  }
435
- });
508
+ };
509
+ promise.then(cleanup, cleanup);
436
510
  return promise;
437
511
  }
438
512
 
@@ -461,7 +535,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
461
535
  const records = resultObj.records || resultObj.value || [];
462
536
  return records[0] || null;
463
537
  } catch (error: unknown) {
464
- if ((error as Record<string, unknown>)?.status === 404) {
538
+ if (is404Error(error)) {
465
539
  return null;
466
540
  }
467
541
  // Fall through to direct GET without $expand — some servers don't
@@ -475,7 +549,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
475
549
  return result.record;
476
550
  } catch (error: unknown) {
477
551
  // If record not found, return null instead of throwing
478
- if ((error as Record<string, unknown>)?.status === 404) {
552
+ if (is404Error(error)) {
479
553
  return null;
480
554
  }
481
555
  throw error;
@@ -908,7 +982,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
908
982
  } catch (error: unknown) {
909
983
  // Check if it's a 404 error
910
984
  const errorObj = error as Record<string, unknown>;
911
- if (errorObj?.status === 404 || errorObj?.statusCode === 404) {
985
+ if (is404Error(errorObj)) {
912
986
  throw new MetadataNotFoundError(objectName, { originalError: error });
913
987
  }
914
988
 
@@ -952,6 +1026,45 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
952
1026
  }
953
1027
  }
954
1028
 
1029
+ /**
1030
+ * Batch-fetch all persisted view overrides for an object.
1031
+ *
1032
+ * Per-view runtime overrides (density, column widths, sort, …) are
1033
+ * stored in the metadata registry under key `<objectName>/<viewName>`.
1034
+ * Loading them per-view fires N HTTP GETs that return 404 for views
1035
+ * the user has never customized — generates console noise on every
1036
+ * page load. This batch method performs a single
1037
+ * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
1038
+ * returns a `{viewName: override}` map. Returns an empty map if the
1039
+ * server doesn't expose the listing or returns no items.
1040
+ *
1041
+ * Result is cached identically to {@link getView}; saving a view via
1042
+ * {@link updateViewConfig} invalidates the cache.
1043
+ *
1044
+ * @param objectName - Object name (e.g. 'lead')
1045
+ * @returns Map keyed by view name with the persisted override config
1046
+ */
1047
+ async listViewOverrides(objectName: string): Promise<Record<string, any>> {
1048
+ await this.connect();
1049
+
1050
+ try {
1051
+ const cacheKey = `view-overrides:${objectName}`;
1052
+ return await this.metadataCache.get(cacheKey, async () => {
1053
+ const result: any = await this.client.meta.getItems(objectName);
1054
+ const items: any[] = Array.isArray(result?.items) ? result.items : [];
1055
+ const out: Record<string, any> = {};
1056
+ for (const it of items) {
1057
+ if (!it || typeof it !== 'object') continue;
1058
+ const key = it.name ?? it.id ?? it._name;
1059
+ if (typeof key === 'string' && key) out[key] = it;
1060
+ }
1061
+ return out;
1062
+ });
1063
+ } catch {
1064
+ return {};
1065
+ }
1066
+ }
1067
+
955
1068
  /**
956
1069
  * Get a view definition for an object.
957
1070
  * Attempts to fetch from the server metadata API.
@@ -969,7 +1082,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
969
1082
  const cacheKey = `view:${objectName}:${viewId}`;
970
1083
  return await this.metadataCache.get(cacheKey, async () => {
971
1084
  // Try meta.getItem for view metadata
972
- const result: any = await this.client.meta.getItem(objectName, `views/${viewId}`);
1085
+ const result: any = await this.client.meta.getItem(objectName, viewId);
973
1086
  if (result && result.item) return result.item;
974
1087
  return result ?? null;
975
1088
  });
@@ -979,6 +1092,50 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
979
1092
  }
980
1093
  }
981
1094
 
1095
+ /**
1096
+ * Persist a view definition for an object.
1097
+ *
1098
+ * Symmetric counterpart to {@link getView}: writes the view to the
1099
+ * server metadata store via `client.meta.saveItem`, then invalidates
1100
+ * the matching cache entry so the next {@link getView} reflects the
1101
+ * new payload. Returns the persisted item when the server echoes it,
1102
+ * otherwise undefined.
1103
+ *
1104
+ * Used by ObjectView for "live" toolbar persistence (density,
1105
+ * column widths, sort, etc.) and by the View Config Panel for
1106
+ * explicit saves.
1107
+ *
1108
+ * @param objectName - Object name (e.g. 'lead')
1109
+ * @param viewId - View identifier (e.g. 'all_leads')
1110
+ * @param config - Full view definition to persist
1111
+ */
1112
+ async updateViewConfig(
1113
+ objectName: string,
1114
+ viewId: string,
1115
+ config: Record<string, any>
1116
+ ): Promise<Record<string, any> | void> {
1117
+ await this.connect();
1118
+ try {
1119
+ const result: any = await this.client.meta.saveItem(
1120
+ objectName,
1121
+ viewId,
1122
+ config
1123
+ );
1124
+ // Invalidate cached read so next getView reflects the change
1125
+ const cacheKey = `view:${objectName}:${viewId}`;
1126
+ this.metadataCache.invalidate?.(cacheKey);
1127
+ // Also invalidate the batch override map so listViewOverrides re-fetches
1128
+ this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
1129
+ if (result && result.item) return result.item;
1130
+ return result ?? undefined;
1131
+ } catch (err) {
1132
+ // Surface the error so the caller can decide whether to toast/log;
1133
+ // we don't swallow it here because persistence failures are
1134
+ // operationally meaningful (unlike read fallbacks).
1135
+ throw err;
1136
+ }
1137
+ }
1138
+
982
1139
  /**
983
1140
  * Get an application definition by name or ID.
984
1141
  * Attempts to fetch from the server metadata API.
@@ -1025,6 +1182,40 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1025
1182
  }
1026
1183
  }
1027
1184
 
1185
+ /**
1186
+ * Update (upsert) a dashboard definition.
1187
+ *
1188
+ * Dashboards are control-plane metadata, not data records. Persist via
1189
+ * `client.meta.saveItem('dashboard', name, schema)` which routes to
1190
+ * `PUT /api/v1/meta/dashboard/:name`. After save, invalidates the
1191
+ * relevant metadata cache entry so the next dashboard read reflects
1192
+ * the new payload.
1193
+ *
1194
+ * @param dashboardName - Dashboard identifier (e.g. 'crm_overview_dashboard')
1195
+ * @param schema - Full dashboard schema (widgets, layout, etc.)
1196
+ */
1197
+ async updateDashboard(
1198
+ dashboardName: string,
1199
+ schema: Record<string, any>
1200
+ ): Promise<Record<string, any> | void> {
1201
+ await this.connect();
1202
+ try {
1203
+ const result: any = await this.client.meta.saveItem(
1204
+ 'dashboard',
1205
+ dashboardName,
1206
+ schema
1207
+ );
1208
+ // Invalidate dashboards list and any cached dashboard read so the
1209
+ // next render reflects the change.
1210
+ this.metadataCache.invalidate?.('dashboards');
1211
+ this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
1212
+ if (result && result.item) return result.item;
1213
+ return result ?? undefined;
1214
+ } catch (err) {
1215
+ throw err;
1216
+ }
1217
+ }
1218
+
1028
1219
  /**
1029
1220
  * Perform server-side aggregation via the ObjectStack analytics API.
1030
1221
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
@@ -1051,7 +1242,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1051
1242
  dimensions: params.groupBy && params.groupBy !== '_all' ? [params.groupBy] : [],
1052
1243
  };
1053
1244
  if (params.filter) {
1054
- payload.filters = params.filter;
1245
+ // Dashboard widgets emit MongoDB-style FilterCondition (per
1246
+ // spec/ui/dashboard.zod.ts). Send via the canonical `where`
1247
+ // field of the analytics endpoint, matching the unified Query
1248
+ // DSL (spec/data/query.zod.ts).
1249
+ payload.where = params.filter;
1055
1250
  }
1056
1251
 
1057
1252
  const data = await this.client.analytics.query(payload);
@@ -1074,7 +1269,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1074
1269
  return true;
1075
1270
  });
1076
1271
  if (measureMissing) {
1077
- const result = await this.find(resource as any);
1272
+ const result = await this.find(resource as any, params.filter ? { $filter: params.filter } as any : undefined);
1078
1273
  const records = result.data || [];
1079
1274
  if (records.length === 0) return [];
1080
1275
  return this.aggregateClientSide(records, params);
@@ -1094,8 +1289,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1094
1289
  });
1095
1290
  } catch {
1096
1291
  // If the analytics endpoint is not available, fall back to
1097
- // find() + client-side aggregation
1098
- const result = await this.find(resource as any);
1292
+ // find() + client-side aggregation. Crucially, forward the same
1293
+ // filter so the fallback aggregates over the SAME row set the
1294
+ // server-side analytics query would have — otherwise the KPI
1295
+ // silently sums the whole table and lies to the user.
1296
+ const result = await this.find(resource as any, params.filter ? { $filter: params.filter } as any : undefined);
1099
1297
  const records = result.data || [];
1100
1298
  if (records.length === 0) return [];
1101
1299