diff --git a/src/index.ts b/src/index.ts index 867748b8..ba16a6d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,8 @@ export { partial, } from './builders'; export { ShieldCache } from './rules'; -export { nexusShield, FieldShieldResolver } from './plugin'; +export { + nexusShield, + FieldShieldResolver, + ObjectTypeShieldResolver, +} from './plugin'; diff --git a/src/plugin.ts b/src/plugin.ts index 8e2db921..141c959a 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -19,7 +19,7 @@ const FieldShieldType = printedGenTyping({ optional: true, name: 'shield', description: ` - Rule to execute + Authorization rule to execute for this field `, type: 'FieldShieldResolver', imports: [FieldShieldImport], @@ -30,6 +30,26 @@ export type FieldShieldResolver< FieldName extends string > = ShieldRule; +const ObjectTypeShieldImport = printedGenTypingImport({ + module: 'nexus-shield', + bindings: ['ObjectTypeShieldResolver'], +}); + +const ObjectTypeFieldShieldType = printedGenTyping({ + optional: true, + name: 'shield', + description: ` + Default authorization rule to execute on all fields of this object + `, + type: 'ObjectTypeShieldResolver', + imports: [ObjectTypeShieldImport], +}); + +export type ObjectTypeShieldResolver = ShieldRule< + TypeName, + never +>; + export const nexusShield = (settings: ShieldPluginSettings) => { const options = { defaultRule: settings.defaultRule || allow, @@ -41,13 +61,18 @@ export const nexusShield = (settings: ShieldPluginSettings) => { name: 'Nexus Shield Plugin', description: 'Ease the creation of the authorization layer', fieldDefTypes: FieldShieldType, + objectTypeDefTypes: ObjectTypeFieldShieldType, onCreateFieldResolver(config) { // Find the field rule + const objectRule = + config.parentTypeConfig.extensions?.nexus?.config.shield; const fieldRule = config.fieldConfig.extensions?.nexus?.config.shield; let rule: ShieldRule | undefined; if (isShieldRule(fieldRule)) { rule = fieldRule; + } else if (isShieldRule(objectRule)) { + rule = objectRule; } else if (options.defaultRule) { rule = options.defaultRule; } diff --git a/tests/fixtures/schema.ts b/tests/fixtures/schema.ts index 13d416a6..6a55d1bf 100644 --- a/tests/fixtures/schema.ts +++ b/tests/fixtures/schema.ts @@ -5,8 +5,12 @@ import { ruleType } from '../../src'; export const Test = objectType({ name: 'Test', + shield: ruleType({ + resolve(_root, _args, _ctx) { + throw new AuthenticationError('OBJECT'); + }, + }), definition(t) { - t.id('id'); t.string('publicProp', { shield: ruleType({ resolve(_root, _args, _ctx) { @@ -28,6 +32,7 @@ export const Test = objectType({ }, }), }); + t.string('defaultProp'); }, }); @@ -38,10 +43,10 @@ export const QueryTest = extendType({ type: Test, resolve(_root, _args, _ctx) { return { - id: 'BEEF', publicProp: 'public', privateProp: 'private', throwProp: 'throwProp', + defaultProp: 'defaultProp', }; }, }); diff --git a/tests/fixtures/server.ts b/tests/fixtures/server.ts index 9fa8f803..dbe4e49b 100644 --- a/tests/fixtures/server.ts +++ b/tests/fixtures/server.ts @@ -2,7 +2,12 @@ import { makeSchema } from '@nexus/schema'; import { ApolloServer, ForbiddenError } from 'apollo-server'; import * as path from 'path'; -import { allow, FieldShieldResolver, nexusShield } from '../../src'; +import { + allow, + FieldShieldResolver, + nexusShield, + ObjectTypeShieldResolver, +} from '../../src'; import * as types from './schema'; declare global { @@ -12,6 +17,10 @@ declare global { > { shield?: FieldShieldResolver; } + + interface NexusGenPluginTypeConfig { + shield?: ObjectTypeShieldResolver; + } } const schema = makeSchema({ diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 7db1c800..6d4210b8 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -17,7 +17,6 @@ describe('Integration tests', () => { const query = gql` query { test { - id publicProp } } @@ -25,14 +24,13 @@ describe('Integration tests', () => { const result = await client.query({ query }); - expect(result.data).toEqual({ test: { id: 'BEEF', publicProp: 'public' } }); + expect(result.data).toEqual({ test: { publicProp: 'public' } }); }); test('Server should return default error if not authorized', async () => { const query = gql` query { test { - id privateProp } } @@ -47,7 +45,6 @@ describe('Integration tests', () => { const query = gql` query { test { - id throwProp } } @@ -57,4 +54,18 @@ describe('Integration tests', () => { expect(result.errors[0].message).toEqual('CUSTOM'); }); + + test('Server should use default object rule', async () => { + const query = gql` + query { + test { + defaultProp + } + } + `; + + const result = await client.query({ query }); + + expect(result.errors[0].message).toEqual('OBJECT'); + }); });