@dx-do/client 6.2.2 → 6.3.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/dist/index.cjs.js CHANGED
@@ -45995,6 +45995,226 @@ exports.OIAlarm = void 0;
45995
45995
  };
45996
45996
  })(exports.OIAlarm || (exports.OIAlarm = {}));
45997
45997
 
45998
+ /**
45999
+ * The `service export` / `service export-hierarchy` file format and the
46000
+ * `service import` boundary validator.
46001
+ *
46002
+ * The envelope wraps a re-postable store body ({@link CreateServices.CreateServiceBody})
46003
+ * with provenance metadata. `import` accepts either the envelope or a bare body.
46004
+ */
46005
+ exports.ServiceExport = void 0;
46006
+ (function (ServiceExport) {
46007
+ ServiceExport.SCHEMA_VERSION = 1;
46008
+ // Structural guard for a store body — does not re-validate every field
46009
+ // (the server is the authority); just enough to reject obviously-wrong files.
46010
+ const BodyShape = v4.z.looseObject({
46011
+ vertices: v4.z.array(v4.z.looseObject({ attributes: v4.z.looseObject({ name: v4.z.string() }) })),
46012
+ edges: v4.z.array(v4.z.looseObject({
46013
+ sourceExternalId: v4.z.string(),
46014
+ targetExternalId: v4.z.string(),
46015
+ })),
46016
+ });
46017
+ const EnvelopeShape = v4.z.looseObject({
46018
+ schemaVersion: v4.z.literal(1),
46019
+ kind: v4.z.enum(['service', 'service-hierarchy']),
46020
+ sourceTenant: v4.z.string().optional(),
46021
+ exportedAt: v4.z.string(),
46022
+ body: BodyShape,
46023
+ });
46024
+ /** Wrap a store body in an export envelope. */
46025
+ function envelope(kind, body, sourceTenant, exportedAt) {
46026
+ return { schemaVersion: ServiceExport.SCHEMA_VERSION, kind, sourceTenant, exportedAt, body };
46027
+ }
46028
+ ServiceExport.envelope = envelope;
46029
+ /**
46030
+ * Validate an import file (already JSON-parsed) and return the store body.
46031
+ * Accepts a full export envelope or a bare `{ vertices, edges }` body.
46032
+ * @throws Error when the input is neither shape.
46033
+ */
46034
+ function parseImport(raw) {
46035
+ const asEnvelope = EnvelopeShape.safeParse(raw);
46036
+ if (asEnvelope.success) {
46037
+ return asEnvelope.data.body;
46038
+ }
46039
+ const asBody = BodyShape.safeParse(raw);
46040
+ if (asBody.success) {
46041
+ return raw;
46042
+ }
46043
+ throw new Error('Import file is neither a service export envelope nor a bare {vertices, edges} body. ' +
46044
+ `Envelope error: ${asEnvelope.error.message}`);
46045
+ }
46046
+ ServiceExport.parseImport = parseImport;
46047
+ })(exports.ServiceExport || (exports.ServiceExport = {}));
46048
+
46049
+ /**
46050
+ * Pure (HTTP-free) transforms between the Service read shapes and the store
46051
+ * (`/oi/v2/sa/save`) body. Kept separate from {@link ServiceService} so the
46052
+ * edge/weight logic — the part with real correctness risk — is unit-testable.
46053
+ *
46054
+ * Empirical constraints these helpers encode (see plan Phase 0):
46055
+ * - A parent's direct-child health/risk weights must each total ≤ 1.0 (server rejects otherwise).
46056
+ * - `replace=true` makes each saved vertex's outgoing-edge set authoritative, so a
46057
+ * faithful re-save must carry the whole subtree's vertices + edges.
46058
+ */
46059
+ exports.ServiceTransform = void 0;
46060
+ (function (ServiceTransform) {
46061
+ /**
46062
+ * Convert a retrieved (read) service vertex into a re-postable store vertex.
46063
+ *
46064
+ * @remarks externalId is keyed by the service **name**, not the server
46065
+ * `SA:<uuid>` id. The store API treats `externalId` as a payload-local handle
46066
+ * and resolves/creates services by `attributes.name`; keying edges by `SA:uuid`
46067
+ * makes the server silently drop them (so a `replace` save wipes the existing
46068
+ * edges and re-adds nothing). Names are tenant-unique, so this is unambiguous
46069
+ * and also makes exports portable across tenants.
46070
+ */
46071
+ function retrievedToServiceVertex(v) {
46072
+ const a = v.attributes;
46073
+ return {
46074
+ externalId: a.name,
46075
+ attributes: {
46076
+ type: 'saService',
46077
+ name: a.name,
46078
+ state: 'ACTIVE',
46079
+ maintenance: a.maintenance ?? false,
46080
+ situationsIncludeChildServices: a.situationsIncludeChildServices ?? false,
46081
+ serviceContent: a.serviceContent ?? [],
46082
+ root_service: null,
46083
+ tags: a.tags ?? [],
46084
+ location: a.location ?? '',
46085
+ description: a.description ?? '',
46086
+ customProperties: a.customProperties ?? [],
46087
+ customMetrics: a.customMetrics ?? [],
46088
+ metrics: a.metrics ?? [],
46089
+ },
46090
+ };
46091
+ }
46092
+ ServiceTransform.retrievedToServiceVertex = retrievedToServiceVertex;
46093
+ /**
46094
+ * Build NAME-keyed store edges from a topology graph, preserving weights +
46095
+ * semantic. Endpoints are mapped from `SA:<uuid>` to service name via the
46096
+ * graph's vertices; edges whose endpoints aren't in `topo.vertices` are
46097
+ * dropped — this is exactly the incoming edge(s) from the root's parent, which
46098
+ * `saTopologyGraph` returns without the parent vertex (and which must not be
46099
+ * re-posted: the parent isn't re-saved, so its edge survives anyway).
46100
+ */
46101
+ function edgesFromTopology(topo) {
46102
+ const idToName = new Map(topo.vertices.map((v) => [v.externalId, v.attributes.name]));
46103
+ return topo.edges
46104
+ .map((e) => ({
46105
+ s: idToName.get(e.sourceExternalId),
46106
+ t: idToName.get(e.targetExternalId),
46107
+ e,
46108
+ }))
46109
+ .filter((x) => x.s !== undefined && x.t !== undefined)
46110
+ .map((x) => ({
46111
+ sourceExternalId: x.s,
46112
+ targetExternalId: x.t,
46113
+ attributes: {
46114
+ health_weight: x.e.attributes?.health_weight ?? 1,
46115
+ risk_weight: x.e.attributes?.risk_weight ?? 1,
46116
+ semantic: 'AggregateOf',
46117
+ },
46118
+ }));
46119
+ }
46120
+ ServiceTransform.edgesFromTopology = edgesFromTopology;
46121
+ /** Merge two store bodies, de-duplicating vertices by externalId and edges by endpoints. */
46122
+ function mergeBodies(a, b) {
46123
+ const vertices = [];
46124
+ const seenV = new Set();
46125
+ for (const v of [...a.vertices, ...b.vertices]) {
46126
+ const key = v.externalId ?? v.attributes.name;
46127
+ if (!seenV.has(key)) {
46128
+ seenV.add(key);
46129
+ vertices.push(v);
46130
+ }
46131
+ }
46132
+ const edges = [];
46133
+ const seenE = new Set();
46134
+ for (const e of [...a.edges, ...b.edges]) {
46135
+ const key = `${e.sourceExternalId}|${e.targetExternalId}`;
46136
+ if (!seenE.has(key)) {
46137
+ seenE.add(key);
46138
+ edges.push(e);
46139
+ }
46140
+ }
46141
+ return { vertices, edges };
46142
+ }
46143
+ ServiceTransform.mergeBodies = mergeBodies;
46144
+ /** Insert or update the parent→child edge with the given weights. */
46145
+ function upsertEdge(edges, source, target, healthWeight, riskWeight) {
46146
+ const next = edges.filter((e) => !(e.sourceExternalId === source && e.targetExternalId === target));
46147
+ next.push({
46148
+ sourceExternalId: source,
46149
+ targetExternalId: target,
46150
+ attributes: {
46151
+ health_weight: healthWeight,
46152
+ risk_weight: riskWeight,
46153
+ semantic: 'AggregateOf',
46154
+ },
46155
+ });
46156
+ return next;
46157
+ }
46158
+ ServiceTransform.upsertEdge = upsertEdge;
46159
+ /** Remove the parent→child edge. */
46160
+ function removeEdge(edges, source, target) {
46161
+ return edges.filter((e) => !(e.sourceExternalId === source && e.targetExternalId === target));
46162
+ }
46163
+ ServiceTransform.removeEdge = removeEdge;
46164
+ /** Edges whose source is the given parent (its direct-child associations). */
46165
+ function childEdges(edges, parent) {
46166
+ return edges.filter((e) => e.sourceExternalId === parent);
46167
+ }
46168
+ ServiceTransform.childEdges = childEdges;
46169
+ /**
46170
+ * Keep only edges whose BOTH endpoints are in the given vertex-id set.
46171
+ *
46172
+ * `saTopologyGraph(root)` returns the root's subtree plus the *incoming* edge(s)
46173
+ * from the root's parent(s) — but not the parent vertices. Re-posting such a
46174
+ * dangling edge is rejected ("vertex referenced by edge is not present in the
46175
+ * input graph"). Dropping it is also correct: the parent is not re-saved, so its
46176
+ * edge to the root is preserved server-side (replace is destructive only per
46177
+ * saved vertex).
46178
+ */
46179
+ function filterEdgesToVertices(edges, vertexIds) {
46180
+ return edges.filter((e) => vertexIds.has(e.sourceExternalId) && vertexIds.has(e.targetExternalId));
46181
+ }
46182
+ ServiceTransform.filterEdgesToVertices = filterEdgesToVertices;
46183
+ /**
46184
+ * Evenly distribute a parent's direct-child weights so each total is ≤ 1.0.
46185
+ * Uses floored thirds (e.g. 3 children → 0.333 each, sum 0.999) to avoid
46186
+ * float drift pushing the total over 100%.
46187
+ */
46188
+ function rebalanceChildren(edges, parent) {
46189
+ const kids = childEdges(edges, parent);
46190
+ if (kids.length === 0)
46191
+ return edges;
46192
+ const w = Math.floor((1 / kids.length) * 1000) / 1000;
46193
+ return edges.map((e) => e.sourceExternalId === parent
46194
+ ? {
46195
+ ...e,
46196
+ attributes: {
46197
+ health_weight: w,
46198
+ risk_weight: w,
46199
+ semantic: 'AggregateOf',
46200
+ },
46201
+ }
46202
+ : e);
46203
+ }
46204
+ ServiceTransform.rebalanceChildren = rebalanceChildren;
46205
+ /** Throw if a parent's direct-child weights exceed 100% (the server's hard limit). */
46206
+ function assertWeightsWithinLimit(edges, parent) {
46207
+ const kids = childEdges(edges, parent);
46208
+ const health = kids.reduce((s, e) => s + (e.attributes?.health_weight ?? 0), 0);
46209
+ const risk = kids.reduce((s, e) => s + (e.attributes?.risk_weight ?? 0), 0);
46210
+ const eps = 1e-6;
46211
+ if (health > 1 + eps || risk > 1 + eps) {
46212
+ throw new Error(`Weight total for parent exceeds 100% (health=${health.toFixed(3)}, risk=${risk.toFixed(3)}). Lower the weights or omit them to auto-distribute evenly.`);
46213
+ }
46214
+ }
46215
+ ServiceTransform.assertWeightsWithinLimit = assertWeightsWithinLimit;
46216
+ })(exports.ServiceTransform || (exports.ServiceTransform = {}));
46217
+
45998
46218
  exports.OIService = void 0;
