Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

Commit

Permalink
Add extensions examples
Browse files Browse the repository at this point in the history
  • Loading branch information
hihuz committed Jan 10, 2020
1 parent fd3b92f commit 9409d3d
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 0 deletions.
43 changes: 43 additions & 0 deletions examples/extensions/authorizer.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { GraphQLResolveInfo, GraphQLObjectType } from "graphql";

import { MiddlewareFn } from "../../src";
import { Context } from "./context.interface";
import { UnauthorizedError } from "../../src/errors";

const extractAuthorizationExtensions = (info: GraphQLResolveInfo) => {
const parentAuthorizationExtensions =
(info.parentType.extensions && info.parentType.extensions.authorization) || {};
const returnType = info.returnType as GraphQLObjectType;
const returnTypeAuthorizationExtensions =
(returnType.extensions && returnType.extensions.authorization) || {};
const field = info.parentType.getFields()[info.fieldName];
const fieldAuthorizationExtensions = (field.extensions && field.extensions.authorization) || {};

return {
...parentAuthorizationExtensions,
...returnTypeAuthorizationExtensions,
...fieldAuthorizationExtensions,
};
};

export const AuthorizerMiddleware: MiddlewareFn = async (
{ context: { user }, info }: { context: Context; info: GraphQLResolveInfo },
next,
) => {
const { restricted = false, roles = [] } = extractAuthorizationExtensions(info);

if (restricted) {
if (!user) {
// if no user, restrict access
throw new UnauthorizedError();
}

if (roles.length > 0 && !user.roles.some(role => roles.includes(role))) {
// if the roles don't overlap, restrict access
throw new UnauthorizedError();
}
}

// grant access in other cases
await next();
};
5 changes: 5 additions & 0 deletions examples/extensions/context.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { User } from "./user.interface";

export interface Context {
user?: User;
}
9 changes: 9 additions & 0 deletions examples/extensions/custom.authorized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Extensions } from "../../src";

export const CustomAuthorized = (roles: string | string[] = []) =>
Extensions({
authorization: {
restricted: true,
roles: typeof roles === "string" ? [roles] : roles,
},
});
36 changes: 36 additions & 0 deletions examples/extensions/examples.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
query GetPublicRecipes {
recipes {
title
description
averageRating
}
}

query GetRecipesForAuthedUser {
recipes {
title
description
ingredients
averageRating
}
}

query GetRecipesForAdmin {
recipes {
title
description
ingredients
averageRating
ratings
}
}

mutation AddRecipeByAuthedUser {
addRecipe(title: "Sample Recipe") {
averageRating
}
}

mutation DeleteRecipeByAdmin {
deleteRecipe(title: "Recipe 1")
}
37 changes: 37 additions & 0 deletions examples/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import { buildSchema } from "../../src";

import { ExampleResolver } from "./resolver";
import { Context } from "./context.interface";
import { AuthorizerMiddleware } from "./authorizer.middleware";
import { LoggerMiddleware } from "./logger.middleware";

void (async function bootstrap() {
// build TypeGraphQL executable schema
const schema = await buildSchema({
resolvers: [ExampleResolver],
globalMiddlewares: [AuthorizerMiddleware, LoggerMiddleware],
});

// Create GraphQL server
const server = new ApolloServer({
schema,
context: () => {
const ctx: Context = {
// create mocked user in context
// in real app you would be mapping user from `req.user` or sth
user: {
id: 1,
name: "Sample user",
roles: ["REGULAR"],
},
};
return ctx;
},
});

// Start the server
const { url } = await server.listen(4000);
console.log(`Server is running, GraphQL Playground available at ${url}`);
})();
21 changes: 21 additions & 0 deletions examples/extensions/logger.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Service } from "typedi";
import { MiddlewareInterface, NextFn, ResolverData } from "../../src";

import { Context } from "./context.interface";
import { Logger } from "./logger";

@Service()
export class LoggerMiddleware implements MiddlewareInterface<Context> {
constructor(private readonly logger: Logger) {}

async use({ context: { user }, info }: ResolverData<Context>, next: NextFn) {
const { logMessage, logLevel = 0 } =
info.parentType.getFields()[info.fieldName].extensions || {};

if (logMessage) {
this.logger.log(`${logMessage}${user ? ` (user: ${user.id})` : ""}`, logLevel);
}

return next();
}
}
9 changes: 9 additions & 0 deletions examples/extensions/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Service } from "typedi";

@Service()
export class Logger {
log(...args: any[]) {
// replace with more sophisticated solution :)
console.log(...args);
}
}
27 changes: 27 additions & 0 deletions examples/extensions/recipe.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { plainToClass } from "class-transformer";

import { Recipe } from "./recipe.type";

export function createRecipe(recipeData: Partial<Recipe>): Recipe {
return plainToClass(Recipe, recipeData);
}

export const sampleRecipes = [
createRecipe({
title: "Recipe 1",
description: "Desc 1",
ingredients: ["one", "two", "three"],
ratings: [3, 4, 5, 5, 5],
}),
createRecipe({
title: "Recipe 2",
description: "Desc 2",
ingredients: ["four", "five", "six"],
ratings: [3, 4, 5, 3, 2],
}),
createRecipe({
title: "Recipe 3",
ingredients: ["seven", "eight", "nine"],
ratings: [4, 4, 5, 5, 4],
}),
];
26 changes: 26 additions & 0 deletions examples/extensions/recipe.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ObjectType, Extensions, Field, Int, Float } from "../../src";
import { CustomAuthorized } from "./custom.authorized";

@ObjectType()
@CustomAuthorized() // restrict access to all receipe fields only for logged users
export class Recipe {
@Field()
title: string;

@Field({ nullable: true })
description?: string;

@Field(type => [String])
@Extensions({ logMessage: "ingredients accessed" })
@Extensions({ logLevel: 4 })
ingredients: string[];

@CustomAuthorized("ADMIN") // restrict access to rates details for admin only, this will override the object type custom authorization
@Field(type => [Int])
ratings: number[];

@Field(type => Float, { nullable: true })
get averageRating(): number | null {
return this.ratings.reduce((a, b) => a + b, 0) / this.ratings.length;
}
}
42 changes: 42 additions & 0 deletions examples/extensions/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Resolver, Query, Mutation, Arg, Extensions } from "../../src";
import { CustomAuthorized } from "./custom.authorized";

import { Recipe } from "./recipe.type";
import { createRecipe, sampleRecipes } from "./recipe.helpers";

@Resolver()
export class ExampleResolver {
private recipesData: Recipe[] = sampleRecipes.slice();

@Extensions({ some: "data" })
@Query(returns => [Recipe])
async recipes(): Promise<Recipe[]> {
return await this.recipesData;
}

@CustomAuthorized() // only logged users can add new recipe
@Mutation()
addRecipe(
@Arg("title") title: string,
@Arg("description", { nullable: true }) description?: string,
): Recipe {
const newRecipe = createRecipe({
title,
description,
ratings: [],
});
this.recipesData.push(newRecipe);
return newRecipe;
}

@CustomAuthorized("ADMIN") // only admin can remove the published recipe
@Mutation()
deleteRecipe(@Arg("title") title: string): boolean {
const foundRecipeIndex = this.recipesData.findIndex(it => it.title === title);
if (!foundRecipeIndex) {
return false;
}
this.recipesData.splice(foundRecipeIndex, 1);
return true;
}
}
5 changes: 5 additions & 0 deletions examples/extensions/user.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface User {
id: number;
name: string;
roles: string[];
}

0 comments on commit 9409d3d

Please sign in to comment.