UNPKG

next-drupal

Version:
2,668 lines 84.8 kB
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/index.ts
var src_exports = {};
__export(src_exports, {
  DRAFT_DATA_COOKIE_NAME: () => DRAFT_DATA_COOKIE_NAME,
  DRAFT_MODE_COOKIE_NAME: () => DRAFT_MODE_COOKIE_NAME,
  DrupalClient: () => DrupalClient,
  DrupalPreview: () => DrupalPreview,
  JsonApiErrors: () => JsonApiErrors,
  NextDrupal: () => NextDrupal,
  NextDrupalBase: () => NextDrupalBase,
  NextDrupalPages: () => NextDrupalPages,
  PreviewHandler: () => PreviewHandler,
  buildUrl: () => buildUrl,
  deserialize: () => deserialize,
  getAccessToken: () => getAccessToken,
  getJsonApiIndex: () => getJsonApiIndex,
  getJsonApiPathForResourceType: () => getJsonApiPathForResourceType,
  getMenu: () => getMenu,
  getPathsFromContext: () => getPathsFromContext,
  getResource: () => getResource,
  getResourceByPath: () => getResourceByPath,
  getResourceCollection: () => getResourceCollection,
  getResourceCollectionFromContext: () => getResourceCollectionFromContext,
  getResourceFromContext: () => getResourceFromContext,
  getResourcePreviewUrl: () => getResourcePreviewUrl,
  getResourceTypeFromContext: () => getResourceTypeFromContext,
  getSearchIndex: () => getSearchIndex,
  getSearchIndexFromContext: () => getSearchIndexFromContext,
  getView: () => getView,
  isAccessTokenAuth: () => isAccessTokenAuth,
  isBasicAuth: () => isBasicAuth,
  isClientIdSecretAuth: () => isClientIdSecretAuth,
  syncDrupalPreviewRoutes: () => syncDrupalPreviewRoutes,
  translatePath: () => translatePath,
  translatePathFromContext: () => translatePathFromContext,
  useJsonaDeserialize: () => useJsonaDeserialize
});
module.exports = __toCommonJS(src_exports);

// src/draft-constants.ts
var DRAFT_DATA_COOKIE_NAME = "next_drupal_draft_data";
var DRAFT_MODE_COOKIE_NAME = "__prerender_bypass";

// src/jsonapi-errors.ts
var JsonApiErrors = class _JsonApiErrors extends Error {
  constructor(errors, statusCode, messagePrefix = "") {
    super();
    this.errors = errors;
    this.statusCode = statusCode;
    this.message = (messagePrefix ? `${messagePrefix} ` : "") + _JsonApiErrors.formatMessage(errors);
  }
  static formatMessage(errors) {
    if (typeof errors === "string") {
      return errors;
    }
    const [error] = errors;
    let message = `${error.status} ${error.title}`;
    if (error.detail) {
      message += `
${error.detail}`;
    }
    return message;
  }
};

// src/next-drupal-base.ts
var import_qs = require("qs");

// src/logger.ts
var LOG_MESSAGE_PREFIX = "[next-drupal][log]:";
var DEBUG_MESSAGE_PREFIX = "[next-drupal][debug]:";
var WARN_MESSAGE_PREFIX = "[next-drupal][warn]:";
var ERROR_MESSAGE_PREFIX = "[next-drupal][error]:";
var logger = {
  log(message) {
    console.log(LOG_MESSAGE_PREFIX, message);
  },
  debug(message) {
    console.debug(DEBUG_MESSAGE_PREFIX, message);
  },
  warn(message) {
    console.warn(WARN_MESSAGE_PREFIX, message);
  },
  error(message) {
    console.error(ERROR_MESSAGE_PREFIX, message);
  }
};

