• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

arackaf/mongo-graphql-starter: Creates a fully functioning, performant GraphQL e ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

arackaf/mongo-graphql-starter

开源软件地址(OpenSource Url):

https://github.com/arackaf/mongo-graphql-starter

开源编程语言(OpenSource Language):

JavaScript 99.9%

开源软件介绍(OpenSource Introduction):

npm version codecov code style: prettier

mongo-graphql-starter

This utility will scaffold GraphQL schema and resolvers, with queries, filters and mutations working out of the box, based on metadata you enter about your Mongo db.

The idea is to auto-generate the mundane, repetitive boilerplate needed for a graphQL endpoint, then get out of your way, leaving you to code anything else you need.

Prior art

This project is heavily inspired by Graph.Cool. It was an amazing graphQL-as-a-service that got me hooked on the idea of auto-generating a graphQL endpoint on your data store. The only thing I disliked about it was that you lost control of your data. You lacked the ability to connect directly to your database and index tune, bulk insert data, bulk update data, etc. This project aims to provide the best of both worlds: your graphQL endpoint is auto generated, but on top of the database you provide, and by extension retain control of. And integrating your own arbitrary content is directly supported.

How do you use it?

Let's work through a simple example.

NOTE: All of the code below assumes you're using John Dalton's ESM loader. Do not try to run this code with Node's native ESM.

First, create your db metadata like this. Each mongo collection you'd like added to your GraphQL endpoint needs to contain the table name, and all of the fields, keyed off of the data types provided. If you're creating a type which will only exist inside another type's Mongo fields, then you can omit the table property.

For any type which is contained in a Mongo collection—ie has a table property—if you leave off the _id field, one will be added for you, of type MongoIdType. Types with a table property will hereafter be referred to as "queryable."

projectSetupA.js

import { dataTypes } from "mongo-graphql-starter";
const {
  MongoIdType,
  MongoIdArrayType,
  StringType,
  StringArrayType,
  BoolType,
  IntType,
  IntArrayType,
  FloatType,
  FloatArrayType,
  DateType,
  arrayOf,
  objectOf,
  formattedDate,
  JSONType,
  typeLiteral
} = dataTypes;

export const Author = {
  fields: {
    name: StringType,
    birthday: DateType
  }
};

export const Book = {
  table: "books",
  fields: {
    _id: MongoIdType,
    title: StringType,
    pages: IntType,
    weight: FloatType,
    keywords: StringArrayType,
    editions: IntArrayType,
    prices: FloatArrayType,
    isRead: BoolType,
    mongoIds: MongoIdArrayType,
    authors: arrayOf(Author),
    primaryAuthor: objectOf(Author),
    strArrs: typeLiteral("[[String]]"),
    createdOn: DateType,
    createdOnYearOnly: formattedDate({ format: "%Y" }),
    jsonContent: JSONType
  }
};

export const Subject = {
  table: "subjects",
  fields: {
    _id: MongoIdType,
    name: StringType
  }
};

Now create your schema and resolvers

import { createGraphqlSchema } from "mongo-graphql-starter";
import * as projectSetup from "./projectSetupA";

import path from "path";

createGraphqlSchema(projectSetup, path.resolve("./test/testProject1"));

There should now be a graphQL folder containing schema, resolver, and type metadata files for your types, as well as a master resolver and schema file, which are aggregates over all the types.

Image of basic scaffolding

Now tell Express about it—and don't forget to add a root object with a db property that resolves to a connection to your database. If you're on Mongo 4 or higher, be sure to also add a client property that resolves to your Mongo client instance, which will be used to create sessions and transactions, for multi-document operations.

If needed, db and client can be functions which returns a promise resolving to those things.

Here's what a minimal, complete example might look like.

import { MongoClient } from "mongodb";
import { graphqlHTTP } from "express-graphql";
import resolvers from "./graphQL/resolver";
import schema from "./graphQL/schema";
import { makeExecutableSchema } from "@graphql-tools/schema";
import express from "express";

const app = express();

const mongoClientPromise = MongoClient.connect(connString, { useNewUrlParser: true });
const mongoDbPromise = mongoClientPromise.then(client => client.db(dbName));

const root = { client: mongoClientPromise, db: mongoDbPromise };
const executableSchema = makeExecutableSchema({ typeDefs: schema, resolvers });

app.use(
  "/graphql",
  graphqlHTTP({
    schema: executableSchema,
    graphiql: true,
    rootValue: root
  })
);
app.listen(3000);

Now http://localhost:3000/graphql should, assuming the database above exists, and has data, allow you to run queries.

Image of graphiQL

Valid types for your fields

Here are the valid types you can import from mongo-graphql-starter

