Skip to content

Commit

Permalink
feat(core): Fixes cache issues
Browse files Browse the repository at this point in the history
+ Introduces three cache options - no_cache, contextual and strict.
+ Fixes #59
+ New renovate rules for auto merging
  • Loading branch information
maticzav committed Jun 18, 2018
1 parent 926cc5d commit 9428a7a
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 29 deletions.
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ type IRuleFunction = (
info: GraphQLResolveInfo,
) => Promise<boolean>

export type ICache = 'strict' | 'contextual' | 'no_cache'

export interface IRuleOptions {
cache?: boolean
cache?: ICache
}

// Logic
Expand Down Expand Up @@ -213,16 +215,38 @@ A rule map must match your schema definition. All rules must be created using th

```jsx
// Normal
const admin = rule({ cache: true })(async (parent, args, ctx, info) => true)
const admin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => true)

// With external data
const admin = bool =>
rule(`name`, { cache: true })(async (parent, args, ctx, info) => bool)
rule(`name`, { cache: 'contextual' })(async (parent, args, ctx, info) => bool)
```

* Cache is enabled by default accross all rules. To prevent `cache` generation, set `{ cache: false }` when generating a rule.
* Cache is enabled by default accross all rules. To prevent `cache` generation, set `{ cache: 'no_cache' }` when generating a rule.
* By default, no rule is executed more than once in complete query execution. This accounts for significantly better load times and quick responses.

##### Cache

You can choose from three different cache options.

1. `no_cache` - prevents rules from being cached.
1. `contextual` - use when rule only relies on `ctx` parameter.
1. `strict` - use when rule relies on `parent` or `args` parameter as well.

```ts
// Contextual
const admin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
return ctx.user.isAdmin
})

// Strict
const admin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
return ctx.user.isAdmin || args.code === 'secret' || parent.id === 'theone'
})
```

> Backward compatiblity: `{ cache: false }` converts to `no_cache`, and `{ cache: true }` converts to `strict`.
#### `options`

