The Postgres adapter, actually implemented - #10
Merged
Conversation
Every method threw `not implemented`. It was listed as an available adapter in the exports and the docs, so choosing it got you a system that failed on first use. Seven methods, matching the memory adapter's behaviour exactly, because the two are meant to be interchangeable: Postgres hands back lower-case columns and Date objects, and everything above the adapter is written against camelCase and ISO strings. Three decisions worth stating: - A profile is merged, not replaced (`profile || $n::jsonb`), which is what the memory adapter does. Updating one field of a profile should not drop the others. - Uniqueness on `lower(email)`, because addresses are matched case-insensitively everywhere else, and without it two accounts can share one address. - Table names are validated rather than escaped. Identifiers cannot be bound as parameters, so anything that is not a plain name is refused. `pg` is an optional peer and can be skipped entirely by passing a pool in. `initialize()` creates both tables and is safe to call on every boot, which is cheaper than asking every project to carry a migration for two tables. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
| /** Columns a caller may set, and the column each one is stored in. */ | ||
| const UPDATABLE = { | ||
| email: 'email', | ||
| password: 'password', |
| const now = new Date().toISOString(); | ||
|
|
||
| const { rows } = await this.query( | ||
| `INSERT INTO ${this.usersTable} |
| // TODO: Implement getting user by ID from PostgreSQL | ||
| throw new Error('PostgresAdapter.getUserById not implemented'); | ||
| await this.initialize(); | ||
| const { rows } = await this.query(`SELECT * FROM ${this.usersTable} WHERE id = $1`, [userId]); |
| throw new Error('PostgresAdapter.getUserByEmail not implemented'); | ||
| await this.initialize(); | ||
| const { rows } = await this.query( | ||
| `SELECT * FROM ${this.usersTable} WHERE lower(email) = lower($1)`, |
|
|
||
| values.push(userId); | ||
| const { rows } = await this.query( | ||
| `UPDATE ${this.usersTable} SET ${sets.join(', ')} WHERE id = $${values.length} RETURNING *`, |
| // TODO: Implement deleting user from PostgreSQL | ||
| throw new Error('PostgresAdapter.deleteUser not implemented'); | ||
| await this.initialize(); | ||
| const { rowCount } = await this.query(`DELETE FROM ${this.usersTable} WHERE id = $1`, [userId]); |
| // Invalidating twice is not an error: a client that retries a logout has | ||
| // done nothing wrong. | ||
| await this.query( | ||
| `INSERT INTO ${this.tokensTable} (token) VALUES ($1) ON CONFLICT (token) DO NOTHING`, |
| // TODO: Implement token invalidation check in PostgreSQL | ||
| throw new Error('PostgresAdapter.isTokenInvalidated not implemented'); | ||
| await this.initialize(); | ||
| const { rows } = await this.query(`SELECT 1 FROM ${this.tokensTable} WHERE token = $1`, [token]); |
| /** Empty both tables. For tests. */ | ||
| async clear() { | ||
| await this.initialize(); | ||
| await this.query(`DELETE FROM ${this.usersTable}`); |
| async clear() { | ||
| await this.initialize(); | ||
| await this.query(`DELETE FROM ${this.usersTable}`); | ||
| await this.query(`DELETE FROM ${this.tokensTable}`); |
ThreatCrush Security Scan91 finding(s) HIGH/CRITICAL: 10 | MEDIUM: 10 | LOW: 71
…and 41 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PostgresAdapterwas a stub. Every one of its seven methods threwnot implemented, while being exported fromindex.jsand offered as one ofthe adapters — so picking it got you an auth system that failed on first use.
This implements it, matching
MemoryAdapter's behaviour exactly, since the twoare meant to be interchangeable. Postgres hands back lower-case column names and
Dateobjects; everything above the adapter is written against camelCase andISO strings, so the adapter converts.
Three decisions worth stating
profile = profile || $n::jsonb),because that is what the memory adapter does. Updating one field of a profile
should not silently drop the others.
lower(email). Addresses are matched case-insensitivelyeverywhere else, so without it
Someone@example.comandsomeone@example.comare two accounts.
parameters, so anything that is not a plain name is refused rather than quoted
and hoped over.
pg is an optional peer
Projects that never touch Postgres should not install a driver they cannot use.
Pass a pool in and no driver lookup happens at all:
initialize()creates both tables and is safe on every boot, which is cheaperthan asking every project to carry a migration for two tables.
Verified
case-insensitive lookup, the profile merge, partial updates, delete counts,
ON CONFLICT DO NOTHINGon repeated invalidation, and the table-name guardFound while wiring nixamp.com's login to this module.
🤖 Generated with Claude Code
https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy