Skip to main content

GraphQL directives

GraphQL directive decorates part of the schema (field, query, mutation) with additional handling. The Exiry core using Apollo GraphQL Server handles perfectly directives.

Directives are preceded by the @ character. Most of the used directive are @deprecated that will show the frontend developper that the field is deprecated and must use another one instead.

type PhoneNumber {
number: String!
countryCode: Int!
}
type User {
phoneNumberDigits: String @deprecated(reason: "Use `phoneNumber`.")
phoneNumberCountryCode: String @deprecated(reason: "Use `phoneNumber`.")
phoneNumber: PhoneNumber
}

Using multiple directive

One or many Directive can be applied to the GraphQL field or object

schema.gql
Mutation {
login(email: String!, password: String!): LoginData @ip @ua
}

Schema directives vs. operation directives

Usually, a given directive appears exclusively in GraphQL schemas or exclusively in GraphQL operations (rarely both, although the spec allows this).

For example, @deprecated is a schema-exclusive directive and @skip and @include are operation-exclusive directives.

Apollo's directives

The GraphQL specification defines the following default directives:

@deprecated

Marks the schema definition of a field or enum value as deprecated with an optional reason.

ArgumentType
reasonString

@skip

If true, the decorated field or fragment in an operation is not resolved by the GraphQL server.

ArgumentType
ifBoolean!

@include

If false, the decorated field or fragment in an operation is not resolved by the GraphQL server.

ArgumentType
ifBoolean!

Exiry's own directives

Beside the @deprecated directive from Apollo Server, Exiry adds a number of in-house Directives.

@isAuthenticated

The @isAuthenticated directive is used to verify is user is logged in.

Example and error

Query {
me: User @isAuthenticated
}

If the user is not connected, Exiry will throw an error

{
"errors": [
{
"message": "You are not authenticated.",
"locations": [...],
"path": ["me"],
"extensions": {...}
}
],
"data": {
"me": null
}
}

Context impact

This function will not check any rights or privileges. If the user is logged, the directive will populate the GraphQL Resolver Context with the a user object like so:

interface UserContext {
id: number;
email: string;
fullName: string;
}
interface Context {
user: UserContext;
}

This data will be populated based on the JWT token data.

@hasAccess

The @hasAccess directive is used to verify is user is logged and has the needed access. This directive uses Exiry's accesses plugin. The @hasAccess directive has 3 arguments, 2 are mandatory: | Argument | Type | Description| |--|--|--| | slug | string | the Access slug, based on UsersAccess model| | scope | string | the Scope can be one of the 5 fundamental rights | | own | boolean? | to be used if the user can have it's own data (like my invoices) |

Attribute: own

If the own attribute is not passed or false, the hasAccess directive will only check for the can_view privilege. If the the attribute is set to true then the directive will verify if the user has can_view or can_view_own privilege. If the user has both, the directive will return can_view

Example and error

schema.gql
Query {
invoices: [Invoice!] @hasAccess(slug: "invoices", scope: "view", own: true)
}
info

@hasAccess directive check's if user is logged in, no need to use @isAuthenticated in the same field or query

If the user is not connected, Exiry will throw an error

{
"errors": [
{
"message": "You are not authorized for this resource.",
"locations": [...],
"path": ["invoices"],
"extensions": {...}
}
],
"data": {
"invoices": null
}
}

Context impact

The directive will verify the user's role and privileges. If the user is has the needed access, the directive will populate the GraphQL Resolver Context with the a user object and a privilege attribute like so:

interface UserContext {
id: number;
email: string;
fullName: string;
}
interface Context {
user: UserContext;
privilege: string;
}

The privilege will be like canView, canViewOwn or canEdit etc...

@ip

If you need to get the user's IP in the resolve the IP directive will injects the user IP into the Context.

Example

schema.gql
Mutation {
login(email: String!, password: String!): LoginData @ip
}
resolver.ts
import type { Context } from "@exiry/core/@types/context";
import type { MutationLoginArgs } from "./@types";

const resolvers = {
Mutation: {
async login(_: any, _args: MutationLoginArgs, ctx: Context) {
// Get the user IP
const ip = ctx.ip;
},
},
};

export default resolvers;

Context impact

interface Context {
ip: string | null;
}

@ua

In some cases, the user's agent is needed to detect information like browser version. The @ua directive is used for such extant. When used it will inject to Context the UserAgent data. The directive uses NPM module express-useragent.

Example

schema.gql
Mutation {
login(email: String!, password: String!): LoginData @ua
}
resolver.ts
import type { Context } from "@exiry/core/@types/context";
import type { MutationLoginArgs } from "./@types";

const resolvers = {
Mutation: {
async login(_: any, _args: MutationLoginArgs, ctx: Context) {
// Get the user UA
const ua = ctx.ua;
},
},
};

export default resolvers;

Context impact

interface Details {
isMobile: boolean;
isMobileNative: boolean;
isTablet: boolean;
isiPad: boolean;
isiPod: boolean;
isiPhone: boolean;
isAndroid: boolean;
isBlackberry: boolean;
isOpera: boolean;
isIE: boolean;
isEdge: boolean;
isIECompatibilityMode: boolean;
isSafari: boolean;
isFirefox: boolean;
isWebkit: boolean;
isChrome: boolean;
isKonqueror: boolean;
isOmniWeb: boolean;
isSeaMonkey: boolean;
isFlock: boolean;
isAmaya: boolean;
isEpiphany: boolean;
isDesktop: boolean;
isWindows: boolean;
isWindowsPhone: boolean;
isLinux: boolean;
isLinux64: boolean;
isMac: boolean;
isChromeOS: boolean;
isBada: boolean;
isSamsung: boolean;
isRaspberry: boolean;
isBot: boolean;
isCurl: boolean;
isAndroidTablet: boolean;
isWinJs: boolean;
isKindleFire: boolean;
isSilk: boolean;
isCaptive: boolean;
isSmartTV: boolean;
silkAccelerated: boolean;
browser: string;
version: string;
os: string;
platform: string;
geoIp: { [key: string]: any };
source: string;
}
interface Context {
agent: Details | null;
}