@nxgt/janus 0.18.0 → 0.19.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/README.md +37 -11
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/memory/users.d.ts.map +1 -1
- package/dist/auth/port/types/stores.d.ts +5 -0
- package/dist/auth/port/types/stores.d.ts.map +1 -1
- package/dist/auth/port/types/users.d.ts +17 -0
- package/dist/auth/port/types/users.d.ts.map +1 -1
- package/dist/auth/types/user-type.d.ts +15 -0
- package/dist/auth/types/user-type.d.ts.map +1 -1
- package/dist/auth/users/find-many.d.ts +29 -0
- package/dist/auth/users/find-many.d.ts.map +1 -0
- package/dist/auth/users/record-flows.d.ts.map +1 -1
- package/dist/chunks/{index-kvjj5xkp.js → index-6bjfv4xv.js} +8 -2
- package/dist/chunks/{index-kvjj5xkp.js.map → index-6bjfv4xv.js.map} +3 -3
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/users/batch.d.ts +8 -0
- package/dist/conformance/cases/users/batch.d.ts.map +1 -0
- package/dist/conformance/cases/users/index.d.ts.map +1 -1
- package/dist/conformance/describe.d.ts +2 -1
- package/dist/conformance/describe.d.ts.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +104 -44
- package/dist/conformance/index.js.map +7 -6
- package/dist/conformance/types.d.ts +10 -2
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/index.js +59 -7
- package/dist/index.js.map +6 -5
- package/docs/guide/adapters.md +44 -4
- package/docs/guide/users.md +48 -0
- package/docs/roadmap.md +12 -14
- package/docs/troubleshooting.md +39 -2
- package/package.json +1 -1
package/docs/guide/users.md
CHANGED
|
@@ -180,6 +180,7 @@ In the one-type form these are on `auth` itself; with several types, on
|
|
|
180
180
|
| --- | --- | --- |
|
|
181
181
|
| `create(fields & { password?, active? })` | the user; no session | `USER_INVALID`, `PASSWORD_TOO_SHORT`, `LOGIN_TAKEN` |
|
|
182
182
|
| `find(id)` | the user, or `null` — for a malformed id, an unknown one, or one of another type | |
|
|
183
|
+
| `findMany(ids)` | the users, in the order of `ids`, each once; an id `find` answers `null` for is left out — see [below](#several-users-at-once--findmany) | |
|
|
183
184
|
| `get(id)` | the user | `NOT_FOUND` |
|
|
184
185
|
| `list({ after?, limit? })` | `CursorPage<User>`, in creation order | `INVALID_CURSOR` |
|
|
185
186
|
| `update(user, patch, { ifVersion? })` | the user as written; sends `user.emailChanged` when the e-mail changed | `USER_INVALID`, `LOGIN_TAKEN`, `VERSION_CONFLICT`, `NOT_FOUND` |
|
|
@@ -249,6 +250,53 @@ idempotent: calling it again finishes the job. When it deleted the user, it
|
|
|
249
250
|
sends a [`user.deleted` event](events.md) — once, after the sessions, tokens and relation tuples are gone, and even when an outage interrupts removing them;
|
|
250
251
|
`create` and `signUp` send `user.created`.
|
|
251
252
|
|
|
253
|
+
### Several users at once — `findMany`
|
|
254
|
+
|
|
255
|
+
A list of rows that each name a user — a staff list showing every member's
|
|
256
|
+
account state — reads them in one call, not one `find` per row:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const rows = await db.staffRows(); // your own table, 100 rows
|
|
260
|
+
const members = await clinic.staff.findMany(rows.map((row) => row.staffId));
|
|
261
|
+
const byId = new Map(members.map((member) => [member.id, member]));
|
|
262
|
+
|
|
263
|
+
const shown = rows.map((row) => {
|
|
264
|
+
const member = byId.get(row.staffId); // undefined: nobody holds this id
|
|
265
|
+
return { ...row, active: member?.active ?? false, verified: member?.emailVerified ?? false };
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- **Order**: the order of `ids`. A repeated id is answered once, at its first
|
|
270
|
+
place.
|
|
271
|
+
- **Absence**: an id `find` would answer `null` for — malformed, unknown, or
|
|
272
|
+
held by a user of another type — is **left out**, never an error. The answer
|
|
273
|
+
may be shorter than `ids`, so match by `user.id`, never by position.
|
|
274
|
+
- **Empty**: `[]`, or a list with no well-formed id, answers `[]` without
|
|
275
|
+
reaching the store.
|
|
276
|
+
- **Size**: any length. The store is asked 100 ids at a time, one batch after
|
|
277
|
+
another, so no query grows with your list.
|
|
278
|
+
- **Failure**: `STORE_FAILED`, never a shorter list. Something other than an
|
|
279
|
+
array is a bare `TypeError`: only your code passes one.
|
|
280
|
+
- **Cost**: one query per 100 ids when the users store implements the
|
|
281
|
+
optional `findUsers` — the reference store, `@nxgt/janus-drizzle` and
|
|
282
|
+
`@nxgt/janus-mongo` do. A store that does not is read with `findUser`,
|
|
283
|
+
**ten at a time**, so a third-party adapter keeps working and never opens
|
|
284
|
+
more than ten reads at once for one call; see
|
|
285
|
+
[writing an adapter](adapters.md#userstorefindusers-optional).
|
|
286
|
+
|
|
287
|
+
**A GraphQL resolver per row** can batch through a DataLoader of your own —
|
|
288
|
+
`@nxgt/janus-graphql` loads no users itself:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import DataLoader from 'dataloader';
|
|
292
|
+
|
|
293
|
+
// One per request, so nothing is cached across users' requests.
|
|
294
|
+
const staffById = new DataLoader(async (ids: readonly string[]) => {
|
|
295
|
+
const found = new Map((await clinic.staff.findMany(ids)).map((u) => [u.id, u]));
|
|
296
|
+
return ids.map((id) => found.get(id) ?? null);
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
252
300
|
### Paging every user
|
|
253
301
|
|
|
254
302
|
```ts
|
package/docs/roadmap.md
CHANGED
|
@@ -20,6 +20,11 @@ Nothing yet.
|
|
|
20
20
|
window. A port method to collect them, as sessions are, would let the
|
|
21
21
|
core do it.
|
|
22
22
|
|
|
23
|
+
- **A batch read across types** — `findMany` reads one user type, as `find`
|
|
24
|
+
does. The shared `findUser(id)` has no batch counterpart yet; one on the
|
|
25
|
+
instance, answering a union narrowed by `user.type`, would serve a list that
|
|
26
|
+
mixes types.
|
|
27
|
+
|
|
23
28
|
- **More official adapters** — the ports are cut where atomicity is not
|
|
24
29
|
required, so users, sessions and permission tuples can each live in the
|
|
25
30
|
database that suits them. MongoDB is the first adapter
|
|
@@ -71,6 +76,13 @@ Nothing yet.
|
|
|
71
76
|
The last ten, newest first, each with the version it came in. Everything
|
|
72
77
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
73
78
|
|
|
79
|
+
- **Several users by id in one query, v0.18.0** — `auth.<type>.findMany(ids)`
|
|
80
|
+
answers the type's users in the order of `ids`, each once, and leaves out an
|
|
81
|
+
id nobody of that type holds — for a list of rows that would otherwise make
|
|
82
|
+
one `find` per row and fill the connection pool. The users store gains an
|
|
83
|
+
optional `findUsers`, implemented by the reference store,
|
|
84
|
+
`@nxgt/janus-drizzle` 0.7.0 and `@nxgt/janus-mongo` 0.8.0; a store without it
|
|
85
|
+
is read with `findUser`, ten at a time, so third-party adapters keep working.
|
|
74
86
|
- **Requests that send e-mail are throttled, v0.18.0** —
|
|
75
87
|
`janus({ mail: { throttle: { attempts, window } } })`, five per ten
|
|
76
88
|
minutes by default and `false` to turn it off. `magicLink.request`,
|
|
@@ -153,20 +165,6 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
153
165
|
user only — or a security settings page; `null` for a user with no active
|
|
154
166
|
factor. The listener can now tell the user how many codes remain, as
|
|
155
167
|
`@nxgt/janus-mail`'s `recoveryCodeUsed` e-mail does.
|
|
156
|
-
- **Recovery codes, v0.10.0** — a user who loses their authenticator app
|
|
157
|
-
still signs in, with no operator resetting the account.
|
|
158
|
-
`secondFactor.activate` answers `{ user, recoveryCodes }`: ten single-use
|
|
159
|
-
codes, shown once and stored only as keyed hashes (breaking: read `.user`).
|
|
160
|
-
`secondFactor.recover(challenge, code)` redeems a sign-in's challenge with
|
|
161
|
-
one and answers the session with `recoveryCodesLeft`;
|
|
162
|
-
`secondFactor.regenerateRecoveryCodes(user, code)` replaces them all on a
|
|
163
|
-
fresh code from the app, and counts its attempts itself — five per user
|
|
164
|
-
per 15-minute window, in the store — so a stolen session cannot guess the
|
|
165
|
-
code, and the route needs no limiter of its own. Two user events,
|
|
166
|
-
`user.recoveryCodesRegenerated` and `user.recoveryCodeUsed`. For adapters:
|
|
167
|
-
`SecondFactorRecord.recoveryCodes`,
|
|
168
|
-
stored by the users stores of `@nxgt/janus-drizzle` and
|
|
169
|
-
`@nxgt/janus-mongo`, with conformance cases.
|
|
170
168
|
- **Sending the e-mails, `@nxgt/janus-mail` v0.1.0** — a package of its
|
|
171
169
|
own, built on the `@nxgt/mail` toolkit: `janusMail({ mailer, from, brand,
|
|
172
170
|
links })` takes what each flow answers —
|
package/docs/troubleshooting.md
CHANGED
|
@@ -49,6 +49,9 @@ How the messages are shaped:
|
|
|
49
49
|
**Users, sessions and tokens**
|
|
50
50
|
- [`STORE_FAILED` — `<slot>.<method>: the store could not answer`](#store_failed--slotmethod-the-store-could-not-answer)
|
|
51
51
|
- [`STORE_FAILED` — `<slot>.<method> answered undefined …`](#store_failed--slotmethod-answered-undefined-an-absence-is-null-so-this-store-forgot-to-answer)
|
|
52
|
+
- [`STORE_FAILED` — `users.findUsers answered no list …`](#store_failed--usersfindusers-answered-no-list-an-absence-is-a-shorter-list-so-this-store-forgot-to-answer)
|
|
53
|
+
- [`<call>: ids must be an array of user ids`](#call-ids-must-be-an-array-of-user-ids)
|
|
54
|
+
- [`findMany` answers fewer users than ids](#findmany-answers-fewer-users-than-ids)
|
|
52
55
|
- [`NOT_FOUND` — `<call>: no <type> has this id`](#not_found--call-no-type-has-this-id)
|
|
53
56
|
- [`LOGIN_TAKEN` — `<call>: the login is taken by another <type>`](#login_taken--call-the-login-is-taken-by-another-type)
|
|
54
57
|
- [`VERSION_CONFLICT` — `<call>: expected version <n>, found <m>`](#version_conflict--call-expected-version-n-found-m)
|
|
@@ -283,10 +286,10 @@ janus({ ..., hasher: scryptHasher(), verifiers: [legacyBcrypt] }); // not [scryp
|
|
|
283
286
|
|
|
284
287
|
### `janus: store.<slot> has no method <method>, which the port requires`
|
|
285
288
|
|
|
286
|
-
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`.
|
|
289
|
+
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`, `janus: store.users.findUsers must be a function or absent`.
|
|
287
290
|
|
|
288
291
|
**When:** `janus({...})`, from JavaScript or with a store typed loosely. TypeScript refuses a partial store at compile time and names the method.
|
|
289
|
-
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. `deleteExpiredSessions`
|
|
292
|
+
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. Two methods are optional — `sessions.deleteExpiredSessions` and `users.findUsers` — and each is absent, or a function: a value in its place (`findUsers: true`, a property copied from a config) is a wiring mistake, not an absent capability.
|
|
290
293
|
An adapter written for an earlier `@nxgt/janus` reports the method a later
|
|
291
294
|
release added to the port: `store.tokens has no method countAttempt` for one
|
|
292
295
|
written against 0.3 (the method came in 0.4), and
|
|
@@ -543,6 +546,40 @@ try {
|
|
|
543
546
|
**Why:** a method that can find nothing must answer `null`. `undefined` is also what a missing `return` produces, so it is treated as a bug in the store, not as "not found".
|
|
544
547
|
**Fix:** `return doc ?? null;` — and run `@nxgt/janus/conformance` against the store.
|
|
545
548
|
|
|
549
|
+
### `STORE_FAILED` — `users.findUsers answered no list: an absence is a shorter list, so this store forgot to answer`
|
|
550
|
+
|
|
551
|
+
**When:** `findMany`, over a users store you wrote that implements the optional `findUsers`.
|
|
552
|
+
**Why:** `findUsers` answered `null`, `undefined` or something else than an array. An id nobody holds is left out of the list, and nobody at all is `[]`; anything else is a store that forgot to answer, so it fails rather than read as "nobody".
|
|
553
|
+
**Fix:** answer the rows you found, mapped to records — or leave `findUsers` out, and `findMany` reads with `findUser`:
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
findUsers: async (ids) => (await db.users.where('id', 'in', ids)).map(toUser), // [] when none
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### `<call>: ids must be an array of user ids`
|
|
560
|
+
|
|
561
|
+
A bare `TypeError` — `findMany: …`, or `staff.findMany: …` with several user types.
|
|
562
|
+
|
|
563
|
+
**When:** `findMany` was given something other than an array: one id, a `Set`, a comma-separated string.
|
|
564
|
+
**Why:** `findMany` takes a list; one id is `find`. Only your code passes the wrong shape, and TypeScript refuses it.
|
|
565
|
+
**Fix:** pass an array, or use `find` for one id:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
await auth.staff.findMany([...selectedIds]); // from a Set
|
|
569
|
+
await auth.staff.find(id); // one id
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### `findMany` answers fewer users than ids
|
|
573
|
+
|
|
574
|
+
**When:** always, when an id is malformed, unknown, held by a user of another type, or repeated.
|
|
575
|
+
**Why:** `findMany` leaves out what `find` answers `null` for, and answers a repeated id once — it never puts `null` in a place. Reading `users[i]` as the user of `ids[i]` then shows one row's account on another.
|
|
576
|
+
**Fix:** match by id:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
const byId = new Map((await auth.staff.findMany(ids)).map((user) => [user.id, user]));
|
|
580
|
+
const member = byId.get(row.staffId); // undefined: nobody of this type holds it
|
|
581
|
+
```
|
|
582
|
+
|
|
546
583
|
### `NOT_FOUND` — `<call>: no <type> has this id`
|
|
547
584
|
|
|
548
585
|
`NotFoundError`, from `get`, `getUser`, `update`, `setActive`, `setPassword`, `verifyEmail.send`, `secondFactor.recoveryCodesLeft`. Also `<call>: the user has no e-mail`.
|