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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tilewise/source-postgres

v0.1.2

Published

**Any resource over one Postgres table: every operator in SQL, counted pages, and versioned writes, from the resource's definition alone.**

Readme

🐘 @tilewise/source-postgres

Any resource over one Postgres table: every operator in SQL, counted pages, and versioned writes, from the resource's definition alone.

PostgreSQL SQL sql-template-tag PGlite

Give it a driver and it makes a @tilewise/source source for any definition: each field a column, every operator its kind allows as SQL, each sort in its kind's order, a search across the text kinds, groups counted by Postgres, pages by any skip, and every query's rows counted.

✨ Highlights

  • 🔌 Any driver: it takes (text, values) => rows, so node-postgres, postgres.js, PGlite and Neon all serve.
  • 🛡️ Nothing spliced: every value is a $n parameter, and every name is quoted.
  • 🗺️ Conventions first: a field's column is its key in snake_case, typed by its kind; fields says otherwise only where it must.
  • 🏷️ Options in order: an option sorts by its place, and its attributes (status.kind) filter, sort and group.
  • 🔁 Versioned writes: with roles.versionField, Postgres sets the version on every write, and a stale one answers 409 conflict.
  • 🧱 Its own table: tableOf writes the CREATE TABLE it reads, for a test's database or a first migration.

🚀 Quick start

import pg from 'pg';
import { router, serve } from '@tilewise/source';
import { postgres } from '@tilewise/source-postgres';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const query = async (text: string, values: unknown[]) => {
  return (await pool.query(text, values)).rows;
};

const source = postgres({
  query,
  fields: { owner: { type: 'bigint' } }, // a ref to numbered rows
});

const route = router([serve(tasks, source)]);

The driver is one function. Each one's own:

| Driver | query | | ----------------------- | ---------------------------------------------------------------------- | | 🐘 node-postgres (pg) | async (text, values) => (await pool.query(text, values)).rows | | 🐘 postgres.js | (text, values) => sql.unsafe(text, values) | | 🪶 PGlite | async (text, values) => (await db.query(text, values)).rows | | ⚡ Neon serverless | (text, values) => neon(process.env.DATABASE_URL).query(text, values) |

🔎 What a query becomes

The query a client sends:

await served.query({
  filter: {
    all: [
      { field: 'status', op: 'in', value: ['todo', 'doing'] },
      { field: 'tags', op: 'in', value: ['web'] },
      { field: 'points', op: 'gte', value: 2 },
    ],
  },
  search: 'login',
  sort: [{ field: 'dueDate', dir: 'asc' }],
  page: { skip: 0, take: 20 },
});

…and the SQL the adapter sends for it, the values apart:

SELECT "id" AS "id", "title" AS "title", "status" AS "status", "tags" AS "tags",
       "points" AS "points", to_char("due_date", 'YYYY-MM-DD') AS "dueDate", …
FROM "tasks"
WHERE (coalesce("status" = ANY($1::text[]), false)
   AND coalesce("tags" && $2::text[], false)
   AND coalesce("points" >= $3::double precision, false))
  AND (coalesce(strpos(lower("title"), lower($4)) > 0, false)
   OR EXISTS (SELECT 1 FROM unnest("tags") AS tilewise_each
              WHERE strpos(lower(tilewise_each), lower($5)) > 0))
ORDER BY "due_date" ASC NULLS LAST, "id" ASC
LIMIT $6 OFFSET $7
-- values: [["todo","doing"], ["web"], 2, "login", "login", 20, 0]

A count(*) over the same WHERE answers the page's total.

[!NOTE] Each condition is wrapped in coalesce(…, false), so it answers true or false, never NULL: not turns it over as JavaScript would. Rows without a value sort last both ways, and the id breaks ties, so every order is total.

🗺️ Kinds and their columns

| Kind | Column | Notes | | --------------------------------------- | --------------------------------------- | ------------------------------------- | | text markdown url email phone | text | sorted by its lower case | | richtext | text | the doc's JSON, kept as written | | number | bigint when whole, double precision | | | boolean | boolean | | | date · time | date · time | read as YYYY-MM-DD · HH24:MI | | datetime | timestamptz | read as ISO 8601 in UTC | | option | text | sorted by its place | | ref | text | bigint for numbered rows (fields) | | json · file | jsonb | a list is one jsonb array |

Any other list is an array of its kind's type: text[], bigint[].

🧱 Its table

tableOf(definition, options) writes the CREATE TABLE the source reads:

await db.exec(tableOf(tasks, { fields: { owner: { type: 'bigint' } } }));
CREATE TABLE IF NOT EXISTS "tasks" (
  "id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  "title" text,
  "status" text,
  "tags" text[],
  "points" double precision,
  "due_date" date,
  "owner" bigint,
  "updated_at" timestamptz
)

