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

graphstrap

v0.2.0

Published

graphstrap allows you to bootstrap a GraphQL server by converting TypeScript types to GraphQL schema and their TypeScript resolvers

Downloads

20

Readme

graphstrap

graphstrap allows you to bootstrap a GraphQL server by converting TypeScript types to GraphQL schema and their TypeScript resolvers.

You define your business models as TypeScript interfaces, and graphstrap will generate the appropriate graphql schema along with their resolvers.

You can install graphstrap globally:

npm i -g graphstrap

Or per project

npm i -D graphstrap

Configuring graphstrap

graphstrap works by taking in models (basically TypeScript types) and outputing schema and resolvers for a functioning GraphQL service. To specify where the models can be found, as well as where the output files should be emitted to, create a graphstrap.json file in the project's root directory.

An example is shown below:

{
  "config": {
    "in": "./models/**/*.ts",
    "schema": {
      "outDir": "./generated/schema"
    },
    "resolvers": {
      "rootDir": "./generated/resolvers",
      "context": {
        "import": "Context",
        "from": "../../src/context"
      },
      "queries": {
        "templates": {
          "model": "./templates/model.template",
          "models": "./templates/models.template"
        }
      },
      "mutations": {
        "templates": {
          "createModel": "./templates/create-model.template",
          "deleteModel": "./templates/delete-model.template",
          "deleteManyModels": "./templates/delete-many-models.template",
          "updateModel": "./templates/update-model.template",
          "updateManyModels": "./templates/update-many-models.template",
          "upsertModel": "./templates/upsert-model.template"
        }
      }
    }
  }
}
  • config.in: a glob pattern for the input model files
  • config.schema.outDir: the directory into which the auto-generated schema will be emitted to. The name of the schema file will be schema.graphql.
  • config.resolvers.rootDir: the root directory for all the auto-generated resolver files.
  • config.resolvers.context: this tells graphstrap how to located the GraphQL context every resolver will have access to.
  • config.resolvers.queries.templates: this is where template code for auto-generated query resolvers are found.
  • config.resolvers.mutations.templates: this is where template code for auto-generated mutation resolvers are found.

Note that the file path specified in config.resolvers.context.from is relative to the root directory for resolvers (config.resolvers.rootDir).

Creating Models

Let's create some models losely based on the popular Microsoft Northwind database.

We will have the following models:

  • Customer
  • Product
  • Order
  • Supplier

Each model will have an identifier called id. To avoid repeating this in every model, we will create this in a base interface called Identifiable.

// file: id.ts

/** @graphql ID */
export type Id = string;

export interface Identifiable {
  id: Id;
}

Each model may also have time stamps createdAt and updatedAt. To avoid repeating these in every model, we will create them in a base interface called TimeStamped.

// file: timestamp.ts

export interface TimeStamped {
  createdAt?: Date;
  updatedAt?: Date;
}

Now, we create our models.

// file: models.ts

import { Identifiable } from "./id";
import { TimeStamped } from "./timestamp";

// Type aliases become 'GraphQL' scalars.
export type Url = string;

export enum Region {
  East,
  North,
  South,
  West
}

export interface Customer extends Identifiable, TimeStamped {
  companyName: string;
  contactName: string;
  contactTitle?: string;
  address?: string;
  phone?: string;
  region?: Region;
}

export interface Product extends Identifiable, TimeStamped {
  name: string;
  quantityPerUnit: number;
  unitPrice: number;
  unitsInStock: number;
}

export interface Order extends Identifiable, TimeStamped {
  customer: Customer;
  product: Product;
  orderDate: Date;
  requiredDate?: Date;
  shippedDate?: Date;
  shipRegion?: Region;
}

export interface Supplier extends Identifiable, TimeStamped {
  companyName: string;
  contactName?: string;
  address: string;
  region?: Region;
  phone?: string;
  homePage?: Url;
}

Declaring Your Models

To determine what TypeScript interfaces should be converted to GraphQL types, graphstrap looks for a manifest.

A manifest is a TypeScript interface that is decorated with the comment /** @graphql manifest */. This interface is expected to contain fields, where each field references an interface that should be converted to a GraphQL type. Note that it is enough to reference the major types; you do not need to reference input types, for instance.

// file: root.ts

import { Employee, Customer } from './models';

/** @graphql manifest */
export interface Manifest {
  customer: Customer;
  product: Product;
  order: Order;
  supplier: Supplier;
}

Emitting Schema and Resolvers

With the graphstrap.json config file defined in the root directory of your project, and the input TypeScript models defined, you can now run graphstrap to bootstrap your GraphQL server.

To run graphstrap globally, in your project's root directory run:

graphstrap

To run the graphstrap instance installed for a project, in the project's root directory run:

./node_modules/graphstrap/bin/graphstrap

The processed and output files should be printed on the terminal.

code4

Generated Schema

graphstrap will generate a schema.graphql file in the schema output directory specified in the graphstrap.config file. Below are the generated queries and mutations from the models specified above.

