@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/CHANGELOG.md +638 -1
- package/README.md +9 -4
- package/dist/index.cjs +417 -91
- package/dist/index.d.cts +294 -21
- package/dist/index.d.ts +294 -21
- package/dist/index.js +412 -90
- package/package.json +5 -4
- package/src/aggregate-capability.test.ts +177 -0
- package/src/cache/MetadataCache.ts +12 -3
- package/src/errors.test.ts +12 -12
- package/src/errors.ts +23 -5
- package/src/exportDownload.test.ts +42 -0
- package/src/filter-entry-translation.test.ts +360 -0
- package/src/filter-operator-ast-parity.test.ts +71 -0
- package/src/index.ts +736 -120
- package/src/metadata-client.ts +17 -3
- package/src/onMutation.test.ts +35 -35
- package/src/onWriteWarning.test.ts +172 -0
- package/src/orderby-serialization.test.ts +115 -0
- package/src/queryDataset.test.ts +50 -1
- package/src/searchAll.test.ts +120 -0
- package/src/security.ts +18 -5
- package/src/spec-symbol-batch6.test.ts +303 -0
- package/src/userState.test.ts +25 -4
- package/src/userState.ts +10 -6
- package/src/validation-error.test.ts +94 -0
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
|
-
|
|
88
|
-
`[ObjectUI]
|
|
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
|
|
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
|
|
936
|
+
var DataApiValidationError = class extends ObjectStackError {
|
|
910
937
|
/**
|
|
911
|
-
* Create a new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
|
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
|
-
|
|
2454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2576
|
-
|
|
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 (
|
|
3380
|
+
if (valueKey in row && row[valueKey] != null) return false;
|
|
3108
3381
|
return true;
|
|
3109
3382
|
});
|
|
3110
3383
|
if (measureMissing) {
|
|
3111
|
-
|
|
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 !==
|
|
3119
|
-
mapped[
|
|
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
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
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, [
|
|
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
|
});
|