@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 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 result2 = await this.rawFindWithPopulate(resource, params);
1109
- return this.normalizeQueryResult(result2, params);
1136
+ const result = await this.rawFindWithPopulate(resource, params);
1137
+ return this.normalizeQueryResult(result, params);
1110
1138
  }
1111
1139
  const queryOptions = this.convertQueryParams(params);
1112
- const result = await this.client.data.find(resource, queryOptions);
1113
- return this.normalizeQueryResult(result, params);
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 isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
1375
- if (!isEmpty) {
1376
- queryParams.set("filter", JSON.stringify(params.$filter));
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
- const base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "view";
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
@@ -734,6 +734,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
734
734
  private token?;
735
735
  private fetchImpl;
736
736
  private inflightFinds;
737
+ private missingResources;
737
738
  constructor(config: {
738
739
  baseUrl: string;
739
740
  token?: string;
package/dist/index.d.ts CHANGED
@@ -734,6 +734,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
734
734
  private token?;
735
735
  private fetchImpl;
736
736
  private inflightFinds;
737
+ private missingResources;
737
738
  constructor(config: {
738
739
  baseUrl: string;
739
740
  token?: string;
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 result2 = await this.rawFindWithPopulate(resource, params);
1066
- return this.normalizeQueryResult(result2, params);
1093
+ const result = await this.rawFindWithPopulate(resource, params);
1094
+ return this.normalizeQueryResult(result, params);
1067
1095
  }
1068
1096
  const queryOptions = this.convertQueryParams(params);
1069
- const result = await this.client.data.find(resource, queryOptions);
1070
- return this.normalizeQueryResult(result, params);
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 isEmpty = Array.isArray(params.$filter) ? params.$filter.length === 0 : typeof params.$filter === "object" && Object.keys(params.$filter).length === 0;
1332
- if (!isEmpty) {
1333
- queryParams.set("filter", JSON.stringify(params.$filter));
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
- const base = String(spec?.label || objectName || "view").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "view";
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.10",
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.10",
27
- "@object-ui/types": "4.0.10"
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
- const result: unknown = await this.client.data.find<T>(resource, queryOptions);
493
- return this.normalizeQueryResult(result, params);
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 — drop empty arrays/objects so we don't send `?filter=%5B%5D`
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 isEmpty = Array.isArray(params.$filter)
830
- ? params.$filter.length === 0
831
- : typeof params.$filter === 'object' && Object.keys(params.$filter).length === 0;
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
- const base = String(spec?.label || objectName || 'view')
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) || 'view';
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
  }