{"id":26901626,"url":"https://github.com/sluukkonen/possu","last_synced_at":"2025-04-01T08:54:49.328Z","repository":{"id":37047436,"uuid":"301143153","full_name":"sluukkonen/possu","owner":"sluukkonen","description":"🐖 A small companion library for node-postgres ","archived":false,"fork":false,"pushed_at":"2024-09-14T05:02:19.000Z","size":1579,"stargazers_count":20,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-09-15T15:42:23.381Z","etag":null,"topics":["javascript","node-postgres","postgresql","tagged-template-literals","transaction-handling","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sluukkonen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-04T14:00:27.000Z","updated_at":"2024-09-14T05:02:21.000Z","dependencies_parsed_at":"2023-01-31T18:31:59.042Z","dependency_job_id":"85175051-a705-4e51-b8d9-a95244744f0d","html_url":"https://github.com/sluukkonen/possu","commit_stats":{"total_commits":583,"total_committers":5,"mean_commits":116.6,"dds":0.6072041166380788,"last_synced_commit":"84433e32acbc71d2c3a03a07375428dd8d814d33"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sluukkonen%2Fpossu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sluukkonen%2Fpossu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sluukkonen%2Fpossu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sluukkonen%2Fpossu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sluukkonen","download_url":"https://codeload.github.com/sluukkonen/possu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246612494,"owners_count":20805354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","node-postgres","postgresql","tagged-template-literals","transaction-handling","typescript"],"created_at":"2025-04-01T08:54:48.747Z","updated_at":"2025-04-01T08:54:49.322Z","avatar_url":"https://github.com/sluukkonen.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Possu 🐖\n\n[![CI](https://github.com/sluukkonen/possu/workflows/CI/badge.svg)](https://github.com/sluukkonen/possu/actions?query=workflow%3ACI)\n![License](https://img.shields.io/npm/l/possu)\n[![NPM](https://img.shields.io/npm/v/possu)](https://www.npmjs.com/package/possu)\n\nA small companion library for [node-postgres](https://node-postgres.com/).\n\n## Features \u0026 Goals\n\n- A Promise-based API, which aims to reduce boilerplate code\n- Write raw SQL queries with tagged template strings\n- Prevent most types of accidental SQL injection vulnerabilities\n- Transaction and savepoint handling, including retrying in case of\n  serialization failures and deadlocks.\n- First-class TypeScript support\n- Not a framework. Most Possu functions take either a\n  [pg.Pool](https://node-postgres.com/api/pool) or a\n  [pg.PoolClient](https://node-postgres.com/api/client) as an argument, so you\n  can integrate Possu easily to an existing application.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Getting started](#getting-started)\n- [API](#api)\n  - [Building queries](#building-queries)\n    - [sql](#sql)\n    - [sql.identifier](#user-content-sqlidentifier)\n    - [sql.json](#user-content-sqljson)\n  - [Executing queries](#executing-queries)\n    - [query](#query)\n    - [queryOne](#queryone)\n    - [queryMaybeOne](#querymaybeone)\n    - [execute](#execute)\n    - [executeOne](#executeone)\n    - [executeMaybeOne](#executemaybeone)\n  - [Transaction handling](#transaction-handling)\n    - [withTransaction](#withTransaction)\n    - [withSavepoint](#withSavepoint)\n\n## Installation\n\nRun either\n\n```shell\n$ npm install possu\n```\n\nor\n\n```shell\n$ yarn add possu\n```\n\ndepending on your favourite package manager.\n\n## Getting started\n\nIf you've ever written an application using\n[node-postgres](https://node-postgres.com/), a lot of your database code\nmight look a bit like this:\n\n```typescript\nasync function getUser(tx, userId) {\n  const result = await tx.query('SELECT * FROM users WHERE user_id = $1', [\n    userId,\n  ])\n  return result.rows[0]\n}\n```\n\nIn addition to the SQL query, there is some boilerplate code that selects the\ncorrect amount of rows from the query result. In a large application, this\ncan get quite repetetive. Things can even more complicated if you're only\ninterested in a single column from the result set.\n\n```typescript\nasync function getUserNames(tx) {\n  const result = await tx.query('SELECT name FROM users')\n  return result.rows.map((row) =\u003e row.name)\n}\n```\n\nThe goal of Possu is to eliminate this kind of boilerplate code from your\napplication.\n\n```typescript\nimport { query, queryMaybeOne, sql } from 'possu'\n\nfunction getUser(tx, userId) {\n  return queryMaybeOne(tx, sql`SELECT * FROM users WHERE user_id = ${userId}`)\n}\n\nfunction getUserNames(tx) {\n  return query(tx, sql`SELECT name FROM users`)\n}\n```\n\nHere we use Possu's `sql` tagged template literal for constructing the\nqueries, while `query` and `queryMaybeOne` functions contain the necessary\ncode for selecting the correct amount of rows from the result set.\n\nIn the `getUserNames` function, possu automatically unwraps the `name` column\nfrom each row, since in most cases, an extra object wrapper in the results of\na single-column query is just extra noise.\n\nThat's it! This was not an exhaustive tour of Possu, but it should be enough\nto get an idea of its main features.\n\n## API\n\n### Building queries\n\n#### sql\n\n\u003c!-- prettier-ignore-start --\u003e\n```typescript\n(parts: TemplateStringsArray, ...values: unknown[]) =\u003e SqlQuery\n```\n\u003c!-- prettier-ignore-end --\u003e\n\nCreate an SQL query.\n\nThis is the only way to create queries in Possu. To prevent accidental SQL injections, other\nPossu functions check at runtime that the query has been created with `sql`.\n\n**Example:**\n\n```typescript\nconst query = sql`SELECT * FROM users WHERE user_id = ${1}`\n// =\u003e SqlQuery { text: 'SELECT * FROM users WHERE user_id = $1', values: [1] }\n```\n\nQueries may be nested within other queries, which can be a powerful mechanism for code reuse.\n\n```typescript\nconst usersQuery = sql`SELECT * FROM users WHERE user_id = ${1}`\nconst existsQuery = sql`SELECT exists(${usersQuery})`\n// =\u003e SqlQuery { text: 'SELECT exists(SELECT * FROM users WHERE user_id = $1)', values: [1] }\n```\n\nNested queries can also be used to customize parts of a query without having to worry about SQL injections.\n\n```typescript\nconst order = 'asc'\nconst query = sql`SELECT * FROM users ORDER BY name ${\n  order === 'asc' ? sql`ASC` : sql`DESC`\n}`\n// =\u003e SqlQuery { text: 'SELECT * FROM users ORDER BY name ASC', values: [] }\n```\n\nCalling the `.prepare()` method on a query causes it be executed as a prepared statement.\n\nThis can sometimes have measurable performance benefits, especially if the query is very complex to parse and plan.\n\nSee the [PostgreSQL manual](https://www.postgresql.org/docs/current/sql-prepare.html)\nfor more information.\n\n```typescript\nsql`SELECT * FROM users`.prepare('fetch-users')\n// =\u003e SqlQuery { text: 'SELECT * FROM users', values: [], name: 'fetch-users' }\n```\n\n---\n\n#### sql.identifier\n\n\u003c!-- prettier-ignore-start --\u003e\n```typescript\n(name: string) =\u003e Identifier\n```\n\u003c!-- prettier-ignore-end --\u003e\n\nEscape an SQL\n[identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)\nto be used in a query. It can be used to create queries which are\nparametrized by table or column names.\n\n**Example:**\n\n```typescript\nsql`SELECT * FROM ${sql.identifier('users')}`\n// =\u003e SqlQuery { text: 'SELECT * FROM \"users\"', values: [] }\n```\n\n```typescript\nsql`SELECT * FROM users ORDER BY ${sql.identifier('name')} DESC`\n// =\u003e SqlQuery { text: 'SELECT * FROM users ORDER BY \"name\" DESC', values: [] }\n```\n\n---\n\n#### sql.json\n\n\u003c!-- prettier-ignore-start --\u003e\n```typescript\n(value: unknown) =\u003e string\n```\n\u003c!-- prettier-ignore-end --\u003e\n\nSerialize a value as JSON to be used in a query.\n\n**Example:**\n\n```typescript\nsql`SELECT * FROM jsonb_array_elements(${sql.json([1, 2, 3])})`\n// =\u003e SqlQuery { text : 'SELECT * FROM jsonb_array_elements($1)', values: ['[1,2,3]'] }\n```\n\n---\n\n### Executing queries\n\nEach of the query functions take a connection pool or a client checked out of\nthe pool as the first argument.\n\nFor queries that return result rows, you may also supply an optional row\nparser, which can validate and transform the value of each row. This\ncan be useful when combined with a library like\n[io-ts](https://github.com/gcanti/io-ts) or\n[runtypes](https://github.com/pelotom/runtypes).\n\nWhen using TypeScript, the type of each result row is `unknown` by default,\nso you must either cast the result to the correct type or to use a row\nparser that helps the TypeScript compiler infer the correct result type.\n\n```typescript\nimport { Record, Number, String } from 'runtypes'\n\nconst result = await query\u003cstring\u003e(db, sql`SELECT name FROM users`)\n// Type inferred to string[]\n\nconst User = Record({\n  id: Number,\n  name: String,\n})\n\nconst users = await query(db, sql`SELECT * FROM users`, User.check)\n// Type inferred to [{ id: number, name: string }]\n```\n\nAs an additional TypeScript helper, possu exports a `Connection` type, which can be used in your own query functions as\na generic connection parameter. It is a type alias for `pg.Pool | pg.PoolClient`.\n\n```typescript\nimport { Connection, query, sql } from 'possu'\n\nexport function getUsers(conn: Connection) {\n  return query(conn, sql`SELECT * FROM users`)\n}\n```\n\nFor actions that must be performed within a transaction, Possu also provides a `Transaction` type, which is just a\nregular `pg.PoolClient` with a type-level brand. Using it is completely optional, but it may improve the readability and\ntype-safety of your code.\n\n```typescript\nimport { Transaction, query, sql } from 'possu'\n\nexport async function insertTwoUsers(tx: Transaction) {\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('Alice')`)\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('Bob')`)\n}\n```\n\n#### query\n\n```typescript\n\u003cT\u003e(connection: Connection, query: SqlQuery, rowParser?: (row: unknown) =\u003e T) =\u003e Promise\u003cT[]\u003e\n```\n\nExecute a `SELECT` or other query that returns zero or more rows. Returns all rows.\n\n**Example:**\n\n```typescript\nconst users = await query(db, sql`SELECT * FROM users`)\n// =\u003e [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]\n```\n\nIf selecting a single column, each result row is unwrapped automatically.\n\n```typescript\nconst names = await query(db, sql`SELECT name FROM users`)\n// =\u003e ['Alice', 'Bob']\n```\n\n---\n\n#### queryOne\n\n```typescript\n\u003cT\u003e(connection: Connection, query: SqlQuery, rowParser?: (row: unknown) =\u003e T) =\u003e Promise\u003cT\u003e\n```\n\nExecute a `SELECT` or other query that returns exactly one row. Returns the first row.\n\n- Throws a `ResultError` if query doesn't return exactly one row.\n\n**Example:**\n\n```typescript\nconst user = await queryOne(db, sql`SELECT * FROM users WHERE id = 1`)\n// =\u003e { id: 1, name: 'Alice' }\n```\n\nIf selecting a single column, it is unwrapped automatically.\n\n```typescript\nconst name = await queryOne(db, sql`SELECT name FROM users WHERE id = 1`)\n// =\u003e 'Alice'\n```\n\nYou can transform the result with a custom row parser. Here we transform the\ncount from a string to a number by using the built-in Number constructor.\n\n```typescript\nconst count = await queryOne(db, sql`SELECT count(*) FROM users`, Number)\n// =\u003e 3\n```\n\n---\n\n#### queryMaybeOne\n\n```typescript\n\u003cT\u003e(connection: Connection, query: SqlQuery, rowParser?: (row: unknown) =\u003e T) =\u003e Promise\u003cT | undefined\u003e\n```\n\nExecute a `SELECT` or other query that returns zero or one rows. Returns the first row or `undefined`.\n\n- Throws a `ResultError` if query returns more than 1 row.\n\n**Example:**\n\n```typescript\nconst user = await queryMaybeOne(db, sql`SELECT * FROM users WHERE id = 1`)\n// =\u003e { id: 1, name: 'Alice' }\n\nconst nil = await queryMaybeOne(db, sql`SELECT * FROM users WHERE false`)\n// =\u003e undefined\n```\n\nIf selecting a single column, it is unwrapped automatically.\n\n```typescript\nconst name = await queryMaybeOne(db, sql`SELECT name FROM users WHERE id = 1`)\n// =\u003e 'Alice'\n```\n\n---\n\n#### execute\n\n```typescript\n(connection: Connection, query: SqlQuery) =\u003e Promise\u003cnumber\u003e\n```\n\nExecute an `INSERT`, `UPDATE`, `DELETE` or other query that is not expected to return any rows. Returns the number of\nrows affected.\n\n**Example:**\n\n```typescript\nconst rowCount = await execute(db, sql`INSERT INTO users (name) VALUES ('Eve')`)\n// =\u003e 1\n```\n\n---\n\n#### executeOne\n\n```typescript\n(tx: Transaction, query: SqlQuery) =\u003e Promise\u003cnumber\u003e\n```\n\nExecute an `INSERT`, `UPDATE`, `DELETE` or other query that affects exactly one row. Returns the number of rows\naffected (1).\n\n- Throws a `ResultError` if the query doesn't affect exactly one row. \n- Unlike [`execute`](#execute), it must be called within an explicit transaction, so the changes can be rolled back.\n\n**Example:**\n\n```typescript\nawait withTransaction(db, (tx) =\u003e {\n  return executeOne(tx, sql`UPDATE users SET name = 'Bob' WHERE id = 1`)\n})\n// =\u003e 1\n```\n\n---\n\n#### executeMaybeOne\n\n```typescript\n(tx: Transaction, query: SqlQuery) =\u003e Promise\u003cnumber\u003e\n```\n\nExecute an `INSERT`, `UPDATE`, `DELETE` or other query that affects zero or one rows. Returns the number of\nrows affected.\n\n- Throws a `ResultError` if the query affects more than one row. \n- Unlike [`execute`](#execute), it must be called within an explicit transaction, so the changes can be rolled back.\n\n**Example:**\n\n```typescript\nawait withTransaction(db, (tx) =\u003e {\n  return executeMaybeOne(tx, sql`UPDATE users SET name = 'Bob' WHERE id = 1`)\n})\n// =\u003e 1\n```\n\n### Transaction handling\n\n#### withTransaction\n\n```typescript\n\u003cT\u003e(pool: pg.Pool, queries: (tx: Transaction) =\u003e PromiseLike\u003cT\u003e, options?: TransactionOptions) =\u003e Promise\u003cT\u003e\n```\n\nExecute a set of queries within a transaction.\n\nStart a transaction and execute a set of queries within it. If the function\ndoes not throw an error, the transaction is committed.\n\nIf the function throws a non-retryable error, the transaction is rolled back\nand the error is rethrown.\n\nIf the function throws a retryable error, the transaction is rolled back and\nretried up to 2 or `maxRetries` times. By default, PostgreSQL errors codes\n`40001` (serialization failure) and `40P01` (deadlock detected) are\nconsidered to be retryable, but you may customize the behavior by supplying a\ncustom `shouldRetry` predicate.\n\nYou may also configure the [access\nmode](https://www.postgresql.org/docs/current/sql-set-transaction.html) and\n[isolation\nlevel](https://www.postgresql.org/docs/current/transaction-iso.html) of the\ntransaction by supplying the `accessMode` and `isolationLevel` options,\nrespectively.\n\n**Example:**\n\n```typescript\nconst userCount = await withTransaction(db, async (tx) =\u003e {\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('${'Alice'}')`)\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('${'Bob'}')`)\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('${'Charlie'}')`)\n  return queryOne(tx, sql`SELECT count(*) FROM users`, Number)\n})\n```\n\n---\n\n#### withSavepoint\n\n```typescript\n\u003cT\u003e(tx: Transaction, queries: (tx: Transaction) =\u003e PromiseLike\u003cT\u003e) =\u003e Promise\u003cT\u003e\n```\n\nExecute a set of queries within a [savepoint](https://www.postgresql.org/docs/current/sql-savepoint.html).\n\nStart a savepoint and execute a set of queries within it. If the function\ndoes not throw an error, the savepoint is released.\n\nIf the function throws any kind of error, the savepoint is rolled back and\nthe error is rethrown.\n\nMay only be used within a transaction.\n\n**Example:**\n\n```typescript\nawait withTransaction(db, async (tx) =\u003e {\n  await execute(tx, sql`INSERT INTO users (name) VALUES ('Alice')`)\n  return withSavepoint(tx, async (tx) =\u003e {\n    await execute(tx, sql`INSERT INTO users (name) VALUES ('Bob')`)\n    await execute(tx, sql`INSERT INTO users (name) VALUES ('Charlie')`)\n  }).catch((err) =\u003e {\n    // Let the first insert to through if the second or third one fail.\n  })\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsluukkonen%2Fpossu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsluukkonen%2Fpossu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsluukkonen%2Fpossu/lists"}