@nxgt/janus-graphql 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 +248 -0
- package/dist/context.d.ts +63 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/directives/apply.d.ts +46 -0
- package/dist/directives/apply.d.ts.map +1 -0
- package/dist/directives/guard.d.ts +21 -0
- package/dist/directives/guard.d.ts.map +1 -0
- package/dist/directives/permission/enforce.d.ts +24 -0
- package/dist/directives/permission/enforce.d.ts.map +1 -0
- package/dist/directives/permission/model.d.ts +35 -0
- package/dist/directives/permission/model.d.ts.map +1 -0
- package/dist/directives/permission/path.d.ts +31 -0
- package/dist/directives/permission/path.d.ts.map +1 -0
- package/dist/directives/permission/validate.d.ts +42 -0
- package/dist/directives/permission/validate.d.ts.map +1 -0
- package/dist/directives/read.d.ts +35 -0
- package/dist/directives/read.d.ts.map +1 -0
- package/dist/directives/validate.d.ts +33 -0
- package/dist/directives/validate.d.ts.map +1 -0
- package/dist/directives/words.d.ts +7 -0
- package/dist/directives/words.d.ts.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/helpers.d.ts +56 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +645 -0
- package/dist/index.js.map +24 -0
- package/dist/memo.d.ts +16 -0
- package/dist/memo.d.ts.map +1 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/sdl.d.ts +12 -0
- package/dist/sdl.d.ts.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/wiring.d.ts +56 -0
- package/dist/wiring.d.ts.map +1 -0
- package/docs/README.md +18 -0
- package/docs/guide/context.md +156 -0
- package/docs/guide/directives.md +312 -0
- package/docs/guide/errors.md +138 -0
- package/docs/guide/federation.md +70 -0
- package/docs/guide/telemetry.md +45 -0
- package/docs/roadmap.md +59 -0
- package/docs/troubleshooting.md +471 -0
- package/graphql/janus.graphqls +36 -0
- package/package.json +71 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Troubleshooting `@nxgt/janus-graphql`
|
|
2
|
+
|
|
3
|
+
Each entry is headed by what you see: a compiler error, a `TypeError` at
|
|
4
|
+
start-up or in a response, a warning, or a code and its status. Search this page for its
|
|
5
|
+
words.
|
|
6
|
+
|
|
7
|
+
This package **defines no error class**. A denial is a `GraphQLError`; any
|
|
8
|
+
other code is one of `@nxgt/janus`'s — see
|
|
9
|
+
[`@nxgt/janus`'s troubleshooting](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/troubleshooting.md)
|
|
10
|
+
for what causes each.
|
|
11
|
+
|
|
12
|
+
## Index
|
|
13
|
+
|
|
14
|
+
**At start-up**
|
|
15
|
+
- [`TypeError: applyJanusDirectives(): @authenticated on … names the user type '…', which is not one of …`](#typeerror-applyjanusdirectives-authenticated-on--names-the-user-type--which-is-not-one-of-)
|
|
16
|
+
- [`TypeError: applyJanusDirectives(): @authenticated on … names no user type`](#typeerror-applyjanusdirectives-authenticated-on--names-no-user-type)
|
|
17
|
+
- [`TypeError: applyJanusDirectives(): the @authenticated on … admit no user type in common`](#typeerror-applyjanusdirectives-the-authenticated-on--admit-no-user-type-in-common)
|
|
18
|
+
- [`TypeError: applyJanusDirectives(): @permission on … needs the permissions() instance`](#typeerror-applyjanusdirectives-permission-on--needs-the-permissions-instance)
|
|
19
|
+
- [`TypeError: applyJanusDirectives(): @permission on … names the object type '…', which is not one of …`](#typeerror-applyjanusdirectives-permission-on--names-the-object-type--which-is-not-one-of-)
|
|
20
|
+
- [`TypeError: applyJanusDirectives(): @permission on … asks '…', which … does not declare`](#typeerror-applyjanusdirectives-permission-on--asks--which--does-not-declare)
|
|
21
|
+
- [`TypeError: applyJanusDirectives(): @permission on … reads its id from '…', which is not args.<name> or parent.<name>`](#typeerror-applyjanusdirectives-permission-on--reads-its-id-from--which-is-not-argsname-or-parentname)
|
|
22
|
+
- [`TypeError: applyJanusDirectives(): @permission on … reads args.…, and … takes no argument …`](#typeerror-applyjanusdirectives-permission-on--reads-args-and--takes-no-argument-)
|
|
23
|
+
- [`TypeError: applyJanusDirectives(): @permission on … reads the …'s id from …, and … reads '…' of the object itself (fromField)`](#typeerror-applyjanusdirectives-permission-on--reads-the-s-id-from--and--reads--of-the-object-itself-fromfield)
|
|
24
|
+
- [`TypeError: applyJanusDirectives(): @permission on … asks '…' of …, which reaches a when()`](#typeerror-applyjanusdirectives-permission-on--asks--of--which-reaches-a-when)
|
|
25
|
+
- [`TypeError: applyJanusDirectives(): @permission on … finds loaders.…, which is not a function`](#typeerror-applyjanusdirectives-permission-on--finds-loaders-which-is-not-a-function)
|
|
26
|
+
- [`Unknown directive "@authenticated"`, or `"@permission"`](#unknown-directive-authenticated-or-permission)
|
|
27
|
+
- [`TypeError: applyJanusDirectives(): type '…' is not a user type of auth`](#typeerror-applyjanusdirectives-type--is-not-a-user-type-of-auth)
|
|
28
|
+
- [`TypeError: useJanus(): auth is not what janus() answered`](#typeerror-usejanus-auth-is-not-what-janus-answered)
|
|
29
|
+
- [`TypeError: useJanus(): access is not what permissions() answered`](#typeerror-usejanus-access-is-not-what-permissions-answered)
|
|
30
|
+
|
|
31
|
+
**In a response**
|
|
32
|
+
- [`UNAUTHENTICATED` for a signed-in user](#unauthenticated-for-a-signed-in-user)
|
|
33
|
+
- [`FORBIDDEN` where the user should be allowed](#forbidden-where-the-user-should-be-allowed)
|
|
34
|
+
- [`SERVICE_UNAVAILABLE`, 503, on every guarded field](#service_unavailable-503-on-every-guarded-field)
|
|
35
|
+
- [`Unexpected error.`, 500, where a `JanusError` was thrown](#unexpected-error-500-where-a-januserror-was-thrown)
|
|
36
|
+
- [`TypeError: … ctx.janus is not set`](#typeerror--ctxjanus-is-not-set)
|
|
37
|
+
- [`TypeError: useJanus(): the GraphQL context has no request to authenticate`](#typeerror-usejanus-the-graphql-context-has-no-request-to-authenticate)
|
|
38
|
+
- [`TypeError: can(): ctx.janus.access is not set`](#typeerror-can-ctxjanusaccess-is-not-set)
|
|
39
|
+
- [`TypeError: requireUser(): type is an empty list, which no user could pass`](#typeerror-requireuser-type-is-an-empty-list-which-no-user-could-pass)
|
|
40
|
+
- [`@permission on … resolved no object id from … — answered NOT_FOUND`](#permission-on--resolved-no-object-id-from---answered-not_found)
|
|
41
|
+
- [`NOT_FOUND` where the user should be allowed](#not_found-where-the-user-should-be-allowed)
|
|
42
|
+
- [A 403 that tells a user the object exists](#a-403-that-tells-a-user-the-object-exists)
|
|
43
|
+
- [`Unexpected error.`, 500, on a field `@permission` guards](#unexpected-error-500-on-a-field-permission-guards)
|
|
44
|
+
- [`TypeError: @permission on …: ctx.janus.access is not set`](#typeerror-permission-on--ctxjanusaccess-is-not-set)
|
|
45
|
+
- [`TypeError: @permission on …: loaders.… answered …`](#typeerror-permission-on--loaders-answered-)
|
|
46
|
+
- [A 401 whose `data` still holds the other fields](#a-401-whose-data-still-holds-the-other-fields)
|
|
47
|
+
|
|
48
|
+
**Types**
|
|
49
|
+
- [`'user' is possibly 'null'`](#user-is-possibly-null)
|
|
50
|
+
- [`Property 'access' does not exist on type …`](#property-access-does-not-exist-on-type-)
|
|
51
|
+
- [`Expected 4 arguments, but got 3`, on `can()`](#expected-4-arguments-but-got-3-on-can)
|
|
52
|
+
- [`'…' does not exist in type 'Loaders<…>'`, or `'Conditions<…>'`](#-does-not-exist-in-type-loaders-or-conditions)
|
|
53
|
+
- [`Type '{ …: … }' is not assignable to type 'never'`, on `loaders`](#type-----is-not-assignable-to-type-never-on-loaders)
|
|
54
|
+
- [`Source has 0 element(s) but target requires 1`, on `requireUser()`](#source-has-0-elements-but-target-requires-1-on-requireuser)
|
|
55
|
+
|
|
56
|
+
## At start-up
|
|
57
|
+
|
|
58
|
+
### `TypeError: applyJanusDirectives(): @authenticated on … names the user type '…', which is not one of …`
|
|
59
|
+
|
|
60
|
+
**When:** the server builds its schema — `createYoga()` with `useJanus()`, or
|
|
61
|
+
`applyJanusDirectives()` called yourself.
|
|
62
|
+
|
|
63
|
+
**Why:** a `type:` names a user type `janus()` does not declare — a typo, or a
|
|
64
|
+
type of another instance. Under `useJanus({ type: 'staff' })`, `'staff'` is
|
|
65
|
+
the only one a directive may name. `(read by Ward.name)` says the directive is
|
|
66
|
+
on the type `Ward`, read for its field `name`.
|
|
67
|
+
|
|
68
|
+
**Fix:** name a user type of `auth.types`.
|
|
69
|
+
|
|
70
|
+
```graphql
|
|
71
|
+
type Query {
|
|
72
|
+
roster: [Staff!]! @authenticated(type: ["staff"]) # not "doctor"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `TypeError: applyJanusDirectives(): @authenticated on … names no user type`
|
|
77
|
+
|
|
78
|
+
**Why:** `@authenticated(type: [])` admits nobody, so every request would be
|
|
79
|
+
refused.
|
|
80
|
+
|
|
81
|
+
**Fix:** leave `type:` out to admit any signed-in user —
|
|
82
|
+
`@authenticated` — or name the user types.
|
|
83
|
+
|
|
84
|
+
### `TypeError: applyJanusDirectives(): the @authenticated on … admit no user type in common`
|
|
85
|
+
|
|
86
|
+
**Why:** every `@authenticated` that applies must hold — the field's, its
|
|
87
|
+
type's, its interfaces' — and their `type:` lists share no user type.
|
|
88
|
+
|
|
89
|
+
```graphql
|
|
90
|
+
type Ward @authenticated(type: ["staff"]) {
|
|
91
|
+
chart: String @authenticated(type: ["patient"]) # staff AND patient: nobody
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Fix:** widen one of them, or move the field to a type whose directive
|
|
96
|
+
admits the users it is for.
|
|
97
|
+
|
|
98
|
+
### `TypeError: applyJanusDirectives(): @permission on … needs the permissions() instance`
|
|
99
|
+
|
|
100
|
+
**When:** at start-up, for a schema that uses `@permission`.
|
|
101
|
+
|
|
102
|
+
**Why:** `useJanus()` — or `applyJanusDirectives()` — was given no `access`,
|
|
103
|
+
or, through `applyJanusDirectives()`, an object with no `model`: the
|
|
104
|
+
directive reads the object types and permissions from `access.model`.
|
|
105
|
+
(`useJanus()` refuses an `access` without `can` earlier — see
|
|
106
|
+
[`access is not what permissions() answered`](#typeerror-usejanus-access-is-not-what-permissions-answered).)
|
|
107
|
+
|
|
108
|
+
**Fix:** `useJanus({ auth, access })`. An instance wrapped by
|
|
109
|
+
`instrumentPermissions()` keeps its `model`, and works as the plain one.
|
|
110
|
+
|
|
111
|
+
### `TypeError: applyJanusDirectives(): @permission on … names the object type '…', which is not one of …`
|
|
112
|
+
|
|
113
|
+
**Why:** `type:` names an object type the model does not declare — a typo, a
|
|
114
|
+
user type, or a type of another model. The message lists the model's.
|
|
115
|
+
|
|
116
|
+
**Fix:** name one of them: `@permission(name: "view", type: "record")`.
|
|
117
|
+
|
|
118
|
+
### `TypeError: applyJanusDirectives(): @permission on … asks '…', which … does not declare`
|
|
119
|
+
|
|
120
|
+
**Why:** `name:` is neither a relation nor a permission of that type — what
|
|
121
|
+
`access.can` would refuse too. The message lists what the type declares.
|
|
122
|
+
|
|
123
|
+
**Fix:** name one of them, or add the permission to the model:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
record: { related: { owners: ['patient'] }, permits: { view: ['owners'], delete: ['owners'] } },
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### `TypeError: applyJanusDirectives(): @permission on … reads its id from '…', which is not args.<name> or parent.<name>`
|
|
130
|
+
|
|
131
|
+
**Why:** `id:` is not a path: it must start with `args.` or `parent.`,
|
|
132
|
+
followed by one or more names — letters, numbers and `_`, not starting with a
|
|
133
|
+
number — separated by dots. `"id"`, `"args"`, `"record.id"` and
|
|
134
|
+
`"parent..id"` are not.
|
|
135
|
+
|
|
136
|
+
**Fix:** `id: "args.recordId"`, `id: "args.input.recordId"`,
|
|
137
|
+
`id: "parent.recordId"`.
|
|
138
|
+
|
|
139
|
+
### `TypeError: applyJanusDirectives(): @permission on … reads args.…, and … takes no argument …`
|
|
140
|
+
|
|
141
|
+
**Why:** the path's first name is not an argument of the field it guards.
|
|
142
|
+
Two common causes:
|
|
143
|
+
|
|
144
|
+
- the field names its argument otherwise — `record(recordId: ID!)` read
|
|
145
|
+
through the default, `args.id`;
|
|
146
|
+
- the default is not what you meant: on a **field**, the default is
|
|
147
|
+
`args.id` even when the field belongs to the object, and the message then
|
|
148
|
+
ends with `— name the id with id: "parent.<field>" or id: "args.<name>"`;
|
|
149
|
+
on a **type**, `id: "args.…"` must be an argument of every field of the
|
|
150
|
+
type.
|
|
151
|
+
|
|
152
|
+
**Fix:** name the path: `@permission(name: "view", type: "record", id: "args.recordId")`,
|
|
153
|
+
or `id: "parent.id"` for the object a field belongs to.
|
|
154
|
+
|
|
155
|
+
### `TypeError: applyJanusDirectives(): @permission on … reads the …'s id from …, and … reads '…' of the object itself (fromField)`
|
|
156
|
+
|
|
157
|
+
**Why:** the type has a `fromField` relation, read from the object's own
|
|
158
|
+
data, and the directive has only an id — from `args`, or from a field of the
|
|
159
|
+
parent other than its `id`. `access.can` would have nothing to read the field
|
|
160
|
+
from.
|
|
161
|
+
|
|
162
|
+
**Fix:** give the type a loader, which answers the object for an id:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
useJanus({ auth, access, loaders: { record: (id, ctx) => records.find(id) } });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Where the parent *is* the object — a type-level `@permission`, or
|
|
169
|
+
`id: "parent.id"` — no loader is needed: the parent is checked itself, and
|
|
170
|
+
must carry the field.
|
|
171
|
+
|
|
172
|
+
### `TypeError: applyJanusDirectives(): @permission on … asks '…' of …, which reaches a when()`
|
|
173
|
+
|
|
174
|
+
**Why:** the permission's rules reach a condition, whose `ctx` `access.can`
|
|
175
|
+
requires, and `useJanus()` has no `conditions` entry for the type to answer
|
|
176
|
+
it.
|
|
177
|
+
|
|
178
|
+
**Fix:**
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
useJanus({
|
|
182
|
+
auth,
|
|
183
|
+
access,
|
|
184
|
+
conditions: { record: (record, ctx: Context) => ({ onShift: ctx.shift.open }) },
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `TypeError: applyJanusDirectives(): @permission on … finds loaders.…, which is not a function`
|
|
189
|
+
|
|
190
|
+
**Why:** a `loaders` entry is something else — the repository object rather
|
|
191
|
+
than a function of it, from JavaScript or past a cast.
|
|
192
|
+
|
|
193
|
+
**Fix:** `loaders: { record: (id) => records.find(id) }`.
|
|
194
|
+
|
|
195
|
+
### `Unknown directive "@authenticated"`, or `"@permission"`
|
|
196
|
+
|
|
197
|
+
**Why:** graphql-js builds the schema before `useJanus()` sees it, and the
|
|
198
|
+
directive is not declared.
|
|
199
|
+
|
|
200
|
+
**Fix:** put `janusTypeDefs` among the type definitions:
|
|
201
|
+
`createSchema({ typeDefs: [janusTypeDefs, typeDefs], resolvers })`. A code
|
|
202
|
+
generator reading files takes
|
|
203
|
+
`node_modules/@nxgt/janus-graphql/graphql/janus.graphqls`.
|
|
204
|
+
|
|
205
|
+
### `TypeError: applyJanusDirectives(): type '…' is not a user type of auth`
|
|
206
|
+
|
|
207
|
+
**When:** at start-up, with `useJanus({ auth, type })` or
|
|
208
|
+
`applyJanusDirectives(schema, { auth, type })` — from JavaScript, or with a
|
|
209
|
+
`type` cast past the compiler, which otherwise refuses it.
|
|
210
|
+
|
|
211
|
+
**Why:** `type` names a user type `janus()` does not declare. The message
|
|
212
|
+
lists the ones it does: `— it knows 'patient', 'staff'`.
|
|
213
|
+
|
|
214
|
+
**Fix:** name one of `auth.types`: `useJanus({ auth, type: 'staff' })`.
|
|
215
|
+
|
|
216
|
+
### `TypeError: useJanus(): auth is not what janus() answered`
|
|
217
|
+
|
|
218
|
+
**Why:** `auth` has no `authenticate` — the `permissions()` instance passed in
|
|
219
|
+
its place, or the module that exports `auth` not loaded yet.
|
|
220
|
+
|
|
221
|
+
**Fix:** `useJanus({ auth, access })`, with `auth` what `janus()` answered.
|
|
222
|
+
|
|
223
|
+
### `TypeError: useJanus(): access is not what permissions() answered`
|
|
224
|
+
|
|
225
|
+
**Why:** `access` has no `can` — `janus()`'s instance passed in its place,
|
|
226
|
+
`null`, or the module that exports `access` not loaded yet.
|
|
227
|
+
|
|
228
|
+
**Fix:** `useJanus({ auth, access })`, with `access` what `permissions()`
|
|
229
|
+
answered — or leave `access` out when no field uses `@permission` or `can()`.
|
|
230
|
+
|
|
231
|
+
## In a response
|
|
232
|
+
|
|
233
|
+
### `UNAUTHENTICATED` for a signed-in user
|
|
234
|
+
|
|
235
|
+
**Why:** `authenticate` found no session in the request. The usual causes: the
|
|
236
|
+
client sends the cookie to another origin without `credentials: 'include'`;
|
|
237
|
+
the token is in a header `authenticate` does not read; the session lapsed or
|
|
238
|
+
was revoked; or `useJanus({ type })` treats this user's type as anonymous.
|
|
239
|
+
|
|
240
|
+
**Fix:** send `Authorization: Bearer <token>`, `X-Session-Token`, or the
|
|
241
|
+
session cookie; check `useJanus()`'s `type`.
|
|
242
|
+
|
|
243
|
+
### `FORBIDDEN` where the user should be allowed
|
|
244
|
+
|
|
245
|
+
**Why:** a `type:` that applies does not name the user's type. Every
|
|
246
|
+
`@authenticated` that applies must hold — a type's or an interface's too, not
|
|
247
|
+
only the field's. Or a `@permission(onDeny: FORBIDDEN)` denied: what
|
|
248
|
+
[`NOT_FOUND` where the user should be allowed](#not_found-where-the-user-should-be-allowed)
|
|
249
|
+
says applies to it too.
|
|
250
|
+
|
|
251
|
+
**Fix:** read the directives on the field, on its type, and on every
|
|
252
|
+
interface the type implements.
|
|
253
|
+
|
|
254
|
+
### `SERVICE_UNAVAILABLE`, 503, on every guarded field
|
|
255
|
+
|
|
256
|
+
**Why:** a store could not answer — `STORE_FAILED`. The sessions store for
|
|
257
|
+
`@authenticated`, `requireUser()` and `ctx.janus.user()`; the relation store
|
|
258
|
+
for `can()` and `@permission` — and a loader of yours that threw
|
|
259
|
+
`StoreFailure`. It is never answered `UNAUTHENTICATED` or `FORBIDDEN`.
|
|
260
|
+
|
|
261
|
+
**Fix:** the store: its connection, its credentials, its availability. The
|
|
262
|
+
message carries nothing of the store's; log the error where you call
|
|
263
|
+
`auth` — `@nxgt/janus`'s `slot` and `operation` name the call that failed.
|
|
264
|
+
|
|
265
|
+
### `Unexpected error.`, 500, where a `JanusError` was thrown
|
|
266
|
+
|
|
267
|
+
**Why:** Yoga masks every error that is not a `GraphQLError`, and nothing
|
|
268
|
+
told it a `JanusError` is one a client should see.
|
|
269
|
+
|
|
270
|
+
**Fix:** `maskedErrors: { maskError: janusMaskError() }`. `CREDENTIALS_INVALID`
|
|
271
|
+
is then 401, `STORE_FAILED` 503, and so on.
|
|
272
|
+
|
|
273
|
+
### `TypeError: … ctx.janus is not set`
|
|
274
|
+
|
|
275
|
+
**Why:** a guarded field, `requireUser()` or `can()` ran under a context
|
|
276
|
+
`useJanus()` did not build: the plugin is missing, or the schema was
|
|
277
|
+
transformed with `applyJanusDirectives()` and served by something else.
|
|
278
|
+
|
|
279
|
+
**Fix:** `plugins: [useJanus({ auth })]`.
|
|
280
|
+
|
|
281
|
+
### `TypeError: useJanus(): the GraphQL context has no request to authenticate`
|
|
282
|
+
|
|
283
|
+
**Why:** the context was built without `request` — a transport that is not
|
|
284
|
+
HTTP, such as a WebSocket subscription.
|
|
285
|
+
|
|
286
|
+
**Fix:** for now, authenticate subscriptions over HTTP (Yoga's server-sent
|
|
287
|
+
events carry the request). A connection-level wiring for graphql-ws is on
|
|
288
|
+
the [roadmap](roadmap.md).
|
|
289
|
+
|
|
290
|
+
### `TypeError: can(): ctx.janus.access is not set`
|
|
291
|
+
|
|
292
|
+
**Why:** `useJanus()` was given no `access`.
|
|
293
|
+
|
|
294
|
+
**Fix:** `useJanus({ auth, access })`, with `access` what `permissions()`
|
|
295
|
+
answered.
|
|
296
|
+
|
|
297
|
+
### `TypeError: requireUser(): type is an empty list, which no user could pass`
|
|
298
|
+
|
|
299
|
+
**Why:** `requireUser(ctx, { type })` was given `[]` — a list of user types
|
|
300
|
+
built at run time that came out empty. The compiler refuses a literal `[]`;
|
|
301
|
+
a `string[]` built elsewhere gets through, and no user could pass it.
|
|
302
|
+
|
|
303
|
+
**Fix:** name at least one user type, or leave `type` out to admit any
|
|
304
|
+
signed-in user:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const user = await requireUser(ctx, allowed.length > 0 ? { type: allowed } : {});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### `@permission on … resolved no object id from … — answered NOT_FOUND`
|
|
311
|
+
|
|
312
|
+
**Where:** a warning, through `process.emitWarning` with the code
|
|
313
|
+
`JANUS_GRAPHQL_NO_OBJECT_ID`, once per directive; the client receives
|
|
314
|
+
`NOT_FOUND`, 404.
|
|
315
|
+
|
|
316
|
+
**Why:** the path read nothing it could check: an optional argument the
|
|
317
|
+
client left out (`record(id: ID)`), a `null` in the parent
|
|
318
|
+
(`parent.wardId` on a visit with no ward), an empty list in the parent, a
|
|
319
|
+
list holding a `null`, or a value that is neither a string nor an integer.
|
|
320
|
+
There is no object to ask about, so there is nothing to allow.
|
|
321
|
+
|
|
322
|
+
**Fix:** make the argument required — `record(id: ID!)` — or point `id:` at a
|
|
323
|
+
field that always holds an id. When a `null` is a valid answer, move the
|
|
324
|
+
directive to a field that is only reached when there is an object.
|
|
325
|
+
|
|
326
|
+
### `NOT_FOUND` where the user should be allowed
|
|
327
|
+
|
|
328
|
+
**Why:** one of the directives that apply denied — `NOT_FOUND` is
|
|
329
|
+
`@permission`'s default denial — or there was nothing to check:
|
|
330
|
+
|
|
331
|
+
- a `@permission` on the **type** or an **interface** it implements applies
|
|
332
|
+
too, and is asked first;
|
|
333
|
+
- the `id:` path reads another object than you think — `args.id` is the
|
|
334
|
+
default on a field, even a field of the object;
|
|
335
|
+
- a loader answered `null`;
|
|
336
|
+
- a `fromField` field is missing from the parent, so the relation it reads
|
|
337
|
+
holds for nobody: the parent's resolver must answer the field;
|
|
338
|
+
- the user holds the relation on another object type than `type:` names.
|
|
339
|
+
|
|
340
|
+
**Fix:** ask `access.can` yourself with the same subject, permission and
|
|
341
|
+
object to see what the model answers, and read every directive on the field,
|
|
342
|
+
its type and its interfaces.
|
|
343
|
+
|
|
344
|
+
### A 403 that tells a user the object exists
|
|
345
|
+
|
|
346
|
+
**Why:** `onDeny: FORBIDDEN` answers *this exists, and not for you*. On
|
|
347
|
+
`record(id: "r42")`, that tells a user who may not know `r42` that it exists.
|
|
348
|
+
|
|
349
|
+
**Fix:** keep `NOT_FOUND`, the default, on anything reached by an id the
|
|
350
|
+
client chose. `FORBIDDEN` fits a field of an object the user may already
|
|
351
|
+
see — guard the type with a `NOT_FOUND` `@permission` and the field with the
|
|
352
|
+
`FORBIDDEN` one; the type's is asked first:
|
|
353
|
+
|
|
354
|
+
```graphql
|
|
355
|
+
type Record @permission(name: "view", type: "record") {
|
|
356
|
+
billing: Billing @permission(name: "manage", type: "record", onDeny: FORBIDDEN)
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### `Unexpected error.`, 500, on a field `@permission` guards
|
|
361
|
+
|
|
362
|
+
**Why:** `@permission` answers a `JanusError` — a relation store's
|
|
363
|
+
`STORE_FAILED`, or one a loader threw — with its status, 503 for an outage.
|
|
364
|
+
An error that is not a `JanusError` is masked by Yoga like any other: a
|
|
365
|
+
database driver's own error thrown by a loader or a condition, or anything
|
|
366
|
+
the resolver throws after the check. And without `janusMaskError()`, a
|
|
367
|
+
`JanusError` the **resolver** lets through is masked too.
|
|
368
|
+
|
|
369
|
+
**Fix:** throw `StoreFailure` from a loader whose database cannot answer, so
|
|
370
|
+
the outage is a 503 rather than a 500, and wire
|
|
371
|
+
`maskedErrors: { maskError: janusMaskError() }`:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { StoreFailure } from '@nxgt/janus';
|
|
375
|
+
|
|
376
|
+
loaders: {
|
|
377
|
+
record: (id) =>
|
|
378
|
+
records.find(id).catch((cause: unknown) => {
|
|
379
|
+
throw new StoreFailure('records.find could not answer', { cause });
|
|
380
|
+
}),
|
|
381
|
+
},
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `TypeError: @permission on …: ctx.janus.access is not set`
|
|
385
|
+
|
|
386
|
+
**Why:** the schema was transformed with `access` — by
|
|
387
|
+
`applyJanusDirectives(schema, { auth, access })` — and is served with a
|
|
388
|
+
context built without it.
|
|
389
|
+
|
|
390
|
+
**Fix:** serve it through `useJanus({ auth, access })`, which builds both
|
|
391
|
+
from the same options.
|
|
392
|
+
|
|
393
|
+
### `TypeError: @permission on …: loaders.… answered …`
|
|
394
|
+
|
|
395
|
+
**Why:** a loader answered `undefined`, or something that is not an object.
|
|
396
|
+
**An absence is `null`**: a loader that forgets to `return` answers
|
|
397
|
+
`undefined`, and that is refused rather than read as "not found".
|
|
398
|
+
|
|
399
|
+
**Fix:** answer the object, or `null` when there is none.
|
|
400
|
+
|
|
401
|
+
### A 401 whose `data` still holds the other fields
|
|
402
|
+
|
|
403
|
+
**Why:** Yoga answers the highest status among the errors. One refused field
|
|
404
|
+
in a query makes the response a 401, while the fields that were allowed are
|
|
405
|
+
answered in `data`.
|
|
406
|
+
|
|
407
|
+
**Fix:** nothing to fix: read `data` and `errors` rather than the status
|
|
408
|
+
alone, or split the query.
|
|
409
|
+
|
|
410
|
+
## Types
|
|
411
|
+
|
|
412
|
+
### `'user' is possibly 'null'`
|
|
413
|
+
|
|
414
|
+
**When:** `const user = await ctx.janus.user(); user.id;` — or, read without
|
|
415
|
+
binding it, `(await ctx.janus.user()).id`, where the message is
|
|
416
|
+
`Object is possibly 'null'`.
|
|
417
|
+
|
|
418
|
+
**Why:** `ctx.janus.user()` answers `null` for an anonymous request, and the
|
|
419
|
+
type does not know a directive guards the field.
|
|
420
|
+
|
|
421
|
+
**Fix:** `requireUser(ctx)`, which answers a user or refuses the request.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const user = await requireUser(ctx);
|
|
425
|
+
user.id;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### `Property 'access' does not exist on type …`
|
|
429
|
+
|
|
430
|
+
**Why:** the context was typed `JanusContext<typeof auth>`, without `access`
|
|
431
|
+
— as it is when `useJanus()` is given none.
|
|
432
|
+
|
|
433
|
+
**Fix:** `JanusContext<typeof auth, typeof access>`, and `useJanus({ auth, access })`.
|
|
434
|
+
|
|
435
|
+
### `Expected 4 arguments, but got 3`, on `can()`
|
|
436
|
+
|
|
437
|
+
**Why:** the permission reaches a condition, a `when()` in the model, and its
|
|
438
|
+
`ctx` is required — as it is for `access.can`.
|
|
439
|
+
|
|
440
|
+
**Fix:** pass it: `can(ctx, 'edit', record, { ctx: { locked: record.locked } })`.
|
|
441
|
+
|
|
442
|
+
### `'…' does not exist in type 'Loaders<…>'`, or `'Conditions<…>'`
|
|
443
|
+
|
|
444
|
+
**Why:** a `loaders` key is not an object type of the model; or a
|
|
445
|
+
`conditions` key is a type none of whose permissions reaches a `when()`, so
|
|
446
|
+
there is no `ctx` to answer.
|
|
447
|
+
|
|
448
|
+
**Fix:** name an object type of the model; drop the `conditions` entry of a
|
|
449
|
+
type with no condition.
|
|
450
|
+
|
|
451
|
+
### `Type '{ …: … }' is not assignable to type 'never'`, on `loaders`
|
|
452
|
+
|
|
453
|
+
**Why:** `useJanus()` was given `loaders` or `conditions` and no `access`:
|
|
454
|
+
without the permissions instance, `@permission` cannot be used, and has
|
|
455
|
+
nothing to load for.
|
|
456
|
+
|
|
457
|
+
**Fix:** `useJanus({ auth, access, loaders })`.
|
|
458
|
+
|
|
459
|
+
### `Source has 0 element(s) but target requires 1`, on `requireUser()`
|
|
460
|
+
|
|
461
|
+
The full message reads `Type '[]' is not assignable to type '"patient" |
|
|
462
|
+
"staff" | readonly ["patient" | "staff", ...("patient" | "staff")[]] |
|
|
463
|
+
undefined'`.
|
|
464
|
+
|
|
465
|
+
**Why:** `requireUser(ctx, { type: [] })` — an empty list admits no user.
|
|
466
|
+
The compiler refuses it here; a list built at run time that comes out empty
|
|
467
|
+
throws
|
|
468
|
+
[`TypeError: requireUser(): type is an empty list, which no user could pass`](#typeerror-requireuser-type-is-an-empty-list-which-no-user-could-pass).
|
|
469
|
+
|
|
470
|
+
**Fix:** name at least one user type, `{ type: ['staff'] }`, or leave `type`
|
|
471
|
+
out to admit any signed-in user.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Only a signed-in user reaches this field, or every field of this type or
|
|
3
|
+
interface. With `type`, only a user of one of these user types: any other is
|
|
4
|
+
answered FORBIDDEN.
|
|
5
|
+
"""
|
|
6
|
+
directive @authenticated(
|
|
7
|
+
type: [String!]
|
|
8
|
+
) on OBJECT | INTERFACE | FIELD_DEFINITION
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
What a denied `@permission` answers.
|
|
12
|
+
"""
|
|
13
|
+
enum JanusPermissionDenial {
|
|
14
|
+
"""
|
|
15
|
+
404: the object is not told to exist. The default.
|
|
16
|
+
"""
|
|
17
|
+
NOT_FOUND
|
|
18
|
+
"""
|
|
19
|
+
403: the object exists, and this user may not do this to it.
|
|
20
|
+
"""
|
|
21
|
+
FORBIDDEN
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
Only a user holding permission `name` on the object of `type` whose id `id`
|
|
26
|
+
reads reaches this field, or every field of this type or interface. `id` is
|
|
27
|
+
`args.<path>` or `parent.<path>`: `args.id` on a field and `parent.id` on a
|
|
28
|
+
type or an interface when absent. A list of ids requires the permission on
|
|
29
|
+
every one. Repeated, every one must hold, in the order written.
|
|
30
|
+
"""
|
|
31
|
+
directive @permission(
|
|
32
|
+
name: String!
|
|
33
|
+
type: String!
|
|
34
|
+
id: String
|
|
35
|
+
onDeny: JanusPermissionDenial! = NOT_FOUND
|
|
36
|
+
) repeatable on OBJECT | INTERFACE | FIELD_DEFINITION
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nxgt/janus-graphql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GraphQL for @nxgt/janus: an envelop plugin with the signed-in user on the context, fields guarded by @authenticated and @permission, and every error with the status it deserves",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"docs",
|
|
12
|
+
"graphql",
|
|
13
|
+
"README.md",
|
|
14
|
+
"package.json",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"identity",
|
|
27
|
+
"authentication",
|
|
28
|
+
"authorization",
|
|
29
|
+
"graphql",
|
|
30
|
+
"envelop",
|
|
31
|
+
"graphql-yoga",
|
|
32
|
+
"directives",
|
|
33
|
+
"janus",
|
|
34
|
+
"typescript"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/softistx/nxgt-janus.git",
|
|
39
|
+
"directory": "packages/janus-graphql"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"registry": "https://registry.npmjs.org",
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "bun run ../../build.ts",
|
|
47
|
+
"test": "bun test src",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
},
|
|
50
|
+
"nxgt": {
|
|
51
|
+
"entrypoints": [
|
|
52
|
+
"src/index.ts"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@envelop/core": "^5.6.1",
|
|
57
|
+
"@graphql-tools/utils": "^12.0.1",
|
|
58
|
+
"@nxgt/janus": "^0.11.0",
|
|
59
|
+
"@types/bun": "^1.4.2",
|
|
60
|
+
"graphql": "^17.0.2",
|
|
61
|
+
"graphql-yoga": "^5.24.1",
|
|
62
|
+
"zod": "^4.6.5"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@envelop/core": "^5.0.0",
|
|
66
|
+
"@graphql-tools/utils": ">=10.0.0 <13",
|
|
67
|
+
"@nxgt/janus": "^0.11.0",
|
|
68
|
+
"graphql": "^16.9.0 || ^17.0.0",
|
|
69
|
+
"typescript": "^6.0.3"
|
|
70
|
+
}
|
|
71
|
+
}
|