⚙️ Options

| Option | What | | ----------- | ------------------------------------------------------------------------ | | query | the driver: runs one statement, numbered $1, $2…, and answers rows | | table | the table, a schema's too ('app.tasks'); the resource's name otherwise | | fields | by field key: its column, type, and what a write may do with it | | kinds | the columns of the app's own kinds, beside tilewise's (baseColumns) | | operators | the SQL of the app's own operators, beside tilewise's (baseSql) |

A field's own options say where it differs from the defaults:

postgres({
  query,
  table: 'app.tasks',
  fields: {
    title: { column: 'name', required: true }, // its column, and a write must set it
    owner: { type: 'bigint' }, // a ref to numbered rows
    createdAt: { create: false, update: false }, // the database fills it
  },
});

🔁 Versions

With roles.versionField (a datetime, or a whole number), Postgres sets the version on every write. A write given a stale one changes nothing:

const task = await served.create({ values: { title: 'Ship it' } });

// ✅ at the version it was read at
await served.update(task.id, {
  values: { status: 'done' },
  version: task.updatedAt,
});

// ❌ 409 conflict: that version is stale now
await served.update(task.id, {
  values: { status: 'todo' },
  version: task.updatedAt,
});

🧩 The app's own kinds and operators

An app's own kind brings its column, and its own operator its SQL, beside tilewise's. Register both in the registry you serve with:

import { baseKinds, baseOperators, createRegistry } from '@tilewise/resource';
import { cast, postgres, sql } from '@tilewise/source-postgres';

const registry = createRegistry({
  kinds: baseKinds.register({ color }),
  operators: baseOperators.register({ between }),
});

const source = postgres({
  query,
  kinds: { color: { type: () => 'text' } },
  operators: {
    between: (on, value) => {
      const [low, high] = value as [number, number];
      const from = cast(low, on.type);
      const to = cast(high, on.type);
      return sql`coalesce(${on.column} BETWEEN ${from} AND ${to}, false)`;
    },
  },
});

serve(swatches, source, { registry });

🚫 What it leaves out

[!WARNING]

  • Sorting by a ref, which would order by the title of the row it points at, through a join.
  • A cursor: one made from an offset shifts as rows are added before it.
  • A templated field is read as none; filling it is the server's job.

🧪 Tests

Its tests run on PGlite, a real Postgres in the process: the table made by tableOf, rows written through the resource, and every operator and sort held to the contract with @tilewise/source/testing.

🗂️ Layout

src/
├── index.ts              the package's exports
├── postgres.ts           postgres(options): the factory
├── types/
│   ├── options.ts        what an app gives: the driver, table, fields, kinds, operators
│   └── rows.ts           a row, a page, a write's body
├── table/
│   ├── table.ts          the table a definition is: its columns
│   ├── options.ts        an option field's places and attributes
│   └── table-of.ts       tableOf: the CREATE TABLE
├── columns/
│   └── columns.ts        each kind as a column
├── operators/
│   └── operators.ts      each operator as SQL
├── query/
│   ├── query.ts          the whole query, with its count
│   ├── filter.ts         the filter
│   ├── sort.ts           the sort
│   ├── search.ts         the search
│   ├── groups.ts         the groups, counted by Postgres
│   └── where.ts          the filter and search as one WHERE
├── sql/
│   └── sql.ts            sql, join, raw, ident, cast
├── writes/
│   └── writes.ts         read, create, update, delete
└── postgres.test.ts      the whole adapter on PGlite

🔑 Key files

  • table/table.ts: turns a definition into the table everything else reads: each field's quoted column, its type, how it's read back. The query and the writes are made from this, never from the options again.
  • columns/columns.ts: how each kind lives in Postgres: its column type, how a list of it is kept, and how it's read, sent and ordered where the column alone isn't the contract's value.
  • operators/operators.ts: tilewise's operators as SQL, each holding as matchesFilter says: a list's by any of its values, contains with its case ignored, notEquals holding where there's no value.
  • query/query.ts: puts the filter, search, sort, groups and offset paging together, and counts the rows each query keeps.
  • writes/writes.ts: reads a row by its id, and creates, updates and deletes it, setting the version and telling a stale write from a missing row.
  • sql/sql.ts: the only place SQL text is built: sql-template-tag for fragments and parameters, a quoted name for a schema's dotted table, a cast.

🧱 Built on

| Library | Used for | | ---------------------------------------------------------------------------------- | ----------------------------------------------------- | | sql-template-tag | building and nesting SQL, each value a $n parameter | | @databases/escape-identifier | quoting the table's and the columns' names | | change-case | a field's key as its column's name, in snake_case | | valibot | reading a kind's settings by its schema |