diff --git a/README.md b/README.md new file mode 100644 index 00000000..2ecf1673 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# graphql-node-resource + +This library implements the [Node GraphQL type defined by Relay](https://relay.dev/docs/guides/graphql-server-specification/#object-identification). Relay requires that a GraphQL server +has a way to identify and refetch GraphQL resources. Its meant to be used within a GraphQL API that fetches its types from a REST API. + + +```mermaid +graph LR; + frontend(frontend using relay)-->graphql-api(GraphQL API); + graphql-api-->rest-api(REST API); + rest-api-->graphql-api; + graphql-api-->frontend; +``` +_example architecture_ + +## Example Application + +For a full end-to-end example of this library, see [example/README.md](example/README.md). This demonstrates how to connect this library to a real backend REST API. + + +## What is a Node? + +A `Node` is a refetchable GraphQL resource. All `Node`s must have a _globally unique ID_ and be fetchable via this ID. Relay will refetch these resources in the following way: + +```graphql +node(id: $resource_id) { + ... on Foo { + bar + baz + } +} +``` + +The spec defines the Node interface in this way: +```graphql +# An object with a Globally Unique ID +interface Node { + # The ID of the object. + id: ID! +} +``` + +It can be used in a GQL schema as such: +```graphql +type User implements Node { + id: ID! + # Full name + name: String! +} +``` + +See the [Global Identification Page](https://graphql.org/learn/global-object-identification/) for more information. + +## The Node Class + +The [NodeType](./src/types/NodeType.ts#) class does two things: +1. it defines the `Node` type required by Relay +1. it implements a solution to asynchronously resolve the Node by fetching it from a backing REST API + +It is an extension of the [GraphQLObjectType](https://graphql.org/graphql-js/type/#graphqlobjecttype) provided by `graphql-js`. + +Every node consists of a backing HTTP REST resource that will be used to fetch the node. This library exposes an [HttpResource class](./src/resources/HttpResource.ts) that implements basic CRUD API operations. + +## Usage + +### Creating a new Node type + +If your API is a standard CRUD API, then all you need to do is create a new instance of a NodeType: + +``` +import {GraphQLInt} from 'graphql'; + +// Foo.ts +export default new NodeType({ + // this determines the GraphQL type name + name: 'Foo', + + // this provides graphql documentation on this type + description: 'Foo is the catch all example for all things programming', + + // returns an object containing all of the fields for this graphql type + fields() { + return { + bar: { + type: GraphQLInt, + description: 'A unit of Foo' + } + } + }, + + createResource(ctx) { + // as long as the `/foo` endpoint follows standard CRUD, then HttpResource should be able + // to handle fetching it correctly + return new HttpResource({ + endpoint: "/foo" + }) + } +}); + + +``` + + + +### Adding the Node Field to Your Query Object + +In order to fulfill the contract required by Relay, you need to expose a field `node` on your `Query` type. + +``` +// query.ts +import { getConfig } from '@4c/graphql-node-resource/config'; + +const config = getConfig(); + +export default new GraphQLObjectType({ + name: 'Query', + + fields: { + node: config.nodeField, + nodes: config.nodesField, + }, +}) +``` + +This will expose `node` and `nodes` on your Query graphql type. + +### Resolving a Node Type + +There are two ways to resolve a Node type from a parent type: +1. use the `NodeType.getResource(context)` function to get the HTTPResource object and use that to get the node +2. resolve the field to an object containing an id such as `{id: 'foo123'} + +### Using Get Resource + +You can resolve the resource directly by getting the HTTPResource object from the NodeType instance: +``` +// Baz.ts +export default new GraphQLObjectType({ + name: 'Baz', + fields: { + foo: { + type: new GraphQLList(Foo), + resolve(context) { + const resource = Foo.getResource(context) as HTTPResource; + return resource.get(); // get a list of Foos + } + } + } +}) +``` + +This is useful when you don't have an ID and need to resolve a list of resources or +fetch a list of resources and then find the object you are looking for. + +### Passing an object with an id + +The `NodeType` class provides default resolvers for all defined fields. These resolvers will try to fetch the Node from the provided resource and then resolve to the matching field in the response. In order to do this, the `id` field must be present in the object that is being resolved. + +In the example below, the resolver for the `foo` field returns an object with an `id` property. The default resolvers provided by the NodeType will use the `id` property to try and fetch the `Foo` resource. + +``` +// Baz.ts +export default new GraphQLObjectType({ + name: 'Baz', + fields: { + foo: { + type: Foo, + resolve: (parent, args, context, info) => { + return { + id: 'foo:123' + } + } + } + } +}) +``` + +This is useful when you have a list of IDs in the parent type. For instance, if `Baz` has a list of Foo `id`s, then you could pass the list of IDs to the Foo type. See [this example](example/types/Author.ts#L30) for more information. + +### Connections + +TODO + +### Fetching Nodes from a list endpoint + +TODO + +### Fetching non node types + +TODO + +### Performance + +TODO + +## Best Practices + +The following is a list of suggested best practices: +1. One HttpResource for one GraphQL type: an HttpResource should be responsible for fetching a single Resource. diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..ecc9be03 --- /dev/null +++ b/example/README.md @@ -0,0 +1,58 @@ +# Example Application + +This example application shows how to create a GraphQL API over a REST API utilizing the `graphql-node-resource` library. There +are two parts to this example application: +1. a REST API implemented via NodeJS with Express. The API exposes a simple blog resource and supports pagination. +2. a GraphQL API that exposes the REST API. This shows how to utilize the `graphql-node-resource` library properly. + +## Running the App + +```sh +# go to root of the example app +cd example; + +# install the deps +yarn # or npm install + +# start the app +yarn start # or npm run start +``` + +Navigate to the URL thats output on the terminal. You should be able to run this query: +```graphql +{ + authors { + name + id + blogPosts { + title + } + blogPostsConnection { + edges { + node { + title + } + } + } + } +} +``` + +## api/api.ts + +This is where the REST API lives. It may be useful to look at the entry and exit points of the API to see what the GraphQL server passes +as input and expects as output. This is especially useful for looking at the paginated connection endpoints. Connections need to be supported +via the underlying REST API. + +## GraphQL API + +The GraphQL API is composed of 3 main parts: +1. `setup.ts` - this is required to tell the library how to fetch resources from the REST API as well as some basic configuration +2. `types/*.ts` - these are the GraphQL types that make up the GraphQL API +3. `index.ts` - this is the entry point for the GraphQL API. It calls the setup.ts module and combines all of the types + +## Starting Point + +A good starting point would be the `types/Query.ts` file. This is the entrypoint into any of the API endpoints. + +The app will run the GraphIQL tool. Navigate to the [outputted server url](http://localhost:8081/graphql) to interact with the API. \ No newline at end of file diff --git a/example/api/api.ts b/example/api/api.ts new file mode 100644 index 00000000..e1cfef47 --- /dev/null +++ b/example/api/api.ts @@ -0,0 +1,359 @@ +/** + * This file contains a simple API to demonstrate the usage of this library. This should give a view + * as to what will come from the GQL server. + */ +import express, { NextFunction, Request, Response } from 'express'; +import morgan from 'morgan'; + +/** + * Generate a sequence of IDs. + */ +class IdGenerator { + static id = 0; + static getId() { + IdGenerator.id = IdGenerator.id + 1; + return `${IdGenerator.id}`; + } +} + +// Data Models +class BlogPost { + id: string; + + // settable properties + authorId: string = ''; + title: string = ''; + content: string = ''; + + constructor(args: Pick) { + // simple validation + const requiredKeys = ['authorId', 'title', 'content']; + for (const k of requiredKeys) { + if (!(k in args)) { + throw new Error(`${k} is required to create a new blog post`); + } + } + + // set instance properties + Object.assign(this, args); + + // generate a unique id + this.id = IdGenerator.getId(); + } +} + +class Author { + id: string; + + // settable properties + name: string = ''; + + constructor(args: Pick) { + // simple validation + const requiredKeys = ['name']; + for (const k of requiredKeys) { + if (!(k in args)) { + throw new Error(`${k} is required to create a new blog post`); + } + } + + // set instance properties + Object.assign(this, args); + + // generate a unique id + this.id = IdGenerator.getId(); + } +} + +class Blog { + posts: BlogPost[]; + authors: Author[]; + + constructor() { + this.posts = []; + this.authors = []; + } + + // BlogPost operations + makeBlogPost(blogPostData: any) { + const blogPost = new BlogPost(blogPostData); + this.posts.push(blogPost); + this.posts.sort((a, b) => parseInt(a.id) - parseInt(b.id)); + return blogPost; + } + + listBlogPosts(): readonly BlogPost[] { + return this.posts; + } + + getBlogPost(id: string) { + return this.posts.find((p) => p.id === id); + } + + // Authors operations + getAuthors(): readonly Author[] { + return this.authors; + } + + getAuthor(authorId: string) { + return this.authors.find((author) => author.id === authorId); + } + + getBlogPostIdsForAuthor(authorId: string): string[] { + return this.posts.filter((p) => p.authorId === authorId).map(p => p.id); + } +} + +const blog = new Blog(); +blog.authors.push(new Author({ name: 'Gino' })); +blog.authors.push(new Author({ name: 'Sam' })); + +for (let i = 0; i < 10; i++) { + blog.makeBlogPost( + new BlogPost({ + authorId: blog.authors[0].id, + content: 'foo content', + title: `Foo ${i}`, + }), + ); +} + +for (let i = 0; i < 10; i++) { + blog.makeBlogPost( + new BlogPost({ + authorId: blog.authors[1].id, + content: 'bar content', + title: `Bar ${i}`, + }), + ); +} + +// App setup +const app = express(); + +// Setup middlewares +app.use(express.json()); +app.use(express.raw()); +app.use(express.urlencoded()); +app.use(morgan('short')); + +// Setup routes + +// BlogPost routes +app.get('/api/blog/posts/', (req, res) => { + const { cursor, before, last, limit } = getConnectionArgsFromQuery( + req.query, + ); + + const authorIdFilter = req.query.authorId ?? null + + let posts = blog.listBlogPosts(); + + if (authorIdFilter) { + posts = posts.filter(p => p.authorId === authorIdFilter) + } + + res.json(paginateConnection(posts, 'BlogPost', cursor, limit, before, last)); +}); + +app.post('/api/blog/posts/', (req, res) => { + const bp = blog.makeBlogPost(req.body); + res.json(bp); +}); + +app.get('/api/blog/posts/:id/', (req, res) => { + res.json(blog.getBlogPost(req.params.id)); +}); + +// Author Routes +app.get('/api/blog/authors/', (req, res) => { + const { cursor, before, last, limit } = getConnectionArgsFromQuery( + req.query, + ); + + let authors = blog + .getAuthors() + // enrich authors will their blog posts ids + .map((author) => { + Object.assign(author, { + blogPostIds: blog.getBlogPostIdsForAuthor(author.id), + }); + return author; + }); + + return res.json( + paginateConnection(authors, 'Author', cursor, limit, before, last), + ); +}); + +app.get('/api/blog/authors/:id/', (req, res) => { + res.json(blog.getAuthor(req.params.id)); +}); + +// setup error handler +app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + return res.status(500).json({ + error: err.name, + description: err.message, + stack: err.stack, + }); +}); + +export default function start(port: number) { + app.listen(port, () => { + console.log('REST API listening on port ' + port); + }); + + return app; +} + +// Utils + +/** + * A global ID is in the form of : that is base64 url encoded. + * + * @param id the id to parse + */ +function getIdFromGlobalId(globalId: string) { + return Buffer.from(globalId, 'base64url').toString('utf-8').split(':')[1]; +} + +/** + * A global ID is in the form of : that is base64 url encoded. + * + * @param id the id to serialize + * @param graphqlType the name of the gql type + */ +function makeGlobalId(id: string, graphqlType: string) { + return Buffer.from(`${graphqlType}:${id}`, 'utf-8').toString('base64url'); +} + +/** + * This impliments the pagination algorithm found here: https://relay.dev/graphql/connections.htm#sec-Pagination-algorithm. + * + * Note: this doesn't use relay's connection util because that assumes indexes for cursors which only work for static arrays. + * + * @param arr The array to slice into a page + * @param after the cursor for which to return items that come after + * @param first the number of items to return after the 'after' cursor + * @param before the cursor for which to return items that come before + * @param last the number of items to return that come before the 'before' cursor + */ +function paginateConnection( + arr: readonly T[], + graphqlType: string, + after?: string, + first?: number, + before?: string, + last?: number, +) { + // by default, return the entire array + let startingIndex = 0; + let endingIndex = arr.length; + + // parse the cursors if it exists + after = after ? getIdFromGlobalId(after) : undefined; + before = before ? getIdFromGlobalId(before) : undefined; + + // since we return the whole array by default, there is no previous or next + let hasPreviousPage = false; + let hasNextPage = false; + + // the user doesn't want any items - return an empty list and forget everything else + if (last === 0 || first === 0) { + return { + data: [], + meta: { + cursors: [], + hasNextPage, + hasPreviousPage, + startCursor: null, + endCursor: null, + }, + }; + } + + // step 1 is to slice the array before or after the given cursor + // after will always be truthy but before will only be truthy if it was explicitly passed + // so we check 'before' first. `before` and `after` are mutually exclusive. Behavior is undefined + // if both are passed. + if (before) { + const elementIndex = arr.findIndex((item) => item.id === before); + + if (elementIndex !== -1) { + // we do not include the cursor in the list of items and `end` is exclusive in Array.slice + endingIndex = elementIndex; + } + } else if (after) { + const elementIndex = arr.findIndex((item) => item.id === after); + + if (elementIndex !== -1) { + // we do not include the cursor in the list of items and `start` is inclusive in Array.slice + startingIndex = elementIndex + 1; + } + } + + // remove elements before or after the cursor + arr = arr.slice(startingIndex, endingIndex); + + // step 2 is to limit the array based on first or last + if (first) { + if (first < 0) { + throw new Error(`first cannot be less than 0`); + } + + if (first < arr.length) { + arr = arr.slice(0, first); + // because we removed elements from the end of the list, we have more pages + hasNextPage = true; + } + } else if (last) { + if (last < 0) { + throw new Error(`last cannot be less than 0`); + } + + if (last < arr.length) { + // remove items from front to make the list as long as last + arr = arr.slice(arr.length - last); + + // because we removed elements from the front of the list, set hasPreviousPage + hasPreviousPage = true; + } + } + + // finally, make the metadata info + const cursors = arr.map((item) => makeGlobalId(item.id, graphqlType)); + + return { + data: arr, + meta: { + // the relay spec does not specify `cursors` but the library expects this + cursors, + hasNextPage, + hasPreviousPage, + startCursor: cursors[0], + endCursor: cursors[cursors.length - 1], + }, + }; +} + +/** + * This function parses the connection args. These are the query args passed when using + * `PaginatedHttpResource.getConnection`. + * + * @param query the req.query object + * @returns parsed connection args from the query object + */ +function getConnectionArgsFromQuery(query: Request['query']) { + const cursor = (query.cursor as string) ?? undefined; + const limit = query.limit ? parseInt(query.limit as string) : undefined; + const before = (query.before as string) ?? undefined; + const last = query.last ? parseInt(query.last as string) : undefined; + + return { + cursor, + limit, + before, + last, + }; +} diff --git a/example/index.ts b/example/index.ts new file mode 100644 index 00000000..68a8c65c --- /dev/null +++ b/example/index.ts @@ -0,0 +1,30 @@ +// make sure we init the setup module first +import { HttpApi } from './setup'; + +import { graphqlHTTP } from 'express-graphql'; +import { GraphQLSchema } from 'graphql'; + +import express from 'express'; +import start from './api/api'; +import Query from './types/Query'; + +// start the REST API server +start(8080); + +var app = express(); +app.use( + '/graphql', + graphqlHTTP({ + schema: new GraphQLSchema({ query: Query }), + graphiql: true, + context: { + httpApi: new HttpApi(), + }, + }), +); + +// start the GQL HTTP server +const port = 8081; +app.listen(port, () => { + console.log(`See the GraphiQL playground at http://localhost:${port}/graphql`); +}); diff --git a/example/package.json b/example/package.json new file mode 100644 index 00000000..a9001fcf --- /dev/null +++ b/example/package.json @@ -0,0 +1,25 @@ +{ + "name": "examples", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "ts-node index.ts" + }, + "dependencies": { + "@4c/graphql-node-resource": "^5.1.0", + "@4c/tsconfig": "^0.4.1", + "@types/express": "^4.17.17", + "@types/morgan": "^1.9.4", + "express": "^4.18.2", + "express-graphql": "^0.12.0", + "graphql": "16.5.0", + "graphql-relay": "^0.10.0", + "morgan": "^1.10.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "resolutions": { + "**/graphql": "16.5.0" + } +} diff --git a/example/setup.ts b/example/setup.ts new file mode 100644 index 00000000..f333c54f --- /dev/null +++ b/example/setup.ts @@ -0,0 +1,72 @@ +// omit the "localId" field - include only the `id` field for NodeTypes + +import { + setup, + HttpApi as BaseHttpApi, + fetch, + HttpMethod, +} from '@4c/graphql-node-resource'; +import { getConfig } from '@4c/graphql-node-resource/config'; +import { Maybe } from '@4c/graphql-node-resource/utils/typing'; + +// see the documentation for `setup` for more information. +setup({ localIdFieldMode: 'omit' }); + +export const config = getConfig(); + +// Define how to fetch data from our backend. This will be injected into the context later and used by the NodeType instance. +export class HttpApi extends BaseHttpApi { + async request( + method: HttpMethod, + reqUrl: string, + data?: unknown, + ): Promise> { + console.log(`Making request`, { method, reqUrl, data }); + + const response = await fetch({ + method, + url: reqUrl, + data: data ? (data as any) : undefined, + }); + + if (!response.ok) { + console.log(`Received response`, { status: response.status }); + throw new Error( + `Bad response: ${response.status} / ${response.statusText}`, + ); + } + + try { + // assume our server only responds with JSON + const jsonResponse: any = await response.json(); + + // we can assume this is a connection object + if ('data' in jsonResponse && 'meta' in jsonResponse) { + // the Connection property on NodeType instances assumes an array of items with a meta property + // that contains the required pageInfo data. See `api.ts` for what the shape of this response is. + const items = jsonResponse.data; + Object.assign(items, { + meta: jsonResponse.meta, + }); + + return items; + } + + return jsonResponse; + } catch (error) { + console.error('Could not read JSON data'); + + throw error; + } + } + + constructor() { + super({ + apiBase: '/api', + origin: 'http://localhost:8080', + // you can use external origin in case your API has both an internal and external endpoint + // in this case, we don't. Set it to a safe URL. + externalOrigin: 'http://example.com', + }); + } +} diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 00000000..0c9891f4 --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@4c/tsconfig/node", + "compilerOptions": { + "rootDir": "./", + "outDir": "./lib", + "module": "CommonJS" + }, + "include": ["./**/*.ts"] +} diff --git a/example/types/Author.ts b/example/types/Author.ts new file mode 100644 index 00000000..b198b744 --- /dev/null +++ b/example/types/Author.ts @@ -0,0 +1,48 @@ +// an Author is fetchable by an ID and needs to be used in a connection so we make this a NodeType +// any item used in a Connection must be a NodeType. Note this also has a connection to BlogPost. + +import { + NodeType, + Resource, + HttpContext, + PaginatedHttpResource, + HttpResource, +} from '@4c/graphql-node-resource'; +import { GraphQLString, GraphQLList } from 'graphql'; +import { connectionArgs } from 'graphql-relay'; +import BlogPost from './BlogPost'; + +// You can parameterize the NodeType to provide type data to be used in the field resolvers. +export default new NodeType, { id: string; blogPostIds: string[] }>({ + name: 'Author', + fields: () => ({ + name: { + type: GraphQLString, + }, + + // Normally you would only use a connection, this is to demonstrate + // how NodeTypes can fetch themselves within a list. + blogPosts: { + type: new GraphQLList(BlogPost), + resolve(source) { + // each node can fetch itself if it has an id in the source object + // map the ids to an object containing an `id` field + return source.blogPostIds.map(id => ({id})) + }, + }, + + blogPostsConnection: { + type: BlogPost.Connection, + args: connectionArgs, + resolve(source, args, context) { + const resource = BlogPost.getResource(context) as HttpResource; + // filter by this author's id. Note that the source object's id is the non-global id. + // whereas the `id` field converts this to a global id automatically. + return resource.getConnection({...args, authorId: source.id}) + } + } + }), + createResource(context) { + return new PaginatedHttpResource(context, { endpoint: '/blog/authors/' }); + }, +}); diff --git a/example/types/BlogPost.ts b/example/types/BlogPost.ts new file mode 100644 index 00000000..0799e4e6 --- /dev/null +++ b/example/types/BlogPost.ts @@ -0,0 +1,40 @@ +// GraphQL Types - these make up our GQL API +// a BlogPost is fetchable by an ID and needs to be used in a connection so we make this a NodeType + +import { + NodeType, + Resource, + HttpContext, + PaginatedHttpResource, +} from '@4c/graphql-node-resource'; +import { GraphQLString } from 'graphql'; + +// any item used in a Connection must be a NodeType +export default new NodeType< + Resource, + { id: string; authorId: string } +>({ + name: 'BlogPost', + // we use a "thunk" for the fields to avoid a circular dependency + // this allows all of the Types to be defined before being referenced + fields: () => ({ + author: { + // avoid a circular dependency + type: require("./Author").default, + + resolve: (source) => { + // a NodeType can fetch itself if `id` is provided. + return { id: source.authorId }; + }, + }, + title: { + type: GraphQLString, + }, + content: { + type: GraphQLString, + }, + }), + createResource(context) { + return new PaginatedHttpResource(context, { endpoint: '/blog/posts/' }); + }, +}); diff --git a/example/types/Query.ts b/example/types/Query.ts new file mode 100644 index 00000000..80157a9c --- /dev/null +++ b/example/types/Query.ts @@ -0,0 +1,57 @@ +import { HttpContext, HttpResource } from '@4c/graphql-node-resource'; +import { GraphQLObjectType, GraphQLList } from 'graphql'; +import { connectionArgs } from 'graphql-relay'; +import { config } from '../setup'; +import Author from './Author'; +import BlogPost from './BlogPost'; + +export default new GraphQLObjectType({ + name: 'Query', + fields: { + // required for relay but also a good practice as this allows getting + // hard to reach nodes easily. + node: config.nodeField, + nodes: config.nodesField, + + posts: { + type: new GraphQLList(BlogPost), + resolve(_rc, _args, context, _info) { + // you can use the resource on the NodeType to fetch the GQL type + // from the backend from a parent type. + const resource = BlogPost.getResource(context) as HttpResource; + return resource.get(); + }, + }, + postsConnection: { + // NodeType has a Connection type that can be used to automatically create connections + type: BlogPost.Connection, + resolve: (_source, args, context) => { + const resource = BlogPost.getResource(context) as HttpResource; + return resource.getConnection(args); + }, + // there is a standard set of args for a connection. You can also add other ones + // by spreading this object and adding additional fields such as filters. + args: connectionArgs, + }, + authors: { + type: new GraphQLList(Author), + resolve(_rc, _args, context, _info) { + // you can use the resource on the NodeType to fetch the GQL type + // from the backend from a parent type. + const resource = Author.getResource(context) as HttpResource; + return resource.get(); + }, + }, + authorsConnection: { + // NodeType has a Connection type that can be used to automatically create connections + type: Author.Connection, + resolve: (_source, args, context) => { + const resource = Author.getResource(context) as HttpResource; + return resource.getConnection(args); + }, + // there is a standard set of args for a connection. You can also add other ones + // by spreading this object and adding additional fields such as filters. + args: connectionArgs, + }, + }, +}); diff --git a/example/yarn.lock b/example/yarn.lock new file mode 100644 index 00000000..9cd7d95a --- /dev/null +++ b/example/yarn.lock @@ -0,0 +1,832 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@4c/graphql-node-resource@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@4c/graphql-node-resource/-/graphql-node-resource-5.1.0.tgz#0cc8fcae9ace9ad176a9f1ec5da4e7a61465df48" + integrity sha512-M2HqibquQ4qu5i+vMG4hiexgSxqfDyH8om8mbxLzy4S8n+6+ptqiEcPsKWqzee9jC24QRXuUNSs6+QoFmpyazA== + dependencies: + "@types/lodash" "^4.14.178" + "@types/node" "^17.0.8" + "@types/node-fetch" "^2.5.12" + "@types/pluralize" "^0.0.29" + dataloader "^2.0.0" + express "^4.17.2" + form-data "^4.0.0" + lodash "^4.17.21" + node-fetch "^2.6.1" + pluralize "^8.0.0" + utility-types "^3.10.0" + +"@4c/tsconfig@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@4c/tsconfig/-/tsconfig-0.4.1.tgz#90f7edc7780387788cb6b1d9885b5350ae50431a" + integrity sha512-uCS2WRXvDPj/8i6zOWfiLMj/n4AwVRNMy8FhhEdIie2/DT7PkIzomRXL5Hgq2TRcG0g80lgQRtBYiRYcx9xC1A== + dependencies: + typescript-workspace-plugin "^2.0.1" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + +"@types/lodash@^4.14.178": + version "4.14.195" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" + integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/morgan@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.4.tgz#99965ad2bdc7c5cee28d8ce95cfa7300b19ea562" + integrity sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ== + dependencies: + "@types/node" "*" + +"@types/node-fetch@^2.5.12": + version "2.6.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" + integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "20.4.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.0.tgz#01d637d1891e419bc85763b46f42809cd2d5addb" + integrity sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g== + +"@types/node@^17.0.8": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/pluralize@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c" + integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + +accepts@^1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.4, content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +dataloader@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" + integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express-graphql@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.12.0.tgz#58deabc309909ca2c9fe2f83f5fbe94429aa23df" + integrity sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg== + dependencies: + accepts "^1.3.7" + content-type "^1.0.4" + http-errors "1.8.0" + raw-body "^2.4.1" + +express@^4.17.2, express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + +graphql-relay@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.10.0.tgz#3b661432edf1cb414cd4a132cf595350e524db2b" + integrity sha512-44yBuw2/DLNEiMypbNZBt1yMDbBmyVPVesPywnteGGALiBmdyy1JP8jSg8ClLePg8ZZxk0O4BLhd1a6U/1jDOQ== + +graphql@16.5.0: + version "16.5.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85" + integrity sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA== + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +http-errors@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" + integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-fetch@^2.6.1: + version "2.6.12" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" + integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + dependencies: + whatwg-url "^5.0.0" + +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@^2.4.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript-workspace-plugin@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/typescript-workspace-plugin/-/typescript-workspace-plugin-2.0.1.tgz#3d88be1c35a7fdf2c0160c8cf569ca8993439a12" + integrity sha512-xjIYNFlPIA7IWXvnOFJoAeHPbPJSo0AiQDCRJzaAp3+xZwz6maTgeRLB0oEHVtCqz4Q1CDN6U9kh/2z8sxdDBQ== + +typescript@^5.0.4: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/package.json b/package.json index df181757..493bc034 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "hookem": "^2.0.1", "jest": "^27.5.1", "lint-staged": "^12.5.0", - "prettier": "^2.7.1", + "prettier": "^2.8.7", "typescript": "^4.7.4" }, "engines": { diff --git a/src/config.ts b/src/config.ts index 0d636119..46c0de9a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -67,6 +67,24 @@ function createConfig({ }; } +/** + * Returns the config created by calling `setup`. This can only be called after `setup` is called. This can be used + * to add the `node` and `nodes` field to your Query type. These fields are required by the Relay client.w + * + * @example + * // Query.ts + * // assumes you have already called setup previously + * const config = getConfig(); + * + * export default new GraphQLObjectType({ + * name: "Query", + * fields: () => ({ + * node: config.nodeField, + * nodesField: config.nodesField + * }) + * }) + * @returns the previously created config + */ export function getConfig() { if (!config) { throw new Error('you must first call `setup`'); @@ -74,9 +92,19 @@ export function getConfig() { return config; } -export function setup(options: Parameters[0]) { +/** + * This is used by the Node class to determine how to generate the resulting GraphQL schema. + * This must be called first by your application before trying to generate the GraphQL schema. + * + * @param options a set of options to alter the behavior of this library + * + * @returns the newly created config + */ +export function setup(options: Parameters[0]): Config { if (config && process.env.NODE_ENV !== 'test') { throw new Error("You can't call `setup` twice"); } config = createConfig(options); + + return config; } diff --git a/src/resources/HttpResource.ts b/src/resources/HttpResource.ts index 511563b9..baaa97f5 100644 --- a/src/resources/HttpResource.ts +++ b/src/resources/HttpResource.ts @@ -14,6 +14,18 @@ export type HttpResourceOptions = { endpoint: Endpoint; }; +/** + * This represents a standard REST resource that folows the CRUD + * pattern. Its used by the NodeType class to fetch the + * resource. It can also be used within mutations to modify or create + * resources. + * + * This class assumes the resource does not provide a paginated connection + * response. For resources that support the connection spec, + * use {@link PaginatedHttpResource}. + * + * To get a NodeType's resource, use {@link NodeType.getResource}. + */ export default class HttpResource< TApi extends HttpApi = HttpApi, TContext extends HttpContext = HttpContext, diff --git a/src/resources/PaginatedHttpResource.ts b/src/resources/PaginatedHttpResource.ts index 0e7a3832..331aae44 100644 --- a/src/resources/PaginatedHttpResource.ts +++ b/src/resources/PaginatedHttpResource.ts @@ -1,6 +1,10 @@ import HttpApi, { Args } from '../api/HttpApi'; import HttpResource from './HttpResource'; +/** + * This resource supports endpoints that adhere to the relay connection spec. Use `getConnection` to fetch + * the connection. + */ export default class PaginatedHttpResource< TApi extends HttpApi = HttpApi, > extends HttpResource { diff --git a/src/types/createResolve.ts b/src/types/createResolve.ts index dd2cab2d..ff5b8853 100644 --- a/src/types/createResolve.ts +++ b/src/types/createResolve.ts @@ -2,6 +2,51 @@ import { GraphQLFieldConfig, GraphQLFieldResolver } from 'graphql'; import NodeType from './NodeType'; +/** + * This helper creates a resolver function that fetches the parent type of the field + * and passes the fetched object to the resolver. The parent type must be a NodeType. + * + * By default, the NodeType will fetch itself when any field tries to be resolved unless + * you implement {@link NodeType.makeObjectStub}. Thus, this should only be used in conjuction with + * a NodeType that implements `makeObjectStub`. @see {@link NodeType.makeObjectStub} for more + * information. + * + * If the source object (ie parent type) already has the fields defined on it, + * then no network request will be made. Otherwise, it will fetch the object + * first. + * + * Even if multiple fields use the `createResolve` helper, there will only + * be one network request as the requests will be batched via a DataLoader. + * + * NOTE: This only works for fields within a NodeType! + * + * @example + * new NodeType({ + * name: "Foo", + * fields: () => ({ + * bar: { + * type: GraphQLString + * }, + * baz: { + * type: Baz, + * resolve: createResolve( + * (source) => { + * // source will now have "bar" defined and it can be used for Baz + * return getBaz(source.bar) + * }, + * // specify that we need "bar" in order to resolve "baz" + * ["bar"] + * ) + * } + * }) + * }) + * + * @param resolve a graphql resolver function + * @param fieldNames a list of fields that need to be resolved before calling the passed in + * `resolve` function + * + * @returns an async resolver + */ export default function createResolve< TSource, TContext, diff --git a/src/utils/translateKeys.ts b/src/utils/translateKeys.ts index 006076ec..077fa60b 100644 --- a/src/utils/translateKeys.ts +++ b/src/utils/translateKeys.ts @@ -1,3 +1,25 @@ +/** + * This helper function transforms all keys of an object calling the provided `translate` function. This helper + * recursively iterates through the entire object. Useful in conjunction with lodash's string transformation functions + * such as "kebabCase". + * + * Note: it has a special behavior if the value is a Date. It calls `Date.toISOString` on any Date value found in the object. + * + * @example + * console.log( + * translateKey({ + * hello: 'world', + * bar: new Date(), + * arr: [1, 2, {test: 10}] + * }, (key) => { + * return `foo_${key}` + * }) + * ); // {foo_hello: 'world', bar: '2022-01-01T00:00:00Z', arr: [1, 2, {foo_test: 10}]} + * + * @param data any + * @param translate a function that is called on every key for any encountered object + * @returns a copy of the original data with all keys or values transformed by the provided `translate` function + */ export default function translateKeys( data: unknown, translate: (key: string) => string,