All files / state/src/driver sqlite3.ts

100% Statements 156/156
100% Branches 27/27
100% Functions 7/7
100% Lines 156/156

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 1571x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x  
import { Database } from 'better-sqlite3';
import { Store } from '../store/store';
 
/**
 * **SQLite3** {@link Store.Driver}. This class provides a facade derived from
 * the built-in {@link Storage} interface to **SQLite3** databases under NodeJS.
 * This class implementing the {@link Store.Driver} contract is used as backing
 * storage by the {@link StateWorker}, if run in a NodeJS environment.
 *
 * @see {@link Store.Driver}
 */
export class SQLite3 implements Store.Driver {
 
  /**
   * Private **database** used as backing storage to read/write key/value pairs.
   */
  private readonly database: Database;
 
  /**
   * Returns the number of key/value pairs.
   */
  public get length(): Promise<number> {
    return new Promise((resolve) => this.database.transaction(() => {
      const result = this.database.prepare(`
        SELECT COUNT(*) AS count FROM '${this.version}'
      `).get() as { count: number };
 
      resolve(result.count);
    })());
  }
 
  /**
   * Public {@link SQLite3} **constructor** consuming the `name` and `version`
   * used to construct this instance of a {@link Store.Driver}.
   *
   * @param name - The `name` to address this instance by.
   * @param version - The `version` of this instance.
   */
  public constructor(
 
    /**
     * The `name` to address this instance by.
     */
    public readonly name: string,
 
    /**
     * The `version` of this instance.
     */
    public readonly version: string
 
  ) {
    // eslint-disable-next-line @typescript-eslint/no-shadow
    const Database = require('better-sqlite3');
    const { createHash } = require('crypto');
    const { join } = require('path');
 
    process.on('exit', () => this.database.close());
    process.on('SIGHUP', () => process.exit(128 + 1));
    process.on('SIGINT', () => process.exit(128 + 2));
    process.on('SIGTERM', () => process.exit(128 + 15));
 
    const hash = createHash('md5').update(this.name).digest('hex');
    this.database = new Database(join(__dirname, hash) + '.sdb').exec(`
      CREATE TABLE IF NOT EXISTS '${this.version}' (
        key TEXT PRIMARY KEY NOT NULL,
        value TEXT
      )
    `);
  }
 
  /**
   * Removes all key/value pairs, if there are any.
   *
   * @returns A {@link Promise} resolving when this instance was **clear**ed.
   */
  public clear(): Promise<void> {
    return new Promise((resolve) => this.database.transaction(() => {
      this.database.prepare(`
        DELETE FROM '${this.version}'
      `).run();
 
      resolve();
    })());
  }
 
  /**
   * Returns the current value associated with the given `key`, or null if the
   * given `key` does not exist.
   *
   * @param key - The `key` to retrieve the current value for.
   * @returns A {@link Promise} resolving to the current value or null.
   */
  public getItem(key: string): Promise<string | null> {
    return new Promise((resolve) => this.database.transaction(() => {
      const result = this.database.prepare(`
        SELECT value FROM '${this.version}' WHERE key = '${key}'
      `).get() as { value: string | null } | undefined;
 
      resolve(result?.value ?? null);
    })());
  }
 
  /**
   * Returns the name of the nth key, or null if n is greater than or equal to
   * the number of key/value pairs.
   *
   * @param index - The `index` of the **key** to retrieve.
   * @returns A {@link Promise} resolving to the name of the **key** or null.
   */
  public key(index: number): Promise<string | null> {
    return new Promise((resolve) => this.database.transaction(() => {
      const result = this.database.prepare(`
        SELECT key FROM '${this.version}' WHERE ROWID = '${index + 1}'
      `).get() as { key: string | null } | undefined;
 
      resolve(result?.key ?? null);
    })());
  }
 
  /**
   * Removes the key/value pair with the given `key`, if a key/value pair with
   * the given `key` exists.
   *
   * @param key - The `key` to delete the key/value pair by.
   * @returns A {@link Promise} resolving when the key/value pair was removed.
   */
  public removeItem(key: string): Promise<void> {
    return new Promise((resolve) => this.database.transaction(() => {
      this.database.prepare(`
        DELETE FROM '${this.version}' WHERE key = '${key}'
      `).run();
 
      resolve();
    })());
  }
 
  /**
   * Sets the `value` of the pair identified by `key` to `value`, creating a new
   * key/value pair if none existed for `key` previously.
   *
   * @param key - The `key` to set the key/value pair by.
   * @param value - The `value` to associate with the `key`.
   * @returns A {@link Promise} resolving when the key/value pair was set.
   */
  public setItem(key: string, value: string): Promise<void> {
    return new Promise((resolve) => this.database.transaction(() => {
      this.database.prepare(`
        INSERT INTO '${this.version}' (key, value) VALUES ('${key}', '${value}')
          ON CONFLICT(key) DO UPDATE SET value=excluded.value
      `).run();
 
      resolve();
    })());
  }
 
}