type Query {
  customer(id: ID!): Customer!
  customers(where: CustomerWhereInput, orderBy: CustomerOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Customer!]!
  product(id: ID!): Product!
  products(where: ProductWhereInput, orderBy: ProductOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Product!]!
  order(id: ID!): Order!
  orders(where: OrderWhereInput, orderBy: OrderOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Order!]!
  supplier(id: ID!): Supplier!
  suppliers(where: SupplierWhereInput, orderBy: SupplierOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Supplier!]!
}
type Mutation {
  createCustomer(data: CustomerCreateInput!): Customer!
  deleteCustomer(id: ID!): Customer!
  deleteManyCustomers(where: CustomerWhereInput): [Customer!]!
  updateCustomer(id: ID!, data: CustomerUpdateInput!): Customer
  updateManyCustomers(data: CustomerUpdateManyMutationInput!, where: CustomerWhereInput): [Customer!]!
  upsertCustomer(id: ID!, create: CustomerCreateInput!, update: CustomerUpdateInput!): Customer!
  createProduct(data: ProductCreateInput!): Product!
  deleteProduct(id: ID!): Product!
  deleteManyProducts(where: ProductWhereInput): [Product!]!
  updateProduct(id: ID!, data: ProductUpdateInput!): Product
  updateManyProducts(data: ProductUpdateManyMutationInput!, where: ProductWhereInput): [Product!]!
  upsertProduct(id: ID!, create: ProductCreateInput!, update: ProductUpdateInput!): Product!
  createOrder(data: OrderCreateInput!): Order!
  deleteOrder(id: ID!): Order!
  deleteManyOrders(where: OrderWhereInput): [Order!]!
  updateOrder(id: ID!, data: OrderUpdateInput!): Order
  updateManyOrders(data: OrderUpdateManyMutationInput!, where: OrderWhereInput): [Order!]!
  upsertOrder(id: ID!, create: OrderCreateInput!, update: OrderUpdateInput!): Order!
  createSupplier(data: SupplierCreateInput!): Supplier!
  deleteSupplier(id: ID!): Supplier!
  deleteManySuppliers(where: SupplierWhereInput): [Supplier!]!
  updateSupplier(id: ID!, data: SupplierUpdateInput!): Supplier
  updateManySuppliers(data: SupplierUpdateManyMutationInput!, where: SupplierWhereInput): [Supplier!]!
  upsertSupplier(id: ID!, create: SupplierCreateInput!, update: SupplierUpdateInput!): Supplier!
}

Not only does graphstrap generate the root queries and mutations, it also generates the appropriate input types, including input types for filtering and ordering. Below is the ordering input (CustomerOrderByInput) for the Customer model.

enum CustomerOrderByInput {
  address_ASC
  address_DESC
  companyName_ASC
  companyName_DESC
  contactName_ASC
  contactName_DESC
  contactTitle_ASC
  contactTitle_DESC
  createdAt_ASC
  createdAt_DESC
  id_ASC
  id_DESC
  phone_ASC
  phone_DESC
  region_ASC
  region_DESC
  updatedAt_ASC
  updatedAt_DESC
}
Generated Resolvers

graphstrap will generate a resolvers.ts file in the resolvers root directory specified in the graphstrap.config file. This file contains the interfaces your query and mutation resolvers need to implement.

export interface QueryResolvers {
  customer: __Resolver<{}, CustomerFindOneQueryArgs, Customer>;
  customers: __Resolver<{}, CustomerFindManyQueryArgs, Customer[]>;
  product: __Resolver<{}, ProductFindOneQueryArgs, Product>;
  products: __Resolver<{}, ProductFindManyQueryArgs, Product[]>;
  order: __Resolver<{}, OrderFindOneQueryArgs, Order>;
  orders: __Resolver<{}, OrderFindManyQueryArgs, Order[]>;
  supplier: __Resolver<{}, SupplierFindOneQueryArgs, Supplier>;
  suppliers: __Resolver<{}, SupplierFindManyQueryArgs, Supplier[]>;
}

