@object-ui/data-objectstack 3.3.2 → 4.0.1
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 +24 -0
- package/dist/index.cjs +95 -9
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +95 -9
- package/package.json +3 -3
- package/src/index.ts +135 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @object-ui/data-objectstack
|
|
2
2
|
|
|
3
|
+
## 4.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@4.0.1
|
|
8
|
+
- @object-ui/core@4.0.1
|
|
9
|
+
|
|
10
|
+
## 4.0.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @object-ui/types@4.0.0
|
|
16
|
+
- @object-ui/core@4.0.0
|
|
17
|
+
|
|
18
|
+
## 3.4.0
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [f1ca238]
|
|
23
|
+
- Updated dependencies [de881ef]
|
|
24
|
+
- @object-ui/types@3.4.0
|
|
25
|
+
- @object-ui/core@3.4.0
|
|
26
|
+
|
|
3
27
|
## 3.3.2
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -829,6 +829,57 @@ function calculateAutoLayout(items, canvasWidth, padding = 40, gap = 40) {
|
|
|
829
829
|
}
|
|
830
830
|
|
|
831
831
|
// src/index.ts
|
|
832
|
+
var FILTER_OPERATOR_ALIASES = {
|
|
833
|
+
equals: "=",
|
|
834
|
+
eq: "=",
|
|
835
|
+
"==": "=",
|
|
836
|
+
not_equals: "!=",
|
|
837
|
+
notequals: "!=",
|
|
838
|
+
ne: "!=",
|
|
839
|
+
greater_than: ">",
|
|
840
|
+
greaterthan: ">",
|
|
841
|
+
gt: ">",
|
|
842
|
+
greater_than_or_equal: ">=",
|
|
843
|
+
greater_than_or_equals: ">=",
|
|
844
|
+
greaterthanorequal: ">=",
|
|
845
|
+
gte: ">=",
|
|
846
|
+
less_than: "<",
|
|
847
|
+
lessthan: "<",
|
|
848
|
+
lt: "<",
|
|
849
|
+
less_than_or_equal: "<=",
|
|
850
|
+
less_than_or_equals: "<=",
|
|
851
|
+
lessthanorequal: "<=",
|
|
852
|
+
lte: "<=",
|
|
853
|
+
in: "in",
|
|
854
|
+
not_in: "nin",
|
|
855
|
+
notin: "nin",
|
|
856
|
+
nin: "nin",
|
|
857
|
+
contains: "contains",
|
|
858
|
+
not_contains: "notcontains",
|
|
859
|
+
notcontains: "notcontains",
|
|
860
|
+
starts_with: "startswith",
|
|
861
|
+
startswith: "startswith",
|
|
862
|
+
ends_with: "endswith",
|
|
863
|
+
endswith: "endswith",
|
|
864
|
+
between: "between",
|
|
865
|
+
is_null: "isnull",
|
|
866
|
+
isnull: "isnull",
|
|
867
|
+
is_not_null: "isnotnull",
|
|
868
|
+
isnotnull: "isnotnull"
|
|
869
|
+
};
|
|
870
|
+
function normalizeFilterOperator(op) {
|
|
871
|
+
if (typeof op !== "string") return null;
|
|
872
|
+
const lower = op.toLowerCase();
|
|
873
|
+
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
874
|
+
}
|
|
875
|
+
function objectFilterEntryToAST(entry) {
|
|
876
|
+
if (!entry || typeof entry !== "object") return null;
|
|
877
|
+
const field = entry.field ?? entry.name;
|
|
878
|
+
const rawOp = entry.operator ?? entry.op ?? "=";
|
|
879
|
+
const op = normalizeFilterOperator(rawOp);
|
|
880
|
+
if (!field || !op) return null;
|
|
881
|
+
return [String(field), op, entry.value];
|
|
882
|
+
}
|
|
832
883
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
833
884
|
async function getSharedDiscovery(baseUrl, fetcher) {
|
|
834
885
|
const key = baseUrl || "<default>";
|
|
@@ -844,6 +895,13 @@ async function getSharedDiscovery(baseUrl, fetcher) {
|
|
|
844
895
|
function clearSharedDiscoveryCache() {
|
|
845
896
|
discoveryCache.clear();
|
|
846
897
|
}
|
|
898
|
+
function stableStringify(value) {
|
|
899
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
900
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
901
|
+
const obj = value;
|
|
902
|
+
const keys = Object.keys(obj).sort();
|
|
903
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
904
|
+
}
|
|
847
905
|
var ObjectStackAdapter = class {
|
|
848
906
|
constructor(config) {
|
|
849
907
|
__publicField(this, "client");
|
|
@@ -860,6 +918,11 @@ var ObjectStackAdapter = class {
|
|
|
860
918
|
__publicField(this, "baseUrl");
|
|
861
919
|
__publicField(this, "token");
|
|
862
920
|
__publicField(this, "fetchImpl");
|
|
921
|
+
// In-flight find() requests keyed by resource + serialized params.
|
|
922
|
+
// Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
|
|
923
|
+
// multiple sibling components requesting the same dataset on first paint)
|
|
924
|
+
// into a single network round trip.
|
|
925
|
+
__publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
|
|
863
926
|
this.client = new import_client.ObjectStackClient(config);
|
|
864
927
|
this.metadataCache = new MetadataCache(config.cache);
|
|
865
928
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -997,14 +1060,26 @@ var ObjectStackAdapter = class {
|
|
|
997
1060
|
* Converts OData-style params to ObjectStack query options.
|
|
998
1061
|
*/
|
|
999
1062
|
async find(resource, params) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1063
|
+
const key = `${resource}::${stableStringify(params)}`;
|
|
1064
|
+
const existing = this.inflightFinds.get(key);
|
|
1065
|
+
if (existing) return existing;
|
|
1066
|
+
const promise = (async () => {
|
|
1067
|
+
await this.connect();
|
|
1068
|
+
if (params?.$expand && params.$expand.length > 0) {
|
|
1069
|
+
const result2 = await this.rawFindWithPopulate(resource, params);
|
|
1070
|
+
return this.normalizeQueryResult(result2, params);
|
|
1071
|
+
}
|
|
1072
|
+
const queryOptions = this.convertQueryParams(params);
|
|
1073
|
+
const result = await this.client.data.find(resource, queryOptions);
|
|
1074
|
+
return this.normalizeQueryResult(result, params);
|
|
1075
|
+
})();
|
|
1076
|
+
this.inflightFinds.set(key, promise);
|
|
1077
|
+
promise.finally(() => {
|
|
1078
|
+
if (this.inflightFinds.get(key) === promise) {
|
|
1079
|
+
this.inflightFinds.delete(key);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
return promise;
|
|
1008
1083
|
}
|
|
1009
1084
|
/**
|
|
1010
1085
|
* Find a single record by ID.
|
|
@@ -1298,7 +1373,18 @@ var ObjectStackAdapter = class {
|
|
|
1298
1373
|
const isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
|
|
1299
1374
|
if (!isEmpty) {
|
|
1300
1375
|
if (Array.isArray(params.$filter)) {
|
|
1301
|
-
|
|
1376
|
+
const isObjectForm = params.$filter.length > 0 && typeof params.$filter[0] === "object" && !Array.isArray(params.$filter[0]) && params.$filter[0].field !== void 0;
|
|
1377
|
+
if (isObjectForm) {
|
|
1378
|
+
const tuples = params.$filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
1379
|
+
if (tuples.length === 0) {
|
|
1380
|
+
} else if (tuples.length === 1) {
|
|
1381
|
+
options.filters = tuples[0];
|
|
1382
|
+
} else {
|
|
1383
|
+
options.filters = ["and", ...tuples];
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
options.filters = params.$filter;
|
|
1387
|
+
}
|
|
1302
1388
|
} else {
|
|
1303
1389
|
options.filters = convertFiltersToAST(params.$filter);
|
|
1304
1390
|
}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -787,6 +787,57 @@ function calculateAutoLayout(items, canvasWidth, padding = 40, gap = 40) {
|
|
|
787
787
|
}
|
|
788
788
|
|
|
789
789
|
// src/index.ts
|
|
790
|
+
var FILTER_OPERATOR_ALIASES = {
|
|
791
|
+
equals: "=",
|
|
792
|
+
eq: "=",
|
|
793
|
+
"==": "=",
|
|
794
|
+
not_equals: "!=",
|
|
795
|
+
notequals: "!=",
|
|
796
|
+
ne: "!=",
|
|
797
|
+
greater_than: ">",
|
|
798
|
+
greaterthan: ">",
|
|
799
|
+
gt: ">",
|
|
800
|
+
greater_than_or_equal: ">=",
|
|
801
|
+
greater_than_or_equals: ">=",
|
|
802
|
+
greaterthanorequal: ">=",
|
|
803
|
+
gte: ">=",
|
|
804
|
+
less_than: "<",
|
|
805
|
+
lessthan: "<",
|
|
806
|
+
lt: "<",
|
|
807
|
+
less_than_or_equal: "<=",
|
|
808
|
+
less_than_or_equals: "<=",
|
|
809
|
+
lessthanorequal: "<=",
|
|
810
|
+
lte: "<=",
|
|
811
|
+
in: "in",
|
|
812
|
+
not_in: "nin",
|
|
813
|
+
notin: "nin",
|
|
814
|
+
nin: "nin",
|
|
815
|
+
contains: "contains",
|
|
816
|
+
not_contains: "notcontains",
|
|
817
|
+
notcontains: "notcontains",
|
|
818
|
+
starts_with: "startswith",
|
|
819
|
+
startswith: "startswith",
|
|
820
|
+
ends_with: "endswith",
|
|
821
|
+
endswith: "endswith",
|
|
822
|
+
between: "between",
|
|
823
|
+
is_null: "isnull",
|
|
824
|
+
isnull: "isnull",
|
|
825
|
+
is_not_null: "isnotnull",
|
|
826
|
+
isnotnull: "isnotnull"
|
|
827
|
+
};
|
|
828
|
+
function normalizeFilterOperator(op) {
|
|
829
|
+
if (typeof op !== "string") return null;
|
|
830
|
+
const lower = op.toLowerCase();
|
|
831
|
+
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
832
|
+
}
|
|
833
|
+
function objectFilterEntryToAST(entry) {
|
|
834
|
+
if (!entry || typeof entry !== "object") return null;
|
|
835
|
+
const field = entry.field ?? entry.name;
|
|
836
|
+
const rawOp = entry.operator ?? entry.op ?? "=";
|
|
837
|
+
const op = normalizeFilterOperator(rawOp);
|
|
838
|
+
if (!field || !op) return null;
|
|
839
|
+
return [String(field), op, entry.value];
|
|
840
|
+
}
|
|
790
841
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
791
842
|
async function getSharedDiscovery(baseUrl, fetcher) {
|
|
792
843
|
const key = baseUrl || "<default>";
|
|
@@ -802,6 +853,13 @@ async function getSharedDiscovery(baseUrl, fetcher) {
|
|
|
802
853
|
function clearSharedDiscoveryCache() {
|
|
803
854
|
discoveryCache.clear();
|
|
804
855
|
}
|
|
856
|
+
function stableStringify(value) {
|
|
857
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
858
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
859
|
+
const obj = value;
|
|
860
|
+
const keys = Object.keys(obj).sort();
|
|
861
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
862
|
+
}
|
|
805
863
|
var ObjectStackAdapter = class {
|
|
806
864
|
constructor(config) {
|
|
807
865
|
__publicField(this, "client");
|
|
@@ -818,6 +876,11 @@ var ObjectStackAdapter = class {
|
|
|
818
876
|
__publicField(this, "baseUrl");
|
|
819
877
|
__publicField(this, "token");
|
|
820
878
|
__publicField(this, "fetchImpl");
|
|
879
|
+
// In-flight find() requests keyed by resource + serialized params.
|
|
880
|
+
// Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
|
|
881
|
+
// multiple sibling components requesting the same dataset on first paint)
|
|
882
|
+
// into a single network round trip.
|
|
883
|
+
__publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
|
|
821
884
|
this.client = new ObjectStackClient(config);
|
|
822
885
|
this.metadataCache = new MetadataCache(config.cache);
|
|
823
886
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -955,14 +1018,26 @@ var ObjectStackAdapter = class {
|
|
|
955
1018
|
* Converts OData-style params to ObjectStack query options.
|
|
956
1019
|
*/
|
|
957
1020
|
async find(resource, params) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1021
|
+
const key = `${resource}::${stableStringify(params)}`;
|
|
1022
|
+
const existing = this.inflightFinds.get(key);
|
|
1023
|
+
if (existing) return existing;
|
|
1024
|
+
const promise = (async () => {
|
|
1025
|
+
await this.connect();
|
|
1026
|
+
if (params?.$expand && params.$expand.length > 0) {
|
|
1027
|
+
const result2 = await this.rawFindWithPopulate(resource, params);
|
|
1028
|
+
return this.normalizeQueryResult(result2, params);
|
|
1029
|
+
}
|
|
1030
|
+
const queryOptions = this.convertQueryParams(params);
|
|
1031
|
+
const result = await this.client.data.find(resource, queryOptions);
|
|
1032
|
+
return this.normalizeQueryResult(result, params);
|
|
1033
|
+
})();
|
|
1034
|
+
this.inflightFinds.set(key, promise);
|
|
1035
|
+
promise.finally(() => {
|
|
1036
|
+
if (this.inflightFinds.get(key) === promise) {
|
|
1037
|
+
this.inflightFinds.delete(key);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
return promise;
|
|
966
1041
|
}
|
|
967
1042
|
/**
|
|
968
1043
|
* Find a single record by ID.
|
|
@@ -1256,7 +1331,18 @@ var ObjectStackAdapter = class {
|
|
|
1256
1331
|
const isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
|
|
1257
1332
|
if (!isEmpty) {
|
|
1258
1333
|
if (Array.isArray(params.$filter)) {
|
|
1259
|
-
|
|
1334
|
+
const isObjectForm = params.$filter.length > 0 && typeof params.$filter[0] === "object" && !Array.isArray(params.$filter[0]) && params.$filter[0].field !== void 0;
|
|
1335
|
+
if (isObjectForm) {
|
|
1336
|
+
const tuples = params.$filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
1337
|
+
if (tuples.length === 0) {
|
|
1338
|
+
} else if (tuples.length === 1) {
|
|
1339
|
+
options.filters = tuples[0];
|
|
1340
|
+
} else {
|
|
1341
|
+
options.filters = ["and", ...tuples];
|
|
1342
|
+
}
|
|
1343
|
+
} else {
|
|
1344
|
+
options.filters = params.$filter;
|
|
1345
|
+
}
|
|
1260
1346
|
} else {
|
|
1261
1347
|
options.filters = convertFiltersToAST(params.$filter);
|
|
1262
1348
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "ObjectStack Data Adapter for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@objectstack/client": "^4.0.4",
|
|
26
|
-
"@object-ui/core": "
|
|
27
|
-
"@object-ui/types": "
|
|
26
|
+
"@object-ui/core": "4.0.1",
|
|
27
|
+
"@object-ui/types": "4.0.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,66 @@ import {
|
|
|
18
18
|
createErrorFromResponse,
|
|
19
19
|
} from './errors';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Map human-readable filter operator names produced by SDUI view configs
|
|
23
|
+
* (e.g. `lead.view.ts`) to the canonical operator symbols expected by the
|
|
24
|
+
* ObjectStack server's filter AST. Unknown operators fall through unchanged
|
|
25
|
+
* so existing AST-style entries keep working.
|
|
26
|
+
*/
|
|
27
|
+
const FILTER_OPERATOR_ALIASES: Record<string, string> = {
|
|
28
|
+
equals: '=',
|
|
29
|
+
eq: '=',
|
|
30
|
+
'==': '=',
|
|
31
|
+
not_equals: '!=',
|
|
32
|
+
notequals: '!=',
|
|
33
|
+
ne: '!=',
|
|
34
|
+
greater_than: '>',
|
|
35
|
+
greaterthan: '>',
|
|
36
|
+
gt: '>',
|
|
37
|
+
greater_than_or_equal: '>=',
|
|
38
|
+
greater_than_or_equals: '>=',
|
|
39
|
+
greaterthanorequal: '>=',
|
|
40
|
+
gte: '>=',
|
|
41
|
+
less_than: '<',
|
|
42
|
+
lessthan: '<',
|
|
43
|
+
lt: '<',
|
|
44
|
+
less_than_or_equal: '<=',
|
|
45
|
+
less_than_or_equals: '<=',
|
|
46
|
+
lessthanorequal: '<=',
|
|
47
|
+
lte: '<=',
|
|
48
|
+
in: 'in',
|
|
49
|
+
not_in: 'nin',
|
|
50
|
+
notin: 'nin',
|
|
51
|
+
nin: 'nin',
|
|
52
|
+
contains: 'contains',
|
|
53
|
+
not_contains: 'notcontains',
|
|
54
|
+
notcontains: 'notcontains',
|
|
55
|
+
starts_with: 'startswith',
|
|
56
|
+
startswith: 'startswith',
|
|
57
|
+
ends_with: 'endswith',
|
|
58
|
+
endswith: 'endswith',
|
|
59
|
+
between: 'between',
|
|
60
|
+
is_null: 'isnull',
|
|
61
|
+
isnull: 'isnull',
|
|
62
|
+
is_not_null: 'isnotnull',
|
|
63
|
+
isnotnull: 'isnotnull',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function normalizeFilterOperator(op: unknown): string | null {
|
|
67
|
+
if (typeof op !== 'string') return null;
|
|
68
|
+
const lower = op.toLowerCase();
|
|
69
|
+
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function objectFilterEntryToAST(entry: any): [string, string, any] | null {
|
|
73
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
74
|
+
const field = entry.field ?? entry.name;
|
|
75
|
+
const rawOp = entry.operator ?? entry.op ?? '=';
|
|
76
|
+
const op = normalizeFilterOperator(rawOp);
|
|
77
|
+
if (!field || !op) return null;
|
|
78
|
+
return [String(field), op, entry.value];
|
|
79
|
+
}
|
|
80
|
+
|
|
21
81
|
// Module-level discovery cache. Multiple ObjectStackAdapter instances pointed
|
|
22
82
|
// at the same baseUrl (e.g. ConditionalAuthWrapper's throwaway adapter +
|
|
23
83
|
// AdapterProvider's main adapter) would otherwise each fire `/discovery`. By
|
|
@@ -88,6 +148,20 @@ export type BatchProgressListener = (event: BatchProgressEvent) => void;
|
|
|
88
148
|
// Re-export FileUploadResult from types for consumers
|
|
89
149
|
export type { FileUploadResult } from '@object-ui/types';
|
|
90
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Deterministic JSON.stringify with sorted object keys, used to build cache
|
|
153
|
+
* keys for in-flight request coalescing. Produces identical output for
|
|
154
|
+
* `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` so callers that build params in
|
|
155
|
+
* different orders still hit the same key.
|
|
156
|
+
*/
|
|
157
|
+
function stableStringify(value: unknown): string {
|
|
158
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
159
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
160
|
+
const obj = value as Record<string, unknown>;
|
|
161
|
+
const keys = Object.keys(obj).sort();
|
|
162
|
+
return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
91
165
|
/**
|
|
92
166
|
* ObjectStack Data Source Adapter
|
|
93
167
|
*
|
|
@@ -132,6 +206,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
132
206
|
private baseUrl: string;
|
|
133
207
|
private token?: string;
|
|
134
208
|
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
209
|
+
// In-flight find() requests keyed by resource + serialized params.
|
|
210
|
+
// Coalesces concurrent identical reads (e.g. React StrictMode double-mount,
|
|
211
|
+
// multiple sibling components requesting the same dataset on first paint)
|
|
212
|
+
// into a single network round trip.
|
|
213
|
+
private inflightFinds = new Map<string, Promise<QueryResult<T>>>();
|
|
135
214
|
|
|
136
215
|
constructor(config: {
|
|
137
216
|
baseUrl: string;
|
|
@@ -323,22 +402,38 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
323
402
|
* Converts OData-style params to ObjectStack query options.
|
|
324
403
|
*/
|
|
325
404
|
async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
|
|
326
|
-
|
|
405
|
+
const key = `${resource}::${stableStringify(params)}`;
|
|
406
|
+
const existing = this.inflightFinds.get(key);
|
|
407
|
+
if (existing) return existing;
|
|
327
408
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
409
|
+
const promise = (async () => {
|
|
410
|
+
await this.connect();
|
|
411
|
+
|
|
412
|
+
// When $expand is requested, use a raw GET request to the REST API with
|
|
413
|
+
// `populate` as a URL query param. The server's REST plugin routes
|
|
414
|
+
// GET /data/:object to protocol.findData({ object, query: req.query }),
|
|
415
|
+
// which parses `populate` (comma-separated) into an array for lookup expansion.
|
|
416
|
+
// We use a raw request because the client SDK's data.find() QueryOptions
|
|
417
|
+
// interface does not include populate/expand fields.
|
|
418
|
+
if (params?.$expand && params.$expand.length > 0) {
|
|
419
|
+
const result = await this.rawFindWithPopulate(resource, params);
|
|
420
|
+
return this.normalizeQueryResult(result, params);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const queryOptions = this.convertQueryParams(params);
|
|
424
|
+
const result: unknown = await this.client.data.find<T>(resource, queryOptions);
|
|
336
425
|
return this.normalizeQueryResult(result, params);
|
|
337
|
-
}
|
|
426
|
+
})();
|
|
338
427
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
428
|
+
this.inflightFinds.set(key, promise);
|
|
429
|
+
promise.finally(() => {
|
|
430
|
+
// Only clear if the entry still points at this promise; a later call
|
|
431
|
+
// that started after settle may have already replaced it.
|
|
432
|
+
if (this.inflightFinds.get(key) === promise) {
|
|
433
|
+
this.inflightFinds.delete(key);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return promise;
|
|
342
437
|
}
|
|
343
438
|
|
|
344
439
|
/**
|
|
@@ -714,8 +809,33 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
714
809
|
: typeof params.$filter === 'object' && Object.keys(params.$filter).length === 0;
|
|
715
810
|
if (!isEmpty) {
|
|
716
811
|
if (Array.isArray(params.$filter)) {
|
|
717
|
-
//
|
|
718
|
-
|
|
812
|
+
// Two array shapes are accepted from upstream:
|
|
813
|
+
// 1. AST tuples: [field, op, value] — pass through.
|
|
814
|
+
// 2. Object form: [{ field, operator, value }, ...] — server-driven
|
|
815
|
+
// view configs (lead.view.ts etc.) use this. Translate each
|
|
816
|
+
// entry into the AST tuple shape and map human-readable
|
|
817
|
+
// operator names (`greater_than_or_equal`, `in`, `contains`,
|
|
818
|
+
// …) to the canonical symbols the server understands.
|
|
819
|
+
const isObjectForm = params.$filter.length > 0
|
|
820
|
+
&& typeof params.$filter[0] === 'object'
|
|
821
|
+
&& !Array.isArray(params.$filter[0])
|
|
822
|
+
&& (params.$filter[0] as any).field !== undefined;
|
|
823
|
+
if (isObjectForm) {
|
|
824
|
+
const tuples = (params.$filter as any[])
|
|
825
|
+
.map(entry => objectFilterEntryToAST(entry))
|
|
826
|
+
.filter((t): t is [string, string, any] => t !== null);
|
|
827
|
+
if (tuples.length === 0) {
|
|
828
|
+
// All entries were unrecognized — drop the filter rather than
|
|
829
|
+
// sending a malformed array.
|
|
830
|
+
} else if (tuples.length === 1) {
|
|
831
|
+
options.filters = tuples[0];
|
|
832
|
+
} else {
|
|
833
|
+
options.filters = ['and', ...tuples];
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
// Already in AST format
|
|
837
|
+
options.filters = params.$filter;
|
|
838
|
+
}
|
|
719
839
|
} else {
|
|
720
840
|
options.filters = convertFiltersToAST(params.$filter);
|
|
721
841
|
}
|