@object-ui/data-objectstack 16.1.0 → 17.2.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/dist/index.cjs CHANGED
@@ -22,19 +22,24 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
22
22
  // src/index.ts
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
+ AnalyticsNotInstalledError: () => AnalyticsNotInstalledError,
26
+ AnalyticsQueryRejectedError: () => AnalyticsQueryRejectedError,
25
27
  AuthenticationError: () => AuthenticationError,
26
28
  BulkOperationError: () => BulkOperationError,
27
29
  CloudOperations: () => CloudOperations,
28
30
  ConcurrentUpdateError: () => ConcurrentUpdateError,
29
31
  ConnectionError: () => ConnectionError,
32
+ DataApiValidationError: () => DataApiValidationError,
33
+ FILTER_OPERATOR_ALIASES: () => FILTER_OPERATOR_ALIASES,
30
34
  IntegrationManager: () => IntegrationManager,
35
+ MalformedFilterError: () => MalformedFilterError,
31
36
  MetadataClient: () => MetadataClient,
32
37
  MetadataNotFoundError: () => MetadataNotFoundError,
33
38
  ObjectStackAdapter: () => ObjectStackAdapter,
34
39
  ObjectStackError: () => ObjectStackError,
35
40
  SecurityManager: () => SecurityManager,
36
- ValidationError: () => ValidationError,
37
41
  calculateAutoLayout: () => calculateAutoLayout,
42
+ classifyAnalyticsFailure: () => classifyAnalyticsFailure,
38
43
  clearSharedDiscoveryCache: () => clearSharedDiscoveryCache,
39
44
  createDefaultCanvasConfig: () => createDefaultCanvasConfig,
40
45
  createErrorFromResponse: () => createErrorFromResponse,
@@ -43,18 +48,42 @@ __export(index_exports, {
43
48
  generateContractManifest: () => generateContractManifest,
44
49
  getSharedDiscovery: () => getSharedDiscovery,
45
50
  is404Error: () => is404Error,
51
+ isAnalyticsNotInstalledError: () => isAnalyticsNotInstalledError,
46
52
  isConcurrentUpdateError: () => isConcurrentUpdateError,
47
53
  isErrorType: () => isErrorType,
54
+ isMalformedFilterError: () => isMalformedFilterError,
48
55
  isObjectStackError: () => isObjectStackError,
49
56
  normaliseClientError: () => normaliseClientError,
50
57
  readTransactionalBatchCapability: () => readTransactionalBatchCapability,
58
+ serializeOrderBy: () => serializeOrderBy,
51
59
  snapToGrid: () => snapToGrid,
52
60
  validatePluginContract: () => validatePluginContract
53
61
  });
54
62
  module.exports = __toCommonJS(index_exports);
55
63
  var import_client = require("@objectstack/client");
56
64
 
65
+ // ../types/src/index.ts
66
+ var import_spec = require("@objectstack/spec");
67
+
68
+ // ../types/src/error-code.ts
69
+ function errorCodeIs(err, code) {
70
+ const raw = err?.code;
71
+ if (typeof raw !== "string" || raw.length === 0) return false;
72
+ return raw.toUpperCase() === code.toUpperCase();
73
+ }
74
+ function errorCodeIsAnyOf(err, codes) {
75
+ return codes.some((c) => errorCodeIs(err, c));
76
+ }
77
+
57
78
  // ../core/src/utils/filter-converter.ts
