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

@entitle/store-sql

v0.2.0

Published

Dialect-independent internals shared by Entitle's SQL PolicyStore backends

Readme

@entitle/store-sql

Everything @entitle/store-sqlite and @entitle/store-postgres agree on, in one place.

This package is not a store. It implements no PolicyStore, opens no connection, holds no SQL a dialect would recognise as a whole statement, and has no factory of its own. It owns the row shapes, the row-to-domain converters, the identifier check, the table-name derivation, the migration ledger and -- since #41 -- the statements over the append-only assignment log, rendered through a dialect descriptor. Those are the parts of two SQL backends that are the same because the schema is the same, not because the dialect is.

Published, and not for you

It is on the registry, and that is a mechanical consequence rather than an invitation: @entitle/store-sqlite and @entitle/store-postgres require it at runtime, and a dependency of a published package has to be installable. There is no version of "keep it private and bundle it into both stores" that does not either declare a runtime import as a devDependency -- which the packaging invariants forbid, for the reason that the declaration is what makes a consumer's install correct -- or fork the shared code back into two copies inside two tarballs, which is the thing this package exists to stop.

So: it is public in the sense that npm can install it, and internal in the sense that its surface is decided by what those two stores need. It ships at the same version as every other Entitle package and moves with them. Depend on it directly and you are depending on an implementation detail; nothing here is a promise to a third party.

If you are writing your own PolicyStore, the thing to reach for is the contract and its executable specification -- PolicyStore and policyStoreConformanceCases from @entitle/core/conformance -- not this.

What it owns

| Export | What it is | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | validateIdentifier | The one regex a table prefix or schema name has to match before it is interpolated into a statement | | TABLE_SUFFIXES, coreTableNames | The tables an Entitle schema has, rendered by a dialect-supplied render | | AssignmentRow, HistoryRow, EventRow, LastEventRow, OverrideRow, PolicyIdRow | The row shapes both schemas read back | | AssignmentColumns, AssignmentKeyColumns, assignmentColumns | One assignment as the write path holds it, and the serialisation that gets it there | | OverrideColumns, overrideColumns | One override as the write path holds it -- nine columns named rather than counted, with #37's limits normalisation | | assignmentRowKey, rowToAssignment, rowToHistoryEntry, rowToOverride | Row to domain, with every fail-closed guard the contract requires | | overrideMapFromEntries, overrideMapFromRows | Override rows as an OverrideMap, built so a feature named __proto__ cannot corrupt it | | schemaVersion, pendingMigrations, storedSchemaVersion, migrationLedgerSql | The migration ledger: the version is the migration count, the marker is rewritten, the pending set is [current, count) | | assignmentsBySubject, overridesBySubject | The scaffolding around a batch read, taking the dialect's statement as a callback | | SqlDialect, createBinder, keysetPredicate, ascendingOrder | The rendering seam: placeholders and text collation, a binder that hands out a placeholder as it records the value, and the keyset predicate built from the same key list as the ORDER BY | | historyPageSql, listAssignmentsPageSql, lastEventForKeySql, insertEventSql, pruneHistorySql | The five statements over the assignment log and the paginated holder read | | EVENT_TYPES, eventTypeCheckList, EVENT_COLUMN_LIST, eventValues, EventColumns | One log event as columns, and the CHECK list rendered from the same array as the TypeScript union | | rowToAssignmentEvent, pageRowToHistoryEntry, lastEventForKey, toPage, historyCursorForRow, listCursorForRow | Log row to domain, and the page-plus-cursor a paginated read answers with |

What it deliberately does not own, and why

The issue that created this package proposed going further: a SqlDriver with query, transaction and placeholder, against which the PolicyStore method bodies would live once. That is not what was built, and the reason is a property of one of the two drivers rather than a preference.

better-sqlite3 is synchronous, and that is load-bearing twice over. createSqliteStore is a synchronous factory, so its migration path cannot await anything; and db.transaction(fn) requires fn to be synchronous, because what makes the SQLite store's multi-statement writes atomic is that no other caller can run at all between the first statement and the commit. A shared async body doing await tx.query(a); await tx.query(b) inside a transaction would give the event loop a turn in the middle of one, at which point a second addAssignment on the same connection reaches BEGIN inside an open transaction. The store's own concurrency suite states the property this would destroy: two handles in one thread take their locks strictly one after the other.

An all-Promise driver therefore costs either the synchronous factory -- a breaking change to a published API -- or the write atomicity that was the whole of one of the fixes this extraction was sequenced after. Both are behaviour changes, and the brief for this work was to move code that is already correct.

A dialect is not a driver, and that is where #41 pushed the line. The statements that issue added -- a self-anti-join for the history page, a lexicographic OR-chain for the keyset, a correlated EXISTS for the prune -- are the first in this repository that are hard to read rather than merely long, and each has a wrong version one character away that is silent in exactly one store. So they are rendered once, from a SqlDialect that says only how the dialect spells a bound parameter and what it appends to a text ordering key. That has no execution model in it: the text is shared, and each store still owns its connection, its transaction shape and its prepared-statement cache.

So the line is drawn where it can be drawn without paying either: the pure and the decodable are shared, the execution model is not. Each store keeps

  • its DDL and its migration bodies, which are transliterations rather than copies -- one dialect rebuilds a table to add a constraint, the other alters a column's type; one holds an instant as constrained TEXT, the other as timestamptz;
  • its runMigrations, which is a BEGIN IMMEDIATE transaction on one side and a session advisory lock on a dedicated pooled client on the other, chosen separately and for reasons written down in each;
  • its statements and its write orchestration, including the batch reads' fast paths, which are an unnest array match against a prepared-per-arity row-value IN;
  • its backend-specific refusals, such as the year-0000 instant that ISO 8601 has and PostgreSQL's calendar does not.

A later change that makes the two execution models genuinely one -- an async SQLite factory, say -- is what would make the driver seam cheap. Until then a seam that forces them together buys line count and sells a durability guarantee.