lightning-graphql v4.0.0
Lightning GraphQL
Lite Cache-less GraphQL client with excellent type-support for Node and Browsers. This library's default datafetcher only works with queries and mutations and not subscriptions, so if you want that still then use the fetcher option and provide your own datafetcher (providing your own datafetcher can also fix other things you may want e.g. the queries being cache-less)
Why?: I couldn't find a simple way to go from writing graphql queries on my server that wouldn't be cached between requests, and I also wanted a library that didn't compromise on type-safety and great developer experience (DX).
Table Of Contents
External Code Demos:
Stackblitz Collection of Demos
Client Examples
Given a queries.graphql like so that has been generated to a file called ./__generated__/client-types.ts
:
query Books {
books {
title
author
}
}
query Authors {
authors
}
query BookByTitle($title: String!) {
findBookByTitle(title: $title) {
title
author
}
}
mutation Noop {
noop
}
We can write our client like so:
client.ts
import { GraphQLClient } from "lightning-graphql";
// Generated from graphql-codegen with TypedDocumentNodes.
import * as source from "./__generated__/client-types";
const client = GraphQLClient({
source,
endpoint: serverURL,
});
// All these methods are camelcased named versions of queries and mutations written in the above queries file.
// The methods below also come with great type-hints and safety.
const book = await client.bookByTitle({
title: "The Great Gatsby",
}); // get Book by title
const books = await client.books({}); // get all Books
const truthy = await client.noop({}); // runs the Noop mutation
const authors = await client.authors({}); // get all Authors
If you notice above our client is generated and populated with all information from the Query file. This is possible because of GraphQL Codegen's TypedDocumentNodes.
Minimal Example with Server and Full Type Support
Server Setup
First we get our ./src/schema.graphql file:
type Book {
title: String
author: String
}
type Query {
books: [Book]
authors: [String]
findBookByTitle(title: String!): Book
}
type Mutation {
noop: Boolean!
}
Then we write a somewhat large ./src/server.js file:
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { readFileSync } from "node:fs";
import { ListenOptions } from "node:net";
import { join } from "node:path";
const books = [
{
title: "The Great Gatsby",
author: "F. Scott Fitzgerald",
},
{
title: "Where the Sidewalk Ends",
author: "Shel Silverstein",
},
];
const typeDefs = readFileSync(join(__dirname, "schema.graphql"), {
encoding: "utf-8",
});
export const resolvers = {
Query: {
books: () => books,
authors: () => books.map((x) => x.author),
findBookByTitle(_root: any, { title }: { title: string }) {
return books.find((x) => x.title === title);
},
},
Mutation: {
noop: () => true,
},
};
const server = (listen: ListenOptions) => {
const server = new ApolloServer({
typeDefs,
resolvers,
});
return {
async listen() {
return startStandaloneServer(server, {
listen,
});
},
async unlisten() {
await server.stop();
},
};
};
const port = 3322;
server({
port: 3322,
}).listen();
Client Setup
With the server finished we then write our query in ./src/queries.graphql
query Books {
books {
title
author
}
}
query Authors {
authors
}
query BookByTitle($title: String!) {
findBookByTitle(title: $title) {
title
author
}
}
mutation Noop {
noop
}
Then we generate types for the Schema and Query:
By first installing packages for codegen:
Choose an npm or yarn install:
npm
npm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
yarn:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
Then setting up the following ./codegen.yml:
# You provide a schema and documents
schema: "./src/schema.graphql"
documents: "./src/queries.graphql"
generates:
# These line can change to where you want the types to go.
./src/__generated__/client-types.ts:
# All three of these are required
plugins:
- "typescript"
- "typescript-operations"
- "typed-document-node"
The following package.json script called generate
:
{
// ...
scripts: {
// ...
generate: "graphql-codegen --config codegen.yml",
},
// ...
}
Then generate the client types with:
npm run generate
Then we can finally write our Queries in a ./src/client.ts file where we get all the type-safe goodness!
import { GraphQLClient } from "lightning-graphql";
const serverURL = "http://localhost:3322/graphql";
const client = GraphQLClient({
source: await import("./__generated__/client-types"),
endpoint: serverURL,
});
// All these methods are camelcased named versions of queries and mutations written in the above queries file.
const book = await client.bookByTitle({
title: "The Great Gatsby",
}); // get Book by title
const books = await client.books({}); // get all Books
const truthy = await client.noop({}); // runs the Noop mutation
const authors = await client.authors({}); // get all Authors
Supplying your own custom fetcher instead of using the default fetch.
import { GraphQLClient, type DefaultFetchReturnType } from "lightning-graphql";
import { execute } from "graphql";
import * as source from "./generated/client-types";
// Use graphql's execute instead
const datasource = GraphQLClient({
source,
endpoint: serverURL,
fetcher(...args) {
return async (...args2) => {
return execute({
schema,
document: args[0].query,
variableValues: args2[0] as Record<string, unknown>,
}) as DefaultFetchReturnType<typeof source>;
};
},
});
or something more like this using the defaultFetcher api:
import {
GraphQLClient,
type DefaultFetcher,
defaultFetcher,
} from "lightning-graphql";
import * as source from "./generated/client-types";
GraphQLClient({
source,
endpoint,
fetcher: (...args) => {
return async (...args2) => {
return (
defaultFetcher as DefaultFetcher<Record<string, unknown>, typeof source>
)(...args)(...args2);
};
},
options: {
fetchOptions: {
credentials: "include",
},
cookieStore,
},
});
Supplying fetchOptions.
There are two ways to supply fetchOptions:
import { GraphQLClient } from "lightning-graphql";
// First way is to send it into the GraphQLClient
const client = GraphQLClient({
source: await import("./__generated__/client-types"),
endpoint: serverURL,
options: {
// Send in a context if you have a custom fetcher.
context: {},
fetchOptions: {
// extra fetch options any options given to fetch's second parameter work here.
},
},
});
// First way is to send it into the GraphQLClient
const books = await client.books(
{},
{
// Send in a context.
context: {},
fetchOptions: {
// Extra fetch options.
},
},
);
Supplying cookieStore
There are two ways you can automate cookies in your requests in node.js or non-browser-like environments where it isn't already handled for you automatically.
First is by supplying the cookieStore argument to the GraphQLClient.
import { CookieStore, GraphQLClient } from "lightning-graphql";
const cookieStore = new CookieStore();
const client = GraphQLClient({
source: await import("./__generated__/client-types"),
endpoint: serverURL,
options: {
cookieStore,
fetchOptions: {
credentials: "include",
},
},
});
// This endpoint uses a set-cookie header. Which the cookieStore then uses to set the login cookie.
const loggedIn = await client.login({});
// This endpoint uses the cookie set in the last request through the cookieStore.
const isLoggedIn = await client.isLoggedIn({});
expect(loggedIn?.data?.login).toEqual(true);
expect(isLoggedIn?.data?.isLoggedIn).toEqual(true);
Second possibility is sending the cookieStore on a per request basis using the cookieStore in the second argument:
const datasource = GraphQLClient({
source: await import("../__generated__/client-types"),
endpoint: serverURL,
options: {
fetchOptions: {
credentials: "include",
},
},
});
const cookieStore = new CookieStore();
// This endpoint uses a set-cookie header. Which the cookieStore then uses to set the login cookie.
const loggedIn = await datasource.login(
{},
{
cookieStore,
},
);
// This endpoint uses the cookie set in the last request through the cookieStore.
const isLoggedIn = await datasource.isLoggedIn(
{},
{
cookieStore,
},
);
// This endpoint has no cookieStore and thus should not show the user as logged-in unless cookies are being handled by the environment (e.g. browser environments automatically handle cookies, so they would show the user as logged-in because of the previous set-cookie header being handled by the browser, and since `credentials: "include"` is set.).
const loggedInCheckNoCookie = await datasource.isLoggedIn({});
console.assert(loggedIn?.data?.login, "Should be able to login.");
expect(isLoggedIn?.data?.isLoggedIn).toEqual(true);
expect(loggedInCheckNoCookie?.data?.isLoggedIn).toEqual(false);```