@object-ui/data-objectstack 4.0.1 → 4.0.4

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,28 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 4.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@4.0.4
8
+ - @object-ui/core@4.0.4
9
+
10
+ ## 4.0.3
11
+
12
+ ### Patch Changes
13
+
14
+ - 4be43e2: **Page-mode record forms (`editMode: 'page'`).** New per-object metadata flag that opts a record's create/edit form into a dedicated full-screen route (`/apps/:appName/:objectName/new`, `/apps/:appName/:objectName/record/:recordId/edit`). Two new declarative actions `navigate_create` and `navigate_edit` open these routes from JSON action buttons. Default modal behavior is preserved for objects that do not set `editMode`.
15
+
16
+ **`@object-ui/plugin-list` & `@object-ui/plugin-detail`: `ComponentRegistry` singleton fix.** Both plugins' Vite configs now mark all `@object-ui/*` packages as external so each plugin no longer bundles its own private copy of `@object-ui/core`. Cross-plugin component lookups now resolve correctly from the same singleton registry. `plugin-list` dist shrank from multi-MB to 67 kB (gzip 16 kB); `plugin-detail` to 124 kB (gzip 28 kB).
17
+
18
+ **`@object-ui/app-shell` `CreateViewDialog` churn fix.** `existingSet` is now memoised on the joined string key of `existingLabels` rather than the raw array reference, preventing the name-suggest `useEffect` from re-firing on every parent render.
19
+
20
+ **CI fixes.** `ReportViewer` conditional-formatting test now accepts both `rgb(...)` and hex color representations. `ObjectView` i18n mocks rewritten to mirror the real hook shapes (`useObjectTranslation`, `useObjectLabel`).
21
+
22
+ - Updated dependencies [4be43e2]
23
+ - @object-ui/types@4.0.3
24
+ - @object-ui/core@4.0.3
25
+
3
26
  ## 4.0.1
4
27
 
5
28
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -1550,6 +1550,18 @@ var ObjectStackAdapter = class {
1550
1550
  }
1551
1551
  const data = await this.client.analytics.query(payload);
1552
1552
  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 : [];
