Skip to content

A simple utility package of GraphQL schema directives

License

Notifications You must be signed in to change notification settings

thuoe/gql-util-directives

Repository files navigation

gql-util-directives

CI status npm version

Simple utility library for custom GraphQL schema directives

Get started

Install package:

npm install --save @thuoe/gql-util-directives

Example of importing the @regex directive & instantiating with Apollo Server:

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { makeExecutableSchema } from "@graphql-tools/schema";
import directives from "@thuoe/gql-util-directives";

const typeDefs = String.raw`#graphql
  type User {
    firstName: String
    lastName: String @regex(pattern: "\\b[A-Z]\\w+\\b")
    age: Int
  }

  type Query {
    user: User
  }
`;

const resolvers = {
  Query: {
    user: () => ({
      firstName: "Michael",
      lastName: "Jordan",
      age: 61,
    }),
  },
};

const { regexDirective } = directives;
const { regexDirectiveTypeDefs, regexDirectiveTransformer } =
  regexDirective("regex");

const transformers = [regexDirectiveTransformer];

let schema = makeExecutableSchema({
  typeDefs: [regexDirectiveTypeDefs, typeDefs],
  resolvers,
});

schema = transformers.reduce(
  (curSchema, transformer) => transformer(curSchema),
  schema,
);

const server = new ApolloServer({
  schema,
});

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`🚀 Server ready at: ${url}`);
});

Here are the possible directive functions that are exposed as part of this util package:

regexDirective | encodingDirective | cacheDirective

Local Development

Install local dependencies:

npm install

Run local environment (Apollo Studio):

npm run dev

Link to Apollo Studio can be found on http://localhost:4000 to perform mutations and queries.

Directives

@encode

encodingDirective(directiveName?: string)

You can use the @encode directive on fields defined using the String scalar type.

Following encoding methods:

ascii | utf8 | utf16le | ucs2 | base64 | base64url | latin1 | binary | hex

type User {
  firstName: String @encode(method: "hex")
  lastName: String @encode(method: "base64")
}

@regex

regexDirective(directiveName?: string)

You can use the @regex directive to validate fields using the String scalar type. It will throw an ValidationError in the event that the pattern defined has a syntax if no matches are found against the field value.

type User {
  firstName: String @regex(pattern: "(John|Micheal)")
  lastName: String @regex(pattern: "\\b[A-Z]\\w+\\b")
}

⚠️ Escaping characters

If you are defining a regex pattern using backslashes must escape them (//) and pattern invoke the function String.raw() to the schema so that the escape characters are not ignored:

const typeDefs = String.raw`
  type User {
    firstName: String @regex(pattern: "(Eddie|Sam)")
    lastName: String @regex(pattern: "\\b[A-Z]\\w+\\b")
    age: Int
  }

  type Query {
    user: User
  }
`;

@cache

cacheDirective({ directiveName, cache }?: { directiveName?: string, cache?: CachingImpl })

You can use @cache directive to take advantage of a in-memory cache for a field value

type Book {
  name: String
  price: String @cache(key: "book_price", ttl: 3000)
}

key - represents the unique key for field value you wish to cache

ttl - time-to-live argument for how long the field value should exist within the cache before expiring (in milliseconds)

Overriding in-memory cache

If you wish to take leverage something more powerful (for example Redis), you can override the in-memory solution with your own implementation.

Example:

import Redis from 'ioredis'

const redis = new Redis()
....
const cache = {
  has: (key: string) => redis.exists(key),
  get: (key: string) => redis.get(key),
  delete:(key: string) => redis.delete(key),
  set: async (key: string, value: string) => {
    await redis.set(key, value)
  },
}
...
const { cacheDirectiveTypeDefs, cacheDirectiveTransformer } = cacheDirective({ cache: callback })

You must confirm to this set of function signatures to make this work:

  • has: (key: string) => Promise<boolean> Checks if a key exists in the cache.
  • get: (key: string) => Promise<string> Retrieves the value associated with a key from the cache.
  • set: (key: string, value: string) => Promise<void> Sets a key-value pair in the cache.
  • delete: (key: string) => Promise<boolean> Deletes a key and its associated value from the cache.

@currency

currencyDirective(directiveName?: string)

You can use the @currency directive to fetch the latest exchange rate of a given amount

type Car {
  make: String
  model: String
  price: String @currency(from: GBP, to: USD)
}

The field can either be resolved with scalar types String or Float

The valid currency codes to use as part of the directive's arguments can be found here.

@log

logDirective({ directiveName, filePath }?: { directiveName?: string, filePath?: string })

Use the @log directive to log fields, queries and mutations once they are resolved.

For example, this graphql schema with the directive on the query:

  type User {
    firstName: String
    lastName: String 
    age: Int 
    amount: String
  }

  type Query {
    user(firstName: String!): User @log(level: INFO)
  }

Will log to the console in the following format:

[<TIMESTAMP>] [INFO] @log - Operation Type: query, Arguments: [{"firstName":"Eddie"}], Return Type: User

The following log levels are valid:

  • INFO
  • DEBUG
  • WARN
  • ERROR

Logging to file

In order to migrate logs to a custom log file, you can define a filepath with the appropriate file name:

  const { logDirectiveTypeDefs, logDirectiveTransformer } = logDirective({
    filePath: path.join(__dirname, 'logs', 'application.log')
  })