@object-ui/core 5.0.2 → 5.2.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 +86 -0
- package/dist/actions/bulkFastPath.d.ts +85 -0
- package/dist/actions/bulkFastPath.js +59 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +6 -2
- package/dist/adapters/ApiDataSource.js +8 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# @object-ui/core
|
|
2
2
|
|
|
3
|
+
## 5.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@5.2.1
|
|
8
|
+
|
|
9
|
+
## 5.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- de0c5e6: Add `DataSource.bulkDelete(resource, ids)` as the symmetric counterpart
|
|
14
|
+
to `bulkUpdate`. Implemented in `data-objectstack` via the client's
|
|
15
|
+
`deleteMany` primitive with a per-id fallback that emulates
|
|
16
|
+
`continueOnError` semantics for older clients.
|
|
17
|
+
|
|
18
|
+
Extract the bulk-vs-per-row decision into a reusable
|
|
19
|
+
`executeBulkBatch(input, ops)` helper in `@object-ui/core`:
|
|
20
|
+
- Single decision tree shared by both update and delete fast paths.
|
|
21
|
+
- Bulk success → no per-row pass.
|
|
22
|
+
- Bulk partial-count → aggregate batch error.
|
|
23
|
+
- Bulk throw → per-row fallback so users still get id-level error detail.
|
|
24
|
+
|
|
25
|
+
`useBulkExecutor` in plugin-grid now uses the helper for both `update`
|
|
26
|
+
and `delete` batches, cutting "delete 500 selected rows" from 500 HTTP
|
|
27
|
+
requests down to ~3.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- d1442e3: test(core): comprehensive security + correctness tests for SafeExpressionParser
|
|
32
|
+
|
|
33
|
+
Add a ~50-case suite covering literals, operators, ternary, property
|
|
34
|
+
access, calls/arrows, and a full security section (blocks
|
|
35
|
+
`constructor` / `__proto__` / `prototype` / `__defineGetter__` /
|
|
36
|
+
`__defineSetter__`, denies `eval` / `Function` / `window` / `process`,
|
|
37
|
+
rejects assignment syntax). No production code changes.
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [de0c5e6]
|
|
40
|
+
- Updated dependencies [9997cae]
|
|
41
|
+
- Updated dependencies [70b5570]
|
|
42
|
+
- @object-ui/types@5.2.0
|
|
43
|
+
|
|
44
|
+
## 5.1.1
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- @object-ui/types@5.1.1
|
|
49
|
+
|
|
50
|
+
## 5.1.0
|
|
51
|
+
|
|
52
|
+
### Minor Changes
|
|
53
|
+
|
|
54
|
+
- 5b80cfd: feat: Optimistic Concurrency Control (OCC) on DataSource writes
|
|
55
|
+
|
|
56
|
+
`DataSource.update()` and `DataSource.delete()` now accept an optional fourth /
|
|
57
|
+
third argument `opts?: { ifMatch?: string }`. When supplied, adapters forward
|
|
58
|
+
the token to the backend; servers that implement OCC (e.g. ObjectStack
|
|
59
|
+
`>=4.2.0`) compare it against the record's current `updated_at` and reject
|
|
60
|
+
with `409 CONCURRENT_UPDATE` on mismatch, preventing silent overwrites in
|
|
61
|
+
multi-user editing scenarios.
|
|
62
|
+
|
|
63
|
+
**`@object-ui/data-objectstack`**
|
|
64
|
+
- Exports `ConcurrentUpdateError` (carries `currentVersion` and
|
|
65
|
+
`currentRecord`) and `isConcurrentUpdateError()` type guard.
|
|
66
|
+
- `update()` / `delete()` accept `opts.ifMatch` and forward it via the
|
|
67
|
+
`@objectstack/client` data API (header: `If-Match`). Requires
|
|
68
|
+
`@objectstack/client@>=4.1.2` for the header to reach the server;
|
|
69
|
+
older clients silently drop the option and fall back to today's
|
|
70
|
+
"last writer wins" behaviour.
|
|
71
|
+
- Adapter-level error handling maps a 409 with `code === 'CONCURRENT_UPDATE'`
|
|
72
|
+
into a typed `ConcurrentUpdateError` so callers can detect and recover
|
|
73
|
+
from conflicts without parsing the wire format.
|
|
74
|
+
|
|
75
|
+
**`@object-ui/core`**
|
|
76
|
+
- `ApiDataSource.update()` and `.delete()` accept `opts.ifMatch` and emit
|
|
77
|
+
the `If-Match` HTTP header.
|
|
78
|
+
|
|
79
|
+
UI consumers (Detail view, inline cell-edit) will be wired in a follow-up
|
|
80
|
+
patch to capture `updated_at` at load time, pass it as `ifMatch` on save,
|
|
81
|
+
and present a Reload / Overwrite / Cancel dialog on conflict.
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- Updated dependencies [cf30cc2]
|
|
86
|
+
- Updated dependencies [5b80cfd]
|
|
87
|
+
- @object-ui/types@5.1.0
|
|
88
|
+
|
|
3
89
|
## 5.0.2
|
|
4
90
|
|
|
5
91
|
### Patch Changes
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* Outcome of a single batch execution. Mirrors the fields callers care
|
|
10
|
+
* about so they can fold these into their own aggregate result.
|
|
11
|
+
*/
|
|
12
|
+
export interface BulkBatchOutcome {
|
|
13
|
+
succeeded: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
errors: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
error: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pluggable callbacks for the executor. Either of these is wired by the
|
|
22
|
+
* caller — typically `bulkCall` issues 1 HTTP request and `perRow` is
|
|
23
|
+
* the per-id fallback used when the bulk endpoint is unavailable or
|
|
24
|
+
* throws. We never call both for the same batch in the same pass.
|
|
25
|
+
*/
|
|
26
|
+
export interface BulkBatchOps {
|
|
27
|
+
/**
|
|
28
|
+
* One server call that mutates every id in the batch with the same
|
|
29
|
+
* shape. Returns the count of rows the server reports as succeeded.
|
|
30
|
+
* Throws when the entire request fails (network, 500, auth) — that
|
|
31
|
+
* signals "fall back to per-row" upstream.
|
|
32
|
+
*/
|
|
33
|
+
bulkCall?: (ids: string[]) => Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Per-row mutation. Called once per id in the batch when bulk is
|
|
36
|
+
* unavailable, when bulk throws, or when the batch is too small for
|
|
37
|
+
* bulk to be worth it. Resolve = success; reject = failure with the
|
|
38
|
+
* reason captured into `errors`.
|
|
39
|
+
*/
|
|
40
|
+
perRow: (id: string) => Promise<unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface BulkBatchInput {
|
|
43
|
+
/**
|
|
44
|
+
* Stable ids for this batch. Empty strings and nulls must already be
|
|
45
|
+
* filtered out by the caller — this helper trusts the contract.
|
|
46
|
+
*/
|
|
47
|
+
ids: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Original batch size before id-stripping. Used to attribute "missing
|
|
50
|
+
* id" failures in the caller's error report.
|
|
51
|
+
*/
|
|
52
|
+
originalSize: number;
|
|
53
|
+
/**
|
|
54
|
+
* Index of the first row of this batch within the run. Used to build
|
|
55
|
+
* stable aggregate-error ids like `batch_0`, `batch_200` etc.
|
|
56
|
+
*/
|
|
57
|
+
offset: number;
|
|
58
|
+
/**
|
|
59
|
+
* When true, the helper is allowed to issue `bulkCall`. When false
|
|
60
|
+
* (e.g. single-row batches where bulk is no win) it goes straight to
|
|
61
|
+
* the per-row path.
|
|
62
|
+
*/
|
|
63
|
+
allowBulk: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Human-readable operation label folded into aggregate error
|
|
66
|
+
* messages ("bulk update", "bulk delete"). Defaults to "bulk".
|
|
67
|
+
*/
|
|
68
|
+
label?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Execute one batch with bulk-first / per-row-fallback semantics.
|
|
72
|
+
*
|
|
73
|
+
* Decision tree:
|
|
74
|
+
* 1. `allowBulk` AND `ops.bulkCall` provided → call it.
|
|
75
|
+
* - returned `n === ids.length` → all good, no per-row pass.
|
|
76
|
+
* - returned `n < ids.length` → aggregate error for the shortfall,
|
|
77
|
+
* no per-row pass (the server already accepted the request; calling
|
|
78
|
+
* per-row would re-mutate the rows that already succeeded).
|
|
79
|
+
* - threw → fall through to per-row to recover
|
|
80
|
+
* actionable per-id error detail for the user.
|
|
81
|
+
* 2. Otherwise → per-row via Promise.allSettled.
|
|
82
|
+
*
|
|
83
|
+
* Symmetric for update + delete + any future same-shape bulk op.
|
|
84
|
+
*/
|
|
85
|
+
export declare function executeBulkBatch(input: BulkBatchInput, ops: BulkBatchOps): Promise<BulkBatchOutcome>;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* Execute one batch with bulk-first / per-row-fallback semantics.
|
|
10
|
+
*
|
|
11
|
+
* Decision tree:
|
|
12
|
+
* 1. `allowBulk` AND `ops.bulkCall` provided → call it.
|
|
13
|
+
* - returned `n === ids.length` → all good, no per-row pass.
|
|
14
|
+
* - returned `n < ids.length` → aggregate error for the shortfall,
|
|
15
|
+
* no per-row pass (the server already accepted the request; calling
|
|
16
|
+
* per-row would re-mutate the rows that already succeeded).
|
|
17
|
+
* - threw → fall through to per-row to recover
|
|
18
|
+
* actionable per-id error detail for the user.
|
|
19
|
+
* 2. Otherwise → per-row via Promise.allSettled.
|
|
20
|
+
*
|
|
21
|
+
* Symmetric for update + delete + any future same-shape bulk op.
|
|
22
|
+
*/
|
|
23
|
+
export async function executeBulkBatch(input, ops) {
|
|
24
|
+
const { ids, allowBulk, offset, label = 'bulk' } = input;
|
|
25
|
+
const outcome = { succeeded: 0, failed: 0, errors: [] };
|
|
26
|
+
if (allowBulk && typeof ops.bulkCall === 'function') {
|
|
27
|
+
try {
|
|
28
|
+
const n = await ops.bulkCall(ids);
|
|
29
|
+
const bulkSucceeded = Math.max(0, Math.min(n, ids.length));
|
|
30
|
+
outcome.succeeded += bulkSucceeded;
|
|
31
|
+
const bulkFailed = ids.length - bulkSucceeded;
|
|
32
|
+
if (bulkFailed > 0) {
|
|
33
|
+
outcome.failed += bulkFailed;
|
|
34
|
+
outcome.errors.push({
|
|
35
|
+
id: `batch_${offset}`,
|
|
36
|
+
error: `${bulkFailed} record${bulkFailed === 1 ? '' : 's'} failed in ${label}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return outcome;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Fall through to per-row — gives the user actionable detail.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const settled = await Promise.allSettled(ids.map((id) => ops.perRow(id)));
|
|
46
|
+
settled.forEach((res, idx) => {
|
|
47
|
+
if (res.status === 'fulfilled') {
|
|
48
|
+
outcome.succeeded += 1;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
outcome.failed += 1;
|
|
52
|
+
outcome.errors.push({
|
|
53
|
+
id: ids[idx],
|
|
54
|
+
error: res.reason instanceof Error ? res.reason.message : String(res.reason),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return outcome;
|
|
59
|
+
}
|
package/dist/actions/index.d.ts
CHANGED
package/dist/actions/index.js
CHANGED
|
@@ -48,8 +48,12 @@ export declare class ApiDataSource<T = any> implements DataSource<T> {
|
|
|
48
48
|
find(_resource: string, params?: QueryParams): Promise<QueryResult<T>>;
|
|
49
49
|
findOne(_resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
|
|
50
50
|
create(_resource: string, data: Partial<T>): Promise<T>;
|
|
51
|
-
update(_resource: string, id: string | number, data: Partial<T
|
|
52
|
-
|
|
51
|
+
update(_resource: string, id: string | number, data: Partial<T>, opts?: {
|
|
52
|
+
ifMatch?: string;
|
|
53
|
+
}): Promise<T>;
|
|
54
|
+
delete(_resource: string, id: string | number, opts?: {
|
|
55
|
+
ifMatch?: string;
|
|
56
|
+
}): Promise<boolean>;
|
|
53
57
|
getObjectSchema(_objectName: string): Promise<any>;
|
|
54
58
|
getView(_objectName: string, _viewId: string): Promise<any | null>;
|
|
55
59
|
getApp(_appId: string): Promise<any | null>;
|
|
@@ -214,20 +214,26 @@ export class ApiDataSource {
|
|
|
214
214
|
body: data,
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
|
-
async update(_resource, id, data) {
|
|
217
|
+
async update(_resource, id, data, opts) {
|
|
218
218
|
const config = this.writeConfig ?? this.readConfig;
|
|
219
219
|
return this.request(config, {
|
|
220
220
|
pathSuffix: String(id),
|
|
221
221
|
method: 'PATCH',
|
|
222
222
|
body: data,
|
|
223
|
+
...(opts?.ifMatch
|
|
224
|
+
? { headers: { 'If-Match': String(opts.ifMatch) } }
|
|
225
|
+
: {}),
|
|
223
226
|
});
|
|
224
227
|
}
|
|
225
|
-
async delete(_resource, id) {
|
|
228
|
+
async delete(_resource, id, opts) {
|
|
226
229
|
const config = this.writeConfig ?? this.readConfig;
|
|
227
230
|
try {
|
|
228
231
|
await this.request(config, {
|
|
229
232
|
pathSuffix: String(id),
|
|
230
233
|
method: 'DELETE',
|
|
234
|
+
...(opts?.ifMatch
|
|
235
|
+
? { headers: { 'If-Match': String(opts.ifMatch) } }
|
|
236
|
+
: {}),
|
|
231
237
|
});
|
|
232
238
|
return true;
|
|
233
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@objectstack/spec": "^4.0.5",
|
|
29
29
|
"lodash": "^4.18.1",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
|
-
"@object-ui/types": "5.
|
|
31
|
+
"@object-ui/types": "5.2.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^6.0.3",
|