1553
+ const measureMissing = rawRows.length > 0 && rawRows.every((row) => {
1554
+ if (row == null) return true;
1555
+ if (measureName in row && row[measureName] != null) return false;
1556
+ if (params.field in row && row[params.field] != null) return false;
1557
+ return true;
1558
+ });
1559
+ if (measureMissing) {
1560
+ const result = await this.find(resource);
1561
+ const records = result.data || [];
1562
+ if (records.length === 0) return [];
1563
+ return this.aggregateClientSide(records, params);
1564
+ }
1553
1565
  return rawRows.map((row) => {
1554
1566
  const mapped = { ...row };
1555
1567
  if (measureName !== params.field && measureName in mapped) {
package/dist/index.js CHANGED
@@ -1508,6 +1508,18 @@ var ObjectStackAdapter = class {
1508
1508
  }
1509
1509
  const data = await this.client.analytics.query(payload);
1510
1510
  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 : [];
1511
+ const measureMissing = rawRows.length > 0 && rawRows.every((row) => {
1512
+ if (row == null) return true;
1513
+ if (measureName in row && row[measureName] != null) return false;
1514
+ if (params.field in row && row[params.field] != null) return false;
1515
+ return true;
1516
+ });
1517
+ if (measureMissing) {
1518
+ const result = await this.find(resource);
1519
+ const records = result.data || [];
1520
+ if (records.length === 0) return [];
1521
+ return this.aggregateClientSide(records, params);
1522
+ }
1511
1523
  return rawRows.map((row) => {
1512
1524
  const mapped = { ...row };
1513
1525
  if (measureName !== params.field && measureName in mapped) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "4.0.1",
3
+ "version": "4.0.4",
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": "4.0.1",
27
- "@object-ui/types": "4.0.1"
26
+ "@object-ui/core": "4.0.4",
27
+ "@object-ui/types": "4.0.4"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
package/src/index.ts CHANGED
@@ -1055,6 +1055,24 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1055
1055
  : data?.results && Array.isArray(data.results) ? data.results
1056
1056
  : [];
1057
1057
 
1058
+ // Defensive guard: if the backend silently dropped the requested measure
1059
+ // (e.g. it doesn't recognise the `${field}_${function}` alias and the
1060
+ // canonical measure is named differently), the rows come back without
1061
+ // any measure value. Detect this and fall back to client-side
1062
+ // aggregation so charts still render.
1063
+ const measureMissing = rawRows.length > 0 && rawRows.every((row: any) => {
1064
+ if (row == null) return true;
1065
+ if (measureName in row && row[measureName] != null) return false;
1066
+ if (params.field in row && row[params.field] != null) return false;
1067
+ return true;
1068
+ });
1069
+ if (measureMissing) {
1070
+ const result = await this.find(resource as any);
1071
+ const records = result.data || [];
1072
+ if (records.length === 0) return [];
1073
+ return this.aggregateClientSide(records, params);
1074
+ }
1075
+
1058
1076
  // Map measure keys back to the original field name so that consumers
1059
1077
  // (ObjectChart, DashboardRenderer, etc.) can access values by field name.
1060
1078
  // This includes count → field (e.g. 'count' → 'amount') to match the
@@ -1,277 +0,0 @@
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
- });
@@ -1,267 +0,0 @@
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
- // We test the adapter's $expand handling.
13
- // When $expand is present, the adapter makes a raw GET request to the REST API
14
- // with `populate` as a URL query param (since the client SDK's data.find()
15
- // QueryOptions does not support populate/expand).
16
- // The key scenarios:
17
- // 1. find() with $expand → raw GET /api/v1/data/:object?populate=...
18
- // 2. find() without $expand → client.data.find() (GET) as before
19
- // 3. findOne() with $expand → raw GET /api/v1/data/:object?filter={id:...}&populate=...
20
- // 4. findOne() without $expand → client.data.get() as before
21
-
22
- describe('ObjectStackAdapter $expand support', () => {
23
- let adapter: ObjectStackAdapter;
24
- let mockClient: any;
25
- let mockFetch: ReturnType<typeof vi.fn>;
26
-
27
- beforeEach(() => {
28
- // Create a mock fetch that returns a successful response
29
- mockFetch = vi.fn().mockResolvedValue({
30
- ok: true,
31
- json: () => Promise.resolve({ records: [], total: 0 }),
32
- });
33
-
34
- adapter = new ObjectStackAdapter({
35
- baseUrl: 'http://localhost:3000',
36
- autoReconnect: false,
37
- fetch: mockFetch,
38
- });
39
-
40
- // Mock the internal client after construction
41
- mockClient = {
42
- data: {
43
- find: vi.fn().mockResolvedValue({ records: [], total: 0 }),
44
- query: vi.fn().mockResolvedValue({ records: [], total: 0 }),
45
- get: vi.fn().mockResolvedValue({ record: { id: '1', name: 'Test' } }),
46
- },
47
- connect: vi.fn().mockResolvedValue(undefined),
48
- discover: vi.fn().mockResolvedValue({ status: 'ok' }),
49
- };
50
-
51
- // Inject mock client and mark as connected to bypass connect()
52
- (adapter as any).client = mockClient;
53
- (adapter as any).connected = true;
54
- });
55
-
56
- describe('find() with $expand', () => {
57
- it('should make a raw GET request with populate query param when $expand is present', async () => {
58
- mockFetch.mockResolvedValue({
59
- ok: true,
60
- json: () => Promise.resolve({
61
- records: [{ id: '1', name: 'Order 1', customer: { id: '2', name: 'Alice' } }],
62
- total: 1,
63
- }),
64
- });
65
-
66
- const result = await adapter.find('order', {
67
- $top: 10,
68
- $expand: ['customer', 'account'],
69
- });
70
-
71
- // Should use raw fetch, not client.data.query or client.data.find
72
- expect(mockFetch).toHaveBeenCalled();
73
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
74
- expect(fetchUrl).toContain('/api/v1/data/order');
75
- expect(fetchUrl).toContain('populate=customer%2Caccount');
76
- expect(fetchUrl).toContain('top=10');
77
- expect(mockClient.data.find).not.toHaveBeenCalled();
78
- expect(result.data).toHaveLength(1);
79
- expect(result.data[0].customer).toEqual({ id: '2', name: 'Alice' });
80
- });
81
-
82
- it('should pass filters and sort as query params', async () => {
83
- mockFetch.mockResolvedValue({
84
- ok: true,
85
- json: () => Promise.resolve({ records: [], total: 0 }),
86
- });
87
-
88
- await adapter.find('order', {
89
- $filter: { status: 'active' },
90
- $orderby: [{ field: 'name', order: 'asc' }],
91
- $top: 50,
92
- $skip: 10,
93
- $expand: ['customer'],
94
- });
95
-
96
- expect(mockFetch).toHaveBeenCalled();
97
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
98
- expect(fetchUrl).toContain('populate=customer');
99
- expect(fetchUrl).toContain('top=50');
100
- expect(fetchUrl).toContain('skip=10');
101
- expect(fetchUrl).toContain('sort=name');
102
- expect(fetchUrl).toContain('filter=');
103
- });
104
-
105
- it('should use data.find() when $expand is not present', async () => {
106
- mockClient.data.find.mockResolvedValue({ records: [{ id: '1', name: 'Test' }], total: 1 });
107
-
108
- const result = await adapter.find('order', { $top: 10 });
109
-
110
- expect(mockClient.data.find).toHaveBeenCalled();
111
- expect(result.data).toHaveLength(1);
112
- });
113
-
114
- it('should use data.find() when $expand is an empty array', async () => {
115
- mockClient.data.find.mockResolvedValue({ records: [], total: 0 });
116
-
117
- await adapter.find('order', { $top: 10, $expand: [] });
118
-
119
- expect(mockClient.data.find).toHaveBeenCalled();
120
- });
121
- });
122
-
123
- describe('findOne() with $expand', () => {
124
- it('should make a raw GET request with id filter and populate when $expand is present', async () => {
125
- mockFetch.mockResolvedValue({
126
- ok: true,
127
- json: () => Promise.resolve({
128
- records: [{ id: 'order-1', name: 'Order 1', customer: { id: '2', name: 'Alice' } }],
129
- }),
130
- });
131
-
132
- const result = await adapter.findOne('order', 'order-1', {
133
- $expand: ['customer', 'account'],
134
- });
135
-
136
- expect(mockFetch).toHaveBeenCalled();
137
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
138
- expect(fetchUrl).toContain('/api/v1/data/order');
139
- expect(fetchUrl).toContain('populate=customer%2Caccount');
140
- expect(fetchUrl).toContain('top=1');
141
- expect(fetchUrl).toContain('filter=');
142
- // Verify the filter contains id
143
- const filterParam = new URL(fetchUrl).searchParams.get('filter');
144
- expect(filterParam).toBeTruthy();
145
- const parsedFilter = JSON.parse(filterParam!);
146
- expect(parsedFilter).toEqual({ id: 'order-1' });
147
- expect(mockClient.data.get).not.toHaveBeenCalled();
148
- expect(result).toEqual({ id: 'order-1', name: 'Order 1', customer: { id: '2', name: 'Alice' } });
149
- });
150
-
151
- it('should return null when raw request returns no records', async () => {
152
- mockFetch.mockResolvedValue({
153
- ok: true,
154
- json: () => Promise.resolve({ records: [] }),
155
- });
156
-
157
- const result = await adapter.findOne('order', 'nonexistent', {
158
- $expand: ['customer'],
159
- });
160
-
161
- expect(result).toBeNull();
162
- });
163
-
164
- it('should use data.get() when $expand is not present', async () => {
165
- mockClient.data.get.mockResolvedValue({ record: { id: '1', name: 'Test' } });
166
-
167
- const result = await adapter.findOne('order', '1');
168
-
169
- expect(mockClient.data.get).toHaveBeenCalledWith('order', '1');
170
- expect(result).toEqual({ id: '1', name: 'Test' });
171
- });
172
-
173
- it('should return null for 404 errors without $expand', async () => {
174
- mockClient.data.get.mockRejectedValue({ status: 404 });
175
-
176
- const result = await adapter.findOne('order', 'nonexistent');
177
-
178
- expect(result).toBeNull();
179
- });
180
-
181
- it('should fall through to data.get() when $expand raw request fails with non-404 error', async () => {
182
- // The raw populate request fails (e.g., server doesn't support the filter+populate API)
183
- mockFetch.mockResolvedValue({
184
- ok: false,
185
- status: 500,
186
- statusText: 'Internal Server Error',
187
- json: () => Promise.resolve({ message: 'unsupported' }),
188
- });
189
- // But the direct data.get() call succeeds
190
- mockClient.data.get.mockResolvedValue({ record: { id: 'order-1', name: 'Order 1' } });
191
-
192
- const result = await adapter.findOne('order', 'order-1', {
193
- $expand: ['customer'],
194
- });
195
-
196
- // Should have tried raw request first
197
- expect(mockFetch).toHaveBeenCalled();
198
- // Then fell through to data.get()
199
- expect(mockClient.data.get).toHaveBeenCalledWith('order', 'order-1');
200
- expect(result).toEqual({ id: 'order-1', name: 'Order 1' });
201
- });
202
- });
203
-
204
- describe('raw request format', () => {
205
- it('should include Authorization header when token is set', async () => {
206
- mockFetch.mockResolvedValue({
207
- ok: true,
208
- json: () => Promise.resolve({ records: [], total: 0 }),
209
- });
210
-
211
- (adapter as any).token = 'test-token';
212
-
213
- await adapter.find('order', {
214
- $expand: ['customer'],
215
- });
216
-
217
- expect(mockFetch).toHaveBeenCalled();
218
- const fetchInit = mockFetch.mock.calls[0][1];
219
- expect(fetchInit.headers.Authorization).toBe('Bearer test-token');
220
- });
221
-
222
- it('should unwrap response envelope with success/data wrapper', async () => {
223
- mockFetch.mockResolvedValue({
224
- ok: true,
225
- json: () => Promise.resolve({
226
- success: true,
227
- data: {
228
- records: [{ id: '1', name: 'Order' }],
229
- total: 1,
230
- },
231
- }),
232
- });
233
-
234
- const result = await adapter.find('order', {
235
- $expand: ['customer'],
236
- });
237
-
238
- expect(result.data).toHaveLength(1);
239
- });
240
-
241
- it('should not double /api/v1 when baseUrl already includes it', async () => {
242
- mockFetch.mockResolvedValue({
243
- ok: true,
244
- json: () => Promise.resolve({ records: [], total: 0 }),
245
- });
246
-
247
- // Create adapter with /api/v1 in baseUrl
248
- const apiAdapter = new ObjectStackAdapter({
249
- baseUrl: 'http://localhost:3000/api/v1',
250
- autoReconnect: false,
251
- fetch: mockFetch,
252
- });
253
- (apiAdapter as any).client = mockClient;
254
- (apiAdapter as any).connected = true;
255
-
256
- await apiAdapter.find('order', {
257
- $expand: ['customer'],
258
- });
259
-
260
- expect(mockFetch).toHaveBeenCalled();
261
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
262
- // Should be /api/v1/data/order not /api/v1/api/v1/data/order
263
- expect(fetchUrl).toBe('http://localhost:3000/api/v1/data/order?populate=customer');
264
- expect(fetchUrl).not.toContain('/api/v1/api/v1');
265
- });
266
- });
267
- });
@@ -1,112 +0,0 @@
1
- /**
2
- * Tests for ObjectStackAdapter file upload integration
3
- */
4
- import { describe, it, expect, vi, beforeEach } from 'vitest';
5
- import { ObjectStackAdapter } from './index';
6
-
7
- describe('ObjectStackAdapter File Upload', () => {
8
- let adapter: ObjectStackAdapter;
9
-
10
- beforeEach(() => {
11
- adapter = new ObjectStackAdapter({
12
- baseUrl: 'http://localhost:3000',
13
- autoReconnect: false,
14
- });
15
- vi.clearAllMocks();
16
- });
17
-
18
- describe('uploadFile', () => {
19
- it('should be a method on the adapter', () => {
20
- expect(typeof adapter.uploadFile).toBe('function');
21
- });
22
-
23
- it('should call fetch with multipart form data when connected', async () => {
24
- const mockResponse = {
25
- ok: true,
26
- json: vi.fn().mockResolvedValue({
27
- id: 'file-1',
28
- filename: 'test.pdf',
29
- mimeType: 'application/pdf',
30
- size: 1024,
31
- url: 'http://localhost:3000/files/file-1',
32
- }),
33
- };
34
-
35
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
36
-
37
- // Manually set connected state by accessing private field
38
- (adapter as any).connected = true;
39
- (adapter as any).connectionState = 'connected';
40
-
41
- const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
42
-
43
- const result = await adapter.uploadFile('documents', file, {
44
- recordId: 'rec-123',
45
- fieldName: 'attachment',
46
- });
47
-
48
- expect(global.fetch).toHaveBeenCalledWith(
49
- expect.stringContaining('/api/data/documents/upload'),
50
- expect.objectContaining({
51
- method: 'POST',
52
- body: expect.any(FormData),
53
- }),
54
- );
55
-
56
- expect(result.id).toBe('file-1');
57
- expect(result.filename).toBe('test.pdf');
58
- });
59
-
60
- it('should throw on upload failure', async () => {
61
- const mockResponse = {
62
- ok: false,
63
- status: 413,
64
- statusText: 'Payload Too Large',
65
- json: vi.fn().mockResolvedValue({ message: 'File too large' }),
66
- };
67
-
68
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
69
-
70
- // Manually set connected state
71
- (adapter as any).connected = true;
72
- (adapter as any).connectionState = 'connected';
73
-
74
- const file = new File(['test'], 'large.bin', { type: 'application/octet-stream' });
75
-
76
- await expect(adapter.uploadFile('documents', file)).rejects.toThrow('File too large');
77
- });
78
- });
79
-
80
- describe('uploadFiles', () => {
81
- it('should be a method on the adapter', () => {
82
- expect(typeof adapter.uploadFiles).toBe('function');
83
- });
84
-
85
- it('should upload multiple files', async () => {
86
- const mockResponse = {
87
- ok: true,
88
- json: vi.fn().mockResolvedValue([
89
- { id: 'file-1', filename: 'a.pdf', mimeType: 'application/pdf', size: 100, url: '/files/1' },
90
- { id: 'file-2', filename: 'b.pdf', mimeType: 'application/pdf', size: 200, url: '/files/2' },
91
- ]),
92
- };
93
-
94
- global.fetch = vi.fn().mockResolvedValue(mockResponse);
95
-
96
- // Manually set connected state
97
- (adapter as any).connected = true;
98
- (adapter as any).connectionState = 'connected';
99
-
100
- const files = [
101
- new File(['content1'], 'a.pdf', { type: 'application/pdf' }),
102
- new File(['content2'], 'b.pdf', { type: 'application/pdf' }),
103
- ];
104
-
105
- const results = await adapter.uploadFiles('documents', files);
106
-
107
- expect(results).toHaveLength(2);
108
- expect(results[0].id).toBe('file-1');
109
- expect(results[1].id).toBe('file-2');
110
- });
111
- });
112
- });