@nxgt/janus-drizzle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # @nxgt/janus-drizzle
2
+
3
+ The PostgreSQL adapter for [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus):
4
+ its identity stores (users, sessions and one-time tokens) and the relation
5
+ store of `@nxgt/janus/permissions`, over one database. It works through your
6
+ [Drizzle](https://orm.drizzle.team) instance and uses
7
+ [`@nxgt/drizzle`](https://www.npmjs.com/package/@nxgt/drizzle) for
8
+ transactions and error translation.
9
+
10
+ It passes both `@nxgt/janus/conformance` suites on a real PostgreSQL 17 —
11
+ over `node-postgres`, postgres.js and Bun's `SQL`, outages and concurrency
12
+ included — and on PGlite. `createDrizzleAdapter(db)`
13
+ returns both sides under the names `janus()` takes, so one spread wires them.
14
+ Each side also works alone, as in `@nxgt/janus`: `createDrizzleStores` for
15
+ identities, `createDrizzleRelations` for permissions.
16
+
17
+ ```ts
18
+ // src/janus/schema.ts — the file drizzle-kit reads
19
+ import { defineJanusTables } from '@nxgt/janus-drizzle';
20
+
21
+ export const { users, logins, sessions, tokens, relations } = defineJanusTables();
22
+ ```
23
+
24
+ ```ts
25
+ import { janus, scryptHasher } from '@nxgt/janus';
26
+ import { permissions } from '@nxgt/janus/permissions';
27
+ import { createDrizzleAdapter } from '@nxgt/janus-drizzle';
28
+ import { drizzle } from 'drizzle-orm/node-postgres';
29
+ import { model, User } from './model'; // your user schema and permission model
30
+
31
+ // Janus's own database: backed up and restored on its own
32
+ const db = drizzle(process.env.JANUS_DATABASE_URL ?? 'postgres://localhost:5432/janus');
33
+ const postgres = createDrizzleAdapter(db); // { store, relations }
34
+
35
+ export const auth = janus({
36
+ user: User,
37
+ password: { login: 'email' },
38
+ hasher: scryptHasher(),
39
+ ...postgres, // deleting a user deletes their tuples too
40
+ });
41
+
42
+ export const access = permissions({ model, store: postgres.relations });
43
+ ```
44
+
45
+ `drizzle-kit generate` then writes the five tables into a migration, the same
46
+ way it writes your own. **Give them a database of their own**, as here, with a
47
+ drizzle-kit config of its own: `pg_dump` and `pg_restore` then back up and
48
+ restore Janus alone. Beside your tables in one database, put them in a
49
+ PostgreSQL schema of their own instead, exported with the tables and passed
50
+ to the adapter as `{ tables }`:
51
+ [Where the tables live](docs/guide/database.md) has both.
52
+
53
+ > **0.x.** A minor version may still change the surface; the changelog says how.
54
+
55
+ ## Install
56
+
57
+ ```sh
58
+ bun add @nxgt/janus-drizzle @nxgt/janus @nxgt/drizzle drizzle-orm
59
+ bun add pg # the driver the example below uses — or postgres, or none on Bun's SQL
60
+ ```
61
+
62
+ Every peer is required:
63
+ - `@nxgt/janus`;
64
+ - `@nxgt/drizzle`, `>=0.6.1 <1`;
65
+ - `drizzle-orm`, 1.0 (from `1.0.0-rc.4`);
66
+ - `typescript` 6.
67
+
68
+ `@nxgt/janus` is a **peer**, never a dependency. This package defines no error
69
+ class and throws the peer's own, so `instanceof StoreFailure` holds in your
70
+ code.
71
+
72
+ It needs **PostgreSQL 15 or later**, because the tuple constraint is
73
+ `unique nulls not distinct`.
74
+
75
+ Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`. The
76
+ declarations import without extensions, so `nodenext` is not supported.
77
+
78
+ ## API
79
+
80
+ | Export | What it is |
81
+ | --- | --- |
82
+ | `createDrizzleAdapter(db, options?)` | Returns `{ store, relations }`, both of the entries below, keyed as `janus()` takes them, so `janus({ …, ...postgres })` wires both. It connects to nothing and creates nothing. `options.tables` is what your schema file's `defineJanusTables(…)` returned. |
83
+ | `DrizzleAdapter` | The type of what `createDrizzleAdapter` returns. |
84
+ | `createDrizzleStores(db, options?)` | The `{ users, sessions, tokens }` that `janus()` takes as `store`. |
85
+ | `createDrizzleRelations(db, options?)` | The `RelationStore` that `permissions()` takes as `store`, and `janus()` takes as `relations`. |
86
+ | `defineJanusTables(options?)` | The five tables, `{ users, logins, sessions, tokens, relations }`, as Drizzle tables. Export each from a schema file so your migrations create them. |
87
+ | `JanusTablesOptions` | `{ schema? }`: a `pgSchema(…)` to put the tables in. Absent, they are in the connection's `search_path`, `public` by default. |
88
+ | `DrizzleAdapterOptions` | `{ tables? }`: the tables the stores query. Absent, `defineJanusTables()`. Pass it whenever the tables are in a schema of their own. |
89
+ | `JanusTables` | The type of what `defineJanusTables` returns. |
90
+
91
+ `db` is any Drizzle PostgreSQL instance: `node-postgres`, `postgres.js`,
92
+ `bun-sql` or PGlite. A connection string, a raw driver client or a SQLite
93
+ Drizzle does not compile.
94
+
95
+ ## What the database holds
96
+
97
+ The port's records, one column per field. Nothing is encoded, so a row read in
98
+ `psql` looks like the record in the code. No name carries a prefix: the
99
+ database, or the PostgreSQL schema, is what keeps them apart from your own
100
+ tables.
101
+
102
+ | Table | Keys and indexes |
103
+ | --- | --- |
104
+ | `users` | `id` · `(id, type)` unique, for the logins' foreign key · `(type, id)` for listing · a check that a password has both its hash and its date, or neither |
105
+ | `logins` | primary key `(type, login)`: **a login is unique per user type** · `(user_id, type)` references the user, `on delete cascade` |
106
+ | `sessions` | `id` · `token_hash` unique · `user_id` · `expires_at`, for `collectExpired()` |
107
+ | `tokens` | `token_hash` · `user_id` · a check on `kind` |
108
+ | `relations` | one row per tuple, unique `nulls not distinct` over all six columns, subject first: the index `findObjects` pages · `(object_type, object_id, relation)` for one hop forwards |
109
+
110
+ `logins` is also a `text[]` on `users`, which the store reads back in order.
111
+ PostgreSQL has no unique index over the elements of an array, so the `logins`
112
+ table plays that role, written in the same transaction as the user.
113
+
114
+ Every key column is `text collate "C"`, compared byte for byte as the port
115
+ requires. No secret is stored: sessions and tokens hold `sha256` of the secret,
116
+ and passwords a self-describing hash.
117
+
118
+ ## Traps
119
+
120
+ - **Your migrations create the tables, not this package.** The core never
121
+ manages a schema, and the stores create nothing per request. Without the
122
+ exports in your schema file, the first sign-up fails with `STORE_FAILED`,
123
+ caused by `relation "users" does not exist`.
124
+ - **Keep all five exports in the schema file.** drizzle-kit writes a
125
+ `DROP TABLE` for a table the schema no longer names.
126
+ - **With a PostgreSQL schema, export it, and pass the adapter its tables.**
127
+ drizzle-kit writes `CREATE SCHEMA` only for an exported `pgSchema`. A
128
+ factory not given `{ tables }` queries `public`: if your application has a
129
+ `users` table there, deleting a user through the store deletes from yours.
130
+ `createDrizzleAdapter(db, { tables: janusTables })`, with the object your
131
+ schema file exports.
132
+ - **Call `auth.collectExpired()` on a schedule.** PostgreSQL has no TTL, so
133
+ lapsed sessions stay in `sessions` until something deletes them. The
134
+ core still refuses them on every read.
135
+ - **A unique violation on anything but a login is a `StoreFailure`, not
136
+ `LOGIN_TAKEN`.** Such a violation is an adapter bug. Reporting it as a taken
137
+ login would tell somebody their e-mail is in use when it is not.
138
+ - **A user's write and a relation write of more than one tuple are
139
+ transactions**, through `@nxgt/drizzle`'s `withTransaction`. They are not
140
+ retried for you. A sign-up and a relation write are idempotent, so retry
141
+ them; an update after a timeout answers `VERSION_CONFLICT` if the first
142
+ attempt landed, so read the user again.
143
+ - **PostgreSQL stores no NUL character, and a user's fields never hold one.**
144
+ `@nxgt/janus` refuses `\u0000` and a lone surrogate in fields with
145
+ `USER_INVALID`, and answers a login holding one as nobody's, before any
146
+ store is asked.
147
+ - **Columns are `snake_case`.** This follows PostgreSQL's own convention and
148
+ `@nxgt/drizzle`'s columns, so hand-written SQL needs no quotes. The records
149
+ the stores return are camelCase, as everywhere in Janus.
150
+
151
+ ## Documentation
152
+
153
+ - [Guides](docs/README.md): wiring the stores, where the tables live and how to back them up, and the tables in your migrations
154
+ - [Troubleshooting](docs/troubleshooting.md): look up the error message you see
155
+ - [Roadmap](docs/roadmap.md): what is next, and what is not planned
156
+
157
+ ## Type safety, counted
158
+
159
+ Six plausible mistakes are refused by the compiler, each with a
160
+ `@ts-expect-error` case in `test/types/adapter.ts`:
161
+ - a connection string instead of a Drizzle instance;
162
+ - the driver's client instead of the Drizzle instance over it;
163
+ - a Drizzle instance over SQLite;
164
+ - a schema's name, `'janus'`, instead of the `pgSchema` the schema file
165
+ exports;
166
+ - `{ schema }` given to a factory, instead of the tables built in it;
167
+ - the tables given bare, instead of as `{ tables }`.
168
+
169
+ ## Licence
170
+
171
+ MIT
@@ -0,0 +1,29 @@
1
+ import type { PgDatabase } from '@nxgt/drizzle/pg';
2
+ import type { JanusStores } from '@nxgt/janus';
3
+ import type { RelationStore } from '@nxgt/janus/permissions';
4
+ import type { DrizzleAdapterOptions } from './tables';
5
+ /** The whole adapter over one database, keyed as `janus()` takes it. */
6
+ export interface DrizzleAdapter {
7
+ /** The identity stores: `createDrizzleStores(db)`. */
8
+ readonly store: JanusStores;
9
+ /** The relation store: `createDrizzleRelations(db)`. */
10
+ readonly relations: RelationStore;
11
+ }
12
+ /**
13
+ * The identity stores and the relation store over one database, under the
14
+ * names `janus()` takes them — so one spread wires both, and deleting a user
15
+ * deletes every tuple naming them:
16
+ *
17
+ * ```ts
18
+ * const postgres = createDrizzleAdapter(db);
19
+ * const auth = janus({ user, password: { login: 'email' }, hasher, ...postgres });
20
+ * const access = permissions({ model, store: postgres.relations });
21
+ * ```
22
+ *
23
+ * It connects to nothing and creates nothing: `db` is your Drizzle instance,
24
+ * and the tables are your migrations'. `{ tables }` is what your schema file
25
+ * exports, needed when they are in a PostgreSQL schema of their own. An
26
+ * application that only authenticates can take `createDrizzleStores(db)` alone.
27
+ */
28
+ export declare function createDrizzleAdapter(db: PgDatabase, options?: DrizzleAdapterOptions): DrizzleAdapter;
29
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;CAClC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,UAAU,EACd,OAAO,GAAE,qBAA0B,GACjC,cAAc,CAKhB"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@nxgt/janus-drizzle` — the PostgreSQL adapter for `@nxgt/janus`, on
3
+ * Drizzle and `@nxgt/drizzle`.
4
+ *
5
+ * Implements all three stores of the `@nxgt/janus` store port, and the
6
+ * relation store of `@nxgt/janus/permissions`, over one database. The tables
7
+ * are exported for the application's own drizzle-kit migrations. It defines
8
+ * **no error class**: every refusal is `@nxgt/janus`'s own, from the peer the
9
+ * application installed, so `instanceof` holds across the two packages.
10
+ */
11
+ export { createDrizzleAdapter, type DrizzleAdapter } from './adapter';
12
+ export { createDrizzleRelations } from './relations';
13
+ export { createDrizzleStores } from './stores';
14
+ export { type DrizzleAdapterOptions, defineJanusTables, type JanusTables, type JanusTablesOptions, } from './tables';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EACN,KAAK,qBAAqB,EAC1B,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACvB,MAAM,UAAU,CAAC"}