@object-ui/data-objectstack 16.0.0 → 16.1.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 +199 -0
- package/README.md +63 -0
- package/dist/index.cjs +1022 -872
- package/dist/index.d.cts +79 -14
- package/dist/index.d.ts +79 -14
- package/dist/index.js +1021 -872
- package/package.json +4 -4
- package/src/exportDownload.test.ts +9 -3
- package/src/index.ts +259 -105
- package/src/integration.ts +8 -0
- package/src/listViews.test.ts +97 -0
- package/src/metadata-client.test.ts +8 -0
- package/src/metadata-client.ts +9 -0
- package/src/onMutation.test.ts +267 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.1.0",
|
|
4
4
|
"description": "ObjectStack Data Adapter for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@objectstack/client": "^
|
|
26
|
-
"@object-ui/core": "16.
|
|
27
|
-
"@object-ui/types": "16.
|
|
25
|
+
"@objectstack/client": "^16.0.0-rc.0",
|
|
26
|
+
"@object-ui/core": "16.1.0",
|
|
27
|
+
"@object-ui/types": "16.1.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|
|
@@ -23,7 +23,9 @@ function csvResponse(body = 'ID,Name\n1,Acme') {
|
|
|
23
23
|
|
|
24
24
|
describe('ObjectStackAdapter.exportDownload', () => {
|
|
25
25
|
it('GETs the /export route with format, fields, orderby, header and limit', async () => {
|
|
26
|
-
|
|
26
|
+
// Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
|
|
27
|
+
// than an empty tuple (the zero-arg impl would otherwise infer `never`).
|
|
28
|
+
const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
|
|
27
29
|
const ds = makeDS(fetchImpl);
|
|
28
30
|
|
|
29
31
|
const blob = await ds.exportDownload('task', {
|
|
@@ -50,7 +52,9 @@ describe('ObjectStackAdapter.exportDownload', () => {
|
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
it('defaults to csv and omits optional params when not provided', async () => {
|
|
53
|
-
|
|
55
|
+
// Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
|
|
56
|
+
// than an empty tuple (the zero-arg impl would otherwise infer `never`).
|
|
57
|
+
const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
|
|
54
58
|
const ds = makeDS(fetchImpl);
|
|
55
59
|
|
|
56
60
|
await ds.exportDownload('task', {});
|
|
@@ -65,7 +69,9 @@ describe('ObjectStackAdapter.exportDownload', () => {
|
|
|
65
69
|
});
|
|
66
70
|
|
|
67
71
|
it('serializes the filter to a JSON AST query param', async () => {
|
|
68
|
-
|
|
72
|
+
// Typed like `fetch` so `fetchImpl.mock.calls[0]` is `[url, init]` rather
|
|
73
|
+
// than an empty tuple (the zero-arg impl would otherwise infer `never`).
|
|
74
|
+
const fetchImpl = vi.fn(async (_url: string, _init: RequestInit) => csvResponse());
|
|
69
75
|
const ds = makeDS(fetchImpl);
|
|
70
76
|
|
|
71
77
|
await ds.exportDownload('task', { filter: [['status', '=', 'open']] });
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
|
|
10
10
|
import type {
|
|
11
11
|
DataSource,
|
|
12
|
+
BatchTransactionOperation,
|
|
12
13
|
MutationEvent,
|
|
13
14
|
QueryParams,
|
|
14
15
|
QueryResult,
|
|
@@ -23,8 +24,13 @@ import type {
|
|
|
23
24
|
ImportJobUndoResult,
|
|
24
25
|
ListImportJobsOptions,
|
|
25
26
|
} from '@object-ui/types';
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
convertFiltersToAST,
|
|
29
|
+
emulateBatchTransaction,
|
|
30
|
+
normalizeSchemaReferenceKeys,
|
|
31
|
+
} from '@object-ui/core';
|
|
27
32
|
import { MetadataCache } from './cache/MetadataCache';
|
|
33
|
+
import { MetadataClient } from './metadata-client';
|
|
28
34
|
import {
|
|
29
35
|
ObjectStackError,
|
|
30
36
|
MetadataNotFoundError,
|
|
@@ -173,6 +179,44 @@ export function clearSharedDiscoveryCache(): void {
|
|
|
173
179
|
discoveryCache.clear();
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Read the cross-object atomic-batch capability from a `discovery` document
|
|
184
|
+
* (framework #3298 / objectui #2693). The server advertises it hierarchically
|
|
185
|
+
* under `capabilities.transactionalBatch.enabled`; the published
|
|
186
|
+
* `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
|
|
187
|
+
* boolean` form and normalizes the two — mirror that here so the adapter reads
|
|
188
|
+
* the same bit regardless of which shape reaches it.
|
|
189
|
+
*
|
|
190
|
+
* Returns:
|
|
191
|
+
* - `true` — the backend GUARANTEES an atomic `/batch` (declared === enforced,
|
|
192
|
+
* i.e. the route is mounted AND the runtime can honour a transaction): the
|
|
193
|
+
* client may drop its non-atomic fallback and treat any batch failure as a
|
|
194
|
+
* real error.
|
|
195
|
+
* - `false` — the backend explicitly does NOT (route absent, or a runtime that
|
|
196
|
+
* can't open a transaction).
|
|
197
|
+
* - `undefined` — the capability is absent, i.e. the backend predates #3298;
|
|
198
|
+
* the caller must keep the legacy runtime-probe fallback (we can't tell
|
|
199
|
+
* whether `/batch` exists without trying it).
|
|
200
|
+
*/
|
|
201
|
+
export function readTransactionalBatchCapability(
|
|
202
|
+
discovery: unknown,
|
|
203
|
+
): boolean | undefined {
|
|
204
|
+
const caps = (discovery as { capabilities?: unknown } | null | undefined)?.capabilities;
|
|
205
|
+
if (!caps || typeof caps !== 'object') return undefined;
|
|
206
|
+
const value = (caps as Record<string, unknown>).transactionalBatch;
|
|
207
|
+
// Flat form: `{ transactionalBatch: true }`.
|
|
208
|
+
if (typeof value === 'boolean') return value;
|
|
209
|
+
// Hierarchical form: `{ transactionalBatch: { enabled: true } }`.
|
|
210
|
+
if (
|
|
211
|
+
value &&
|
|
212
|
+
typeof value === 'object' &&
|
|
213
|
+
typeof (value as { enabled?: unknown }).enabled === 'boolean'
|
|
214
|
+
) {
|
|
215
|
+
return (value as { enabled: boolean }).enabled;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
/**
|
|
177
221
|
* Detect "missing resource" errors regardless of where they originate.
|
|
178
222
|
*
|
|
@@ -404,6 +448,21 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
404
448
|
// so optional collections like sys_presence don't hammer the server with
|
|
405
449
|
// failing requests on every record open / panel render.
|
|
406
450
|
private missingResources = new Set<string>();
|
|
451
|
+
// Set once the server has told us it can't do a cross-object transactional
|
|
452
|
+
// batch (the client SDK's data.batchTransaction threw HTTP 404/405/501). After
|
|
453
|
+
// that, batchTransaction skips the SDK call and serves every call via the
|
|
454
|
+
// client-side emulation — the non-atomic fallback lives HERE, isolated to the
|
|
455
|
+
// one adapter that has to cope with a backend lacking server atomicity (#2679).
|
|
456
|
+
private batchUnsupported = false;
|
|
457
|
+
// The server's declared cross-object atomic-batch capability, read from
|
|
458
|
+
// discovery at connect() (framework #3298 / objectui #2693). `true` → the
|
|
459
|
+
// backend GUARANTEES an atomic `/batch`, so batchTransaction trusts it and
|
|
460
|
+
// never degrades to the non-atomic emulation (any failure surfaces as a real
|
|
461
|
+
// error). `false` or `undefined` (capability absent → backend predates #3298)
|
|
462
|
+
// → keep the legacy runtime-probe + emulation fallback so a save is still
|
|
463
|
+
// possible; dropping it there would turn "saves, less safe" into "no save
|
|
464
|
+
// path" on older backends (#2679 compatibility constraint).
|
|
465
|
+
private atomicBatchCapability: boolean | undefined;
|
|
407
466
|
// Subscribers registered via onMutation(). Emitted after each successful
|
|
408
467
|
// create/update/delete so data-bound views (ListView, ObjectView, kanban,
|
|
409
468
|
// calendar) auto-refresh — the interface ListView relies on to reflect
|
|
@@ -479,6 +538,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
479
538
|
// helpers continue to work without a redundant fetch.
|
|
480
539
|
(this.client as unknown as { discoveryInfo?: unknown }).discoveryInfo = data;
|
|
481
540
|
|
|
541
|
+
// Record the declared cross-object atomic-batch capability (#3298) so
|
|
542
|
+
// batchTransaction can decide declaratively at call time whether it may
|
|
543
|
+
// trust server atomicity instead of runtime-probing 404/405/501.
|
|
544
|
+
this.atomicBatchCapability = readTransactionalBatchCapability(data);
|
|
545
|
+
|
|
482
546
|
this.connected = true;
|
|
483
547
|
this.reconnectAttempts = 0;
|
|
484
548
|
this.setConnectionState('connected');
|
|
@@ -927,42 +991,119 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
927
991
|
* @returns Promise resolving to array of results
|
|
928
992
|
*/
|
|
929
993
|
/**
|
|
930
|
-
* Cross-object transactional batch (ObjectStack #1604
|
|
931
|
-
* in ONE server transaction — commit all or roll back
|
|
932
|
-
* `{ $ref: <earlier op index> }` resolves to that op's
|
|
933
|
-
* can reference its parent created earlier in the same
|
|
994
|
+
* Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
|
|
995
|
+
* Runs the operations in ONE server transaction — commit all or roll back
|
|
996
|
+
* all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
|
|
997
|
+
* created id, so a child can reference its parent created earlier in the same
|
|
998
|
+
* batch (master-detail).
|
|
999
|
+
*
|
|
1000
|
+
* Transport: the published `@objectstack/client` SDK method
|
|
1001
|
+
* `data.batchTransaction` (framework #3271; shipped since client v16, our
|
|
1002
|
+
* dependency floor). Per AGENTS.md §7 data always flows through the client —
|
|
1003
|
+
* never a hand-rolled `fetch('/api/v1/batch')`.
|
|
1004
|
+
*
|
|
1005
|
+
* Fallback decision — declarative capability negotiation (framework #3298 /
|
|
1006
|
+
* objectui #2693). At connect() we read `capabilities.transactionalBatch`
|
|
1007
|
+
* from discovery:
|
|
1008
|
+
* - Declared `true` → the backend GUARANTEES atomicity (declared ===
|
|
1009
|
+
* enforced). We TRUST it: any batch failure — including 404/405/501 —
|
|
1010
|
+
* surfaces as a real error. No non-atomic client-side compensation. This
|
|
1011
|
+
* is the path modern backends take.
|
|
1012
|
+
* - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
|
|
1013
|
+
* server atomicity, so we keep the legacy behaviour: on 404/405 (no
|
|
1014
|
+
* endpoint) or 501 (runtime without transactions) degrade to the
|
|
1015
|
+
* client-side, NON-atomic {@link emulateBatchTransaction} so a save is
|
|
1016
|
+
* still possible. Removing that here would regress older backends from
|
|
1017
|
+
* "saves, less safe" to "no save path" (#2679 compatibility constraint).
|
|
1018
|
+
* The non-atomic fallback stays isolated to THIS adapter.
|
|
934
1019
|
*/
|
|
935
1020
|
async batchTransaction(
|
|
936
|
-
operations:
|
|
1021
|
+
operations: BatchTransactionOperation[],
|
|
937
1022
|
): Promise<{ results: any[] }> {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1023
|
+
// Ensure discovery (and thus the #3298 capability) is loaded so the
|
|
1024
|
+
// decision below is declarative, not "fire a batch and read the status".
|
|
1025
|
+
await this.connect();
|
|
1026
|
+
|
|
1027
|
+
// When the backend declares atomic batch support we never degrade: a
|
|
1028
|
+
// failure is a real error, not a cue to fall back. Otherwise (declared
|
|
1029
|
+
// false, or capability absent on a pre-#3298 backend) the emulation
|
|
1030
|
+
// fallback below stays active.
|
|
1031
|
+
const guaranteed = this.atomicBatchCapability === true;
|
|
1032
|
+
|
|
1033
|
+
// Already degraded on a non-declaring backend — skip the SDK call.
|
|
1034
|
+
// (Unreachable once `guaranteed`: that path never sets `batchUnsupported`.)
|
|
1035
|
+
if (!guaranteed && this.batchUnsupported) {
|
|
1036
|
+
return emulateBatchTransaction(this, operations);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
try {
|
|
1040
|
+
// Typed SDK method — guaranteed present by the `@objectstack/client@^16`
|
|
1041
|
+
// dependency floor (framework #3271). No hand-rolled POST /api/v1/batch.
|
|
1042
|
+
const payload = await this.client.data.batchTransaction(operations);
|
|
1043
|
+
this.emitBatchMutations(operations, payload?.results);
|
|
1044
|
+
return payload;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
// On a non-declaring backend, endpoint missing (404/405) or a runtime that
|
|
1047
|
+
// can't do transactions (the framework rest-server answers 501
|
|
1048
|
+
// "Transactional batch not supported by this runtime") → degrade to the
|
|
1049
|
+
// non-atomic client emulation so the save still goes through. When the
|
|
1050
|
+
// backend DECLARED support (`guaranteed`), even these are hard errors — a
|
|
1051
|
+
// server that advertised the capability must honour it. Every other status
|
|
1052
|
+
// (400 validation, 401/403 auth, 409 conflict, 500 fault) is a real error
|
|
1053
|
+
// the caller must see — never silently retried.
|
|
1054
|
+
const status = this.errorStatusOf(err);
|
|
1055
|
+
if (!guaranteed && this.batchStatusUnsupported(status)) {
|
|
1056
|
+
return this.fallbackToEmulation(operations, status);
|
|
1057
|
+
}
|
|
1058
|
+
throw err;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** True for statuses that mean "this backend can't do a transactional batch". */
|
|
1063
|
+
private batchStatusUnsupported(status: number | undefined): boolean {
|
|
1064
|
+
return status === 404 || status === 405 || status === 501;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** Best-effort HTTP status extraction from a thrown SDK/client error. */
|
|
1068
|
+
private errorStatusOf(err: unknown): number | undefined {
|
|
1069
|
+
if (!err || typeof err !== 'object') return undefined;
|
|
1070
|
+
const e = err as Record<string, unknown>;
|
|
1071
|
+
const s = e.httpStatus ?? e.status ?? e.statusCode;
|
|
1072
|
+
return typeof s === 'number' ? s : undefined;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Mark the endpoint unsupported (warn once) and serve via emulation. */
|
|
1076
|
+
private fallbackToEmulation(
|
|
1077
|
+
operations: BatchTransactionOperation[],
|
|
1078
|
+
status: number | undefined,
|
|
1079
|
+
): Promise<{ results: any[] }> {
|
|
1080
|
+
if (!this.batchUnsupported) {
|
|
1081
|
+
this.batchUnsupported = true;
|
|
1082
|
+
console.warn(
|
|
1083
|
+
`ObjectStackAdapter: POST /api/v1/batch unavailable (HTTP ${status ?? '?'}) — ` +
|
|
1084
|
+
'falling back to non-atomic client-side batch emulation. Cross-object ' +
|
|
1085
|
+
'saves on this backend are best-effort, not transactional.',
|
|
955
1086
|
);
|
|
956
1087
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1088
|
+
return emulateBatchTransaction(this, operations);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Emit one MutationEvent per committed operation so the invalidation bus
|
|
1093
|
+
* (#2269) sees writes that went through /batch exactly like single
|
|
1094
|
+
* create/update/delete calls — master-detail ModalForm saves otherwise leave
|
|
1095
|
+
* related lists and count badges stale (#2582). `results` is index-aligned
|
|
1096
|
+
* with `operations`; creates take id/record from the server echo.
|
|
1097
|
+
*
|
|
1098
|
+
* Only called on the server-committed paths. The emulation branch drives the
|
|
1099
|
+
* adapter's own create/update/delete primitives, which already emit — so it
|
|
1100
|
+
* must NOT be routed through here, or events would double-fire.
|
|
1101
|
+
*/
|
|
1102
|
+
private emitBatchMutations(
|
|
1103
|
+
operations: BatchTransactionOperation[],
|
|
1104
|
+
rawResults: unknown,
|
|
1105
|
+
): void {
|
|
1106
|
+
const results = Array.isArray(rawResults) ? rawResults : [];
|
|
966
1107
|
operations.forEach((op, i) => {
|
|
967
1108
|
const action = op.action ?? 'create';
|
|
968
1109
|
const echo = results[i];
|
|
@@ -974,7 +1115,6 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
974
1115
|
this.emitMutation({ type: 'delete', resource: op.object, id: op.id });
|
|
975
1116
|
}
|
|
976
1117
|
});
|
|
977
|
-
return payload;
|
|
978
1118
|
}
|
|
979
1119
|
|
|
980
1120
|
async bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]> {
|
|
@@ -1930,32 +2070,25 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1930
2070
|
config: Record<string, any>
|
|
1931
2071
|
): Promise<Record<string, any> | void> {
|
|
1932
2072
|
await this.connect();
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
return result ?? undefined;
|
|
1953
|
-
} catch (err) {
|
|
1954
|
-
// Surface the error so the caller can decide whether to toast/log;
|
|
1955
|
-
// we don't swallow it here because persistence failures are
|
|
1956
|
-
// operationally meaningful (unlike read fallbacks).
|
|
1957
|
-
throw err;
|
|
1958
|
-
}
|
|
2073
|
+
// ADR-0005 metadata customization overlay: persist views under
|
|
2074
|
+
// `type='view'` (NOT `type=<objectName>` — that was a pre-overlay
|
|
2075
|
+
// misuse that hit `/api/v1/meta/<objectName>/<viewId>`, which the
|
|
2076
|
+
// server never wired). The view's `data.object` field is what
|
|
2077
|
+
// associates it back to the object on read.
|
|
2078
|
+
const merged = { ...(config || {}), object: (config as any)?.object || objectName, name: viewId };
|
|
2079
|
+
const result: any = await this.client.meta.saveItem(
|
|
2080
|
+
'view',
|
|
2081
|
+
viewId,
|
|
2082
|
+
merged
|
|
2083
|
+
);
|
|
2084
|
+
// Invalidate cached read so next getView reflects the change
|
|
2085
|
+
const cacheKey = `view:${objectName}:${viewId}`;
|
|
2086
|
+
this.metadataCache.invalidate?.(cacheKey);
|
|
2087
|
+
// Also invalidate the batch override map so listViewOverrides re-fetches
|
|
2088
|
+
this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
|
|
2089
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2090
|
+
if (result && result.item) return result.item;
|
|
2091
|
+
return result ?? undefined;
|
|
1959
2092
|
}
|
|
1960
2093
|
|
|
1961
2094
|
/**
|
|
@@ -1968,13 +2101,31 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1968
2101
|
* Filters by `data.object === objectName` (or top-level `object`)
|
|
1969
2102
|
* client-side because the metadata index is name-only, not field-typed.
|
|
1970
2103
|
*/
|
|
1971
|
-
async listViews(
|
|
2104
|
+
async listViews(
|
|
2105
|
+
objectName: string,
|
|
2106
|
+
options?: { previewDrafts?: boolean },
|
|
2107
|
+
): Promise<any[]> {
|
|
1972
2108
|
await this.connect();
|
|
1973
2109
|
try {
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2110
|
+
let items: any[];
|
|
2111
|
+
if (options?.previewDrafts) {
|
|
2112
|
+
// ADR-0037 + #2767 (P2/P3): a SINGLE `?preview=draft` request already
|
|
2113
|
+
// returns the active+draft OVERLAID list — draft wins by name,
|
|
2114
|
+
// draft-only views surface, each draft tagged `_draft: true`. So we
|
|
2115
|
+
// REPLACE the published list wholesale (never fetch both and append,
|
|
2116
|
+
// which double-lists a draft that edits a published view). Route it
|
|
2117
|
+
// through `MetadataClient` rather than a hand-rolled fetch so the
|
|
2118
|
+
// metadata route + any environment scoping stay in one place.
|
|
2119
|
+
const draftItems = await this.metadataClient()
|
|
2120
|
+
.withPreviewDrafts(true)
|
|
2121
|
+
.list<any>('view');
|
|
2122
|
+
items = Array.isArray(draftItems) ? draftItems : [];
|
|
2123
|
+
} else {
|
|
2124
|
+
const result: any = await this.client.meta.getItems('view');
|
|
2125
|
+
items = Array.isArray(result?.items)
|
|
2126
|
+
? result.items
|
|
2127
|
+
: Array.isArray(result) ? result : [];
|
|
2128
|
+
}
|
|
1978
2129
|
// This feeds the list-view switcher (ViewTabBar), so it must return
|
|
1979
2130
|
// LIST-family views only. The backend now exposes each view as an
|
|
1980
2131
|
// independent ViewItem carrying a `viewKind` discriminant (ADR-0017);
|
|
@@ -1993,6 +2144,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1993
2144
|
return !(viewKind && FORM_FAMILY.has(viewKind));
|
|
1994
2145
|
}).map((v: any) => {
|
|
1995
2146
|
const spec = v.list ?? v;
|
|
2147
|
+
// Preserve the draft provenance flag so the switcher can badge an
|
|
2148
|
+
// unpublished view (ADR-0037). The overlay tags the item, not its
|
|
2149
|
+
// nested spec, so read from either.
|
|
2150
|
+
const isDraft = v._draft === true || spec?._draft === true;
|
|
1996
2151
|
// Canonical ViewItem (ADR-0017) carries its body under `config`;
|
|
1997
2152
|
// the display `type` (grid/kanban/gallery/…) lives at `config.type`,
|
|
1998
2153
|
// and only the list/form *family* sits at the top level (`viewKind`).
|
|
@@ -2008,9 +2163,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2008
2163
|
name: spec.name ?? spec.config.name,
|
|
2009
2164
|
label: spec.label ?? spec.config.label,
|
|
2010
2165
|
isDefault: !!spec.isDefault,
|
|
2166
|
+
...(isDraft ? { _draft: true } : {}),
|
|
2011
2167
|
};
|
|
2012
2168
|
}
|
|
2013
|
-
return spec;
|
|
2169
|
+
return isDraft ? { ...spec, _draft: true } : spec;
|
|
2014
2170
|
});
|
|
2015
2171
|
} catch (err) {
|
|
2016
2172
|
console.warn('[OBJECTSTACKDataSource] listViews failed:', err);
|
|
@@ -2018,6 +2174,20 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2018
2174
|
}
|
|
2019
2175
|
}
|
|
2020
2176
|
|
|
2177
|
+
/**
|
|
2178
|
+
* Build a {@link MetadataClient} bound to this adapter's server + auth. Used
|
|
2179
|
+
* by draft-aware reads (`listViews({ previewDrafts })`) so the `/meta` route,
|
|
2180
|
+
* `?preview=draft` flag, and environment scoping live in the SDK rather than
|
|
2181
|
+
* being hand-assembled at each call site (#2767 P3).
|
|
2182
|
+
*/
|
|
2183
|
+
private metadataClient(): MetadataClient {
|
|
2184
|
+
return new MetadataClient({
|
|
2185
|
+
baseUrl: this.baseUrl,
|
|
2186
|
+
fetch: this.fetchImpl,
|
|
2187
|
+
...(this.token ? { headers: { Authorization: `Bearer ${this.token}` } } : {}),
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2021
2191
|
/**
|
|
2022
2192
|
* List registered import `mapping` artifacts targeting a given object
|
|
2023
2193
|
* (framework #2611). Reads the `mapping` metadata kind via the overlay API
|
|
@@ -2075,14 +2245,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2075
2245
|
object: spec?.object || objectName,
|
|
2076
2246
|
data: spec?.data || { provider: 'object', object: objectName },
|
|
2077
2247
|
};
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
return fullSpec;
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
throw err;
|
|
2085
|
-
}
|
|
2248
|
+
const result: any = await this.client.meta.saveItem('view', name, fullSpec);
|
|
2249
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2250
|
+
if (result && result.item) return result.item;
|
|
2251
|
+
return fullSpec;
|
|
2086
2252
|
}
|
|
2087
2253
|
|
|
2088
2254
|
/**
|
|
@@ -2112,15 +2278,11 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2112
2278
|
name: viewName,
|
|
2113
2279
|
object: current?.object || (current as any)?.data?.object || objectName,
|
|
2114
2280
|
};
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
return merged;
|
|
2121
|
-
} catch (err) {
|
|
2122
|
-
throw err;
|
|
2123
|
-
}
|
|
2281
|
+
const result: any = await this.client.meta.saveItem('view', viewName, merged);
|
|
2282
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2283
|
+
this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
|
|
2284
|
+
if (result && result.item) return result.item;
|
|
2285
|
+
return merged;
|
|
2124
2286
|
}
|
|
2125
2287
|
|
|
2126
2288
|
/**
|
|
@@ -2133,14 +2295,10 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2133
2295
|
viewName: string,
|
|
2134
2296
|
): Promise<{ deleted: boolean }> {
|
|
2135
2297
|
await this.connect();
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
|
|
2141
|
-
} catch (err) {
|
|
2142
|
-
throw err;
|
|
2143
|
-
}
|
|
2298
|
+
const result: any = await this.client.meta.deleteItem('view', viewName);
|
|
2299
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2300
|
+
this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
|
|
2301
|
+
return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
|
|
2144
2302
|
}
|
|
2145
2303
|
|
|
2146
2304
|
|
|
@@ -2207,21 +2365,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2207
2365
|
schema: Record<string, any>
|
|
2208
2366
|
): Promise<Record<string, any> | void> {
|
|
2209
2367
|
await this.connect();
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
return result ?? undefined;
|
|
2222
|
-
} catch (err) {
|
|
2223
|
-
throw err;
|
|
2224
|
-
}
|
|
2368
|
+
const result: any = await this.client.meta.saveItem(
|
|
2369
|
+
'dashboard',
|
|
2370
|
+
dashboardName,
|
|
2371
|
+
schema
|
|
2372
|
+
);
|
|
2373
|
+
// Invalidate dashboards list and any cached dashboard read so the
|
|
2374
|
+
// next render reflects the change.
|
|
2375
|
+
this.metadataCache.invalidate?.('dashboards');
|
|
2376
|
+
this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
|
|
2377
|
+
if (result && result.item) return result.item;
|
|
2378
|
+
return result ?? undefined;
|
|
2225
2379
|
}
|
|
2226
2380
|
|
|
2227
2381
|
/**
|
package/src/integration.ts
CHANGED
|
@@ -143,6 +143,10 @@ export class IntegrationManager {
|
|
|
143
143
|
}
|
|
144
144
|
} catch (e) {
|
|
145
145
|
if (e instanceof TypeError) {
|
|
146
|
+
// A malformed URL surfaces as its own message; we can't attach `e`
|
|
147
|
+
// as the `Error` `cause` because this package targets ES2020, whose
|
|
148
|
+
// lib types the 1-arg `Error` constructor only. Scoped disable.
|
|
149
|
+
// eslint-disable-next-line preserve-caught-error
|
|
146
150
|
throw new Error(`Invalid webhook URL: ${url}`);
|
|
147
151
|
}
|
|
148
152
|
throw e;
|
|
@@ -168,6 +172,10 @@ export class IntegrationManager {
|
|
|
168
172
|
}
|
|
169
173
|
} catch (e) {
|
|
170
174
|
if (e instanceof TypeError) {
|
|
175
|
+
// A malformed URL surfaces as its own message; we can't attach `e`
|
|
176
|
+
// as the `Error` `cause` because this package targets ES2020, whose
|
|
177
|
+
// lib types the 1-arg `Error` constructor only. Scoped disable.
|
|
178
|
+
// eslint-disable-next-line preserve-caught-error
|
|
171
179
|
throw new Error(`Invalid Slack webhook URL: ${url}`);
|
|
172
180
|
}
|
|
173
181
|
throw e;
|