diff --git a/bonus/graphql-dynamic-queries/README.md b/bonus/graphql-dynamic-queries/README.md index 1b704af..20992e2 100644 --- a/bonus/graphql-dynamic-queries/README.md +++ b/bonus/graphql-dynamic-queries/README.md @@ -131,10 +131,10 @@ async function buildApp () { }, Grid: { resolveType (obj) { - if (obj.adminColumn) { + if (Object.hasOwnProperty.call(obj, 'adminColumn')) { return 'AdminGrid' } - if (obj.moderatorColumn) { + if (Object.hasOwnProperty.call(obj, 'moderatorColumn')) { return 'ModeratorGrid' } return 'UserGrid' @@ -214,14 +214,112 @@ async function doQuery (app, userType, query) { } ``` -For the sake of simplicity, we will not write all the tests here, -but you can find the complete code in the [GitHub repository](https://github.com/Eomm/fastify-discord-bot-demo/tree/HEAD/bonus/graphql-dynamic-queries). +For the sake of simplicity, we will not list all the tests here, +but you can find the complete complete source code in the [GitHub repository](https://github.com/Eomm/fastify-discord-bot-demo/tree/HEAD/bonus/graphql-dynamic-queries). Running the tests with `node test.js` will fail because we have not implemented the business logic yet. So, let's start writing the code! ## Implementing the server-side Dynamic Queries +To implement the business logic, there are these main steps: + +1. Retrieve the user's role from the request headers +2. Manage the GraphQL query to return the correct type based on the user's role + +Let's solve the first point. + +### How to retrieve the user's role + +We can implement the user role retrieval by installing the [`mercurius-auth`](https://github.com/mercurius-js/auth) plugin. + +```bash +npm i mercurius-auth@3 +``` + +Then, we can register the plugin in our `app.js` file. +To understand what the plugin does, you can read its documentation. + +In the following example, we will compare the `x-user-type` HTTP header with the `@auth` directive we are going to define in the schema. +If they match, the user will be authorized to access the field and run the query. + +Let's start by defining the `@auth` directive in the schema: + +```graphql +directive @auth( + role: String +) on OBJECT + +# ..same as before + +type AdminGrid @auth(role: "admin") { + totalRevenue: Float +} + +type ModeratorGrid @auth(role: "moderator") { + banHammer: Boolean +} + +type UserGrid @auth(role: "user") { + basicColumn: String +} +``` + +Then, we can register the plugin in our `app.js` file and implement a simple `searchData` resolver: + +```js +async function buildApp () { + const app = Fastify() // the same as before + + await app.register(GQL, { + schema, + resolvers: { + Query: { + searchData: async function (root, args, context, info) { + switch (context.auth.identity) { + case 'admin': + return { totalRevenue: 42 } + + case 'moderator': + return { banHammer: true } + + default: + return { basicColumn: 'basic' } + } + } + }, + }, + Grid: {} // the same as before + }) + + app.register(require('mercurius-auth'), { + authContext (context) { + return { + identity: context.reply.request.headers['x-user-type'] + } + }, + async applyPolicy (policy, parent, args, context, info) { + const role = policy.arguments[0].value.value + app.log.info('Applying policy %s on user %s', role, context.auth.identity) + + // we compare the schema role directive with the user role + return context.auth.identity === role + }, + authDirective: 'auth' + }) + + return app +} +``` + +Now, the user should be able to retrieve the `totalRevenue` field only if the `x-user-type` header is set to `admin`. + +Nevertheless, we can't run the tests yet because we have not implemented the second point. + +### Implementing the Dynamic Queries + + + ## Summary diff --git a/bonus/graphql-dynamic-queries/app.js b/bonus/graphql-dynamic-queries/app.js index f9a8161..addb0fd 100644 --- a/bonus/graphql-dynamic-queries/app.js +++ b/bonus/graphql-dynamic-queries/app.js @@ -10,23 +10,31 @@ if (process.argv[2] === 'run') { } async function buildApp () { - const app = Fastify() + const app = Fastify({ logger: !true }) await app.register(GQL, { schema, resolvers: { Query: { searchData: async function (root, args, context, info) { - // TODO: implement the business logic - return {} + switch (context.auth.identity) { + case 'admin': + return { totalRevenue: 42 } + + case 'moderator': + return { banHammer: true } + + default: + return { basicColumn: 'basic' } + } } }, Grid: { resolveType (obj) { - if (obj.adminColumn) { + if (Object.hasOwnProperty.call(obj, 'adminColumn')) { return 'AdminGrid' } - if (obj.moderatorColumn) { + if (Object.hasOwnProperty.call(obj, 'moderatorColumn')) { return 'ModeratorGrid' } return 'UserGrid' @@ -35,6 +43,23 @@ async function buildApp () { } }) + app.register(require('mercurius-auth'), { + authContext (context) { + // you can validate the headers here + return { + identity: context.reply.request.headers['x-user-type'] + } + }, + async applyPolicy (policy, parent, args, context, info) { + const role = policy.arguments[0].value.value + app.log.info('Applying policy %s on user %s', role, context.auth.identity) + + // we compare the schema role directive with the user role + return context.auth.identity === role + }, + authDirective: 'auth' + }) + return app } diff --git a/bonus/graphql-dynamic-queries/gql-schema.js b/bonus/graphql-dynamic-queries/gql-schema.js index cc45288..85850ca 100644 --- a/bonus/graphql-dynamic-queries/gql-schema.js +++ b/bonus/graphql-dynamic-queries/gql-schema.js @@ -10,16 +10,15 @@ type Query { union Grid = AdminGrid | ModeratorGrid | UserGrid - -type AdminGrid { +type AdminGrid @auth(role: "admin") { totalRevenue: Float } -type ModeratorGrid { +type ModeratorGrid @auth(role: "moderator") { banHammer: Boolean } -type UserGrid { +type UserGrid @auth(role: "user") { basicColumn: String } ` diff --git a/bonus/graphql-dynamic-queries/test.js b/bonus/graphql-dynamic-queries/test.js index 2ae5a14..6a921b9 100644 --- a/bonus/graphql-dynamic-queries/test.js +++ b/bonus/graphql-dynamic-queries/test.js @@ -4,6 +4,21 @@ const { test } = require('tap') const buildApp = require('./app') +test('Should not access', { skip: 'bug' }, async t => { + const app = await buildApp() + const res = await doQuery(app, 'none', ` + query { + searchData { + ... on AdminGrid { + totalRevenue + } + } + } + `) + + t.equal(res.data.errors[0].message, 'Failed auth policy check on totalRevenue') +}) + test('A user with the `admin` role should be able to retrieve the `totalRevenue` field without inline fragments', async t => { const app = await buildApp() const res = await doQuery(app, 'admin', ` @@ -14,8 +29,6 @@ test('A user with the `admin` role should be able to retrieve the `totalRevenue` } `) - console.log(JSON.stringify(res, null, 2)) - t.same(res.data.searchData, { totalRevenue: 42 }) })