@object-ui/data-objectstack 17.3.0 → 17.6.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +888 -0
  2. package/README.md +3 -11
  3. package/dist/index.cjs +1511 -1115
  4. package/dist/index.d.cts +1222 -824
  5. package/dist/index.d.ts +1222 -824
  6. package/dist/index.js +1503 -1111
  7. package/package.json +5 -6
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/bulkDelete.test.ts +0 -75
  10. package/src/bulkUpdate.test.ts +0 -93
  11. package/src/cache/MetadataCache.test.ts +0 -477
  12. package/src/cache/MetadataCache.ts +0 -289
  13. package/src/cloud.ts +0 -109
  14. package/src/connection.test.ts +0 -188
  15. package/src/contracts.ts +0 -115
  16. package/src/errors.test.ts +0 -426
  17. package/src/errors.ts +0 -293
  18. package/src/exportDownload.test.ts +0 -141
  19. package/src/filter-entry-translation.test.ts +0 -360
  20. package/src/filter-operator-ast-parity.test.ts +0 -71
  21. package/src/getObjectSchema.test.ts +0 -145
  22. package/src/importJob.test.ts +0 -71
  23. package/src/index.ts +0 -3658
  24. package/src/integration.ts +0 -200
  25. package/src/listViews.test.ts +0 -175
  26. package/src/metadata-client.test.ts +0 -303
  27. package/src/metadata-client.ts +0 -849
  28. package/src/occ.test.ts +0 -101
  29. package/src/onMutation.test.ts +0 -562
  30. package/src/onWriteWarning.test.ts +0 -273
  31. package/src/orderby-serialization.test.ts +0 -115
  32. package/src/queryDataset.test.ts +0 -132
  33. package/src/searchAll.test.ts +0 -120
  34. package/src/security.ts +0 -243
  35. package/src/spec-symbol-batch6.test.ts +0 -306
  36. package/src/studio.ts +0 -152
  37. package/src/userState.test.ts +0 -315
  38. package/src/userState.ts +0 -238
  39. package/src/v3-compat.test.ts +0 -240
  40. package/src/validation-error.test.ts +0 -94
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "17.3.0",
3
+ "version": "17.6.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,16 +16,15 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "src",
20
19
  "README.md",
21
20
  "CHANGELOG.md",
22
21
  "LICENSE"
23
22
  ],
24
23
  "dependencies": {
25
- "@objectstack/client": "^17.0.0-rc.2",
26
- "@objectstack/spec": "^17.0.0-rc.2",
27
- "@object-ui/core": "17.3.0",
28
- "@object-ui/types": "17.3.0"
24
+ "@objectstack/client": "^17.0.0",
25
+ "@objectstack/spec": "^17.0.0",
26
+ "@object-ui/core": "17.6.0",
27
+ "@object-ui/types": "17.6.0"
29
28
  },
