@object-ui/data-objectstack 3.1.5 → 3.3.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/dist/index.cjs CHANGED
@@ -213,9 +213,9 @@ var ObjectStackError = class extends Error {
213
213
  */
214
214
  constructor(message, code, statusCode, details) {
215
215
  super(message);
216
- this.code = code;
217
- this.statusCode = statusCode;
218
- this.details = details;
216
+ __publicField(this, "code", code);
217
+ __publicField(this, "statusCode", statusCode);
218
+ __publicField(this, "details", details);
219
219
  this.name = "ObjectStackError";
220
220
  if (Error.captureStackTrace) {
221
221
  Error.captureStackTrace(this, this.constructor);
@@ -269,9 +269,9 @@ var BulkOperationError = class extends ObjectStackError {
269
269
  ...details
270
270
  }
271
271
  );
272
- this.successCount = successCount;
273
- this.failureCount = failureCount;
274
- this.errors = errors;
272
+ __publicField(this, "successCount", successCount);
273
+ __publicField(this, "failureCount", failureCount);
274
+ __publicField(this, "errors", errors);
275
275
  this.name = "BulkOperationError";
276
276
  }
277
277
  /**
@@ -298,7 +298,7 @@ var ConnectionError = class extends ObjectStackError {
298
298
  statusCode || 503,
299
299
  { url, ...details }
300
300
  );
301
- this.url = url;
301
+ __publicField(this, "url", url);
302
302
  this.name = "ConnectionError";
303
303
  }
304
304
  };
@@ -333,8 +333,8 @@ var ValidationError = class extends ObjectStackError {
333
333
  ...details
334
334
  }
335
335
  );
336
- this.field = field;
337
- this.validationErrors = validationErrors;
336
+ __publicField(this, "field", field);
337
+ __publicField(this, "validationErrors", validationErrors);
338
338
  this.name = "ValidationError";
339
339
  }
340
340
  /**
@@ -391,7 +391,7 @@ function isErrorType(error, errorClass) {
391
391
  // src/cloud.ts
392
392
  var CloudOperations = class {
393
393
  constructor(getClient) {
394
- this.getClient = getClient;
394
+ __publicField(this, "getClient", getClient);
395
395
  }
396
396
  /**
397
397
  * Deploy an application to the cloud.
@@ -1318,19 +1318,26 @@ var ObjectStackAdapter = class {
1318
1318
  async aggregate(resource, params) {
1319
1319
  await this.connect();
1320
1320
  try {
1321
+ const measureName = params.function === "count" ? "count" : `${params.field}_${params.function}`;
1321
1322
  const payload = {
1322
- object: resource,
1323
- measures: [{ field: params.field, function: params.function }],
1324
- dimensions: [params.groupBy]
1323
+ cube: resource,
1324
+ measures: [measureName],
1325
+ // When groupBy is '_all' no dimensions are needed (single-bucket).
1326
+ dimensions: params.groupBy && params.groupBy !== "_all" ? [params.groupBy] : []
1325
1327
  };
1326
1328
  if (params.filter) {
1327
1329
  payload.filters = params.filter;
1328
1330
  }
1329
1331
  const data = await this.client.analytics.query(payload);
1330
- if (Array.isArray(data)) return data;
1331
- if (data?.data && Array.isArray(data.data)) return data.data;
1332
- if (data?.results && Array.isArray(data.results)) return data.results;
1333
- return [];
1332
+ const rawRows = Array.isArray(data) ? data : data?.rows && Array.isArray(data.rows) ? data.rows : data?.data && Array.isArray(data.data) ? data.data : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows : data?.results && Array.isArray(data.results) ? data.results : [];
1333
+ return rawRows.map((row) => {
1334
+ const mapped = { ...row };
1335
+ if (measureName !== params.field && measureName in mapped) {
1336
+ mapped[params.field] = mapped[measureName];
1337
+ delete mapped[measureName];
1338
+ }
1339
+ return mapped;
1340
+ });
1334
1341
  } catch {
1335
1342
  const result = await this.find(resource);
1336
1343
  const records = result.data || [];
package/dist/index.js CHANGED
@@ -173,9 +173,9 @@ var ObjectStackError = class extends Error {
173
173
  */
174
174
  constructor(message, code, statusCode, details) {
175
175
  super(message);
176
- this.code = code;
177
- this.statusCode = statusCode;
178
- this.details = details;
176
+ __publicField(this, "code", code);
177
+ __publicField(this, "statusCode", statusCode);
178
+ __publicField(this, "details", details);
179
179
  this.name = "ObjectStackError";
180
180
  if (Error.captureStackTrace) {
181
181
  Error.captureStackTrace(this, this.constructor);
@@ -229,9 +229,9 @@ var BulkOperationError = class extends ObjectStackError {
229
229
  ...details
230
230
  }
231
231
  );
232
- this.successCount = successCount;
233
- this.failureCount = failureCount;
234
- this.errors = errors;
232
+ __publicField(this, "successCount", successCount);
233
+ __publicField(this, "failureCount", failureCount);
234
+ __publicField(this, "errors", errors);
235
235
  this.name = "BulkOperationError";
236
236
  }
237
237
  /**
@@ -258,7 +258,7 @@ var ConnectionError = class extends ObjectStackError {
258
258
  statusCode || 503,
259
259
  { url, ...details }
260
260
  );
261
- this.url = url;
261
+ __publicField(this, "url", url);
262
262
  this.name = "ConnectionError";
263
263
  }
264
264
  };
@@ -293,8 +293,8 @@ var ValidationError = class extends ObjectStackError {
293
293
  ...details
294
294
  }
295
295
  );
296
- this.field = field;
297
- this.validationErrors = validationErrors;
296
+ __publicField(this, "field", field);
297
+ __publicField(this, "validationErrors", validationErrors);
298
298
  this.name = "ValidationError";
299
299
  }
300
300
  /**
@@ -351,7 +351,7 @@ function isErrorType(error, errorClass) {
351
351
  // src/cloud.ts
352
352
  var CloudOperations = class {
353
353
  constructor(getClient) {
354
- this.getClient = getClient;
354
+ __publicField(this, "getClient", getClient);
355
355
  }
356
356
  /**
357
357
  * Deploy an application to the cloud.
@@ -1278,19 +1278,26 @@ var ObjectStackAdapter = class {
1278
1278
  async aggregate(resource, params) {
1279
1279
  await this.connect();
1280
1280
  try {
1281
+ const measureName = params.function === "count" ? "count" : `${params.field}_${params.function}`;
1281
1282
  const payload = {
1282
- object: resource,
1283
- measures: [{ field: params.field, function: params.function }],
1284
- dimensions: [params.groupBy]
1283
+ cube: resource,
1284
+ measures: [measureName],
1285
+ // When groupBy is '_all' no dimensions are needed (single-bucket).
1286
+ dimensions: params.groupBy && params.groupBy !== "_all" ? [params.groupBy] : []
1285
1287
  };
1286
1288
  if (params.filter) {
1287
1289
  payload.filters = params.filter;
1288
1290
  }
1289
1291
  const data = await this.client.analytics.query(payload);
1290
- if (Array.isArray(data)) return data;
1291
- if (data?.data && Array.isArray(data.data)) return data.data;
1292
- if (data?.results && Array.isArray(data.results)) return data.results;
1293
- return [];
1292
+ const rawRows = Array.isArray(data) ? data : data?.rows && Array.isArray(data.rows) ? data.rows : data?.data && Array.isArray(data.data) ? data.data : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows : data?.results && Array.isArray(data.results) ? data.results : [];
1293
+ return rawRows.map((row) => {
1294
+ const mapped = { ...row };
1295
+ if (measureName !== params.field && measureName in mapped) {
1296
+ mapped[params.field] = mapped[measureName];
1297
+ delete mapped[measureName];
1298
+ }
1299
+ return mapped;
1300
+ });
1294
1301
  } catch {
1295
1302
  const result = await this.find(resource);
1296
1303
  const records = result.data || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "3.1.5",
3
+ "version": "3.3.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,14 +20,14 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@objectstack/client": "^3.3.0",
24
- "@object-ui/core": "3.1.5",
25
- "@object-ui/types": "3.1.5"
23
+ "@objectstack/client": "^4.0.3",
24
+ "@object-ui/core": "3.3.0",
25
+ "@object-ui/types": "3.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsup": "^8.5.1",
29
- "typescript": "^5.9.3",
30
- "vitest": "^4.1.0"
29
+ "typescript": "^6.0.2",
30
+ "vitest": "^4.1.4"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -0,0 +1,277 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { ObjectStackAdapter } from './index';
11
+
12
+ /**
13
+ * Tests for ObjectStackAdapter.aggregate() — verifies that the analytics
14
+ * query payload uses the correct string-based measure/dimension format
15
+ * expected by the backend analytics service (MemoryAnalyticsService).
16
+ *
17
+ * See: https://github.com/objectstack-ai/objectui/issues (measures format bug)
18
+ */
19
+ describe('ObjectStackAdapter aggregate()', () => {
20
+ let adapter: ObjectStackAdapter;
21
+ let mockAnalyticsQuery: ReturnType<typeof vi.fn>;
22
+
23
+ beforeEach(() => {
24
+ mockAnalyticsQuery = vi.fn().mockResolvedValue({ data: [] });
25
+
26
+ adapter = new ObjectStackAdapter({
27
+ baseUrl: 'http://localhost:3000',
28
+ autoReconnect: false,
29
+ });
30
+
31
+ // Inject mock client and mark as connected to bypass connect()
32
+ (adapter as any).client = {
33
+ data: {
34
+ find: vi.fn().mockResolvedValue({ records: [], total: 0 }),
35
+ },
36
+ analytics: {
37
+ query: mockAnalyticsQuery,
38
+ },
39
+ connect: vi.fn().mockResolvedValue(undefined),
40
+ discover: vi.fn().mockResolvedValue({ status: 'ok' }),
41
+ };
42
+ (adapter as any).connected = true;
43
+ });
44
+
45
+ it('should send measures as string array with field_function format for sum', async () => {
46
+ mockAnalyticsQuery.mockResolvedValue({ data: [] });
47
+
48
+ await adapter.aggregate('opportunity', {
49
+ field: 'amount',
50
+ function: 'sum',
51
+ groupBy: 'stage',
52
+ });
53
+
54
+ expect(mockAnalyticsQuery).toHaveBeenCalledWith({
55
+ cube: 'opportunity',
56
+ measures: ['amount_sum'],
57
+ dimensions: ['stage'],
58
+ });
59
+ });
60
+
61
+ it('should send measures as ["count"] for count aggregation', async () => {
62
+ mockAnalyticsQuery.mockResolvedValue({ data: [] });
63
+
64
+ await adapter.aggregate('opportunity', {
65
+ field: 'amount',
66
+ function: 'count',
67
+ groupBy: 'stage',
68
+ });
69
+
70
+ expect(mockAnalyticsQuery).toHaveBeenCalledWith({
71
+ cube: 'opportunity',
72
+ measures: ['count'],
73
+ dimensions: ['stage'],
74
+ });
75
+ });
76
+
77
+ it('should send measures as string for avg aggregation', async () => {
78
+ mockAnalyticsQuery.mockResolvedValue({ data: [] });
79
+
80
+ await adapter.aggregate('opportunity', {
81
+ field: 'amount',
82
+ function: 'avg',
83
+ groupBy: 'stage',
84
+ });
85
+
86
+ expect(mockAnalyticsQuery).toHaveBeenCalledWith({
87
+ cube: 'opportunity',
88
+ measures: ['amount_avg'],
89
+ dimensions: ['stage'],
90
+ });
91
+ });
92
+
93
+ it('should send empty dimensions when groupBy is _all', async () => {
94
+ mockAnalyticsQuery.mockResolvedValue({ data: [] });
95
+
96
+ await adapter.aggregate('opportunity', {
97
+ field: 'amount',
98
+ function: 'sum',
99
+ groupBy: '_all',
100
+ });
101
+
102
+ expect(mockAnalyticsQuery).toHaveBeenCalledWith({
103
+ cube: 'opportunity',
104
+ measures: ['amount_sum'],
105
+ dimensions: [],
106
+ });
107
+ });
108
+
109
+ it('should include filters in payload when provided', async () => {
110
+ const filter = [{ member: 'stage', operator: 'equals', values: ['Closed Won'] }];
111
+ mockAnalyticsQuery.mockResolvedValue({ data: [] });
112
+
113
+ await adapter.aggregate('opportunity', {
114
+ field: 'amount',
115
+ function: 'sum',
116
+ groupBy: 'stage',
117
+ filter,
118
+ });
119
+
120
+ expect(mockAnalyticsQuery).toHaveBeenCalledWith({
121
+ cube: 'opportunity',
122
+ measures: ['amount_sum'],
123
+ dimensions: ['stage'],
124
+ filters: filter,
125
+ });
126
+ });
127
+
128
+ it('should map measure key back to field name in response', async () => {
129
+ mockAnalyticsQuery.mockResolvedValue({
130
+ data: [
131
+ { stage: 'Prospect', amount_sum: 300 },
132
+ { stage: 'Closed Won', amount_sum: 500 },
133
+ ],
134
+ });
135
+
136
+ const result = await adapter.aggregate('opportunity', {
137
+ field: 'amount',
138
+ function: 'sum',
139
+ groupBy: 'stage',
140
+ });
141
+
142
+ expect(result).toEqual([
143
+ { stage: 'Prospect', amount: 300 },
144
+ { stage: 'Closed Won', amount: 500 },
145
+ ]);
146
+ });
147
+
148
+ it('should map count measure back to field name in response', async () => {
149
+ mockAnalyticsQuery.mockResolvedValue({
150
+ data: [
151
+ { stage: 'Prospect', count: 5 },
152
+ { stage: 'Closed Won', count: 3 },
153
+ ],
154
+ });
155
+
156
+ const result = await adapter.aggregate('opportunity', {
157
+ field: 'amount',
158
+ function: 'count',
159
+ groupBy: 'stage',
160
+ });
161
+
162
+ expect(result).toEqual([
163
+ { stage: 'Prospect', amount: 5 },
164
+ { stage: 'Closed Won', amount: 3 },
165
+ ]);
166
+ });
167
+
168
+ it('should handle direct array response from analytics', async () => {
169
+ mockAnalyticsQuery.mockResolvedValue([
170
+ { stage: 'Prospect', amount_sum: 300 },
171
+ ]);
172
+
173
+ const result = await adapter.aggregate('opportunity', {
174
+ field: 'amount',
175
+ function: 'sum',
176
+ groupBy: 'stage',
177
+ });
178
+
179
+ expect(result).toEqual([
180
+ { stage: 'Prospect', amount: 300 },
181
+ ]);
182
+ });
183
+
184
+ it('should handle results wrapper in response', async () => {
185
+ mockAnalyticsQuery.mockResolvedValue({
186
+ results: [
187
+ { stage: 'Prospect', amount_avg: 150 },
188
+ ],
189
+ });
190
+
191
+ const result = await adapter.aggregate('opportunity', {
192
+ field: 'amount',
193
+ function: 'avg',
194
+ groupBy: 'stage',
195
+ });
196
+
197
+ expect(result).toEqual([
198
+ { stage: 'Prospect', amount: 150 },
199
+ ]);
200
+ });
201
+
202
+ it('should extract rows from { rows: [...] } envelope (SDK unwraps outer success/data)', async () => {
203
+ mockAnalyticsQuery.mockResolvedValue({
204
+ rows: [
205
+ { stage: 'closed_won', expected_revenue_sum: 225000 },
206
+ { stage: 'negotiation', expected_revenue_sum: 36000 },
207
+ ],
208
+ fields: [
209
+ { name: 'stage', type: 'string' },
210
+ { name: 'expected_revenue_sum', type: 'number' },
211
+ ],
212
+ });
213
+
214
+ const result = await adapter.aggregate('opportunity', {
215
+ field: 'expected_revenue',
216
+ function: 'sum',
217
+ groupBy: 'stage',
218
+ });
219
+
220
+ expect(result).toEqual([
221
+ { stage: 'closed_won', expected_revenue: 225000 },
222
+ { stage: 'negotiation', expected_revenue: 36000 },
223
+ ]);
224
+ });
225
+
226
+ it('should extract rows from { data: { rows: [...] } } envelope (SDK does not unwrap)', async () => {
227
+ mockAnalyticsQuery.mockResolvedValue({
228
+ success: true,
229
+ data: {
230
+ rows: [
231
+ { stage: 'closed_won', expected_revenue_sum: 225000 },
232
+ { stage: 'negotiation', expected_revenue_sum: 36000 },
233
+ ],
234
+ fields: [
235
+ { name: 'stage', type: 'string' },
236
+ { name: 'expected_revenue_sum', type: 'number' },
237
+ ],
238
+ sql: '-- MongoDB Aggregation Pipeline',
239
+ },
240
+ });
241
+
242
+ const result = await adapter.aggregate('opportunity', {
243
+ field: 'expected_revenue',
244
+ function: 'sum',
245
+ groupBy: 'stage',
246
+ });
247
+
248
+ expect(result).toEqual([
249
+ { stage: 'closed_won', expected_revenue: 225000 },
250
+ { stage: 'negotiation', expected_revenue: 36000 },
251
+ ]);
252
+ });
253
+
254
+ it('should fall back to client-side aggregation when analytics endpoint fails', async () => {
255
+ mockAnalyticsQuery.mockRejectedValue(new Error('Analytics not available'));
256
+
257
+ // Mock find() to return records for client-side aggregation
258
+ (adapter as any).client.data.find = vi.fn().mockResolvedValue({
259
+ records: [
260
+ { stage: 'Prospect', amount: 100 },
261
+ { stage: 'Prospect', amount: 200 },
262
+ { stage: 'Closed Won', amount: 500 },
263
+ ],
264
+ total: 3,
265
+ });
266
+
267
+ const result = await adapter.aggregate('opportunity', {
268
+ field: 'amount',
269
+ function: 'sum',
270
+ groupBy: 'stage',
271
+ });
272
+
273
+ expect(result).toHaveLength(2);
274
+ expect(result.find((r: any) => r.stage === 'Prospect')?.amount).toBe(300);
275
+ expect(result.find((r: any) => r.stage === 'Closed Won')?.amount).toBe(500);
276
+ });
277
+ });
package/src/errors.ts CHANGED
@@ -28,8 +28,8 @@ export class ObjectStackError extends Error {
28
28
  this.name = 'ObjectStackError';
29
29
 
30
30
  // Maintains proper stack trace for where error was thrown (only in V8)
31
- if (Error.captureStackTrace) {
32
- Error.captureStackTrace(this, this.constructor);
31
+ if ((Error as any).captureStackTrace) {
32
+ (Error as any).captureStackTrace(this, this.constructor);
33
33
  }
34
34
  }
35
35
 
package/src/index.ts CHANGED
@@ -829,20 +829,44 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
829
829
  await this.connect();
830
830
 
831
831
  try {
832
+ // Build measure name in the format expected by the backend analytics
833
+ // service (memory-analytics / cube). For 'count' the measure key is
834
+ // simply 'count'; for other aggregation functions it follows the
835
+ // convention `${field}_${function}` (e.g. 'amount_sum').
836
+ const measureName = params.function === 'count'
837
+ ? 'count'
838
+ : `${params.field}_${params.function}`;
839
+
832
840
  const payload: Record<string, unknown> = {
833
- object: resource,
834
- measures: [{ field: params.field, function: params.function }],
835
- dimensions: [params.groupBy],
841
+ cube: resource,
842
+ measures: [measureName],
843
+ // When groupBy is '_all' no dimensions are needed (single-bucket).
844
+ dimensions: params.groupBy && params.groupBy !== '_all' ? [params.groupBy] : [],
836
845
  };
837
846
  if (params.filter) {
838
847
  payload.filters = params.filter;
839
848
  }
840
849
 
841
850
  const data = await this.client.analytics.query(payload);
842
- if (Array.isArray(data)) return data;
843
- if (data?.data && Array.isArray(data.data)) return data.data;
844
- if (data?.results && Array.isArray(data.results)) return data.results;
845
- return [];
851
+ const rawRows: any[] = Array.isArray(data) ? data
852
+ : data?.rows && Array.isArray(data.rows) ? data.rows
853
+ : data?.data && Array.isArray(data.data) ? data.data
854
+ : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows
855
+ : data?.results && Array.isArray(data.results) ? data.results
856
+ : [];
857
+
858
+ // Map measure keys back to the original field name so that consumers
859
+ // (ObjectChart, DashboardRenderer, etc.) can access values by field name.
860
+ // This includes count → field (e.g. 'count' → 'amount') to match the
861
+ // output format of aggregateClientSide() which always uses params.field.
862
+ return rawRows.map((row: any) => {
863
+ const mapped = { ...row };
864
+ if (measureName !== params.field && measureName in mapped) {
865
+ mapped[params.field] = mapped[measureName];
866
+ delete mapped[measureName];
867
+ }
868
+ return mapped;
869
+ });
846
870
  } catch {
847
871
  // If the analytics endpoint is not available, fall back to
848
872
  // find() + client-side aggregation