@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.
- package/CHANGELOG.md +888 -0
- package/README.md +3 -11
- package/dist/index.cjs +1511 -1115
- package/dist/index.d.cts +1222 -824
- package/dist/index.d.ts +1222 -824
- package/dist/index.js +1503 -1111
- package/package.json +5 -6
- package/src/aggregate-capability.test.ts +0 -177
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud.ts +0 -109
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -71
- package/src/getObjectSchema.test.ts +0 -145
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -3658
- package/src/integration.ts +0 -200
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -849
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -562
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -132
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -306
- package/src/studio.ts +0 -152
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -240
- 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
|
+
"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
|
|
26
|
-
"@objectstack/spec": "^17.0.0
|
|
27
|
-
"@object-ui/core": "17.
|
|
28
|
-
"@object-ui/types": "17.
|
|
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
|
-
});
|
package/src/bulkDelete.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/bulkUpdate.test.ts
DELETED
|
@@ -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
|
-
});
|