GQLoom

Getting Started

To quickly get started with GQLoom, we will build a simple GraphQL backend application together.

We will build a cattery application and provide a GraphQL API to the outside. This application will include some simple functions:

  • Cat basic information management: Enter the basic information of cats, including name, birthday, etc., update, delete and query cats;
  • User (cat owner) registration management: Enter user information, a simple login function, and view one's own or other users' cats;

We will use the following technologies:

  • TypeScript: As our development language;
  • Node.js: As the runtime of our application;
  • graphql.js: The JavaScript implementation of GraphQL;
  • GraphQL Yoga: A comprehensive GraphQL HTTP adapter;
  • Drizzle ORM: A fast and type-safe ORM that helps us operate the database;
  • Valibot or Zod: Used to define and validate inputs;
  • GQLoom: Allows us to define GraphQL Schema comfortably and efficiently and write resolvers;

Prerequisites

We only need to install Node.js version 20 or higher to run our application.

Create the Application

Project Structure

Our application will have the following structure:

index.ts
drizzle.config.ts
package.json
tsconfig.json

Among them, the functions of each folder or file under the src directory are as follows:

  • contexts: Store contexts, such as the current user;
  • providers: Store functions that need to interact with external services, such as database connections and Redis connections;
  • resolvers: Store GraphQL resolvers;
  • schema: Store the schema, mainly the database table structure;
  • services: Store business logic, such as user login, user registration, etc.;
  • index.ts: Used to run the GraphQL application in the form of an HTTP service;

GQLoom has no requirements for the project's file structure. Here is just for reference. In practice, you can organize the files according to your needs and preferences.

Initialize the Project

First, let's create a new folder and initialize the project:

mkdir cattery
cd ./cattery
npm init -y

Then, we will install some necessary dependencies to run a TypeScript application in Node.js:

npm i -D typescript @types/node tsx
npx tsc --init

Next, we will install GQLoom and related dependencies. We can choose Valibot or Zod to define and validate inputs:

# use Valibot
npm i graphql graphql-yoga @gqloom/core valibot @gqloom/valibot
 
# use Zod
npm i graphql graphql-yoga @gqloom/core zod @gqloom/zod

Hello World

Let's write our first resolver:

src/resolvers/index.ts
import { ,  } from "@gqloom/core"
import * as  from "valibot"
 
const  = ({
  : (.())
    .({ : .(.(), "World") })
    .(({  }) => `Hello ${}!`),
})
 
export const  = []

We need to weave this resolver into a GraphQL Schema and run it as an HTTP server:

src/index.ts
import { createServer } from "node:http"
import { weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod"
import { createYoga } from "graphql-yoga"
import { resolvers } from "./resolvers"
 
const schema = weave(ZodWeaver, ...resolvers)
 
const yoga = createYoga({ schema })
createServer(yoga).listen(4000, () => {
  console.info("Server is running on http://localhost:4000/graphql")
})

Great, we have already created a simple GraphQL application. Next, let's try to run this application. Add the dev script to the package.json:

{
  "scripts": {
    "dev": "tsx watch src/index.ts"
  }
}

Now let's run it:

npm run dev

Open http://localhost:4000/graphql in the browser and you can see the GraphQL playground. Let's try to send a GraphQL query. Enter the following in the playground:

GraphQL Query
{
  hello(name: "GQLoom")
}

Click the query button, and you can see the result:

{
  "data": {
    "hello": "Hello GQLoom!"
  }
}

So far, we have created the simplest GraphQL application.

Next, we will use Drizzle ORM to interact with the database and add complete functions.

Initialize the Database and Tables

First, let's install Drizzle ORM. We will use it to operate the SQLite database.

npm i @gqloom/drizzle drizzle-orm @libsql/client dotenv
npm i -D drizzle-kit

Define Database Tables

Next, define the database tables in the src/schema/index.ts file. We will define two tables, users and cats, and establish the relationship between them:

src/schema/index.ts
import {  } from "@gqloom/drizzle"
import {  } from "drizzle-orm"
import * as  from "drizzle-orm/sqlite-core"
 
export const  = (
  .("users", {
    : .().({ : true }),
    : .().(),
    : .().().(),
  })
)
 
export const  = (, ({  }) => ({
  : (),
}))
 
export const  = (
  .("cats", {
    : .().({ : true }),
    : .().(),
    : .({ : "timestamp" }).(),
    : 
      .()
      .()
      .(() => .),
  })
)
 
export const  = (, ({  }) => ({
  : (, {
    : [.],
    : [.],
  }),
}))

Initialize the Database

We need to create a configuration file:

drizzle.config.ts
import "dotenv/config"
import { defineConfig } from "drizzle-kit"
 
export default defineConfig({
  out: "./drizzle",
  schema: "./src/schema/index.ts",
  dialect: "sqlite",
  dbCredentials: {
    url: process.env.DB_FILE_NAME ?? "file:local.db",
  },
})

Then we run the drizzle-kit push command to create the defined tables in the database:

npx drizzle-kit push

Use the Database

To use the database in the application, we need to create a database instance:

src/providers/index.ts
import { drizzle } from "drizzle-orm/libsql"
import * as schema from "../schema"
 
export const db = drizzle(process.env.DB_FILE_NAME ?? "file:local.db", {
  schema,
})

Let's first create a user service, which will contain a series of operations on the user table. We will implement the user service in the src/services/user.ts file and export the entire user.ts as userService in the src/resolvers/index.ts file:

src/services/user.ts
import { eq } from "drizzle-orm"
import { db } from "../providers"
import { users } from "../schema"
 
export async function createUser(input: typeof users.$inferInsert) {
  const [user] = await db.insert(users).values(input).returning()
  return user
}
 
export async function findUsersByName(name: string) {
  return await db.query.users.findMany({
    where: eq(users.name, name),
  })
}
 
export async function findUserByPhone(phone: string) {
  return await db.query.users.findFirst({
    where: eq(users.phone, phone),
  })
}

Resolvers

Now, we can use the user service in the resolver. We will create a user resolver and add the following operations:

  • usersByName: Find users by name
  • userByPhone: Find users by phone number
  • createUser: Create a user

After completing the user resolver, we also need to add it to the resolvers in the src/resolvers/index.ts file:

src/resolvers/user.ts
import { , ,  } from "@gqloom/core"
import * as  from "valibot"
import {  } from "../schema"
import {  } from "../services"
 
export const  = .(, {
  : (.())
    .({ : .() })
    .(({  }) => .()),
 
  : (.())
    .({ : .() })
    .(({  }) => .()),
 
  : ()
    .({
      : .({
        : .(),
        : .(),
      }),
    })
    .(async ({  }) => .()),
})

Great, now let's try it in the playground:

GraphQL Mutation
mutation {
  createUser(data: {name: "Bob", phone: "001"}) {
    id
    name
    phone
  }
}

Let's continue to try to retrieve the user we just created:

GraphQL Query
{
  usersByName(name: "Bob") {
    id
    name
    phone
  }
}

Current User Context

Next, let's try to add a simple login function and add a query operation to the user resolver:

  • mine: Return the current user information

To implement this query, we first need to have a login function. Let's write a simple one:

src/contexts/index.ts
import { ,  } from "@gqloom/core"
import {  } from "graphql"
import type { YogaInitialContext } from "graphql-yoga"
import {  } from "../services"
 
export const  = (async () => {
  const  =
    <YogaInitialContext>()...("authorization")
  if ( == null) throw new ("Unauthorized")
 
  const  = await .()
  if ( == null) throw new ("Unauthorized")
  return 
})

In the above code, we created a context function for getting the current user, which will return the information of the current user. We use createMemoization() to memoize this function, which ensures that this function is only executed once within the same request to avoid unnecessary database queries.

We used useContext() to get the context provided by Yoga, and obtained the user's phone number from the request header, and found the user according to the phone number. If the user does not exist, a GraphQLError will be thrown.

As you can see, this login function is very simple and is only used for demonstration purposes, and it does not guarantee security at all. In practice, it is usually recommended to use solutions such as session or jwt.

Now, we add the new query operation in the resolver:

src/resolvers/user.ts
import { mutation, query, resolver } from "@gqloom/core"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { users } from "../schema"
import { userService } from "../services"
 
export const userResolver = resolver({
  mine: query(users).resolve(() => useCurrentUser()), 
 
  usersByName: query(users.$list())
    .input({ name: v.string() })
    .resolve(({ name }) => userService.findUsersByName(name)),
 
  userByPhone: query(users.$nullable())
    .input({ phone: v.string() })
    .resolve(({ phone }) => userService.findUserByPhone(phone)),
 
  createUser: mutation(users)
    .input({
      data: v.object({
        name: v.string(),
        phone: v.string(),
      }),
    })
    .resolve(async ({ data }) => userService.createUser(data)),
})

If we directly call this new query in the playground, the application will give us an unauthorized error:

Graphql Query
{
  mine {
    id
    name
    phone
  }
}

Open the Headers at the bottom of the playground and add the authorization field to the request header. Here we use the phone number of Bob created in the previous step, so we are logged in as Bob:

{
  "authorization": "001"
}

Resolver Factory

Next, we will add the business logic related to cats.

We use the resolver factory to quickly create interfaces:

src/resolvers/cat.ts
// @filename: schema.ts
import {  } from "@gqloom/drizzle"
import {  } from "drizzle-orm"
import * as  from "drizzle-orm/sqlite-core"
 
export const  = (
  .("users", {
    : .().({ : true }),
    : .().(),
    : .().().(),
  })
)
 
export const  = (, ({  }) => ({
  : (),
}))
 
export const  = (
  .("cats", {
    : .().({ : true }),
    : .().(),
    : .({ : "timestamp" }).(),
    : 
      .()
      .()
      .(() => .),
  })
)
 
export const  = (, ({  }) => ({
  : (, {
    : [.],
    : [.],
  }),
}))
// @filename: providers/index.ts
import {  } from "drizzle-orm/libsql"
import * as  from "../schema"
 
export const  = (.. ?? "file:local.db", {
  ,
})
// @filename: resolvers/cat.ts
import { ,  } from "@gqloom/core"
import {  } from "@gqloom/drizzle"
import * as  from "valibot"
import {  } from "../providers"
import {  } from "../schema"
 
const  = (, "cats")
 
export const  = .(, {
  : .(),
 
  : (.(.()))
    .({
      : .(.(.(), .()), () =>
        new ().()
      ),
    })
    .((, {  }) => {
      return  - ..()
    }),
})

In the above code, we used drizzleResolverFactory() to create catResolverFactory for quickly building resolvers.

We added a query that uses catResolverFactory to select data and named it cats. This query will provide full query operations on the cats table. In addition, we also added an additional age field for cats to get the age of the cats.

Next, let's try to add a createCat mutation. We want only logged-in users to access this interface, and the created cats will belong to the current user:

src/resolvers/cat.ts
import { ,  } from "@gqloom/core"
import {  } from "@gqloom/drizzle"
import * as  from "valibot"
import {  } from "../contexts"
import {  } from "../providers"
import {  } from "../schema"
 
const  = (, "cats")
 
export const  = .(, {
  : .(),
 
  : (.(.()))
    .({
      : .(.(.(), .()), () =>
        new ().()
      ),
    })
    .((, {  }) => {
      return  - ..()
    }),
 
  : .({ 
    : .( 
      .({ 
        : .( 
          .( 
            .({ 
              : .(), 
              : .( 
                .(), 
                .(() => new ()) 
              ), 
            }), 
            .(async ({ ,  }) => ({ 
              , 
              , 
              : (await ())., 
            })) 
          ) 
        ), 
      }) 
    ), 
  }), 
})

In the above code, we used catResolverFactory to create a mutation that adds more data to the cats table, and we overwrote the input of this mutation. When validating the input, we used useCurrentUser() to get the ID of the currently logged-in user and pass it as the value of ownerId to the cats table.

Now let's try to add a few cats in the playground:

GraphQL Mutation
mutation {
  createCats(values: [
    { name: "Mittens", birthday: "2021-01-01" },
    { name: "Fluffy", birthday: "2022-02-02" },
  ]) {
    id
    name
    age
  }
}

Let's use the cats query to confirm the data in the database again:

GraphQL Query
{
  cats {
    id
    name   
    age
  }
}

Associated Objects

We want to be able to get the owner of a cat when querying the cat, and also be able to get all the cats of a user when querying the user. This is very easy to achieve in GraphQL. Let's add an additional owner field to cats and an additional cats field to users:

src/resolvers/cat.ts
import { field, resolver } from "@gqloom/core"
import { drizzleResolverFactory } from "@gqloom/drizzle"
import * as v from "valibot"
import { useCurrentUser } from "../contexts"
import { db } from "../providers"
import { cats } from "../schema"
 
const catResolverFactory = drizzleResolverFactory(db, "cats")
 
export const catResolver = resolver.of(cats, {
  cats: catResolverFactory.selectArrayQuery(),
 
  age: field(v.pipe(v.number()))
    .input({
      currentYear: v.nullish(v.pipe(v.number(), v.integer()), () =>
        new Date().getFullYear()
      ),
    })
    .resolve((cat, { currentYear }) => {
      return currentYear - cat.birthday.getFullYear()
    }),
 
  owner: catResolverFactory.relationField("owner"), 
 
  createCats: catResolverFactory.insertArrayMutation({
    input: v.pipeAsync(
      v.objectAsync({
        values: v.arrayAsync(
          v.pipeAsync(
            v.object({
              name: v.string(),
              birthday: v.pipe(
                v.string(),
                v.transform((x) => new Date(x))
              ),
            }),
            v.transformAsync(async ({ name, birthday }) => ({
              name,
              birthday,
              ownerId: (await useCurrentUser()).id,
            }))
          )
        ),
      })
    ),
  }),
})

In the above code, we used the resolver factory to create the owner field for cats; similarly, we also created the cats field for users. Behind the scenes, the relationship fields created by the resolver factory will use DataLoader to query from the database to avoid the N+1 problem.

Let's try to query the owner of a cat in the playground:

GraphQL Query
{
  cats {
    id
    name
    age
    owner {
      id
      name
      phone
    }
  }
}

Let's try to query the cats of the current user:

GraphQL Query
{
  mine {
    name
    cats {
      id
      name
      age
    }
  }
}

Conclusion

In this article, we created a simple GraphQL server-side application. We used the following tools:

  • Valibot or Zod: Used to define and validate inputs;
  • Drizzle: Used to operate the database, and directly use the Drizzle table as the GraphQL output type;
  • Context: Used to share data between different parts of the program, which is very useful for scenarios such as implementing login and tracking logs;
  • Resolver factory: Used to quickly create resolvers and operations;
  • GraphQL Yoga: Used to create a GraphQL HTTP service and provides a GraphiQL playground;

Our application has implemented the functions of adding and querying users and cats, but due to space limitations, the update and delete functions have not been implemented. They can be quickly added through the resolver factory.

Next Steps

On this page