import { dataTypes } from "mongo-graphql-starter";
const {
  MongoIdType,
  MongoIdArrayType,
  StringType,
  StringArrayType,
  BoolType,
  IntType,
  IntArrayType,
  FloatType,
  FloatArrayType,
  DateType,
  arrayOf,
  objectOf,
  formattedDate,
  JSONType,
  typeLiteral
} = dataTypes;
Type Description
MongoIdType Will create your field as a string, and return whatever Mongo uid that was created. Any filters using this id will wrap the string in Mongo's ObjectId function.
MongoIdArrayType An array of mongo ids
BoolType Self explanatory
StringType Self explanatory
StringArrayType An array of strings
IntType Self explanatory
IntArrayType An array of integers
FloatType Self explanatory
FloatArrayType An array of floating point numbers
DateType Will create your field as a string, but any filters against this field will convert the string arguments you send into a proper date object, before passing to Mongo. Querying this date will by default format it as MM/DD/YYYY. To override this, use formattedDate.
formattedDate Function: Pass it an object with a format property to create a date field with that (Mongo) format. For example, createdOnYearOnly: formattedDate({ format: "%Y" })
JSONType Store arbitrary json structures in your Mongo collections
objectOf Function: Pass it a type you've created to specify a single object of that type
arrayOf Function: Pass it a type you've created to specify an array of that type
typeLiteral Function: pass it an arbitrary string to specify a field of that GraphQL type. The field will be available in queries, but no filters will be created, though you can add your own to the generated code.

Adding property traits

If you'd like to modify the default behavior of a type's properties, you can use the fieldOf builder. Pass it a type from the prior section, and call the appropriate method to add the desired trait.

For now, the only available trait is nonQueryable, which prevents queries from being generated for a property. This will remove some bloat from your GraphQL api. These properties will still be editable and readable, but no queries will be generated for them. Properties can be made nonQueryable with the nonQueryable() method.

import { MongoIdType, StringType, fieldOf } from "../../src/dataTypes";

export const Book = {
  table: "books",
  fields: {
    _id: fieldOf(MongoIdType).nonQueryable(),
    title: fieldOf(StringType).nonQueryable(),
    isRead: BoolType
  }
};

Readonly types

Add readonly: true to any type if you want only queries, and no mutations (both discussed below) created.

Circular dependencies are fine

Feel free to have your types reference each other. Just use a getter to reference types created downstream. For example, the following will generate a valid schema.

import { dataTypes } from "mongo-graphql-starter";
const { MongoIdType, StringType, arrayOf } = dataTypes;

export const Tag = {
  table: "tags",
  fields: {
    _id: MongoIdType,
    tagName: StringType,
    get authors() {
      return arrayOf(Author);
    }
  }
};

export const Author = {
  table: "authors",
  fields: {
    name: StringType,
    tags: arrayOf(Tag)
  }
};

VS Code integration

At the root of the GraphQL folder that's created with your endpoint code, there should be an entireSchema.gql file. You can configure the VS Code GraphQL plugin to use it to validate, and provide auto-complete inside your .graphql files. Check the plugin's docs for more info

TypeScript integration

In order to generate TypeScript typings for the various types, query responses, etc. in your endpoint, just specify a typings value in the options (third argument) for createGraphqlSchema.

createGraphqlSchema(projectSetupTS, path.resolve("./my/path"), { typings: path.resolve("./path/to/graphql-types.ts") });

That'll create graphql-types.ts in the location you specify. Put it somewhere it'll be convenient to import the types from, in order to integrate with your application code.

The typings are created with GraphQL Code Generator. In addition the types in your endpoint, this library also inserts some helpers for typing queries and mutations.

The QueryOf type takes one, or two generic types, representing the queries a GraphQL query may have. The simplest use is to just pass a string to it, for the query that was run

let queryResults = useQuery<QueryOf<"allSubjects">>(packet);

That will generate a type with an allSubjects key, and a value of whatever was generated for the allSubjects query. If you have two different queries in one request, you can do

let queryResults = useQuery<QueryOf<"allSubjects" | "allBooks">>(packet);

If you'd like some autocomplete for your query names, you can use the Queries type that's generated, and do

let queryResults = useQuery<QueryOf<Queries["allSubjects"] | Queries["allBooks"]>>(packet);

The result is the same, but inside Queries[""] you should get autocomplete with all the query names.

If you have aliased queries, specify them like this

let queryResults = useQuery<QueryOf<{ bookQuery: "allBooks" }>>(packet);

or of course you can still use the Queries type for better DX.

let queryResults = useQuery<QueryOf<{ bookQuery: Queries["allBooks"] }>>(packet);

