@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/dist/context.d.ts +63 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/directives/apply.d.ts +46 -0
  6. package/dist/directives/apply.d.ts.map +1 -0
  7. package/dist/directives/guard.d.ts +21 -0
  8. package/dist/directives/guard.d.ts.map +1 -0
  9. package/dist/directives/permission/enforce.d.ts +24 -0
  10. package/dist/directives/permission/enforce.d.ts.map +1 -0
  11. package/dist/directives/permission/model.d.ts +35 -0
  12. package/dist/directives/permission/model.d.ts.map +1 -0
  13. package/dist/directives/permission/path.d.ts +31 -0
  14. package/dist/directives/permission/path.d.ts.map +1 -0
  15. package/dist/directives/permission/validate.d.ts +42 -0
  16. package/dist/directives/permission/validate.d.ts.map +1 -0
  17. package/dist/directives/read.d.ts +35 -0
  18. package/dist/directives/read.d.ts.map +1 -0
  19. package/dist/directives/validate.d.ts +33 -0
  20. package/dist/directives/validate.d.ts.map +1 -0
  21. package/dist/directives/words.d.ts +7 -0
  22. package/dist/directives/words.d.ts.map +1 -0
  23. package/dist/errors.d.ts +44 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/helpers.d.ts +56 -0
  26. package/dist/helpers.d.ts.map +1 -0
  27. package/dist/index.d.ts +27 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +645 -0
  30. package/dist/index.js.map +24 -0
  31. package/dist/memo.d.ts +16 -0
  32. package/dist/memo.d.ts.map +1 -0
  33. package/dist/plugin.d.ts +32 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/sdl.d.ts +12 -0
  36. package/dist/sdl.d.ts.map +1 -0
  37. package/dist/types.d.ts +64 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/wiring.d.ts +56 -0
  40. package/dist/wiring.d.ts.map +1 -0
  41. package/docs/README.md +18 -0
  42. package/docs/guide/context.md +156 -0
  43. package/docs/guide/directives.md +312 -0
  44. package/docs/guide/errors.md +138 -0
  45. package/docs/guide/federation.md +70 -0
  46. package/docs/guide/telemetry.md +45 -0
  47. package/docs/roadmap.md +59 -0
  48. package/docs/troubleshooting.md +471 -0
  49. package/graphql/janus.graphqls +36 -0
  50. 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
+ }