export interface MutationResolvers {
  createCustomer: __Resolver<{}, CustomerCreateOneMutationArgs, Customer>;
  deleteCustomer: __Resolver<{}, CustomerFindOneQueryArgs, Customer>;
  deleteManyCustomers: __Resolver<{}, CustomerDeleteManyMutationArgs, Customer[]>;
  updateCustomer: __Resolver<{}, CustomerUpdateOneMutationArgs, Customer>;
  updateManyCustomers: __Resolver<{}, CustomerUpdateManyMutationArgs, Customer[]>;
  upsertCustomer: __Resolver<{}, CustomerUpsertOneMutationArgs, Customer>;
  createProduct: __Resolver<{}, ProductCreateOneMutationArgs, Product>;
  deleteProduct: __Resolver<{}, ProductFindOneQueryArgs, Product>;
  deleteManyProducts: __Resolver<{}, ProductDeleteManyMutationArgs, Product[]>;
  updateProduct: __Resolver<{}, ProductUpdateOneMutationArgs, Product>;
  updateManyProducts: __Resolver<{}, ProductUpdateManyMutationArgs, Product[]>;
  upsertProduct: __Resolver<{}, ProductUpsertOneMutationArgs, Product>;
  createOrder: __Resolver<{}, OrderCreateOneMutationArgs, Order>;
  deleteOrder: __Resolver<{}, OrderFindOneQueryArgs, Order>;
  deleteManyOrders: __Resolver<{}, OrderDeleteManyMutationArgs, Order[]>;
  updateOrder: __Resolver<{}, OrderUpdateOneMutationArgs, Order>;
  updateManyOrders: __Resolver<{}, OrderUpdateManyMutationArgs, Order[]>;
  upsertOrder: __Resolver<{}, OrderUpsertOneMutationArgs, Order>;
  createSupplier: __Resolver<{}, SupplierCreateOneMutationArgs, Supplier>;
  deleteSupplier: __Resolver<{}, SupplierFindOneQueryArgs, Supplier>;
  deleteManySuppliers: __Resolver<{}, SupplierDeleteManyMutationArgs, Supplier[]>;
  updateSupplier: __Resolver<{}, SupplierUpdateOneMutationArgs, Supplier>;
  updateManySuppliers: __Resolver<{}, SupplierUpdateManyMutationArgs, Supplier[]>;
  upsertSupplier: __Resolver<{}, SupplierUpsertOneMutationArgs, Supplier>;
}

export interface Resolvers {
  Query: QueryResolvers;
  Mutation: MutationResolvers;
}

All necessary input types are also generated. For instance, below are the definitions for CustomerFindManyQueryArgs and CustomerOrderByInput.

export interface CustomerFindManyQueryArgs {
  where?: CustomerWhereInput | null | undefined;
  orderBy?: CustomerOrderByInput | null | undefined;
  skip?: number;
  after?: string;
  before?: string;
  first?: number;
  last?: number;
}

export enum CustomerOrderByInput {
  address_ASC,
  address_DESC,
  companyName_ASC,
  companyName_DESC,
  contactName_ASC,
  contactName_DESC,
  contactTitle_ASC,
  contactTitle_DESC,
  createdAt_ASC,
  createdAt_DESC,
  id_ASC,
  id_DESC,
  phone_ASC,
  phone_DESC,
  region_ASC,
  region_DESC,
  updatedAt_ASC,
  updatedAt_DESC
}

In addition to these interfaces, actual query and mutation resolvers implementing these interfaces are generated in the queries and mutations subdirectories of the resolvers directory. As discussed in the section below, these files are typically generated in pairs: one which cannot be manually edited (and is regenerated every time you run graphstrap) and one which can be edited (and is generated ONLY if the file is missing).

Making Changes

Beyond generating the interfaces your resolvers need to implement, graphstrap also generates the boiler plate code for these resolvers. These resolvers are typically split between 2 files; one which should not be edited and one which can be edited to modify the default behaviours of the resolvers.

Files that should not be edited

Files which should not be edited contain default implementation/behaviour as generated by graphstrap. These files are overwritten every time you run graphstrap. You can easily identify these files as they typically contain CondeGen in their names, and have comments at the top saying This file was generated using graphstrap Do not edit this file manually.

An example is shown below.

/resolvers/queries/customer/CustomerQueryResolversCodeGen.ts

/*
This file was generated using graphstrap
Do not edit this file manually
*/

import { QueryResolvers, Customer } from '../../resolvers';

export const CustomerQueryResolversCodeGen: Pick<QueryResolvers, "customer" | "customers"> = {
  async customer(root, args, ctx, info) {
    
    const { dataSources: { db } } = ctx;
    const { id } = args;
    
    const customer = await db.findOne<Customer>({ id, table: "customers" });
    if (!customer) {
      throw new Error(`Could not find the Customer with ID ${id}`);
    }
    
    return customer;
  },
  async customers(root, args, ctx, info) {
    const { dataSources: { db } } = ctx;
    const { where, orderBy } = args;
    
    const customers = await db.findMany<Customer>({ table: "customers" })
    
    return customers;
  }
}
Files that can be edited

Files which can be edited allow you to modify the default implementation/behaviour generated by graphstrap. These files are generated only if they do not exist when run graphstrap. You can easily identify these files as they typically do not contain CondeGen in their names, and have comments at the top saying This file will be generated by graphstrap only if it does NOT exist.

An example is shown below.

/resolvers/queries/customer/CustomerQueryResolvers.ts

/*
This file will be generated by graphstrap only if it does NOT exist
*/

import { QueryResolvers } from '../../resolvers';
import { CustomerQueryResolversCodeGen } from './CustomerQueryResolversCodeGen';

export const CustomerQueryResolvers: Pick<QueryResolvers, "customer" | "customers"> = {
  ...CustomerQueryResolversCodeGen,
  // You can add more functions and even override those in CustomerQueryResolversCodeGen
}

Decorations

/** @graphql manifest */
/** @graphql input */
/** @graphql directive */

Templates

Custom Schema

Object Resolvers