{"id":13700852,"url":"https://github.com/oslabs-beta/GraphQL-Gate","last_synced_at":"2025-05-04T19:33:30.288Z","repository":{"id":37024225,"uuid":"490906858","full_name":"oslabs-beta/GraphQL-Gate","owner":"oslabs-beta","description":"A GraphQL rate limiting library with query complexity analysisfor Node.js","archived":false,"fork":false,"pushed_at":"2022-08-20T17:22:55.000Z","size":739,"stargazers_count":57,"open_issues_count":14,"forks_count":2,"subscribers_count":3,"default_branch":"dev","last_synced_at":"2024-10-10T22:43:23.933Z","etag":null,"topics":["complexity-analysis","expressjs","graphql","middleware","nodejs","ratelimiter"],"latest_commit_sha":null,"homepage":"http://graphqlgate.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oslabs-beta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-11T00:46:12.000Z","updated_at":"2024-08-28T06:42:10.000Z","dependencies_parsed_at":"2022-06-29T08:13:21.200Z","dependency_job_id":null,"html_url":"https://github.com/oslabs-beta/GraphQL-Gate","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oslabs-beta%2FGraphQL-Gate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oslabs-beta%2FGraphQL-Gate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oslabs-beta%2FGraphQL-Gate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oslabs-beta%2FGraphQL-Gate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oslabs-beta","download_url":"https://codeload.github.com/oslabs-beta/GraphQL-Gate/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224406065,"owners_count":17305718,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["complexity-analysis","expressjs","graphql","middleware","nodejs","ratelimiter"],"created_at":"2024-08-02T20:01:03.371Z","updated_at":"2024-11-13T06:31:30.279Z","avatar_url":"https://github.com/oslabs-beta.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n   \u003cimg width=\"50px\" src=\"https://user-images.githubusercontent.com/89324687/182067950-54c00964-2be4-481a-976b-773d9112a4c0.png\"/\u003e\n   \u003ch1\u003eGraphQLGate\u003c/h1\u003e\n   \u003ca href=\"https://github.com/oslabs-beta/GraphQL-Gate\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue\"/\u003e\u003c/a\u003e \u003ca href=\"https://github.com/oslabs-    beta/GraphQL-Gate/stargazers\"\u003e\u003cimg alt=\"GitHub stars\" src=\"https://img.shields.io/github/stars/oslabs-beta/GraphQL-Gate\"\u003e\u003c/a\u003e \u003ca             href=\"https://github.com/oslabs-beta/GraphQL-Gate/issues\"\u003e\u003cimg alt=\"GitHub issues\" src=\"https://img.shields.io/github/issues/oslabs-beta/GraphQL-Gate\"\u003e\u003c/a\u003e \u003cimg alt=\"GitHub last commit\" src=\"https://img.shields.io/github/last-commit/oslabs-beta/GraphQL-Gate\"\u003e\n\n   \u003ch3 align=\"center\"\u003e \u003cstrong\u003eA GraphQL rate-limiting library with query complextiy analysis for Node.js and Express\u003c/strong\u003e\u003c/h3\u003e\n   \u003c/div\u003e\n   \n\u0026nbsp;\n## Summary\n\nDeveloped under tech-accelerator [OSLabs](https://opensourcelabs.io/), GraphQLGate strives for a principled approach to complexity analysis and rate-limiting for GraphQL queries by accurately estimating an upper-bound of the response size of the query. Within a loosely opinionated framework with lots of configuration options, you can reliably throttle GraphQL queries by complexity and depth to protect your GraphQL API. Our solution is inspired by [this paper](https://github.com/Alan-Cha/fse20/blob/master/submissions/functional/FSE-24/graphql-paper.pdf) from IBM research teams.\n\n## Table of Contents\n\n-   [Getting Started](#getting-started)\n-   [Configuration](#configuration)\n-   [Notes on Lists](#lists)\n-   [How It Works](#how-it-works)\n-   [Response](#response)\n-   [Error Handling](#error-handling)\n-   [Internals](#internals)\n-   [Future Development](#future-development)\n-   [Contributions](#contributions)\n-   [Developers](#developers)\n-   [License](#license)\n\n## \u003ca name=\"getting-started\"\u003e\u003c/a\u003e Getting Started\n\nInstall the package\n\n```\nnpm i graphql-limiter\n```\n\nImport the package and add the rate-limiting middleware to the Express middleware chain before the GraphQL server.\n\nNOTE: a Redis server instance will need to be started in order for the limiter to cache data.\n\n```javascript\n// import package\nimport { expressGraphQLRateLimiter } from 'graphql-limiter';\n\n/**\n * Import other dependencies\n * */\n\n//Add the middleware into your GraphQL middleware chain\napp.use(\n    'gql',\n    expressGraphQLRateLimiter(schemaObject, {\n        rateLimiter: {\n            type: 'TOKEN_BUCKET',\n            refillRate: 10,\n            capacity: 100,\n        },\n    }) /** add GraphQL server here */\n);\n```\n\n## \u003ca name=\"configuration\"\u003e\u003c/a\u003e Configuration\n\n1. #### `schema: GraphQLSchema` | required\n\n2. #### `config: ExpressMiddlewareConfig` | required\n\n    - `rateLimiter: RateLimiterOptions` | required\n\n        - `type: 'TOKEN_BUCKET' | 'FIXED_WINDOW' | 'SLIDING_WINDOW_LOG' | 'SLIDING_WINDOW_COUTER'`\n        - `capacity: number`\n        - `refillRate: number` | bucket algorithms only\n        - `windowSize: number` | (in ms) window algorithms only\n\n    - `redis: RedisConfig`\n\n        - `options: RedisOptions` | [ioredis configuration options](https://github.com/luin/ioredis) | defaults to standard ioredis connection options (`localhost:6379`)\n        - `keyExpiry: number` (ms) | custom expiry of keys in redis cache | defaults to 24 hours\n\n    - \u003ca name=\"typeWeights\"\u003e\u003c/a\u003e`typeWeights: TypeWeightObject`\n\n        - `mutation: number` | assigned weight to mutations | defaults to 10\n        - `query: number` | assigned weight of a query | defaults to 1\n        - `object: number` | assigned weight of GraphQL object, interface and union types | defaults to `1`\n        - `scalar: number` | assigned weight of GraphQL scalar and enum types | defaults to `0`\n\n    - `depthLimit: number` | throttle queies by the depth of the nested stucture | defaults to `Infinity` (ie. no limit)\n    - `enforceBoundedLists: boolean` | if true, an error will be thrown if any lists types are not bound by slicing arguments [`first`, `last`, `limit`] or directives | defaults to `false`\n    - `dark: boolean` | if true, the package will calculate complexity, depth and tokens but not throttle any queries. Use this to dark launch the package and monitor the rate limiter's impact without limiting user requests.\n\n    All configuration options\n\n    ```javascript\n    expressGraphQLRateLimiter(schemaObject, {\n        rateLimiter: {\n            type: 'SLIDING_WINDOW_LOG', // rate-limiter selection\n            windowSize: 6000, // 6 seconds\n            capacity: 100,\n        },\n        redis: {\n            keyExpiry: 14400000 // 4 hours, defaults to 86400000 (24 hours)\n            options: {\n                host: 'localhost' // ioredis connection options\n                port: 6379,\n            }\n        },\n        typeWeights: { // weights of GraphQL types\n            mutation: 10,\n            query: 1,\n            object: 1,\n            scalar: 0,\n        },\n        enforceBoundedLists: false, // defaults to false\n        dark: false, // defaults to false\n        depthLimit: 7 // defaults to Infinity (ie. no depth limiting)\n    });\n    ```\n\n## \u003ca name=\"lists\"\u003e\u003c/a\u003e Notes on Lists\n\nFor queries that return a list, the complexity can be determined by providing a slicing argument to the query (`first`, `last`, `limit`), or using a schema directive.\n\n1. Slicing arguments: lists must be bounded by one integer slicing argument in order to calculate the complexity for the field. This package supports the slicing arguments `first`, `last` and `limit`. The complexity of the list will be the value passed as the argument to the field.\n\n2. Directives: To use directives, `@listCost` must be defined in your schema with `directive @listCost(cost: Int!) on FIELD_DEFINITION`. Then, on any field which resolves to an unbounded list, add `@listCost(cost: [Int])` where `[Int]` is the complexity for this field.\n\n(Note: Slicing arguments are preferred and will override the the `@listCost` directive! `@listCost` is in place as a fall back.)\n\n```graphql\ndirective @listCost(cost: Int!) on FIELD_DEFINITION\ntype Human {\n    id: ID!\n}\ntype Query {\n    humans: [Human] @listCost(cost: 10)\n}\n```\n\n## \u003ca name=\"how-it-works\"\u003e\u003c/a\u003e How It Works\n\nRequests are rate-limited based on the IP address associated with the request.\n\nOn startup, the GraphQL (GQL) schema is parsed to build an object that maps GQL types/fields to their corresponding weights. Type weights can be provided during \u003ca href=\"#typeWeights\"\u003einitial configuration\u003c/a\u003e. When a request is received, this object is used to cross reference the fields queried by the user and compute the complexity of each field. The total complexity of the request is the sum of these values.\n\nComplexity is determined, statically (before any resolvers are called) to estimate the upper bound of the response size - a proxy for the work done by the server to build the response. The total complexity is then used to allow/block the request based on popular rate-limiting algorithms.\n\nRequests for each user are processed sequentially by the rate limiter.\n\nExample (with default weights):\n\n```graphql\nquery {\n    # 1 query\n    hero(episode: EMPIRE) {\n        # 1 object\n        name # 0 scalar\n        id # 0 scalar\n        friends(first: 3) {\n            # 3 objects\n            name # 0 scalar\n            id # 0 scalar\n        }\n    }\n    reviews(episode: EMPIRE, limit: 5) {\n        #   5 objects\n        stars # 0 scalar\n        commentary # 0 scalar\n    }\n} # total complexity of 10\n```\n\n## \u003ca name=\"response\"\u003e\u003c/a\u003e Response\n\n1. \u003cb\u003eBlocked Requests\u003c/b\u003e: blocked requests recieve a response with,\n\n    - status of `429` for `Too Many Requests`\n    - `Retry-After` header indicating the time to wait in seconds before the request could be approved (`Infinity` if the complexity is greater than rate-limiting capacity).\n    - A JSON response with the remaining `tokens` available, `complexity` of the query, `depth` of the query, `success` of the query set to `false`, and the UNIX `timestamp` of the request\n\n2. \u003cb\u003eSuccessful Requests\u003c/b\u003e: successful requests are passed on to the next function in the middleware chain with the following properties saved to `res.locals`\n\n```javascript\n{\n   graphqlGate: {\n      success: boolean, // true when successful\n      tokens: number, // tokens available after request\n      compexity: number, // complexity of the query\n      depth: number, // depth of the query\n      timestamp: number, // UNIX timestamp\n   }\n}\n```\n\n## \u003ca name=\"error-handling\"\u003e\u003c/a\u003e Error Handling\n\n-   Incoming queries are validated against the GraphQL schema. If the query is invalid, a response with status code `400` is returned along with an array of GraphQL Errors that were found.\n-   To avoid disrupting server activity, errors thrown during the analysis and rate-limiting of the query are logged and the request is passed onto the next piece of middleware in the chain.\n\n## \u003ca name=\"internals\"\u003e\u003c/a\u003e Internals\n\nThis package exposes 3 additional functionalities which comprise the internals of the package. This is a breif documentaion on them.\n\n### Complexity Analysis\n\n1. #### `typeWeightsFromSchema` | function to create the type weight object from the schema for complexity analysis\n\n    - `schema: GraphQLSchema` | GraphQL schema object\n    - `typeWeightsConfig: TypeWeightConfig = defaultTypeWeightsConfig` | type weight configuration\n    - `enforceBoundedLists = false`\n    - returns: `TypeWeightObject`\n    - usage:\n\n        ```ts\n        import { typeWeightsFromSchema } from 'graphql-limiter';\n        import { GraphQLSchema } from 'graphql/type/schema';\n        import { buildSchema } from 'graphql';\n\n        let schema: GraphQLSchema = buildSchema(`...`);\n\n        const typeWeights: TypeWeightObject = typeWeightsFromSchema(schema);\n        ```\n\n2. #### `QueryParser` | class to calculate the complexity of the query based on the type weights and variables\n\n    - `typeWeights: TypeWeightObject`\n    - `variables: Variables` | variables on request\n    - returns a class with method:\n\n        - `processQuery(queryAST: DocumentNode): number`\n        - returns: complexity of the query and exposes `maxDepth` property for depth limiting\n\n            ```ts\n            import { typeWeightsFromSchema } from 'graphql-limiter';\n            import { parse, validate } from 'graphql';\n\n            let queryAST: DocumentNode = parse(`...`);\n\n            const queryParser: QueryParser = new QueryParser(typeWeights, variables);\n\n            // query must be validatied against the schema before processing the query\n            const validationErrors = validate(schema, queryAST);\n\n            const complexity: number = queryParser.processQuery(queryAST);\n            ```\n\n### Rate-limiting\n\n3. #### `rateLimiter` | returns a rate limiting class instance based on selections\n\n    - `rateLimiter: RateLimiterConfig` | see \"configuration\" -\u003e rateLimiter\n    - `client: Redis` | an ioredis client\n    - `keyExpiry: number` | time (ms) for key to persist in cache\n    - returns a rate limiter class with method:\n\n        - `processRequest(uuid: string, timestamp: number, tokens = 1): Promise\u003cRateLimiterResponse\u003e`\n        - returns: `{ success: boolean, tokens: number, retryAfter?: number }` | where `tokens` is tokens available, `retryAfter` is time to wait in seconds before the request would be successful and `success` is false if the request is blocked\n\n        ```ts\n        import { rateLimiter } from 'graphql-limiter';\n\n        const limiter: RateLimiter = rateLimiter(\n            {\n                type: 'TOKEN_BUCKET',\n                refillRate: 1,\n                capacity: 10,\n            },\n            redisClient,\n            86400000 // 24 hours\n        );\n\n        const response: RateLimiterResponse = limiter.processRequest(\n            'user-1',\n            new Date().valueOf(),\n            5\n        );\n        ```\n\n## \u003ca name=\"future-development\"\u003e\u003c/a\u003e Future Development\n\n-   Ability to use this package with other caching technologies or libraries\n-   Implement \"resolve complexity analysis\" for queries\n-   Implement leaky bucket algorithm for rate-limiting\n-   Experiment with performance improvements\n    -   caching optimization\n-   Ensure connection pagination conventions can be accuratly acconuted for in complexity analysis\n-   Ability to use middleware with other server frameworks\n\n## \u003ca name=\"contributions\"\u003e\u003c/a\u003e Contributions\n\nContributions to the code, examples, documentation, etc. are very much appreciated.\n\n-   Please report issues and bugs directly in this [GitHub project](https://github.com/oslabs-beta/GraphQL-Gate/issues).\n\n## \u003ca name=\"developers\"\u003e\u003c/a\u003e Developers\n\n-   [Evan McNeely](https://github.com/evanmcneely)\n-   [Stephan Halarewicz](https://github.com/shalarewicz)\n-   [Flora Yufei Wu](https://github.com/feiw101)\n-   [Jon Dewey](https://github.com/donjewey)\n-   [Milos Popovic](https://github.com/milos381)\n\n## \u003ca name=\"license\"\u003e\u003c/a\u003e License\n\nThis product is licensed under the MIT License - see the LICENSE.md file for details.\n\nThis is an open source product.\n\nThis product is accelerated by OS Labs.\n","funding_links":[],"categories":["Implementations"],"sub_categories":["JavaScript/TypeScript"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foslabs-beta%2FGraphQL-Gate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foslabs-beta%2FGraphQL-Gate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foslabs-beta%2FGraphQL-Gate/lists"}