And if you have both, together, then just add a second generic type for each (order doesn't matter; either can be first).

let queryResults = useQuery<QueryOf<{ bookQuery: Queries["allBooks"] }, Queries["allSubjects"] | Queries["allBooks"]>>(packet);

For mutations, there are MutationOf and Mutations types, which work identically.

These types (and the rest from your endpoint) are generated from the typings file you specified above

import { QueryOf, Queries, MutationOf, Mutations } from "./path/to/graphql-typings";

Queries created

For each queryable type, there will be a get<Type> query which receives an _id argument, and returns the single, matching object keyed under <Type>.

For example

{getBook(_id: "59e3dbdf94dc6983d41deece"){Book{createdOn}}}

will retrieve that book, bringing back only the createdOn field.


There will also be an all<Type>s query created, which receives filters for each field, described below. This query returns an array of matching results under the <Type>s key, as well as a Meta object which has a count property, which if specified, will return the record count for the entire query, beyond just the current page.

For example

{allBooks(SORT: {title: 1}, PAGE: 1, PAGE_SIZE: 5){Books{title}, Meta{count}}}

will retrieve the first page of books' titles, as well as the count of all books matching whatever filters were specified in the query (in this case there were none).

Note, if you don't query Meta.count from the results, then the total query will not be execute. Similarly, if you don't query anything from the main result set, then that query will not execute.

The generated resolvers will analyze the AST and only query what you ask for.

Projecting results from queries

Use standard GraphQL syntax to select only the fields you want from your query. The incoming ast will be parsed, and the generated query will only pull what was requested. This applies to nested fields as well. For example, given this GraphQL setup, this unit test, and the others in the suite demonstrate the degree to which you can select nested field values.

Fragment support

As of version 0.11.1, fragments are now supported

fragment d1 on Book {
  title
}
fragment d2 on Book {
  ...d1
  pages
}
fragment d3 on Book {
  ...d2
  isRead
}
query {
  getBook(_id: "59e3dbdf94dc6983d41deece") {
    Book {
      ...d3
      weight
    }
  }
}

Custom query arguments

If you'd like to add custom arguments to these queries, you can do so like this

export const Thing = {
  table: "things",
  fields: {
    name: StringType,
    strs: StringArrayType,
    ints: IntArrayType,
    floats: FloatArrayType
  },
  manualQueryArgs: [{ name: "ManualArg", type: "String" }]
};

Now ManualArg can be sent over to the getThing and allThings queries. This can be useful if you need to do custom processing in the middleware hooks (covered later)

Filters created

All scalar fields, and scalar array fields (StringArray, IntArray, etc) will have the following filters created

Exact match

field: <value> - will match results with exactly that value.

Not equal

field_ne: <value> - will match results that do not have this value. For array types, pass in a whole array of values, and Mongo will do an element by element comparison.

in match

field_in: [<value1>, <value2>] - will match results which match any of those exact values.

not in match

field_nin: [<value1>, <value2>] - will match results which do not match any of those exact values.

For Date fields, the strings you send over will be converted to Date objects before being passed to Mongo. Similarly, for MongoIds, the Mongo ObjectId method will be applied before running the filter. For the array types, the value will be an entire array, which will be matched by Mongo item by item.

All array types, both of scalars, like StringArray, and of arrays of user-defined types, will support the following queries:

Count

field_count: <value> - will match results with that number of entries in the array

null values

If you pass null for an exact match query, matches will be returned if that value is literally null, or doesn't exist at all (per Mongo's behavior). If you pass null for a not equal (ne) query, matches will come back if any value exists.

If you pass null for any other filter, it will be ignored.

String filters

If your field is named title then the following filters will be available

Filter Description
String contains title_contains: "My" - will match results with the string My anywhere inside, case insensitively.
String starts with title_startsWith: "My" - will match results that start with the string My, case insensitively.
String ends with title_endsWith: "title" - will match results that end with the string title, case insensitively.
String matches regex title_regex: "^Foo" - will match results that match that regex, case insensitively.

String array filters

If your field is named keywords then the following filters will be available

Filter Description
String array contains keywords_contains: "JavaScript" - will match results with an array containing the string JavaScript.
String array contains any keywords_containsAny: ["c#", "JavaScript"] - will match results with an array containing any of those strings.
String array contains all keywords_containsAll: ["c#", "JavaScript"] - will match results with an array containing all of those strings.
String array element contains keywords_textContains: "scri" - will match results with an array that has an entry containing the string scri case insensitively.
String array element starts with keywords_startsWith: "Ja" - will match results with an array that has an entry starting with the string Ja case insensitively.
String array element ends with keywords_endsWith: "ipt" - will match results with an array that has an entry ending with the string ipt case insensitively.
String array element regex keywords_regex: "^Foo" - will match results with an array that has an entry matching that regex, case insensitively.

Int filters

If your field is named pages then the following filters will be available

Filter Description
Less than pages_lt: 200 - will match results where pages is less than 200
Less than or equal pages_lte: 200 - will match results where pages is less than or equal to 200
Greater than pages_gt: 200 - will match results where pages is greater than 200
Greater than or equal pages_gte: 200 - will match results where pages is greater than or equal to 200

Int array filters

If your field is named editions then the following filters will be available

Filter Description
Int array contains editions_contains: 2 - will match results with an array containing the value 2
Int array contains any editio

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap