@object-ui/data-objectstack 16.1.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +513 -1
- package/README.md +3 -3
- package/dist/index.cjs +411 -85
- package/dist/index.d.cts +243 -8
- package/dist/index.d.ts +243 -8
- package/dist/index.js +407 -85
- package/package.json +5 -4
- package/src/aggregate-capability.test.ts +177 -0
- 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 +726 -116
- 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/userState.test.ts +25 -4
- package/src/userState.ts +10 -6
- package/src/validation-error.test.ts +94 -0
package/dist/index.js
CHANGED
|
@@ -5,7 +5,32 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { ObjectStackClient } from "@objectstack/client";
|
|
7
7
|
|
|
8
|
+
// ../types/src/index.ts
|
|
9
|
+
import {
|
|
10
|
+
defineStack,
|
|
11
|
+
ObjectStackSchema,
|
|
12
|
+
ObjectStackDefinitionSchema
|
|
13
|
+
} from "@objectstack/spec";
|
|
14
|
+
|
|
15
|
+
// ../types/src/error-code.ts
|
|
16
|
+
function errorCodeIs(err, code) {
|
|
17
|
+
const raw = err?.code;
|
|
18
|
+
if (typeof raw !== "string" || raw.length === 0) return false;
|
|
19
|
+
return raw.toUpperCase() === code.toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
function errorCodeIsAnyOf(err, codes) {
|
|
22
|
+
return codes.some((c) => errorCodeIs(err, c));
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
// ../core/src/utils/filter-converter.ts
|
|
26
|
+
var FilterOperatorError = class extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
__publicField(this, "code", "INVALID_FILTER");
|
|
30
|
+
__publicField(this, "httpStatus", 400);
|
|
31
|
+
this.name = "FilterOperatorError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
9
34
|
function convertOperatorToAST(operator) {
|
|
10
35
|
const operatorMap = {
|
|
11
36
|
"$eq": "=",
|
|
@@ -35,11 +60,9 @@ function convertFiltersToAST(filter) {
|
|
|
35
60
|
if (typeof value === "object" && !Array.isArray(value)) {
|
|
36
61
|
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
37
62
|
if (operator === "$regex") {
|
|
38
|
-
|
|
39
|
-
`[ObjectUI]
|
|
63
|
+
throw new FilterOperatorError(
|
|
64
|
+
`[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.`
|
|
40
65
|
);
|
|
41
|
-
conditions.push([field, "contains", operatorValue]);
|
|
42
|
-
continue;
|
|
43
66
|
}
|
|
44
67
|
if (operator === "$null") {
|
|
45
68
|
conditions.push([field, operatorValue ? "is_null" : "is_not_null", true]);
|
|
@@ -53,7 +76,7 @@ function convertFiltersToAST(filter) {
|
|
|
53
76
|
if (astOperator) {
|
|
54
77
|
conditions.push([field, astOperator, operatorValue]);
|
|
55
78
|
} else {
|
|
56
|
-
throw new
|
|
79
|
+
throw new FilterOperatorError(
|
|
57
80
|
`[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.`
|
|
58
81
|
);
|
|
59
82
|
}
|
|
@@ -1061,10 +1084,9 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1061
1084
|
return [];
|
|
1062
1085
|
};
|
|
1063
1086
|
const upsert = async (items) => {
|
|
1064
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1065
1087
|
if (cachedRowId !== null) {
|
|
1066
1088
|
try {
|
|
1067
|
-
await dataSource.update(resource, cachedRowId, { value: items
|
|
1089
|
+
await dataSource.update(resource, cachedRowId, { value: items });
|
|
1068
1090
|
return;
|
|
1069
1091
|
} catch (updateError) {
|
|
1070
1092
|
cachedRowId = null;
|
|
@@ -1074,15 +1096,14 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1074
1096
|
const existing = await findExisting();
|
|
1075
1097
|
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
1076
1098
|
cachedRowId = existing.id;
|
|
1077
|
-
await dataSource.update(resource, existing.id, { value: items
|
|
1099
|
+
await dataSource.update(resource, existing.id, { value: items });
|
|
1078
1100
|
return;
|
|
1079
1101
|
}
|
|
1080
1102
|
try {
|
|
1081
1103
|
const created = await dataSource.create(resource, {
|
|
1082
1104
|
user_id: userId,
|
|
1083
1105
|
key,
|
|
1084
|
-
value: items
|
|
1085
|
-
updated_at: now
|
|
1106
|
+
value: items
|
|
1086
1107
|
});
|
|
1087
1108
|
const newId = created?.id;
|
|
1088
1109
|
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
@@ -1090,7 +1111,7 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1090
1111
|
const recovered = await findExisting();
|
|
1091
1112
|
if (recovered && recovered.id !== void 0 && recovered.id !== null) {
|
|
1092
1113
|
cachedRowId = recovered.id;
|
|
1093
|
-
await dataSource.update(resource, recovered.id, { value: items
|
|
1114
|
+
await dataSource.update(resource, recovered.id, { value: items });
|
|
1094
1115
|
return;
|
|
1095
1116
|
}
|
|
1096
1117
|
throw createError;
|
|
@@ -1419,38 +1440,107 @@ var FILTER_OPERATOR_ALIASES = {
|
|
|
1419
1440
|
is_null: "isnull",
|
|
1420
1441
|
isnull: "isnull",
|
|
1421
1442
|
is_not_null: "isnotnull",
|
|
1422
|
-
isnotnull: "isnotnull"
|
|
1443
|
+
isnotnull: "isnotnull",
|
|
1444
|
+
// Date comparisons. `before`/`after` are CANONICAL members of the spec's
|
|
1445
|
+
// `VIEW_FILTER_OPERATORS` (ui/view.zod.ts), so a stored view legitimately
|
|
1446
|
+
// carries them — but they are absent from `VALID_AST_OPERATORS`
|
|
1447
|
+
// (data/filter.zod.ts), which gates `isFilterAST()`. Without these two
|
|
1448
|
+
// entries they reached the wire unchanged, the server's AST gate rejected
|
|
1449
|
+
// the shape, and driver-sql skipped the filter ENTIRELY — an unfiltered
|
|
1450
|
+
// result set with no error anywhere. objectstack#3948.
|
|
1451
|
+
before: "<",
|
|
1452
|
+
after: ">"
|
|
1423
1453
|
};
|
|
1424
1454
|
function normalizeFilterOperator(op) {
|
|
1425
1455
|
if (typeof op !== "string") return null;
|
|
1426
1456
|
const lower = op.toLowerCase();
|
|
1427
1457
|
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
1428
1458
|
}
|
|
1459
|
+
var MalformedFilterError = class extends Error {
|
|
1460
|
+
constructor(entry, index) {
|
|
1461
|
+
const shown = JSON.stringify(entry) ?? String(entry);
|
|
1462
|
+
super(
|
|
1463
|
+
`Filter entry ${index} is not a usable filter rule (${shown}). Expected { field, operator, value } with a non-empty field.`
|
|
1464
|
+
);
|
|
1465
|
+
__publicField(this, "code", "INVALID_FILTER");
|
|
1466
|
+
__publicField(this, "httpStatus", 400);
|
|
1467
|
+
__publicField(this, "entry");
|
|
1468
|
+
__publicField(this, "index");
|
|
1469
|
+
this.name = "MalformedFilterError";
|
|
1470
|
+
this.entry = entry;
|
|
1471
|
+
this.index = index;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
function isMalformedFilterError(error) {
|
|
1475
|
+
if (!error || typeof error !== "object") return false;
|
|
1476
|
+
const e = error;
|
|
1477
|
+
return e.code === "INVALID_FILTER" || e.name === "MalformedFilterError";
|
|
1478
|
+
}
|
|
1429
1479
|
function objectFilterEntryToAST(entry) {
|
|
1430
1480
|
if (!entry || typeof entry !== "object") return null;
|
|
1431
|
-
const field = entry.field
|
|
1481
|
+
const field = entry.field;
|
|
1432
1482
|
const rawOp = entry.operator ?? entry.op ?? "=";
|
|
1433
1483
|
const op = normalizeFilterOperator(rawOp);
|
|
1434
1484
|
if (!field || !op) return null;
|
|
1435
1485
|
return [String(field), op, entry.value];
|
|
1436
1486
|
}
|
|
1487
|
+
function isObjectFilterEntryForm(filter) {
|
|
1488
|
+
const first = filter[0];
|
|
1489
|
+
return filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
|
|
1490
|
+
}
|
|
1491
|
+
function objectFilterEntriesToAST(entries) {
|
|
1492
|
+
const nodes = entries.map((entry, i) => {
|
|
1493
|
+
if (Array.isArray(entry)) return translateFilterChild(entry);
|
|
1494
|
+
const tuple = objectFilterEntryToAST(entry);
|
|
1495
|
+
if (!tuple) throw new MalformedFilterError(entry, i);
|
|
1496
|
+
return tuple;
|
|
1497
|
+
});
|
|
1498
|
+
return nodes.length === 1 ? nodes[0] : ["and", ...nodes];
|
|
1499
|
+
}
|
|
1500
|
+
var LOGICAL_AST_HEADS = /* @__PURE__ */ new Set(["and", "or"]);
|
|
1501
|
+
function translateFilterArray(filter) {
|
|
1502
|
+
if (isObjectFilterEntryForm(filter)) return objectFilterEntriesToAST(filter);
|
|
1503
|
+
const head = filter[0];
|
|
1504
|
+
if (typeof head === "string" && LOGICAL_AST_HEADS.has(head.toLowerCase())) {
|
|
1505
|
+
return [head, ...filter.slice(1).map(translateFilterChild)];
|
|
1506
|
+
}
|
|
1507
|
+
if (filter.every((child) => Array.isArray(child))) return filter.map(translateFilterChild);
|
|
1508
|
+
return filter;
|
|
1509
|
+
}
|
|
1510
|
+
function translateFilterChild(child) {
|
|
1511
|
+
if (Array.isArray(child)) return child.length > 0 ? translateFilterArray(child) : child;
|
|
1512
|
+
if (child && typeof child === "object" && child.field !== void 0) {
|
|
1513
|
+
const tuple = objectFilterEntryToAST(child);
|
|
1514
|
+
if (tuple) return tuple;
|
|
1515
|
+
}
|
|
1516
|
+
return child;
|
|
1517
|
+
}
|
|
1437
1518
|
function translateFilterToAST(filter) {
|
|
1438
1519
|
if (filter === void 0 || filter === null) return void 0;
|
|
1439
1520
|
if (Array.isArray(filter)) {
|
|
1440
1521
|
if (filter.length === 0) return void 0;
|
|
1441
|
-
|
|
1442
|
-
const isObjectForm = filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
|
|
1443
|
-
if (isObjectForm) {
|
|
1444
|
-
const tuples = filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
1445
|
-
if (tuples.length === 0) return void 0;
|
|
1446
|
-
if (tuples.length === 1) return tuples[0];
|
|
1447
|
-
return ["and", ...tuples];
|
|
1448
|
-
}
|
|
1449
|
-
return filter;
|
|
1522
|
+
return translateFilterArray(filter);
|
|
1450
1523
|
}
|
|
1451
1524
|
if (typeof filter === "object") {
|
|
1452
1525
|
if (Object.keys(filter).length === 0) return void 0;
|
|
1453
|
-
return filter;
|
|
1526
|
+
return convertFiltersToAST(filter);
|
|
1527
|
+
}
|
|
1528
|
+
return void 0;
|
|
1529
|
+
}
|
|
1530
|
+
function serializeOrderBy(orderby) {
|
|
1531
|
+
if (orderby === void 0 || orderby === null) return void 0;
|
|
1532
|
+
if (typeof orderby === "string") {
|
|
1533
|
+
const trimmed = orderby.trim();
|
|
1534
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1535
|
+
}
|
|
1536
|
+
const shorthand = (field, order) => String(order).toLowerCase() === "desc" ? `-${field}` : field;
|
|
1537
|
+
if (Array.isArray(orderby)) {
|
|
1538
|
+
const parts = orderby.map((item) => typeof item === "string" ? item.trim() : shorthand(item.field, item.order)).filter((s) => s.length > 0);
|
|
1539
|
+
return parts.length > 0 ? parts.join(",") : void 0;
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof orderby === "object") {
|
|
1542
|
+
const parts = Object.entries(orderby).map(([field, order]) => shorthand(field, order));
|
|
1543
|
+
return parts.length > 0 ? parts.join(",") : void 0;
|
|
1454
1544
|
}
|
|
1455
1545
|
return void 0;
|
|
1456
1546
|
}
|
|
@@ -1486,7 +1576,46 @@ function is404Error(error) {
|
|
|
1486
1576
|
return true;
|
|
1487
1577
|
}
|
|
1488
1578
|
const code = typeof err.code === "string" ? err.code : "";
|
|
1489
|
-
return code
|
|
1579
|
+
return errorCodeIsAnyOf({ code }, ["OBJECT_NOT_FOUND", "RECORD_NOT_FOUND"]);
|
|
1580
|
+
}
|
|
1581
|
+
var AnalyticsNotInstalledError = class extends Error {
|
|
1582
|
+
constructor(surface, detail) {
|
|
1583
|
+
super(
|
|
1584
|
+
`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})` : "")
|
|
1585
|
+
);
|
|
1586
|
+
__publicField(this, "code", "ANALYTICS_NOT_INSTALLED");
|
|
1587
|
+
/** The surface that was unavailable, for the message a host renders. */
|
|
1588
|
+
__publicField(this, "surface");
|
|
1589
|
+
this.name = "AnalyticsNotInstalledError";
|
|
1590
|
+
this.surface = surface;
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
function isAnalyticsNotInstalledError(error) {
|
|
1594
|
+
if (!error || typeof error !== "object") return false;
|
|
1595
|
+
return error.code === "ANALYTICS_NOT_INSTALLED";
|
|
1596
|
+
}
|
|
1597
|
+
var AnalyticsQueryRejectedError = class extends Error {
|
|
1598
|
+
constructor(detail, serverCode) {
|
|
1599
|
+
super(
|
|
1600
|
+
`Analytics rejected the query: ${detail ?? "the request body does not match the AnalyticsQuery contract"}`
|
|
1601
|
+
);
|
|
1602
|
+
__publicField(this, "code", "ANALYTICS_QUERY_REJECTED");
|
|
1603
|
+
/** The server's own error code (e.g. `VALIDATION_FAILED`), when it sent one. */
|
|
1604
|
+
__publicField(this, "serverCode");
|
|
1605
|
+
this.name = "AnalyticsQueryRejectedError";
|
|
1606
|
+
this.serverCode = serverCode;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
function classifyAnalyticsFailure(error) {
|
|
1610
|
+
const err = error ?? {};
|
|
1611
|
+
const code = typeof err.code === "string" ? err.code : void 0;
|
|
1612
|
+
const message = typeof err.message === "string" ? err.message : void 0;
|
|
1613
|
+
const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
|
|
1614
|
+
if (code === "VALIDATION_FAILED" || status === 400) return { kind: "rejected", code, message };
|
|
1615
|
+
if (status === 404 || status === 501 || code === "NOT_IMPLEMENTED" || code === "ROUTE_NOT_FOUND") {
|
|
1616
|
+
return { kind: "not-installed", code, message };
|
|
1617
|
+
}
|
|
1618
|
+
return { kind: "unknown", code, message };
|
|
1490
1619
|
}
|
|
1491
1620
|
var ConcurrentUpdateError = class extends Error {
|
|
1492
1621
|
constructor(opts) {
|
|
@@ -1508,9 +1637,25 @@ function isConcurrentUpdateError(error) {
|
|
|
1508
1637
|
function normaliseClientError(error) {
|
|
1509
1638
|
if (!error || typeof error !== "object") return error;
|
|
1510
1639
|
const e = error;
|
|
1640
|
+
const details = e.details ?? {};
|
|
1641
|
+
if (e.code === "VALIDATION_FAILED" || e.name === "ValidationError") {
|
|
1642
|
+
const rawFields = Array.isArray(details.fields) ? details.fields : Array.isArray(e.fields) ? e.fields : [];
|
|
1643
|
+
const validationErrors = rawFields.map((f) => {
|
|
1644
|
+
const rec = f ?? {};
|
|
1645
|
+
const field = typeof rec.field === "string" ? rec.field : void 0;
|
|
1646
|
+
if (!field) return null;
|
|
1647
|
+
const message = typeof rec.message === "string" && rec.message.trim() ? rec.message : typeof rec.code === "string" ? rec.code : "";
|
|
1648
|
+
return { field, message };
|
|
1649
|
+
}).filter((x) => x !== null);
|
|
1650
|
+
return new ValidationError(
|
|
1651
|
+
typeof e.message === "string" ? e.message : "Validation failed",
|
|
1652
|
+
validationErrors[0]?.field,
|
|
1653
|
+
validationErrors,
|
|
1654
|
+
{ fields: rawFields }
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1511
1657
|
if (e.code !== "CONCURRENT_UPDATE" && e.httpStatus !== 409) return error;
|
|
1512
1658
|
if (e.code !== "CONCURRENT_UPDATE") return error;
|
|
1513
|
-
const details = e.details ?? {};
|
|
1514
1659
|
return new ConcurrentUpdateError({
|
|
1515
1660
|
currentVersion: typeof details.currentVersion === "string" ? details.currentVersion : null,
|
|
1516
1661
|
currentRecord: details.currentRecord ?? null,
|
|
@@ -1524,7 +1669,7 @@ function createQuietHttpLogger() {
|
|
|
1524
1669
|
const errBody = meta.error;
|
|
1525
1670
|
if (errBody && typeof errBody === "object") {
|
|
1526
1671
|
const code = errBody.code;
|
|
1527
|
-
if (code
|
|
1672
|
+
if (errorCodeIsAnyOf({ code }, ["OBJECT_NOT_FOUND", "RECORD_NOT_FOUND"])) return true;
|
|
1528
1673
|
}
|
|
1529
1674
|
return false;
|
|
1530
1675
|
};
|
|
@@ -1568,6 +1713,8 @@ var ObjectStackAdapter = class {
|
|
|
1568
1713
|
__publicField(this, "reconnectAttempts", 0);
|
|
1569
1714
|
__publicField(this, "baseUrl");
|
|
1570
1715
|
__publicField(this, "token");
|
|
1716
|
+
/** One "analytics capability is missing" console line per adapter, not per widget. */
|
|
1717
|
+
__publicField(this, "analyticsCapabilityWarned", false);
|
|
1571
1718
|
__publicField(this, "fetchImpl");
|
|
1572
1719
|
// In-flight find() requests keyed by resource + serialized params.
|
|
1573
1720
|
// Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
|
|
@@ -1599,6 +1746,10 @@ var ObjectStackAdapter = class {
|
|
|
1599
1746
|
// calendar) auto-refresh — the interface ListView relies on to reflect
|
|
1600
1747
|
// inline-edit "Save All" writes without a manual reload.
|
|
1601
1748
|
__publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
|
|
1749
|
+
// Subscribers registered via onWriteWarning(). Emitted after a create/update
|
|
1750
|
+
// whose response carried `droppedFields` (framework #3431/#3455) so the app
|
|
1751
|
+
// shell can surface a toast instead of the strip passing silently.
|
|
1752
|
+
__publicField(this, "writeWarningListeners", /* @__PURE__ */ new Set());
|
|
1602
1753
|
this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
1603
1754
|
this.metadataCache = new MetadataCache(config.cache);
|
|
1604
1755
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -1770,6 +1921,77 @@ var ObjectStackAdapter = class {
|
|
|
1770
1921
|
promise.then(cleanup, cleanup);
|
|
1771
1922
|
return promise;
|
|
1772
1923
|
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Full-text search across every searchable object in a single round-trip.
|
|
1926
|
+
*
|
|
1927
|
+
* Hits `GET /api/v1/search?q=`, the platform's global search endpoint served
|
|
1928
|
+
* by the registered search service (the pinyin full-text plugin) and backed
|
|
1929
|
+
* by `metadata-protocol`'s `searchAll`. Unlike `find(resource, { $search })`
|
|
1930
|
+
* — a per-object metadata-driven search (ADR-0061) — this consults the search
|
|
1931
|
+
* index and ranks hits across objects, so it surfaces records the per-object
|
|
1932
|
+
* fanout misses. Global affordances (⌘K command palette, search page) prefer
|
|
1933
|
+
* this path (framework #3371).
|
|
1934
|
+
*
|
|
1935
|
+
* Returns `{ query, hits }`. A backend without the search plugin installed
|
|
1936
|
+
* answers `404`; we treat that as "no global search here" and return an empty
|
|
1937
|
+
* hit set so callers can fall back to a per-object fanout rather than surface
|
|
1938
|
+
* an error.
|
|
1939
|
+
*/
|
|
1940
|
+
async searchAll(query, options) {
|
|
1941
|
+
const trimmed = (query ?? "").trim();
|
|
1942
|
+
if (trimmed === "") return { query: "", hits: [] };
|
|
1943
|
+
await this.connect();
|
|
1944
|
+
const queryParams = new URLSearchParams();
|
|
1945
|
+
queryParams.set("q", trimmed);
|
|
1946
|
+
if (options?.limit != null && options.limit > 0) {
|
|
1947
|
+
queryParams.set("limit", String(options.limit));
|
|
1948
|
+
}
|
|
1949
|
+
if (options?.objects && options.objects.length > 0) {
|
|
1950
|
+
queryParams.set("objects", options.objects.join(","));
|
|
1951
|
+
}
|
|
1952
|
+
const baseUrl = (this.baseUrl || "").replace(/\/$/, "");
|
|
1953
|
+
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
|
|
1954
|
+
const searchPath = hasApiVersionSuffix ? "/search" : "/api/v1/search";
|
|
1955
|
+
const url = `${baseUrl}${searchPath}?${queryParams.toString()}`;
|
|
1956
|
+
const headers = {
|
|
1957
|
+
"Content-Type": "application/json"
|
|
1958
|
+
};
|
|
1959
|
+
if (this.token) {
|
|
1960
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
1961
|
+
}
|
|
1962
|
+
const res = await this.fetchImpl(url, { method: "GET", headers });
|
|
1963
|
+
if (!res.ok) {
|
|
1964
|
+
if (res.status === 404) return { query: trimmed, hits: [] };
|
|
1965
|
+
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
1966
|
+
const err = new Error(
|
|
1967
|
+
errorBody?.error?.message || errorBody?.message || res.statusText
|
|
1968
|
+
);
|
|
1969
|
+
err.status = res.status;
|
|
1970
|
+
throw err;
|
|
1971
|
+
}
|
|
1972
|
+
const body = await res.json();
|
|
1973
|
+
const payload = body && typeof body === "object" && typeof body.success === "boolean" && "data" in body ? body.data : body;
|
|
1974
|
+
const rawHits = Array.isArray(payload?.hits) ? payload.hits : Array.isArray(payload) ? payload : [];
|
|
1975
|
+
const hits = [];
|
|
1976
|
+
for (const entry of rawHits) {
|
|
1977
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1978
|
+
const h = entry;
|
|
1979
|
+
const object = h.object ?? h.objectName ?? h.object_name;
|
|
1980
|
+
const id = h.id ?? h.record?.id ?? h.record?._id;
|
|
1981
|
+
if (typeof object !== "string" || id == null) continue;
|
|
1982
|
+
hits.push({
|
|
1983
|
+
object,
|
|
1984
|
+
id: String(id),
|
|
1985
|
+
title: typeof h.title === "string" ? h.title : void 0,
|
|
1986
|
+
snippet: typeof h.snippet === "string" ? h.snippet : void 0,
|
|
1987
|
+
record: h.record && typeof h.record === "object" ? h.record : void 0
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
query: typeof payload?.query === "string" ? payload.query : trimmed,
|
|
1992
|
+
hits
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1773
1995
|
/**
|
|
1774
1996
|
* Find a single record by ID.
|
|
1775
1997
|
*/
|
|
@@ -1833,11 +2055,84 @@ var ObjectStackAdapter = class {
|
|
|
1833
2055
|
this.mutationListeners.delete(callback);
|
|
1834
2056
|
};
|
|
1835
2057
|
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Notify all write-warning subscribers. Isolated like {@link emitMutation}: a
|
|
2060
|
+
* throwing listener must not break the write or starve the others.
|
|
2061
|
+
*/
|
|
2062
|
+
emitWriteWarning(event) {
|
|
2063
|
+
for (const listener of this.writeWarningListeners) {
|
|
2064
|
+
try {
|
|
2065
|
+
listener(event);
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
console.warn("ObjectStackAdapter: write-warning listener error", err);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Read `droppedFields` off a create/update response (framework #3431/#3455)
|
|
2073
|
+
* and, when present, notify write-warning subscribers. Tolerant of a client
|
|
2074
|
+
* whose response type predates `droppedFields`: the field is read structurally
|
|
2075
|
+
* and validated, so an older client (or a backend that never drops) is a no-op.
|
|
2076
|
+
*/
|
|
2077
|
+
notifyDroppedFields(operation, resource, result, id) {
|
|
2078
|
+
const dropped = result?.droppedFields;
|
|
2079
|
+
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2080
|
+
const droppedFields = dropped.filter(
|
|
2081
|
+
(e) => !!e && typeof e === "object" && Array.isArray(e.fields) && e.fields.length > 0
|
|
2082
|
+
);
|
|
2083
|
+
if (droppedFields.length === 0) return;
|
|
2084
|
+
this.emitWriteWarning({ operation, resource, ...id !== void 0 ? { id } : {}, droppedFields });
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Same, for the cross-object transactional batch (framework #3794). Its
|
|
2088
|
+
* response hangs the events off a top-level `droppedFields` list, each tagged
|
|
2089
|
+
* with the `index` of the operation it came from — `results` entries are bare
|
|
2090
|
+
* record echoes with nowhere to hang a per-row list.
|
|
2091
|
+
*
|
|
2092
|
+
* This is the path that matters most for the warning: `batchTransaction` is
|
|
2093
|
+
* how the console's record form saves a master-detail record, so a
|
|
2094
|
+
* `readonlyWhen`-locked field edited in that form was stripped server-side
|
|
2095
|
+
* while the UI reported a plain success. The operation kind is taken from the
|
|
2096
|
+
* originating op so the toast doesn't call an update a create.
|
|
2097
|
+
*/
|
|
2098
|
+
notifyBatchDroppedFields(operations, payload) {
|
|
2099
|
+
const dropped = payload?.droppedFields;
|
|
2100
|
+
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2101
|
+
for (const entry of dropped) {
|
|
2102
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2103
|
+
const e = entry;
|
|
2104
|
+
if (!Array.isArray(e.fields) || e.fields.length === 0) continue;
|
|
2105
|
+
const op = typeof e.index === "number" ? operations[e.index] : void 0;
|
|
2106
|
+
const operation = (op?.action ?? "create") === "create" ? "create" : "update";
|
|
2107
|
+
this.emitWriteWarning({
|
|
2108
|
+
operation,
|
|
2109
|
+
resource: e.object ?? op?.object ?? "",
|
|
2110
|
+
...op?.id !== void 0 && op?.id !== null ? { id: op.id } : {},
|
|
2111
|
+
droppedFields: [{ object: e.object, fields: e.fields, reason: e.reason }]
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Subscribe to write-warning events (a create/update dropped caller-supplied
|
|
2117
|
+
* fields — #3431/#3455). Returns an unsubscribe function. The app shell uses
|
|
2118
|
+
* this to toast the user; the write itself already succeeded.
|
|
2119
|
+
*/
|
|
2120
|
+
onWriteWarning(callback) {
|
|
2121
|
+
this.writeWarningListeners.add(callback);
|
|
2122
|
+
return () => {
|
|
2123
|
+
this.writeWarningListeners.delete(callback);
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
1836
2126
|
async create(resource, data) {
|
|
1837
2127
|
await this.connect();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2128
|
+
try {
|
|
2129
|
+
const result = await this.client.data.create(resource, data);
|
|
2130
|
+
this.emitMutation({ type: "create", resource, record: { ...result.record } });
|
|
2131
|
+
this.notifyDroppedFields("create", resource, result, result.record?.id);
|
|
2132
|
+
return result.record;
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
throw normaliseClientError(err);
|
|
2135
|
+
}
|
|
1841
2136
|
}
|
|
1842
2137
|
/**
|
|
1843
2138
|
* Update an existing record.
|
|
@@ -1860,6 +2155,7 @@ var ObjectStackAdapter = class {
|
|
|
1860
2155
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
1861
2156
|
);
|
|
1862
2157
|
this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
|
|
2158
|
+
this.notifyDroppedFields("update", resource, result, id);
|
|
1863
2159
|
return result.record;
|
|
1864
2160
|
} catch (err) {
|
|
1865
2161
|
throw normaliseClientError(err);
|
|
@@ -2017,6 +2313,7 @@ var ObjectStackAdapter = class {
|
|
|
2017
2313
|
try {
|
|
2018
2314
|
const payload = await this.client.data.batchTransaction(operations);
|
|
2019
2315
|
this.emitBatchMutations(operations, payload?.results);
|
|
2316
|
+
this.notifyBatchDroppedFields(operations, payload);
|
|
2020
2317
|
return payload;
|
|
2021
2318
|
} catch (err) {
|
|
2022
2319
|
const status = this.errorStatusOf(err);
|
|
@@ -2048,7 +2345,7 @@ var ObjectStackAdapter = class {
|
|
|
2048
2345
|
return emulateBatchTransaction(this, operations);
|
|
2049
2346
|
}
|
|
2050
2347
|
/**
|
|
2051
|
-
* Emit one
|
|
2348
|
+
* Emit one DataSourceMutationEvent per committed operation so the invalidation bus
|
|
2052
2349
|
* (#2269) sees writes that went through /batch exactly like single
|
|
2053
2350
|
* create/update/delete calls — master-detail ModalForm saves otherwise leave
|
|
2054
2351
|
* related lists and count badges stale (#2582). `results` is index-aligned
|
|
@@ -2401,20 +2698,8 @@ var ObjectStackAdapter = class {
|
|
|
2401
2698
|
const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
|
|
2402
2699
|
queryParams.set("select", selectFields.join(","));
|
|
2403
2700
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
const sortStr = params.$orderby.map((item) => {
|
|
2407
|
-
if (typeof item === "string") return item;
|
|
2408
|
-
const field = item.field;
|
|
2409
|
-
const order = item.order || "asc";
|
|
2410
|
-
return order === "desc" ? `-${field}` : field;
|
|
2411
|
-
}).join(",");
|
|
2412
|
-
queryParams.set("sort", sortStr);
|
|
2413
|
-
} else {
|
|
2414
|
-
const sortStr = Object.entries(params.$orderby).map(([field, order]) => order === "desc" ? `-${field}` : field).join(",");
|
|
2415
|
-
queryParams.set("sort", sortStr);
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2701
|
+
const sortStr = serializeOrderBy(params.$orderby);
|
|
2702
|
+
if (sortStr) queryParams.set("sort", sortStr);
|
|
2418
2703
|
if (params.$filter !== void 0 && params.$filter !== null) {
|
|
2419
2704
|
const translated = translateFilterToAST(params.$filter);
|
|
2420
2705
|
if (translated !== void 0) {
|
|
@@ -2478,6 +2763,13 @@ var ObjectStackAdapter = class {
|
|
|
2478
2763
|
queryParams.set("filter", JSON.stringify(translated));
|
|
2479
2764
|
}
|
|
2480
2765
|
}
|
|
2766
|
+
const searchTerm = typeof request.search === "string" ? request.search.trim() : "";
|
|
2767
|
+
if (searchTerm) {
|
|
2768
|
+
queryParams.set("search", searchTerm);
|
|
2769
|
+
if (request.searchFields && request.searchFields.length > 0) {
|
|
2770
|
+
queryParams.set("searchFields", request.searchFields.join(","));
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2481
2773
|
const baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
2482
2774
|
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
|
|
2483
2775
|
const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
|
|
@@ -2506,38 +2798,14 @@ var ObjectStackAdapter = class {
|
|
|
2506
2798
|
const isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
|
|
2507
2799
|
if (!isEmpty) {
|
|
2508
2800
|
if (Array.isArray(params.$filter)) {
|
|
2509
|
-
|
|
2510
|
-
if (isObjectForm) {
|
|
2511
|
-
const tuples = params.$filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
2512
|
-
if (tuples.length === 0) {
|
|
2513
|
-
} else if (tuples.length === 1) {
|
|
2514
|
-
options.filters = tuples[0];
|
|
2515
|
-
} else {
|
|
2516
|
-
options.filters = ["and", ...tuples];
|
|
2517
|
-
}
|
|
2518
|
-
} else {
|
|
2519
|
-
options.filters = params.$filter;
|
|
2520
|
-
}
|
|
2801
|
+
options.filters = translateFilterArray(params.$filter);
|
|
2521
2802
|
} else {
|
|
2522
2803
|
options.filters = convertFiltersToAST(params.$filter);
|
|
2523
2804
|
}
|
|
2524
2805
|
}
|
|
2525
2806
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
options.sort = params.$orderby.map((item) => {
|
|
2529
|
-
if (typeof item === "string") return item;
|
|
2530
|
-
const field = item.field;
|
|
2531
|
-
const order = item.order || "asc";
|
|
2532
|
-
return order === "desc" ? `-${field}` : field;
|
|
2533
|
-
});
|
|
2534
|
-
} else {
|
|
2535
|
-
const sortArray = Object.entries(params.$orderby).map(([field, order]) => {
|
|
2536
|
-
return order === "desc" ? `-${field}` : field;
|
|
2537
|
-
});
|
|
2538
|
-
options.sort = sortArray;
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2807
|
+
const sort = serializeOrderBy(params.$orderby);
|
|
2808
|
+
if (sort) options.sort = sort;
|
|
2541
2809
|
if (params.$skip !== void 0) {
|
|
2542
2810
|
options.skip = params.$skip;
|
|
2543
2811
|
}
|
|
@@ -3041,6 +3309,7 @@ var ObjectStackAdapter = class {
|
|
|
3041
3309
|
}
|
|
3042
3310
|
try {
|
|
3043
3311
|
const measureName = params.function === "count" ? "count" : `${params.field}_${params.function}`;
|
|
3312
|
+
const valueKey = this.aggregateValueKey(params);
|
|
3044
3313
|
const payload = {
|
|
3045
3314
|
cube: resource,
|
|
3046
3315
|
measures: [measureName],
|
|
@@ -3055,30 +3324,62 @@ var ObjectStackAdapter = class {
|
|
|
3055
3324
|
const measureMissing = rawRows.length > 0 && rawRows.every((row) => {
|
|
3056
3325
|
if (row == null) return true;
|
|
3057
3326
|
if (measureName in row && row[measureName] != null) return false;
|
|
3058
|
-
if (
|
|
3327
|
+
if (valueKey in row && row[valueKey] != null) return false;
|
|
3059
3328
|
return true;
|
|
3060
3329
|
});
|
|
3061
3330
|
if (measureMissing) {
|
|
3062
|
-
|
|
3063
|
-
const records = result.data || [];
|
|
3064
|
-
if (records.length === 0) return [];
|
|
3065
|
-
return this.aggregateClientSide(records, params);
|
|
3331
|
+
return await this.aggregateViaFind(resource, params);
|
|
3066
3332
|
}
|
|
3067
3333
|
return rawRows.map((row) => {
|
|
3068
3334
|
const mapped = { ...row };
|
|
3069
|
-
if (measureName !==
|
|
3070
|
-
mapped[
|
|
3335
|
+
if (measureName !== valueKey && measureName in mapped) {
|
|
3336
|
+
mapped[valueKey] = mapped[measureName];
|
|
3071
3337
|
delete mapped[measureName];
|
|
3072
3338
|
}
|
|
3073
3339
|
return mapped;
|
|
3074
3340
|
});
|
|
3075
|
-
} catch {
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3341
|
+
} catch (e) {
|
|
3342
|
+
const failure = classifyAnalyticsFailure(e);
|
|
3343
|
+
if (failure.kind === "rejected") {
|
|
3344
|
+
throw new AnalyticsQueryRejectedError(failure.message, failure.code);
|
|
3345
|
+
}
|
|
3346
|
+
if (failure.kind === "not-installed") {
|
|
3347
|
+
this.warnAnalyticsCapabilityOnce(failure.message);
|
|
3348
|
+
}
|
|
3349
|
+
return await this.aggregateViaFind(resource, params);
|
|
3080
3350
|
}
|
|
3081
3351
|
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Client-side aggregation over a server-scoped `find()` — the fallback used
|
|
3354
|
+
* whenever the analytics endpoint cannot answer (capability absent, network
|
|
3355
|
+
* failure, or a result whose measure came back missing).
|
|
3356
|
+
*
|
|
3357
|
+
* Forwarding `params.filter` is load-bearing: without it the fallback
|
|
3358
|
+
* aggregates the whole table while the caller believes it applied a filter,
|
|
3359
|
+
* which is the "KPI silently sums everything" failure this adapter has
|
|
3360
|
+
* guarded against since the widget filter was threaded through.
|
|
3361
|
+
*/
|
|
3362
|
+
async aggregateViaFind(resource, params) {
|
|
3363
|
+
const result = await this.find(
|
|
3364
|
+
resource,
|
|
3365
|
+
params.filter ? { $filter: params.filter } : void 0
|
|
3366
|
+
);
|
|
3367
|
+
const records = result.data || [];
|
|
3368
|
+
if (records.length === 0) return [];
|
|
3369
|
+
return this.aggregateClientSide(records, params);
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Say "the analytics capability isn't installed" ONCE per adapter, not once
|
|
3373
|
+
* per widget: a dashboard fans out one aggregate() per KPI, and N identical
|
|
3374
|
+
* console lines read like N different failures.
|
|
3375
|
+
*/
|
|
3376
|
+
warnAnalyticsCapabilityOnce(detail) {
|
|
3377
|
+
if (this.analyticsCapabilityWarned) return;
|
|
3378
|
+
this.analyticsCapabilityWarned = true;
|
|
3379
|
+
console.warn(
|
|
3380
|
+
"[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}` : "")
|
|
3381
|
+
);
|
|
3382
|
+
}
|
|
3082
3383
|
/**
|
|
3083
3384
|
* Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
|
|
3084
3385
|
*
|
|
@@ -3117,6 +3418,9 @@ var ObjectStackAdapter = class {
|
|
|
3117
3418
|
detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
3118
3419
|
} catch {
|
|
3119
3420
|
}
|
|
3421
|
+
if (res.status === 501 || res.status === 404) {
|
|
3422
|
+
throw new AnalyticsNotInstalledError("POST /analytics/dataset/query", detail || void 0);
|
|
3423
|
+
}
|
|
3120
3424
|
throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
3121
3425
|
}
|
|
3122
3426
|
const payload = await res.json();
|
|
@@ -3130,8 +3434,18 @@ var ObjectStackAdapter = class {
|
|
|
3130
3434
|
return { rows, fields, object, dimensionFields, drillRawRows, totals };
|
|
3131
3435
|
}
|
|
3132
3436
|
/** Client-side aggregation fallback */
|
|
3437
|
+
/**
|
|
3438
|
+
* The result column an object-bound `aggregate` projects its value under
|
|
3439
|
+
* (framework#3701, `chartAggregateValueKey` in `@objectstack/spec/ui`): the
|
|
3440
|
+
* raw `field` name — no `sum_`-style decoration, unlike a dataset measure —
|
|
3441
|
+
* or the literal `count` when a count names no field.
|
|
3442
|
+
*/
|
|
3443
|
+
aggregateValueKey(params) {
|
|
3444
|
+
return params.field || params.function || "count";
|
|
3445
|
+
}
|
|
3133
3446
|
aggregateClientSide(records, params) {
|
|
3134
3447
|
const { field, function: aggFn, groupBy } = params;
|
|
3448
|
+
const valueKey = this.aggregateValueKey(params);
|
|
3135
3449
|
const groups = {};
|
|
3136
3450
|
for (const record of records) {
|
|
3137
3451
|
const key = String(record[groupBy] ?? "Unknown");
|
|
@@ -3139,7 +3453,7 @@ var ObjectStackAdapter = class {
|
|
|
3139
3453
|
groups[key].push(record);
|
|
3140
3454
|
}
|
|
3141
3455
|
return Object.entries(groups).map(([key, group]) => {
|
|
3142
|
-
const values = group.map((r) => Number(r[field]) || 0);
|
|
3456
|
+
const values = field ? group.map((r) => Number(r[field]) || 0) : [];
|
|
3143
3457
|
let result;
|
|
3144
3458
|
switch (aggFn) {
|
|
3145
3459
|
case "count":
|
|
@@ -3159,7 +3473,7 @@ var ObjectStackAdapter = class {
|
|
|
3159
3473
|
result = values.reduce((a, b) => a + b, 0);
|
|
3160
3474
|
break;
|
|
3161
3475
|
}
|
|
3162
|
-
return { [groupBy]: key, [
|
|
3476
|
+
return { [groupBy]: key, [valueKey]: result };
|
|
3163
3477
|
});
|
|
3164
3478
|
}
|
|
3165
3479
|
/**
|
|
@@ -3361,12 +3675,16 @@ function createObjectStackAdapter(config) {
|
|
|
3361
3675
|
return new ObjectStackAdapter(config);
|
|
3362
3676
|
}
|
|
3363
3677
|
export {
|
|
3678
|
+
AnalyticsNotInstalledError,
|
|
3679
|
+
AnalyticsQueryRejectedError,
|
|
3364
3680
|
AuthenticationError,
|
|
3365
3681
|
BulkOperationError,
|
|
3366
3682
|
CloudOperations,
|
|
3367
3683
|
ConcurrentUpdateError,
|
|
3368
3684
|
ConnectionError,
|
|
3685
|
+
FILTER_OPERATOR_ALIASES,
|
|
3369
3686
|
IntegrationManager,
|
|
3687
|
+
MalformedFilterError,
|
|
3370
3688
|
MetadataClient,
|
|
3371
3689
|
MetadataNotFoundError,
|
|
3372
3690
|
ObjectStackAdapter,
|
|
@@ -3374,6 +3692,7 @@ export {
|
|
|
3374
3692
|
SecurityManager,
|
|
3375
3693
|
ValidationError,
|
|
3376
3694
|
calculateAutoLayout,
|
|
3695
|
+
classifyAnalyticsFailure,
|
|
3377
3696
|
clearSharedDiscoveryCache,
|
|
3378
3697
|
createDefaultCanvasConfig,
|
|
3379
3698
|
createErrorFromResponse,
|
|
@@ -3382,11 +3701,14 @@ export {
|
|
|
3382
3701
|
generateContractManifest,
|
|
3383
3702
|
getSharedDiscovery,
|
|
3384
3703
|
is404Error,
|
|
3704
|
+
isAnalyticsNotInstalledError,
|
|
3385
3705
|
isConcurrentUpdateError,
|
|
3386
3706
|
isErrorType,
|
|
3707
|
+
isMalformedFilterError,
|
|
3387
3708
|
isObjectStackError,
|
|
3388
3709
|
normaliseClientError,
|
|
3389
3710
|
readTransactionalBatchCapability,
|
|
3711
|
+
serializeOrderBy,
|
|
3390
3712
|
snapToGrid,
|
|
3391
3713
|
validatePluginContract
|
|
3392
3714
|
};
|