@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 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
- await this.connect();
1001
- if (params?.$expand && params.$expand.length > 0) {
1002
- const result2 = await this.rawFindWithPopulate(resource, params);
1003
- return this.normalizeQueryResult(result2, params);
1004
- }
1005
- const queryOptions = this.convertQueryParams(params);
1006
- const result = await this.client.data.find(resource, queryOptions);
1007
- return this.normalizeQueryResult(result, params);
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
- options.filters = params.$filter;
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
@@ -723,6 +723,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
723
723
  private baseUrl;
724
724
  private token?;
725
725
  private fetchImpl;
726
+ private inflightFinds;
726
727
  constructor(config: {
727
728
  baseUrl: string;
728
729
  token?: string;
package/dist/index.d.ts CHANGED
@@ -723,6 +723,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
723
723
  private baseUrl;
724
724
  private token?;
725
725
  private fetchImpl;
726
+ private inflightFinds;
726
727
  constructor(config: {
727
728
  baseUrl: string;
728
729
  token?: string;
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
- await this.connect();
959
- if (params?.$expand && params.$expand.length > 0) {
960
- const result2 = await this.rawFindWithPopulate(resource, params);
961
- return this.normalizeQueryResult(result2, params);
962
- }
963
- const queryOptions = this.convertQueryParams(params);
964
- const result = await this.client.data.find(resource, queryOptions);
965
- return this.normalizeQueryResult(result, params);
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
- options.filters = params.$filter;
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.3.2",
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": "3.3.2",
27
- "@object-ui/types": "3.3.2"
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
- await this.connect();
405
+ const key = `${resource}::${stableStringify(params)}`;
406
+ const existing = this.inflightFinds.get(key);
407
+ if (existing) return existing;
327
408
 
328
- // When $expand is requested, use a raw GET request to the REST API with
329
- // `populate` as a URL query param. The server's REST plugin routes
330
- // GET /data/:object to protocol.findData({ object, query: req.query }),
331
- // which parses `populate` (comma-separated) into an array for lookup expansion.
332
- // We use a raw request because the client SDK's data.find() QueryOptions
333
- // interface does not include populate/expand fields.
334
- if (params?.$expand && params.$expand.length > 0) {
335
- const result = await this.rawFindWithPopulate(resource, params);
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
- const queryOptions = this.convertQueryParams(params);
340
- const result: unknown = await this.client.data.find<T>(resource, queryOptions);
341
- return this.normalizeQueryResult(result, params);
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
- // Assume active AST format if it's already an array
718
- options.filters = params.$filter;
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
  }