@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 +21 -0
- package/README.md +171 -0
- package/dist/adapter.d.ts +29 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +448 -0
- package/dist/index.js.map +14 -0
- package/dist/relations.d.ts +18 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/stores.d.ts +31 -0
- package/dist/stores.d.ts.map +1 -0
- package/dist/tables.d.ts +694 -0
- package/dist/tables.d.ts.map +1 -0
- package/dist/translate.d.ts +31 -0
- package/dist/translate.d.ts.map +1 -0
- package/docs/README.md +12 -0
- package/docs/guide/database.md +172 -0
- package/docs/guide/migrations.md +153 -0
- package/docs/guide/wiring.md +170 -0
- package/docs/roadmap.md +44 -0
- package/docs/troubleshooting.md +324 -0
- package/package.json +69 -0
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|