| Property | Required | Default | Description |
Expand Down
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
},
"dependencies": {
"graphql-middleware": "1.2.5",
"object-hash": "^1.3.0",
"opencollective": "1.0.3"
},
"devDependencies": {
"@types/graphql": "0.13.0",
"@types/node": "9.6.18",
"@types/object-hash": "^1.2.0",
"ava": "0.25.0",
"graphql": "0.13.2",
"graphql-tools": "3.0.1",
Expand All @@ -35,7 +37,9 @@
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0"
},
"files": ["dist"],
"files": [
"dist"
],
"release": {
"branch": "master"
},
Expand All @@ -47,7 +51,12 @@
"bugs": {
"url": "https://github.com/maticzav/graphql-shield/issues"
},
"keywords": ["graphql", "permissions", "shield", "server"],
"keywords": [
"graphql",
"permissions",
"shield",
"server"
],
"license": "MIT",
"collective": {
"type": "opencollective",
Expand Down
4 changes: 2 additions & 2 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"extends": ["config:base", "docker:disable"],
"extends": ["config:base", "docker:disable", ":automergePatch"],
"pathRules": [
{
"paths": ["examples/**"],
"extends": [":semanticCommitTypeAll(chore)", ":automergeMinor"],
"extends": [":semanticCommitTypeAll(chore)", ":automergePatch"],
"branchName":
"{{branchPrefix}}examples-{{depNameSanitized}}-{{newVersionMajor}}.x"
}
Expand Down
75 changes: 58 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { IMiddleware } from 'graphql-middleware'
import { IRuleFunction, IRule, IRuleOptions, IRules, IOptions } from './types'
import * as hash from 'object-hash'
import {
IRuleFunction,
IRule,
IRuleOptions,
IRules,
IOptions,
ICache,
ICacheOptions,
} from './types'
import { IMiddlewareFunction } from 'graphql-middleware/dist/types'

export { IRules }
Expand All @@ -14,7 +23,7 @@ export class CustomError extends Error {

export class Rule {
readonly name: string = undefined
private cache: boolean = true
private cache: ICache = 'contextual'
private _func: IRuleFunction

constructor(name: string, func: IRuleFunction, _options: IRuleOptions = {}) {
Expand All @@ -25,29 +34,54 @@ export class Rule {
this._func = func
}

normalizeOptions(options: IRuleOptions) {
return {
cache: options.cache !== undefined ? options.cache : true,
normalizeCacheOption(cache: ICacheOptions): ICache {
switch (cache) {
case true: {
return 'strict'
}
case false: {
return 'no_cache'
}
default: {
return cache
}
}
}

async _resolve(parent, args, ctx, info) {
return this._func(parent, args, ctx, info)
normalizeOptions(options: IRuleOptions) {
return {
cache:
options.cache !== undefined
? this.normalizeCacheOption(options.cache)
: 'contextual',
}
}

async _resolveWithCache(parent, args, ctx, info) {
if (!ctx._shield.cache[this.name]) {
ctx._shield.cache[this.name] = this._resolve(parent, args, ctx, info)
generateCacheKey(parent, args, ctx, info): string {
switch (this.cache) {
case 'strict': {
const _hash = hash({
parent,
args,
})
return `${this.name}-${_hash}`
}
case 'contextual': {
return this.name
}
case 'no_cache': {
return `${this.name}-${Math.random()}`
}
}
return ctx._shield.cache[this.name]
}

async resolve(parent, args, ctx, info): Promise<boolean> {
if (this.cache) {
return this._resolveWithCache(parent, args, ctx, info)
} else {
return this._resolve(parent, args, ctx, info)
const cacheKey = this.generateCacheKey(parent, args, ctx, info)

if (!ctx._shield.cache[cacheKey]) {
ctx._shield.cache[cacheKey] = this._func(parent, args, ctx, info)
}
return ctx._shield.cache[cacheKey]
}

equals(rule: Rule) {
Expand Down Expand Up @@ -234,7 +268,11 @@ const wrapResolverWithRule = (options: IOptions) => (
throw new CustomError('Not Authorised!')
}
} catch (err) {
if (err instanceof CustomError || options.debug || options.allowExternalErrors) {
if (
err instanceof CustomError ||
options.debug ||
options.allowExternalErrors
) {
throw err
} else {
throw new Error('Not Authorised!')
Expand Down Expand Up @@ -274,7 +312,10 @@ function generateMiddleware(ruleTree: IRules, options: IOptions): IMiddleware {
function normalizeOptions(options: IOptions): IOptions {
return {
debug: options.debug !== undefined ? options.debug : false,
allowExternalErrors: options.allowExternalErrors !== undefined ? options.allowExternalErrors : false,
allowExternalErrors:
options.allowExternalErrors !== undefined
? options.allowExternalErrors
: false,
}
}

Expand Down
16 changes: 14 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
} from 'graphql'
import { Rule, LogicRule } from './'

// Rules

export type IRuleFunction = (
parent: any,
args: any,
Expand All @@ -15,10 +17,18 @@ export type IRuleFunction = (

export type IRule = Rule | LogicRule

export type ICache = 'strict' | 'contextual' | 'no_cache'

// Rule Options

export type ICacheOptions = 'strict' | 'contextual' | 'no_cache' | boolean

export interface IRuleOptions {
cache?: boolean
cache?: ICacheOptions
}

// RuleMap

export interface IRuleTypeMap {
[key: string]: IRule | IRuleFieldMap
}
Expand All @@ -29,7 +39,9 @@ export interface IRuleFieldMap {

export type IRules = IRule | IRuleTypeMap

// Options

export interface IOptions {
debug?: boolean,
debug?: boolean
allowExternalErrors?: boolean
}
Loading

0 comments on commit 9428a7a

Please sign in to comment.