@object-ui/data-objectstack 6.2.3 → 7.0.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 +133 -0
- package/dist/index.cjs +344 -15
- package/dist/index.d.cts +368 -5
- package/dist/index.d.ts +368 -5
- package/dist/index.js +344 -15
- package/package.json +5 -5
- package/src/index.ts +216 -8
- package/src/listViews.test.ts +78 -0
- package/src/metadata-client.test.ts +141 -0
- package/src/metadata-client.ts +469 -11
- package/src/queryDataset.test.ts +83 -0
package/dist/index.js
CHANGED
|
@@ -766,9 +766,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
766
766
|
__publicField(this, "base");
|
|
767
767
|
__publicField(this, "fetchImpl");
|
|
768
768
|
__publicField(this, "headers");
|
|
769
|
+
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
770
|
+
__publicField(this, "previewDrafts");
|
|
769
771
|
this.base = buildBase(config);
|
|
770
772
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
771
773
|
this.headers = { Accept: "application/json", ...config.headers ?? {} };
|
|
774
|
+
this.previewDrafts = config.previewDrafts === true;
|
|
772
775
|
}
|
|
773
776
|
/** Update the client's environment scope at runtime. */
|
|
774
777
|
withEnvironment(environmentId) {
|
|
@@ -776,7 +779,24 @@ var MetadataClient = class _MetadataClient {
|
|
|
776
779
|
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
777
780
|
environmentId,
|
|
778
781
|
fetch: this.fetchImpl,
|
|
779
|
-
headers: this.headers
|
|
782
|
+
headers: this.headers,
|
|
783
|
+
previewDrafts: this.previewDrafts
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Derive a client whose reads render the draft-overlaid world (or not).
|
|
788
|
+
* Same base/fetch/headers; used by the app-shell to switch the whole
|
|
789
|
+
* renderer tree into ADR-0037 preview mode off one URL flag.
|
|
790
|
+
*/
|
|
791
|
+
withPreviewDrafts(previewDrafts) {
|
|
792
|
+
if (previewDrafts === this.previewDrafts) return this;
|
|
793
|
+
const envMatch = this.base.match(/\/api\/v\d+\/environments\/([^/]+)\/meta$/);
|
|
794
|
+
return new _MetadataClient({
|
|
795
|
+
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
796
|
+
...envMatch ? { environmentId: decodeURIComponent(envMatch[1]) } : {},
|
|
797
|
+
fetch: this.fetchImpl,
|
|
798
|
+
headers: this.headers,
|
|
799
|
+
previewDrafts
|
|
780
800
|
});
|
|
781
801
|
}
|
|
782
802
|
/** List all registered metadata types (returns the registry rows). */
|
|
@@ -794,7 +814,10 @@ var MetadataClient = class _MetadataClient {
|
|
|
794
814
|
}
|
|
795
815
|
/** List items of a metadata type (e.g. `object`, `field`, `view`). */
|
|
796
816
|
async list(type, options = {}) {
|
|
797
|
-
const
|
|
817
|
+
const params = [];
|
|
818
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
819
|
+
if (this.previewDrafts) params.push("preview=draft");
|
|
820
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
798
821
|
const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
|
|
799
822
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
800
823
|
if (!res.ok) throw await parseError(res);
|
|
@@ -805,25 +828,70 @@ var MetadataClient = class _MetadataClient {
|
|
|
805
828
|
}
|
|
806
829
|
return [];
|
|
807
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* List pending DRAFT items (ADR-0033) — what an AI authored but nobody
|
|
833
|
+
* published yet. `list()` only sees published/active metadata, so a
|
|
834
|
+
* just-built app package looks empty there; this surfaces the drafts so the
|
|
835
|
+
* console can show a "pending changes" view and draft-aware package contents.
|
|
836
|
+
* Optionally narrow by `packageId` and/or `type`. Returns light headers
|
|
837
|
+
* (no body) carrying `packageId` for grouping.
|
|
838
|
+
*/
|
|
839
|
+
async listDrafts(options = {}) {
|
|
840
|
+
const params = [];
|
|
841
|
+
if (options.packageId) params.push(`packageId=${encodeURIComponent(options.packageId)}`);
|
|
842
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
843
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
844
|
+
const url = `${this.base}/_drafts${qs}`;
|
|
845
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
846
|
+
if (!res.ok) throw await parseError(res);
|
|
847
|
+
const data = await res.json();
|
|
848
|
+
if (Array.isArray(data)) return data;
|
|
849
|
+
return data?.drafts ?? data?.data?.drafts ?? [];
|
|
850
|
+
}
|
|
808
851
|
/**
|
|
809
852
|
* Get a single metadata item. Returns the unwrapped item content
|
|
810
853
|
* (matching the framework REST handler which calls `res.json(item)`).
|
|
811
854
|
* Returns `null` on 404 to keep the call-site ergonomic.
|
|
812
855
|
*/
|
|
813
|
-
async get(type, name) {
|
|
814
|
-
const
|
|
856
|
+
async get(type, name, options = {}) {
|
|
857
|
+
const params = [];
|
|
858
|
+
if (options.state === "draft") params.push("state=draft");
|
|
859
|
+
else if (this.previewDrafts) params.push("preview=draft");
|
|
860
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
861
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
862
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
815
863
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
816
864
|
if (res.status === 404) return null;
|
|
817
865
|
if (!res.ok) throw await parseError(res);
|
|
818
866
|
return await res.json();
|
|
819
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Read the pending draft body for an item (`?state=draft`). Returns
|
|
870
|
+
* `null` when there is no draft pending. Draft reads do NOT fall
|
|
871
|
+
* back to the published overlay or the artifact registry — a `null`
|
|
872
|
+
* unambiguously means "nothing to publish".
|
|
873
|
+
*
|
|
874
|
+
* Note: the framework wraps draft responses in an envelope
|
|
875
|
+
* `{ type, name, item }` (matching `getMetaItem`); callers should
|
|
876
|
+
* read `.item` to get the body. The legacy `get()` returns the
|
|
877
|
+
* unwrapped body, so this method preserves that asymmetry by
|
|
878
|
+
* returning whatever the server sent.
|
|
879
|
+
*/
|
|
880
|
+
async getDraft(type, name, options = {}) {
|
|
881
|
+
return this.get(type, name, { state: "draft", ...options.packageId ? { packageId: options.packageId } : {} });
|
|
882
|
+
}
|
|
820
883
|
/**
|
|
821
884
|
* Save (PUT) a metadata item. The framework accepts both the bare
|
|
822
885
|
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
823
|
-
* envelopes; we send bare for consistency.
|
|
886
|
+
* envelopes; we send bare for consistency. Pass `mode: 'draft'` to
|
|
887
|
+
* stage the change without publishing (Studio's "Save" button).
|
|
824
888
|
*/
|
|
825
889
|
async save(type, name, item, options = {}) {
|
|
826
|
-
const
|
|
890
|
+
const params = [];
|
|
891
|
+
if (options.force) params.push("force=true");
|
|
892
|
+
if (options.mode === "draft") params.push("mode=draft");
|
|
893
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
894
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
827
895
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
828
896
|
const headers = {
|
|
829
897
|
...this.headers,
|
|
@@ -839,13 +907,38 @@ var MetadataClient = class _MetadataClient {
|
|
|
839
907
|
if (!res.ok) throw await parseError(res);
|
|
840
908
|
return await res.json();
|
|
841
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Publish a single pending draft BY REFERENCE — promotes the draft row for
|
|
912
|
+
* `(type, name)` into the active overlay and drops the draft. Works for ANY
|
|
913
|
+
* draft, including ones with no `packageId` binding (which the package-scoped
|
|
914
|
+
* `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
|
|
915
|
+
* exact set returned by {@link listDrafts} without needing a package.
|
|
916
|
+
*
|
|
917
|
+
* Returns the server result. For `seed` drafts the protocol also materializes
|
|
918
|
+
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
919
|
+
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
920
|
+
* rather than assume the data went live.
|
|
921
|
+
*/
|
|
922
|
+
async publishDraft(type, name) {
|
|
923
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
924
|
+
const res = await this.fetchImpl(url, {
|
|
925
|
+
method: "POST",
|
|
926
|
+
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
927
|
+
body: "{}"
|
|
928
|
+
});
|
|
929
|
+
if (!res.ok) throw await parseError(res);
|
|
930
|
+
const body = await res.json().catch(() => ({}));
|
|
931
|
+
const inner = body?.data && typeof body.data === "object" ? body.data : body;
|
|
932
|
+
return inner;
|
|
933
|
+
}
|
|
842
934
|
/**
|
|
843
935
|
* Get the 3-state layered view of a metadata item (Phase 3a). Returns
|
|
844
936
|
* `code` (the artifact / fallback default), `overlay` (the saved
|
|
845
937
|
* customisation, if any), and `effective` (what the runtime sees).
|
|
846
938
|
*/
|
|
847
|
-
async layered(type, name) {
|
|
848
|
-
const
|
|
939
|
+
async layered(type, name, options = {}) {
|
|
940
|
+
const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
|
|
941
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
|
|
849
942
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
850
943
|
if (res.status === 404) {
|
|
851
944
|
return { code: null, overlay: null, overlayScope: null, effective: null };
|
|
@@ -865,7 +958,50 @@ var MetadataClient = class _MetadataClient {
|
|
|
865
958
|
code: body.code ?? null,
|
|
866
959
|
overlay: body.overlay ?? null,
|
|
867
960
|
overlayScope: body.overlayScope ?? null,
|
|
868
|
-
effective: body.effective ?? null
|
|
961
|
+
effective: body.effective ?? null,
|
|
962
|
+
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
963
|
+
// ADR-0010 Phase 4 — pass through the metadata-protection envelope so
|
|
964
|
+
// the editor can render lock affordances without a second round trip.
|
|
965
|
+
...body.lock !== void 0 ? { lock: body.lock } : {},
|
|
966
|
+
...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
|
|
967
|
+
...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
|
|
968
|
+
...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
|
|
969
|
+
...body.provenance !== void 0 ? { provenance: body.provenance } : {},
|
|
970
|
+
...body.packageId !== void 0 ? { packageId: body.packageId } : {},
|
|
971
|
+
...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
|
|
972
|
+
...body.editable !== void 0 ? { editable: body.editable } : {},
|
|
973
|
+
...body.deletable !== void 0 ? { deletable: body.deletable } : {},
|
|
974
|
+
...body.resettable !== void 0 ? { resettable: body.resettable } : {}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Cross-type sweep of load-time validation results — calls
|
|
979
|
+
* `GET /meta/diagnostics`. Returns every entry the framework
|
|
980
|
+
* considers invalid (or, when `severity: 'warning'`, also entries
|
|
981
|
+
* with only warnings).
|
|
982
|
+
*
|
|
983
|
+
* Used by the Studio's governance overview page and by the
|
|
984
|
+
* directory page to show "N invalid" badges per metadata type.
|
|
985
|
+
*/
|
|
986
|
+
async diagnostics(options = {}) {
|
|
987
|
+
const params = [];
|
|
988
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
989
|
+
if (options.severity) params.push(`severity=${encodeURIComponent(options.severity)}`);
|
|
990
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
991
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
992
|
+
const url = `${this.base}/diagnostics${qs}`;
|
|
993
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
994
|
+
if (res.status === 404) {
|
|
995
|
+
return { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
|
|
996
|
+
}
|
|
997
|
+
if (!res.ok) throw await parseError(res);
|
|
998
|
+
const data = await res.json();
|
|
999
|
+
return {
|
|
1000
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
1001
|
+
total: typeof data.total === "number" ? data.total : 0,
|
|
1002
|
+
scannedTypes: typeof data.scannedTypes === "number" ? data.scannedTypes : 0,
|
|
1003
|
+
scannedItems: typeof data.scannedItems === "number" ? data.scannedItems : 0,
|
|
1004
|
+
stats: data.stats && typeof data.stats === "object" ? data.stats : {}
|
|
869
1005
|
};
|
|
870
1006
|
}
|
|
871
1007
|
/**
|
|
@@ -884,9 +1020,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
884
1020
|
/**
|
|
885
1021
|
* Reset a metadata customization overlay back to the artifact default.
|
|
886
1022
|
* Idempotent: returns the result even when no overlay row existed.
|
|
1023
|
+
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
1024
|
+
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
887
1025
|
*/
|
|
888
1026
|
async reset(type, name, options = {}) {
|
|
889
|
-
const
|
|
1027
|
+
const qs = options.state === "draft" ? "?state=draft" : "";
|
|
1028
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
890
1029
|
const headers = { ...this.headers };
|
|
891
1030
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
892
1031
|
if (options.actor) headers["X-Actor"] = options.actor;
|
|
@@ -894,6 +1033,75 @@ var MetadataClient = class _MetadataClient {
|
|
|
894
1033
|
if (!res.ok) throw await parseError(res);
|
|
895
1034
|
return await res.json();
|
|
896
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Promote the pending draft of an item to the active overlay
|
|
1038
|
+
* (POST `/meta/:type/:name/publish`). Returns
|
|
1039
|
+
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
1040
|
+
* nothing is pending and `409 metadata_conflict` if the published
|
|
1041
|
+
* overlay moved while the draft was sitting.
|
|
1042
|
+
*/
|
|
1043
|
+
async publish(type, name, options = {}) {
|
|
1044
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
1045
|
+
const headers = {
|
|
1046
|
+
...this.headers,
|
|
1047
|
+
"Content-Type": "application/json"
|
|
1048
|
+
};
|
|
1049
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1050
|
+
const res = await this.fetchImpl(url, {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers,
|
|
1053
|
+
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
1054
|
+
});
|
|
1055
|
+
if (!res.ok) throw await parseError(res);
|
|
1056
|
+
return await res.json();
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
|
|
1060
|
+
* The server reads the history row at `toVersion`, writes its body back
|
|
1061
|
+
* as the active overlay with `operation_type='revert'`. Returns
|
|
1062
|
+
* `{ success, version, seq, restoredFromVersion, message }`. Throws
|
|
1063
|
+
* `404 version_not_found` for unknown versions and `409 version_not_restorable`
|
|
1064
|
+
* when the target version is a delete tombstone.
|
|
1065
|
+
*/
|
|
1066
|
+
async rollback(type, name, toVersion, options = {}) {
|
|
1067
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`;
|
|
1068
|
+
const headers = {
|
|
1069
|
+
...this.headers,
|
|
1070
|
+
"Content-Type": "application/json"
|
|
1071
|
+
};
|
|
1072
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1073
|
+
const res = await this.fetchImpl(url, {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
headers,
|
|
1076
|
+
body: JSON.stringify({
|
|
1077
|
+
toVersion,
|
|
1078
|
+
...options.message ? { message: options.message } : {}
|
|
1079
|
+
})
|
|
1080
|
+
});
|
|
1081
|
+
if (!res.ok) throw await parseError(res);
|
|
1082
|
+
return await res.json();
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Compute a structured top-level key diff between two history versions
|
|
1086
|
+
* (GET `/meta/:type/:name/diff?from=&to=`). Returns
|
|
1087
|
+
* `{ added, removed, changed }` where each entry carries a `path` and
|
|
1088
|
+
* the relevant value(s). Useful for "what changed in this draft?" and
|
|
1089
|
+
* pre-rollback previews.
|
|
1090
|
+
*/
|
|
1091
|
+
async diff(type, name, fromVersion, toVersion) {
|
|
1092
|
+
const params = new URLSearchParams();
|
|
1093
|
+
if (fromVersion !== void 0) params.set("from", String(fromVersion));
|
|
1094
|
+
if (toVersion !== void 0) params.set("to", String(toVersion));
|
|
1095
|
+
const qs = params.toString();
|
|
1096
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ""}`;
|
|
1097
|
+
const res = await this.fetchImpl(url, {
|
|
1098
|
+
method: "GET",
|
|
1099
|
+
headers: this.headers,
|
|
1100
|
+
cache: "no-store"
|
|
1101
|
+
});
|
|
1102
|
+
if (!res.ok) throw await parseError(res);
|
|
1103
|
+
return await res.json();
|
|
1104
|
+
}
|
|
897
1105
|
/** Fetch the durable history (change log) for one metadata item. */
|
|
898
1106
|
async history(type, name, options = {}) {
|
|
899
1107
|
const params = new URLSearchParams();
|
|
@@ -905,6 +1113,22 @@ var MetadataClient = class _MetadataClient {
|
|
|
905
1113
|
if (!res.ok) throw await parseError(res);
|
|
906
1114
|
return await res.json();
|
|
907
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Fetch the protection-audit trail for one metadata item
|
|
1118
|
+
* (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
|
|
1119
|
+
* delete/reset attempt — allowed, denied, or forced — so the
|
|
1120
|
+
* Studio "审计日志 / Audit log" tab can render who tried what
|
|
1121
|
+
* and whether a lock blocked it.
|
|
1122
|
+
*/
|
|
1123
|
+
async audit(type, name, options = {}) {
|
|
1124
|
+
const params = new URLSearchParams();
|
|
1125
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
1126
|
+
const qs = params.toString();
|
|
1127
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs ? `?${qs}` : ""}`;
|
|
1128
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
1129
|
+
if (!res.ok) throw await parseError(res);
|
|
1130
|
+
return await res.json();
|
|
1131
|
+
}
|
|
908
1132
|
};
|
|
909
1133
|
|
|
910
1134
|
// src/security.ts
|
|
@@ -1389,7 +1613,7 @@ var ObjectStackAdapter = class {
|
|
|
1389
1613
|
if (existing) return existing;
|
|
1390
1614
|
const promise = (async () => {
|
|
1391
1615
|
await this.connect();
|
|
1392
|
-
if (params?.$expand && params.$expand.length > 0) {
|
|
1616
|
+
if (params?.$expand && params.$expand.length > 0 || params?.$search != null && String(params.$search).trim() !== "") {
|
|
1393
1617
|
const result = await this.rawFindWithPopulate(resource, params);
|
|
1394
1618
|
return this.normalizeQueryResult(result, params);
|
|
1395
1619
|
}
|
|
@@ -1587,6 +1811,34 @@ var ObjectStackAdapter = class {
|
|
|
1587
1811
|
* @param data - Array of records to process
|
|
1588
1812
|
* @returns Promise resolving to array of results
|
|
1589
1813
|
*/
|
|
1814
|
+
/**
|
|
1815
|
+
* Cross-object transactional batch (ObjectStack #1604). Runs the operations
|
|
1816
|
+
* in ONE server transaction — commit all or roll back all. A field value of
|
|
1817
|
+
* `{ $ref: <earlier op index> }` resolves to that op's created id, so a child
|
|
1818
|
+
* can reference its parent created earlier in the same batch (master-detail).
|
|
1819
|
+
*/
|
|
1820
|
+
async batchTransaction(operations) {
|
|
1821
|
+
const url = `${this.baseUrl}/api/v1/batch`;
|
|
1822
|
+
const response = await fetch(url, {
|
|
1823
|
+
method: "POST",
|
|
1824
|
+
headers: { "Content-Type": "application/json", ...this.getAuthHeaders() },
|
|
1825
|
+
// Send the session cookie too: in the browser console auth is cookie-based
|
|
1826
|
+
// (no bearer `token`), so without `credentials: 'include'` this raw fetch
|
|
1827
|
+
// is unauthenticated — every master-detail batch save would 401. Bearer
|
|
1828
|
+
// (server-to-server) and cookie (console) auth now both work.
|
|
1829
|
+
credentials: "include",
|
|
1830
|
+
body: JSON.stringify({ operations })
|
|
1831
|
+
});
|
|
1832
|
+
if (!response.ok) {
|
|
1833
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
1834
|
+
throw new ObjectStackError(
|
|
1835
|
+
error.error || error.message || `Batch failed with status ${response.status}`,
|
|
1836
|
+
"BATCH_ERROR",
|
|
1837
|
+
response.status
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return response.json();
|
|
1841
|
+
}
|
|
1590
1842
|
async bulk(resource, operation, data) {
|
|
1591
1843
|
await this.connect();
|
|
1592
1844
|
if (!data || data.length === 0) {
|
|
@@ -1724,13 +1976,14 @@ var ObjectStackAdapter = class {
|
|
|
1724
1976
|
const resultObj = result;
|
|
1725
1977
|
const records = resultObj.records || resultObj.value || [];
|
|
1726
1978
|
const total = resultObj.total ?? resultObj.count ?? records.length;
|
|
1979
|
+
const hasMore = typeof resultObj.hasMore === "boolean" ? resultObj.hasMore : params?.$top ? records.length === params.$top : false;
|
|
1727
1980
|
return {
|
|
1728
1981
|
data: records,
|
|
1729
1982
|
total,
|
|
1730
1983
|
// Calculate page number safely
|
|
1731
1984
|
page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
|
|
1732
1985
|
pageSize: params?.$top,
|
|
1733
|
-
hasMore
|
|
1986
|
+
hasMore
|
|
1734
1987
|
};
|
|
1735
1988
|
}
|
|
1736
1989
|
/**
|
|
@@ -1750,6 +2003,12 @@ var ObjectStackAdapter = class {
|
|
|
1750
2003
|
if (params.$skip !== void 0) {
|
|
1751
2004
|
queryParams.set("skip", String(params.$skip));
|
|
1752
2005
|
}
|
|
2006
|
+
if (params.$search != null && String(params.$search).trim() !== "") {
|
|
2007
|
+
queryParams.set("search", String(params.$search).trim());
|
|
2008
|
+
}
|
|
2009
|
+
if (params.$searchFields && params.$searchFields.length > 0) {
|
|
2010
|
+
queryParams.set("searchFields", params.$searchFields.join(","));
|
|
2011
|
+
}
|
|
1753
2012
|
if (params.$select && params.$select.length > 0) {
|
|
1754
2013
|
const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
|
|
1755
2014
|
queryParams.set("select", selectFields.join(","));
|
|
@@ -1850,6 +2109,12 @@ var ObjectStackAdapter = class {
|
|
|
1850
2109
|
if (params.$top !== void 0) {
|
|
1851
2110
|
options.top = params.$top;
|
|
1852
2111
|
}
|
|
2112
|
+
if (params.$search != null && String(params.$search).trim() !== "") {
|
|
2113
|
+
options.search = String(params.$search).trim();
|
|
2114
|
+
}
|
|
2115
|
+
if (params.$searchFields && params.$searchFields.length > 0) {
|
|
2116
|
+
options.searchFields = params.$searchFields;
|
|
2117
|
+
}
|
|
1853
2118
|
return options;
|
|
1854
2119
|
}
|
|
1855
2120
|
/**
|
|
@@ -1956,7 +2221,7 @@ var ObjectStackAdapter = class {
|
|
|
1956
2221
|
try {
|
|
1957
2222
|
const cacheKey = `view:${objectName}:${viewId}`;
|
|
1958
2223
|
return await this.metadataCache.get(cacheKey, async () => {
|
|
1959
|
-
const result = await this.client.meta.getItem(
|
|
2224
|
+
const result = await this.client.meta.getItem("view", viewId);
|
|
1960
2225
|
if (result && result.item) return result.item;
|
|
1961
2226
|
return result ?? null;
|
|
1962
2227
|
});
|
|
@@ -2015,12 +2280,26 @@ var ObjectStackAdapter = class {
|
|
|
2015
2280
|
try {
|
|
2016
2281
|
const result = await this.client.meta.getItems("view");
|
|
2017
2282
|
const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
2283
|
+
const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
|
|
2018
2284
|
return items.filter((v) => {
|
|
2019
2285
|
if (!v) return false;
|
|
2020
2286
|
const spec = v.list ?? v;
|
|
2021
2287
|
const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
|
|
2022
|
-
|
|
2023
|
-
|
|
2288
|
+
if (obj !== objectName) return false;
|
|
2289
|
+
const viewKind = v.viewKind ?? spec?.viewKind;
|
|
2290
|
+
return !(viewKind && FORM_FAMILY.has(viewKind));
|
|
2291
|
+
}).map((v) => {
|
|
2292
|
+
const spec = v.list ?? v;
|
|
2293
|
+
if (spec && spec.config && typeof spec.config === "object") {
|
|
2294
|
+
return {
|
|
2295
|
+
...spec.config,
|
|
2296
|
+
name: spec.name ?? spec.config.name,
|
|
2297
|
+
label: spec.label ?? spec.config.label,
|
|
2298
|
+
isDefault: !!spec.isDefault
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
return spec;
|
|
2302
|
+
});
|
|
2024
2303
|
} catch (err) {
|
|
2025
2304
|
console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
|
|
2026
2305
|
return [];
|
|
@@ -2237,6 +2516,56 @@ var ObjectStackAdapter = class {
|
|
|
2237
2516
|
return this.aggregateClientSide(records, params);
|
|
2238
2517
|
}
|
|
2239
2518
|
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
|
|
2521
|
+
*
|
|
2522
|
+
* Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
|
|
2523
|
+
* `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
|
|
2524
|
+
* inline draft definition — the inline form is what the Studio dataset
|
|
2525
|
+
* editor sends to preview an unsaved draft. The adapter's bearer token is
|
|
2526
|
+
* forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
|
|
2527
|
+
*
|
|
2528
|
+
* Unlike {@link aggregate}, this does NOT fall back to client-side
|
|
2529
|
+
* aggregation: cross-object joins can only run on the server, so a failure
|
|
2530
|
+
* is surfaced to the caller (the preview panel shows the error) rather than
|
|
2531
|
+
* silently returning wrong numbers.
|
|
2532
|
+
*
|
|
2533
|
+
* @param dataset - An inline dataset definition (draft) OR a saved dataset name.
|
|
2534
|
+
* @param selection - Dimension/measure names to project + runtime directives.
|
|
2535
|
+
*/
|
|
2536
|
+
async queryDataset(dataset, selection) {
|
|
2537
|
+
await this.connect();
|
|
2538
|
+
const base = (this.baseUrl || "").replace(/\/$/, "");
|
|
2539
|
+
const url = `${base}/api/v1/analytics/dataset/query`;
|
|
2540
|
+
const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
|
|
2541
|
+
const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
|
|
2542
|
+
const res = await this.fetchImpl(url, {
|
|
2543
|
+
method: "POST",
|
|
2544
|
+
headers: {
|
|
2545
|
+
"Content-Type": "application/json",
|
|
2546
|
+
...this.token ? { Authorization: `Bearer ${this.token}` } : {}
|
|
2547
|
+
},
|
|
2548
|
+
body: JSON.stringify(requestBody)
|
|
2549
|
+
});
|
|
2550
|
+
if (!res.ok) {
|
|
2551
|
+
let detail = "";
|
|
2552
|
+
try {
|
|
2553
|
+
const errBody = await res.json();
|
|
2554
|
+
detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
2555
|
+
} catch {
|
|
2556
|
+
}
|
|
2557
|
+
throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
2558
|
+
}
|
|
2559
|
+
const payload = await res.json();
|
|
2560
|
+
const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
|
|
2561
|
+
const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
|
|
2562
|
+
const fields = Array.isArray(data?.fields) ? data.fields : [];
|
|
2563
|
+
const object = typeof data?.object === "string" ? data.object : void 0;
|
|
2564
|
+
const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
|
|
2565
|
+
const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
|
|
2566
|
+
const totals = Array.isArray(data?.totals) ? data.totals : void 0;
|
|
2567
|
+
return { rows, fields, object, dimensionFields, drillRawRows, totals };
|
|
2568
|
+
}
|
|
2240
2569
|
/** Client-side aggregation fallback */
|
|
2241
2570
|
aggregateClientSide(records, params) {
|
|
2242
2571
|
const { field, function: aggFn, groupBy } = params;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "ObjectStack Data Adapter for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@objectstack/client": "^
|
|
26
|
-
"@object-ui/core": "
|
|
27
|
-
"@object-ui/types": "
|
|
25
|
+
"@objectstack/client": "^10.0.0",
|
|
26
|
+
"@object-ui/core": "7.0.0",
|
|
27
|
+
"@object-ui/types": "7.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|
|
31
31
|
"typescript": "^6.0.3",
|
|
32
|
-
"vitest": "^4.1.
|
|
32
|
+
"vitest": "^4.1.9"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|