graphql-shield-node23 7.6.5

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 (56) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cjs/constructors.js +134 -0
  3. package/dist/cjs/generator.js +205 -0
  4. package/dist/cjs/index.js +15 -0
  5. package/dist/cjs/package.json +1 -0
  6. package/dist/cjs/rules.js +402 -0
  7. package/dist/cjs/shield.js +52 -0
  8. package/dist/cjs/types.js +2 -0
  9. package/dist/cjs/utils.js +97 -0
  10. package/dist/cjs/validation.js +84 -0
  11. package/dist/esm/constructors.js +124 -0
  12. package/dist/esm/generator.js +201 -0
  13. package/dist/esm/index.js +2 -0
  14. package/dist/esm/rules.js +366 -0
  15. package/dist/esm/shield.js +45 -0
  16. package/dist/esm/types.js +1 -0
  17. package/dist/esm/utils.js +88 -0
  18. package/dist/esm/validation.js +79 -0
  19. package/dist/package.json +47 -0
  20. package/dist/typings/constructors.d.cts +91 -0
  21. package/dist/typings/constructors.d.ts +91 -0
  22. package/dist/typings/generator.d.cts +11 -0
  23. package/dist/typings/generator.d.ts +11 -0
  24. package/dist/typings/index.d.cts +3 -0
  25. package/dist/typings/index.d.ts +3 -0
  26. package/dist/typings/rules.d.cts +159 -0
  27. package/dist/typings/rules.d.ts +159 -0
  28. package/dist/typings/shield.d.cts +11 -0
  29. package/dist/typings/shield.d.ts +11 -0
  30. package/dist/typings/types.d.cts +64 -0
  31. package/dist/typings/types.d.ts +64 -0
  32. package/dist/typings/utils.d.cts +52 -0
  33. package/dist/typings/utils.d.ts +52 -0
  34. package/dist/typings/validation.d.cts +19 -0
  35. package/dist/typings/validation.d.ts +19 -0
  36. package/package.json +67 -0
  37. package/src/constructors.ts +157 -0
  38. package/src/generator.ts +294 -0
  39. package/src/index.ts +13 -0
  40. package/src/rules.ts +521 -0
  41. package/src/shield.ts +53 -0
  42. package/src/types.ts +94 -0
  43. package/src/utils.ts +101 -0
  44. package/src/validation.ts +90 -0
  45. package/tests/__snapshots__/input.test.ts.snap +7 -0
  46. package/tests/cache.test.ts +545 -0
  47. package/tests/constructors.test.ts +136 -0
  48. package/tests/fallback.test.ts +618 -0
  49. package/tests/fragments.test.ts +113 -0
  50. package/tests/generator.test.ts +356 -0
  51. package/tests/input.test.ts +63 -0
  52. package/tests/integration.test.ts +65 -0
  53. package/tests/logic.test.ts +530 -0
  54. package/tests/utils.test.ts +55 -0
  55. package/tests/validation.test.ts +139 -0
  56. package/tsconfig.json +10 -0
