Slonik Node.js PostgreSQL Client

repository·main·Indexed 26 days ago

https://github.com/gajus/slonik

A high-integrity Node.js PostgreSQL client prioritizing type safety, raw SQL usage, and robust error handling. It includes a driver abstraction layer, specialized data loaders for Node-by-ID and Relay-style connections, and a suite of interceptors for field name transformation (snake_case to camelCase), query caching via SQL comments, and query logging using Roarr.

Tokens
29.5K
Snippets
74
Records
144
Agent score
88%

What's inside Slonik

  1. Overview of Slonik

    main

    Slonik is a battle-tested Node.js PostgreSQL client designed with strict types, detailed logging, and assertions. It is built on several core principles:

    • Promotes writing raw SQL: Instead of using an abstraction layer, Slonik encourages using actual SQL.
    • Discourages ad-hoc dynamic generation of SQL: It aims to prevent the unsafe and complex generation of SQL strings at runtime.

    Key features include runtime validation, safe connection and transaction handling, transaction nesting, query retrying, and a rich set of interceptors (middlewares).

  2. Overview of Slonik Driver

    main
    The slonik-driver package provides an abstraction layer that defines a consistent API for interacting with different PostgreSQL clients. It is intended for developers who want to implement their own custom Slonik driver to support alternative PostgreSQL client libraries.
  3. Use the Slonik `sql` tag

    main
    The sql tag is a template literal tag used to create safe, parameterized SQL queries. It prevents SQL injection by ensuring that all interpolated values are treated as parameters rather than raw SQL text. This tag is the core mechanism for building queries in Slonik.
  4. Understand Slonik vs pg and pg-promise

    main

    Slonik is a high-level abstraction built on top of pg. Unlike pg, which is unopinionated and minimal, Slonik provides convenience methods for building queries and querying data.

    Key differences from pg-promise include:

    • Security: Slonik does not allow raw text queries; all queries must be constructed using sql tagged template literals to protect against unsafe value interpolation.
    • Extensibility: Slonik implements an interceptor API (middleware) to modify connection handling, override queries, and modify results (e.g., field name transformation or query logging).

    Comparison mapping for developers migrating from pg-promise:

    • Use Slonik tagged template value expressions instead of formatting filters.
    • Use slonik-sql-tag-raw instead of Query files.
    • Use pool.connect() instead of Tasks.
    • Use interceptors instead of Events.
  5. Create and manage a Slonik connection pool

    main

    Use createPool to initialize a connection pool. You can run queries directly on the pool for single queries, or use pool.connect() to check out a connection for multiple queries that must share the same backend.

    Terminate the pool

    Use pool.end() to end idle connections and prevent new ones from being created. This returns a promise that resolves when all connections are ended. Note that pool.end() does not terminate active connections or transactions.

    import { createPool, sql } from "slonik";
    import { createPgDriverFactory } from "@slonik/pg-driver";
    
    const pool = await createPool("postgres://", {
      driverFactory: createPgDriverFactory(),
    });
    
    const main = async () => {
      await pool.query(sql.typeAlias("id")`
        SELECT 1 AS id
      `);
    
      await pool.end();
    };
    
    main();
  6. Run PostgreSQL with SSL using Docker

    main

    You can run a PostgreSQL instance configured with SSL for testing purposes using the provided Docker setup. Build the image and then run the container, mapping the internal port 5432 to host port 5433.

    docker build -t slonik-ssl-test .
    docker run --name slonik-ssl-test --rm -it -e POSTGRES_PASSWORD=postgres -p 5433:5432 slonik-ssl-test
  7. Install and configure slonik-interceptor-query-cache

    main

    The slonik-interceptor-query-cache interceptor allows you to cache Slonik query results using a custom storage service.

    To use it, you must initialize the interceptor with a storage object that implements get and set methods. The interceptor only caches queries that include specific cache attributes in their SQL comments (starting with -- @cache-).

    Important Behavior:

    • Queries executed inside a transaction are not cached.
    import NodeCache from "node-cache";
    import { createPool } from "slonik";
    import { createQueryCacheInterceptor } from "slonik-interceptor-query-cache";
    
    const nodeCache = new NodeCache({
      checkperiod: 60,
      stdTTL: 60,
      useClones: false,
    });
    
    const pool = await createPool("postgres://", {
      interceptors: [
        createQueryCacheInterceptor({
          storage: {
            get: (query, cacheAttributes) => {
              // Returning null results in the query being executed.
              return nodeCache.get(cacheAttributes.key) || null;
            },
            set: (query, cacheAttributes, queryResult) => {
              nodeCache.set(cacheAttributes.key, queryResult, cacheAttributes.ttl);
            },
          },
        }),
      ],
    });
  8. Use `createQueryLoggingInterceptor` to log Slonik queries

    main

    The slonik-interceptor-query-logging package allows you to log Slonik queries. To see logs in your console, you must set the ROARR_LOG=true environment variable. The interceptor uses Roarr for logging.

    To use it, include the interceptor returned by createQueryLoggingInterceptor() in your Slonik pool configuration.

    import { createPool } from "slonik";
    import { createQueryLoggingInterceptor } from "slonik-interceptor-query-logging";
    
    const interceptors = [createQueryLoggingInterceptor()];
    
    const pool = createPool("postgres://", {
      interceptors,
    });
    
    await pool.any(sql`
      SELECT
        id,
        code_alpha_2
      FROM country
    `);
  9. Validate SQL queries with eslint-plugin-slonik

    main

    Use eslint-plugin-slonik to validate Slonik SQL queries against your database schema at lint time. This helps catch:

    • References to non-existent tables and columns.
    • Type mismatches between query results and Zod schemas.
    • Other common SQL mistakes before they reach runtime.
  10. Generate SSL certificates for testing Slonik

    main

    To test Slonik with SSL, you can generate a self-signed Root Certificate (CA) and a Client Certificate using OpenSSL. Follow these steps in order:

    1. Generate a Root Certificate (CA): Create a private key and a self-signed certificate.
    2. Generate a Client Key: Create a private key for the client.
    3. Create a Certificate Signing Request (CSR): Generate a CSR for the client using the client key.
    4. Sign the Client Certificate: Use the Root CA to sign the client's CSR.
    5. Verify: Ensure the certificates are valid.
    # Generate a Root Certificate (CA)
    openssl genrsa -out root.key 2048
    openssl req -x509 -new -nodes -key root.key -sha256 -days 365 -out root.crt -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=RootCA"
    
    # Generate a Client Key
    openssl genrsa -out slonik.key 2048
    
    # Create a Certificate Signing Request (CSR) for the Client
    openssl req -new -key slonik.key -out slonik.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=Client"
    
    # Sign the Client Certificate with the Root Certificate
    openssl x509 -req -in slonik.csr -CA root.crt -CAkey root.key -CAcreateserial -out slonik.crt -days 365 -sha256
    
    # Verify the Certificates
    openssl verify -CAfile root.crt slonik.crt
  11. Capture stack traces in query logs

    main
    To include a stack trace in your query logs (showing exactly where the query was invoked in your code), you must use the slonik-interceptor-query-logging interceptor and enable the captureStackTrace configuration option.
  12. Capture PostgreSQL `auto_explain` logs in Slonik

    main

    While Slonik's executionTime includes network latency and connection overhead, you can capture the real PostgreSQL query execution time by using the auto_explain module.

    To enable this, you must first ensure the auto_explain extension is loaded and configured in PostgreSQL. You can automate this setup in Slonik using the afterPoolConnection interceptor to run the necessary LOAD and SET commands on every new connection.

    const pool = createPool("postgres://localhost", {
      interceptors: [
        {
          afterPoolConnection: async (connection) => {
            await connection.query(sql`LOAD 'auto_explain'`);
            await connection.query(sql`SET auto_explain.log_analyze=true`);
            await connection.query(sql`SET auto_explain.log_format=json`);
            await connection.query(sql`SET auto_explain.log_min_duration=0`);
            await connection.query(sql`SET auto_explain.log_timing=true`);
            await connection.query(sql`SET client_min_messages=log`);
          },
        },
      ],
    });