30
29
  "devDependencies": {
31
30
  "tsup": "^8.5.1",
@@ -1,177 +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
- /**
10
- * `aggregate()` against an analytics endpoint that cannot answer.
11
- *
12
- * WHY THIS EXISTS. `@objectstack/client`'s `analytics.query()` returns
13
- * `res.json()` WITHOUT checking `res.ok`, so a non-2xx never throws — the error
14
- * body arrives where rows were expected. The adapter's row parser matched none
15
- * of its shapes, produced `[]`, and returned it as a RESULT: the `catch` that
16
- * promises a client-side fallback never ran, and a KPI on a deployment without
17
- * `@objectstack/service-analytics` rendered a confident **zero**.
18
- *
19
- * That is the same lie framework#3891 removed from the server side (a degraded
20
- * shim answering 200 with unscoped numbers), reproduced one layer up. These
21
- * tests pin the three outcomes:
22
- * - capability absent (404 / 501) → client-side fallback over a scoped find();
23
- * - our body is off-contract (400) → THROWS, never silently re-answered;
24
- * - a normal result → untouched.
25
- */
26
-
27
- import { describe, it, expect, beforeEach, vi } from 'vitest';
28
- import { ObjectStackAdapter, clearSharedDiscoveryCache } from './index';
29
-
30
- const RECORDS = [
31
- { id: '1', stage: 'won', amount: 100 },
32
- { id: '2', stage: 'won', amount: 50 },
33
- { id: '3', stage: 'lost', amount: 20 },
34
- ];
35
-
36
- /**
37
- * A fetch mock answering discovery, the analytics query (with `analyticsBody`
38
- * at `analyticsStatus`), and the `/data` read the fallback performs.
39
- */
40
- function makeFetch(analyticsStatus: number, analyticsBody: unknown) {
41
- const calls: string[] = [];
42
- const fetchImpl = vi.fn(async (url: any, init?: any) => {
43
- const u = String(url);
44
- calls.push(`${init?.method ?? 'GET'} ${u}`);
45
- if (u.includes('/api/v1/discovery')) {
46
- return { ok: true, status: 200, statusText: 'OK', json: async () => ({ success: true, data: { version: 'v1', routes: {} } }) } as any;
47
- }
48
- if (u.includes('/api/v1/analytics/query')) {
49
- return {
50
- ok: analyticsStatus >= 200 && analyticsStatus < 300,
51
- status: analyticsStatus,
52
- statusText: 'x',
53
- json: async () => analyticsBody,
54
- } as any;
55
- }
56
- // The fallback's scoped read.
57
- return {
58
- ok: true, status: 200, statusText: 'OK',
59
- json: async () => ({ success: true, data: { object: 'opportunity', records: RECORDS, total: RECORDS.length } }),
60
- } as any;
61
- });
62
- return { fetchImpl, calls };
63
- }
64
-
65
- function makeAdapter(fetchImpl: any) {
66
- return new ObjectStackAdapter({
67
- baseUrl: 'http://localhost:3000',
68
- autoReconnect: false,
69
- fetch: fetchImpl as any,
70
- });
71
- }
72
-
73
- const SUM_BY_STAGE = { function: 'sum', field: 'amount', groupBy: 'stage' };
74
-
75
- describe('aggregate() when the analytics capability is absent', () => {
76
- beforeEach(() => clearSharedDiscoveryCache());
77
-
78
- it('404 (routes not mounted, framework#4019) falls back instead of returning an empty result', async () => {
79
- const { fetchImpl, calls } = makeFetch(404, { error: 'Not found' });
80
- const rows = await makeAdapter(fetchImpl).aggregate('opportunity', SUM_BY_STAGE);
81
-
82
- // Pre-fix this was `[]` — a chart reading "no data" on a populated table.
83
- expect(rows.length).toBeGreaterThan(0);
84
- const won = rows.find((r: any) => r.stage === 'won');
85
- expect(won?.amount).toBe(150);
86
- // It really did fall back to a server-side read (not invent numbers).
87
- expect(calls.some((c) => c.includes('/api/v1/data'))).toBe(true);
88
- });
89
-
90
- it('501 NOT_IMPLEMENTED (REST route, no service) falls back the same way', async () => {
91
- const { fetchImpl } = makeFetch(501, {
92
- code: 'NOT_IMPLEMENTED',
93
- message: 'Analytics is not available on this deployment.',
94
- });
95
- const rows = await makeAdapter(fetchImpl).aggregate('opportunity', SUM_BY_STAGE);
96
- expect(rows.find((r: any) => r.stage === 'won')?.amount).toBe(150);
97
- });
98
-
99
- it('warns ONCE per adapter, not once per widget', async () => {
100
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
101
- try {
102
- const { fetchImpl } = makeFetch(404, { error: 'Not found' });
103
- const adapter = makeAdapter(fetchImpl);
104
- await adapter.aggregate('opportunity', SUM_BY_STAGE);
105
- await adapter.aggregate('opportunity', SUM_BY_STAGE);
106
- await adapter.aggregate('opportunity', SUM_BY_STAGE);
107
-
108
- const capabilityWarnings = warn.mock.calls.filter((c) =>
109
- String(c[0]).includes('analytics capability unavailable'),
110
- );
111
- expect(capabilityWarnings).toHaveLength(1);
112
- expect(String(capabilityWarnings[0][0])).toContain('@objectstack/service-analytics');
113
- } finally {
114
- warn.mockRestore();
115
- }
116
- });
117
-
118
- it('forwards the widget filter into the fallback (never aggregates the whole table)', async () => {
119
- const { fetchImpl, calls } = makeFetch(404, { error: 'Not found' });
120
- await makeAdapter(fetchImpl).aggregate('opportunity', { ...SUM_BY_STAGE, filter: { stage: 'won' } });
121
-
122
- const dataCall = calls.find((c) => c.includes('/api/v1/data'));
123
- expect(dataCall).toBeDefined();
124
- expect(decodeURIComponent(dataCall!)).toContain('won');
125
- });
126
- });
127
-
128
- describe('aggregate() when the server REJECTS our query body', () => {
129
- beforeEach(() => clearSharedDiscoveryCache());
130
-
131
- // framework#4010 validates /analytics/query at the entry. A 400 means the
132
- // adapter sent an off-contract body — answering it with numbers from the
133
- // client-side path would bury our own bug behind plausible output, which is
134
- // exactly the misdirection framework#3878 documented.
135
- it('400 VALIDATION_FAILED throws — it must NOT be answered by the fallback', async () => {
136
- const { fetchImpl, calls } = makeFetch(400, {
137
- success: false,
138
- error: {
139
- code: 'VALIDATION_FAILED',
140
- httpStatus: 400,
141
- message: 'Invalid AnalyticsQuery body: measures: Invalid input',
142
- details: { fields: [{ field: 'measures', code: 'invalid_type' }] },
143
- },
144
- });
145
-
146
- const err = await makeAdapter(fetchImpl).aggregate('opportunity', SUM_BY_STAGE).catch((e) => e);
147
-
148
- expect(err).toBeInstanceOf(Error);
149
- expect((err as { code?: string }).code).toBe('ANALYTICS_QUERY_REJECTED');
150
- expect(String(err.message)).toContain('measures');
151
- // No silent second answer from a different code path.
152
- expect(calls.some((c) => c.includes('/api/v1/data'))).toBe(false);
153
- });
154
- });
155
-
156
- describe('aggregate() on a healthy analytics endpoint', () => {
157
- beforeEach(() => clearSharedDiscoveryCache());
158
-
159
- it('returns the server rows untouched (no false capability detection)', async () => {
160
- const { fetchImpl, calls } = makeFetch(200, {
161
- rows: [{ stage: 'won', amount_sum: 150 }],
162
- fields: [{ name: 'amount_sum', type: 'number' }],
163
- });
164
-
165
- const rows = await makeAdapter(fetchImpl).aggregate('opportunity', SUM_BY_STAGE);
166
-
167
- // Measure key mapped back to the caller's column (`amount`), rows from the server.
168
- expect(rows).toEqual([{ stage: 'won', amount: 150 }]);
169
- expect(calls.some((c) => c.includes('/api/v1/data'))).toBe(false);
170
- });
171
-
172
- it('a `{ success, data: { rows } }` envelope is still a result, not an error', async () => {
173
- const { fetchImpl } = makeFetch(200, { success: true, data: { rows: [{ stage: 'won', amount_sum: 150 }] } });
174
- const rows = await makeAdapter(fetchImpl).aggregate('opportunity', SUM_BY_STAGE);
175
- expect(rows).toEqual([{ stage: 'won', amount: 150 }]);
176
- });
177
- });
@@ -1,75 +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, vi } from 'vitest';
10
- import { ObjectStackAdapter } from './index';
11
-
12
- function makeDS(stub: { deleteMany?: any; delete?: any }) {
13
- const ds: any = new ObjectStackAdapter({
14
- baseUrl: 'http://test.local',
15
- fetch: vi.fn(async () => {
16
- const body = { success: true, data: { capabilities: {}, routes: {} } };
17
- return new Response(JSON.stringify(body), {
18
- status: 200,
19
- headers: { 'Content-Type': 'application/json' },
20
- });
21
- }),
22
- });
23
- ds.connected = true;
24
- ds.connectionState = 'connected';
25
- ds.client = { data: stub };
26
- return ds;
27
- }
28
-
29
- describe('ObjectStackDataSource.bulkDelete', () => {
30
- it('returns 0 and skips network when ids is empty', async () => {
31
- const deleteMany = vi.fn();
32
- const ds = makeDS({ deleteMany });
33
- const n = await ds.bulkDelete('sys_notification', []);
34
- expect(n).toBe(0);
35
- expect(deleteMany).not.toHaveBeenCalled();
36
- });
37
-
38
- it('issues a single deleteMany call with the id list', async () => {
39
- const deleteMany = vi.fn().mockResolvedValue({ succeeded: 3, failed: 0 });
40
- const ds = makeDS({ deleteMany });
41
- const n = await ds.bulkDelete('task', ['a', 'b', 'c']);
42
- expect(n).toBe(3);
43
- expect(deleteMany).toHaveBeenCalledTimes(1);
44
- const [resource, ids, opts] = deleteMany.mock.calls[0];
45
- expect(resource).toBe('task');
46
- expect(ids).toEqual(['a', 'b', 'c']);
47
- expect(opts).toEqual({ continueOnError: true });
48
- });
49
-
50
- it('returns ids.length when deleteMany returns void (older clients)', async () => {
51
- const deleteMany = vi.fn().mockResolvedValue(undefined);
52
- const ds = makeDS({ deleteMany });
53
- const n = await ds.bulkDelete('task', ['a', 'b']);
54
- expect(n).toBe(2);
55
- });
56
-
57
- it('falls back to per-id delete loop when deleteMany is missing', async () => {
58
- const del = vi.fn(async (_: string, id: string) => {
59
- if (id === 'b') throw new Error('FK violation');
60
- });
61
- const ds = makeDS({ delete: del });
62
- const n = await ds.bulkDelete('task', ['a', 'b', 'c']);
63
- // continueOnError-emulated: b throws, a + c succeed.
64
- expect(n).toBe(2);
65
- expect(del).toHaveBeenCalledTimes(3);
66
- });
67
-
68
- it('coerces non-string ids before sending', async () => {
69
- const deleteMany = vi.fn().mockResolvedValue({ succeeded: 2 });
70
- const ds = makeDS({ deleteMany });
71
- await ds.bulkDelete('task', [1, 2]);
72
- const [, ids] = deleteMany.mock.calls[0];
73
- expect(ids).toEqual(['1', '2']);
74
- });
75
- });
@@ -1,93 +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, vi } from 'vitest';
10
- import { ObjectStackAdapter } from './index';
11
-
12
- /**
13
- * Build a DataSource instance with a stubbed `fetch` that satisfies the
14
- * one-shot discovery probe inside `connect()`. The client object is then
15
- * swapped out for a lightweight mock so we can assert directly against
16
- * the `updateMany` / `update` calls without spinning real HTTP.
17
- */
18
- function makeDS(stub: { updateMany?: any; update?: any }) {
19
- const ds: any = new ObjectStackAdapter({
20
- baseUrl: 'http://test.local',
21
- fetch: vi.fn(async (_url: any, _init: any) => {
22
- const body = { success: true, data: { capabilities: {}, routes: {} } };
23
- return new Response(JSON.stringify(body), {
24
- status: 200,
25
- headers: { 'Content-Type': 'application/json' },
26
- });
27
- }),
28
- });
29
- // Pre-mark connected so connect() short-circuits without hitting fetch.
30
- ds.connected = true;
31
- ds.connectionState = 'connected';
32
- ds.client = { data: stub };
33
- return ds;
34
- }
35
-
36
- describe('ObjectStackDataSource.bulkUpdate', () => {
37
- it('returns 0 and skips network when ids is empty', async () => {
38
- const updateMany = vi.fn();
39
- const ds = makeDS({ updateMany });
40
- const n = await ds.bulkUpdate('sys_notification', [], { is_read: true });
41
- expect(n).toBe(0);
42
- expect(updateMany).not.toHaveBeenCalled();
43
- });
44
-
45
- it('issues a single updateMany call with the per-id record shape', async () => {
46
- const updateMany = vi.fn().mockResolvedValue({ succeeded: 3, failed: 0, results: [] });
47
- const ds = makeDS({ updateMany });
48
- const n = await ds.bulkUpdate('sys_notification', ['a', 'b', 'c'], {
49
- is_read: true,
50
- read_at: '2026-05-23T05:00:00.000Z',
51
- });
52
- expect(n).toBe(3);
53
- expect(updateMany).toHaveBeenCalledTimes(1);
54
- const [resource, records, opts] = updateMany.mock.calls[0];
55
- expect(resource).toBe('sys_notification');
56
- expect(opts).toEqual({ continueOnError: true });
57
- expect(records).toEqual([
58
- { id: 'a', data: { is_read: true, read_at: '2026-05-23T05:00:00.000Z' } },
59
- { id: 'b', data: { is_read: true, read_at: '2026-05-23T05:00:00.000Z' } },
60
- { id: 'c', data: { is_read: true, read_at: '2026-05-23T05:00:00.000Z' } },
61
- ]);
62
- });
63
-
64
- it('honors server-reported partial success counts', async () => {
65
- const updateMany = vi.fn().mockResolvedValue({ succeeded: 2, failed: 1, results: [] });
66
- const ds = makeDS({ updateMany });
67
- const n = await ds.bulkUpdate('sys_notification', ['a', 'b', 'c'], { is_read: true });
68
- expect(n).toBe(2);
69
- });
70
-
71
- it('falls back to per-id updates when client.data.updateMany is missing', async () => {
72
- const update = vi.fn().mockResolvedValue({ record: {} });
73
- const ds = makeDS({ update });
74
- const n = await ds.bulkUpdate('sys_notification', ['a', 'b'], { is_read: true });
75
- expect(n).toBe(2);
76
- expect(update).toHaveBeenCalledTimes(2);
77
- expect(update.mock.calls[0][0]).toBe('sys_notification');
78
- expect(update.mock.calls[0][1]).toBe('a');
79
- expect(update.mock.calls[0][2]).toEqual({ is_read: true });
80
- });
81
-
82
- it('fallback path tolerates per-row failures (continueOnError semantics)', async () => {
83
- const update = vi
84
- .fn()
85
- .mockResolvedValueOnce({ record: {} })
86
- .mockRejectedValueOnce(new Error('row 2 failed'))
87
- .mockResolvedValueOnce({ record: {} });
88
- const ds = makeDS({ update });
89
- const n = await ds.bulkUpdate('sys_notification', ['a', 'b', 'c'], { is_read: true });
90
- expect(n).toBe(2);
91
- expect(update).toHaveBeenCalledTimes(3);
92
- });
93
- });