package/src/rules.ts ADDED
@@ -0,0 +1,521 @@
1
+ import * as Yup from 'yup'
2
+ import {
3
+ IRuleFunction,
4
+ IRule,
5
+ IRuleOptions,
6
+ ICache,
7
+ IFragment,
8
+ ICacheContructorOptions,
9
+ IRuleConstructorOptions,
10
+ ILogicRule,
11
+ ShieldRule,
12
+ IRuleResult,
13
+ IOptions,
14
+ IShieldContext,
15
+ } from './types.js'
16
+ import { isLogicRule } from './utils.js'
17
+ import { GraphQLResolveInfo } from 'graphql'
18
+
19
+ export class Rule implements IRule {
20
+ readonly name: string
21
+
22
+ private cache: ICache
23
+ private fragment?: IFragment
24
+ private func: IRuleFunction
25
+
26
+ constructor(
27
+ name: string,
28
+ func: IRuleFunction,
29
+ constructorOptions: IRuleConstructorOptions,
30
+ ) {
31
+ const options = this.normalizeOptions(constructorOptions)
32
+
33
+ this.name = name
34
+ this.func = func
35
+ this.cache = options.cache
36
+ this.fragment = options.fragment
37
+ }
38
+
39
+ /**
40
+ *
41
+ * @param parent
42
+ * @param args
43
+ * @param ctx
44
+ * @param info
45
+ *
46
+ * Resolves rule and writes to cache its result.
47
+ *
48
+ */
49
+ async resolve(
50
+ parent: object,
51
+ args: object,
52
+ ctx: IShieldContext,
53
+ info: GraphQLResolveInfo,
54
+ options: IOptions,
55
+ ): Promise<IRuleResult> {
56
+ try {
57
+ /* Resolve */
58
+ const res = await this.executeRule(parent, args, ctx, info, options)
59
+
60
+ if (res instanceof Error) {
61
+ return res
62
+ } else if (typeof res === 'string') {
63
+ return new Error(res)
64
+ } else if (res === true) {
65
+ return true
66
+ } else {
67
+ return false
68
+ }
69
+ } catch (err) {
70
+ if (options.debug) {
71
+ throw err
72
+ } else {
73
+ return false
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ *
80
+ * @param rule
81
+ *
82
+ * Compares a given rule with the current one
83
+ * and checks whether their functions are equal.
84
+ *
85
+ */
86
+ equals(rule: Rule): boolean {
87
+ return this.func === rule.func
88
+ }
89
+
90
+ /**
91
+ *
92
+ * Extracts fragment from the rule.
93
+ *
94
+ */
95
+ extractFragment(): IFragment | undefined {
96
+ return this.fragment
97
+ }
98
+
99
+ /**
100
+ *
101
+ * @param options
102
+ *
103
+ * Sets default values for options.
104
+ *
105
+ */
106
+ private normalizeOptions(options: IRuleConstructorOptions): IRuleOptions {
107
+ return {
108
+ cache:
109
+ options.cache !== undefined
110
+ ? this.normalizeCacheOption(options.cache)
111
+ : 'no_cache',
112
+ fragment: options.fragment !== undefined ? options.fragment : undefined,
113
+ }
114
+ }
115
+
116
+ /**
117
+ *
118
+ * @param cache
119
+ *
120
+ * This ensures backward capability of shield.
121
+ *
122
+ */
123
+ private normalizeCacheOption(cache: ICacheContructorOptions): ICache {
124
+ switch (cache) {
125
+ case true: {
126
+ return 'strict'
127
+ }
128
+ case false: {
129
+ return 'no_cache'
130
+ }
131
+ default: {
132
+ return cache
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Executes a rule and writes to cache if needed.
139
+ *
140
+ * @param parent
141
+ * @param args
142
+ * @param ctx
143
+ * @param info
144
+ */
145
+ private executeRule(
146
+ parent: object,
147
+ args: object,
148
+ ctx: IShieldContext,
149
+ info: GraphQLResolveInfo,
150
+ options: IOptions,
151
+ ): string | boolean | Error | Promise<IRuleResult> {
152
+ switch (typeof this.cache) {
153
+ case 'function': {
154
+ /* User defined cache function. */
155
+ const key = `${this.name}-${this.cache(parent, args, ctx, info)}`
156
+ return this.writeToCache(key)(parent, args, ctx, info)
157
+ }
158
+ case 'string': {
159
+ /* Standard cache option. */
160
+ switch (this.cache) {
161
+ case 'strict': {
162
+ const key = options.hashFunction({ parent, args })
163
+
164
+ return this.writeToCache(`${this.name}-${key}`)(
165
+ parent,
166
+ args,
167
+ ctx,
168
+ info,
169
+ )
170
+ }
171
+ case 'contextual': {
172
+ return this.writeToCache(this.name)(parent, args, ctx, info)
173
+ }
174
+ case 'no_cache': {
175
+ return this.func(parent, args, ctx, info)
176
+ }
177
+ }
178
+ }
179
+ /* istanbul ignore next */
180
+ default: {
181
+ throw new Error(`Unsupported cache format: ${typeof this.cache}`)
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Writes or reads result from cache.
188
+ *
189
+ * @param key
190
+ */
191
+
192
+ private writeToCache(
193
+ key: string,
194
+ ): (
195
+ parent: object,
196
+ args: object,
197
+ ctx: IShieldContext,
198
+ info: GraphQLResolveInfo,
199
+ ) => string | boolean | Error | Promise<IRuleResult> {
200
+ return (parent, args, ctx, info) => {
201
+ if (!ctx._shield.cache[key]) {
202
+ ctx._shield.cache[key] = this.func(parent, args, ctx, info)
203
+ }
204
+ return ctx._shield.cache[key]
205
+ }
206
+ }
207
+ }
208
+
209
+ export class InputRule<T> extends Rule {
210
+ constructor(
211
+ name: string,
212
+ schema: (yup: typeof Yup, ctx: IShieldContext) => Yup.BaseSchema<T>,
213
+ options?: Parameters<Yup.BaseSchema<T>['validate']>[1],
214
+ ) {
215
+ const validationFunction: IRuleFunction = (
216
+ parent: object,
217
+ args: object,
218
+ ctx: IShieldContext,
219
+ ) =>
220
+ schema(Yup, ctx)
221
+ .validate(args, options)
222
+ .then(() => true)
223
+ .catch((err) => err)
224
+
225
+ super(name, validationFunction, { cache: 'strict', fragment: undefined })
226
+ }
227
+ }
228
+
229
+ export class LogicRule implements ILogicRule {
230
+ private rules: ShieldRule[]
231
+
232
+ constructor(rules: ShieldRule[]) {
233
+ this.rules = rules
234
+ }
235
+
236
+ /**
237
+ * By default logic rule resolves to false.
238
+ */
239
+ async resolve(
240
+ parent: object,
241
+ args: object,
242
+ ctx: IShieldContext,
243
+ info: GraphQLResolveInfo,
244
+ options: IOptions,
245
+ ): Promise<IRuleResult> {
246
+ return false
247
+ }
248
+
249
+ /**
250
+ * Evaluates all the rules.
251
+ */
252
+ async evaluate(
253
+ parent: object,
254
+ args: object,
255
+ ctx: IShieldContext,
256
+ info: GraphQLResolveInfo,
257
+ options: IOptions,
258
+ ): Promise<IRuleResult[]> {
259
+ const rules = this.getRules()
260
+ const tasks = rules.map((rule) =>
261
+ rule.resolve(parent, args, ctx, info, options),
262
+ )
263
+
264
+ return Promise.all(tasks)
265
+ }
266
+
267
+ /**
268
+ * Returns rules in a logic rule.
269
+ */
270
+ getRules() {
271
+ return this.rules
272
+ }
273
+
274
+ /**
275
+ * Extracts fragments from the defined rules.
276
+ */
277
+ extractFragments(): IFragment[] {
278
+ const fragments = this.rules.reduce<IFragment[]>((fragments, rule) => {
279
+ if (isLogicRule(rule)) {
280
+ return fragments.concat(...rule.extractFragments())
281
+ }
282
+
283
+ const fragment = rule.extractFragment()
284
+ if (fragment) return fragments.concat(fragment)
285
+
286
+ return fragments
287
+ }, [])
288
+
289
+ return fragments
290
+ }
291
+ }
292
+
293
+ // Extended Types
294
+
295
+ export class RuleOr extends LogicRule {
296
+ constructor(rules: ShieldRule[]) {
297
+ super(rules)
298
+ }
299
+
300
+ /**
301
+ * Makes sure that at least one of them has evaluated to true.
302
+ */
303
+ async resolve(
304
+ parent: object,
305
+ args: object,
306
+ ctx: IShieldContext,
307
+ info: GraphQLResolveInfo,
308
+ options: IOptions,
309
+ ): Promise<IRuleResult> {
310
+ const result = await this.evaluate(parent, args, ctx, info, options)
311
+
312
+ if (result.every((res) => res !== true)) {
313
+ const customError = result.find((res) => res instanceof Error)
314
+ return customError || false
315
+ } else {
316
+ return true
317
+ }
318
+ }
319
+ }
320
+
321
+ export class RuleAnd extends LogicRule {
322
+ constructor(rules: ShieldRule[]) {
323
+ super(rules)
324
+ }
325
+
326
+ /**
327
+ * Makes sure that all of them have resolved to true.
328
+ */
329
+ async resolve(
330
+ parent: object,
331
+ args: object,
332
+ ctx: IShieldContext,
333
+ info: GraphQLResolveInfo,
334
+ options: IOptions,
335
+ ): Promise<IRuleResult> {
336
+ const result = await this.evaluate(parent, args, ctx, info, options)
337
+
338
+ if (result.some((res) => res !== true)) {
339
+ const customError = result.find((res) => res instanceof Error)
340
+ return customError || false
341
+ } else {
342
+ return true
343
+ }
344
+ }
345
+ }
346
+
347
+ export class RuleChain extends LogicRule {
348
+ constructor(rules: ShieldRule[]) {
349
+ super(rules)
350
+ }
351
+
352
+ /**
353
+ * Makes sure that all of them have resolved to true.
354
+ */
355
+ async resolve(
356
+ parent: object,
357
+ args: object,
358
+ ctx: IShieldContext,
359
+ info: GraphQLResolveInfo,
360
+ options: IOptions,
361
+ ): Promise<IRuleResult> {
362
+ const result = await this.evaluate(parent, args, ctx, info, options)
363
+
364
+ if (result.some((res) => res !== true)) {
365
+ const customError = result.find((res) => res instanceof Error)
366
+ return customError || false
367
+ } else {
368
+ return true
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Evaluates all the rules.
374
+ */
375
+ async evaluate(
376
+ parent: object,
377
+ args: object,
378
+ ctx: IShieldContext,
379
+ info: GraphQLResolveInfo,
380
+ options: IOptions,
381
+ ): Promise<IRuleResult[]> {
382
+ const rules = this.getRules()
383
+
384
+ return iterate(rules)
385
+
386
+ async function iterate([rule, ...otherRules]: ShieldRule[]): Promise<
387
+ IRuleResult[]
388
+ > {
389
+ if (rule === undefined) return []
390
+ return rule.resolve(parent, args, ctx, info, options).then((res) => {
391
+ if (res !== true) {
392
+ return [res]
393
+ } else {
394
+ return iterate(otherRules).then((ress) => ress.concat(res))
395
+ }
396
+ })
397
+ }
398
+ }
399
+ }
400
+
401
+ export class RuleRace extends LogicRule {
402
+ constructor(rules: ShieldRule[]) {
403
+ super(rules)
404
+ }
405
+
406
+ /**
407
+ * Makes sure that at least one of them resolved to true.
408
+ */
409
+ async resolve(
410
+ parent: object,
411
+ args: object,
412
+ ctx: IShieldContext,
413
+ info: GraphQLResolveInfo,
414
+ options: IOptions,
415
+ ): Promise<IRuleResult> {
416
+ const result = await this.evaluate(parent, args, ctx, info, options)
417
+
418
+ if (result.some((res) => res === true)) {
419
+ return true
420
+ } else {
421
+ const customError = result.find((res) => res instanceof Error)
422
+ return customError || false
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Evaluates all the rules.
428
+ */
429
+ async evaluate(
430
+ parent: object,
431
+ args: object,
432
+ ctx: IShieldContext,
433
+ info: GraphQLResolveInfo,
434
+ options: IOptions,
435
+ ): Promise<IRuleResult[]> {
436
+ const rules = this.getRules()
437
+
438
+ return iterate(rules)
439
+
440
+ async function iterate([rule, ...otherRules]: ShieldRule[]): Promise<
441
+ IRuleResult[]
442
+ > {
443
+ if (rule === undefined) return []
444
+ return rule.resolve(parent, args, ctx, info, options).then((res) => {
445
+ if (res === true) {
446
+ return [res]
447
+ } else {
448
+ return iterate(otherRules).then((ress) => ress.concat(res))
449
+ }
450
+ })
451
+ }
452
+ }
453
+ }
454
+
455
+ export class RuleNot extends LogicRule {
456
+ error?: Error
457
+
458
+ constructor(rule: ShieldRule, error?: Error) {
459
+ super([rule])
460
+ this.error = error
461
+ }
462
+
463
+ /**
464
+ *
465
+ * @param parent
466
+ * @param args
467
+ * @param ctx
468
+ * @param info
469
+ *
470
+ * Negates the result.
471
+ *
472
+ */
473
+ async resolve(
474
+ parent: object,
475
+ args: object,
476
+ ctx: IShieldContext,
477
+ info: GraphQLResolveInfo,
478
+ options: IOptions,
479
+ ): Promise<IRuleResult> {
480
+ const [res] = await this.evaluate(parent, args, ctx, info, options)
481
+
482
+ if (res instanceof Error) {
483
+ return true
484
+ } else if (res !== true) {
485
+ return true
486
+ } else {
487
+ if (this.error) return this.error
488
+ return false
489
+ }
490
+ }
491
+ }
492
+
493
+ export class RuleTrue extends LogicRule {
494
+ constructor() {
495
+ super([])
496
+ }
497
+
498
+ /**
499
+ *
500
+ * Always true.
501
+ *
502
+ */
503
+ async resolve(): Promise<IRuleResult> {
504
+ return true
505
+ }
506
+ }
507
+
508
+ export class RuleFalse extends LogicRule {
509
+ constructor() {
510
+ super([])
511
+ }
512
+
513
+ /**
514
+ *
515
+ * Always false.
516
+ *
517
+ */
518
+ async resolve(): Promise<IRuleResult> {
519
+ return false
520
+ }
521
+ }
package/src/shield.ts ADDED
@@ -0,0 +1,53 @@
1
+ import hash from 'object-hash'
2
+ import { middleware, IMiddlewareGenerator } from 'graphql-middleware'
3
+ import { ValidationError, validateRuleTree } from './validation.js'
4
+ import { IRules, IOptions, IOptionsConstructor, ShieldRule, IHashFunction, IFallbackErrorType } from './types.js'
5
+ import { generateMiddlewareGeneratorFromRuleTree } from './generator.js'
6
+ import { allow } from './constructors.js'
7
+ import { withDefault } from './utils.js'
8
+
9
+ /**
10
+ *
11
+ * @param options
12
+ *
13
+ * Makes sure all of defined rules are in accord with the options
14
+ * shield can process.
15
+ *
16
+ */
17
+ function normalizeOptions(options: IOptionsConstructor): IOptions {
18
+ if (typeof options.fallbackError === 'string') {
19
+ options.fallbackError = new Error(options.fallbackError)
20
+ }
21
+
22
+ return {
23
+ debug: options.debug !== undefined ? options.debug : false,
24
+ allowExternalErrors: withDefault(false)(options.allowExternalErrors),
25
+ fallbackRule: withDefault<ShieldRule>(allow)(options.fallbackRule),
26
+ fallbackError: withDefault<IFallbackErrorType>(new Error('Not Authorised!'))(options.fallbackError),
27
+ hashFunction: withDefault<IHashFunction>(hash)(options.hashFunction),
28
+ }
29
+ }
30
+
31
+ /**
32
+ *
33
+ * @param ruleTree
34
+ * @param options
35
+ *
36
+ * Validates rules and generates middleware from defined rule tree.
37
+ *
38
+ */
39
+ export function shield<TSource = any, TContext = any, TArgs = any>(
40
+ ruleTree: IRules,
41
+ options: IOptionsConstructor = {},
42
+ ): IMiddlewareGenerator<TSource, TContext, TArgs> {
43
+ const normalizedOptions = normalizeOptions(options)
44
+ const ruleTreeValidity = validateRuleTree(ruleTree)
45
+
46
+ if (ruleTreeValidity.status === 'ok') {
47
+ const generatorFunction = generateMiddlewareGeneratorFromRuleTree<TSource, TContext, TArgs>(ruleTree, normalizedOptions)
48
+
49
+ return middleware(generatorFunction)
50
+ } else {
51
+ throw new ValidationError(ruleTreeValidity.message)
52
+ }
53
+ }
package/src/types.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { GraphQLResolveInfo } from 'graphql'
2
+ import { IMiddlewareGenerator } from 'graphql-middleware'
3
+
4
+ // Rule
5
+
6
+ export type ShieldRule = IRule | ILogicRule
7
+
8
+ export interface IRule {
9
+ readonly name: string
10
+
11
+ equals(rule: IRule): boolean
12
+ extractFragment(): IFragment | undefined
13
+ resolve(parent: object, args: object, ctx: IShieldContext, info: GraphQLResolveInfo, options: IOptions): Promise<IRuleResult>
14
+ }
15
+
16
+ export interface IRuleOptions {
17
+ cache: ICache
18
+ fragment?: IFragment
19
+ }
20
+
21
+ export interface ILogicRule {
22
+ getRules(): ShieldRule[]
23
+ extractFragments(): IFragment[]
24
+ evaluate(parent: object, args: object, ctx: IShieldContext, info: GraphQLResolveInfo, options: IOptions): Promise<IRuleResult[]>
25
+ resolve(parent: object, args: object, ctx: IShieldContext, info: GraphQLResolveInfo, options: IOptions): Promise<IRuleResult>
26
+ }
27
+
28
+ export type IFragment = string
29
+ export type ICache = 'strict' | 'contextual' | 'no_cache' | ICacheKeyFn
30
+ export type ICacheKeyFn = (parent: any, args: any, ctx: any, info: GraphQLResolveInfo) => string
31
+ export type IRuleResult = boolean | string | Error
32
+ export type IRuleFunction = (parent: any, args: any, ctx: any, info: GraphQLResolveInfo) => IRuleResult | Promise<IRuleResult>
33
+
34
+ // Rule Constructor Options
35
+
36
+ export type ICacheContructorOptions = ICache | boolean
37
+
38
+ export interface IRuleConstructorOptions {
39
+ cache?: ICacheContructorOptions
40
+ fragment?: IFragment
41
+ }
42
+
43
+ // Rules Definition Tree
44
+
45
+ export interface IRuleTypeMap {
46
+ [key: string]: ShieldRule | IRuleFieldMap
47
+ }
48
+
49
+ export interface IRuleFieldMap {
50
+ [key: string]: ShieldRule
51
+ }
52
+
53
+ export type IRules = ShieldRule | IRuleTypeMap
54
+
55
+ export type IHashFunction = (arg: { parent: any; args: any }) => string
56
+
57
+ export type IFallbackErrorMapperType = (
58
+ err: unknown,
59
+ parent: object,
60
+ args: object,
61
+ ctx: IShieldContext,
62
+ info: GraphQLResolveInfo,
63
+ ) => Promise<Error> | Error
64
+
65
+ export type IFallbackErrorType = Error | IFallbackErrorMapperType
66
+
67
+ // Generator Options
68
+
69
+ export interface IOptions {
70
+ debug: boolean
71
+ allowExternalErrors: boolean
72
+ fallbackRule: ShieldRule
73
+ fallbackError?: IFallbackErrorType
74
+ hashFunction: IHashFunction
75
+ }
76
+
77
+ export interface IOptionsConstructor {
78
+ debug?: boolean
79
+ allowExternalErrors?: boolean
80
+ fallbackRule?: ShieldRule
81
+ fallbackError?: string | IFallbackErrorType
82
+ hashFunction?: IHashFunction
83
+ }
84
+
85
+ export declare function shield<TSource = any, TContext = any, TArgs = any>(
86
+ ruleTree: IRules,
87
+ options: IOptions,
88
+ ): IMiddlewareGenerator<TSource, TContext, TArgs>
89
+
90
+ export interface IShieldContext {
91
+ _shield: {
92
+ cache: { [key: string]: IRuleResult | Promise<IRuleResult> }
93
+ }
94
+ }