npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@scinorandex/yoko

v0.0.2

Published

GraphQL schema and resolver mapping using Zod

Downloads

3

Readme

Yoko - a declarative approach to GraphQL

Yoko allows developers to create GraphQL type definitions and resolvers without the need for classes, aiming to be the complete opposite of TypeGraphQL.

Getting started

To get started, create a new TypeScript project and install @scinorandex/yoko

In this example, we're going to create a graphql schema that looks like this:

type User {
  id: String!
  name: String!
  hobbies: [Hobby!]!
  capitalized(suffix: String): String!
}

type Hobby {
  id: String!
  name: String!
  description: String!
  users: [User!]!
}

type Query {
  getUser(id: String!): User!
  getUsers(id: String!): [User!]!
}

type Mutation {
  createUser(name: String!): User!
}

Defining base fields

Zod is used to define the base fields of a model. Currently, only boolean, strings, numbers, and their optional versions are supported.

import { z } from "zod";

export const HobbyModel = z.object({
  id: z.string(),
  name: z.string(),
  description: z.string(),
});

export const UserModel = z.object({
  id: z.string(),
  name: z.string(),
});

Defining field resolvers

Once our models have been defined, we can define their associated resolvers using the defineType() method. This method accepts the base model as the 1st argument, and a function that returns an object containing resolvers as the 2nd argument.

The second argument is given a function as a parameter, named defineField in this case, that allows us to have typesafe field resolvers.

import { defineResolver, defineType, yoko } from "@scinorandex/yoko"

const UserType = defineType(UserModel, (defineField) => ({
  // Defines a field resolver called "hobbies" in the UserType
  hobbies: defineField({
    // This field resolver accepts no arguments
    args: undefined,
    
    // Define what this field resolver returns, in this case an array of `Hobby`s. 
    // Currently it only supports literal type, models, and their optional versions.
    returns: z.array(HobbyValidator),

    // Define the function that computes the field from its parent
    // parent is completely typesafe. In this case, it's type is { id: string, name: string }
    resolver: (parent) => {
      // find and return all hobbies that the user has
      return relation.filter((r) => r.userId === parent.id).map((r) => hobbies.find((h) => h.id === r.hobbyId)!);
    },
  }),

  // This field resolver accepts an optional suffix parameter and returns a string
  // It computes the field from the parent's name property and the suffix parameter
  capitalized: defineField({
    args: z.object({ suffix: z.string().optional() }),
    returns: z.string(),
    resolver: (parent, args) => parent.name.toUpperCase() + (args.suffix ?? ""),
  }),
}));

We can then define the Hobby model's resovlers the exact same way

const HobbyType = defineType(HobbyModel, (defineField) => ({
  users: defineField({
    returns: z.array(UserModel),
    args: undefined,
    resolver: (parent) => {
      const filtered = relation.filter((r) => r.hobbyId === parent.id);
      return filtered.map((r) => users.find((user) => user.id === r.userId)!);
    },
  }),
}));

Defining queries and mutations

Queries and mutations are written as resolver maps. These are objects whose key is the name of the query / mutation and the value is a resovler endpoint.

A resolver endpoint is defined using the defineResolver() method, that works very similar to the defineType() method. It accepts an object that defines the parameters, return type, and function for that resolver.

const queries = {
  // The getUser query returns a user model and requires an argument named id.
  // It throws an error if a user was not found with that ID.
  getUser: defineResolver({
    returns: UserModel,
    args: z.object({ id: z.string() }),
    resolver: async ({ id }) => {
      const user = users.find((user) => user.id === id);
      if (user) return user;
      else throw new Error("User not found");
    },
  }),

  // The getUsers query returns an array of users, requiring no arguments
  getUsers: defineResolver({
    returns: z.array(UserModel),
    args: undefined,
    resolver: () => users,
  }),
};

let maxId = 4;
const mutations = {
  // The createUser mutation requires a name string parameter,
  // and returns the newly created user object.
  createUser: defineResolver({
    returns: UserModel,
    args: z.object({ name: z.string() }),
    async resolver({ name }) {
      const user = { id: (++maxId).toString(), name };
      users.push(user);
      return user;
    },
  }),
};

Building it all together

Once our types, queries, and mutations are complete, we use the yoko() function to build the schema and rootValue that we run queries against.

// schema is a GraphQLSchema object that can be passed into the `graphql()` function or be used by a library.
// rootValue is the object that merges the query and mutation resolver maps.
// schemaString is a stringified version of the GraphQL schema, that can be saved to a `schema.graphql` file.
export const { schema, schemaString, rootValue } = yoko({
  // The keys on the types object matter, these keys are reflected on the type in the schema
  types: { User: UserType, Hobby: HobbyType },
  queries,
  mutations,
});

Example usage (no api)

We can query against the schema and resolvers using the graphql() function from the graphql package.

import { graphql } from "graphql";
import { schema, rootValue } from "./schema";

const query = `
{
  getUser(id: "1") {
    id, name, capitalized(suffix: " - is the soldier")
    
    hobbies {
      id, name
    }
  }
}
`;

graphql({ schema, rootValue, source: query }).then((testing) => {
  if (testing.data) return console.log(JSON.stringify(testing.data));
  else console.log(testing);
});

Which prints:

{
  "getUser": {
    "id": "1",
    "name": "John Doe",
    "capitalized": "JOHN DOE - is the soldier",
    "hobbies": [
      {
        "id": "1",
        "name": "Reading"
      },
      {
        "id": "2",
        "name": "Running"
      }
    ]
  }
}

Example usage (with API)

We can serve a GraphQL API very easily using the graphql-http package and ruru for graphiql

import { createHandler } from "graphql-http/lib/use/http";
import { rootValue, schema } from "./schema";
import express from "express";
import { ruruHTML } from "ruru/server";

async function main() {
  const app = express();

  // Serve ruru on /graphql as a playground
  app.get("/graphql", (req, res, next) => {
    res.writeHead(200, { "Content-Type": "text/html" });
    return res.end(ruruHTML({ endpoint: "/graphql" }));
  });
  
  // Create the handler using the schema we built from Yoko
  app.post("/graphql", createHandler({ schema, rootValue }));

  // Start the server on port 7000
  app.listen(7000, () => console.log("Server is running on port 7000"));
}

main().catch(console.error);