From b20f3cdb21d9a41616794f4dcb9350894e263571 Mon Sep 17 00:00:00 2001
From: Alan Plum <alan.plum-extern@arangodb.com>
Date: Mon, 6 Jan 2025 12:08:19 +0100
Subject: [PATCH] Implement missing HTTP API methods

Fixes DE-148. Fixes DE-149. Fixes DE-150. Fixes DE-151. Fixes DE-906. Fixes DE-932. Fixes DE-939. Fixes DE-949.
---
 CHANGELOG.md      |  38 ++--
 src/collection.ts |  41 +++++
 src/database.ts   | 438 +++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 446 insertions(+), 71 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad3f8e73c..c49804d55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,22 @@ This driver uses semantic versioning:
 
 ### Added
 
+- Added `db.compact` method (DE-906)
+
+- Added `db.engineStats` method (DE-932)
+
+- Added `db.getLicense` and `db.setLicense` methods (DE-949)
+
+- Added `db.listQueryCacheEntries` method (DE-149)
+
+- Added `db.clearQueryCache` method (DE-148)
+
+- Added `db.getQueryCacheProperties` method (DE-150)
+
+- Added `db.setQueryCacheProperties` method (DE-151)
+
+- Added `collection.shards` method (DE-939)
+
 - Added support for `mdi-prefixed` indexes (DE-956)
 
 - Restored `fulltext` index type support (DE-957)
@@ -33,13 +49,13 @@ This driver uses semantic versioning:
 
 ### Added
 
-- Added `database.availability` method
+- Added `db.availability` method
 
-- Added `database.engine` method (DE-931)
+- Added `db.engine` method (DE-931)
 