45999
46219
  (function (OIService) {
46000
46220
  /** @internal */
@@ -73721,6 +73941,18 @@ function detectIPv4() {
73721
73941
  }
73722
73942
  return '127.0.0.1';
73723
73943
  }
73944
+ /**
73945
+ * Coalesce a missing *or empty* value to a fallback.
73946
+ *
73947
+ * `??` only catches `null`/`undefined`, but an explicit `""` is a valid string
73948
+ * that would flow straight through. That breaks `temp_fields`: a blank component
73949
+ * collapses one of its space-separated slots, shifting every later field's
73950
+ * position when the SaaS Kafka consumer parses `agentbase_msg`. Treat `""`
73951
+ * exactly like an omitted value.
73952
+ */
73953
+ function orDefault(value, fallback) {
73954
+ return value ? value : fallback;
73955
+ }
73724
73956
  /**
73725
73957
  * Service for querying and ingesting log analytics data.
73726
73958
  */
@@ -73749,14 +73981,17 @@ class LogsService {
73749
73981
  exports.LogIngest.LogIngestOptionsSchema.parse(options);
73750
73982
  const cohortId = this.dxSaasService.dxdoConfiguration.dxConfiguration.cohortId;
73751
73983
  const tenantIdUpper = cohortId.toUpperCase();
73752
- const logtype = options.logtype ?? 'generic';
73753
- const host = options.host ?? require$$0$4.hostname();
73754
- const ip = options.ip ?? detectIPv4();
73755
- const agentInstance = options.agentInstance ?? 'dx-do';
73756
- const agentName = options.agentName ?? 'Logs Collector';
73757
- const containerId = options.containerId ?? host;
73758
- const containerName = options.containerName ?? agentInstance;
73759
- const file = options.file ?? 'dx-do.cli';
73984
+ // `orDefault` (not `??`) so an explicit empty string is treated as omitted —
73985
+ // a blank value in any temp_fields component shifts every downstream
73986
+ // positional field. See the temp_fields comment below.
73987
+ const logtype = orDefault(options.logtype, 'generic');
73988
+ const host = orDefault(options.host, require$$0$4.hostname());
73989
+ const ip = orDefault(options.ip, detectIPv4());
73990
+ const agentInstance = orDefault(options.agentInstance, 'dx-do');
73991
+ const agentName = orDefault(options.agentName, 'Logs Collector');
73992
+ const containerId = orDefault(options.containerId, host);
73993
+ const containerName = orDefault(options.containerName, agentInstance);
73994
+ const file = orDefault(options.file, 'dx-do.cli');
73760
73995
  const tag = `${logtype} logs`;
73761
73996
  // temp_fields must have exactly 9 space-separated components before the message
73762
73997
  // is appended by the SaaS filter (agentbase_msg = temp_fields + " " + message).
@@ -75427,19 +75662,90 @@ class ServiceService {
75427
75662
  };
75428
75663
  return this.dxSaaSService.oiPost('oi/v2/sa/serviceDetails', serviceDetailRequest);
75429
75664
  }
75665
+ /**
75666
+ * Retrieve a Service — the full read-endpoint envelope (vertices + edges +
75667
+ * paging metadata). Backs the `service detail` and `service export` commands.
75668
+ *
75669
+ * @param serviceName - Service name (URL path segment).
75670
+ * @param subservices - When true, include the service's subservices in the response.
75671
+ */
75672
+ async getService(serviceName, subservices = false) {
75673
+ return this.dxSaaSService.oiGet(`oi/v2/sa/services/${serviceName}`, { subservices });
75674
+ }
75675
+ /**
75676
+ * Returns the full reachable subtree (all descendants + association edges with
75677
+ * weights) for a service via `oi/v2/sa/saTopologyGraph`. Backs the hierarchy
75678
+ * read used by `export-hierarchy`/`associate`/`dissociate`.
75679
+ *
75680
+ * @remarks The documented `oi/v2/sa/service/details/hierarchy` path is not
75681
+ * routable on the tenant gateway; this topology endpoint is the working one.
75682
+ * Its vertex attributes are a subset — use {@link getService} per vertex when
75683
+ * faithful re-save attributes (tags, customProperties, metrics) are needed.
75684
+ */
75685
+ async getServiceTopology(serviceName) {
75686
+ return this.dxSaaSService.oiPost('oi/v2/sa/saTopologyGraph', { serviceName, startTime: Date.now(), endTime: Date.now() });
75687
+ }
75688
+ /**
75689
+ * Builds a faithful, re-postable store body for a service's entire subtree:
75690
+ * topology for structure + a per-vertex {@link getService} for full attributes
75691
+ * (serviceContent, tags, customProperties, metrics). Shared by
75692
+ * `export-hierarchy`, `associate`, and `dissociate`.
75693
+ *
75694
+ * @remarks One GET per vertex — fine for typical (small) hierarchies; reads
75695
+ * are not throttled like saves.
75696
+ */
75697
+ async buildHierarchyBody(rootName) {
75698
+ const topo = await this.getServiceTopology(rootName);
75699
+ const names = new Set([rootName]);
75700
+ topo.vertices.forEach((v) => names.add(v.attributes.name));
75701
+ const vertices = [];
75702
+ const seen = new Set();
75703
+ for (const name of names) {
75704
+ const full = await this.getService(name, false);
75705
+ const fv = full.vertices[0];
75706
+ if (fv && !seen.has(fv.externalId)) {
75707
+ seen.add(fv.externalId);
75708
+ vertices.push(exports.ServiceTransform.retrievedToServiceVertex(fv));
75709
+ }
75710
+ }
75711
+ // Drop edges that reference vertices outside this subtree (e.g. the incoming
75712
+ // edge from the root's parent, which saTopologyGraph returns without the
75713
+ // parent vertex). Re-posting such a dangling edge is rejected by the server.
75714
+ const presentIds = new Set(vertices
75715
+ .map((v) => v.externalId)
75716
+ .filter((id) => id !== undefined));
75717
+ const edges = exports.ServiceTransform.filterEdgesToVertices(exports.ServiceTransform.edgesFromTopology(topo), presentIds);
75718
+ return { vertices, edges };
75719
+ }
75720
+ /** Builds a single-service store body (no edges) for `service export`. */
75721
+ async exportServiceBody(serviceName) {
75722
+ const r = await this.getService(serviceName, false);
75723
+ if (!r.vertices.length) {
75724
+ throw new Error(`No service with name '${serviceName}' found`);
75725
+ }
75726
+ return {
75727
+ vertices: [exports.ServiceTransform.retrievedToServiceVertex(r.vertices[0])],
75728
+ edges: [],
75729
+ };
75730
+ }
75731
+ /**
75732
+ * Resolves a service's server externalId (`SA:<cohort>:<uuid>`).
75733
+ * @throws Error if the service does not exist.
75734
+ */
75735
+ async getServiceExternalId(serviceName) {
75736
+ const r = await this.getService(serviceName, false);
75737
+ const id = r.vertices[0]?.externalId;
75738
+ if (!id)
75739
+ throw new Error(`No service with name '${serviceName}' found`);
75740
+ return id;
75741
+ }
75430
75742
  /** Returns the overview vertex for a named service; throws if not found. */
75431
75743
  async getServiceOverview(serviceName) {
75432
- return this.dxSaaSService
75433
- .oiGet(`oi/v2/sa/services/${serviceName}`, {
75434
- subservices: false,
75435
- })
75436
- .then((graph) => {
75437
- if (graph.vertices.length == 1) {
75438
- return graph.vertices[0];
75439
- }
75440
- else
75441
- throw new Error(`No Service with name '${serviceName}' Found`);
75442
- });
75744
+ const response = await this.getService(serviceName, false);
75745
+ if (response.vertices.length === 1) {
75746
+ return response.vertices[0];
75747
+ }
75748
+ throw new Error(`No Service with name '${serviceName}' Found`);
75443
75749
  }
75444
75750
  /** Returns paginated top-level (grouped) services up to `maxRequests` pages. */
75445
75751
  async getTopLevelServices(pageSize = 100, maxRequests = 20) {
@@ -75529,12 +75835,109 @@ class ServiceService {
75529
75835
  sirb.customFilter.and.expressions[0].or.expressions[0].value = serviceName;
75530
75836
  return sirb;
75531
75837
  }
75532
- /** Creates a new service from the given body; replaces any existing service with the same name. */
75533
- async createService(createServiceBody) {
75534
- return this.dxSaaSService.tenantPost('/oi/v2/sa/save', createServiceBody, {
75838
+ /**
75839
+ * Creates/updates one or more services via the store API (`/oi/v2/sa/save`).
75840
+ *
75841
+ * @remarks Edges not present in the payload are removed for the services it
75842
+ * covers — the store API treats the supplied edge set as authoritative.
75843
+ * @param createServiceBody - vertices + edges to store.
75844
+ * @param opts.replace - When true (default), replaces an existing service of the
75845
+ * same name rather than erroring.
75846
+ * @param opts.autoWeight - When true, the server computes edge health/risk weights,
75847
+ * overriding any weights supplied in the payload. Omitted by default.
75848
+ */
75849
+ async createService(createServiceBody, opts = {}) {
75850
+ const params = { replace: opts.replace ?? true };
75851
+ if (opts.autoWeight !== undefined) {
75852
+ params['auto_weight'] = opts.autoWeight;
75853
+ }
75854
+ return this.dxSaaSService.tenantPost('/oi/v2/sa/save', createServiceBody, params);
75855
+ }
75856
+ /**
75857
+ * Builds the store body for associating `childName` under `parentName` as a
75858
+ * new `AggregateOf` child, WITHOUT saving (so callers can dry-run it).
75859
+ *
75860
+ * Because `replace=true` makes each saved vertex's edge set authoritative, the
75861
+ * body carries both services' full subtrees. Weights: explicit
75862
+ * `healthWeight`/`riskWeight` are applied to the new edge and validated against
75863
+ * the ≤100% limit; otherwise the parent's direct children are evenly rebalanced.
75864
+ */
75865
+ async buildAssociateBody(parentName, childName, opts = {}) {
75866
+ if (parentName === childName) {
75867
+ throw new Error(`A service cannot be associated with itself ('${parentName}')`);
75868
+ }
75869
+ // Validate both services exist (throws "No service with name … found").
75870
+ await this.getServiceExternalId(parentName);
75871
+ await this.getServiceExternalId(childName);
75872
+ // Bodies are keyed by service name (see ServiceTransform.retrievedToServiceVertex).
75873
+ const body = exports.ServiceTransform.mergeBodies(await this.buildHierarchyBody(parentName), await this.buildHierarchyBody(childName));
75874
+ const explicit = opts.healthWeight !== undefined || opts.riskWeight !== undefined;
75875
+ body.edges = exports.ServiceTransform.upsertEdge(body.edges, parentName, childName, opts.healthWeight ?? 1, opts.riskWeight ?? 1);
75876
+ if (explicit) {
75877
+ exports.ServiceTransform.assertWeightsWithinLimit(body.edges, parentName);
75878
+ }
75879
+ else {
75880
+ body.edges = exports.ServiceTransform.rebalanceChildren(body.edges, parentName);
75881
+ }
75882
+ return body;
75883
+ }
75884
+ /** Associates `childName` under `parentName` and saves it. See {@link buildAssociateBody}. */
75885
+ async associateServices(parentName, childName, opts = {}) {
75886
+ const body = await this.buildAssociateBody(parentName, childName, opts);
75887
+ return this.createService(body, {
75535
75888
  replace: true,
75889
+ autoWeight: opts.autoWeight,
75536
75890
  });
75537
75891
  }
75892
+ /**
75893
+ * Builds the store body for removing the `parentName`→`childName` association,
75894
+ * WITHOUT saving. The child (and its own subtree) is preserved as a now-detached
75895
+ * service.
75896
+ * @throws Error if `childName` is not a direct child of `parentName`.
75897
+ */
75898
+ async buildDissociateBody(parentName, childName) {
75899
+ // Validate both services exist (throws "No service with name … found").
75900
+ await this.getServiceExternalId(parentName);
75901
+ await this.getServiceExternalId(childName);
75902
+ const body = await this.buildHierarchyBody(parentName);
75903
+ const before = body.edges.length;
75904
+ body.edges = exports.ServiceTransform.removeEdge(body.edges, parentName, childName);
75905
+ if (body.edges.length === before) {
75906
+ throw new Error(`'${childName}' is not a direct child of '${parentName}' — nothing to dissociate`);
75907
+ }
75908
+ return body;
75909
+ }
75910
+ /** Removes the `parentName`→`childName` association and saves it. See {@link buildDissociateBody}. */
75911
+ async dissociateServices(parentName, childName) {
75912
+ const body = await this.buildDissociateBody(parentName, childName);
75913
+ return this.createService(body, { replace: true });
75914
+ }
75915
+ /**
75916
+ * Builds the store body for updating a service's content queries, WITHOUT
75917
+ * saving. Re-saves the service's full subtree (via {@link buildHierarchyBody})
75918
+ * so its children edges are preserved — only the target vertex's
75919
+ * `serviceContent` changes.
75920
+ *
75921
+ * @param mode - 'set' replaces the content; 'add' appends to the existing content.
75922
+ * @returns the store body plus the service's previous content (for diffing).
75923
+ * @throws Error if the service does not exist.
75924
+ */
75925
+ async buildServiceContentBody(serviceName, newContent, mode) {
75926
+ const body = await this.buildHierarchyBody(serviceName);
75927
+ const target = body.vertices.find((v) => v.attributes.name === serviceName);
75928
+ if (!target) {
75929
+ throw new Error(`No service with name '${serviceName}' found`);
75930
+ }
75931
+ const previousContent = target.attributes.serviceContent;
75932
+ target.attributes.serviceContent =
75933
+ mode === 'set' ? newContent : [...previousContent, ...newContent];
75934
+ return { body, previousContent };
75935
+ }
75936
+ /** Updates a service's content queries and saves it. See {@link buildServiceContentBody}. */
75937
+ async updateServiceContent(serviceName, newContent, mode) {
75938
+ const { body } = await this.buildServiceContentBody(serviceName, newContent, mode);
75939
+ return this.createService(body, { replace: true });
75940
+ }
75538
75941
  /** Deletes a service by name; optionally deletes child services recursively. */
75539
75942
  async deleteService(serviceName, recursive = false) {
75540
75943
  return this.dxSaaSService.tenantPost('/oi/v2/sa/service/status', { name: serviceName, action: 'delete' }, { recursive });