// src/next-drupal-base.ts
var DEFAULT_API_PREFIX = "";
var DEFAULT_FRONT_PAGE = "/home";
var DEFAULT_WITH_AUTH = false;
var DEFAULT_AUTH_URL = "/oauth/token";
var DEFAULT_HEADERS = {
  "Content-Type": "application/json",
  Accept: "application/json"
};
var NextDrupalBase = class {
  /**
   * Instantiates a new NextDrupalBase.
   *
   * const client = new NextDrupalBase(baseUrl)
   *
   * @param {baseUrl} baseUrl The baseUrl of your Drupal site. Do not add the /jsonapi suffix.
   * @param {options} options Options for NextDrupalBase.
   */
  constructor(baseUrl, options = {}) {
    if (!baseUrl || typeof baseUrl !== "string") {
      throw new Error("The 'baseUrl' param is required.");
    }
    const {
      accessToken,
      apiPrefix = DEFAULT_API_PREFIX,
      auth,
      debug = false,
      fetcher,
      frontPage = DEFAULT_FRONT_PAGE,
      headers = DEFAULT_HEADERS,
      logger: logger2 = logger,
      withAuth = DEFAULT_WITH_AUTH
    } = options;
    this.accessToken = accessToken;
    this.apiPrefix = apiPrefix;
    this.auth = auth;
    this.baseUrl = baseUrl;
    this.fetcher = fetcher;
    this.frontPage = frontPage;
    this.isDebugEnabled = !!debug;
    this.headers = headers;
    this.logger = logger2;
    this.withAuth = withAuth;
    this.debug("Debug mode is on.");
  }
  set apiPrefix(apiPrefix) {
    this._apiPrefix = apiPrefix === "" || apiPrefix.startsWith("/") ? apiPrefix : `/${apiPrefix}`;
  }
  get apiPrefix() {
    return this._apiPrefix;
  }
  set auth(auth) {
    if (typeof auth === "object") {
      const checkUsernamePassword = auth;
      const checkAccessToken = auth;
      const checkClientIdSecret = auth;
      if (checkUsernamePassword.username !== void 0 || checkUsernamePassword.password !== void 0) {
        if (!checkUsernamePassword.username || !checkUsernamePassword.password) {
          throw new Error(
            "'username' and 'password' are required for auth. See https://next-drupal.org/docs/client/auth"
          );
        }
      } else if (checkAccessToken.access_token !== void 0 || checkAccessToken.token_type !== void 0) {
        if (!checkAccessToken.access_token || !checkAccessToken.token_type) {
          throw new Error(
            "'access_token' and 'token_type' are required for auth. See https://next-drupal.org/docs/client/auth"
          );
        }
      } else if (!checkClientIdSecret.clientId || !checkClientIdSecret.clientSecret) {
        throw new Error(
          "'clientId' and 'clientSecret' are required for auth. See https://next-drupal.org/docs/client/auth"
        );
      }
      this._auth = {
        ...isClientIdSecretAuth(auth) ? { url: DEFAULT_AUTH_URL } : {},
        ...auth
      };
    } else {
      this._auth = auth;
    }
  }
  get auth() {
    return this._auth;
  }
  set headers(headers) {
    this._headers = new Headers(headers);
  }
  get headers() {
    return this._headers;
  }
  set token(token) {
    this._token = token;
    this._tokenExpiresOn = Date.now() + token.expires_in * 1e3;
  }
  get token() {
    return this._token;
  }
  /**
   * Fetches a resource from the given input URL or path.
   *
   * @param {RequestInfo} input The url to fetch from.
   * @param {FetchOptions} init The fetch options with `withAuth`.
   *   If `withAuth` is set, `fetch` will fetch an `Authorization` header before making the request.
   * @returns {Promise<Response>} The fetch response.
   * @remarks
   * To provide your own custom fetcher, see the fetcher docs.
   * @example
   * ```ts
   * const url = drupal.buildUrl("/jsonapi/node/article", {
   *   sort: "-created",
   *   "fields[node--article]": "title,path",
   * })
   *
   * const response = await drupal.fetch(url.toString())
   * ```
   */
  async fetch(input, { withAuth, ...init } = {}) {
    init.credentials = "include";
    const headers = new Headers(this.headers);
    if (init?.headers) {
      const initHeaders = new Headers(init?.headers);
      for (const key of initHeaders.keys()) {
        headers.set(key, initHeaders.get(key));
      }
    }
    if (withAuth) {
      headers.set(
        "Authorization",
        await this.getAuthorizationHeader(
          withAuth === true ? this.auth : withAuth
        )
      );
    }
    init.headers = headers;
    if (typeof input === "string" && input.startsWith("/")) {
      input = `${this.baseUrl}${input}`;
    }
    if (this.fetcher) {
      this.debug(`Using custom fetcher, fetching: ${input}`);
      return await this.fetcher(input, init);
    }
    this.debug(`Using default fetch, fetching: ${input}`);
    return await fetch(input, init);
  }
  /**
   * Gets the authorization header value based on the provided auth configuration.
   *
   * @param {NextDrupalAuth} auth The auth configuration.
   * @returns {Promise<string>} The authorization header value.
   */
  async getAuthorizationHeader(auth) {
    let header;
    if (isBasicAuth(auth)) {
      const basic = Buffer.from(`${auth.username}:${auth.password}`).toString(
        "base64"
      );
      header = `Basic ${basic}`;
      this.debug("Using basic authorization header.");
    } else if (isClientIdSecretAuth(auth)) {
      const token = await this.getAccessToken(auth);
      header = `Bearer ${token.access_token}`;
      this.debug(
        "Using access token authorization header retrieved from Client Id/Secret."
      );
    } else if (isAccessTokenAuth(auth)) {
      header = `${auth.token_type} ${auth.access_token}`;
      this.debug("Using access token authorization header.");
    } else if (typeof auth === "string") {
      header = auth;
      this.debug("Using custom authorization header.");
    } else if (typeof auth === "function") {
      header = auth();
      this.debug("Using custom authorization callback.");
    } else {
      throw new Error(
        "auth is not configured. See https://next-drupal.org/docs/client/auth"
      );
    }
    return header;
  }
  /**
   * Builds a URL with the given path and search parameters.
   *
   * @param {string} path The path for the url. Example: "/example"
   * @param {string | Record<string, string> | URLSearchParams | JsonApiParams} searchParams Optional query parameters.
   * @returns {URL} The constructed URL.
   * @example
   * ```ts
   * const drupal = new DrupalClient("https://example.com")
   *
   * // https://drupal.org
   * drupal.buildUrl("https://drupal.org").toString()
   *
   * // https://example.com/foo
   * drupal.buildUrl("/foo").toString()
   *
   * // https://example.com/foo?bar=baz
   * client.buildUrl("/foo", { bar: "baz" }).toString()
   * ```
   *
   * Build a URL from `DrupalJsonApiParams`
   * ```ts
   * const params = {
   *   getQueryObject: () => ({
   *     sort: "-created",
   *     "fields[node--article]": "title,path",
   *   }),
   * }
   *
   * // https://example.com/jsonapi/node/article?sort=-created&fields%5Bnode--article%5D=title%2Cpath
   * drupal.buildUrl("/jsonapi/node/article", params).toString()
   * ```
   */
  buildUrl(path, searchParams) {
    const url = new URL(path, this.baseUrl);
    const search = (
      // Handle DrupalJsonApiParams objects.
      searchParams && typeof searchParams === "object" && "getQueryObject" in searchParams ? searchParams.getQueryObject() : searchParams
    );
    if (search) {
      url.search = (0, import_qs.stringify)(search);
    }
    return url;
  }
  /**
   * Builds an endpoint URL with the given options.
   *
   * @param {Object} options The options for building the endpoint.
   * @param {string} options.locale The locale.
   * @param {string} options.path The path.
   * @param {EndpointSearchParams} options.searchParams The search parameters.
   * @returns {Promise<string>} The constructed endpoint URL.
   */
  async buildEndpoint({
    locale = "",
    path = "",
    searchParams
  } = {}) {
    const localeSegment = locale ? `/${locale}` : "";
    if (path && !path.startsWith("/")) {
      path = `/${path}`;
    }
    return this.buildUrl(
      `${localeSegment}${this.apiPrefix}${path}`,
      searchParams
    ).toString();
  }
  /**
   * Constructs a path from the given segment and options.
   *
   * @param {string | string[]} segment The path segment.
   * @param {Object} options The options for constructing the path.
   * @param {Locale} options.locale The locale.
   * @param {Locale} options.defaultLocale The default locale.
   * @param {PathPrefix} options.pathPrefix The path prefix.
   * @returns {string} The constructed path.
   */
  constructPathFromSegment(segment, options = {}) {
    let { pathPrefix = "" } = options;
    const { locale, defaultLocale } = options;
    if (pathPrefix) {
      if (!pathPrefix?.startsWith("/")) {
        pathPrefix = `/${options.pathPrefix}`;
      }
      if (pathPrefix.endsWith("/")) {
        pathPrefix = pathPrefix.slice(0, -1);
      }
    }
    if (!Array.isArray(segment)) {
      segment = segment ? [segment] : [];
    }
    segment = segment.map((part) => encodeURIComponent(part)).join("/");
    if (!segment && !pathPrefix) {
      segment = this.frontPage;
    }
    if (segment && !segment.startsWith("/")) {
      segment = `/${segment}`;
    }
    if (segment.endsWith("/")) {
      segment = segment.slice(0, -1);
    }
    return this.addLocalePrefix(`${pathPrefix}${segment}`, {
      locale,
      defaultLocale
    });
  }
  /**
   * Adds a locale prefix to the given path.
   *
   * @param {string} path The path.
   * @param {Object} options The options for adding the locale prefix.
   * @param {Locale} options.locale The locale.
   * @param {Locale} options.defaultLocale The default locale.
   * @returns {string} The path with the locale prefix.
   */
  addLocalePrefix(path, options = {}) {
    const { locale, defaultLocale } = options;
    if (!path.startsWith("/")) {
      path = `/${path}`;
    }
    let localePrefix = "";
    if (locale && !path.startsWith(`/${locale}`) && locale !== defaultLocale) {
      localePrefix = `/${locale}`;
    }
    return `${localePrefix}${path}`;
  }
  /**
   * Retrieve an access token.
   *
   * @param {NextDrupalAuthClientIdSecret} clientIdSecret The client ID and secret.
   * @returns {Promise<AccessToken>} The access token.
   * @remarks
   * If options is not provided, `DrupalClient` will use the `clientId` and `clientSecret` configured in `auth`.
   * @example
   * ```ts
   * const accessToken = await drupal.getAccessToken({
   *   clientId: "7034f4db-7151-466f-a711-8384bddb9e60",
   *   clientSecret: "d92Fm^ds",
   * })
   * ```
   */
  async getAccessToken(clientIdSecret) {
    if (this.accessToken) {
      return this.accessToken;
    }
    let auth;
    if (isClientIdSecretAuth(clientIdSecret)) {
      auth = {
        url: DEFAULT_AUTH_URL,
        ...clientIdSecret
      };
    } else if (isClientIdSecretAuth(this.auth)) {
      auth = { ...this.auth };
    } else if (typeof this.auth === "undefined") {
      throw new Error(
        "auth is not configured. See https://next-drupal.org/docs/client/auth"
      );
    } else {
      throw new Error(
        `'clientId' and 'clientSecret' required. See https://next-drupal.org/docs/client/auth`
      );
    }
    const url = this.buildUrl(auth.url);
    if (this.token && Date.now() < this._tokenExpiresOn && this._tokenRequestDetails?.clientId === auth?.clientId && this._tokenRequestDetails?.clientSecret === auth?.clientSecret && this._tokenRequestDetails?.scope === auth?.scope) {
      this.debug(`Using existing access token.`);
      return this.token;
    }
    this.debug(`Fetching new access token.`);
    const clientCredentials = {
      username: auth.clientId,
      password: auth.clientSecret
    };
    const body = new URLSearchParams({ grant_type: "client_credentials" });
    if (auth?.scope) {
      body.set("scope", auth.scope);
      this.debug(`Using scope: ${auth.scope}`);
    }
    const response = await this.fetch(url.toString(), {
      method: "POST",
      headers: {
        Authorization: await this.getAuthorizationHeader(clientCredentials),
        Accept: "application/json",
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body
    });
    await this.throwIfJsonErrors(
      response,
      "Error while fetching new access token: "
    );
    const result = await response.json();
    this.token = result;
    this._tokenRequestDetails = auth;
    return result;
  }
  /**
   * Validates the draft URL using the provided search parameters.
   *
   * @param {URLSearchParams} searchParams The search parameters.
   * @returns {Promise<Response>} The validation response.
   */
  async validateDraftUrl(searchParams) {
    const path = searchParams.get("path");
    this.debug(`Fetching draft url validation for ${path}.`);
    let response;
    try {
      const validateUrl = this.buildUrl("/next/draft-url").toString();
      response = await this.fetch(validateUrl, {
        method: "POST",
        headers: {
          Accept: "application/vnd.api+json",
          "Content-Type": "application/json"
        },
        body: JSON.stringify(Object.fromEntries(searchParams.entries()))
      });
    } catch (error) {
      response = new Response(JSON.stringify({ message: error.message }), {
        status: 401
      });
    }
    this.debug(
      response.status !== 200 ? `Could not validate path, ${path}` : `Validated path, ${path}`
    );
    return response;
  }
  /**
   * Logs a debug message if debug mode is enabled.
   *
   * @param {string} message The debug message.
   */
  debug(message) {
    this.isDebugEnabled && this.logger.debug(message);
  }
  /**
   * Throws an error if the response contains JSON:API errors.
   *
   * @param {Response} response The fetch response.
   * @param {string} messagePrefix The error message prefix.
   * @throws {JsonApiErrors} The JSON:API errors.
   */
  async throwIfJsonErrors(response, messagePrefix = "") {
    if (!response?.ok) {
      const errors = await this.getErrorsFromResponse(response);
      throw new JsonApiErrors(errors, response.status, messagePrefix);
    }
  }
  /**
   * Extracts errors from the fetch response.
   *
   * @param {Response} response The fetch response.
   * @returns {Promise<string | JsonApiResponse>} The extracted errors.
   */
  async getErrorsFromResponse(response) {
    const type = response.headers.get("content-type");
    let error;
    if (type === "application/json") {
      error = await response.json();
      if (error?.message) {
        return error.message;
      }
    } else if (type === "application/vnd.api+json") {
      error = await response.json();
      if (error?.errors?.length) {
        return error.errors;
      }
    }
    return response.statusText;
  }
};
function isBasicAuth(auth) {
  return auth?.username !== void 0 && auth?.password !== void 0;
}
function isAccessTokenAuth(auth) {
  return auth?.access_token !== void 0 && auth?.token_type !== void 0;
}
function isClientIdSecretAuth(auth) {
  return auth?.clientId !== void 0 && auth?.clientSecret !== void 0;
}

// src/next-drupal.ts
var import_jsona = require("jsona");
var import_qs2 = require("qs");

// src/menu-tree.ts
var DrupalMenuTree = class _DrupalMenuTree extends Array {
  constructor(menuItems, parentId = "", depth = 1) {
    super();
    this.parentId = parentId;
    this.depth = depth;
    if (menuItems?.length) {
      this.build(menuItems, parentId);
    }
  }
  build(menuItems, parentId) {
    const children = menuItems.filter(
      (menuItem) => menuItem?.parent === parentId
    );
    for (const menuItem of children) {
      const subtree = new _DrupalMenuTree(
        menuItems,
        menuItem.id,
        this.depth + 1
      );
      this.push({
        ...menuItem,
        items: subtree.length ? subtree : void 0
      });
    }
  }
};

// src/next-drupal.ts
var DEFAULT_API_PREFIX2 = "/jsonapi";
var DEFAULT_HEADERS2 = {
  "Content-Type": "application/vnd.api+json",
  Accept: "application/vnd.api+json"
};
function useJsonaDeserialize() {
  const jsonFormatter = new import_jsona.Jsona();
  return function jsonaDeserialize(body, options) {
    return jsonFormatter.deserialize(body, options);
  };
}
var NextDrupal = class extends NextDrupalBase {
  /**
   * Instantiates a new NextDrupal.
   *
   * const client = new NextDrupal(baseUrl)
   *
   * @param {baseUrl} baseUrl The baseUrl of your Drupal site. Do not add the /jsonapi suffix.
   * @param {options} options Options for NextDrupal.
   */
  constructor(baseUrl, options = {}) {
    super(baseUrl, options);
    const {
      apiPrefix = DEFAULT_API_PREFIX2,
      cache: cache2 = null,
      deserializer,
      headers = DEFAULT_HEADERS2,
      throwJsonApiErrors = true,
      useDefaultEndpoints = true
    } = options;
    this.apiPrefix = apiPrefix;
    this.cache = cache2;
    this.deserializer = deserializer ?? useJsonaDeserialize();
    this.headers = headers;
    this.throwJsonApiErrors = !!throwJsonApiErrors;
    this.useDefaultEndpoints = !!useDefaultEndpoints;
    if (process.env.NODE_ENV === "production") {
      this.throwJsonApiErrors = false;
    }
  }
  /**
   * Creates a new resource of the specified type.
   *
   * @param {string} type The type of the resource. Example: `node--article`, `taxonomy_term--tags`, or `block_content--basic`.
   * @param {JsonApiCreateResourceBody} body The body payload with data.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<T>} The created resource.
   * @example
   * Create a node--page resource
   * ```
   * const page = await drupal.createResource("node--page", {
   *   data: {
   *     attributes: {
   *       title: "Page Title",
   *       body: {
   *         value: "<p>Content of body field</p>",
   *         format: "full_html",
   *       },
   *     },
   *   },
   * })
   * ```
   * Create a node--article with a taxonomy term
   * ```
   * const article = await drupal.createResource("node--article", {
   *   data: {
   *     attributes: {
   *       title: "Title of Article",
   *       body: {
   *         value: "<p>Content of body field</p>",
   *         format: "full_html",
   *       },
   *     },
   *     relationships: {
   *       field_category: {
   *         data: {
   *           type: "taxonomy_term--category",
   *           id: "28ab9f26-927d-4e33-9510-b59a7ccdafe6",
   *         },
   *       },
   *     },
   *   },
   * })
   * ```
   * Using filters
   * ```
   * const page = await drupal.createResource(
   *   "node--page",
   *   {
   *     data: {
   *       attributes: {
   *         title: "Page Title",
   *         body: {
   *           value: "<p>Content of body field</p>",
   *           format: "full_html",
   *         },
   *       },
   *     },
   *   },
   *   {
   *     params: {
   *       "fields[node--page]": "title,path",
   *     },
   *   }
   * )
   * ```
   * Using TypeScript with DrupalNode
   * ```
   * import { DrupalNode } from "next-drupal"
   * const page = await drupal.createResource<DrupalNode>("node--page", {
   *   data: {
   *     attributes: {
   *       title: "Page Title",
   *       body: {
   *         value: "<p>Content of body field</p>",
   *         format: "full_html",
   *       },
   *     },
   *   },
   * })
   * ```
   */
  async createResource(type, body, options) {
    options = {
      deserialize: true,
      withAuth: true,
      ...options
    };
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? (
        /* c8 ignore next */
        options.locale
      ) : void 0,
      resourceType: type,
      searchParams: options?.params
    });
    this.debug(`Creating resource of type ${type}.`);
    body.data.type = type;
    const response = await this.fetch(endpoint, {
      method: "POST",
      body: JSON.stringify(body),
      withAuth: options.withAuth,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while creating resource: ");
    const json = await response.json();
    return options.deserialize ? this.deserialize(json) : (
      /* c8 ignore next */
      json
    );
  }
  /**
   * Creates a new file resource for the specified media type.
   *
   * @param {string} type The type of the resource. In most cases this is `file--file`.
   * @param {JsonApiCreateFileResourceBody} body The body payload with data.
   *   - type: The resource type of the host entity. Example: `media--image`
   *   - field: The name of the file field on the host entity. Example: `field_media_image`
   *   - filename: The name of the file with extension. Example: `avatar.jpg`
   *   - file: The file as a Buffer
   * @param {JsonApiOptions} options Options for the request.
   * @returns {Promise<T>} The created file resource.
   * @example
   * Create a file resource for a media--image entity
   * ```ts
   * const file = await drupal.createFileResource("file--file", {
   *   data: {
   *     attributes: {
   *       type: "media--image", // The type of the parent resource
   *       field: "field_media_image", // The name of the field on the parent resource
   *       filename: "filename.jpg",
   *       file: await fs.readFile("/path/to/file.jpg"),
   *     },
   *   },
   * })
   * ```
   *
   * You can then use this to create a new media--image with a relationship to the file:
   * ```ts
   * const media = await drupal.createResource<DrupalMedia>("media--image", {
   *   data: {
   *     attributes: {
   *       name: "Name for the media",
   *     },
   *     relationships: {
   *       field_media_image: {
   *         data: {
   *           type: "file--file",
   *           id: file.id,
   *         },
   *       },
   *     },
   *   },
   * })
   * ```
   */
  async createFileResource(type, body, options) {
    options = {
      deserialize: true,
      withAuth: true,
      ...options
    };
    const resourceType = body?.data?.attributes?.type;
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      resourceType,
      path: `/${body.data.attributes.field}`,
      searchParams: options?.params
    });
    this.debug(`Creating file resource for media of type ${type}.`);
    const response = await this.fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/octet-stream",
        Accept: "application/vnd.api+json",
        "Content-Disposition": `file; filename="${body.data.attributes.filename}"`
      },
      body: body.data.attributes.file,
      withAuth: options.withAuth,
      cache: options.cache
    });
    await this.throwIfJsonErrors(
      response,
      "Error while creating file resource: "
    );
    const json = await response.json();
    return options.deserialize ? this.deserialize(json) : json;
  }
  /**
   * Updates an existing resource of the specified type.
   *
   * @param {string} type The type of the resource. Example: `node--article`, `taxonomy_term--tags`, or `block_content--basic`.
   * @param {string} uuid The resource id. Example: `a50ffee7-ba94-46c9-9705-f9f8f440db94`.
   * @param {JsonApiUpdateResourceBody} body The body payload with data.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<T>} The updated resource.
   * @example
   * Update a node--page resource
   * ```ts
   * const page = await drupal.updateResource(
   *   "node--page",
   *   "a50ffee7-ba94-46c9-9705-f9f8f440db94",
   *   {
   *     data: {
   *       attributes: {
   *         title: "Updated Title",
   *       },
   *     },
   *   }
   * )
   * ```
   *
   * Using TypeScript with DrupalNode for a node entity type
   * ```ts
   * import { DrupalNode } from "next-drupal"
   *
   * const page = await drupal.updateResource<DrupalNode>(
   *   "node--page",
   *   "a50ffee7-ba94-46c9-9705-f9f8f440db94",
   *   {
   *     data: {
   *       attributes: {
   *         title: "Updated Title",
   *       },
   *     },
   *   }
   * )
   * ```
   */
  async updateResource(type, uuid, body, options) {
    options = {
      deserialize: true,
      withAuth: true,
      ...options
    };
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? (
        /* c8 ignore next */
        options.locale
      ) : void 0,
      resourceType: type,
      path: `/${uuid}`,
      searchParams: options?.params
    });
    this.debug(`Updating resource of type ${type} with id ${uuid}.`);
    body.data.type = type;
    body.data.id = uuid;
    const response = await this.fetch(endpoint, {
      method: "PATCH",
      body: JSON.stringify(body),
      withAuth: options.withAuth,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while updating resource: ");
    const json = await response.json();
    return options.deserialize ? this.deserialize(json) : (
      /* c8 ignore next */
      json
    );
  }
  /**
   * Deletes an existing resource of the specified type.
   *
   * @param {string} type The type of the resource. Example: `node--article`, `taxonomy_term--tags`, or `block_content--basic`.
   * @param {string} uuid The resource id. Example: `a50ffee7-ba94-46c9-9705-f9f8f440db94`.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<boolean>} True if the resource was deleted, false otherwise.
   * @example
   * Delete a node--page resource
   * ```ts
   * const isDeleted = await drupal.deleteResource(
   *   "node--page",
   *   "a50ffee7-ba94-46c9-9705-f9f8f440db94"
   * )
   * ```
   */
  async deleteResource(type, uuid, options) {
    options = {
      withAuth: true,
      params: {},
      ...options
    };
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? (
        /* c8 ignore next */
        options.locale
      ) : void 0,
      resourceType: type,
      path: `/${uuid}`,
      searchParams: options?.params
    });
    this.debug(`Deleting resource of type ${type} with id ${uuid}.`);
    const response = await this.fetch(endpoint, {
      method: "DELETE",
      withAuth: options.withAuth,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while deleting resource: ");
    return response.status === 204;
  }
  /**
   * Fetches a resource of the specified type by its UUID.
   *
   * @param {string} type The resource type. Example: `node--article`, `taxonomy_term--tags`, or `block_content--basic`.
   * @param {string} uuid The id of the resource. Example: `15486935-24bf-4be7-b858-a5b2de78d09d`.
   * @param {JsonApiOptions & JsonApiWithCacheOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<T>} The fetched resource.
   * @examples
   * Get a page by uuid.
   * ```ts
   * const node = await drupal.getResource(
   *  "node--page",
   *  "07464e9f-9221-4a4f-b7f2-01389408e6c8"
   * )
   * ```
   * Get the es translation for a page by uuid.
   * ```ts
   * const node = await drupal.getResource(
   *   "node--page",
   *   "07464e9f-9221-4a4f-b7f2-01389408e6c8",
   *   {
   *     locale: "es",
   *     defaultLocale: "en",
   *   }
   * )
   * ```
   * Get the raw JSON:API response.
   * ```ts
   * const { data, meta, links } = await drupal.getResource(
   *   "node--page",
   *   "07464e9f-9221-4a4f-b7f2-01389408e6c8",
   *   {
   *     deserialize: false,
   *   }
   * )
   * ```
   * Get a node--article resource using cache.
   * ```ts
   * const id = "07464e9f-9221-4a4f-b7f2-01389408e6c8"
   *
   * const article = await drupal.getResource("node--article", id, {
   *   withCache: true,
   *   cacheKey: `node--article:${id}`,
   * })
   * ```
   * Get a page resource with time-based revalidation.
   * ```ts
   * const node = await drupal.getResource(
   *   "node--page",
   *   "07464e9f-9221-4a4f-b7f2-01389408e6c8",
   *   { next: { revalidate: 3600 } }
   * )
   * ```
   * Get a page resource with tag-based revalidation.
   * ```ts
   * const {slug} = params;
   * const path = drupal.translatePath(slug)
   *
   * const type = path.jsonapi.resourceName
   * const tag = `${path.entity.type}:${path.entity.id}`
   *
   * const node = await drupal.getResource(path, path.entity.uuid, {
   *   params: params.getQueryObject(),
   *   tags: [tag]
   * })
   * ```
   * Using DrupalNode for a node entity type.
   * ```ts
   * import { DrupalNode } from "next-drupal"
   *
   * const node = await drupal.getResource<DrupalNode>(
   *   "node--page",
   *   "07464e9f-9221-4a4f-b7f2-01389408e6c8"
   * )
   * ```
   * Using DrupalTaxonomyTerm for a taxonomy term entity type.
   * ```ts
   * import { DrupalTaxonomyTerm } from "next-drupal"
   *
   * const term = await drupal.getResource<DrupalTaxonomyTerm>(
   *   "taxonomy_term--tags",
   *   "7b47d7cc-9b1b-4867-a909-75dc1d61dfd3"
   * )
   * ```
   */
  async getResource(type, uuid, options) {
    options = {
      deserialize: true,
      withAuth: this.withAuth,
      withCache: false,
      params: {},
      ...options
    };
    if (options.withCache) {
      const cached = await this.cache.get(options.cacheKey);
      if (cached) {
        this.debug(`Returning cached resource ${type} with id ${uuid}.`);
        const json2 = JSON.parse(cached);
        return options.deserialize ? this.deserialize(json2) : json2;
      }
    }
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      resourceType: type,
      path: `/${uuid}`,
      searchParams: options?.params
    });
    this.debug(`Fetching resource ${type} with id ${uuid}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while fetching resource: ");
    const json = await response.json();
    if (options.withCache) {
      await this.cache.set(options.cacheKey, JSON.stringify(json));
    }
    return options.deserialize ? this.deserialize(json) : json;
  }
  /**
   * Fetches a resource of the specified type by its path.
   *
   * @param {string} path The path of the resource. Example: `/blog/slug-for-article`.
   * @param { { isVersionable?: boolean } & JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   *   - isVersionable: Set to true if you're fetching the revision for a resource. Automatically set to true for node entity types
   * @returns {Promise<T>} The fetched resource.
   * @requires Decoupled Router module
   * @example
   * Get a page by path
   * ```
   * const node = await drupal.getResourceByPath("/blog/slug-for-article")
   * ```
   * Get the raw JSON:API response
   * ```
   * const { data, meta, links } = await drupal.getResourceByPath(
   *   "/blog/slug-for-article",
   *   {
   *     deserialize: false,
   *   }
   * )
   *```
   * Using DrupalNode for a node entity type
   * ```
   * import { DrupalNode } from "next-drupal"
   * const node = await drupal.getResourceByPath<DrupalNode>(
   *   "/blog/slug-for-article"
   * )
   * ```
   */
  async getResourceByPath(path, options) {
    options = {
      deserialize: true,
      isVersionable: false,
      withAuth: this.withAuth,
      params: {},
      ...options
    };
    if (!path) {
      return null;
    }
    path = this.addLocalePrefix(path, {
      locale: options.locale,
      defaultLocale: options.defaultLocale
    });
    if (options.params.resourceVersion) {
      options.isVersionable = true;
    }
    const { resourceVersion = "rel:latest-version", ...params } = options.params;
    if (options.isVersionable) {
      params.resourceVersion = resourceVersion;
    }
    const resourceParams = (0, import_qs2.stringify)(params);
    const payload = [
      {
        requestId: "router",
        action: "view",
        uri: `/router/translate-path?path=${path}&_format=json`,
        headers: { Accept: "application/vnd.api+json" }
      },
      {
        requestId: "resolvedResource",
        action: "view",
        uri: `{{router.body@$.jsonapi.individual}}?${resourceParams.toString()}`,
        waitFor: ["router"]
      }
    ];
    const subrequestsEndpoint = this.addLocalePrefix("/subrequests", {
      locale: options.locale,
      defaultLocale: options.defaultLocale
    });
    const endpoint = this.buildUrl(subrequestsEndpoint, {
      _format: "json"
    }).toString();
    this.debug(`Fetching resource by path, ${path}.`);
    const response = await this.fetch(endpoint, {
      method: "POST",
      credentials: "include",
      redirect: "follow",
      body: JSON.stringify(payload),
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    const errorMessagePrefix = "Error while fetching resource by path:";
    if (response.status !== 207) {
      const errors = await this.getErrorsFromResponse(response);
      throw new JsonApiErrors(errors, response.status, errorMessagePrefix);
    }
    const json = await response.json();
    if (!json?.["resolvedResource#uri{0}"]?.body) {
      const status = json?.router?.headers?.status?.[0];
      if (status === 404) {
        return null;
      }
      const message = json?.router?.body && JSON.parse(json.router.body)?.message || "Unknown error";
      throw new JsonApiErrors(message, status, errorMessagePrefix);
    }
    const data = JSON.parse(json["resolvedResource#uri{0}"]?.body);
    if (data.errors) {
      const status = json?.["resolvedResource#uri{0}"]?.headers?.status?.[0];
      this.logOrThrowError(
        new JsonApiErrors(data.errors, status, errorMessagePrefix)
      );
    }
    return options.deserialize ? this.deserialize(data) : data;
  }
  /**
   * Fetches a collection of resources of the specified type.
   *
   * @param {string} type The type of the resources. Example: `node--article` or `user--user`.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   *   - deserialize: Set to false to return the raw JSON:API response
   * @returns {Promise<T>} The fetched collection of resources.
   * @example
   * Get all articles
   * ```
   * const articles = await drupal.getResourceCollection("node--article")
   * ```
   * Using filters
   * ```
   * const publishedArticles = await drupal.getResourceCollection("node--article", {
   *   params: {
   *     "filter[status]": "1",
   *   },
   * })
   * ```
   * Get the raw JSON:API response
   * ```
   * const { data, meta, links } = await drupal.getResourceCollection("node--page", {
   *   deserialize: false,
   * })
   * ```
   * Using TypeScript with DrupalNode for a node entity type
   * ```
   * import { DrupalNode } from "next-drupal"
   * const nodes = await drupal.getResourceCollection<DrupalNode[]>("node--article")
   * ```
   */
  async getResourceCollection(type, options) {
    options = {
      withAuth: this.withAuth,
      deserialize: true,
      ...options
    };
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      resourceType: type,
      searchParams: options?.params
    });
    this.debug(`Fetching resource collection of type ${type}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    await this.throwIfJsonErrors(
      response,
      "Error while fetching resource collection: "
    );
    const json = await response.json();
    return options.deserialize ? this.deserialize(json) : json;
  }
  /**
   * Fetches path segments for a collection of resources of the specified types.
   *
   * @param {string | string[]} types The types of the resources.
   * @param {JsonApiOptions & JsonApiWithAuthOption & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<{ path: string, type: string, locale: Locale, segments: string[] }[]>} The fetched path segments.
   */
  async getResourceCollectionPathSegments(types, options) {
    options = {
      withAuth: this.withAuth,
      pathPrefix: "",
      params: {},
      ...options
    };
    if (typeof types === "string") {
      types = [types];
    }
    const paths = await Promise.all(
      types.map(async (type) => {
        const params = {
          [`fields[${type}]`]: "path",
          ...options?.params
        };
        const locales = options?.locales?.length ? options.locales : [void 0];
        return Promise.all(
          locales.map(async (locale) => {
            let opts = {
              params,
              withAuth: options.withAuth,
              next: options.next,
              cache: options.cache
            };
            if (locale) {
              opts = {
                ...opts,
                deserialize: true,
                locale,
                defaultLocale: options.defaultLocale
              };
            }
            const resources = await this.getResourceCollection(type, opts);
            return resources.map((resource) => {
              return resource?.path?.alias === this.frontPage ? (
                /* c8 ignore next */
                "/"
              ) : resource?.path?.alias;
            }).filter(Boolean).map((path) => {
              let segmentPath = path;
              if (options.pathPrefix && (segmentPath.startsWith(
                `${options.pathPrefix}/`
              ) || segmentPath === options.pathPrefix)) {
                segmentPath = segmentPath.slice(options.pathPrefix.length);
              }
              const segments = segmentPath.split("/").filter(Boolean);
              return {
                path,
                type,
                locale,
                segments
              };
            });
          })
        );
      })
    );
    return paths.flat(2);
  }
  /**
   * Translates a path to a DrupalTranslatedPath object.
   *
   * @param {string} path The resource path. Example: `/blog/slug-for-article`.
   * @param {JsonApiWithAuthOption & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<DrupalTranslatedPath | null>} The translated path.
   * @requires Decoupled Router module
   * @example
   * Get info about a `/blog/slug-for-article` path
   * ```ts
   * const path = await drupal.translatePath("/blog/slug-for-article")
   * ```
   */
  async translatePath(path, options) {
    options = {
      withAuth: this.withAuth,
      ...options
    };
    const endpoint = this.buildUrl("/router/translate-path", {
      path
    }).toString();
    this.debug(`Fetching translated path, ${path}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    if (response.status === 404) {
      return null;
    }
    await this.throwIfJsonErrors(response);
    return await response.json();
  }
  /**
   * Fetches the JSON:API index.
   *
   * @param {Locale} locale The locale for the request.
   * @param {JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<JsonApiResponse>} The JSON:API index.
   */
  async getIndex(locale, options) {
    const endpoint = await this.buildEndpoint({
      locale
    });
    this.debug(`Fetching JSON:API index.`);
    const response = await this.fetch(endpoint, {
      // As per https://www.drupal.org/node/2984034 /jsonapi is public.
      withAuth: false,
      next: options?.next,
      cache: options?.cache
    });
    await this.throwIfJsonErrors(
      response,
      `Failed to fetch JSON:API index at ${endpoint}: `
    );
    return await response.json();
  }
  /**
   * Builds an endpoint URL for the specified parameters.
   *
   * @param {Parameters<NextDrupalBase["buildEndpoint"]>[0] & { resourceType?: string }} params The parameters for the endpoint.
   * @returns {Promise<string>} The built endpoint URL.
   */
  async buildEndpoint({
    locale = "",
    resourceType = "",
    path = "",
    searchParams
  } = {}) {
    let localeSegment = locale ? `/${locale}` : "";
    let apiSegment = this.apiPrefix;
    let resourceSegment = "";
    if (resourceType) {
      if (this.useDefaultEndpoints) {
        const [id, bundle] = resourceType.split("--");
        resourceSegment = `/${id}` + (bundle ? `/${bundle}` : "");
      } else {
        resourceSegment = (await this.fetchResourceEndpoint(resourceType, locale)).pathname;
        localeSegment = "";
        apiSegment = "";
      }
    }
    if (path && !path.startsWith("/")) {
      path = `/${path}`;
    }
    return this.buildUrl(
      `${localeSegment}${apiSegment}${resourceSegment}${path}`,
      searchParams
    ).toString();
  }
  /**
   * Fetches the endpoint URL for the specified resource type.
   *
   * @param {string} type The type of the resource.
   * @param {Locale} locale The locale for the request.
   * @returns {Promise<URL>} The fetched endpoint URL.
   */
  async fetchResourceEndpoint(type, locale) {
    const index = await this.getIndex(locale);
    const link = index.links?.[type];
    if (!link) {
      throw new Error(`Resource of type '${type}' not found.`);
    }
    const url = new URL(link.href);
    if (locale && !url.pathname.startsWith(`/${locale}`)) {
      url.pathname = `/${locale}${url.pathname}`;
    }
    return url;
  }
  /**
   * Fetches a menu by its name.
   *
   * @param {string} menuName The name of the menu. Example: `main` or `footer`.
   * @param {JsonApiOptions & JsonApiWithCacheOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<{ items: T[], tree: T[] }>} The fetched menu.
   *   - items: An array of `DrupalMenuLinkContent`
   *   - tree: An array of `DrupalMenuLinkContent` with children nested to match the hierarchy from Drupal
   * @requires JSON:API Menu Items module
   * @example
   * Get the `main` menu
   * ```ts
   * const { menu, items } = await drupal.getMenu("main")
   * ```
   *
   * Get the `main` menu using cache
   * ```ts
   * const menu = await drupal.getMenu("main", {
   *   withCache: true,
   *   cacheKey: "menu--main",
   * })
   * ```
   */
  async getMenu(menuName, options) {
    options = {
      withAuth: this.withAuth,
      deserialize: true,
      params: {},
      withCache: false,
      ...options
    };
    if (options.withCache) {
      const cached = await this.cache.get(options.cacheKey);
      if (cached) {
        this.debug(`Returning cached menu items for ${menuName}.`);
        return JSON.parse(cached);
      }
    }
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      resourceType: "menu_items",
      path: menuName,
      searchParams: options.params
    });
    this.debug(`Fetching menu items for ${menuName}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while fetching menu items: ");
    const data = await response.json();
    const items = options.deserialize ? this.deserialize(data) : (
      /* c8 ignore next */
      data
    );
    const tree = new DrupalMenuTree(items);
    const menu = {
      items,
      tree: tree.length ? tree : void 0
    };
    if (options.withCache) {
      await this.cache.set(options.cacheKey, JSON.stringify(menu));
    }
    return menu;
  }
  /**
   * Fetches a view by its name.
   *
   * @param {string} name The name of the view and the display id. Example: `articles--promoted`.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<DrupalView<T>>} The fetched view.
   * @requires JSON:API Views module
   * @example
   * Get a view named `articles` and display id `promoted`
   * ```ts
   * const view = await drupal.getView("articles--promoted")
   * ```
   *
   * Using sparse fieldsets to only fetch the title and body fields
   * ```ts
   * const view = await drupal.getView("articles--promoted", {
   *   params: {
   *     fields: {
   *       "node--article": "title,body",
   *     },
   *   },
   * })
   * ```
   *
   * Using TypeScript with DrupalNode for a node entity type
   * ```ts
   * import { DrupalNode } from "next-drupal"
   *
   * const view = await drupal.getView<DrupalNode>("articles--promoted")
   * ```
   */
  async getView(name, options) {
    options = {
      withAuth: this.withAuth,
      deserialize: true,
      params: {},
      ...options
    };
    const [viewId, displayId] = name.split("--");
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      path: `/views/${viewId}/${displayId}`,
      searchParams: options.params
    });
    this.debug(`Fetching view, ${viewId}.${displayId}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    await this.throwIfJsonErrors(response, "Error while fetching view: ");
    const data = await response.json();
    const results = options.deserialize ? this.deserialize(data) : data;
    return {
      id: name,
      results,
      meta: data.meta,
      links: data.links
    };
  }
  /**
   * Fetches a search index by its name.
   *
   * @param {string} name The name of the search index.
   * @param {JsonApiOptions & JsonApiWithNextFetchOptions} options Options for the request.
   * @returns {Promise<T>} The fetched search index.
   * @requires JSON:API Search API module
   * @example
   * Get search results from an index named `articles`
   * ```ts
   * const results = await drupal.getSearchIndex("articles")
   * ```
   *
   * Using TypeScript with DrupalNode for a node entity type
   * ```ts
   * import { DrupalNode } from "next-drupal"
   *
   * const results = await drupal.getSearchIndex<DrupalNode>("articles")
   * ```
   */
  async getSearchIndex(name, options) {
    options = {
      withAuth: this.withAuth,
      deserialize: true,
      ...options
    };
    const endpoint = await this.buildEndpoint({
      locale: options?.locale !== options?.defaultLocale ? options.locale : void 0,
      path: `/index/${name}`,
      searchParams: options.params
    });
    this.debug(`Fetching search index, ${name}.`);
    const response = await this.fetch(endpoint, {
      withAuth: options.withAuth,
      next: options.next,
      cache: options.cache
    });
    await this.throwIfJsonErrors(
      response,
      "Error while fetching search index: "
    );
    const json = await response.json();
    return options.deserialize ? this.deserialize(json) : json;
  }
  /**
   * Deserializes the response body.
   *
   * @param {any} body The response body.
   * @param {any} options Options for deserialization.
   * @returns {any} The deserialized response body.
   * @remarks
   * To provide your own custom deserializer, see the serializer docs.
   * @example
   * ```ts
   * const url = drupal.buildUrl("/jsonapi/node/article", {
   *   sort: "-created",
   *   "fields[node--article]": "title,path",
   * })
   *
   * const response = await drupal.fetch(url.toString())
   * const json = await response.json()
   *
   * const resource = drupal.deserialize(json)
   * ```
   */
  deserialize(body, options) {
    if (!body) return null;
    return this.deserializer(body, options);
  }
  /**
   * Logs or throws an error based on the throwJsonApiErrors flag.
   *
   * @param {Error} error The error to log or throw.
   */
  logOrThrowError(error) {
    if (!this.throwJsonApiErrors) {
      this.logger.error(error);
      return;
    }
    throw error;
  }
};

// src/next-drupal-pages.ts
var import_jsona2 = require("jsona");
var NextDrupalPages = class extends NextDrupal {
  /**
   * Instantiates a new NextDrupalPages.
   *
   * const client = new NextDrupalPages(baseUrl)
   *
   * @param {baseUrl} baseUrl The baseUrl of your Drupal site. Do not add the /jsonapi suffix.
   * @param {options} options Options for the client. See Experiment_DrupalClientOptions.
   */
  constructor(baseUrl, options = {}) {
    super(baseUrl, options);
    this.getPathsFromContext = this.getStaticPathsFromContext;
    const {
      serializer = new import_jsona2.Jsona(),
      useDefaultResourceTypeEntry = false,
      useDefaultEndpoints = null
    } = options;
    if (useDefaultEndpoints === null) {
      this.useDefaultEndpoints = !!useDefaultResourceTypeEntry;
    }
    this.serializer = serializer;
    this.deserializer = (body, options2) => this.serializer.deserialize(body, options2);
  }
  /**
   * Get the JSON:API entry for a resource type.
   *
   * @param {string} resourceType The resource type. Example: `node--article`.
   * @param {Locale} locale Optional. The locale to fetch the index. Example: `es` or `fr`.
   * @returns {Promise<string>} The entry point URL.
   * @remarks
   * By default, when retrieving resources in `getResource` or `getResourceCollection`,
   * the `DrupalClient` make a request to Drupal to fetch the JSON:API resource entry.
   *
   * Example: if you provide `node--article`, `DrupalClient` will make a request to
   * `http://example.com/jsonapi/node/article`.
   *
   * If you would like to infer the entry from the resource type, use the useDefaultResourceTypeEntry option:
   * ```ts
   * const drupal = new DrupalClient(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL, {
   *   useDefaultResourceTypeEntry: true,
   * })
   * ```
   * @example
   * ```ts
   * // https://example.com/jsonapi/node/article
   * const url = await drupal.getEntryForResourceType(`node--article`)
   * ```
   */
  async getEntryForResourceType(resourceType, locale) {
    return this.buildEndpoint({
      locale,
      resourceType
    });
  }
  /* c8 ignore next 3 */
  buildMenuTree(links, parent = "") {
    return new DrupalMenuTree(links, parent);
  }
  /**
   * Gets a resource from the context.
   *
   * @param {string | DrupalTranslatedPath} input Either a resource type (e.g. "node--article") or a translated path from translatePath().
   * @param {GetStaticPropsContext} context The Next.js context from getStaticProps.
   * @param {Object} options Options for the request.
   * @param {PathPrefix} [options.pathPrefix] The path prefix to use for the request (defaults to "/").
   * @param {boolean} [options.isVersionable] Whether the resource is versionable (defaults to false for all entity types except nodes).
   * @returns {Promise<T>} The fetched resource.
   * @remarks
   * The localized resource will be fetched based on the `locale` and `defaultLocale` values from `context`.
   *
   * If you pass in a `DrupalTranslatedPath` for input, `getResourceFromContext` will take the `type` and `id` from the path and make a `getResource` call to Drupal:
   * ```ts
   * export async function getStaticProps(context) {
   *   const path = await drupal.translatePathFromContext(context)
   *
   *   const node = await drupal.getResourceFromContext(path, context)
   *
   *   return {
   *     props: {
   *       node,
   *     },
   *   }
   * }
   * ```
   *
   * If you pass in a `string` input, such as `node--article`, `getResourceFromContext` will make a subrequest call to Drupal to translate the path and then fetch the resource.
   * You will need both the [Subrequests](https://drupal.org/project/subrequests) and [Decoupled Router](https://drupal.org/project/decoupled_router) modules:
   * ```ts
   * export async function getStaticProps(context) {
   *   const node = await drupal.getResourceFromContext("node--article", context)
   *
   *   return {
   *     props: {
   *       node,
   *     },
   *   }
   * }
   * ```
   * @examples
   * Fetch a resource from context.
   * ```ts title=pages/[[...slug]].tsx
   * export async function getStaticProps(context) {
   *   const node = await drupal.getResourceFromContext("node--page", context)
   *
   *   return {
   *     props: {
   *       node,
   *     },
   *   }
   * }
   * ```
   * Fetch a resource from context in a sub directory.
   * ```ts title=pages/articles/[[...slug]].tsx
   * export async function getStaticProps(context) {
   *   const node = await drupal.getResourceFromContext("node--page", context, {
   *     pathPrefix: "/articles",
   *   })
   *
   *   return {
   *     props: {
   *       node,
   *     },
   *   }
   * }
   * ```
   * Using DrupalNode type:
   * ```ts
   * import { DrupalNode } from "next-drupal"
   *
   * const node = await drupal.getResourceFromContext<DrupalNode>(
   *   "node--page",
   *   context
   * )
   * ```
   * Using DrupalTaxonomyTerm type:
   * ```ts
   * import { DrupalTaxonomyTerm } from "next-drupal"
   *
   * const term = await drupal.getResourceFromContext<DrupalTaxonomyTerm>(
   *   "taxonomy_term--tags",
   *   context
   * )
   * ```
   * @see {@link https://next-drupal.org/docs/typescript} for more built-in types.
   */
  async getResourceFromContext(input, context, options) {
    const type = typeof input === "string" ? input : input.jsonapi.resourceName;
    const previewData = context.previewData;
    options = {
      deserialize: true,
      pathPrefix: "/",
      withAuth: this.getAuthFromContextAndOptions(context, options),
      params: {},
      ...options
    };
    const _options = {
      deserialize: options.deserialize,
      isVersionable: options.isVersionable,
      locale: context.locale,
      defaultLocale: context.defaultLocale,
      withAuth: options?.withAuth,
      params: options?.params
    };
    const isVersionable = options.isVersionable || /^node--/.test(type);
    if (isVersionable && typeof options.params.resourceVersion === "undefined") {
      options.params.resourceVersion = previewData?.resourceVersion || "rel:latest-version";
    }
    if (typeof input !== "string") {
      if (context.locale && input.entity.langcode !== context.locale) {
        context.locale = input.entity.langcode;
      }
      if (input.entity?.uuid) {
        return await this.getResource(type, input.entity.uuid, _options);
      }
    }
    const path = this.getPathFromContext(context, {
      pathPrefix: options?.pathPrefix
    });
    const resource = await this.getResourceByPath(path, _options);
    return resource;
  }
  /**
   * Gets a collection of resources from the context.
   *
   * @param {string} type The type of the resources. Example: `node--article` or `user--user`.
   * @param {GetStaticPropsContext} context The static props context from getStaticProps or getServerSideProps.
   * @param {Object} options Options for the request.
   *   - deserialize: Set to false to return the raw JSON:API response
   * @returns {Promise<T>} The fetched collection of resources.
   * @remarks
   * The localized resources will be fetched based on the `locale` and `defaultLocale` values from `context`.
   * @example
   * Get all articles from context
   * ```
   * export async function getStaticProps(context) {
   *   const articles = await drupal.getResourceCollectionFromContext(
   *     "node--article",
   *     context
   *   )
   *
   *   return {
   *     props: {
   *       articles,
   *     },
   *   }
   * }
   * ```
   * Using TypeScript with DrupalNode for a node entity type
   * ```
   * import { DrupalNode } from "next-drupal"
   * const nodes = await drupal.getResourceCollectionFromContext<DrupalNode[]>(
   *   "node--article",
   *   context
   * )
   * ```
   */
  async getResourceCollectionFromContext(type, context, options) {
    options = {
      deserialize: true,
      ...options
    };
    return await this.getResourceCollection(type, {
      ...options,
      locale: context.locale,
      defaultLocale: context.defaultLocale,
      withAuth: this.getAuthFromContextAndOptions(context, options)
    });
  }
  /**
   * Gets a search index from the context.
   *
   * @param {string} name The name of the search index.
   * @param {GetStaticPropsContext} context The static props context.
   * @param {Object} options Options for the request.
   * @returns {Promise<T>} The fetched search index.
   */
  async getSearchIndexFromContext(name, context, options) {
    return await this.getSearchIndex(name, {
      ...options,
      locale: context.locale,
      defaultLocale: context.defaultLocale
    });
  }
  /**
   * Translates a path from the context.
   *
   * @param {GetStaticPropsContext} context The context from `getStaticProps` or `getServerSideProps`.
   * @param {Object} options Options for the request.
   * @returns {Promise<DrupalTranslatedPath | null>} The translated path.
   * @requires Decoupled Router module
   * @example
   * Get info about a path from `getStaticProps` context
   * ```ts
   * export async function getStaticProps(context) {
   *   const path = await drupal.translatePathFromContext(context)
   * }
   * ```
   */
  async translatePathFromContext(context, options) {
    options = {
      pathPrefix: "/",
      ...options
    };
    const path = this.getPathFromContext(context, {
      pathPrefix: options.pathPrefix
    });
    return await this.translatePath(path, {
      withAuth: this.getAuthFromContextAndOptions(context, options)
    });
  }
  /**
   * Return the path (slug) from getStaticProps or getServerSideProps context.
   *
   * @param {GetStaticPropsContext} context The context from `getStaticProps` or `getServerSideProps`.
   * @param {Object} options Options for the request.
   * @returns {string} The constructed path.
   * @example
   * Get the path (slug) from `getStaticProps` context
   * ```ts
   * export async function getStaticProps(context) {
   *   const slug = await drupal.getPathFromContext(context)
   * }
   * ```
   */
  getPathFromContext(context, options) {
    return this.constructPathFromSegment(context.params?.slug, {
      locale: context.locale,
      defaultLocale: context.defaultLocale,
      pathPrefix: options?.pathPrefix
    });
  }
  /**
   * Gets static paths from the context.
   *
   * @param {string | string[]} types The resource types. Example: `node--article` or `["taxonomy_term--tags", "user--user"]`.
   * @param {GetStaticPathsContext} context The context from `getStaticPaths`.
   * @param {object} options Options for the request.
   * @returns {Promise<GetStaticPathsResult<{ slug: string[] }>["paths"]>} The static paths.
   * @example
   * Return static paths for `node--page` resources
   * ```ts
   * export async function getStaticPaths(context) {
   *   return {
   *     paths: await drupal.getStaticPathsFromContext("node--page", context),
   *     fallback: "blocking",
   *   }
   * }
   * ```
   *
   * Return static paths for `node--page` and `node--article` resources
   * ```ts
   * export async function getStaticPaths(context) {
   *   return {
   *     paths: await drupal.getStaticPathsFromContext(
   *       ["node--page", "node--article"],
   *       context
   *     ),
   *     fallback: "blocking",
   *   }
   * }
   * ```
   */
  async getStaticPathsFromContext(types, context, options) {
    options = {
      withAuth: this.withAuth,
      pathPrefix: "/",
      params: {},
      ...options
    };
    if (typeof types === "string") {
      types = [types];
    }
    const paths = await Promise.all(
      types.map(async (type) => {
        const params = {
          [`fields[${type}]`]: "path",
          ...options?.params
        };
        if (!context.locales?.length) {
          const resources = await this.getResourceCollection(type, {
            params,
            withAuth: options.withAuth
          });
          return this.buildStaticPathsFromResources(resources, {
            pathPrefix: options.pathPrefix
          });
        }
        const paths2 = await Promise.all(
          context.locales.map(async (locale) => {
            const resources = await this.getResourceCollection(type, {
              deserialize: true,
              locale,
              defaultLocale: context.defaultLocale,
              params,
              withAuth: options.withAuth
            });
            return this.buildStaticPathsFromResources(resources, {
              locale,
              pathPrefix: options.pathPrefix
            });
          })
        );
        return paths2.flat();
      })
    );
    return paths.flat();
  }
  /**
   * Builds static paths from resources.
   *
   * @param {Object[]} resources The resources.
   * @param {Object} options Options for the request.
   * @returns {Object[]} The built static paths.
   */
  buildStaticPathsFromResources(resources, options) {
    const paths = resources?.flatMap((resource) => {
      return resource?.path?.alias === this.frontPage ? "/" : resource?.path?.alias;
    }).filter(Boolean);
    return paths?.length ? this.buildStaticPathsParamsFromPaths(paths, options) : [];
  }
  /**
   * Builds static paths parameters from paths.
   *
   * @param {string[]} paths The paths.
   * @param {Object} options Options for the request.
   * @returns {Object[]} The built static paths parameters.
   */
  buildStaticPathsParamsFromPaths(paths, options) {
    return paths.flatMap((_path) => {
      _path = _path.replace(/^\/|\/$/g, "");
      if (options?.pathPrefix && options.pathPrefix !== "/") {
        const pathPrefix = options.pathPrefix.replace(/^\//, "");
        _path = _path.replace(`${pathPrefix}/`, "");
      }
      const path = {
        params: {
          slug: _path.split("/")
        }
      };
      if (options?.locale) {
        path["locale"] = options.locale;
      }
      return path;
    });
  }
  /**
   * Handle preview mode for resources.
   *
   * @param {NextApiRequest} request The `request` from an API route.
   * @param {NextApiResponse} response The `response` from an API route.
   * @param {Object} options Options for the request.
   * @returns {Promise<void>}
   * @remarks
   * The `preview` method should be called in an API route.
   * Remember to set a `previewSecret` on the client.
   * ```ts
   * // lib/drupal.ts
   * export const drupal = new DrupalClient(
   *   process.env.NEXT_PUBLIC_DRUPAL_BASE_URL,
   *   {
   *     previewSecret: process.env.DRUPAL_PREVIEW_SECRET,
   *   }
   * )
   * ```
   * @example
   * ```ts
   * // pages/api/preview.ts
   * import { drupal } from "lib/drupal"
   *
   * export default async function handler(req, res) {
   *   return await drupal.preview(req, res)
   * }
   * ```
   */
  async preview(request, response, options) {
    const { path, resourceVersion, plugin, secret, scope, ...draftData } = request.query;
    const useDraftMode = options?.enable;
    try {
      response.clearPreviewData();
      const result = await this.validateDraftUrl(
        new URL(request.url, `http://${request.headers.host}`).searchParams
      );
      const validationPayload = await result.json();
      const previewData = {
        resourceVersion,
        plugin,
        ...validationPayload
      };
      if (!result.ok) {
        this.debug(`Draft url validation error: ${validationPayload.message}`);
        response.statusCode = result.status;
        return response.json(validationPayload);
      }
      if (useDraftMode) {
        response.setDraftMode(options);
      }
      response.setPreviewData(previewData);
      const cookies = response.getHeader("Set-Cookie").map(
        (cookie) => cookie.replace("SameSite=Lax", "SameSite=None; Secure")
      );
      if (useDraftMode) {
        cookies.push(
          `${DRAFT_DATA_COOKIE_NAME}=${encodeURIComponent(
            JSON.stringify({ path, resourceVersion, ...draftData })
          )}; Path=/; HttpOnly; SameSite=None; Secure`
        );
      }
      response.setHeader("Set-Cookie", cookies);
      response.writeHead(307, { Location: path });
      this.debug(`${useDraftMode ? "Draft" : "Preview"} mode enabled.`);
      return response.end();
    } catch (error) {
      this.debug(`Preview failed: ${error.message}`);
      return response.status(422).end();
    }
  }
  /**
   * Disables preview mode.
   *
   * @param {NextApiRequest} request The API request.
   * @param {NextApiResponse} response The API response.
   */
  async previewDisable(request, response) {
    response.clearPreviewData();
    response.setDraftMode({ enable: false });
    const cookies = response.getHeader("Set-Cookie");
    cookies.push(
      `${DRAFT_DATA_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=None; Secure`
    );
    response.setHeader("Set-Cookie", cookies);
    response.writeHead(307, { Location: "/" });
    response.end();
  }
  /**
   * Gets the authentication configuration from the context and options.
   *
   * @param {GetStaticPropsContext} context The static props context.
   * @param {JsonApiWithAuthOption} options Options for the request.
   * @returns {NextDrupalAuth} The authentication configuration.
   */
  getAuthFromContextAndOptions(context, options) {
    if (!context.preview) {
      if (typeof options?.withAuth !== "undefined") {
        return options.withAuth;
      }
      return this.withAuth;
    }
    const plugin = context.previewData?.["plugin"];
    if (!plugin) {
      return null;
    }
    let withAuth = this.auth;
    if (plugin === "simple_oauth") {
      if (isClientIdSecretAuth(withAuth) && context.previewData?.["scope"]) {
        withAuth = {
          ...withAuth,
          scope: context.previewData?.["scope"]
        };
      }
    }
    if (plugin === "jwt") {
      const accessToken = context.previewData?.["access_token"];
      if (accessToken) {
        return `Bearer ${accessToken}`;
      }
    }
    return withAuth;
  }
};

// src/deprecated/get-cache.ts
var import_node_cache = __toESM(require("node-cache"), 1);
var cache = new import_node_cache.default();

// src/deprecated/get-access-token.ts
var CACHE_KEY = "NEXT_DRUPAL_ACCESS_TOKEN";
async function getAccessToken() {
  if (!process.env.DRUPAL_CLIENT_ID || !process.env.DRUPAL_CLIENT_SECRET) {
    return null;
  }
  const cached = cache.get(CACHE_KEY);
  if (cached?.access_token) {
    return cached;
  }
  const basic = Buffer.from(
    `${process.env.DRUPAL_CLIENT_ID}:${process.env.DRUPAL_CLIENT_SECRET}`
  ).toString("base64");
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}/oauth/token`,
    {
      method: "POST",
      headers: {
        Authorization: `Basic ${basic}`,
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: `grant_type=client_credentials`
    }
  );
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const result = await response.json();
  cache.set(CACHE_KEY, result, result.expires_in);
  return result;
}

// src/deprecated/utils.ts
var import_jsona3 = require("jsona");
var import_qs3 = require("qs");
var JSONAPI_PREFIX = process.env.DRUPAL_JSONAPI_PREFIX || "/jsonapi";
var dataFormatter = new import_jsona3.Jsona();
function deserialize(body, options) {
  if (!body) return null;
  return dataFormatter.deserialize(body, options);
}
async function getJsonApiPathForResourceType(type, locale) {
  const index = await getJsonApiIndex(locale);
  return index?.links[type]?.href;
}
async function getJsonApiIndex(locale, options) {
  const url = buildUrl(
    locale ? `/${locale}${JSONAPI_PREFIX}` : `${JSONAPI_PREFIX}`
  );
  const response = await fetch(url.toString(), {
    headers: locale || options ? await buildHeaders(options) : {
      "Content-Type": "application/json"
    }
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  return await response.json();
}
function buildUrl(path, params) {
  const url = new URL(
    path.charAt(0) === "/" ? `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${path}` : path
  );
  if (params) {
    url.search = (0, import_qs3.stringify)(params);
  }
  return url;
}
async function buildHeaders({
  accessToken,
  headers = {
    "Content-Type": "application/json"
  }
} = {}) {
  if (process.env.UNSTABLE_DRUPAL_ACCESS_TOKEN) {
    headers["Authorization"] = `Bearer ${process.env.UNSTABLE_DRUPAL_ACCESS_TOKEN}`;
    return headers;
  }
  const token = accessToken || await getAccessToken();
  if (token) {
    headers["Authorization"] = `Bearer ${token.access_token}`;
  }
  return headers;
}
function getPathFromContext(context, prefix = "") {
  let { slug } = context.params;
  slug = Array.isArray(slug) ? slug.map((s) => encodeURIComponent(s)).join("/") : slug;
  if (context.locale && context.locale !== context.defaultLocale) {
    slug = `/${context.locale}/${slug}`;
  }
  return !slug ? process.env.DRUPAL_FRONT_PAGE : prefix ? `${prefix}/${slug}` : slug;
}
function syncDrupalPreviewRoutes(path) {
  if (window && window.top !== window.self) {
    window.parent.postMessage(
      { type: "NEXT_DRUPAL_ROUTE_SYNC", path },
      process.env.NEXT_PUBLIC_DRUPAL_BASE_URL
    );
  }
}

// src/deprecated/get-menu.ts
async function getMenu(name, options) {
  options = {
    deserialize: true,
    ...options
  };
  const localePrefix = options?.locale && options.locale !== options.defaultLocale ? `/${options.locale}` : "";
  const url = buildUrl(`${localePrefix}/jsonapi/menu_items/${name}`);
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const data = await response.json();
  const items = options.deserialize ? deserialize(data) : data;
  const { items: tree } = buildMenuTree(items);
  return {
    items,
    tree
  };
}
function buildMenuTree(links, parent = "") {
  if (!links?.length) {
    return {
      items: []
    };
  }
  const children = links.filter((link) => link.parent === parent);
  return children.length ? {
    items: children.map((link) => ({
      ...link,
      ...buildMenuTree(links, link.id)
    }))
  } : {};
}

// src/deprecated/get-resource-collection.ts
async function getResourceCollection(type, options) {
  options = {
    deserialize: true,
    ...options
  };
  const apiPath = await getJsonApiPathForResourceType(
    type,
    options?.locale !== options?.defaultLocale ? options.locale : void 0
  );
  if (!apiPath) {
    throw new Error(`Error: resource of type ${type} not found.`);
  }
  const url = buildUrl(apiPath, {
    ...options?.params
  });
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const json = await response.json();
  return options.deserialize ? deserialize(json) : json;
}
async function getResourceCollectionFromContext(type, context, options) {
  options = {
    deserialize: true,
    ...options
  };
  return await getResourceCollection(type, {
    ...options,
    locale: context.locale,
    defaultLocale: context.defaultLocale
  });
}

// src/deprecated/get-paths.ts
async function getPathsFromContext(types, context, options = {}) {
  if (typeof types === "string") {
    types = [types];
  }
  const paths = await Promise.all(
    types.map(async (type) => {
      options.params = {
        [`fields[${type}]`]: "path",
        ...options?.params
      };
      if (!context.locales?.length) {
        const resources = await getResourceCollection(type, {
          deserialize: true,
          ...options
        });
        return buildPathsFromResources(resources);
      }
      const paths2 = await Promise.all(
        context.locales.map(async (locale) => {
          const resources = await getResourceCollection(type, {
            deserialize: true,
            locale,
            defaultLocale: context.defaultLocale,
            ...options
          });
          return buildPathsFromResources(resources, locale);
        })
      );
      return paths2.flat();
    })
  );
  return paths.flat();
}
function buildPathsFromResources(resources, locale) {
  return resources?.flatMap((resource) => {
    const slug = resource?.path?.alias === process.env.DRUPAL_FRONT_PAGE ? "/" : resource?.path?.alias;
    const path = {
      params: {
        slug: `${slug?.replace(/^\/|\/$/g, "")}`.split("/")
      }
    };
    if (locale) {
      path["locale"] = locale;
    }
    return path;
  });
}

// src/deprecated/get-resource.ts
var import_qs4 = require("qs");
async function getResourceFromContext(type, context, options) {
  options = {
    deserialize: true,
    // Add support for revisions for node by default.
    // TODO: Make this required before stable?
    isVersionable: /^node--/.test(type),
    ...options
  };
  const path = getPathFromContext(context, options?.prefix);
  const previewData = context.previewData;
  const resource = await getResourceByPath(path, {
    deserialize: options.deserialize,
    isVersionable: options.isVersionable,
    locale: context.locale,
    defaultLocale: context.defaultLocale,
    params: {
      resourceVersion: previewData?.resourceVersion,
      ...options?.params
    }
  });
  return resource;
}
async function getResourceByPath(path, options) {
  options = {
    deserialize: true,
    isVersionable: false,
    params: {},
    ...options
  };
  if (!path) {
    return null;
  }
  if (options.locale && options.defaultLocale && path.indexOf(options.locale) !== 1) {
    path = path === "/" ? path : path.replace(/^\/+/, "");
    path = getPathFromContext({
      params: { slug: [path] },
      locale: options.locale,
      defaultLocale: options.defaultLocale
    });
  }
  const { resourceVersion = "rel:latest-version", ...params } = options.params;
  if (options.isVersionable) {
    params.resourceVersion = resourceVersion;
  }
  const resourceParams = (0, import_qs4.stringify)(params);
  const payload = [
    {
      requestId: "router",
      action: "view",
      uri: `/router/translate-path?path=${path}&_format=json`,
      headers: { Accept: "application/vnd.api+json" }
    },
    {
      requestId: "resolvedResource",
      action: "view",
      uri: `{{router.body@$.jsonapi.individual}}?${resourceParams.toString()}`,
      waitFor: ["router"]
    }
  ];
  let subrequestsPath = "/subrequests";
  if (options.locale && options.defaultLocale && options.locale !== options.defaultLocale) {
    subrequestsPath = `/${options.locale}/subrequests`;
  }
  const url = buildUrl(subrequestsPath, {
    _format: "json"
  });
  const response = await fetch(url.toString(), {
    method: "POST",
    credentials: "include",
    headers: await buildHeaders(options),
    redirect: "follow",
    body: JSON.stringify(payload)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const json = await response.json();
  if (!json["resolvedResource#uri{0}"]) {
    return null;
  }
  const data = JSON.parse(json["resolvedResource#uri{0}"]?.body);
  if (data.errors) {
    throw new Error(data.errors[0].detail);
  }
  return options.deserialize ? deserialize(data) : data;
}
async function getResource(type, uuid, options) {
  options = {
    deserialize: true,
    params: {},
    ...options
  };
  const apiPath = await getJsonApiPathForResourceType(
    type,
    options?.locale !== options?.defaultLocale ? options.locale : void 0
  );
  if (!apiPath) {
    throw new Error(`Error: resource of type ${type} not found.`);
  }
  const url = buildUrl(`${apiPath}/${uuid}`, {
    ...options?.params
  });
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const json = await response.json();
  return options.deserialize ? deserialize(json) : json;
}

// src/deprecated/preview.ts
function DrupalPreview(options) {
  return (request, response) => PreviewHandler(request, response, options);
}
async function PreviewHandler(request, response, options) {
  const { path, resourceVersion, secret, locale, defaultLocale } = request.query;
  if (secret !== process.env.DRUPAL_PREVIEW_SECRET) {
    return response.status(401).json({
      message: options?.errorMessages.secret || "Invalid preview secret."
    });
  }
  if (!path) {
    return response.status(401).end({ message: options?.errorMessages.slug || "Invalid slug." });
  }
  let _options = {
    isVersionable: typeof resourceVersion !== "undefined"
  };
  if (locale && defaultLocale) {
    _options = {
      ..._options,
      locale,
      defaultLocale
    };
  }
  const url = await getResourcePreviewUrl(path, _options);
  if (!url) {
    response.status(404).end({ message: options?.errorMessages.slug || "Invalid slug" });
  }
  response.setPreviewData({
    resourceVersion
  });
  response.writeHead(307, { Location: url });
  return response.end();
}
async function getResourcePreviewUrl(slug, options) {
  const entity = await getResourceByPath(slug, options);
  if (!entity) {
    return null;
  }
  if (!entity?.path) {
    throw new Error(
      `Error: the path attribute is missing for entity type ${entity.type}`
    );
  }
  return entity?.default_langcode ? entity.path.alias : `/${entity.path.langcode}${entity.path.alias}`;
}

// src/deprecated/translate-path.ts
async function translatePath(path, options) {
  const url = buildUrl("/router/translate-path", {
    path
  });
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    return null;
  }
  const json = await response.json();
  return json;
}
async function translatePathFromContext(context, options) {
  options = {
    prefix: "",
    ...options
  };
  const path = getPathFromContext(context, options.prefix);
  const response = await translatePath(path, {
    accessToken: options.accessToken
  });
  return response;
}

// src/deprecated/get-resource-type.ts
async function getResourceTypeFromContext(context, options) {
  try {
    const response = await translatePathFromContext(context, options);
    return response.jsonapi.resourceName;
  } catch (error) {
    return null;
  }
}

// src/deprecated/get-search-index.ts
async function getSearchIndex(name, options) {
  options = {
    deserialize: true,
    ...options
  };
  const localePrefix = options?.locale && options.locale !== options.defaultLocale ? `/${options.locale}` : "";
  const url = buildUrl(`${localePrefix}/jsonapi/index/${name}`, options.params);
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const json = await response.json();
  return options.deserialize ? deserialize(json) : json;
}
async function getSearchIndexFromContext(name, context, options) {
  options = {
    deserialize: true,
    ...options
  };
  return await getSearchIndex(name, {
    ...options,
    locale: context.locale,
    defaultLocale: context.defaultLocale
  });
}

// src/deprecated/get-view.ts
async function getView(name, options) {
  options = {
    deserialize: true,
    ...options
  };
  const localePrefix = options?.locale && options.locale !== options.defaultLocale ? `/${options.locale}` : "";
  const [viewId, displayId] = name.split("--");
  const url = buildUrl(
    `${localePrefix}/jsonapi/views/${viewId}/${displayId}`,
    options.params
  );
  const response = await fetch(url.toString(), {
    headers: await buildHeaders(options)
  });
  if (!response.ok) {
    throw new Error(response.statusText);
  }
  const data = await response.json();
  const results = options.deserialize ? deserialize(data) : data;
  return {
    results,
    meta: data.meta,
    links: data.links
  };
}

// src/deprecated.ts
var DrupalClient = NextDrupalPages;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  DRAFT_DATA_COOKIE_NAME,
  DRAFT_MODE_COOKIE_NAME,
  DrupalClient,
  DrupalPreview,
  JsonApiErrors,
  NextDrupal,
  NextDrupalBase,
  NextDrupalPages,
  PreviewHandler,
  buildUrl,
  deserialize,
  getAccessToken,
  getJsonApiIndex,
  getJsonApiPathForResourceType,
  getMenu,
  getPathsFromContext,
  getResource,
  getResourceByPath,
  getResourceCollection,
  getResourceCollectionFromContext,
  getResourceFromContext,
  getResourcePreviewUrl,
  getResourceTypeFromContext,
  getSearchIndex,
  getSearchIndexFromContext,
  getView,
  isAccessTokenAuth,
  isBasicAuth,
  isClientIdSecretAuth,
  syncDrupalPreviewRoutes,
  translatePath,
  translatePathFromContext,
  useJsonaDeserialize
});
//# sourceMappingURL=index.cjs.map