@object-ui/data-objectstack 4.0.10 → 4.0.12
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 +14 -0
- package/dist/index.cjs +47 -8
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +47 -8
- package/package.json +3 -3
- package/src/index.ts +86 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @object-ui/data-objectstack
|
|
2
2
|
|
|
3
|
+
## 4.0.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@4.0.12
|
|
8
|
+
- @object-ui/core@4.0.12
|
|
9
|
+
|
|
10
|
+
## 4.0.11
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- @object-ui/types@4.0.11
|
|
15
|
+
- @object-ui/core@4.0.11
|
|
16
|
+
|
|
3
17
|
## 4.0.10
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -881,6 +881,26 @@ function objectFilterEntryToAST(entry) {
|
|
|
881
881
|
if (!field || !op) return null;
|
|
882
882
|
return [String(field), op, entry.value];
|
|
883
883
|
}
|
|
884
|
+
function translateFilterToAST(filter) {
|
|
885
|
+
if (filter === void 0 || filter === null) return void 0;
|
|
886
|
+
if (Array.isArray(filter)) {
|
|
887
|
+
if (filter.length === 0) return void 0;
|
|
888
|
+
const first = filter[0];
|
|
889
|
+
const isObjectForm = filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
|
|
890
|
+
if (isObjectForm) {
|
|
891
|
+
const tuples = filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
892
|
+
if (tuples.length === 0) return void 0;
|
|
893
|
+
if (tuples.length === 1) return tuples[0];
|
|
894
|
+
return ["and", ...tuples];
|
|
895
|
+
}
|
|
896
|
+
return filter;
|
|
897
|
+
}
|
|
898
|
+
if (typeof filter === "object") {
|
|
899
|
+
if (Object.keys(filter).length === 0) return void 0;
|
|
900
|
+
return filter;
|
|
901
|
+
}
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
884
904
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
885
905
|
async function getSharedDiscovery(baseUrl, fetcher) {
|
|
886
906
|
const key = baseUrl || "<default>";
|
|
@@ -962,6 +982,11 @@ var ObjectStackAdapter = class {
|
|
|
962
982
|
// multiple sibling components requesting the same dataset on first paint)
|
|
963
983
|
// into a single network round trip.
|
|
964
984
|
__publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
|
|
985
|
+
// Resources that have responded 404 at least once (collection not installed
|
|
986
|
+
// on this backend). Subsequent find() calls short-circuit to an empty result
|
|
987
|
+
// so optional collections like sys_presence don't hammer the server with
|
|
988
|
+
// failing requests on every record open / panel render.
|
|
989
|
+
__publicField(this, "missingResources", /* @__PURE__ */ new Set());
|
|
965
990
|
this.client = new import_client.ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
966
991
|
this.metadataCache = new MetadataCache(config.cache);
|
|
967
992
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -1099,18 +1124,29 @@ var ObjectStackAdapter = class {
|
|
|
1099
1124
|
* Converts OData-style params to ObjectStack query options.
|
|
1100
1125
|
*/
|
|
1101
1126
|
async find(resource, params) {
|
|
1127
|
+
if (this.missingResources.has(resource)) {
|
|
1128
|
+
return { data: [], total: 0 };
|
|
1129
|
+
}
|
|
1102
1130
|
const key = `${resource}::${stableStringify(params)}`;
|
|
1103
1131
|
const existing = this.inflightFinds.get(key);
|
|
1104
1132
|
if (existing) return existing;
|
|
1105
1133
|
const promise = (async () => {
|
|
1106
1134
|
await this.connect();
|
|
1107
1135
|
if (params?.$expand && params.$expand.length > 0) {
|
|
1108
|
-
const
|
|
1109
|
-
return this.normalizeQueryResult(
|
|
1136
|
+
const result = await this.rawFindWithPopulate(resource, params);
|
|
1137
|
+
return this.normalizeQueryResult(result, params);
|
|
1110
1138
|
}
|
|
1111
1139
|
const queryOptions = this.convertQueryParams(params);
|
|
1112
|
-
|
|
1113
|
-
|
|
1140
|
+
try {
|
|
1141
|
+
const result = await this.client.data.find(resource, queryOptions);
|
|
1142
|
+
return this.normalizeQueryResult(result, params);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (is404Error(err)) {
|
|
1145
|
+
this.missingResources.add(resource);
|
|
1146
|
+
return { data: [], total: 0 };
|
|
1147
|
+
}
|
|
1148
|
+
throw err;
|
|
1149
|
+
}
|
|
1114
1150
|
})();
|
|
1115
1151
|
this.inflightFinds.set(key, promise);
|
|
1116
1152
|
const cleanup = () => {
|
|
@@ -1371,9 +1407,9 @@ var ObjectStackAdapter = class {
|
|
|
1371
1407
|
}
|
|
1372
1408
|
}
|
|
1373
1409
|
if (params.$filter !== void 0 && params.$filter !== null) {
|
|
1374
|
-
const
|
|
1375
|
-
if (
|
|
1376
|
-
queryParams.set("filter", JSON.stringify(
|
|
1410
|
+
const translated = translateFilterToAST(params.$filter);
|
|
1411
|
+
if (translated !== void 0) {
|
|
1412
|
+
queryParams.set("filter", JSON.stringify(translated));
|
|
1377
1413
|
}
|
|
1378
1414
|
}
|
|
1379
1415
|
const baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
@@ -1640,7 +1676,10 @@ var ObjectStackAdapter = class {
|
|
|
1640
1676
|
await this.connect();
|
|
1641
1677
|
let name = String(spec?.name || "").trim();
|
|
1642
1678
|
if (!name) {
|
|
1643
|
-
|
|
1679
|
+
let base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
1680
|
+
if (!base || /^[0-9]/.test(base)) {
|
|
1681
|
+
base = base ? `view_${base}` : "view";
|
|
1682
|
+
}
|
|
1644
1683
|
const suffix = Date.now().toString(36);
|
|
1645
1684
|
name = `${base}_${suffix}`;
|
|
1646
1685
|
}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -838,6 +838,26 @@ function objectFilterEntryToAST(entry) {
|
|
|
838
838
|
if (!field || !op) return null;
|
|
839
839
|
return [String(field), op, entry.value];
|
|
840
840
|
}
|
|
841
|
+
function translateFilterToAST(filter) {
|
|
842
|
+
if (filter === void 0 || filter === null) return void 0;
|
|
843
|
+
if (Array.isArray(filter)) {
|
|
844
|
+
if (filter.length === 0) return void 0;
|
|
845
|
+
const first = filter[0];
|
|
846
|
+
const isObjectForm = filter.length > 0 && typeof first === "object" && first !== null && !Array.isArray(first) && first.field !== void 0;
|
|
847
|
+
if (isObjectForm) {
|
|
848
|
+
const tuples = filter.map((entry) => objectFilterEntryToAST(entry)).filter((t) => t !== null);
|
|
849
|
+
if (tuples.length === 0) return void 0;
|
|
850
|
+
if (tuples.length === 1) return tuples[0];
|
|
851
|
+
return ["and", ...tuples];
|
|
852
|
+
}
|
|
853
|
+
return filter;
|
|
854
|
+
}
|
|
855
|
+
if (typeof filter === "object") {
|
|
856
|
+
if (Object.keys(filter).length === 0) return void 0;
|
|
857
|
+
return filter;
|
|
858
|
+
}
|
|
859
|
+
return void 0;
|
|
860
|
+
}
|
|
841
861
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
842
862
|
async function getSharedDiscovery(baseUrl, fetcher) {
|
|
843
863
|
const key = baseUrl || "<default>";
|
|
@@ -919,6 +939,11 @@ var ObjectStackAdapter = class {
|
|
|
919
939
|
// multiple sibling components requesting the same dataset on first paint)
|
|
920
940
|
// into a single network round trip.
|
|
921
941
|
__publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
|
|
942
|
+
// Resources that have responded 404 at least once (collection not installed
|
|
943
|
+
// on this backend). Subsequent find() calls short-circuit to an empty result
|
|
944
|
+
// so optional collections like sys_presence don't hammer the server with
|
|
945
|
+
// failing requests on every record open / panel render.
|
|
946
|
+
__publicField(this, "missingResources", /* @__PURE__ */ new Set());
|
|
922
947
|
this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
923
948
|
this.metadataCache = new MetadataCache(config.cache);
|
|
924
949
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -1056,18 +1081,29 @@ var ObjectStackAdapter = class {
|
|
|
1056
1081
|
* Converts OData-style params to ObjectStack query options.
|
|
1057
1082
|
*/
|
|
1058
1083
|
async find(resource, params) {
|
|
1084
|
+
if (this.missingResources.has(resource)) {
|
|
1085
|
+
return { data: [], total: 0 };
|
|
1086
|
+
}
|
|
1059
1087
|
const key = `${resource}::${stableStringify(params)}`;
|
|
1060
1088
|
const existing = this.inflightFinds.get(key);
|
|
1061
1089
|
if (existing) return existing;
|
|
1062
1090
|
const promise = (async () => {
|
|
1063
1091
|
await this.connect();
|
|
1064
1092
|
if (params?.$expand && params.$expand.length > 0) {
|
|
1065
|
-
const
|
|
1066
|
-
return this.normalizeQueryResult(
|
|
1093
|
+
const result = await this.rawFindWithPopulate(resource, params);
|
|
1094
|
+
return this.normalizeQueryResult(result, params);
|
|
1067
1095
|
}
|
|
1068
1096
|
const queryOptions = this.convertQueryParams(params);
|
|
1069
|
-
|
|
1070
|
-
|
|
1097
|
+
try {
|
|
1098
|
+
const result = await this.client.data.find(resource, queryOptions);
|
|
1099
|
+
return this.normalizeQueryResult(result, params);
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
if (is404Error(err)) {
|
|
1102
|
+
this.missingResources.add(resource);
|
|
1103
|
+
return { data: [], total: 0 };
|
|
1104
|
+
}
|
|
1105
|
+
throw err;
|
|
1106
|
+
}
|
|
1071
1107
|
})();
|
|
1072
1108
|
this.inflightFinds.set(key, promise);
|
|
1073
1109
|
const cleanup = () => {
|
|
@@ -1328,9 +1364,9 @@ var ObjectStackAdapter = class {
|
|
|
1328
1364
|
}
|
|
1329
1365
|
}
|
|
1330
1366
|
if (params.$filter !== void 0 && params.$filter !== null) {
|
|
1331
|
-
const
|
|
1332
|
-
if (
|
|
1333
|
-
queryParams.set("filter", JSON.stringify(
|
|
1367
|
+
const translated = translateFilterToAST(params.$filter);
|
|
1368
|
+
if (translated !== void 0) {
|
|
1369
|
+
queryParams.set("filter", JSON.stringify(translated));
|
|
1334
1370
|
}
|
|
1335
1371
|
}
|
|
1336
1372
|
const baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
@@ -1597,7 +1633,10 @@ var ObjectStackAdapter = class {
|
|
|
1597
1633
|
await this.connect();
|
|
1598
1634
|
let name = String(spec?.name || "").trim();
|
|
1599
1635
|
if (!name) {
|
|
1600
|
-
|
|
1636
|
+
let base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
1637
|
+
if (!base || /^[0-9]/.test(base)) {
|
|
1638
|
+
base = base ? `view_${base}` : "view";
|
|
1639
|
+
}
|
|
1601
1640
|
const suffix = Date.now().toString(36);
|
|
1602
1641
|
name = `${base}_${suffix}`;
|
|
1603
1642
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.12",
|
|
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.5",
|
|
26
|
-
"@object-ui/core": "4.0.
|
|
27
|
-
"@object-ui/types": "4.0.
|
|
26
|
+
"@object-ui/core": "4.0.12",
|
|
27
|
+
"@object-ui/types": "4.0.12"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,54 @@ function objectFilterEntryToAST(entry: any): [string, string, any] | null {
|
|
|
78
78
|
return [String(field), op, entry.value];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Translate any of the filter shapes accepted by ObjectUI into the AST format
|
|
83
|
+
* understood by the ObjectStack server's `parseFilterAST()`.
|
|
84
|
+
*
|
|
85
|
+
* Accepted inputs:
|
|
86
|
+
* - `[{ field, operator, value }, ...]` — ViewFilterRule[] from view configs
|
|
87
|
+
* - `[field, op, value]` — single AST tuple (passed through)
|
|
88
|
+
* - `['and'|'or', ...children]` — logical AST node (passed through)
|
|
89
|
+
* - `[[...], [...]]` — legacy nested AST (passed through)
|
|
90
|
+
* - `{ field: value }` / `{ field: { $op: value } }` — MongoDB-style object
|
|
91
|
+
*
|
|
92
|
+
* Returns `undefined` when the input is empty/unrecognized so callers can
|
|
93
|
+
* skip emitting `?filter=` entirely.
|
|
94
|
+
*/
|
|
95
|
+
function translateFilterToAST(filter: unknown): unknown | undefined {
|
|
96
|
+
if (filter === undefined || filter === null) return undefined;
|
|
97
|
+
|
|
98
|
+
if (Array.isArray(filter)) {
|
|
99
|
+
if (filter.length === 0) return undefined;
|
|
100
|
+
|
|
101
|
+
// Object form: [{ field, operator, value }, ...]
|
|
102
|
+
const first = filter[0];
|
|
103
|
+
const isObjectForm = filter.length > 0
|
|
104
|
+
&& typeof first === 'object'
|
|
105
|
+
&& first !== null
|
|
106
|
+
&& !Array.isArray(first)
|
|
107
|
+
&& (first as any).field !== undefined;
|
|
108
|
+
if (isObjectForm) {
|
|
109
|
+
const tuples = (filter as any[])
|
|
110
|
+
.map(entry => objectFilterEntryToAST(entry))
|
|
111
|
+
.filter((t): t is [string, string, any] => t !== null);
|
|
112
|
+
if (tuples.length === 0) return undefined;
|
|
113
|
+
if (tuples.length === 1) return tuples[0];
|
|
114
|
+
return ['and', ...tuples];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Already AST — pass through.
|
|
118
|
+
return filter;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof filter === 'object') {
|
|
122
|
+
if (Object.keys(filter as Record<string, unknown>).length === 0) return undefined;
|
|
123
|
+
return filter;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
81
129
|
// Module-level discovery cache. Multiple ObjectStackAdapter instances pointed
|
|
82
130
|
// at the same baseUrl (e.g. ConditionalAuthWrapper's throwaway adapter +
|
|
83
131
|
// AdapterProvider's main adapter) would otherwise each fire `/discovery`. By
|
|
@@ -275,6 +323,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
275
323
|
// multiple sibling components requesting the same dataset on first paint)
|
|
276
324
|
// into a single network round trip.
|
|
277
325
|
private inflightFinds = new Map<string, Promise<QueryResult<T>>>();
|
|
326
|
+
// Resources that have responded 404 at least once (collection not installed
|
|
327
|
+
// on this backend). Subsequent find() calls short-circuit to an empty result
|
|
328
|
+
// so optional collections like sys_presence don't hammer the server with
|
|
329
|
+
// failing requests on every record open / panel render.
|
|
330
|
+
private missingResources = new Set<string>();
|
|
278
331
|
|
|
279
332
|
constructor(config: {
|
|
280
333
|
baseUrl: string;
|
|
@@ -470,6 +523,12 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
470
523
|
* Converts OData-style params to ObjectStack query options.
|
|
471
524
|
*/
|
|
472
525
|
async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
|
|
526
|
+
// Short-circuit when this resource has previously responded 404 — the
|
|
527
|
+
// collection isn't installed on this backend. Callers (AppHeader,
|
|
528
|
+
// RecordDetailView, …) treat empty data as "feature unavailable".
|
|
529
|
+
if (this.missingResources.has(resource)) {
|
|
530
|
+
return { data: [], total: 0 } as QueryResult<T>;
|
|
531
|
+
}
|
|
473
532
|
const key = `${resource}::${stableStringify(params)}`;
|
|
474
533
|
const existing = this.inflightFinds.get(key);
|
|
475
534
|
if (existing) return existing;
|
|
@@ -489,8 +548,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
489
548
|
}
|
|
490
549
|
|
|
491
550
|
const queryOptions = this.convertQueryParams(params);
|
|
492
|
-
|
|
493
|
-
|
|
551
|
+
try {
|
|
552
|
+
const result: unknown = await this.client.data.find<T>(resource, queryOptions);
|
|
553
|
+
return this.normalizeQueryResult(result, params);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
if (is404Error(err)) {
|
|
556
|
+
// Mark the resource so subsequent calls don't repeat the 404.
|
|
557
|
+
this.missingResources.add(resource);
|
|
558
|
+
return { data: [], total: 0 } as QueryResult<T>;
|
|
559
|
+
}
|
|
560
|
+
throw err;
|
|
561
|
+
}
|
|
494
562
|
})();
|
|
495
563
|
|
|
496
564
|
this.inflightFinds.set(key, promise);
|
|
@@ -824,13 +892,15 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
824
892
|
}
|
|
825
893
|
}
|
|
826
894
|
|
|
827
|
-
// Filter —
|
|
895
|
+
// Filter — translate ViewFilterRule[] (`[{field, operator, value}]`)
|
|
896
|
+
// and other shapes into AST tuples the server understands. Without this,
|
|
897
|
+
// server-driven views (e.g. `at_risk_accounts`, `hot_leads`) ship raw
|
|
898
|
+
// `[{field,operator,value}]` arrays which `parseFilterAST` silently
|
|
899
|
+
// discards, returning every record instead of the filtered subset.
|
|
828
900
|
if (params.$filter !== undefined && params.$filter !== null) {
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (!isEmpty) {
|
|
833
|
-
queryParams.set('filter', JSON.stringify(params.$filter));
|
|
901
|
+
const translated = translateFilterToAST(params.$filter);
|
|
902
|
+
if (translated !== undefined) {
|
|
903
|
+
queryParams.set('filter', JSON.stringify(translated));
|
|
834
904
|
}
|
|
835
905
|
}
|
|
836
906
|
|
|
@@ -1188,11 +1258,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1188
1258
|
await this.connect();
|
|
1189
1259
|
let name = String(spec?.name || '').trim();
|
|
1190
1260
|
if (!name) {
|
|
1191
|
-
|
|
1261
|
+
let base = String(spec?.label || objectName || 'view')
|
|
1192
1262
|
.toLowerCase()
|
|
1193
1263
|
.replace(/[^a-z0-9_]+/g, '_')
|
|
1194
1264
|
.replace(/^_+|_+$/g, '')
|
|
1195
|
-
.slice(0, 40)
|
|
1265
|
+
.slice(0, 40);
|
|
1266
|
+
// Spec requires snake_case starting with a letter or underscore.
|
|
1267
|
+
// Labels like "表格 1" collapse to "1" after non-ascii stripping, so we
|
|
1268
|
+
// need a fallback / prefix to keep the identifier valid.
|
|
1269
|
+
if (!base || /^[0-9]/.test(base)) {
|
|
1270
|
+
base = base ? `view_${base}` : 'view';
|
|
1271
|
+
}
|
|
1196
1272
|
const suffix = Date.now().toString(36);
|
|
1197
1273
|
name = `${base}_${suffix}`;
|
|
1198
1274
|
}
|