-- Added `database.status` method ([#811](https://github.com/arangodb/arangojs/issues/811))
+- Added `db.status` method ([#811](https://github.com/arangodb/arangojs/issues/811))
 
-- Added `database.supportInfo` method
+- Added `db.supportInfo` method
 
 - Added `keepNull` option to `CollectionInsertOptions` type (DE-946)
 
@@ -1132,7 +1148,7 @@ For a detailed list of changes between pre-release versions of v7 see the
 
 - Changed `db.createDatabase` return type to `Database`
 
-- Renamed `database.setQueryTracking` to `database.queryTracking`
+- Renamed `db.setQueryTracking` to `db.queryTracking`
 
   The method will now return the existing query tracking properties or set the
   new query tracking properties depending on whether an argument is provided.
@@ -1528,7 +1544,7 @@ For a detailed list of changes between pre-release versions of v7 see the
 
 - Added support for ArangoDB 3.5 Analyzers API
 
-  See the documentation of the `database.analyzer` method and the `Analyzer`
+  See the documentation of the `db.analyzer` method and the `Analyzer`
   instances for information on using this API.
 
 - Added `collection.getResponsibleShard` method
@@ -1702,7 +1718,7 @@ For a detailed list of changes between pre-release versions of v7 see the
 
 - Fixed `edgeCollection.save` not respecting options ([#554](https://github.com/arangodb/arangojs/issues/554))
 
-- Fixed `database.createDatabase` TypeScript signature ([#561](https://github.com/arangodb/arangojs/issues/561))
+- Fixed `db.createDatabase` TypeScript signature ([#561](https://github.com/arangodb/arangojs/issues/561))
 
 ## [6.5.0] - 2018-08-03
 
@@ -1743,7 +1759,7 @@ For a detailed list of changes between pre-release versions of v7 see the
 
 - Added `ArangoError` and `CollectionType` to public exports
 
-- Added `database.close` method
+- Added `db.close` method
 
 - Added `opts` parameter to `EdgeCollection#save`
 
@@ -1751,11 +1767,11 @@ For a detailed list of changes between pre-release versions of v7 see the
 
 ### Added
 
-- Added `database.version` method
+- Added `db.version` method
 
-- Added `database.login` method
+- Added `db.login` method
 
-- Added `database.exists` method
+- Added `db.exists` method
 
 - Added `collection.exists` method
 
diff --git a/src/collection.ts b/src/collection.ts
index 2a9ff28cd..2ce5a09da 100644
--- a/src/collection.ts
+++ b/src/collection.ts
@@ -1308,6 +1308,34 @@ export interface DocumentCollection<
    * ```
    */
   loadIndexes(): Promise<boolean>;
+  /**
+   * Retrieves the collection's shard IDs.
+   *
+   * @param details - If set to `true`, the response will include the responsible
+   * servers for each shard.
+   */
+  shards(
+    details?: false
+  ): Promise<
+    ArangoApiResponse<
+      CollectionMetadata & CollectionProperties & { shards: string[] }
+    >
+  >;
+  /**
+   * Retrieves the collection's shard IDs and the responsible servers for each
+   * shard.
+   *
+   * @param details - If set to `false`, the response will only include the
+   * shard IDs without the responsible servers for each shard.
+   */
+  shards(
+    details: true
+  ): Promise<
+    ArangoApiResponse<
+      CollectionMetadata &
+        CollectionProperties & { shards: Record<string, string[]> }
+    >
+  >;
   /**
    * Renames the collection and updates the instance's `name` to `newName`.
    *
@@ -2952,6 +2980,19 @@ export class Collection<
     );
   }
 
+  shards(
+    details?: boolean
+  ): Promise<
+    ArangoApiResponse<
+      CollectionMetadata & CollectionProperties & { shards: any }
+    >
+  > {
+    return this._db.request({
+      path: `/_api/collection/${encodeURIComponent(this._name)}/shards`,
+      search: { details },
+    });
+  }
+
   async rename(newName: string) {
     const result = await this._db.renameCollection(this._name, newName);
     this._name = newName;
diff --git a/src/database.ts b/src/database.ts
index 13836284e..388cd1758 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -20,11 +20,11 @@ import {
   ArangoCollection,
   Collection,
   CollectionMetadata,
-  collectionToString,
   CollectionType,
   CreateCollectionOptions,
   DocumentCollection,
   EdgeCollection,
+  collectionToString,
   isArangoCollection,
 } from "./collection.js";
 import {
@@ -619,6 +619,109 @@ export type QueryTrackingOptions = {
   trackSlowQueries?: boolean;
 };
 
+/**
+ * Entry in the AQL query results cache.
+ */
+export type QueryCacheEntry = {
+  /**
+   * Hash of the query results.
+   */
+  hash: string;
+  /**
+   * Query string.
+   */
+  query: string;
+  /**
+   * Bind parameters used in the query. Only shown if tracking for bind
+   * variables was enabled at server start.
+   */
+  bindVars: Record<string, any>;
+  /**
+   * Size of the query results and bind parameters in bytes.
+   */
+  size: number;
+  /**
+   * Number of documents/rows in the query results.
+   */
+  results: number;
+  /**
+   * Date and time the query was started as an ISO 8601 timestamp.
+   */
+  started: string;
+  /**
+   * Number of times the result was served from the cache.
+   */
+  hits: number;
+  /**
+   * Running time of the query in seconds.
+   */
+  runTime: number;
+  /**
+   * Collections and views involved in the query.
+   */
+  dataSources: string[];
+};
+
+/**
+ * Properties of the global AQL query results cache configuration.
+ */
+export type QueryCacheProperties = {
+  /**
+   * If set to `true`, the query cache will include queries that involve
+   * system collections.
+   */
+  includeSystem: boolean;
+  /**
+   * Maximum individual size of query results that will be stored per
+   * database-specific cache.
+   */
+  maxEntrySize: number;
+  /**
+   * Maximum number of query results that will be stored per database-specific
+   * cache.
+   */
+  maxResults: number;
+  /**
+   * Maximum cumulated size of query results that will be stored per
+   * database-specific cache.
+   */
+  maxResultsSize: number;
+  /**
+   * Mode the AQL query cache should operate in.
+   */
+  mode: "off" | "on" | "demand";
+};
+
+/**
+ * Options for adjusting the global properties for the AQL query results cache.
+ */
+export type QueryCachePropertiesOptions = {
+  /**
+   * If set to `true`, the query cache will include queries that involve
+   * system collections.
+   */
+  includeSystem?: boolean;
+  /**
+   * Maximum individual size of query results that will be stored per
+   * database-specific cache.
+   */
+  maxEntrySize?: number;
+  /**
+   * Maximum number of query results that will be stored per database-specific
+   * cache.
+   */
+  maxResults?: number;
+  /**
+   * Maximum cumulated size of query results that will be stored per
+   * database-specific cache.
+   */
+  maxResultsSize?: number;
+  /**
+   * Mode the AQL query cache should operate in.
+   */
+  mode?: "off" | "on" | "demand";
+};
+
 /**
  * Object describing a query.
  */
@@ -980,10 +1083,18 @@ export type EngineInfo = {
        * Index type aliases supported by the storage engine.
        */
       indexes?: Record<string, string>;
-    }
+    };
   };
 };
 
+/**
+ * Performance and resource usage information about the storage engine.
+ */
+export type EngineStatsInfo = Record<
+  string,
+  string | number | Record<string, number | string>
+>;
+
 /**
  * Information about the server status.
  */
@@ -1049,7 +1160,7 @@ export type ServerStatusInformation = {
    */
   foxxApi: boolean;
   /**
-   * A host identifier defined by the HOST or NODE_NAME environment variable, 
+   * A host identifier defined by the HOST or NODE_NAME environment variable,
    * or a fallback value using a machine identifier or the cluster/Agency address.
    */
   host: string;
@@ -1063,7 +1174,7 @@ export type ServerStatusInformation = {
   license: "community" | "enterprise";
   /**
    * Server operation mode.
-   * 
+   *
    * @deprecated use `operationMode` instead
    */
   mode: "server" | "console";
@@ -1135,7 +1246,7 @@ export type ServerStatusInformation = {
     version: string;
     /**
      * Whether writes are enabled.
-     * 
+     *
      * @deprecated Use `readOnly` instead.
      */
     writeOpsEnabled: boolean;
@@ -1146,9 +1257,9 @@ export type ServerStatusInformation = {
  * Server availability.
  *
  * - `"default"`: The server is operational.
- * 
+ *
  * - `"readonly"`: The server is in read-only mode.
- * 
+ *
  * - `false`: The server is not available.
  */
 export type ServerAvailability = "default" | "readonly" | false;
@@ -1167,9 +1278,9 @@ export type SingleServerSupportInfo = {
   deployment: {
     /**
      * Deployment mode:
-     * 
+     *
      * - `"single"`: A single server deployment.
-     * 
+     *
      * - `"cluster"`: A cluster deployment.
      */
     type: "single";
@@ -1190,9 +1301,9 @@ export type ClusterSupportInfo = {
   deployment: {
     /**
      * Deployment mode:
-     * 
+     *
      * - `"single"`: A single server deployment.
-     * 
+     *
      * - `"cluster"`: A cluster deployment.
      */
     type: "cluster";
@@ -1240,14 +1351,78 @@ export type ClusterSupportInfo = {
        * Number of servers in the cluster.
        */
       servers: number;
-    }
+    };
   };
   /**
    * (Cluster only.) Information about the ArangoDB instance as well as the
    * host machine.
    */
   host: Record<string, any>;
-}
+};
+
+/**
+ * Information about the server license.
+ */
+export type LicenseInfo = {
+  /**
+   * Properties of the license.
+   */
+  features: {
+    /**
+     * The timestamp of the expiration date of the license in seconds since the
+     * Unix epoch.
+     */
+    expires?: number;
+  };
+  /**
+   * The hash value of the license.
+   */
+  hash: string;
+  /**
+   * The encrypted license key in base 64 encoding, or `"none"` when running
+   * in the Community Edition.
+   */
+  license?: string;
+  /**
+   * The status of the installed license.
+   *
+   * - `"good"`: The license is valid for more than 2 weeks.
+   *
+   * - `"expiring"`: The license is valid for less than 2 weeks.
+   *
+   * - `"expired"`: The license has expired.
+   *
+   * - `"read-only"`: The license has been expired for more than 2 weeks.
+   */
+  status: "good" | "expiring" | "expired" | "read-only";
+  /**
+   * Whether the server is performing a database upgrade.
+   */
+  upgrading: boolean;
+  /**
+   * The license version number.
+   */
+  version: number;
+};
+
+/**
+ * Options for compacting all databases on the server.
+ */
+export type CompactOptions = {
+  /**
+   * Whether compacted data should be moved to the minimum possible level.
+   *
+   * Default: `false`.
+   */
+  changeLevel?: boolean;
+  /**
+   * Whether to compact the bottom-most level of data.
+   *
+   * Default: `false`.
+   */
+  compactBottomMostLevel?: boolean;
+};
+
 /**
  * Definition of an AQL User Function.
  */
@@ -1539,14 +1714,14 @@ export type ServiceConfiguration = {
    * by software when managing the service.
    */
   type:
-  | "integer"
-  | "boolean"
-  | "string"
-  | "number"
-  | "json"
-  | "password"
-  | "int"
-  | "bool";
+    | "integer"
+    | "boolean"
+    | "string"
+    | "number"
+    | "json"
+    | "password"
+    | "int"
+    | "bool";
   /**
    * Current value of the configuration option as stored internally.
    */
@@ -1726,10 +1901,10 @@ export type ServiceTestSuiteReport = {
 export type ServiceTestXunitTest =
   | ["testcase", { classname: string; name: string; time: number }]
   | [
-    "testcase",
-    { classname: string; name: string; time: number },
-    ["failure", { message: string; type: string }, string],
-  ];
+      "testcase",
+      { classname: string; name: string; time: number },
+      ["failure", { message: string; type: string }, string],
+    ];
 
 /**
  * Test results for a Foxx service's tests in XUnit format using the JSONML
@@ -2224,7 +2399,8 @@ export class Database {
       basePath,
       ...opts
     }: RequestOptions & { absolutePath?: boolean },
-    transform: false | ((res: ArangojsResponse) => ReturnType) = (res) => res.parsedBody
+    transform: false | ((res: ArangojsResponse) => ReturnType) = (res) =>
+      res.parsedBody
   ): Promise<ReturnType> {
     if (!absolutePath) {
       basePath = `/_db/${encodeURIComponent(this._name)}${basePath || ""}`;
@@ -2534,6 +2710,24 @@ export class Database {
     });
   }
 
+  /**
+   * Fetches detailed storage engine performance and resource usage information
+   * from the ArangoDB server.
+   *
+   * @example
+   * ```js
+   * const db = new Database();
+   * const stats = await db.engineStats();
+   * // the stats object contains the storage engine stats
+   * ```
+   */
+  engineStats(): Promise<EngineStatsInfo> {
+    return this.request({
+      method: "GET",
+      path: "/_api/engine/stats",
+    });
+  }
+
   /**
    * Retrives the server's current system time in milliseconds with microsecond
    * precision.
@@ -2569,7 +2763,7 @@ export class Database {
 
   /**
    * Fetches availability information about the server.
-   * 
+   *
    * @param graceful - If set to `true`, the method will always return `false`
    * instead of throwing an error; otherwise `false` will only be returned
    * when the server responds with a 503 status code or an ArangoDB error with
@@ -2583,10 +2777,13 @@ export class Database {
    */
   async availability(graceful = false): Promise<ServerAvailability> {
     try {
-      return this.request({
-        method: "GET",
-        path: "/_admin/server/availability",
-      }, (res) => res.parsedBody.mode);
+      return this.request(
+        {
+          method: "GET",
+          path: "/_admin/server/availability",
+        },
+        (res) => res.parsedBody.mode
+      );
     } catch (e) {
       if (graceful) return false;
       if ((isArangoError(e) || e instanceof HttpError) && e.code === 503) {
@@ -2598,7 +2795,7 @@ export class Database {
 
   /**
    * Fetches deployment information about the server for support purposes.
-   * 
+   *
    * Note that this API may reveal sensitive data about the deployment.
    */
   supportInfo(): Promise<SingleServerSupportInfo | ClusterSupportInfo> {
@@ -2608,6 +2805,51 @@ export class Database {
     });
   }
 
+  /**
+   * Fetches the license information and status of an Enterprise Edition server.
+   */
+  getLicense(): Promise<LicenseInfo> {
+    return this.request({
+      method: "GET",
+      path: "/_admin/license",
+    });
+  }
+
+  /**
+   * Set a new license for an Enterprise Edition server.
+   *
+   * @param license - The license as a base 64 encoded string.
+   * @param force - If set to `true`, the license will be changed even if it
+   * expires sooner than the current license.
+   */
+  setLicense(license: string, force = false): Promise<void> {
+    return this.request(
+      {
+        method: "PUT",
+        path: "/_admin/license",
+        body: license,
+        search: { force },
+      },
+      () => undefined
+    );
+  }
+
+  /**
+   * Compacts all databases on the server.
+   *
+   * @param options - Options for compacting the databases.
+   */
+  compact(options: CompactOptions = {}): Promise<void> {
+    return this.request(
+      {
+        method: "PUT",
+        path: "/_admin/compact",
+        body: options,
+      },
+      () => undefined
+    );
+  }
+
   /**
    * Attempts to initiate a clean shutdown of the server.
    */
@@ -2700,6 +2942,8 @@ export class Database {
    * Computes a set of move shard operations to rebalance the cluster and
    * executes them.
    *
+   * @param options - Options for rebalancing the cluster.
+   *
    * @example
    * ```js
    * const db = new Database();
@@ -2711,14 +2955,14 @@ export class Database {
    * ```
    */
   rebalanceCluster(
-    opts: ClusterRebalanceOptions
+    options: ClusterRebalanceOptions
   ): Promise<ClusterRebalanceResult> {
     return this.request({
       method: "PUT",
       path: "/_admin/cluster/rebalance",
       body: {
         version: 1,
-        ...opts,
+        ...options,
       },
     });
   }
@@ -3739,14 +3983,14 @@ export class Database {
   ): Promise<AccessLevel> {
     const databaseName = isArangoDatabase(database)
       ? database.name
-      : database ??
-      (isArangoCollection(collection)
-        ? ((collection as any)._db as Database).name
-        : this._name);
+      : (database ??
+        (isArangoCollection(collection)
+          ? ((collection as any)._db as Database).name
+          : this._name));
     const suffix = collection
       ? `/${encodeURIComponent(
-        isArangoCollection(collection) ? collection.name : collection
-      )}`
+          isArangoCollection(collection) ? collection.name : collection
+        )}`
       : "";
     return this.request(
       {
@@ -3840,14 +4084,14 @@ export class Database {
   ): Promise<ArangoApiResponse<Record<string, AccessLevel>>> {
     const databaseName = isArangoDatabase(database)
       ? database.name
-      : database ??
-      (isArangoCollection(collection)
-        ? ((collection as any)._db as Database).name
-        : this._name);
+      : (database ??
+        (isArangoCollection(collection)
+          ? ((collection as any)._db as Database).name
+          : this._name));
     const suffix = collection
       ? `/${encodeURIComponent(
-        isArangoCollection(collection) ? collection.name : collection
-      )}`
+          isArangoCollection(collection) ? collection.name : collection
+        )}`
       : "";
     return this.request(
       {
@@ -3930,14 +4174,14 @@ export class Database {
   ): Promise<ArangoApiResponse<Record<string, AccessLevel>>> {
     const databaseName = isArangoDatabase(database)
       ? database.name
-      : database ??
-      (isArangoCollection(collection)
-        ? ((collection as any)._db as Database).name
-        : this._name);
+      : (database ??
+        (isArangoCollection(collection)
+          ? ((collection as any)._db as Database).name
+          : this._name));
     const suffix = collection
       ? `/${encodeURIComponent(
-        isArangoCollection(collection) ? collection.name : collection
-      )}`
+          isArangoCollection(collection) ? collection.name : collection
+        )}`
       : "";
     return this.request(
       {
@@ -4458,7 +4702,7 @@ export class Database {
     } catch (e) {
       try {
         await trx.abort();
-      } catch { }
+      } catch {}
       throw e;
     }
   }
@@ -4881,14 +5125,14 @@ export class Database {
     return this.request(
       options
         ? {
-          method: "PUT",
-          path: "/_api/query/properties",
-          body: options,
-        }
+            method: "PUT",
+            path: "/_api/query/properties",
+            body: options,
+          }
         : {
-          method: "GET",
-          path: "/_api/query/properties",
-        }
+            method: "GET",
+            path: "/_api/query/properties",
+          }
     );
   }
 
@@ -4981,6 +5225,80 @@ export class Database {
       () => undefined
     );
   }
+
+  /**
+   * Fetches a list of all entries in the AQL query results cache of the
+   * current database.
+   *
+   * @example
+   * ```js
+   * const db = new Database();
+   * const entries = await db.listQueryCacheEntries();
+   * console.log(entries);
+   * ```
+   */
+  listQueryCacheEntries(): Promise<QueryCacheEntry[]> {
+    return this.request({
+      path: "/_api/query-cache/entries",
+    });
+  }
+
+  /**
+   * Clears the AQL query results cache of the current database.
+   *
+   * @example
+   * ```js
+   * const db = new Database();
+   * await db.clearQueryCache();
+   * // Cache is now cleared
+   * ```
+   */
+  clearQueryCache(): Promise<void> {
+    return this.request(
+      {
+        method: "DELETE",
+        path: "/_api/query-cache",
+      },
+      () => undefined
+    );
+  }
+
+  /**
+   * Fetches the global properties for the AQL query results cache.
+   *
+   * @example
+   * ```js
+   * const db = new Database();
+   * const properties = await db.getQueryCacheProperties();
+   * console.log(properties);
+   * ```
+   */
+  getQueryCacheProperties(): Promise<QueryCacheProperties> {
+    return this.request({
+      path: "/_api/query-cache/properties",
+    });
+  }
+
+  /**
+   * Updates the global properties for the AQL query results cache.
+   *
+   * @param properties - The new properties for the AQL query results cache.
+   *
+   * @example
+   * ```js
+   * const db = new Database();
+   * await db.setQueryCacheProperties({ maxResults: 9000 });
+   * ```
+   */
+  setQueryCacheProperties(
+    properties: QueryCachePropertiesOptions
+  ): Promise<QueryCacheProperties> {
+    return this.request({
+      method: "PUT",
+      path: "/_api/query-cache/properties",
+      body: properties,
+    });
+  }
   //#endregion
 
   //#region functions