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
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.
Argument | Type |
---|---|
reason | String |
@skip
If true
, the decorated field or fragment in an operation is not resolved by the GraphQL server.
Argument | Type |
---|---|
if | Boolean! |
@include
If false
, the decorated field or fragment in an operation is not resolved by the GraphQL server.
Argument | Type |
---|---|
if | Boolean! |
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) |
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
Query {
invoices: [Invoice!] @hasAccess(slug: "invoices", scope: "view", own: true)
}
@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
Mutation {
login(email: String!, password: String!): LoginData @ip
}
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
Mutation {
login(email: String!, password: String!): LoginData @ua
}
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;
}