79
+ var FilterOperatorError = class extends Error {
80
+ constructor(message) {
81
+ super(message);
82
+ __publicField(this, "code", "INVALID_FILTER");
83
+ __publicField(this, "httpStatus", 400);
84
+ this.name = "FilterOperatorError";
85
+ }
86
+ };
58
87
  function convertOperatorToAST(operator) {
59
88
  const operatorMap = {
60
89
  "$eq": "=",
@@ -84,11 +113,9 @@ function convertFiltersToAST(filter) {
84
113
  if (typeof value === "object" && !Array.isArray(value)) {
85
114
  for (const [operator, operatorValue] of Object.entries(value)) {
86
115
  if (operator === "$regex") {
87
- console.warn(
88
- `[ObjectUI] Warning: $regex operator is not fully supported. Converting to 'contains' which only supports substring matching, not regex patterns. Field: '${field}', Value: ${JSON.stringify(operatorValue)}. Consider using $contains or $startsWith instead.`
116
+ throw new FilterOperatorError(
117
+ `[ObjectUI] The '$regex' filter operator is not supported. It used to be converted to 'contains', which matches a literal substring rather than a pattern \u2014 a different result, not a degraded one. Field: '${field}', Value: ${JSON.stringify(operatorValue)}. Use $contains, $startsWith or $endsWith.`
89
118
  );
90
- conditions.push([field, "contains", operatorValue]);
91
- continue;
92
119
  }
93
120
  if (operator === "$null") {
94
121
  conditions.push([field, operatorValue ? "is_null" : "is_not_null", true]);
@@ -102,7 +129,7 @@ function convertFiltersToAST(filter) {
102
129
  if (astOperator) {
103
130
  conditions.push([field, astOperator, operatorValue]);
104
131
  } else {
105
- throw new Error(
132
+ throw new FilterOperatorError(
106
133
  `[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains, $startsWith, $endsWith, $null, $exists. If you need exact object matching, use the value directly without an operator.`
107
134
  );
108
135
  }
@@ -906,10 +933,10 @@ var AuthenticationError = class extends ObjectStackError {
906
933
  this.name = "AuthenticationError";
907
934
  }
908
935
  };
909
- var ValidationError = class extends ObjectStackError {
936
+ var DataApiValidationError = class extends ObjectStackError {
910
937
  /**
911
- * Create a new ValidationError
912
- *
938
+ * Create a new DataApiValidationError
939
+ *
913
940
  * @param message - Human-readable error message
914
941
  * @param field - The field that failed validation (optional)
915
942
  * @param validationErrors - Array of validation error details
@@ -960,7 +987,7 @@ function createErrorFromResponse(response, context) {
960
987
  }
961
988
  return new ObjectStackError(message, "NOT_FOUND", 404, details);
962
989
  case 400:
963
- return new ValidationError(message, void 0, response?.data?.errors, details);
990
+ return new DataApiValidationError(message, void 0, response?.data?.errors, details);
964
991
  case 503:
965
992
  return new ConnectionError(message, response?.config?.url, details, 503);
966
993
  case 504:
@@ -1110,10 +1137,9 @@ function createObjectStackUserStateAdapter(options) {
1110
1137
  return [];
1111
1138
  };
1112
1139
  const upsert = async (items) => {
1113
- const now = (/* @__PURE__ */ new Date()).toISOString();
1114
1140
  if (cachedRowId !== null) {
1115
1141
  try {
1116
- await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
1142
+ await dataSource.update(resource, cachedRowId, { value: items });
1117
1143
  return;
1118
1144
  } catch (updateError) {
1119
1145
  cachedRowId = null;
@@ -1123,15 +1149,14 @@ function createObjectStackUserStateAdapter(options) {
1123
1149
  const existing = await findExisting();
1124
1150
  if (existing && existing.id !== void 0 && existing.id !== null) {
1125
1151
  cachedRowId = existing.id;
1126
- await dataSource.update(resource, existing.id, { value: items, updated_at: now });
1152
+ await dataSource.update(resource, existing.id, { value: items });
1127
1153
  return;
1128
1154
  }
1129
1155
  try {
1130
1156
  const created = await dataSource.create(resource, {
1131
1157
  user_id: userId,
1132
1158
  key,
1133
- value: items,
1134
- updated_at: now
1159
+ value: items
1135
1160
  });
1136
1161
  const newId = created?.id;
1137
1162
  if (newId !== void 0 && newId !== null) cachedRowId = newId;
@@ -1139,7 +1164,7 @@ function createObjectStackUserStateAdapter(options) {
1139
1164
  const recovered = await findExisting();
1140
1165
  if (recovered && recovered.id !== void 0 && recovered.id !== null) {
1141
1166
  cachedRowId = recovered.id;
1142
- await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
1167
+ await dataSource.update(resource, recovered.id, { value: items });
1143
1168
  return;
1144
1169
  }
1145
1170
  throw createError;
@@ -1468,38 +1493,107 @@ var FILTER_OPERATOR_ALIASES = {
1468
1493
  is_null: "isnull",
1469
1494
  isnull: "isnull",
1470
1495
  is_not_null: "isnotnull",
1471
- isnotnull: "isnotnull"
1496
+ isnotnull: "isnotnull",
1497
+ // Date comparisons. `before`/`after` are CANONICAL members of the spec's
1498
+ // `VIEW_FILTER_OPERATORS` (ui/view.zod.ts), so a stored view legitimately
1499
+ // carries them — but they are absent from `VALID_AST_OPERATORS`
1500
+ // (data/filter.zod.ts), which gates `isFilterAST()`. Without these two
1501
+ // entries they reached the wire unchanged, the server's AST gate rejected
1502
+ // the shape, and driver-sql skipped the filter ENTIRELY — an unfiltered
1503
+ // result set with no error anywhere. objectstack#3948.
1504
+ before: "<",
1505
+ after: ">"
1472
1506
  };
1473
1507
  function normalizeFilterOperator(op) {
1474
1508
  if (typeof op !== "string") return null;
1475
1509
  const lower = op.toLowerCase();
1476
1510
  return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
1477
1511
  }
1512
+ var MalformedFilterError = class extends Error {
1513
+ constructor(entry, index) {
1514
+ const shown = JSON.stringify(entry) ?? String(entry);
1515
+ super(
1516
+ `Filter entry ${index} is not a usable filter rule (${shown}). Expected { field, operator, value } with a non-empty field.`
1517
+ );
1518
+ __publicField(this, "code", "INVALID_FILTER");
1519
+ __publicField(this, "httpStatus", 400);
1520
+ __publicField(this, "entry");
1521
+ __publicField(this, "index");
1522
+ this.name = "MalformedFilterError";
1523
+ this.entry = entry;
1524
+ this.index = index;
1525
+ }
1526
+ };
1527
+ function isMalformedFilterError(error) {
1528
+ if (!error || typeof error !== "object") return false;
1529
+ const e = error;
1530
+ return e.code === "INVALID_FILTER" || e.name === "MalformedFilterError";
1531
+ }
1478
1532
  function objectFilterEntryToAST(entry) {
1479
1533
  if (!entry || typeof entry !== "object") return null;
1480
- const field = entry.field ?? entry.name;
1534
+ const field = entry.field;
1481
1535
  const rawOp = entry.operator ?? entry.op ?? "=";
1482
1536
  const op = normalizeFilterOperator(rawOp);
1483
1537
  if (!field || !op) return null;
1484
1538
  return [String(field), op, entry.value];
1485
1539
  }
1540
+ function isObjectFilterEntryForm(filter) {
1541
+ const first = filter[0];
1542
+ return filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
1543
+ }
1544
+ function objectFilterEntriesToAST(entries) {
1545
+ const nodes = entries.map((entry, i) => {
1546
+ if (Array.isArray(entry)) return translateFilterChild(entry);
1547
+ const tuple = objectFilterEntryToAST(entry);
1548
+ if (!tuple) throw new MalformedFilterError(entry, i);
1549
+ return tuple;
1550
+ });
1551
+ return nodes.length === 1 ? nodes[0] : ["and", ...nodes];
1552
+ }
1553
+ var LOGICAL_AST_HEADS = /* @__PURE__ */ new Set(["and", "or"]);
1554
+ function translateFilterArray(filter) {
1555
+ if (isObjectFilterEntryForm(filter)) return objectFilterEntriesToAST(filter);
1556
+ const head = filter[0];
1557
+ if (typeof head === "string" && LOGICAL_AST_HEADS.has(head.toLowerCase())) {
1558
+ return [head, ...filter.slice(1).map(translateFilterChild)];
1559
+ }
1560
+ if (filter.every((child) => Array.isArray(child))) return filter.map(translateFilterChild);
1561
+ return filter;
1562
+ }
1563
+ function translateFilterChild(child) {
1564
+ if (Array.isArray(child)) return child.length > 0 ? translateFilterArray(child) : child;
1565
+ if (child && typeof child === "object" && child.field !== void 0) {
1566
+ const tuple = objectFilterEntryToAST(child);
1567
+ if (tuple) return tuple;
1568
+ }
1569
+ return child;
1570
+ }
1486
1571
  function translateFilterToAST(filter) {
1487
1572
  if (filter === void 0 || filter === null) return void 0;
1488
1573
  if (Array.isArray(filter)) {
1489
1574
  if (filter.length === 0) return void 0;
1490
- const first = filter[0];
1491
- const isObjectForm = filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
1492
- if (isObjectForm) {
1493
- const tuples = filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
1494
- if (tuples.length === 0) return void 0;
1495
- if (tuples.length === 1) return tuples[0];
1496
- return ["and", ...tuples];
1497
- }
1498
- return filter;
1575
+ return translateFilterArray(filter);
1499
1576
  }
1500
1577
  if (typeof filter === "object") {
1501
1578
  if (Object.keys(filter).length === 0) return void 0;
1502
- return filter;
1579
+ return convertFiltersToAST(filter);
1580
+ }
1581
+ return void 0;
1582
+ }
1583
+ function serializeOrderBy(orderby) {
1584
+ if (orderby === void 0 || orderby === null) return void 0;
1585
+ if (typeof orderby === "string") {
1586
+ const trimmed = orderby.trim();
1587
+ return trimmed.length > 0 ? trimmed : void 0;
1588
+ }
1589
+ const shorthand = (field, order) => String(order).toLowerCase() === "desc" ? `-${field}` : field;
1590
+ if (Array.isArray(orderby)) {
1591
+ const parts = orderby.map((item) => typeof item === "string" ? item.trim() : shorthand(item.field, item.order)).filter((s) => s.length > 0);
1592
+ return parts.length > 0 ? parts.join(",") : void 0;
1593
+ }
1594
+ if (typeof orderby === "object") {
1595
+ const parts = Object.entries(orderby).map(([field, order]) => shorthand(field, order));
1596
+ return parts.length > 0 ? parts.join(",") : void 0;
1503
1597
  }
1504
1598
  return void 0;
1505
1599
  }
@@ -1535,7 +1629,46 @@ function is404Error(error) {
1535
1629
  return true;
1536
1630
  }
1537
1631
  const code = typeof err.code === "string" ? err.code : "";
1538
- return code === "object_not_found" || code === "record_not_found";
1632
+ return errorCodeIsAnyOf({ code }, ["OBJECT_NOT_FOUND", "RECORD_NOT_FOUND"]);
1633
+ }
1634
+ var AnalyticsNotInstalledError = class extends Error {
1635
+ constructor(surface, detail) {
1636
+ super(
1637
+ `Analytics capability is not installed on this deployment \u2014 ${surface} is unavailable. Install @objectstack/service-analytics and mount AnalyticsServicePlugin to enable it.` + (detail ? ` (server said: ${detail})` : "")
1638
+ );
1639
+ __publicField(this, "code", "ANALYTICS_NOT_INSTALLED");
1640
+ /** The surface that was unavailable, for the message a host renders. */
1641
+ __publicField(this, "surface");
1642
+ this.name = "AnalyticsNotInstalledError";
1643
+ this.surface = surface;
1644
+ }
1645
+ };
1646
+ function isAnalyticsNotInstalledError(error) {
1647
+ if (!error || typeof error !== "object") return false;
1648
+ return error.code === "ANALYTICS_NOT_INSTALLED";
1649
+ }
1650
+ var AnalyticsQueryRejectedError = class extends Error {
1651
+ constructor(detail, serverCode) {
1652
+ super(
1653
+ `Analytics rejected the query: ${detail ?? "the request body does not match the AnalyticsQuery contract"}`
1654
+ );
1655
+ __publicField(this, "code", "ANALYTICS_QUERY_REJECTED");
1656
+ /** The server's own error code (e.g. `VALIDATION_FAILED`), when it sent one. */
1657
+ __publicField(this, "serverCode");
1658
+ this.name = "AnalyticsQueryRejectedError";
1659
+ this.serverCode = serverCode;
1660
+ }
1661
+ };
1662
+ function classifyAnalyticsFailure(error) {
1663
+ const err = error ?? {};
1664
+ const code = typeof err.code === "string" ? err.code : void 0;
1665
+ const message = typeof err.message === "string" ? err.message : void 0;
1666
+ const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
1667
+ if (code === "VALIDATION_FAILED" || status === 400) return { kind: "rejected", code, message };
1668
+ if (status === 404 || status === 501 || code === "NOT_IMPLEMENTED" || code === "ROUTE_NOT_FOUND") {
1669
+ return { kind: "not-installed", code, message };
1670
+ }
1671
+ return { kind: "unknown", code, message };
1539
1672
  }
1540
1673
  var ConcurrentUpdateError = class extends Error {
1541
1674
  constructor(opts) {
@@ -1557,9 +1690,25 @@ function isConcurrentUpdateError(error) {
1557
1690
  function normaliseClientError(error) {
1558
1691
  if (!error || typeof error !== "object") return error;
1559
1692
  const e = error;
1693
+ const details = e.details ?? {};
1694
+ if (e.code === "VALIDATION_FAILED" || e.name === "ValidationError") {
1695
+ const rawFields = Array.isArray(details.fields) ? details.fields : Array.isArray(e.fields) ? e.fields : [];
1696
+ const validationErrors = rawFields.map((f) => {
1697
+ const rec = f ?? {};
1698
+ const field = typeof rec.field === "string" ? rec.field : void 0;
1699
+ if (!field) return null;
1700
+ const message = typeof rec.message === "string" && rec.message.trim() ? rec.message : typeof rec.code === "string" ? rec.code : "";
1701
+ return { field, message };
1702
+ }).filter((x) => x !== null);
1703
+ return new DataApiValidationError(
1704
+ typeof e.message === "string" ? e.message : "Validation failed",
1705
+ validationErrors[0]?.field,
1706
+ validationErrors,
1707
+ { fields: rawFields }
1708
+ );
1709
+ }
1560
1710
  if (e.code !== "CONCURRENT_UPDATE" && e.httpStatus !== 409) return error;
1561
1711
  if (e.code !== "CONCURRENT_UPDATE") return error;
1562
- const details = e.details ?? {};
1563
1712
  return new ConcurrentUpdateError({
1564
1713
  currentVersion: typeof details.currentVersion === "string" ? details.currentVersion : null,
1565
1714
  currentRecord: details.currentRecord ?? null,
@@ -1573,7 +1722,7 @@ function createQuietHttpLogger() {
1573
1722
  const errBody = meta.error;
1574
1723
  if (errBody && typeof errBody === "object") {
1575
1724
  const code = errBody.code;
1576
- if (code === "object_not_found" || code === "record_not_found") return true;
1725
+ if (errorCodeIsAnyOf({ code }, ["OBJECT_NOT_FOUND", "RECORD_NOT_FOUND"])) return true;
1577
1726
  }
1578
1727
  return false;
1579
1728
  };
@@ -1617,6 +1766,8 @@ var ObjectStackAdapter = class {
1617
1766
  __publicField(this, "reconnectAttempts", 0);
1618
1767
  __publicField(this, "baseUrl");
1619
1768
  __publicField(this, "token");
1769
+ /** One "analytics capability is missing" console line per adapter, not per widget. */
1770
+ __publicField(this, "analyticsCapabilityWarned", false);
1620
1771
  __publicField(this, "fetchImpl");
1621
1772
  // In-flight find() requests keyed by resource + serialized params.
1622
1773
  // Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
@@ -1648,6 +1799,10 @@ var ObjectStackAdapter = class {
1648
1799
  // calendar) auto-refresh — the interface ListView relies on to reflect
1649
1800
  // inline-edit "Save All" writes without a manual reload.
1650
1801
  __publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
1802
+ // Subscribers registered via onWriteWarning(). Emitted after a create/update
1803
+ // whose response carried `droppedFields` (framework #3431/#3455) so the app
1804
+ // shell can surface a toast instead of the strip passing silently.
1805
+ __publicField(this, "writeWarningListeners", /* @__PURE__ */ new Set());
1651
1806
  this.client = new import_client.ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
1652
1807
  this.metadataCache = new MetadataCache(config.cache);
1653
1808
  this.autoReconnect = config.autoReconnect ?? true;
@@ -1819,6 +1974,77 @@ var ObjectStackAdapter = class {
1819
1974
  promise.then(cleanup, cleanup);
1820
1975
  return promise;
1821
1976
  }
1977
+ /**
1978
+ * Full-text search across every searchable object in a single round-trip.
1979
+ *
1980
+ * Hits `GET /api/v1/search?q=`, the platform's global search endpoint served
1981
+ * by the registered search service (the pinyin full-text plugin) and backed
1982
+ * by `metadata-protocol`'s `searchAll`. Unlike `find(resource, { $search })`
1983
+ * — a per-object metadata-driven search (ADR-0061) — this consults the search
1984
+ * index and ranks hits across objects, so it surfaces records the per-object
1985
+ * fanout misses. Global affordances (⌘K command palette, search page) prefer
1986
+ * this path (framework #3371).
1987
+ *
1988
+ * Returns `{ query, hits }`. A backend without the search plugin installed
1989
+ * answers `404`; we treat that as "no global search here" and return an empty
1990
+ * hit set so callers can fall back to a per-object fanout rather than surface
1991
+ * an error.
1992
+ */
1993
+ async searchAll(query, options) {
1994
+ const trimmed = (query ?? "").trim();
1995
+ if (trimmed === "") return { query: "", hits: [] };
1996
+ await this.connect();
1997
+ const queryParams = new URLSearchParams();
1998
+ queryParams.set("q", trimmed);
1999
+ if (options?.limit != null && options.limit > 0) {
2000
+ queryParams.set("limit", String(options.limit));
2001
+ }
2002
+ if (options?.objects && options.objects.length > 0) {
2003
+ queryParams.set("objects", options.objects.join(","));
2004
+ }
2005
+ const baseUrl = (this.baseUrl || "").replace(/\/$/, "");
2006
+ const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2007
+ const searchPath = hasApiVersionSuffix ? "/search" : "/api/v1/search";
2008
+ const url = `${baseUrl}${searchPath}?${queryParams.toString()}`;
2009
+ const headers = {
2010
+ "Content-Type": "application/json"
2011
+ };
2012
+ if (this.token) {
2013
+ headers["Authorization"] = `Bearer ${this.token}`;
2014
+ }
2015
+ const res = await this.fetchImpl(url, { method: "GET", headers });
2016
+ if (!res.ok) {
2017
+ if (res.status === 404) return { query: trimmed, hits: [] };
2018
+ const errorBody = await res.json().catch(() => ({ message: res.statusText }));
2019
+ const err = new Error(
2020
+ errorBody?.error?.message || errorBody?.message || res.statusText
2021
+ );
2022
+ err.status = res.status;
2023
+ throw err;
2024
+ }
2025
+ const body = await res.json();
2026
+ const payload = body && typeof body === "object" && typeof body.success === "boolean" && "data" in body ? body.data : body;
2027
+ const rawHits = Array.isArray(payload?.hits) ? payload.hits : Array.isArray(payload) ? payload : [];
2028
+ const hits = [];
2029
+ for (const entry of rawHits) {
2030
+ if (!entry || typeof entry !== "object") continue;
2031
+ const h = entry;
2032
+ const object = h.object ?? h.objectName ?? h.object_name;
2033
+ const id = h.id ?? h.record?.id ?? h.record?._id;
2034
+ if (typeof object !== "string" || id == null) continue;
2035
+ hits.push({
2036
+ object,
2037
+ id: String(id),
2038
+ title: typeof h.title === "string" ? h.title : void 0,
2039
+ snippet: typeof h.snippet === "string" ? h.snippet : void 0,
2040
+ record: h.record && typeof h.record === "object" ? h.record : void 0
2041
+ });
2042
+ }
2043
+ return {
2044
+ query: typeof payload?.query === "string" ? payload.query : trimmed,
2045
+ hits
2046
+ };
2047
+ }
1822
2048
  /**
1823
2049
  * Find a single record by ID.
1824
2050
  */
@@ -1882,11 +2108,84 @@ var ObjectStackAdapter = class {
1882
2108
  this.mutationListeners.delete(callback);
1883
2109
  };
1884
2110
  }
2111
+ /**
2112
+ * Notify all write-warning subscribers. Isolated like {@link emitMutation}: a
2113
+ * throwing listener must not break the write or starve the others.
2114
+ */
2115
+ emitWriteWarning(event) {
2116
+ for (const listener of this.writeWarningListeners) {
2117
+ try {
2118
+ listener(event);
2119
+ } catch (err) {
2120
+ console.warn("ObjectStackAdapter: write-warning listener error", err);
2121
+ }
2122
+ }
2123
+ }
2124
+ /**
2125
+ * Read `droppedFields` off a create/update response (framework #3431/#3455)
2126
+ * and, when present, notify write-warning subscribers. Tolerant of a client
2127
+ * whose response type predates `droppedFields`: the field is read structurally
2128
+ * and validated, so an older client (or a backend that never drops) is a no-op.
2129
+ */
2130
+ notifyDroppedFields(operation, resource, result, id) {
2131
+ const dropped = result?.droppedFields;
2132
+ if (!Array.isArray(dropped) || dropped.length === 0) return;
2133
+ const droppedFields = dropped.filter(
2134
+ (e) => !!e && typeof e === "object" && Array.isArray(e.fields) && e.fields.length > 0
2135
+ );
2136
+ if (droppedFields.length === 0) return;
2137
+ this.emitWriteWarning({ operation, resource, ...id !== void 0 ? { id } : {}, droppedFields });
2138
+ }
2139
+ /**
2140
+ * Same, for the cross-object transactional batch (framework #3794). Its
2141
+ * response hangs the events off a top-level `droppedFields` list, each tagged
2142
+ * with the `index` of the operation it came from — `results` entries are bare
2143
+ * record echoes with nowhere to hang a per-row list.
2144
+ *
2145
+ * This is the path that matters most for the warning: `batchTransaction` is
2146
+ * how the console's record form saves a master-detail record, so a
2147
+ * `readonlyWhen`-locked field edited in that form was stripped server-side
2148
+ * while the UI reported a plain success. The operation kind is taken from the
2149
+ * originating op so the toast doesn't call an update a create.
2150
+ */
2151
+ notifyBatchDroppedFields(operations, payload) {
2152
+ const dropped = payload?.droppedFields;
2153
+ if (!Array.isArray(dropped) || dropped.length === 0) return;
2154
+ for (const entry of dropped) {
2155
+ if (!entry || typeof entry !== "object") continue;
2156
+ const e = entry;
2157
+ if (!Array.isArray(e.fields) || e.fields.length === 0) continue;
2158
+ const op = typeof e.index === "number" ? operations[e.index] : void 0;
2159
+ const operation = (op?.action ?? "create") === "create" ? "create" : "update";
2160
+ this.emitWriteWarning({
2161
+ operation,
2162
+ resource: e.object ?? op?.object ?? "",
2163
+ ...op?.id !== void 0 && op?.id !== null ? { id: op.id } : {},
2164
+ droppedFields: [{ object: e.object, fields: e.fields, reason: e.reason }]
2165
+ });
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Subscribe to write-warning events (a create/update dropped caller-supplied
2170
+ * fields — #3431/#3455). Returns an unsubscribe function. The app shell uses
2171
+ * this to toast the user; the write itself already succeeded.
2172
+ */
2173
+ onWriteWarning(callback) {
2174
+ this.writeWarningListeners.add(callback);
2175
+ return () => {
2176
+ this.writeWarningListeners.delete(callback);
2177
+ };
2178
+ }
1885
2179
  async create(resource, data) {
1886
2180
  await this.connect();
1887
- const result = await this.client.data.create(resource, data);
1888
- this.emitMutation({ type: "create", resource, record: { ...result.record } });
1889
- return result.record;
2181
+ try {
2182
+ const result = await this.client.data.create(resource, data);
2183
+ this.emitMutation({ type: "create", resource, record: { ...result.record } });
2184
+ this.notifyDroppedFields("create", resource, result, result.record?.id);
2185
+ return result.record;
2186
+ } catch (err) {
2187
+ throw normaliseClientError(err);
2188
+ }
1890
2189
  }
1891
2190
  /**
1892
2191
  * Update an existing record.
@@ -1909,6 +2208,7 @@ var ObjectStackAdapter = class {
1909
2208
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
1910
2209
  );
1911
2210
  this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
2211
+ this.notifyDroppedFields("update", resource, result, id);
1912
2212
  return result.record;
1913
2213
  } catch (err) {
1914
2214
  throw normaliseClientError(err);
@@ -2066,6 +2366,7 @@ var ObjectStackAdapter = class {
2066
2366
  try {
2067
2367
  const payload = await this.client.data.batchTransaction(operations);
2068
2368
  this.emitBatchMutations(operations, payload?.results);
2369
+ this.notifyBatchDroppedFields(operations, payload);
2069
2370
  return payload;
2070
2371
  } catch (err) {
2071
2372
  const status = this.errorStatusOf(err);
@@ -2097,7 +2398,7 @@ var ObjectStackAdapter = class {
2097
2398
  return emulateBatchTransaction(this, operations);
2098
2399
  }
2099
2400
  /**
2100
- * Emit one MutationEvent per committed operation so the invalidation bus
2401
+ * Emit one DataSourceMutationEvent per committed operation so the invalidation bus
2101
2402
  * (#2269) sees writes that went through /batch exactly like single
2102
2403
  * create/update/delete calls — master-detail ModalForm saves otherwise leave
2103
2404
  * related lists and count badges stale (#2582). `results` is index-aligned
@@ -2450,20 +2751,8 @@ var ObjectStackAdapter = class {
2450
2751
  const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
2451
2752
  queryParams.set("select", selectFields.join(","));
2452
2753
  }
2453
- if (params.$orderby) {
2454
- if (Array.isArray(params.$orderby)) {
2455
- const sortStr = params.$orderby.map((item) => {
2456
- if (typeof item === "string") return item;
2457
- const field = item.field;
2458
- const order = item.order || "asc";
2459
- return order === "desc" ? `-${field}` : field;
2460
- }).join(",");
2461
- queryParams.set("sort", sortStr);
2462
- } else {
2463
- const sortStr = Object.entries(params.$orderby).map(([field, order]) => order === "desc" ? `-${field}` : field).join(",");
2464
- queryParams.set("sort", sortStr);
2465
- }
2466
- }
2754
+ const sortStr = serializeOrderBy(params.$orderby);
2755
+ if (sortStr) queryParams.set("sort", sortStr);
2467
2756
  if (params.$filter !== void 0 && params.$filter !== null) {
2468
2757
  const translated = translateFilterToAST(params.$filter);
2469
2758
  if (translated !== void 0) {
@@ -2527,6 +2816,13 @@ var ObjectStackAdapter = class {
2527
2816
  queryParams.set("filter", JSON.stringify(translated));
2528
2817
  }
2529
2818
  }
2819
+ const searchTerm = typeof request.search === "string" ? request.search.trim() : "";
2820
+ if (searchTerm) {
2821
+ queryParams.set("search", searchTerm);
2822
+ if (request.searchFields && request.searchFields.length > 0) {
2823
+ queryParams.set("searchFields", request.searchFields.join(","));
2824
+ }
2825
+ }
2530
2826
  const baseUrl = this.baseUrl.replace(/\/$/, "");
2531
2827
  const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2532
2828
  const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
@@ -2555,38 +2851,14 @@ var ObjectStackAdapter = class {
2555
2851
  const isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
2556
2852
  if (!isEmpty) {
2557
2853
  if (Array.isArray(params.$filter)) {
2558
- const isObjectForm = params.$filter.length > 0 && typeof params.$filter[0] === "object" && !Array.isArray(params.$filter[0]) && params.$filter[0].field !== void 0;
2559
- if (isObjectForm) {
2560
- const tuples = params.$filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
2561
- if (tuples.length === 0) {
2562
- } else if (tuples.length === 1) {
2563
- options.filters = tuples[0];
2564
- } else {
2565
- options.filters = ["and", ...tuples];
2566
- }
2567
- } else {
2568
- options.filters = params.$filter;
2569
- }
2854
+ options.filters = translateFilterArray(params.$filter);
2570
2855
  } else {
2571
2856
  options.filters = convertFiltersToAST(params.$filter);
2572
2857
  }
2573
2858
  }
2574
2859
  }
2575
- if (params.$orderby) {
2576
- if (Array.isArray(params.$orderby)) {
2577
- options.sort = params.$orderby.map((item) => {
2578
- if (typeof item === "string") return item;
2579
- const field = item.field;
2580
- const order = item.order || "asc";
2581
- return order === "desc" ? `-${field}` : field;
2582
- });
2583
- } else {
2584
- const sortArray = Object.entries(params.$orderby).map(([field, order]) => {
2585
- return order === "desc" ? `-${field}` : field;
2586
- });
2587
- options.sort = sortArray;
2588
- }
2589
- }
2860
+ const sort = serializeOrderBy(params.$orderby);
2861
+ if (sort) options.sort = sort;
2590
2862
  if (params.$skip !== void 0) {
2591
2863
  options.skip = params.$skip;
2592
2864
  }
@@ -3090,6 +3362,7 @@ var ObjectStackAdapter = class {
3090
3362
  }
3091
3363
  try {
3092
3364
  const measureName = params.function === "count" ? "count" : `${params.field}_${params.function}`;
3365
+ const valueKey = this.aggregateValueKey(params);
3093
3366
  const payload = {
3094
3367
  cube: resource,
3095
3368
  measures: [measureName],
@@ -3104,30 +3377,62 @@ var ObjectStackAdapter = class {
3104
3377
  const measureMissing = rawRows.length > 0 && rawRows.every((row) => {
3105
3378
  if (row == null) return true;
3106
3379
  if (measureName in row && row[measureName] != null) return false;
3107
- if (params.field in row && row[params.field] != null) return false;
3380
+ if (valueKey in row && row[valueKey] != null) return false;
3108
3381
  return true;
3109
3382
  });
3110
3383
  if (measureMissing) {
3111
- const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
3112
- const records = result.data || [];
3113
- if (records.length === 0) return [];
3114
- return this.aggregateClientSide(records, params);
3384
+ return await this.aggregateViaFind(resource, params);
3115
3385
  }
3116
3386
  return rawRows.map((row) => {
3117
3387
  const mapped = { ...row };
3118
- if (measureName !== params.field && measureName in mapped) {
3119
- mapped[params.field] = mapped[measureName];
3388
+ if (measureName !== valueKey && measureName in mapped) {
3389
+ mapped[valueKey] = mapped[measureName];
3120
3390
  delete mapped[measureName];
3121
3391
  }
3122
3392
  return mapped;
3123
3393
  });
3124
- } catch {
3125
- const result = await this.find(resource, params.filter ? { $filter: params.filter } : void 0);
3126
- const records = result.data || [];
3127
- if (records.length === 0) return [];
3128
- return this.aggregateClientSide(records, params);
3394
+ } catch (e) {
3395
+ const failure = classifyAnalyticsFailure(e);
3396
+ if (failure.kind === "rejected") {
3397
+ throw new AnalyticsQueryRejectedError(failure.message, failure.code);
3398
+ }
3399
+ if (failure.kind === "not-installed") {
3400
+ this.warnAnalyticsCapabilityOnce(failure.message);
3401
+ }
3402
+ return await this.aggregateViaFind(resource, params);
3129
3403
  }
3130
3404
  }
3405
+ /**
3406
+ * Client-side aggregation over a server-scoped `find()` — the fallback used
3407
+ * whenever the analytics endpoint cannot answer (capability absent, network
3408
+ * failure, or a result whose measure came back missing).
3409
+ *
3410
+ * Forwarding `params.filter` is load-bearing: without it the fallback
3411
+ * aggregates the whole table while the caller believes it applied a filter,
3412
+ * which is the "KPI silently sums everything" failure this adapter has
3413
+ * guarded against since the widget filter was threaded through.
3414
+ */
3415
+ async aggregateViaFind(resource, params) {
3416
+ const result = await this.find(
3417
+ resource,
3418
+ params.filter ? { $filter: params.filter } : void 0
3419
+ );
3420
+ const records = result.data || [];
3421
+ if (records.length === 0) return [];
3422
+ return this.aggregateClientSide(records, params);
3423
+ }
3424
+ /**
3425
+ * Say "the analytics capability isn't installed" ONCE per adapter, not once
3426
+ * per widget: a dashboard fans out one aggregate() per KPI, and N identical
3427
+ * console lines read like N different failures.
3428
+ */
3429
+ warnAnalyticsCapabilityOnce(detail) {
3430
+ if (this.analyticsCapabilityWarned) return;
3431
+ this.analyticsCapabilityWarned = true;
3432
+ console.warn(
3433
+ "[OBJECTSTACKDataSource] analytics capability unavailable \u2014 aggregating client-side from a scoped find(). Numbers stay correct but the semantic layer (cubes, joins, server-side rollups) is off. Install @objectstack/service-analytics to enable it." + (detail ? ` Server said: ${detail}` : "")
3434
+ );
3435
+ }
3131
3436
  /**
3132
3437
  * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
3133
3438
  *
@@ -3166,6 +3471,9 @@ var ObjectStackAdapter = class {
3166
3471
  detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
3167
3472
  } catch {
3168
3473
  }
3474
+ if (res.status === 501 || res.status === 404) {
3475
+ throw new AnalyticsNotInstalledError("POST /analytics/dataset/query", detail || void 0);
3476
+ }
3169
3477
  throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
3170
3478
  }
3171
3479
  const payload = await res.json();
@@ -3179,8 +3487,18 @@ var ObjectStackAdapter = class {
3179
3487
  return { rows, fields, object, dimensionFields, drillRawRows, totals };
3180
3488
  }
3181
3489
  /** Client-side aggregation fallback */
3490
+ /**
3491
+ * The result column an object-bound `aggregate` projects its value under
3492
+ * (framework#3701, `chartAggregateValueKey` in `@objectstack/spec/ui`): the
3493
+ * raw `field` name — no `sum_`-style decoration, unlike a dataset measure —
3494
+ * or the literal `count` when a count names no field.
3495
+ */
3496
+ aggregateValueKey(params) {
3497
+ return params.field || params.function || "count";
3498
+ }
3182
3499
  aggregateClientSide(records, params) {
3183
3500
  const { field, function: aggFn, groupBy } = params;
3501
+ const valueKey = this.aggregateValueKey(params);
3184
3502
  const groups = {};
3185
3503
  for (const record of records) {
3186
3504
  const key = String(record[groupBy] ?? "Unknown");
@@ -3188,7 +3506,7 @@ var ObjectStackAdapter = class {
3188
3506
  groups[key].push(record);
3189
3507
  }
3190
3508
  return Object.entries(groups).map(([key, group]) => {
3191
- const values = group.map((r) => Number(r[field]) || 0);
3509
+ const values = field ? group.map((r) => Number(r[field]) || 0) : [];
3192
3510
  let result;
3193
3511
  switch (aggFn) {
3194
3512
  case "count":
@@ -3208,7 +3526,7 @@ var ObjectStackAdapter = class {
3208
3526
  result = values.reduce((a, b) => a + b, 0);
3209
3527
  break;
3210
3528
  }
3211
- return { [groupBy]: key, [field]: result };
3529
+ return { [groupBy]: key, [valueKey]: result };
3212
3530
  });
3213
3531
  }
3214
3532
  /**
@@ -3411,19 +3729,24 @@ function createObjectStackAdapter(config) {
3411
3729
  }
3412
3730
  // Annotate the CommonJS export names for ESM import in node:
3413
3731
  0 && (module.exports = {
3732
+ AnalyticsNotInstalledError,
3733
+ AnalyticsQueryRejectedError,
3414
3734
  AuthenticationError,
3415
3735
  BulkOperationError,
3416
3736
  CloudOperations,
3417
3737
  ConcurrentUpdateError,
3418
3738
  ConnectionError,
3739
+ DataApiValidationError,
3740
+ FILTER_OPERATOR_ALIASES,
3419
3741
  IntegrationManager,
3742
+ MalformedFilterError,
3420
3743
  MetadataClient,
3421
3744
  MetadataNotFoundError,
3422
3745
  ObjectStackAdapter,
3423
3746
  ObjectStackError,
3424
3747
  SecurityManager,
3425
- ValidationError,
3426
3748
  calculateAutoLayout,
3749
+ classifyAnalyticsFailure,
3427
3750
  clearSharedDiscoveryCache,
3428
3751
  createDefaultCanvasConfig,
3429
3752
  createErrorFromResponse,
@@ -3432,11 +3755,14 @@ function createObjectStackAdapter(config) {
3432
3755
  generateContractManifest,
3433
3756
  getSharedDiscovery,
3434
3757
  is404Error,
3758
+ isAnalyticsNotInstalledError,
3435
3759
  isConcurrentUpdateError,
3436
3760
  isErrorType,
3761
+ isMalformedFilterError,
3437
3762
  isObjectStackError,
3438
3763
  normaliseClientError,
3439
3764
  readTransactionalBatchCapability,
3765
+ serializeOrderBy,
3440
3766
  snapToGrid,
3441
3767
  validatePluginContract
3442
3768
  });