Source: src/js/models/dataONEServices/SysMetaService.js

define([
  "models/dataONEServices/DataONEService",
  "models/dataONEServices/DataONEHttpClient",
  "models/sysmeta/SystemMetadata",
  "common/UrlUtilities",
  "common/ValueUtilities",
], (
  DataONEService,
  DataONEHttpClient,
  SystemMetadata,
  UrlUtilities,
  ValueUtilities,
) => {
  /**
   * Service for fetching and caching DataONE system metadata.
   * @class SysMetaService
   * @since 2.37.0
   * @classcategory Models/DataONEServices
   * @augments DataONEService
   */
  class SysMetaService extends DataONEService {
    /**
     * @param {object} [options] Options for the SysMetaService. See
     * {@link DataONEService.optionsFromDescriptor} for the shared option shape.
     * @param {string} [options.readBaseUrl] Base URL for System Metadata reads
     * @param {string} [options.writeBaseUrl] Base URL for System Metadata
     * writes and reads after a write
     */
    constructor(options = {}) {
      const readBaseUrl = UrlUtilities.normalizeUrl(options.readBaseUrl);
      if (!readBaseUrl) {
        throw new Error("SysMetaService: readBaseUrl is required");
      }
      const serviceOptions = SysMetaService.optionsFromDescriptor({
        ...options,
        baseUrl: readBaseUrl,
      });
      super(serviceOptions);
      this.readBaseUrl = readBaseUrl;
      this.writeBaseUrl = UrlUtilities.normalizeUrl(options.writeBaseUrl);
      this.writeClientConfig = {
        ...serviceOptions.clientConfig,
        baseUrl: "",
      };
    }

    /**
     * Return the client for System Metadata writes and subsequent reads.
     * @param {string} operation Operation name for error reporting
     * @returns {DataONEHttpClient} Client for the write endpoint
     * @private
     * @since 2.39.0
     * @throws {Error} When writeBaseUrl is missing
     */
    getWriteClient(operation) {
      if (!this.writeBaseUrl) {
        throw new Error(
          `SysMetaService: writeBaseUrl is required for ${operation}`,
        );
      }
      return DataONEHttpClient.get({
        ...this.writeClientConfig,
        baseUrl: this.writeBaseUrl,
      });
    }

    /**
     * Fetch System Metadata for a PID. Returns the raw sysmeta response text.
     * @param {string} pid The PID of the object to fetch System Metadata for.
     * @param {object} [options] Options passed to
     * {@link DataONEService#download}
     * @returns {Promise<SystemMetadata>} Parsed System Metadata model.
     */
    async download(pid, options = {}) {
      const normalizedPid = this.constructor.normalizePid(
        pid,
        "pid",
        "SysMetaService.download requires a PID",
      );
      const { cacheKey } = options;
      const resolvedCacheKey = this.constructor.resolveCacheKey(
        normalizedPid,
        cacheKey,
      );

      const xmlString = await super.download(
        this.constructor.encodePidPath(normalizedPid),
        {
          ...options,
          cacheKey: resolvedCacheKey,
          encodePath: false,
        },
      );

      let sysMeta;
      try {
        sysMeta = SystemMetadata.fromXml(xmlString);
      } catch (error) {
        // Remove from cache if parsing fails
        await this.removeCached(resolvedCacheKey);
        error.message = `Failed to parse SystemMetadata XML for PID ${normalizedPid}: ${error.message}`;
        throw error;
      }

      return sysMeta;
    }

    /**
     * Fetch uncached System Metadata from the repository that accepts writes.
     * @param {string} pid PID of the object to fetch System Metadata for
     * @param {object} [options] Request options
     * @returns {Promise<SystemMetadata>} Parsed System Metadata model
     * @since 2.39.0
     * @throws {Error} When the request or XML parsing fails
     */
    async downloadFromWriteTarget(pid, options = {}) {
      const normalizedPid = this.constructor.normalizePid(pid, "pid");
      const response = await this.requestWithClient(
        this.getWriteClient("downloadFromWriteTarget"),
        this.constructor.buildRequestOptions({
          options,
          path: this.constructor.encodePidPath(normalizedPid),
          method: "GET",
        }),
      );

      try {
        return SystemMetadata.fromXml(response.data);
      } catch (error) {
        error.message = `Failed to parse SystemMetadata XML for PID ${normalizedPid}: ${error.message}`;
        throw error;
      }
    }

    /**
     * Remove a cached System Metadata record for a PID.
     * @param {string} pid PID to invalidate.
     * @returns {Promise<void>} Promise resolving when invalidation completes.
     */
    async invalidate(pid) {
      const normalizedPid = ValueUtilities.normalizeText(pid);
      if (!normalizedPid) return;
      await this.removeCached(normalizedPid);
    }

    /**
     * Upload System Metadata XML to the service.
     * @param {string} sysMetaXml System Metadata XML string.
     * @param {object} [options] Options passed to {@link DataONEService#upload}.
     * @returns {Promise<DataONEHttpResponse>} Promise resolving to the upload response.
     */
    async upload(sysMetaXml, options = {}) {
      const normalizedSysMetaXml = ValueUtilities.requireNonEmptyString(
        sysMetaXml,
        "SysMetaService.upload requires sysMetaXml",
      );
      // TODO: accept pid?
      return this.requestWithClient(
        this.getWriteClient("upload"),
        this.constructor.buildRequestOptions({
          options: {
            ...options,
            headers: {
              ...(options.headers || {}),
              "Content-Type": "application/xml",
            },
          },
          path: "",
          method: "POST",
          body: normalizedSysMetaXml,
        }),
      );
    }

    /**
     * Update System Metadata XML for an existing object.
     * @param {string} pid PID to update system metadata for.
     * @param {string} sysMetaXml System Metadata XML string.
     * @param {object} [options] Options passed to {@link DataONEService#upload}.
     * @returns {Promise<DataONEHttpResponse>} Promise resolving to the update response.
     * @throws {Error} When required input is missing or the request fails
     * @since 2.39.0
     */
    async update(pid, sysMetaXml, options = {}) {
      const normalizedPid = this.constructor.normalizePid(
        pid,
        "pid",
        "SysMetaService.update requires a PID",
      );
      const normalizedSysMetaXml = ValueUtilities.requireNonEmptyString(
        sysMetaXml,
        "SysMetaService.update requires sysMetaXml",
      );

      const formData = new FormData();
      formData.append("pid", normalizedPid);
      const xmlBlob = new Blob([normalizedSysMetaXml], {
        type: "application/xml",
      });
      formData.append("sysmeta", xmlBlob, "sysmeta.xml");

      return this.requestWithClient(
        this.getWriteClient("update"),
        this.constructor.buildRequestOptions({
          options,
          path: this.constructor.encodePidPath(normalizedPid),
          method: "PUT",
          dedupe: false,
          responseType: "text",
          body: formData,
          extra: { transport: "xhr", useCache: false },
        }),
      );
    }
  }

  /** @type {DataONEService#DataONEServiceConfig} */
  SysMetaService.config = {
    endpoint: "sysmeta",
    client: {
      timeoutMs: 2 * 60 * 1000, // 2 minutes
      methods: ["GET", "POST", "PUT"],
      responseTypes: ["text"],
      dedupeHeaders: ["Authorization"],
    },
    storage: {
      ttlMs: 60 * 60 * 1000, // 1 hour
      schemaVersion: 1,
    },
    persistPrivate: true,
    defaultAuth: true,
  };
  SysMetaService.SystemMetadata = SystemMetadata;

  return SysMetaService;
});