npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@prairielearn/postgres

v2.1.4

Published

Tools for loading and executing Postgres queries.

Downloads

169

Readme

@prairielearn/postgres

Tools for loading and executing Postgres queries.

Usage

Before making any queries, you must initialize the library with your connection details and an error handler:

import sqldb from '@prairielearn/postgres';

function idleErrorHandler(err: any) {
  console.error(err);
  process.exit(1);
}

await sqldb.initAsync(
  {
    user: '...',
    database: '...',
    host: '...',
    password: '...',
    max: 2,
    idleTimeoutMillis: 30000,
    errorOnUnusedParameters: false, // defaults to false
  },
  idleErrorHandler,
);

The options argument accepts any values that the pg.Pool constructor does.

Loading queries from files

The recommended way to write queries is to store them in a .sql file adjacent to the file from which they'll be used. For instance, if we want to make some queries in an index.js file, we can put the following in index.sql:

-- BLOCK select_user
SELECT
  *
FROM
  users
WHERE
  id = $user_id;

-- BLOCK select_course
SELECT
  *
FROM
  courses
WHERE
  id = $course_id;

You can then load these queries in your JavaScript file:

import sqldb from '@prairielearn/postgres';
const sql = sqldb.loadSqlEquiv(import.meta.url);

console.log(sql.select_user);
console.log(sql.select_course);

Making queries

Once you've loaded your SQL, you can use them to query the database:

import sqldb from '@prairielearn/postgres';
const sql = sqldb.loadSqlEquiv(import.meta.url);

const result = await sqldb.queryAsync(sql.select_user, { user_id: '1' });
console.log(result.rows);

The queryAsync function returns a pg.Result object; see linked documentation for a list of additional properties that are available on that object.

There are also utility methods that can make assertions about the results:

  • queryOneRowAsync: Throws an error if the result doesn't have exactly one row.
  • queryZeroOrOneRowAsync: Throws an error if the result has more than one row.

Stored procedures (sprocs)

There are also functions that make it easy to call a stored procedure with a given set of arguments. Consider a database that has the following sproc defined:

CREATE PROCEDURE insert_data (a integer, b integer) LANGUAGE SQL
BEGIN ATOMIC
INSERT INTO
  tbl
VALUES
  (a);

INSERT INTO
  tbl
VALUES
  (b);

END;

You can call this sproc in your JavaScript code:

await sqldb.callAsync('insert_data', [1, 2]);

Zod validation

For increased safety and confidence, you can describe the shape of data you expect from the database with a Zod schema. You can then provide this schema when making a query, and the data returned from the database will be parsed with that schema.

import { z } from 'zod';
import { loadSqlEquiv, queryRows, queryRow, queryOptionalRow } from '@prairielearn/postgres';

const sql = loadSqlEquiv(import.meta.url);

const User = z.object({
  name: z.string(),
  email: z.string(),
  age: z.number(),
});

// Get all users. Returns an array of objects.
const users = await queryRows(sql.select_users, User);

// Get single user. Returns a single object.
const user = await queryRow(sql.select_user, { user_id: '1' }, User);

// Get a user that may not exist. Returns `null` if the user cannot be found.
const maybeUser = await queryOptionalRow(sql.select_user, { user_id: '1' }, User);

// Call a stored procedure. Return value is equivalent to the functions above.
const users = await callRows('select_users', User);
const user = await callRow('select_user', ['1'], User);
const maybeUser = await callOptionalRow('select_user', ['1'], User);

These functions have some behaviors that can make them more convenient to work with:

  • Passing an object or array with parameters is optional.

  • If the query returns a single column, that column is validated and returned directly. For example, consider the following query:

    -- BLOCK select_user_names
    SELECT
      name
    FROM
      users;

    If we then use that query with queryRows, the returned Promise resolves to an array of strings:

    const userNames = await queryRows(sql.select_user_names, z.string());
    
    // Prints something like `["Alice", "Bob"]`.
    console.log(userNames);

There are also a number of legacy functions available, though these are discouraged in new code.

  • queryValidatedRows
  • queryValidatedOneRow
  • queryValidatedZeroOrOneRow
  • queryValidatedSingleColumnRows
  • queryValidatedSingleColumnOneRow
  • queryValidatedSingleColumnZeroOrOneRow
  • callValidatedRows
  • callValidatedOneRow
  • callValidatedZeroOrOneRow

For details on the behavior of these functions, see the source code.

Transactions

To use transactions, wrap your queries with the runInTransactionAsync function:

const { user, course } = await sqldb.runInTransactionAsync(async () => {
  const user = await sqldb.queryAsync(sql.insert_user, { name: 'Kevin Young' });
  const course = await sqldb.queryAsync(sql.insert_course, { rubric: 'CS 101' });
  return { user, course };
});

runInTransaction will start a transaction and then execute the provided function. Any nested query will use the same client and thus run inside the transaction. If the function throws an error, the transaction is rolled back; otherwise, it is committed.

Cursors

For very large queries that don't need to fit in memory all at once, it's possible to use a cursor to read a limited number of rows at a time.

import { queryCursor } from '@prairielearn/postgres';

const cursor = await queryCursor(sql.select_all_users, {});
for await (const users of cursor.iterate(100)) {
  // `users` will have up to 100 rows in it.
  for (const user of users) {
    console.log(user);
  }
}

You can optionally pass a Zod schema to parse and validate each row:

import { z } from 'zod';
import { queryValidatedCursor } from '@prairielearn/postgres';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

const cursor = await queryValidatedCursor(sql.select_all_users, {}, UserSchema);
for await (const users of cursor.iterate(100)) {
  for (const user of users) {
    console.log(user);
  }
}

You can also use cursor.stream(...) to get an object stream, which can be useful for piping it somewhere else:

import { queryCursor } from '@prairielearn/postgres';

const cursor = await queryCursor(sql.select_all_users, {});
cursor.stream(100).pipe(makeStreamSomehow());

Callback-style functions

For most functions that return promises, there are corresponding versions that work with Node-style callbacks:

sqldb.query(sql.select_user, (err, result) => {
  if (err) {
    console.error('Error running query', err);
  } else {
    console.log(result.rows);
  }
});

However, these should be avoided in new code:

  • They make it more difficult to correctly handle errors
  • Callback-style code tends to be more verbose and suffer from "callback hell"