@object-ui/data-objectstack 6.2.3 → 7.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 +144 -0
- package/dist/index.cjs +390 -22
- package/dist/index.d.cts +386 -5
- package/dist/index.d.ts +386 -5
- package/dist/index.js +390 -22
- package/package.json +5 -5
- package/src/getObjectSchema.test.ts +77 -0
- package/src/index.ts +279 -18
- 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.cjs
CHANGED
|
@@ -814,9 +814,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
814
814
|
__publicField(this, "base");
|
|
815
815
|
__publicField(this, "fetchImpl");
|
|
816
816
|
__publicField(this, "headers");
|
|
817
|
+
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
818
|
+
__publicField(this, "previewDrafts");
|
|
817
819
|
this.base = buildBase(config);
|
|
818
820
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
819
821
|
this.headers = { Accept: "application/json", ...config.headers ?? {} };
|
|
822
|
+
this.previewDrafts = config.previewDrafts === true;
|
|
820
823
|
}
|
|
821
824
|
/** Update the client's environment scope at runtime. */
|
|
822
825
|
withEnvironment(environmentId) {
|
|
@@ -824,7 +827,24 @@ var MetadataClient = class _MetadataClient {
|
|
|
824
827
|
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
825
828
|
environmentId,
|
|
826
829
|
fetch: this.fetchImpl,
|
|
827
|
-
headers: this.headers
|
|
830
|
+
headers: this.headers,
|
|
831
|
+
previewDrafts: this.previewDrafts
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Derive a client whose reads render the draft-overlaid world (or not).
|
|
836
|
+
* Same base/fetch/headers; used by the app-shell to switch the whole
|
|
837
|
+
* renderer tree into ADR-0037 preview mode off one URL flag.
|
|
838
|
+
*/
|
|
839
|
+
withPreviewDrafts(previewDrafts) {
|
|
840
|
+
if (previewDrafts === this.previewDrafts) return this;
|
|
841
|
+
const envMatch = this.base.match(/\/api\/v\d+\/environments\/([^/]+)\/meta$/);
|
|
842
|
+
return new _MetadataClient({
|
|
843
|
+
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
844
|
+
...envMatch ? { environmentId: decodeURIComponent(envMatch[1]) } : {},
|
|
845
|
+
fetch: this.fetchImpl,
|
|
846
|
+
headers: this.headers,
|
|
847
|
+
previewDrafts
|
|
828
848
|
});
|
|
829
849
|
}
|
|
830
850
|
/** List all registered metadata types (returns the registry rows). */
|
|
@@ -842,7 +862,10 @@ var MetadataClient = class _MetadataClient {
|
|
|
842
862
|
}
|
|
843
863
|
/** List items of a metadata type (e.g. `object`, `field`, `view`). */
|
|
844
864
|
async list(type, options = {}) {
|
|
845
|
-
const
|
|
865
|
+
const params = [];
|
|
866
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
867
|
+
if (this.previewDrafts) params.push("preview=draft");
|
|
868
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
846
869
|
const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
|
|
847
870
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
848
871
|
if (!res.ok) throw await parseError(res);
|
|
@@ -853,25 +876,70 @@ var MetadataClient = class _MetadataClient {
|
|
|
853
876
|
}
|
|
854
877
|
return [];
|
|
855
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* List pending DRAFT items (ADR-0033) — what an AI authored but nobody
|
|
881
|
+
* published yet. `list()` only sees published/active metadata, so a
|
|
882
|
+
* just-built app package looks empty there; this surfaces the drafts so the
|
|
883
|
+
* console can show a "pending changes" view and draft-aware package contents.
|
|
884
|
+
* Optionally narrow by `packageId` and/or `type`. Returns light headers
|
|
885
|
+
* (no body) carrying `packageId` for grouping.
|
|
886
|
+
*/
|
|
887
|
+
async listDrafts(options = {}) {
|
|
888
|
+
const params = [];
|
|
889
|
+
if (options.packageId) params.push(`packageId=${encodeURIComponent(options.packageId)}`);
|
|
890
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
891
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
892
|
+
const url = `${this.base}/_drafts${qs}`;
|
|
893
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
894
|
+
if (!res.ok) throw await parseError(res);
|
|
895
|
+
const data = await res.json();
|
|
896
|
+
if (Array.isArray(data)) return data;
|
|
897
|
+
return data?.drafts ?? data?.data?.drafts ?? [];
|
|
898
|
+
}
|
|
856
899
|
/**
|
|
857
900
|
* Get a single metadata item. Returns the unwrapped item content
|
|
858
901
|
* (matching the framework REST handler which calls `res.json(item)`).
|
|
859
902
|
* Returns `null` on 404 to keep the call-site ergonomic.
|
|
860
903
|
*/
|
|
861
|
-
async get(type, name) {
|
|
862
|
-
const
|
|
904
|
+
async get(type, name, options = {}) {
|
|
905
|
+
const params = [];
|
|
906
|
+
if (options.state === "draft") params.push("state=draft");
|
|
907
|
+
else if (this.previewDrafts) params.push("preview=draft");
|
|
908
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
909
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
910
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
863
911
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
864
912
|
if (res.status === 404) return null;
|
|
865
913
|
if (!res.ok) throw await parseError(res);
|
|
866
914
|
return await res.json();
|
|
867
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Read the pending draft body for an item (`?state=draft`). Returns
|
|
918
|
+
* `null` when there is no draft pending. Draft reads do NOT fall
|
|
919
|
+
* back to the published overlay or the artifact registry — a `null`
|
|
920
|
+
* unambiguously means "nothing to publish".
|
|
921
|
+
*
|
|
922
|
+
* Note: the framework wraps draft responses in an envelope
|
|
923
|
+
* `{ type, name, item }` (matching `getMetaItem`); callers should
|
|
924
|
+
* read `.item` to get the body. The legacy `get()` returns the
|
|
925
|
+
* unwrapped body, so this method preserves that asymmetry by
|
|
926
|
+
* returning whatever the server sent.
|
|
927
|
+
*/
|
|
928
|
+
async getDraft(type, name, options = {}) {
|
|
929
|
+
return this.get(type, name, { state: "draft", ...options.packageId ? { packageId: options.packageId } : {} });
|
|
930
|
+
}
|
|
868
931
|
/**
|
|
869
932
|
* Save (PUT) a metadata item. The framework accepts both the bare
|
|
870
933
|
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
871
|
-
* envelopes; we send bare for consistency.
|
|
934
|
+
* envelopes; we send bare for consistency. Pass `mode: 'draft'` to
|
|
935
|
+
* stage the change without publishing (Studio's "Save" button).
|
|
872
936
|
*/
|
|
873
937
|
async save(type, name, item, options = {}) {
|
|
874
|
-
const
|
|
938
|
+
const params = [];
|
|
939
|
+
if (options.force) params.push("force=true");
|
|
940
|
+
if (options.mode === "draft") params.push("mode=draft");
|
|
941
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
942
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
875
943
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
876
944
|
const headers = {
|
|
877
945
|
...this.headers,
|
|
@@ -887,13 +955,38 @@ var MetadataClient = class _MetadataClient {
|
|
|
887
955
|
if (!res.ok) throw await parseError(res);
|
|
888
956
|
return await res.json();
|
|
889
957
|
}
|
|
958
|
+
/**
|
|
959
|
+
* Publish a single pending draft BY REFERENCE — promotes the draft row for
|
|
960
|
+
* `(type, name)` into the active overlay and drops the draft. Works for ANY
|
|
961
|
+
* draft, including ones with no `packageId` binding (which the package-scoped
|
|
962
|
+
* `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
|
|
963
|
+
* exact set returned by {@link listDrafts} without needing a package.
|
|
964
|
+
*
|
|
965
|
+
* Returns the server result. For `seed` drafts the protocol also materializes
|
|
966
|
+
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
967
|
+
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
968
|
+
* rather than assume the data went live.
|
|
969
|
+
*/
|
|
970
|
+
async publishDraft(type, name) {
|
|
971
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
972
|
+
const res = await this.fetchImpl(url, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
975
|
+
body: "{}"
|
|
976
|
+
});
|
|
977
|
+
if (!res.ok) throw await parseError(res);
|
|
978
|
+
const body = await res.json().catch(() => ({}));
|
|
979
|
+
const inner = body?.data && typeof body.data === "object" ? body.data : body;
|
|
980
|
+
return inner;
|
|
981
|
+
}
|
|
890
982
|
/**
|
|
891
983
|
* Get the 3-state layered view of a metadata item (Phase 3a). Returns
|
|
892
984
|
* `code` (the artifact / fallback default), `overlay` (the saved
|
|
893
985
|
* customisation, if any), and `effective` (what the runtime sees).
|
|
894
986
|
*/
|
|
895
|
-
async layered(type, name) {
|
|
896
|
-
const
|
|
987
|
+
async layered(type, name, options = {}) {
|
|
988
|
+
const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
|
|
989
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
|
|
897
990
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
898
991
|
if (res.status === 404) {
|
|
899
992
|
return { code: null, overlay: null, overlayScope: null, effective: null };
|
|
@@ -913,7 +1006,50 @@ var MetadataClient = class _MetadataClient {
|
|
|
913
1006
|
code: body.code ?? null,
|
|
914
1007
|
overlay: body.overlay ?? null,
|
|
915
1008
|
overlayScope: body.overlayScope ?? null,
|
|
916
|
-
effective: body.effective ?? null
|
|
1009
|
+
effective: body.effective ?? null,
|
|
1010
|
+
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
1011
|
+
// ADR-0010 Phase 4 — pass through the metadata-protection envelope so
|
|
1012
|
+
// the editor can render lock affordances without a second round trip.
|
|
1013
|
+
...body.lock !== void 0 ? { lock: body.lock } : {},
|
|
1014
|
+
...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
|
|
1015
|
+
...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
|
|
1016
|
+
...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
|
|
1017
|
+
...body.provenance !== void 0 ? { provenance: body.provenance } : {},
|
|
1018
|
+
...body.packageId !== void 0 ? { packageId: body.packageId } : {},
|
|
1019
|
+
...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
|
|
1020
|
+
...body.editable !== void 0 ? { editable: body.editable } : {},
|
|
1021
|
+
...body.deletable !== void 0 ? { deletable: body.deletable } : {},
|
|
1022
|
+
...body.resettable !== void 0 ? { resettable: body.resettable } : {}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Cross-type sweep of load-time validation results — calls
|
|
1027
|
+
* `GET /meta/diagnostics`. Returns every entry the framework
|
|
1028
|
+
* considers invalid (or, when `severity: 'warning'`, also entries
|
|
1029
|
+
* with only warnings).
|
|
1030
|
+
*
|
|
1031
|
+
* Used by the Studio's governance overview page and by the
|
|
1032
|
+
* directory page to show "N invalid" badges per metadata type.
|
|
1033
|
+
*/
|
|
1034
|
+
async diagnostics(options = {}) {
|
|
1035
|
+
const params = [];
|
|
1036
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
1037
|
+
if (options.severity) params.push(`severity=${encodeURIComponent(options.severity)}`);
|
|
1038
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
1039
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
1040
|
+
const url = `${this.base}/diagnostics${qs}`;
|
|
1041
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
1042
|
+
if (res.status === 404) {
|
|
1043
|
+
return { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
|
|
1044
|
+
}
|
|
1045
|
+
if (!res.ok) throw await parseError(res);
|
|
1046
|
+
const data = await res.json();
|
|
1047
|
+
return {
|
|
1048
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
1049
|
+
total: typeof data.total === "number" ? data.total : 0,
|
|
1050
|
+
scannedTypes: typeof data.scannedTypes === "number" ? data.scannedTypes : 0,
|
|
1051
|
+
scannedItems: typeof data.scannedItems === "number" ? data.scannedItems : 0,
|
|
1052
|
+
stats: data.stats && typeof data.stats === "object" ? data.stats : {}
|
|
917
1053
|
};
|
|
918
1054
|
}
|
|
919
1055
|
/**
|
|
@@ -932,9 +1068,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
932
1068
|
/**
|
|
933
1069
|
* Reset a metadata customization overlay back to the artifact default.
|
|
934
1070
|
* Idempotent: returns the result even when no overlay row existed.
|
|
1071
|
+
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
1072
|
+
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
935
1073
|
*/
|
|
936
1074
|
async reset(type, name, options = {}) {
|
|
937
|
-
const
|
|
1075
|
+
const qs = options.state === "draft" ? "?state=draft" : "";
|
|
1076
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
938
1077
|
const headers = { ...this.headers };
|
|
939
1078
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
940
1079
|
if (options.actor) headers["X-Actor"] = options.actor;
|
|
@@ -942,6 +1081,75 @@ var MetadataClient = class _MetadataClient {
|
|
|
942
1081
|
if (!res.ok) throw await parseError(res);
|
|
943
1082
|
return await res.json();
|
|
944
1083
|
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Promote the pending draft of an item to the active overlay
|
|
1086
|
+
* (POST `/meta/:type/:name/publish`). Returns
|
|
1087
|
+
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
1088
|
+
* nothing is pending and `409 metadata_conflict` if the published
|
|
1089
|
+
* overlay moved while the draft was sitting.
|
|
1090
|
+
*/
|
|
1091
|
+
async publish(type, name, options = {}) {
|
|
1092
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
1093
|
+
const headers = {
|
|
1094
|
+
...this.headers,
|
|
1095
|
+
"Content-Type": "application/json"
|
|
1096
|
+
};
|
|
1097
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1098
|
+
const res = await this.fetchImpl(url, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers,
|
|
1101
|
+
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
1102
|
+
});
|
|
1103
|
+
if (!res.ok) throw await parseError(res);
|
|
1104
|
+
return await res.json();
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
|
|
1108
|
+
* The server reads the history row at `toVersion`, writes its body back
|
|
1109
|
+
* as the active overlay with `operation_type='revert'`. Returns
|
|
1110
|
+
* `{ success, version, seq, restoredFromVersion, message }`. Throws
|
|
1111
|
+
* `404 version_not_found` for unknown versions and `409 version_not_restorable`
|
|
1112
|
+
* when the target version is a delete tombstone.
|
|
1113
|
+
*/
|
|
1114
|
+
async rollback(type, name, toVersion, options = {}) {
|
|
1115
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`;
|
|
1116
|
+
const headers = {
|
|
1117
|
+
...this.headers,
|
|
1118
|
+
"Content-Type": "application/json"
|
|
1119
|
+
};
|
|
1120
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1121
|
+
const res = await this.fetchImpl(url, {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
headers,
|
|
1124
|
+
body: JSON.stringify({
|
|
1125
|
+
toVersion,
|
|
1126
|
+
...options.message ? { message: options.message } : {}
|
|
1127
|
+
})
|
|
1128
|
+
});
|
|
1129
|
+
if (!res.ok) throw await parseError(res);
|
|
1130
|
+
return await res.json();
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Compute a structured top-level key diff between two history versions
|
|
1134
|
+
* (GET `/meta/:type/:name/diff?from=&to=`). Returns
|
|
1135
|
+
* `{ added, removed, changed }` where each entry carries a `path` and
|
|
1136
|
+
* the relevant value(s). Useful for "what changed in this draft?" and
|
|
1137
|
+
* pre-rollback previews.
|
|
1138
|
+
*/
|
|
1139
|
+
async diff(type, name, fromVersion, toVersion) {
|
|
1140
|
+
const params = new URLSearchParams();
|
|
1141
|
+
if (fromVersion !== void 0) params.set("from", String(fromVersion));
|
|
1142
|
+
if (toVersion !== void 0) params.set("to", String(toVersion));
|
|
1143
|
+
const qs = params.toString();
|
|
1144
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ""}`;
|
|
1145
|
+
const res = await this.fetchImpl(url, {
|
|
1146
|
+
method: "GET",
|
|
1147
|
+
headers: this.headers,
|
|
1148
|
+
cache: "no-store"
|
|
1149
|
+
});
|
|
1150
|
+
if (!res.ok) throw await parseError(res);
|
|
1151
|
+
return await res.json();
|
|
1152
|
+
}
|
|
945
1153
|
/** Fetch the durable history (change log) for one metadata item. */
|
|
946
1154
|
async history(type, name, options = {}) {
|
|
947
1155
|
const params = new URLSearchParams();
|
|
@@ -953,6 +1161,22 @@ var MetadataClient = class _MetadataClient {
|
|
|
953
1161
|
if (!res.ok) throw await parseError(res);
|
|
954
1162
|
return await res.json();
|
|
955
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Fetch the protection-audit trail for one metadata item
|
|
1166
|
+
* (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
|
|
1167
|
+
* delete/reset attempt — allowed, denied, or forced — so the
|
|
1168
|
+
* Studio "审计日志 / Audit log" tab can render who tried what
|
|
1169
|
+
* and whether a lock blocked it.
|
|
1170
|
+
*/
|
|
1171
|
+
async audit(type, name, options = {}) {
|
|
1172
|
+
const params = new URLSearchParams();
|
|
1173
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
1174
|
+
const qs = params.toString();
|
|
1175
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs ? `?${qs}` : ""}`;
|
|
1176
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
1177
|
+
if (!res.ok) throw await parseError(res);
|
|
1178
|
+
return await res.json();
|
|
1179
|
+
}
|
|
956
1180
|
};
|
|
957
1181
|
|
|
958
1182
|
// src/security.ts
|
|
@@ -1437,7 +1661,7 @@ var ObjectStackAdapter = class {
|
|
|
1437
1661
|
if (existing) return existing;
|
|
1438
1662
|
const promise = (async () => {
|
|
1439
1663
|
await this.connect();
|
|
1440
|
-
if (params?.$expand && params.$expand.length > 0) {
|
|
1664
|
+
if (params?.$expand && params.$expand.length > 0 || params?.$search != null && String(params.$search).trim() !== "") {
|
|
1441
1665
|
const result = await this.rawFindWithPopulate(resource, params);
|
|
1442
1666
|
return this.normalizeQueryResult(result, params);
|
|
1443
1667
|
}
|
|
@@ -1635,6 +1859,34 @@ var ObjectStackAdapter = class {
|
|
|
1635
1859
|
* @param data - Array of records to process
|
|
1636
1860
|
* @returns Promise resolving to array of results
|
|
1637
1861
|
*/
|
|
1862
|
+
/**
|
|
1863
|
+
* Cross-object transactional batch (ObjectStack #1604). Runs the operations
|
|
1864
|
+
* in ONE server transaction — commit all or roll back all. A field value of
|
|
1865
|
+
* `{ $ref: <earlier op index> }` resolves to that op's created id, so a child
|
|
1866
|
+
* can reference its parent created earlier in the same batch (master-detail).
|
|
1867
|
+
*/
|
|
1868
|
+
async batchTransaction(operations) {
|
|
1869
|
+
const url = `${this.baseUrl}/api/v1/batch`;
|
|
1870
|
+
const response = await fetch(url, {
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
headers: { "Content-Type": "application/json", ...this.getAuthHeaders() },
|
|
1873
|
+
// Send the session cookie too: in the browser console auth is cookie-based
|
|
1874
|
+
// (no bearer `token`), so without `credentials: 'include'` this raw fetch
|
|
1875
|
+
// is unauthenticated — every master-detail batch save would 401. Bearer
|
|
1876
|
+
// (server-to-server) and cookie (console) auth now both work.
|
|
1877
|
+
credentials: "include",
|
|
1878
|
+
body: JSON.stringify({ operations })
|
|
1879
|
+
});
|
|
1880
|
+
if (!response.ok) {
|
|
1881
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
1882
|
+
throw new ObjectStackError(
|
|
1883
|
+
error.error || error.message || `Batch failed with status ${response.status}`,
|
|
1884
|
+
"BATCH_ERROR",
|
|
1885
|
+
response.status
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return response.json();
|
|
1889
|
+
}
|
|
1638
1890
|
async bulk(resource, operation, data) {
|
|
1639
1891
|
await this.connect();
|
|
1640
1892
|
if (!data || data.length === 0) {
|
|
@@ -1772,13 +2024,14 @@ var ObjectStackAdapter = class {
|
|
|
1772
2024
|
const resultObj = result;
|
|
1773
2025
|
const records = resultObj.records || resultObj.value || [];
|
|
1774
2026
|
const total = resultObj.total ?? resultObj.count ?? records.length;
|
|
2027
|
+
const hasMore = typeof resultObj.hasMore === "boolean" ? resultObj.hasMore : params?.$top ? records.length === params.$top : false;
|
|
1775
2028
|
return {
|
|
1776
2029
|
data: records,
|
|
1777
2030
|
total,
|
|
1778
2031
|
// Calculate page number safely
|
|
1779
2032
|
page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
|
|
1780
2033
|
pageSize: params?.$top,
|
|
1781
|
-
hasMore
|
|
2034
|
+
hasMore
|
|
1782
2035
|
};
|
|
1783
2036
|
}
|
|
1784
2037
|
/**
|
|
@@ -1798,6 +2051,12 @@ var ObjectStackAdapter = class {
|
|
|
1798
2051
|
if (params.$skip !== void 0) {
|
|
1799
2052
|
queryParams.set("skip", String(params.$skip));
|
|
1800
2053
|
}
|
|
2054
|
+
if (params.$search != null && String(params.$search).trim() !== "") {
|
|
2055
|
+
queryParams.set("search", String(params.$search).trim());
|
|
2056
|
+
}
|
|
2057
|
+
if (params.$searchFields && params.$searchFields.length > 0) {
|
|
2058
|
+
queryParams.set("searchFields", params.$searchFields.join(","));
|
|
2059
|
+
}
|
|
1801
2060
|
if (params.$select && params.$select.length > 0) {
|
|
1802
2061
|
const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
|
|
1803
2062
|
queryParams.set("select", selectFields.join(","));
|
|
@@ -1898,6 +2157,12 @@ var ObjectStackAdapter = class {
|
|
|
1898
2157
|
if (params.$top !== void 0) {
|
|
1899
2158
|
options.top = params.$top;
|
|
1900
2159
|
}
|
|
2160
|
+
if (params.$search != null && String(params.$search).trim() !== "") {
|
|
2161
|
+
options.search = String(params.$search).trim();
|
|
2162
|
+
}
|
|
2163
|
+
if (params.$searchFields && params.$searchFields.length > 0) {
|
|
2164
|
+
options.searchFields = params.$searchFields;
|
|
2165
|
+
}
|
|
1901
2166
|
return options;
|
|
1902
2167
|
}
|
|
1903
2168
|
/**
|
|
@@ -1910,13 +2175,10 @@ var ObjectStackAdapter = class {
|
|
|
1910
2175
|
async getObjectSchema(objectName) {
|
|
1911
2176
|
await this.connect();
|
|
1912
2177
|
try {
|
|
1913
|
-
const schema = await this.metadataCache.get(
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
1918
|
-
return result;
|
|
1919
|
-
});
|
|
2178
|
+
const schema = await this.metadataCache.get(
|
|
2179
|
+
objectName,
|
|
2180
|
+
() => this.fetchObjectSchemaFresh(objectName)
|
|
2181
|
+
);
|
|
1920
2182
|
return schema;
|
|
1921
2183
|
} catch (error) {
|
|
1922
2184
|
const errorObj = error;
|
|
@@ -1929,6 +2191,48 @@ var ObjectStackAdapter = class {
|
|
|
1929
2191
|
throw createErrorFromResponse(errorObj, `getObjectSchema(${objectName})`);
|
|
1930
2192
|
}
|
|
1931
2193
|
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Fetch a single object's schema while always revalidating the browser cache.
|
|
2196
|
+
*
|
|
2197
|
+
* The server serves `GET /api/v1/meta/object/:name` with
|
|
2198
|
+
* `Cache-Control: public, max-age=3600`, so the default `fetch` the SDK uses
|
|
2199
|
+
* keeps returning the same response from the browser HTTP cache for up to an
|
|
2200
|
+
* hour without contacting the origin. Because the create/edit form reads the
|
|
2201
|
+
* object schema through {@link getObjectSchema}, a field added + published in
|
|
2202
|
+
* the same session never appears in the form even though it is live (the LIST
|
|
2203
|
+
* endpoint, `/meta/object`, is uncached — which is why list views update).
|
|
2204
|
+
*
|
|
2205
|
+
* Issuing the read with `cache: 'no-cache'` forces a conditional revalidation
|
|
2206
|
+
* (`If-None-Match`): a changed ETag returns the fresh schema, an unchanged one
|
|
2207
|
+
* still gets a cheap `304`. We go through `fetchImpl` (the adapter's
|
|
2208
|
+
* authenticated fetch) rather than `client.meta.getItem` because the SDK does
|
|
2209
|
+
* not expose the request cache mode.
|
|
2210
|
+
*/
|
|
2211
|
+
async fetchObjectSchemaFresh(objectName) {
|
|
2212
|
+
const baseUrl = (this.baseUrl || "").replace(/\/$/, "");
|
|
2213
|
+
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
|
|
2214
|
+
const metaPath = hasApiVersionSuffix ? "/meta" : "/api/v1/meta";
|
|
2215
|
+
const url = `${baseUrl}${metaPath}/object/${encodeURIComponent(objectName)}`;
|
|
2216
|
+
const headers = { "Content-Type": "application/json" };
|
|
2217
|
+
if (this.token) {
|
|
2218
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
2219
|
+
}
|
|
2220
|
+
const res = await this.fetchImpl(url, {
|
|
2221
|
+
method: "GET",
|
|
2222
|
+
headers,
|
|
2223
|
+
// Revalidate instead of serving the stale `max-age` body (see doc above).
|
|
2224
|
+
cache: "no-cache"
|
|
2225
|
+
});
|
|
2226
|
+
if (!res.ok) {
|
|
2227
|
+
const errBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
2228
|
+
const err = new Error(errBody?.error?.message || errBody?.message || res.statusText);
|
|
2229
|
+
err.status = res.status;
|
|
2230
|
+
throw err;
|
|
2231
|
+
}
|
|
2232
|
+
const body = await res.json();
|
|
2233
|
+
const data = body && typeof body === "object" && "success" in body && "data" in body ? body.data : body;
|
|
2234
|
+
return data && typeof data === "object" && "item" in data ? data.item : data;
|
|
2235
|
+
}
|
|
1932
2236
|
/**
|
|
1933
2237
|
* Get access to the underlying ObjectStack client for advanced operations.
|
|
1934
2238
|
*/
|
|
@@ -2004,7 +2308,7 @@ var ObjectStackAdapter = class {
|
|
|
2004
2308
|
try {
|
|
2005
2309
|
const cacheKey = `view:${objectName}:${viewId}`;
|
|
2006
2310
|
return await this.metadataCache.get(cacheKey, async () => {
|
|
2007
|
-
const result = await this.client.meta.getItem(
|
|
2311
|
+
const result = await this.client.meta.getItem("view", viewId);
|
|
2008
2312
|
if (result && result.item) return result.item;
|
|
2009
2313
|
return result ?? null;
|
|
2010
2314
|
});
|
|
@@ -2063,12 +2367,26 @@ var ObjectStackAdapter = class {
|
|
|
2063
2367
|
try {
|
|
2064
2368
|
const result = await this.client.meta.getItems("view");
|
|
2065
2369
|
const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
2370
|
+
const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
|
|
2066
2371
|
return items.filter((v) => {
|
|
2067
2372
|
if (!v) return false;
|
|
2068
2373
|
const spec = v.list ?? v;
|
|
2069
2374
|
const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
|
|
2070
|
-
|
|
2071
|
-
|
|
2375
|
+
if (obj !== objectName) return false;
|
|
2376
|
+
const viewKind = v.viewKind ?? spec?.viewKind;
|
|
2377
|
+
return !(viewKind && FORM_FAMILY.has(viewKind));
|
|
2378
|
+
}).map((v) => {
|
|
2379
|
+
const spec = v.list ?? v;
|
|
2380
|
+
if (spec && spec.config && typeof spec.config === "object") {
|
|
2381
|
+
return {
|
|
2382
|
+
...spec.config,
|
|
2383
|
+
name: spec.name ?? spec.config.name,
|
|
2384
|
+
label: spec.label ?? spec.config.label,
|
|
2385
|
+
isDefault: !!spec.isDefault
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
return spec;
|
|
2389
|
+
});
|
|
2072
2390
|
} catch (err) {
|
|
2073
2391
|
console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
|
|
2074
2392
|
return [];
|
|
@@ -2285,6 +2603,56 @@ var ObjectStackAdapter = class {
|
|
|
2285
2603
|
return this.aggregateClientSide(records, params);
|
|
2286
2604
|
}
|
|
2287
2605
|
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
|
|
2608
|
+
*
|
|
2609
|
+
* Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
|
|
2610
|
+
* `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
|
|
2611
|
+
* inline draft definition — the inline form is what the Studio dataset
|
|
2612
|
+
* editor sends to preview an unsaved draft. The adapter's bearer token is
|
|
2613
|
+
* forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
|
|
2614
|
+
*
|
|
2615
|
+
* Unlike {@link aggregate}, this does NOT fall back to client-side
|
|
2616
|
+
* aggregation: cross-object joins can only run on the server, so a failure
|
|
2617
|
+
* is surfaced to the caller (the preview panel shows the error) rather than
|
|
2618
|
+
* silently returning wrong numbers.
|
|
2619
|
+
*
|
|
2620
|
+
* @param dataset - An inline dataset definition (draft) OR a saved dataset name.
|
|
2621
|
+
* @param selection - Dimension/measure names to project + runtime directives.
|
|
2622
|
+
*/
|
|
2623
|
+
async queryDataset(dataset, selection) {
|
|
2624
|
+
await this.connect();
|
|
2625
|
+
const base = (this.baseUrl || "").replace(/\/$/, "");
|
|
2626
|
+
const url = `${base}/api/v1/analytics/dataset/query`;
|
|
2627
|
+
const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
|
|
2628
|
+
const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
|
|
2629
|
+
const res = await this.fetchImpl(url, {
|
|
2630
|
+
method: "POST",
|
|
2631
|
+
headers: {
|
|
2632
|
+
"Content-Type": "application/json",
|
|
2633
|
+
...this.token ? { Authorization: `Bearer ${this.token}` } : {}
|
|
2634
|
+
},
|
|
2635
|
+
body: JSON.stringify(requestBody)
|
|
2636
|
+
});
|
|
2637
|
+
if (!res.ok) {
|
|
2638
|
+
let detail = "";
|
|
2639
|
+
try {
|
|
2640
|
+
const errBody = await res.json();
|
|
2641
|
+
detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
2642
|
+
} catch {
|
|
2643
|
+
}
|
|
2644
|
+
throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
2645
|
+
}
|
|
2646
|
+
const payload = await res.json();
|
|
2647
|
+
const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
|
|
2648
|
+
const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
|
|
2649
|
+
const fields = Array.isArray(data?.fields) ? data.fields : [];
|
|
2650
|
+
const object = typeof data?.object === "string" ? data.object : void 0;
|
|
2651
|
+
const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
|
|
2652
|
+
const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
|
|
2653
|
+
const totals = Array.isArray(data?.totals) ? data.totals : void 0;
|
|
2654
|
+
return { rows, fields, object, dimensionFields, drillRawRows, totals };
|
|
2655
|
+
}
|
|
2288
2656
|
/** Client-side aggregation fallback */
|
|
2289
2657
|
aggregateClientSide(records, params) {
|
|
2290
2658
|
const { field, function: aggFn, groupBy } = params;
|