├── .circleci └── config.yml ├── .gitignore ├── README.md ├── __init__.py ├── check_graphqljson.py ├── codegen.properties ├── convert.js ├── copy_types_yaml.sh ├── lean_schema ├── __init__.py ├── decomp.py ├── get_types.py ├── post_process.py ├── project_logging.py └── visitors.py ├── makefile ├── requirements.txt ├── setup.py ├── test.requirements.txt └── tests ├── all_films.graphql ├── swapi.js ├── swapi.sdl ├── swapi2.sdl ├── swapi_schema.json ├── test_decomp_swapi.py ├── test_get_types.py ├── test_post_process.py ├── tweet_schema.json ├── tweet_schema.sdl └── types.yaml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` 11 | - image: circleci/python:3.7.6 12 | 13 | # Specify service dependencies here if necessary 14 | # CircleCI maintains a library of pre-built images 15 | # documented at https://circleci.com/docs/2.0/circleci-images/ 16 | # - image: circleci/postgres:9.4 17 | 18 | working_directory: ~/lean_schema 19 | 20 | steps: 21 | - checkout 22 | 23 | # Download and cache dependencies 24 | - restore_cache: 25 | keys: 26 | - v1-dependencies-{{ checksum "requirements.txt" }} 27 | # fallback to using the latest cache if no exact match is found 28 | - v1-dependencies- 29 | 30 | - run: 31 | name: install dependencies 32 | command: | 33 | python -m venv ./venv 34 | pip install --upgrade pip 35 | pip install -r requirements.txt 36 | 37 | - save_cache: 38 | paths: 39 | - ./venv 40 | key: v1-dependencies-{{ checksum "requirements.txt" }} 41 | 42 | # run tests! 43 | # this example uses Django's built-in test-runner 44 | # other common Python testing frameworks include pytest and nose 45 | # https://pytest.org 46 | # https://nose.readthedocs.io 47 | - run: 48 | name: run tests 49 | command: | 50 | make test 51 | 52 | - store_test_results: 53 | path: test-reports 54 | 55 | - store_artifacts: 56 | path: test-reports 57 | destination: test-reports 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | test-reports/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE 105 | .idea/ 106 | 107 | # JS stuff for convert.js 108 | node_modules/ 109 | package-lock.json 110 | tests/schema.json 111 | 112 | # Project 113 | cov_html 114 | codegen 115 | queries 116 | log.decomp 117 | lean_schema.json 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Intuit Lean Schema 3 | 4 | | LeanSchema Version | Supported Apollo Version(s) | 5 | | ------------- |:-------------:| 6 | | 2.0.1 | 2.22.0 | 7 | 8 | ## Coverage 9 | ```bash 10 | Name Stmts Miss Cover 11 | ------------------------------- 12 | decomp.py 319 31 90% 13 | ``` 14 | 15 | ## Overview 16 | Welcome to Intuit LeanSchema! LeanSchema is a tool to shrink your GraphQL Schema for your production Mobile Applications. If you have a semi-large Schema (say, > 100 Types) then LeanSchema will help you reduce your Mobile App sizes and reduce your compilation times. 17 | 18 | ### Sure, but what does this **do**? 19 | It takes a set of GraphQL Queries and a GraphQL Schema as input and produces a much **smaller** Lean Schema. The lean GraphQL Schema is passed to Apollo Client/Codegen to generate iOS Swift Code. 20 | 21 | **Any** application/service/library/widget that uses the a large GraphQL Schema can benefit. Here's a real-life example for the Intuit QuickBooks Self-Employed Mobile iOS App: 22 | ``` 23 | GraphQL Query folder size: 24 | Full Schema : 7.1 MB 25 | Lean Schema : 1.8 MB 26 | 27 | Swift compile times clean build: 28 | Full Schema : 526 seconds 29 | Lean Schema : 216 seconds 30 | ``` 31 | LeanSchema reduced the amount of generated Swift code by 75% and reduced compile time by 50%. 32 | 33 | ### Who's using this? 34 | LeanSchema is used by many Production Mobile Apps at Intuit, including (but not limited to): 35 | - QuickBooks Self-Employed 36 | - Payments Mobile 37 | - Turbo Mobile 38 | 39 | ## Requirements 40 | - [Python](https://www.python.org/downloads/) >= 3.7.3. 41 | - [NPM](https://www.npmjs.com/get-npm) >= 6.13.7. 42 | - A set of GraphQL queries compatible with `Apollo v2.22.0`. Any set of valid GraphQL queries **should** work. 43 | 44 | ### Install Requirements on Mac OS 45 | ```bash 46 | brew install python 47 | brew upgrade python 48 | brew install npm 49 | brew upgrade npm 50 | ``` 51 | 52 | ## Get the Code 53 | [Download a release](https://github.com/intuit/lean-schema/releases) or clone [this repo](https://github.com/intuit/lean-schema). 54 | 55 | ## Install the Project 56 | ```bash 57 | cd lean-schema 58 | make install 59 | ``` 60 | 61 | ## Read and Edit the `codegen.properties` file 62 | `codegen.properties` is used to control important things like: 63 | - Where are your queries located? 64 | - Where is your GraphQL Schema located? 65 | 66 | ### Set the `GRAPHQL_QUERIES_DIR` variable 67 | Example: `GRAPHQL_QUERIES_DIR=/home/$YOU/proj/qb-mobile-graphql` 68 | 69 | ### Set the `GRAPHQL_SCHEMA_FILE` variable 70 | Example: `GRAPHQL_SCHEMA_FILE=/home/$YOU/proj/graphql.json` 71 | 72 | **Please Note**! LeanSchema currently understands [GraphQL Introspection Format](https://blog.apollographql.com/three-ways-to-represent-your-graphql-schema-a41f4175100d) Schemas. Please see the linked article for how to convert SDL and GraphQLSchemaObject Schemas to the Introsepction Format. 73 | 74 | ### Set the `COPY_UNMATCHED_FILES_DIR` variable 75 | 76 | Like it says in the file, this controls where generated code files 77 | that can't be matched by filename end up. 78 | 79 | Example: Swift code generation creates a file called 80 | `Types.swift`. Where does this file go after codegen? If 81 | COPY_UNMATCHED_FILES_DIR is properly set, then it will be copied to 82 | $COPY_UNMATCHED_FILES_DIR/Types.swift. 83 | 84 | ## Run the Code Generation 85 | ```bash 86 | make codegen 87 | ``` 88 | 89 | ## Build Artifacts 90 | The generated code is located in `./codegen`. If 91 | `COPY_GENERATED_FILES_AFTER_CODEGEN=true`, then all the generated 92 | files are copied to $GRAPHQL_QUERIES_DIR by matching filenames. 93 | 94 | Example: 95 | ``` 96 | │   ├── updateTripRule.graphql # This is a query 97 | │   ├── updateTripRule.graphql.swift # This is the matching Swift file 98 | │   ├── updateVehicle.graphql # This is a query 99 | │   └── updateVehicle.graphql.swift # This is the matching Swift file 100 | ├── package.json 101 | ├── README.md 102 | ├── updateCompanyInfoFromSettings.graphql # This is a query 103 | └── updateCompanyInfoFromSettings.graphql.swift # This is the matching Swift file 104 | ``` 105 | 106 | ## Clean-up ie Reset the Project 107 | ```bash 108 | make clean 109 | ``` 110 | 111 | # Extra Options 112 | ## Missing Types 113 | LeanSchema is fairly aggresive in how many Types it prunes from the Schema. If you notice certain Types or Domains-of-Types are missing in the `lean_schema.json` file, you have these options: 114 | 115 | ## Increase the INPUT_OBJECT_DEPTH_LEVEL variable 116 | In `./codegen.properties`: 117 | ``` 118 | # Default value is zero 119 | INPUT_OBJECT_DEPTH_LEVEL=0 120 | ``` 121 | Increasing INPUT_OBJECT_DEPTH_LEVEL "unfolds" GraphQL InputObject Types. Example: your Queries include the `CreateSales_SaleInput` Type, which references the `Sales_SaleInput` Type. 122 | - If `INPUT_OBJECT_DEPTH_LEVEL=0`, only `CreateSales_SaleInput` is included. 123 | - Else if `INPUT_OBJECT_DEPTH_LEVEL=1`, both `CreateSales_SaleInput` and `Sales_SaleInput` are included 124 | - Else if `INPUT_OBJECT_DEPTH_LEVEL=2`, then **everything** that `Sales_SaleInput` references is included as well. 125 | 126 | INPUT_OBJECT_DEPTH_LEVEL affects all of your InputObjects. These are Types that are typicaly used by Mutation Queries. 127 | 128 | ## Specify Types/Domains in types.yaml 129 | The `./types.yaml` file is an optional file to specify which Types and Domains-of-Types you want to include in `lean_schema.json`. Example: 130 | ```yaml 131 | # We want the following Schema Types 132 | types: 133 | # We want everything that CreateSales_SaleInput references, in addition to CreateSales_SaleInput itself 134 | - "CreateSales_SaleInput": 135 | depth: 1 136 | # We just want Network_Contact and Entity 137 | - "Network_Contact" 138 | - "Entity" 139 | 140 | # We want everything in the "risk" Domain at depth=0 141 | domains: 142 | - "risk" 143 | ``` 144 | 145 | `types.yaml` lets you exactly state "trees-of-Types" to include in `lean_schema.json` by stating the root Types. Currently, Domains-of-Types are only included at depth=0. In the above example, everything under `risk` is included but **not** their direct references unless those types are found in your Queries. 146 | 147 | # Questions & Answers 148 | 149 | ## When do I need to run `make install`? 150 | On initial project setup and if a new version of the tool is released. 151 | 152 | ## When do I need to run `make codegen`? 153 | When your GraphQL queries or GraphQL Schema change. 154 | 155 | ## How do I edit the Apollo command for Codegen? 156 | If you need to change the Apollo commands, just change the `codegen` rule in the `makefile`: 157 | ```makefile 158 | apollo client:codegen --passthroughCustomScalars --localSchemaFile=lean_schema.json --queries="queries/**/*.graphql" --target=swift codegen/ 159 | ``` 160 | 161 | ## Turn off the file copy & match for generated files? 162 | Set `COPY_GENERATED_FILES_AFTER_CODEGEN=false` in `codegen.properties` 163 | 164 | ## Generate a single large file for codegen? 165 | Change the `codegen` makefile command to this: 166 | ```makefile 167 | apollo client:codegen --passthroughCustomScalars --localSchemaFile=lean_schema.json --queries="queries/**/*.graphql" --target=swift codegen.lean.swift 168 | ``` 169 | Only a single file named `codegen.lean.swift` will be created. 170 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/lean-schema/3105329a08d92a57dd7c2a08c6144577eb58d204/__init__.py -------------------------------------------------------------------------------- /check_graphqljson.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | import os 6 | import argparse 7 | 8 | def main(): 9 | 10 | parser = argparse.ArgumentParser(description='Check if a schema file is valid for our purposes') 11 | parser.add_argument('schema_file', help='Path to the schema file to validate') 12 | args = parser.parse_args() 13 | 14 | abs_path = os.path.abspath(args.schema_file) 15 | if not (os.path.exists(abs_path) and os.path.isfile(abs_path)): 16 | print("schema_file must be a file", file=sys.stderr) 17 | sys.exit(1) 18 | 19 | if not is_file_good(abs_path): 20 | print("You must have a valid schema_file. If you only have SDL, you may use the convert.js script, which requires node.", file=sys.stderr) 21 | sys.exit(2) 22 | 23 | """ 24 | Check if the schema file is good for our purposes. 25 | Basically, this boils down to if it is valid json with either: 26 | .data.__schema 27 | or 28 | .__schema 29 | """ 30 | def is_file_good(file_path): 31 | 32 | # open file and read as JSON 33 | with open(file_path) as f: 34 | try: 35 | contents = json.load(f) 36 | except: 37 | return False 38 | 39 | # check the .__schema case 40 | if '__schema' in contents: 41 | return True 42 | # check the .data.__schema case 43 | elif 'data' in contents and '__schema' in contents['data']: 44 | return True 45 | else: 46 | return False 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /codegen.properties: -------------------------------------------------------------------------------- 1 | # Change this to the top-level directory of your GraphQL queries 2 | # Ex: all your queries are located under /home/YOU/queries 3 | GRAPHQL_QUERIES_DIR=SET_THIS_VARIABLE 4 | 5 | # This is the absolute path to the FULL schema. 6 | GRAPHQL_SCHEMA_FILE=SET_THIS_VARIABLE 7 | 8 | # Copy all the Swift/$Language files to $GRAPHQL_QUERIES_DIR, and 9 | # attempt to match my filename? If yes, then go through the directory 10 | # structure. If we see a query like getMyStuff.graphql and we have a 11 | # Swift file named getMyStuff.swift, then copy the Swift file to the 12 | # matching directory. Repeat until done. 13 | COPY_GENERATED_FILES_AFTER_CODEGEN=true 14 | 15 | # If we can't match a generated file to a .graphql file, then copy if 16 | # to this directory. Ex: Apollo Codegen for Swift creates a 17 | # Types.swift file. This doesn't match a .graphql file, but still 18 | # needs to be somewhere in your project. Specify that as a directory 19 | # using this variable. 20 | COPY_UNMATCHED_FILES_DIR=SET_THIS_VARIABLE 21 | 22 | # This is the YAML file that specifies the List of Types and 23 | # Domains-of-Types to include. Optionally, you can specify the 24 | # transitive Depth of each Type as well. 25 | # Example: 26 | # types: 27 | # - "CreateSales_SaleInput": 28 | # depth: 1 29 | # The above means "Add the CreateSales_SaleInput type and its direct 30 | # references (ie depth 1 refs) to the Lean Schema" See the LeanSchema 31 | # github doc for more explanation 32 | TYPES_YAML_FILE=types.yaml 33 | 34 | # Please see the README for an explanation of what this does 35 | INPUT_OBJECT_DEPTH_LEVEL=0 -------------------------------------------------------------------------------- /convert.js: -------------------------------------------------------------------------------- 1 | //const transform = require('graphql-to-json-schema'); 2 | const fs = require('fs'); 3 | //const { buildSchema, graphqlSync, introspectionQuery } = require("graphql"); 4 | //const { buildSchema, graphql, introspectionQuery } = require("graphql"); 5 | const graphql = require('graphql'); 6 | 7 | function usage() { 8 | console.log("Some usage function"); 9 | } 10 | 11 | function main(argv) { 12 | if (!argv) { 13 | argv = process.argv.slice(2); 14 | } 15 | 16 | if (argv.length < 1) { 17 | usage(); 18 | process.exit(0) 19 | } 20 | 21 | argv.forEach(function(S) { 22 | if (S === "--help" || S === "-h") { 23 | usage(); 24 | process.exit(0); 25 | } 26 | }); 27 | 28 | var schemaPath = argv[0]; 29 | 30 | if (!schemaPath) { 31 | console.error("Missing required parameter $schemaPath"); 32 | process.exit(1); 33 | } 34 | 35 | fs.readFile(schemaPath, "utf-8", function(err, data) { 36 | //console.log(data); 37 | const graphqlSchemaObj = graphql.buildSchema(data); 38 | const result = graphql.graphqlSync(graphqlSchemaObj, graphql.introspectionQuery).data; 39 | console.log(JSON.stringify(result)); 40 | }); 41 | } 42 | 43 | main() 44 | -------------------------------------------------------------------------------- /copy_types_yaml.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | if [[ $1 ]] 4 | then 5 | if [[ -e $1 ]] 6 | then 7 | echo "Types YAML file $1 exists, copying it" 8 | cp $1 $2 9 | else 10 | echo "Types YAML file $1 does not exist, ignoring it" 11 | exit 0 12 | fi 13 | else 14 | echo "Types YAML property is not set, not copying anything" 15 | fi 16 | -------------------------------------------------------------------------------- /lean_schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/lean-schema/3105329a08d92a57dd7c2a08c6144577eb58d204/lean_schema/__init__.py -------------------------------------------------------------------------------- /lean_schema/decomp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create some subset of a GraphQL Schema given some input parameters 3 | 4 | """ 5 | __author__ = "prussell" 6 | 7 | import argparse 8 | import json 9 | import logging 10 | import os 11 | import queue 12 | import select 13 | import sys 14 | import traceback 15 | import typing 16 | import uuid 17 | import yaml 18 | 19 | LOG_LEVELS = { 20 | "DEBUG": logging.DEBUG, 21 | "INFO": logging.INFO, 22 | "WARNING": logging.WARNING, 23 | "ERROR": logging.ERROR, 24 | "CRITICAL": logging.CRITICAL, 25 | } 26 | DEFAULT_LOG_FILE = "log.decomp" 27 | 28 | 29 | class SchemaNode(object): 30 | def __init__(self, key, value): 31 | self.key = key 32 | self.value = value 33 | self.inbound = [] 34 | self.outbound = [] 35 | 36 | def __repr__(self): 37 | return "SchemaNode -> {}".format(self.key) 38 | 39 | 40 | """ 41 | User defined GraphQL Types. In GraphQL, Object is NOT the root of 42 | the Type heiarchy, it's just one of these 43 | 44 | """ 45 | GRAPHQL_DEFINED_TYPES = {"enum", "input_object", "interface", "object", "union"} 46 | 47 | 48 | class SwiftLanguage(object): 49 | """ 50 | Data for the Swift Language back-end. Example: if we're 51 | generating for Swift, how do we handle the V4 Schema BigDecimal 52 | Type? Answer: replace if with Swift's Decimal type. 53 | 54 | """ 55 | 56 | # Mapping between V4 Schema BigDecimal Type and the swift language 57 | # Decimal Type 58 | scalars_dict = {"BigDecimal": "Decimal"} 59 | KEY = "swift" 60 | # Additional Types that we need to define and add to the 61 | # sub-schema 62 | additional_types = { 63 | "Decimal": { 64 | "inputFields": None, 65 | "interfaces": None, 66 | "possibleTypes": None, 67 | "kind": "SCALAR", 68 | "name": "Decimal", 69 | "description": "Swift's Decimal Type, see https://developer.apple.com/documentation/foundation/decimal", 70 | "fields": None, 71 | "enumValues": None, 72 | # Notify later logic that this type replace BigDecimal and to remove BigDecimal from graph 73 | "$decomp.type_replaces": "BigDecimal", 74 | } 75 | } 76 | 77 | 78 | LANGUAGES_TABLE = {SwiftLanguage.KEY: SwiftLanguage} 79 | 80 | 81 | class GraphQLTypeRef(object): 82 | """ 83 | Proxy Type Reference for a a GraphQL Type. 84 | 85 | Possible fields for a Type seem to be: 86 | ['kind', 87 | 'name', 88 | 'description', 89 | 'fields', 90 | 'inputFields', 91 | 'interfaces', 92 | 'enumValues', 93 | 'possibleTypes'] 94 | 95 | """ 96 | 97 | @classmethod 98 | def to_dict(cls, kind: str = None, name: str = None, description: str = None): 99 | return { 100 | "kind": (kind if kind is not None else cls.kind), 101 | "name": (name if name is not None else cls.__name__), 102 | "description": ( 103 | description if description is not None else "GraphQLTypeRef" 104 | ), 105 | "fields": (cls.fields if hasattr(cls, "fields") else None), 106 | "inputFields": (cls.inputFields if hasattr(cls, "inputFields") else None), 107 | "interfaces": (cls.interfaces if hasattr(cls, "interfaces") else None), 108 | "enumValues": (cls.enumValues if hasattr(cls, "enumValues") else None), 109 | "possibleTypes": ( 110 | cls.possibleTypes if hasattr(cls, "possibleTypes") else None 111 | ), 112 | } 113 | 114 | @classmethod 115 | def get_name(cls): 116 | return cls.name if hasattr(cls, "name") else cls.__name__ 117 | 118 | 119 | class GraphQLObjectTypeRef(GraphQLTypeRef): 120 | kind = "OBJECT" 121 | interfaces = [] 122 | fields = [] 123 | 124 | 125 | class GraphQLInterfaceTypeRef(GraphQLTypeRef): 126 | kind = "INTERFACE" 127 | fields = [] 128 | 129 | 130 | class GraphQLUnionTypeRef(GraphQLTypeRef): 131 | kind = "UNION" 132 | possibleTypes = [] 133 | 134 | 135 | class GraphQLEnumTypeRef(GraphQLTypeRef): 136 | kind = "ENUM" 137 | enumValues = [] 138 | 139 | 140 | class GraphQLInputObjectTypeRef(GraphQLTypeRef): 141 | kind = "INPUT_OBJECT" 142 | inputFields = [] 143 | 144 | 145 | GRAPHQL_TYPE_REF_DICT = { 146 | "enum": GraphQLEnumTypeRef, 147 | "object": GraphQLObjectTypeRef, 148 | "union": GraphQLUnionTypeRef, 149 | "input_object": GraphQLInputObjectTypeRef, 150 | "interface": GraphQLInterfaceTypeRef, 151 | } 152 | 153 | 154 | def get_typeref_for(kind: str) -> GraphQLTypeRef: 155 | return GRAPHQL_TYPE_REF_DICT[kind.lower()] 156 | 157 | 158 | def is_graphql_type_ref(node: dict) -> bool: 159 | """ 160 | Is this a GraphQL Schema JSON Type reference? 161 | 162 | From testing, all the GraphQL User Defined Types have these fields: 163 | - kind 164 | - name 165 | 166 | """ 167 | return ( 168 | "kind" in node 169 | and "name" in node 170 | and 171 | # 'ofType' in node 172 | # and 173 | node["kind"].lower() in GRAPHQL_DEFINED_TYPES 174 | ) 175 | 176 | 177 | def is_scalar_ref(node: dict) -> bool: 178 | """ 179 | Is this a Scalar reference to the scalar_name Type? 180 | 181 | """ 182 | return "kind" in node and node["kind"].lower() == "scalar" 183 | 184 | 185 | def get_outbound_type_refs( 186 | root: dict, res: typing.List[str] = None 187 | ) -> typing.List[str]: 188 | """ 189 | Get all outbound vertex keys for some GraphQL Schema Node. 190 | Basically an adjacent node reference is defined as: 191 | A sub-JSON Object with obj['type']['kind'] == OBJECT 192 | Then the key is obj['type']['name'] 193 | 194 | This isn't as clear as in JSON Schema, where all refs are marked 195 | by $ref. 196 | 197 | """ 198 | if res is None: 199 | res = [] 200 | 201 | stack = [root] 202 | while stack: 203 | node = stack.pop() 204 | if type(node) is list: 205 | stack.extend(node) 206 | elif type(node) is dict: 207 | if is_graphql_type_ref(node): 208 | res.append(node["name"]) 209 | stack.extend(node.items()) 210 | elif type(node) is tuple: 211 | stack.append(node[0]) 212 | stack.append(node[1]) 213 | 214 | return res 215 | 216 | 217 | def update_type_refs(root, graph, subgraph_keys, scalars_dict: dict = None): 218 | """ 219 | Update all outbound refs of a GraphQL schema node to only 220 | reference things that are in the sub-graph 221 | 222 | """ 223 | if scalars_dict is None: 224 | scalars_dict = {} 225 | 226 | stack = [root] 227 | while stack: 228 | node = stack.pop() 229 | if type(node) is list: 230 | stack.extend(node) 231 | elif type(node) is dict: 232 | # Is this an Object reference ie another non-Scalar type? 233 | if is_graphql_type_ref(node): 234 | # Check name of object and if its allowed inthe subgraph 235 | node_key = node["name"] 236 | node_kind = node["kind"] 237 | if node_key not in subgraph_keys: 238 | type_ref = get_typeref_for(node_kind) 239 | logging.debug( 240 | "Replacing {} with {}, is not in subgraph".format( 241 | node_key, type_ref.get_name() 242 | ) 243 | ) 244 | node["name"] = type_ref.get_name() 245 | node["typeref_name"] = node_key 246 | 247 | # Update Scalar Types if we need to 248 | elif is_scalar_ref(node): 249 | scalar_key = node["name"] 250 | if scalar_key in scalars_dict: 251 | # Replace with the value 252 | scalar_value = scalars_dict[scalar_key] 253 | node["name"] = scalar_value 254 | 255 | for key in node: 256 | value = node[key] 257 | stack.append(value) 258 | 259 | 260 | def mk_graph_from_schema(graphql_schema: dict) -> dict: 261 | """ 262 | Make a simple Adjacency List representation of the Schema Types 263 | from a GraphQL Schema formatted Object. 264 | 265 | A GraphQL Schema is basically just something that looks like this: 266 | {"errors" : [], 267 | "data" : {"__schema" : {..., 'types', ...}} 268 | } 269 | or just 270 | {"__schema" : {..., 'types', ...}} 271 | 272 | """ 273 | # We don't care about anything other than types 274 | if "data" in graphql_schema: 275 | types = graphql_schema["data"]["__schema"]["types"] 276 | elif "__schema" in graphql_schema: 277 | types = graphql_schema["__schema"]["types"] 278 | elif "types" in graphql_schema: 279 | types = graphql_schema["types"] 280 | else: 281 | raise ValueError("Invalid GraphQL Schema, must have a 'types' section") 282 | adj = {} 283 | 284 | for T in types: 285 | K = T["name"] 286 | V = T 287 | node = SchemaNode(K, V) 288 | node.outbound = get_outbound_type_refs(T) 289 | adj[K] = node 290 | 291 | for K in adj: 292 | node = adj[K] 293 | for K2 in node.outbound: 294 | node2 = adj[K2] 295 | node2.inbound.append(K) 296 | 297 | return adj 298 | 299 | 300 | def load_schema(file_path: str): 301 | with open(file_path) as ifile: 302 | return json.load(ifile) 303 | 304 | 305 | def convert_type_path_key(type_path: str, sep="/") -> str: 306 | """ 307 | Convert some Intuit Type path key to GraphQL Schema Type key. 308 | Ex: '/network/Contact' -> 'Network_Contact' 309 | """ 310 | return "_".join(t[0].upper() + t[1:] for t in type_path.split(sep) if t) 311 | 312 | 313 | def get_subtypes_of_domain(schema, domain, key_func=convert_type_path_key) -> list: 314 | """ 315 | Q&D version of this, just examine the paths of each type 316 | 317 | """ 318 | R = [] 319 | norm_domain = key_func(domain) 320 | 321 | for type_path in schema: 322 | if type_path.startswith(norm_domain): 323 | R.append(type_path) 324 | 325 | return R 326 | 327 | 328 | def reduce_graphql_schema(schema, graph, subgraph_keys): 329 | """ 330 | Reduce the GraphQL Schema to only include what's in the computed 331 | sub-graph 332 | 333 | """ 334 | # Get root set 335 | schema["__schema"]["types"] = [ 336 | graph[key].value for key in subgraph_keys if key in graph 337 | ] 338 | return schema 339 | 340 | 341 | def all_scalar_types(root): 342 | 343 | stack = [root] 344 | R = set() 345 | 346 | while stack: 347 | 348 | node = stack.pop() 349 | 350 | if type(node) is list: 351 | stack.extend(node) 352 | 353 | elif type(node) is dict: 354 | if "kind" in node and node["kind"].lower() == "scalar": 355 | R.add(node["name"]) 356 | 357 | stack.extend(node.items()) 358 | 359 | elif type(node) is tuple: 360 | stack.append(node[0]) 361 | stack.append(node[1]) 362 | 363 | return R 364 | 365 | 366 | def get_neighboring_types(G: dict, type_key, depth_level): 367 | Q = queue.Queue() 368 | Q.put((type_key, 0)) 369 | seen = set() 370 | 371 | while not Q.empty(): 372 | (node_key, depth) = Q.get() 373 | 374 | if node_key not in seen: 375 | seen.add(node_key) 376 | 377 | if depth >= depth_level: 378 | continue 379 | 380 | if node_key in G: 381 | node_val = G[node_key] 382 | for neighbor in node_val.outbound: 383 | Q.put((neighbor, depth + 1)) 384 | else: 385 | logging.warning( 386 | "Invalid Type Key {} not in Schema, cannot find any neighboring Types for it".format( 387 | node_key 388 | ) 389 | ) 390 | 391 | return seen 392 | 393 | 394 | def get_types_from_file(G: dict, types_file: dict, types_set: set = None) -> set: 395 | """ 396 | Get additional User-specified Types from a object from a file. 397 | 398 | @param types_file: the object loaded from the --types-file CLI parameter 399 | 400 | return: the set of additional Types constructed from the YAML specification 401 | 402 | """ 403 | if types_set is None: 404 | types_set = set() 405 | 406 | if "types" in types_file: 407 | types_from_file = types_file["types"] 408 | if type(types_from_file) is str: 409 | types_set.add(convert_type_path_key(types_from_file)) 410 | elif type(types_from_file) is list: 411 | for subtype in types_from_file: 412 | # Allow String or dict in the Types 413 | if type(subtype) is str: 414 | types_set.add(convert_type_path_key(subtype)) 415 | elif type(subtype) is dict and len(subtype.keys()) == 1: 416 | type_key = list(subtype.keys())[0] 417 | if "depth" in subtype[type_key]: 418 | depth = int(subtype[type_key]["depth"]) 419 | neighbors = get_neighboring_types( 420 | G, convert_type_path_key(type_key), depth 421 | ) 422 | types_set.update(neighbors) 423 | else: 424 | 425 | error_msg = "Unrecognized value for key 'types' in file {}, must have type list or str, but is {}".format( 426 | types_file, type(types_from_file) 427 | ) 428 | logging.error(error_msg) 429 | raise ValueError(error_msg) 430 | 431 | # Load all types of domain 432 | if "domains" in types_file: 433 | for domain in types_file["domains"]: 434 | types_set.update(get_subtypes_of_domain(G, convert_type_path_key(domain))) 435 | 436 | return types_set 437 | 438 | 439 | def get_types_from_input(): 440 | """ 441 | Check any input JSON for additional Types 442 | """ 443 | types = set() 444 | 445 | if select.select([sys.stdin], [], [], 0.0)[0]: 446 | text_in = sys.stdin.read() 447 | if text_in.strip(): 448 | logging.debug("Additional keys from stdin: {}".format(text_in)) 449 | types.update(json.loads(text_in)["types"]) 450 | else: 451 | logging.debug("No additional subgraph keys from stdin") 452 | 453 | return types 454 | 455 | 456 | def check_input_object_depth_level(value): 457 | try: 458 | if int(value) < 0: 459 | raise ValueError 460 | except: 461 | if value is None or value == "": 462 | return 0 463 | else: 464 | raise argparse.ArgumentTypeError("{} must be an integer >= 0".format(value)) 465 | 466 | return int(value) 467 | 468 | 469 | def main(args): 470 | 471 | parser = argparse.ArgumentParser() 472 | parser.add_argument("SCHEMA_FILE", help="Path to the Intuit Schema JSON file") 473 | parser.add_argument( 474 | "--types-file", help="Path to the (optional) Types YAML file", default=None 475 | ) 476 | parser.add_argument( 477 | "--log-level", 478 | help="The log level for all non-JSON output", 479 | choices=LOG_LEVELS.keys(), 480 | default="ERROR", 481 | ) 482 | parser.add_argument( 483 | "--log-file", 484 | help="The file to log all non-JSON output to", 485 | default=DEFAULT_LOG_FILE, 486 | ) 487 | parser.add_argument( 488 | "--input-object-depth-level", 489 | help="The level of Type references to include for GraphQL InputObjects. If certain Types are missing in the generated code, try setting this to 1 or 2. All transitive Types up to N levels away will be included.", 490 | default=0, 491 | type=check_input_object_depth_level, 492 | ) 493 | parser.add_argument( 494 | "--target-language", 495 | help="The target Programming Language. It's toolchain will consume the output LeanSchema and generate code. Ex: Apollo Codegen generates Swift, TypeScript and Scala code", 496 | choices=LANGUAGES_TABLE.keys(), 497 | default=SwiftLanguage.KEY, 498 | ) 499 | args = parser.parse_args(args) 500 | 501 | with open(args.SCHEMA_FILE, encoding="utf-8") as ifile: 502 | schema = json.load(ifile) 503 | 504 | schema = schema["data"] if "data" in schema else schema 505 | 506 | # Types file is optional, data can come from stdin or it 507 | types_file = {} 508 | if args.types_file is not None: 509 | types_file_path = os.path.abspath(args.types_file) 510 | if os.path.exists(types_file_path): 511 | with open(types_file_path, encoding="utf-8") as ifile: 512 | types_file = yaml.load(ifile.read()) or {} 513 | 514 | try: 515 | log_level = LOG_LEVELS[args.log_level] 516 | except KeyError: 517 | print( 518 | "Invalid logging level {}, use one of {}".format(args.log_level, LOG_LEVELS) 519 | ) 520 | exit(1) 521 | 522 | logfile_path_prefix = os.path.split(args.log_file)[0] 523 | if logfile_path_prefix and not os.path.exists(logfile_path_prefix): 524 | print("Log file directory {} does not exist".format(logfile_path_prefix)) 525 | exit(1) 526 | 527 | log_file = args.log_file or DEFAULT_LOG_FILE 528 | 529 | logging.basicConfig(filename=log_file, level=log_level) 530 | logging.getLogger().addHandler(logging.StreamHandler()) 531 | run_uuid = uuid.uuid4() 532 | logging.debug("START run {}".format(run_uuid)) 533 | 534 | logging.debug("Adding GraphQLTypeRef types to Schema") 535 | for typeref_type in GRAPHQL_TYPE_REF_DICT.values(): 536 | schema["__schema"]["types"].append(typeref_type.to_dict()) 537 | 538 | graph = mk_graph_from_schema(schema) 539 | # Load all directly stated Types/Domains from file 540 | root_keys = set() 541 | types_size = 0 542 | root_keys.update(get_types_from_file(graph, types_file)) 543 | types_size = len(root_keys) 544 | logging.debug( 545 | "Types increased from 0 to {} from types-from-file".format(types_size) 546 | ) 547 | 548 | # Get any additional Root Keys specified from stdin 549 | try: 550 | root_keys.update(get_types_from_input()) 551 | except: 552 | logging.debug(traceback.format_exc()) 553 | logging.error("Error reading type keys from stdin, is it valid JSON?") 554 | exit(1) 555 | logging.debug( 556 | "Types increased from {} to {} from types-from-input".format( 557 | types_size, len(root_keys) 558 | ) 559 | ) 560 | types_size = len(root_keys) 561 | 562 | # Get the set of keys for the valid subgraph 563 | subgraph_keys = {obj.get_name() for obj in GRAPHQL_TYPE_REF_DICT.values()} 564 | subgraph_keys.update(root_keys) 565 | 566 | # Stuff like {'BigDecimal', 'Boolean', 'Float', 'ID', 'Int', 567 | # 'Long', 'String'} is defined in the Schema, so have to add it 568 | # back 569 | subgraph_keys.update(all_scalar_types(schema)) 570 | logging.debug( 571 | "Types increased from {} to {} by adding all scalar types".format( 572 | types_size, len(subgraph_keys) 573 | ) 574 | ) 575 | types_size = len(subgraph_keys) 576 | # 'Hard-coded' types to add 577 | subgraph_keys.add("Schema_Schema_StringSchema0") 578 | logging.debug( 579 | "Types increased from {} to {} by adding hard-coded types".format( 580 | types_size, len(subgraph_keys) 581 | ) 582 | ) 583 | types_size = len(subgraph_keys) 584 | 585 | # Unfold InputObjects up to some depth 586 | for input_object in [ 587 | k 588 | for k in subgraph_keys 589 | if k in graph and graph[k].value["kind"] == "INPUT_OBJECT" 590 | ]: 591 | nset = get_neighboring_types(graph, input_object, args.input_object_depth_level) 592 | subgraph_keys.update(nset) 593 | 594 | logging.debug( 595 | "Types increased from {} to {} by unfolding InputObjects to depth = {}".format( 596 | types_size, len(subgraph_keys), args.input_object_depth_level 597 | ) 598 | ) 599 | 600 | # Prune/clean up subraph by removing references to Types not in 601 | # the subraph. Also replace any Scalar Types that don't exist for 602 | # our target language. 603 | target_language = LANGUAGES_TABLE[args.target_language] 604 | # Add all additional type from the target language, if any 605 | if hasattr(target_language, "additional_types"): 606 | for tkey in target_language.additional_types: 607 | tvalue = target_language.additional_types[tkey] 608 | schema_node = SchemaNode(tkey, tvalue) 609 | graph[tkey] = schema_node 610 | subgraph_keys.add(tkey) 611 | if "$decomp.type_replaces" in tvalue: 612 | replaced_type = tvalue["$decomp.type_replaces"] 613 | if replaced_type in graph: 614 | del graph[replaced_type] 615 | subgraph_keys.remove(replaced_type) 616 | else: 617 | logging.warning( 618 | "Cannot remove Scalar Type {}, does it exist in Schema?".format( 619 | replaced_type 620 | ) 621 | ) 622 | 623 | for key in subgraph_keys: 624 | if key in graph: 625 | node = graph[key].value 626 | update_type_refs( 627 | node, graph, subgraph_keys, scalars_dict=target_language.scalars_dict 628 | ) 629 | assert graph[key].value == node 630 | else: 631 | logging.warning( 632 | "Bad Type key, is it defined in the Schema?: {}".format(key) 633 | ) 634 | 635 | # Shrink the schema to only include whats in the subgraph 636 | reduce_graphql_schema(schema, graph, subgraph_keys) 637 | 638 | logging.debug("END run {}".format(run_uuid)) 639 | print(json.dumps(schema)) 640 | return schema 641 | 642 | 643 | if __name__ == "__main__": 644 | main(sys.argv[1:]) 645 | -------------------------------------------------------------------------------- /lean_schema/get_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Input 3 | A Collection and a GraphQL Schema. 4 | 5 | # Process 6 | ## Visit 7 | For-each GraphQLQuery, parse it into a Document. Visit each Document 8 | using the AllTypesVisitor to get all directly referenced Types in the 9 | Document. Build a Set as you go and return that. 10 | 11 | ## Expansion 12 | For-each directly referenced Type, Expand it based on rules such as: 13 | 14 | 1. If the Type is a Generic Type, then 'unfold' the Type until we 15 | reach a Concrete Type. Ex: List -> String 16 | 17 | 2. If the Type is an Interface Type, then find all Concrete Types that 18 | implement the Interface. At least one of them will be referenced. 19 | 20 | Other expansion rules can apply. 21 | 22 | # Output 23 | A Set of Type Names that are referenced, or should be 24 | referenced according to our Type Expansion rules. 25 | 26 | The Output of this program is the Input to the Decomp program, which 27 | actually breaks down the Graph by using the referenced Types as Roots. 28 | 29 | """ 30 | 31 | __author__ = "prussell" 32 | 33 | import json 34 | import os 35 | import sys 36 | import typing 37 | import argparse 38 | import enum 39 | import graphql 40 | from graphql.language import DocumentNode, visit 41 | from graphql.language.visitor import TypeInfoVisitor 42 | from graphql.utilities import TypeInfo 43 | from graphql.validation.validation_context import ValidationContext 44 | from lean_schema.project_logging import logger 45 | from lean_schema.visitors import AllTypesVisitor 46 | 47 | 48 | class ExitErrorCodes(enum.Enum): 49 | """ 50 | Exit codes for the get_types.py program 51 | 52 | """ 53 | 54 | OK = 0 55 | SCHEMA_FILE_NOT_EXISTS = 1 56 | SCHEMA_FILE_NOT_A_FILE = 2 57 | QUERY_PATH_NOT_EXISTS = 3 58 | QUERY_PATH_NOT_FILE_OR_DIRECTORY = 4 59 | 60 | 61 | def load_schema(schema_path): 62 | """Load the Intuit Schema. Apparently it differs a bit from what 63 | Graphene wants, specifically Graphene doesn't recognize the 64 | top-level "errors" and "data" fields 65 | """ 66 | 67 | with open(os.path.abspath(schema_path)) as ifile: 68 | ischema = json.load(ifile) 69 | if "data" in ischema: 70 | ischema = ischema["data"] 71 | return graphql.build_client_schema(ischema) 72 | 73 | 74 | def visit_document_file(query_path, schema): 75 | with open(os.path.abspath(query_path)) as ifile: 76 | doc = ifile.read() 77 | 78 | return visit_document(graphql.parse(doc), schema) 79 | 80 | 81 | def visit_document(document_ast: DocumentNode, schema): 82 | 83 | if not document_ast or not isinstance(document_ast, DocumentNode): 84 | raise TypeError("You must provide a document node.") 85 | # If the schema used for validation is invalid, throw an error. 86 | type_info = TypeInfo(schema) 87 | context = ValidationContext(schema, document_ast, type_info) 88 | # This uses a specialized visitor which runs multiple visitors in parallel, 89 | # while maintaining the visitor skip and break API. 90 | 91 | # Visit the whole document with each instance of all provided rules. 92 | # A single document can have multiple Fragments, and we assume a single Query 93 | all_types = set() 94 | for def_ast in document_ast.definitions: 95 | visitor = AllTypesVisitor(context) 96 | visit(def_ast, TypeInfoVisitor(type_info, visitor)) 97 | all_types.update(visitor.types) 98 | 99 | return all_types 100 | 101 | 102 | def get_named_type(_type): 103 | """Equivalent of this function from the Facebook GrapHQL lib from graphql/type/definitions.js: 104 | function getNamedType(type) { 105 | /* eslint-enable no-redeclare */ 106 | if (type) { 107 | var unwrappedType = type; 108 | while (isWrappingType(unwrappedType)) { 109 | unwrappedType = unwrappedType.ofType; 110 | } 111 | return unwrappedType; 112 | } 113 | } 114 | 115 | Basically we need to 'un-wrap' Type References that are 116 | generic/composite. The JS GraphQL lib defines a Wrapping Type 117 | (currently) as a ListType or GraphQLNonNull 118 | 119 | """ 120 | unwrapped_type = _type 121 | while hasattr(unwrapped_type, "of_type"): 122 | unwrapped_type = unwrapped_type.of_type 123 | 124 | return unwrapped_type 125 | 126 | 127 | def expand_type(_type, schema, type_names: set = None) -> typing.Set[str]: 128 | if type_names is None: 129 | type_names = set() 130 | 131 | if hasattr(_type, "name"): 132 | type_names.add(_type.name) 133 | try: 134 | for sub_type in schema.get_possible_types(_type): 135 | type_names.add(sub_type.name) 136 | except KeyError: 137 | logger.debug("No sub-types for %s", _type.name) 138 | 139 | if hasattr(_type, "of_type"): 140 | type_names.add(get_named_type(_type).name) 141 | 142 | return type_names 143 | 144 | 145 | def expand_types(types, schema) -> typing.Set[str]: 146 | type_names = set() 147 | for _type in types: 148 | if _type: 149 | type_names.update(expand_type(_type, schema, type_names)) 150 | 151 | return type_names 152 | 153 | 154 | def visit_document_directory( 155 | root_path: str, schema, file_extensions=("graphql", "gql"), all_types: set = None 156 | ) -> set: 157 | if all_types is None: 158 | all_types = set() 159 | 160 | for root, _, files in os.walk(root_path): 161 | for filename in files: 162 | if filename.split(".")[-1] in file_extensions: 163 | full_path = os.path.join(root, filename) 164 | logger.debug("Processing file %s", full_path) 165 | all_types.update(visit_document_file(full_path, schema)) 166 | 167 | return all_types 168 | 169 | 170 | def main(main_args): 171 | parser = argparse.ArgumentParser(main_args) 172 | parser.add_argument("SCHEMA_FILE", help="The Schema File to load") 173 | parser.add_argument( 174 | "INPUT_TLD", help="Top-level-directory of the set of Queries to process" 175 | ) 176 | parser.add_argument( 177 | "--verbose", help="Verbose output, DEBUG and below", action="store_true" 178 | ) 179 | parser.add_argument( 180 | "--sorted", help="Sort the output type names", action="store_true" 181 | ) 182 | args = parser.parse_args() 183 | 184 | if not args.verbose: 185 | logger.setLevel("WARNING") 186 | 187 | if not os.path.exists(os.path.abspath(args.SCHEMA_FILE)): 188 | print( 189 | "SCHEMA_FILE does not exist: {}".format(args.SCHEMA_FILE), file=sys.stderr 190 | ) 191 | sys.exit(ExitErrorCodes.SCHEMA_FILE_NOT_EXISTS) 192 | 193 | if not os.path.isfile(os.path.abspath(args.SCHEMA_FILE)): 194 | print("SCHEMA_FILE is not a file: {}".format(args.SCHEMA_FILE), file=sys.stderr) 195 | sys.exit(ExitErrorCodes.SCHEMA_FILE_NOT_A_FILE) 196 | 197 | abs_input_tld = os.path.abspath(args.INPUT_TLD) 198 | if not os.path.exists(abs_input_tld): 199 | print("INPUT_TLD does not exist: {}".format(args.INPUT_TLD), file=sys.stderr) 200 | sys.exit(ExitErrorCodes.QUERY_PATH_NOT_EXISTS) 201 | 202 | input_is_file = os.path.isfile(abs_input_tld) 203 | if not input_is_file and not os.path.isdir(abs_input_tld): 204 | print( 205 | "INPUT_TLD is not a file or directory: {}".format(args.INPUT_TLD), 206 | file=sys.stderr, 207 | ) 208 | sys.exit(ExitErrorCodes.QUERY_PATH_NOT_FILE_OR_DIRECTORY) 209 | 210 | schema = load_schema(os.path.abspath(args.SCHEMA_FILE)) 211 | all_types = set() 212 | # Support both single file / top-level-directory 213 | if input_is_file: 214 | all_types.update(visit_document_file(abs_input_tld, schema)) 215 | else: 216 | visit_document_directory(abs_input_tld, schema, all_types=all_types) 217 | 218 | type_names = expand_types(all_types, schema) 219 | 220 | return json.dumps({"types": list(type_names)}, indent=2) 221 | 222 | 223 | if __name__ == "__main__": 224 | print(main(sys.argv[1:])) 225 | -------------------------------------------------------------------------------- /lean_schema/post_process.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | Post processing for Lean Schema Codegen. Does stuff like: 5 | - Copy/match codegen files to the source queries directory 6 | 7 | """ 8 | 9 | __author__ = "prussell" 10 | 11 | import argparse 12 | import os 13 | import shutil 14 | import sys 15 | import typing 16 | 17 | 18 | def main(prog_args: typing.List[str]): 19 | parser = argparse.ArgumentParser(prog_args) 20 | parser.add_argument( 21 | "src_dir", help="The source directory of the generated codegen files" 22 | ) 23 | parser.add_argument( 24 | "dst_dir", 25 | help="The destination directory to copy the generated codegen files to", 26 | ) 27 | parser.add_argument( 28 | "--copy-codegen-files", 29 | help=( 30 | "Copy generated code files to" 31 | + " some destination directory and attempt" 32 | + " to match by query filenames" 33 | ), 34 | choices=("true", "True", "False", "false", True, False), 35 | default=True, 36 | ) 37 | parser.add_argument( 38 | "--src-files-extension", 39 | help="The extension of the codegen sourcefiles", 40 | default="swift", 41 | ) 42 | parser.add_argument("--debug", action="store_true", help="Turn on debugging info") 43 | parser.add_argument( 44 | "--copy-unmatched-files-dir", 45 | help="Directory to copy unmatched code-generated files to", 46 | default=None, 47 | ) 48 | 49 | args = parser.parse_args(prog_args) 50 | 51 | if args.copy_codegen_files not in {True, "true", "True"}: 52 | print( 53 | "--copy-codegen-files is not set to True: {}".format( 54 | args.copy_codegen_files 55 | ) 56 | ) 57 | sys.exit(0) 58 | 59 | # Error checking 60 | src_dir = os.path.abspath(args.src_dir) 61 | if not (os.path.exists(src_dir) and os.path.isdir(src_dir)): 62 | print("src_dir {} does not exist!".format(src_dir), file=sys.stderr) 63 | sys.exit(1) 64 | 65 | dst_dir = os.path.abspath(args.dst_dir) 66 | if not (os.path.exists(dst_dir) and os.path.isdir(dst_dir)): 67 | print("dst_dir {} does not exist!".format(dst_dir), file=sys.stderr) 68 | sys.exit(1) 69 | 70 | if args.copy_unmatched_files_dir is not None: 71 | copy_unmatched_files_dir = os.path.abspath(args.copy_unmatched_files_dir) 72 | if not ( 73 | os.path.exists(copy_unmatched_files_dir) 74 | and os.path.isdir(copy_unmatched_files_dir) 75 | ): 76 | print( 77 | "copy-unmatched-files-dir {} does not exist!".format( 78 | copy_unmatched_files_dir 79 | ), 80 | file=sys.stderr, 81 | ) 82 | sys.exit(1) 83 | else: 84 | copy_unmatched_files_dir = None 85 | 86 | # Get all codegen filenames and store as a dict. Apollo 87 | # (currently) dumps them out as a flat directory 88 | files_table = { 89 | filename.split(".")[0]: { 90 | "src": os.path.join(os.path.abspath(src_dir), filename), 91 | "dst": copy_unmatched_files_dir, 92 | } 93 | for filename in os.listdir(src_dir) 94 | if filename.endswith(args.src_files_extension) 95 | } 96 | 97 | # Trawl through the dst tree and match by file prefix to find the dest file-path 98 | for root, dirs, files in os.walk(dst_dir): 99 | for fname in files: 100 | key = fname.split(".")[0] 101 | if key in files_table: 102 | files_table[key]["dst"] = os.path.join(root, fname) 103 | 104 | # We now have a complete mapping of src->dst, just copy everything 105 | for value in files_table.values(): 106 | src = value["src"] 107 | dst = value["dst"] 108 | if dst is None: 109 | print("Error: codegen file {} does not have a match in dst?".format(src)) 110 | continue 111 | 112 | if not os.path.isdir(dst): 113 | dst_dir = os.path.split(os.path.abspath(dst))[0] 114 | else: 115 | dst_dir = dst 116 | 117 | src_fname = os.path.split(src)[1] 118 | dst_fname = os.path.join(dst_dir, src_fname) 119 | if args.debug: 120 | print("Copying {} to {}".format(src, dst_fname)) 121 | shutil.copyfile(src, dst_fname) 122 | 123 | 124 | if __name__ == "__main__": 125 | main(sys.argv[1:]) 126 | -------------------------------------------------------------------------------- /lean_schema/project_logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple logging configuration. All log statements go to the various 3 | handlers defined below. 4 | 5 | """ 6 | 7 | __author__ = "prussell" 8 | 9 | ##### STDLIB 10 | import logging 11 | import logging.config 12 | import os 13 | 14 | ##### INIT AND DECLARATIONS 15 | LOG_LEVELS = {"NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} 16 | LOG_LEVEL_ENV_VAR = "LOGGING_LEVEL" 17 | LOGGER_NAME = os.getenv("LOGGER_NAME", "common-logger") 18 | LOG_LEVEL = os.getenv(LOG_LEVEL_ENV_VAR, "DEBUG") 19 | if LOG_LEVEL not in LOG_LEVELS: 20 | raise EnvironmentError( 21 | "Invalid value '{}' for env var {}, must be one of: {}".format( 22 | LOG_LEVEL, LOG_LEVEL_ENV_VAR, LOG_LEVELS 23 | ) 24 | ) 25 | LOGGING_SETTINGS = { 26 | "version": 1, 27 | "formatters": { 28 | "verbose": { 29 | "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d: %(message)s" 30 | }, 31 | "simple": {"format": "%(levelname)s %(asctime)s %(module)s: %(message)s"}, 32 | }, 33 | "handlers": { 34 | "console": { 35 | "level": "DEBUG", 36 | "class": "logging.StreamHandler", 37 | "formatter": "simple", 38 | } 39 | }, 40 | "loggers": { 41 | LOGGER_NAME: {"handlers": ["console"], "level": LOG_LEVEL, "propagate": True} 42 | }, 43 | } 44 | 45 | logging.config.dictConfig(LOGGING_SETTINGS) 46 | # Actual logger object that we use to write log statements 47 | logger = logging.getLogger(LOGGER_NAME) 48 | -------------------------------------------------------------------------------- /lean_schema/visitors.py: -------------------------------------------------------------------------------- 1 | from graphql.language.visitor import Visitor 2 | 3 | 4 | class AllTypesVisitor(Visitor): 5 | """ 6 | Visitor to get all referenced Types in a Query and collect them as 7 | a single Set. Abstract types are expanded in get_types.py, then the 8 | entire Root Set is passed to decomp.py to create the actual sub-graph. 9 | 10 | """ 11 | 12 | __slots__ = ("context",) 13 | 14 | def __init__(self, context): 15 | self.types = set() 16 | self.context = context 17 | 18 | def enter( 19 | self, 20 | node, # type: Field 21 | key, # type: int 22 | parent, # type: Union[List[Union[Field, InlineFragment]], List[Field]] 23 | path, # type: List[Union[int, str]] 24 | ancestors, # type: List[Any] 25 | ): 26 | self.types.add(self.context.get_type()) 27 | self.types.add(self.context.get_input_type()) 28 | self.types.add(self.context.get_parent_type()) 29 | self.types.add(self.context.get_parent_input_type()) 30 | 31 | def enter_FragmentSpread( 32 | self, 33 | node, # type: Field 34 | key, # type: int 35 | parent, # type: Union[List[Union[Field, InlineFragment]], List[Field]] 36 | path, # type: List[Union[int, str]] 37 | ancestors, # type: List[Any] 38 | ): 39 | fragName = node.name.value 40 | fragType = self.context.getFragmentType(fragName) 41 | if fragType: 42 | self.types.add(fragType) 43 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Include everything from the properties file as an env var referencable here 2 | include codegen.properties 3 | export 4 | 5 | VENV_DIR = ./venv 6 | PYTHON3 = $(VENV_DIR)/bin/python3 7 | PIP3 = $(VENV_DIR)/bin/pip3 8 | PYTEST = $(VENV_DIR)/bin/python3 -m pytest 9 | APOLLO_PACKAGE_VERSION=2.22.0 10 | 11 | .PHONY: lean_schema test clean install codegen 12 | 13 | test: 14 | $(PIP3) install -r requirements.txt 15 | $(PIP3) install -r test.requirements.txt 16 | $(PYTEST) --cov-report term --cov-report html --junitxml=test-reports/junit.xml --cov=lean_schema/ tests/ 17 | 18 | install: 19 | python3 -m venv $(VENV_DIR) 20 | npm install -g apollo@$(APOLLO_PACKAGE_VERSION) 21 | $(PIP3) install --upgrade pip 22 | $(PIP3) install -r requirements.txt 23 | 24 | codegen: lean_schema 25 | ls -lah lean_schema.json && apollo client:codegen --passthroughCustomScalars --localSchemaFile=lean_schema.json --queries="queries/**/*.graphql" --target=swift codegen/ 26 | $(PYTHON3) ./lean_schema/post_process.py --copy-unmatched-files-dir=$(COPY_UNMATCHED_FILES_DIR) --copy-codegen-files=$(COPY_GENERATED_FILES_AFTER_CODEGEN) ./codegen $(GRAPHQL_QUERIES_DIR) 27 | 28 | lean_schema: 29 | ./check_graphqljson.py $(GRAPHQL_SCHEMA_FILE) 30 | mkdir -p queries/ 31 | cp $(GRAPHQL_SCHEMA_FILE) queries/graphql_schema.json 32 | find $(GRAPHQL_QUERIES_DIR) -name '*.graphql' | xargs -I % cp % ./queries/ 33 | find $(GRAPHQL_QUERIES_DIR) -name '*.gql' | xargs -I % cp % ./queries/ 34 | bash ./copy_types_yaml.sh "$(TYPES_YAML_FILE)" queries/types.yaml 35 | $(PYTHON3) -m lean_schema.get_types queries/graphql_schema.json queries/ | $(PYTHON3) -m lean_schema.decomp queries/graphql_schema.json --types-file queries/types.yaml --input-object-depth-level=$(INPUT_OBJECT_DEPTH_LEVEL) | tee lean_schema.json 1> /dev/null 36 | 37 | clean: 38 | - find . -name "*~" | xargs rm 39 | - rm -rf .pytest_cache/ 40 | - rm ./*.scala 41 | - rm ./*.swift 42 | - echo "" > apollo.log 43 | - find . -name __generated__ | xargs rm -rf 44 | - rm -rf ./queries/* 45 | - rm -rf ./queries/.[!.]* 46 | - rm -rf ./codegen/ 47 | - rm ./lean_schema.json 48 | - rm ./log.decomp 49 | - rm ./apollo.log 50 | - rm -rf lean_schema.egg-info 51 | - find . -name __pycache__ | xargs rm -rf 52 | - rm -rf ./node_modules 53 | - rm package-lock.json 54 | - rm -rf cov_html/ 55 | - rm -rf htmlcov/ 56 | - rm -rf test-reports/ 57 | - rm -rf venv/ 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML 2 | graphql-core-next==1.1.1 3 | . 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | from setuptools import find_packages 5 | from os import getenv 6 | 7 | setup( 8 | name="lean-schema", 9 | version=getenv("PYTHON_ARTIFACT_VERSION", "0.2.0"), 10 | description="The LeanSchema Project to decompose your GraphQL Schema.", 11 | author="Philip Russell", 12 | author_email="philip_russell@intuit.com", 13 | url="https://github.com/intuit/lean-schema/", 14 | packages=find_packages(), 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.8", 18 | ], 19 | include_package_data=True, 20 | ) 21 | -------------------------------------------------------------------------------- /test.requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | 4 | -------------------------------------------------------------------------------- /tests/all_films.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allFilms { 3 | films { 4 | title 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/swapi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | /** 9 | * Copyright (c) 2015, Facebook, Inc. 10 | * All rights reserved. 11 | * 12 | * This source code is licensed under the license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | const schemaString = ` 16 | schema { 17 | query: Query 18 | mutation: Mutation 19 | subscription: Subscription 20 | } 21 | 22 | # The query type, represents all of the entry points into our object graph 23 | type Query { 24 | hero(episode: Episode): Character 25 | reviews(episode: Episode!): [Review] 26 | search(text: String): [SearchResult] 27 | character(id: ID!): Character 28 | droid(id: ID!): Droid 29 | human(id: ID!): Human 30 | starship(id: ID!): Starship 31 | } 32 | 33 | # The mutation type, represents all updates we can make to our data 34 | type Mutation { 35 | createReview(episode: Episode, review: ReviewInput!): Review 36 | } 37 | 38 | # The subscription type, represents all subscriptions we can make to our data 39 | type Subscription { 40 | reviewAdded(episode: Episode): Review 41 | } 42 | 43 | # The episodes in the Star Wars trilogy 44 | enum Episode { 45 | # Star Wars Episode IV: A New Hope, released in 1977. 46 | NEWHOPE 47 | 48 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 49 | EMPIRE 50 | 51 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 52 | JEDI 53 | } 54 | 55 | # A character from the Star Wars universe 56 | interface Character { 57 | # The ID of the character 58 | id: ID! 59 | 60 | # The name of the character 61 | name: String! 62 | 63 | # The friends of the character, or an empty list if they have none 64 | friends: [Character] 65 | 66 | # The friends of the character exposed as a connection with edges 67 | friendsConnection(first: Int, after: ID): FriendsConnection! 68 | 69 | # The movies this character appears in 70 | appearsIn: [Episode]! 71 | } 72 | 73 | # Units of height 74 | enum LengthUnit { 75 | # The standard unit around the world 76 | METER 77 | 78 | # Primarily used in the United States 79 | FOOT 80 | } 81 | 82 | # A humanoid creature from the Star Wars universe 83 | type Human implements Character { 84 | # The ID of the human 85 | id: ID! 86 | 87 | # What this human calls themselves 88 | name: String! 89 | 90 | # The home planet of the human, or null if unknown 91 | homePlanet: String 92 | 93 | # Height in the preferred unit, default is meters 94 | height(unit: LengthUnit = METER): Float 95 | 96 | # Mass in kilograms, or null if unknown 97 | mass: Float 98 | 99 | # This human's friends, or an empty list if they have none 100 | friends: [Character] 101 | 102 | # The friends of the human exposed as a connection with edges 103 | friendsConnection(first: Int, after: ID): FriendsConnection! 104 | 105 | # The movies this human appears in 106 | appearsIn: [Episode]! 107 | 108 | # A list of starships this person has piloted, or an empty list if none 109 | starships: [Starship] 110 | } 111 | 112 | # An autonomous mechanical character in the Star Wars universe 113 | type Droid implements Character { 114 | # The ID of the droid 115 | id: ID! 116 | 117 | # What others call this droid 118 | name: String! 119 | 120 | # This droid's friends, or an empty list if they have none 121 | friends: [Character] 122 | 123 | # The friends of the droid exposed as a connection with edges 124 | friendsConnection(first: Int, after: ID): FriendsConnection! 125 | 126 | # The movies this droid appears in 127 | appearsIn: [Episode]! 128 | 129 | # This droid's primary function 130 | primaryFunction: String 131 | } 132 | 133 | # A connection object for a character's friends 134 | type FriendsConnection { 135 | # The total number of friends 136 | totalCount: Int 137 | 138 | # The edges for each of the character's friends. 139 | edges: [FriendsEdge] 140 | 141 | # A list of the friends, as a convenience when edges are not needed. 142 | friends: [Character] 143 | 144 | # Information for paginating this connection 145 | pageInfo: PageInfo! 146 | } 147 | 148 | # An edge object for a character's friends 149 | type FriendsEdge { 150 | # A cursor used for pagination 151 | cursor: ID! 152 | 153 | # The character represented by this friendship edge 154 | node: Character 155 | } 156 | 157 | # Information for paginating this connection 158 | type PageInfo { 159 | startCursor: ID 160 | endCursor: ID 161 | hasNextPage: Boolean! 162 | } 163 | 164 | # Represents a review for a movie 165 | type Review { 166 | # The movie 167 | episode: Episode 168 | 169 | # The number of stars this review gave, 1-5 170 | stars: Int! 171 | 172 | # Comment about the movie 173 | commentary: String 174 | } 175 | 176 | # The input object sent when someone is creating a new review 177 | input ReviewInput { 178 | # 0-5 stars 179 | stars: Int! 180 | 181 | # Comment about the movie, optional 182 | commentary: String 183 | 184 | # Favorite color, optional 185 | favorite_color: ColorInput 186 | } 187 | 188 | # The input object sent when passing in a color 189 | input ColorInput { 190 | red: Int! 191 | green: Int! 192 | blue: Int! 193 | } 194 | 195 | type Starship { 196 | # The ID of the starship 197 | id: ID! 198 | 199 | # The name of the starship 200 | name: String! 201 | 202 | # Length of the starship, along the longest axis 203 | length(unit: LengthUnit = METER): Float 204 | 205 | coordinates: [[Float!]!] 206 | } 207 | 208 | union SearchResult = Human | Droid | Starship 209 | `; 210 | 211 | console.log(schemaString); 212 | var _default = schemaString; 213 | exports.default = _default; 214 | -------------------------------------------------------------------------------- /tests/swapi.sdl: -------------------------------------------------------------------------------- 1 | 2 | schema { 3 | query: Query 4 | mutation: Mutation 5 | subscription: Subscription 6 | } 7 | 8 | # The query type, represents all of the entry points into our object graph 9 | type Query { 10 | hero(episode: Episode): Character 11 | reviews(episode: Episode!): [Review] 12 | search(text: String): [SearchResult] 13 | character(id: ID!): Character 14 | droid(id: ID!): Droid 15 | human(id: ID!): Human 16 | starship(id: ID!): Starship 17 | } 18 | 19 | # The mutation type, represents all updates we can make to our data 20 | type Mutation { 21 | createReview(episode: Episode, review: ReviewInput!): Review 22 | } 23 | 24 | # The subscription type, represents all subscriptions we can make to our data 25 | type Subscription { 26 | reviewAdded(episode: Episode): Review 27 | } 28 | 29 | # The episodes in the Star Wars trilogy 30 | enum Episode { 31 | # Star Wars Episode IV: A New Hope, released in 1977. 32 | NEWHOPE 33 | 34 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 35 | EMPIRE 36 | 37 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 38 | JEDI 39 | } 40 | 41 | # A character from the Star Wars universe 42 | interface Character { 43 | # The ID of the character 44 | id: ID! 45 | 46 | # The name of the character 47 | name: String! 48 | 49 | # The friends of the character, or an empty list if they have none 50 | friends: [Character] 51 | 52 | # The friends of the character exposed as a connection with edges 53 | friendsConnection(first: Int, after: ID): FriendsConnection! 54 | 55 | # The movies this character appears in 56 | appearsIn: [Episode]! 57 | } 58 | 59 | # Units of height 60 | enum LengthUnit { 61 | # The standard unit around the world 62 | METER 63 | 64 | # Primarily used in the United States 65 | FOOT 66 | } 67 | 68 | # A humanoid creature from the Star Wars universe 69 | type Human implements Character { 70 | # The ID of the human 71 | id: ID! 72 | 73 | # What this human calls themselves 74 | name: String! 75 | 76 | # The home planet of the human, or null if unknown 77 | homePlanet: String 78 | 79 | # Height in the preferred unit, default is meters 80 | height(unit: LengthUnit = METER): Float 81 | 82 | # Mass in kilograms, or null if unknown 83 | mass: Float 84 | 85 | # This human's friends, or an empty list if they have none 86 | friends: [Character] 87 | 88 | # The friends of the human exposed as a connection with edges 89 | friendsConnection(first: Int, after: ID): FriendsConnection! 90 | 91 | # The movies this human appears in 92 | appearsIn: [Episode]! 93 | 94 | # A list of starships this person has piloted, or an empty list if none 95 | starships: [Starship] 96 | } 97 | 98 | # An autonomous mechanical character in the Star Wars universe 99 | type Droid implements Character { 100 | # The ID of the droid 101 | id: ID! 102 | 103 | # What others call this droid 104 | name: String! 105 | 106 | # This droid's friends, or an empty list if they have none 107 | friends: [Character] 108 | 109 | # The friends of the droid exposed as a connection with edges 110 | friendsConnection(first: Int, after: ID): FriendsConnection! 111 | 112 | # The movies this droid appears in 113 | appearsIn: [Episode]! 114 | 115 | # This droid's primary function 116 | primaryFunction: String 117 | } 118 | 119 | # A connection object for a character's friends 120 | type FriendsConnection { 121 | # The total number of friends 122 | totalCount: Int 123 | 124 | # The edges for each of the character's friends. 125 | edges: [FriendsEdge] 126 | 127 | # A list of the friends, as a convenience when edges are not needed. 128 | friends: [Character] 129 | 130 | # Information for paginating this connection 131 | pageInfo: PageInfo! 132 | } 133 | 134 | # An edge object for a character's friends 135 | type FriendsEdge { 136 | # A cursor used for pagination 137 | cursor: ID! 138 | 139 | # The character represented by this friendship edge 140 | node: Character 141 | } 142 | 143 | # Information for paginating this connection 144 | type PageInfo { 145 | startCursor: ID 146 | endCursor: ID 147 | hasNextPage: Boolean! 148 | } 149 | 150 | # Represents a review for a movie 151 | type Review { 152 | # The movie 153 | episode: Episode 154 | 155 | # The number of stars this review gave, 1-5 156 | stars: Int! 157 | 158 | # Comment about the movie 159 | commentary: String 160 | } 161 | 162 | # The input object sent when someone is creating a new review 163 | input ReviewInput { 164 | # 0-5 stars 165 | stars: Int! 166 | 167 | # Comment about the movie, optional 168 | commentary: String 169 | 170 | # Favorite color, optional 171 | favorite_color: ColorInput 172 | } 173 | 174 | # The input object sent when passing in a color 175 | input ColorInput { 176 | red: Int! 177 | green: Int! 178 | blue: Int! 179 | } 180 | 181 | type Starship { 182 | # The ID of the starship 183 | id: ID! 184 | 185 | # The name of the starship 186 | name: String! 187 | 188 | # Length of the starship, along the longest axis 189 | length(unit: LengthUnit = METER): Float 190 | 191 | coordinates: [[Float!]!] 192 | } 193 | 194 | union SearchResult = Human | Droid | Starship 195 | 196 | -------------------------------------------------------------------------------- /tests/swapi2.sdl: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Root 3 | } 4 | 5 | """A single film.""" 6 | type Film implements Node { 7 | """The title of this film.""" 8 | title: String 9 | 10 | """The episode number of this film.""" 11 | episodeID: Int 12 | 13 | """The opening paragraphs at the beginning of this film.""" 14 | openingCrawl: String 15 | 16 | """The name of the director of this film.""" 17 | director: String 18 | 19 | """The name(s) of the producer(s) of this film.""" 20 | producers: [String] 21 | 22 | """The ISO 8601 date format of film release at original creator country.""" 23 | releaseDate: String 24 | speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection 25 | starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection 26 | vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection 27 | characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection 28 | planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection 29 | 30 | """The ISO 8601 date format of the time that this resource was created.""" 31 | created: String 32 | 33 | """The ISO 8601 date format of the time that this resource was edited.""" 34 | edited: String 35 | 36 | """The ID of an object""" 37 | id: ID! 38 | } 39 | 40 | """A connection to a list of items.""" 41 | type FilmCharactersConnection { 42 | """Information to aid in pagination.""" 43 | pageInfo: PageInfo! 44 | 45 | """A list of edges.""" 46 | edges: [FilmCharactersEdge] 47 | 48 | """ 49 | A count of the total number of objects in this connection, ignoring pagination. 50 | This allows a client to fetch the first five objects by passing "5" as the 51 | argument to "first", then fetch the total count so it could display "5 of 83", 52 | for example. 53 | """ 54 | totalCount: Int 55 | 56 | """ 57 | A list of all of the objects returned in the connection. This is a convenience 58 | field provided for quickly exploring the API; rather than querying for 59 | "{ edges { node } }" when no edge data is needed, this field can be be used 60 | instead. Note that when clients like Relay need to fetch the "cursor" field on 61 | the edge to enable efficient pagination, this shortcut cannot be used, and the 62 | full "{ edges { node } }" version should be used instead. 63 | """ 64 | characters: [Person] 65 | } 66 | 67 | """An edge in a connection.""" 68 | type FilmCharactersEdge { 69 | """The item at the end of the edge""" 70 | node: Person 71 | 72 | """A cursor for use in pagination""" 73 | cursor: String! 74 | } 75 | 76 | """A connection to a list of items.""" 77 | type FilmPlanetsConnection { 78 | """Information to aid in pagination.""" 79 | pageInfo: PageInfo! 80 | 81 | """A list of edges.""" 82 | edges: [FilmPlanetsEdge] 83 | 84 | """ 85 | A count of the total number of objects in this connection, ignoring pagination. 86 | This allows a client to fetch the first five objects by passing "5" as the 87 | argument to "first", then fetch the total count so it could display "5 of 83", 88 | for example. 89 | """ 90 | totalCount: Int 91 | 92 | """ 93 | A list of all of the objects returned in the connection. This is a convenience 94 | field provided for quickly exploring the API; rather than querying for 95 | "{ edges { node } }" when no edge data is needed, this field can be be used 96 | instead. Note that when clients like Relay need to fetch the "cursor" field on 97 | the edge to enable efficient pagination, this shortcut cannot be used, and the 98 | full "{ edges { node } }" version should be used instead. 99 | """ 100 | planets: [Planet] 101 | } 102 | 103 | """An edge in a connection.""" 104 | type FilmPlanetsEdge { 105 | """The item at the end of the edge""" 106 | node: Planet 107 | 108 | """A cursor for use in pagination""" 109 | cursor: String! 110 | } 111 | 112 | """A connection to a list of items.""" 113 | type FilmsConnection { 114 | """Information to aid in pagination.""" 115 | pageInfo: PageInfo! 116 | 117 | """A list of edges.""" 118 | edges: [FilmsEdge] 119 | 120 | """ 121 | A count of the total number of objects in this connection, ignoring pagination. 122 | This allows a client to fetch the first five objects by passing "5" as the 123 | argument to "first", then fetch the total count so it could display "5 of 83", 124 | for example. 125 | """ 126 | totalCount: Int 127 | 128 | """ 129 | A list of all of the objects returned in the connection. This is a convenience 130 | field provided for quickly exploring the API; rather than querying for 131 | "{ edges { node } }" when no edge data is needed, this field can be be used 132 | instead. Note that when clients like Relay need to fetch the "cursor" field on 133 | the edge to enable efficient pagination, this shortcut cannot be used, and the 134 | full "{ edges { node } }" version should be used instead. 135 | """ 136 | films: [Film] 137 | } 138 | 139 | """An edge in a connection.""" 140 | type FilmsEdge { 141 | """The item at the end of the edge""" 142 | node: Film 143 | 144 | """A cursor for use in pagination""" 145 | cursor: String! 146 | } 147 | 148 | """A connection to a list of items.""" 149 | type FilmSpeciesConnection { 150 | """Information to aid in pagination.""" 151 | pageInfo: PageInfo! 152 | 153 | """A list of edges.""" 154 | edges: [FilmSpeciesEdge] 155 | 156 | """ 157 | A count of the total number of objects in this connection, ignoring pagination. 158 | This allows a client to fetch the first five objects by passing "5" as the 159 | argument to "first", then fetch the total count so it could display "5 of 83", 160 | for example. 161 | """ 162 | totalCount: Int 163 | 164 | """ 165 | A list of all of the objects returned in the connection. This is a convenience 166 | field provided for quickly exploring the API; rather than querying for 167 | "{ edges { node } }" when no edge data is needed, this field can be be used 168 | instead. Note that when clients like Relay need to fetch the "cursor" field on 169 | the edge to enable efficient pagination, this shortcut cannot be used, and the 170 | full "{ edges { node } }" version should be used instead. 171 | """ 172 | species: [Species] 173 | } 174 | 175 | """An edge in a connection.""" 176 | type FilmSpeciesEdge { 177 | """The item at the end of the edge""" 178 | node: Species 179 | 180 | """A cursor for use in pagination""" 181 | cursor: String! 182 | } 183 | 184 | """A connection to a list of items.""" 185 | type FilmStarshipsConnection { 186 | """Information to aid in pagination.""" 187 | pageInfo: PageInfo! 188 | 189 | """A list of edges.""" 190 | edges: [FilmStarshipsEdge] 191 | 192 | """ 193 | A count of the total number of objects in this connection, ignoring pagination. 194 | This allows a client to fetch the first five objects by passing "5" as the 195 | argument to "first", then fetch the total count so it could display "5 of 83", 196 | for example. 197 | """ 198 | totalCount: Int 199 | 200 | """ 201 | A list of all of the objects returned in the connection. This is a convenience 202 | field provided for quickly exploring the API; rather than querying for 203 | "{ edges { node } }" when no edge data is needed, this field can be be used 204 | instead. Note that when clients like Relay need to fetch the "cursor" field on 205 | the edge to enable efficient pagination, this shortcut cannot be used, and the 206 | full "{ edges { node } }" version should be used instead. 207 | """ 208 | starships: [Starship] 209 | } 210 | 211 | """An edge in a connection.""" 212 | type FilmStarshipsEdge { 213 | """The item at the end of the edge""" 214 | node: Starship 215 | 216 | """A cursor for use in pagination""" 217 | cursor: String! 218 | } 219 | 220 | """A connection to a list of items.""" 221 | type FilmVehiclesConnection { 222 | """Information to aid in pagination.""" 223 | pageInfo: PageInfo! 224 | 225 | """A list of edges.""" 226 | edges: [FilmVehiclesEdge] 227 | 228 | """ 229 | A count of the total number of objects in this connection, ignoring pagination. 230 | This allows a client to fetch the first five objects by passing "5" as the 231 | argument to "first", then fetch the total count so it could display "5 of 83", 232 | for example. 233 | """ 234 | totalCount: Int 235 | 236 | """ 237 | A list of all of the objects returned in the connection. This is a convenience 238 | field provided for quickly exploring the API; rather than querying for 239 | "{ edges { node } }" when no edge data is needed, this field can be be used 240 | instead. Note that when clients like Relay need to fetch the "cursor" field on 241 | the edge to enable efficient pagination, this shortcut cannot be used, and the 242 | full "{ edges { node } }" version should be used instead. 243 | """ 244 | vehicles: [Vehicle] 245 | } 246 | 247 | """An edge in a connection.""" 248 | type FilmVehiclesEdge { 249 | """The item at the end of the edge""" 250 | node: Vehicle 251 | 252 | """A cursor for use in pagination""" 253 | cursor: String! 254 | } 255 | 256 | """An object with an ID""" 257 | interface Node { 258 | """The id of the object.""" 259 | id: ID! 260 | } 261 | 262 | """Information about pagination in a connection.""" 263 | type PageInfo { 264 | """When paginating forwards, are there more items?""" 265 | hasNextPage: Boolean! 266 | 267 | """When paginating backwards, are there more items?""" 268 | hasPreviousPage: Boolean! 269 | 270 | """When paginating backwards, the cursor to continue.""" 271 | startCursor: String 272 | 273 | """When paginating forwards, the cursor to continue.""" 274 | endCursor: String 275 | } 276 | 277 | """A connection to a list of items.""" 278 | type PeopleConnection { 279 | """Information to aid in pagination.""" 280 | pageInfo: PageInfo! 281 | 282 | """A list of edges.""" 283 | edges: [PeopleEdge] 284 | 285 | """ 286 | A count of the total number of objects in this connection, ignoring pagination. 287 | This allows a client to fetch the first five objects by passing "5" as the 288 | argument to "first", then fetch the total count so it could display "5 of 83", 289 | for example. 290 | """ 291 | totalCount: Int 292 | 293 | """ 294 | A list of all of the objects returned in the connection. This is a convenience 295 | field provided for quickly exploring the API; rather than querying for 296 | "{ edges { node } }" when no edge data is needed, this field can be be used 297 | instead. Note that when clients like Relay need to fetch the "cursor" field on 298 | the edge to enable efficient pagination, this shortcut cannot be used, and the 299 | full "{ edges { node } }" version should be used instead. 300 | """ 301 | people: [Person] 302 | } 303 | 304 | """An edge in a connection.""" 305 | type PeopleEdge { 306 | """The item at the end of the edge""" 307 | node: Person 308 | 309 | """A cursor for use in pagination""" 310 | cursor: String! 311 | } 312 | 313 | """An individual person or character within the Star Wars universe.""" 314 | type Person implements Node { 315 | """The name of this person.""" 316 | name: String 317 | 318 | """ 319 | The birth year of the person, using the in-universe standard of BBY or ABY - 320 | Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is 321 | a battle that occurs at the end of Star Wars episode IV: A New Hope. 322 | """ 323 | birthYear: String 324 | 325 | """ 326 | The eye color of this person. Will be "unknown" if not known or "n/a" if the 327 | person does not have an eye. 328 | """ 329 | eyeColor: String 330 | 331 | """ 332 | The gender of this person. Either "Male", "Female" or "unknown", 333 | "n/a" if the person does not have a gender. 334 | """ 335 | gender: String 336 | 337 | """ 338 | The hair color of this person. Will be "unknown" if not known or "n/a" if the 339 | person does not have hair. 340 | """ 341 | hairColor: String 342 | 343 | """The height of the person in centimeters.""" 344 | height: Int 345 | 346 | """The mass of the person in kilograms.""" 347 | mass: Float 348 | 349 | """The skin color of this person.""" 350 | skinColor: String 351 | 352 | """A planet that this person was born on or inhabits.""" 353 | homeworld: Planet 354 | filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection 355 | 356 | """The species that this person belongs to, or null if unknown.""" 357 | species: Species 358 | starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection 359 | vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection 360 | 361 | """The ISO 8601 date format of the time that this resource was created.""" 362 | created: String 363 | 364 | """The ISO 8601 date format of the time that this resource was edited.""" 365 | edited: String 366 | 367 | """The ID of an object""" 368 | id: ID! 369 | } 370 | 371 | """A connection to a list of items.""" 372 | type PersonFilmsConnection { 373 | """Information to aid in pagination.""" 374 | pageInfo: PageInfo! 375 | 376 | """A list of edges.""" 377 | edges: [PersonFilmsEdge] 378 | 379 | """ 380 | A count of the total number of objects in this connection, ignoring pagination. 381 | This allows a client to fetch the first five objects by passing "5" as the 382 | argument to "first", then fetch the total count so it could display "5 of 83", 383 | for example. 384 | """ 385 | totalCount: Int 386 | 387 | """ 388 | A list of all of the objects returned in the connection. This is a convenience 389 | field provided for quickly exploring the API; rather than querying for 390 | "{ edges { node } }" when no edge data is needed, this field can be be used 391 | instead. Note that when clients like Relay need to fetch the "cursor" field on 392 | the edge to enable efficient pagination, this shortcut cannot be used, and the 393 | full "{ edges { node } }" version should be used instead. 394 | """ 395 | films: [Film] 396 | } 397 | 398 | """An edge in a connection.""" 399 | type PersonFilmsEdge { 400 | """The item at the end of the edge""" 401 | node: Film 402 | 403 | """A cursor for use in pagination""" 404 | cursor: String! 405 | } 406 | 407 | """A connection to a list of items.""" 408 | type PersonStarshipsConnection { 409 | """Information to aid in pagination.""" 410 | pageInfo: PageInfo! 411 | 412 | """A list of edges.""" 413 | edges: [PersonStarshipsEdge] 414 | 415 | """ 416 | A count of the total number of objects in this connection, ignoring pagination. 417 | This allows a client to fetch the first five objects by passing "5" as the 418 | argument to "first", then fetch the total count so it could display "5 of 83", 419 | for example. 420 | """ 421 | totalCount: Int 422 | 423 | """ 424 | A list of all of the objects returned in the connection. This is a convenience 425 | field provided for quickly exploring the API; rather than querying for 426 | "{ edges { node } }" when no edge data is needed, this field can be be used 427 | instead. Note that when clients like Relay need to fetch the "cursor" field on 428 | the edge to enable efficient pagination, this shortcut cannot be used, and the 429 | full "{ edges { node } }" version should be used instead. 430 | """ 431 | starships: [Starship] 432 | } 433 | 434 | """An edge in a connection.""" 435 | type PersonStarshipsEdge { 436 | """The item at the end of the edge""" 437 | node: Starship 438 | 439 | """A cursor for use in pagination""" 440 | cursor: String! 441 | } 442 | 443 | """A connection to a list of items.""" 444 | type PersonVehiclesConnection { 445 | """Information to aid in pagination.""" 446 | pageInfo: PageInfo! 447 | 448 | """A list of edges.""" 449 | edges: [PersonVehiclesEdge] 450 | 451 | """ 452 | A count of the total number of objects in this connection, ignoring pagination. 453 | This allows a client to fetch the first five objects by passing "5" as the 454 | argument to "first", then fetch the total count so it could display "5 of 83", 455 | for example. 456 | """ 457 | totalCount: Int 458 | 459 | """ 460 | A list of all of the objects returned in the connection. This is a convenience 461 | field provided for quickly exploring the API; rather than querying for 462 | "{ edges { node } }" when no edge data is needed, this field can be be used 463 | instead. Note that when clients like Relay need to fetch the "cursor" field on 464 | the edge to enable efficient pagination, this shortcut cannot be used, and the 465 | full "{ edges { node } }" version should be used instead. 466 | """ 467 | vehicles: [Vehicle] 468 | } 469 | 470 | """An edge in a connection.""" 471 | type PersonVehiclesEdge { 472 | """The item at the end of the edge""" 473 | node: Vehicle 474 | 475 | """A cursor for use in pagination""" 476 | cursor: String! 477 | } 478 | 479 | """ 480 | A large mass, planet or planetoid in the Star Wars Universe, at the time of 481 | 0 ABY. 482 | """ 483 | type Planet implements Node { 484 | """The name of this planet.""" 485 | name: String 486 | 487 | """The diameter of this planet in kilometers.""" 488 | diameter: Int 489 | 490 | """ 491 | The number of standard hours it takes for this planet to complete a single 492 | rotation on its axis. 493 | """ 494 | rotationPeriod: Int 495 | 496 | """ 497 | The number of standard days it takes for this planet to complete a single orbit 498 | of its local star. 499 | """ 500 | orbitalPeriod: Int 501 | 502 | """ 503 | A number denoting the gravity of this planet, where "1" is normal or 1 standard 504 | G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. 505 | """ 506 | gravity: String 507 | 508 | """The average population of sentient beings inhabiting this planet.""" 509 | population: Float 510 | 511 | """The climates of this planet.""" 512 | climates: [String] 513 | 514 | """The terrains of this planet.""" 515 | terrains: [String] 516 | 517 | """ 518 | The percentage of the planet surface that is naturally occuring water or bodies 519 | of water. 520 | """ 521 | surfaceWater: Float 522 | residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection 523 | filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection 524 | 525 | """The ISO 8601 date format of the time that this resource was created.""" 526 | created: String 527 | 528 | """The ISO 8601 date format of the time that this resource was edited.""" 529 | edited: String 530 | 531 | """The ID of an object""" 532 | id: ID! 533 | } 534 | 535 | """A connection to a list of items.""" 536 | type PlanetFilmsConnection { 537 | """Information to aid in pagination.""" 538 | pageInfo: PageInfo! 539 | 540 | """A list of edges.""" 541 | edges: [PlanetFilmsEdge] 542 | 543 | """ 544 | A count of the total number of objects in this connection, ignoring pagination. 545 | This allows a client to fetch the first five objects by passing "5" as the 546 | argument to "first", then fetch the total count so it could display "5 of 83", 547 | for example. 548 | """ 549 | totalCount: Int 550 | 551 | """ 552 | A list of all of the objects returned in the connection. This is a convenience 553 | field provided for quickly exploring the API; rather than querying for 554 | "{ edges { node } }" when no edge data is needed, this field can be be used 555 | instead. Note that when clients like Relay need to fetch the "cursor" field on 556 | the edge to enable efficient pagination, this shortcut cannot be used, and the 557 | full "{ edges { node } }" version should be used instead. 558 | """ 559 | films: [Film] 560 | } 561 | 562 | """An edge in a connection.""" 563 | type PlanetFilmsEdge { 564 | """The item at the end of the edge""" 565 | node: Film 566 | 567 | """A cursor for use in pagination""" 568 | cursor: String! 569 | } 570 | 571 | """A connection to a list of items.""" 572 | type PlanetResidentsConnection { 573 | """Information to aid in pagination.""" 574 | pageInfo: PageInfo! 575 | 576 | """A list of edges.""" 577 | edges: [PlanetResidentsEdge] 578 | 579 | """ 580 | A count of the total number of objects in this connection, ignoring pagination. 581 | This allows a client to fetch the first five objects by passing "5" as the 582 | argument to "first", then fetch the total count so it could display "5 of 83", 583 | for example. 584 | """ 585 | totalCount: Int 586 | 587 | """ 588 | A list of all of the objects returned in the connection. This is a convenience 589 | field provided for quickly exploring the API; rather than querying for 590 | "{ edges { node } }" when no edge data is needed, this field can be be used 591 | instead. Note that when clients like Relay need to fetch the "cursor" field on 592 | the edge to enable efficient pagination, this shortcut cannot be used, and the 593 | full "{ edges { node } }" version should be used instead. 594 | """ 595 | residents: [Person] 596 | } 597 | 598 | """An edge in a connection.""" 599 | type PlanetResidentsEdge { 600 | """The item at the end of the edge""" 601 | node: Person 602 | 603 | """A cursor for use in pagination""" 604 | cursor: String! 605 | } 606 | 607 | """A connection to a list of items.""" 608 | type PlanetsConnection { 609 | """Information to aid in pagination.""" 610 | pageInfo: PageInfo! 611 | 612 | """A list of edges.""" 613 | edges: [PlanetsEdge] 614 | 615 | """ 616 | A count of the total number of objects in this connection, ignoring pagination. 617 | This allows a client to fetch the first five objects by passing "5" as the 618 | argument to "first", then fetch the total count so it could display "5 of 83", 619 | for example. 620 | """ 621 | totalCount: Int 622 | 623 | """ 624 | A list of all of the objects returned in the connection. This is a convenience 625 | field provided for quickly exploring the API; rather than querying for 626 | "{ edges { node } }" when no edge data is needed, this field can be be used 627 | instead. Note that when clients like Relay need to fetch the "cursor" field on 628 | the edge to enable efficient pagination, this shortcut cannot be used, and the 629 | full "{ edges { node } }" version should be used instead. 630 | """ 631 | planets: [Planet] 632 | } 633 | 634 | """An edge in a connection.""" 635 | type PlanetsEdge { 636 | """The item at the end of the edge""" 637 | node: Planet 638 | 639 | """A cursor for use in pagination""" 640 | cursor: String! 641 | } 642 | 643 | type Root { 644 | allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection 645 | film(id: ID, filmID: ID): Film 646 | allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection 647 | person(id: ID, personID: ID): Person 648 | allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection 649 | planet(id: ID, planetID: ID): Planet 650 | allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection 651 | species(id: ID, speciesID: ID): Species 652 | allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection 653 | starship(id: ID, starshipID: ID): Starship 654 | allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection 655 | vehicle(id: ID, vehicleID: ID): Vehicle 656 | 657 | """Fetches an object given its ID""" 658 | node( 659 | """The ID of an object""" 660 | id: ID! 661 | ): Node 662 | } 663 | 664 | """A type of person or character within the Star Wars Universe.""" 665 | type Species implements Node { 666 | """The name of this species.""" 667 | name: String 668 | 669 | """The classification of this species, such as "mammal" or "reptile".""" 670 | classification: String 671 | 672 | """The designation of this species, such as "sentient".""" 673 | designation: String 674 | 675 | """The average height of this species in centimeters.""" 676 | averageHeight: Float 677 | 678 | """The average lifespan of this species in years, null if unknown.""" 679 | averageLifespan: Int 680 | 681 | """ 682 | Common eye colors for this species, null if this species does not typically 683 | have eyes. 684 | """ 685 | eyeColors: [String] 686 | 687 | """ 688 | Common hair colors for this species, null if this species does not typically 689 | have hair. 690 | """ 691 | hairColors: [String] 692 | 693 | """ 694 | Common skin colors for this species, null if this species does not typically 695 | have skin. 696 | """ 697 | skinColors: [String] 698 | 699 | """The language commonly spoken by this species.""" 700 | language: String 701 | 702 | """A planet that this species originates from.""" 703 | homeworld: Planet 704 | personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection 705 | filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection 706 | 707 | """The ISO 8601 date format of the time that this resource was created.""" 708 | created: String 709 | 710 | """The ISO 8601 date format of the time that this resource was edited.""" 711 | edited: String 712 | 713 | """The ID of an object""" 714 | id: ID! 715 | } 716 | 717 | """A connection to a list of items.""" 718 | type SpeciesConnection { 719 | """Information to aid in pagination.""" 720 | pageInfo: PageInfo! 721 | 722 | """A list of edges.""" 723 | edges: [SpeciesEdge] 724 | 725 | """ 726 | A count of the total number of objects in this connection, ignoring pagination. 727 | This allows a client to fetch the first five objects by passing "5" as the 728 | argument to "first", then fetch the total count so it could display "5 of 83", 729 | for example. 730 | """ 731 | totalCount: Int 732 | 733 | """ 734 | A list of all of the objects returned in the connection. This is a convenience 735 | field provided for quickly exploring the API; rather than querying for 736 | "{ edges { node } }" when no edge data is needed, this field can be be used 737 | instead. Note that when clients like Relay need to fetch the "cursor" field on 738 | the edge to enable efficient pagination, this shortcut cannot be used, and the 739 | full "{ edges { node } }" version should be used instead. 740 | """ 741 | species: [Species] 742 | } 743 | 744 | """An edge in a connection.""" 745 | type SpeciesEdge { 746 | """The item at the end of the edge""" 747 | node: Species 748 | 749 | """A cursor for use in pagination""" 750 | cursor: String! 751 | } 752 | 753 | """A connection to a list of items.""" 754 | type SpeciesFilmsConnection { 755 | """Information to aid in pagination.""" 756 | pageInfo: PageInfo! 757 | 758 | """A list of edges.""" 759 | edges: [SpeciesFilmsEdge] 760 | 761 | """ 762 | A count of the total number of objects in this connection, ignoring pagination. 763 | This allows a client to fetch the first five objects by passing "5" as the 764 | argument to "first", then fetch the total count so it could display "5 of 83", 765 | for example. 766 | """ 767 | totalCount: Int 768 | 769 | """ 770 | A list of all of the objects returned in the connection. This is a convenience 771 | field provided for quickly exploring the API; rather than querying for 772 | "{ edges { node } }" when no edge data is needed, this field can be be used 773 | instead. Note that when clients like Relay need to fetch the "cursor" field on 774 | the edge to enable efficient pagination, this shortcut cannot be used, and the 775 | full "{ edges { node } }" version should be used instead. 776 | """ 777 | films: [Film] 778 | } 779 | 780 | """An edge in a connection.""" 781 | type SpeciesFilmsEdge { 782 | """The item at the end of the edge""" 783 | node: Film 784 | 785 | """A cursor for use in pagination""" 786 | cursor: String! 787 | } 788 | 789 | """A connection to a list of items.""" 790 | type SpeciesPeopleConnection { 791 | """Information to aid in pagination.""" 792 | pageInfo: PageInfo! 793 | 794 | """A list of edges.""" 795 | edges: [SpeciesPeopleEdge] 796 | 797 | """ 798 | A count of the total number of objects in this connection, ignoring pagination. 799 | This allows a client to fetch the first five objects by passing "5" as the 800 | argument to "first", then fetch the total count so it could display "5 of 83", 801 | for example. 802 | """ 803 | totalCount: Int 804 | 805 | """ 806 | A list of all of the objects returned in the connection. This is a convenience 807 | field provided for quickly exploring the API; rather than querying for 808 | "{ edges { node } }" when no edge data is needed, this field can be be used 809 | instead. Note that when clients like Relay need to fetch the "cursor" field on 810 | the edge to enable efficient pagination, this shortcut cannot be used, and the 811 | full "{ edges { node } }" version should be used instead. 812 | """ 813 | people: [Person] 814 | } 815 | 816 | """An edge in a connection.""" 817 | type SpeciesPeopleEdge { 818 | """The item at the end of the edge""" 819 | node: Person 820 | 821 | """A cursor for use in pagination""" 822 | cursor: String! 823 | } 824 | 825 | """A single transport craft that has hyperdrive capability.""" 826 | type Starship implements Node { 827 | """The name of this starship. The common name, such as "Death Star".""" 828 | name: String 829 | 830 | """ 831 | The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 832 | Orbital Battle Station". 833 | """ 834 | model: String 835 | 836 | """ 837 | The class of this starship, such as "Starfighter" or "Deep Space Mobile 838 | Battlestation" 839 | """ 840 | starshipClass: String 841 | 842 | """The manufacturers of this starship.""" 843 | manufacturers: [String] 844 | 845 | """The cost of this starship new, in galactic credits.""" 846 | costInCredits: Float 847 | 848 | """The length of this starship in meters.""" 849 | length: Float 850 | 851 | """The number of personnel needed to run or pilot this starship.""" 852 | crew: String 853 | 854 | """The number of non-essential people this starship can transport.""" 855 | passengers: String 856 | 857 | """ 858 | The maximum speed of this starship in atmosphere. null if this starship is 859 | incapable of atmosphering flight. 860 | """ 861 | maxAtmospheringSpeed: Int 862 | 863 | """The class of this starships hyperdrive.""" 864 | hyperdriveRating: Float 865 | 866 | """ 867 | The Maximum number of Megalights this starship can travel in a standard hour. 868 | A "Megalight" is a standard unit of distance and has never been defined before 869 | within the Star Wars universe. This figure is only really useful for measuring 870 | the difference in speed of starships. We can assume it is similar to AU, the 871 | distance between our Sun (Sol) and Earth. 872 | """ 873 | MGLT: Int 874 | 875 | """The maximum number of kilograms that this starship can transport.""" 876 | cargoCapacity: Float 877 | 878 | """ 879 | The maximum length of time that this starship can provide consumables for its 880 | entire crew without having to resupply. 881 | """ 882 | consumables: String 883 | pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection 884 | filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection 885 | 886 | """The ISO 8601 date format of the time that this resource was created.""" 887 | created: String 888 | 889 | """The ISO 8601 date format of the time that this resource was edited.""" 890 | edited: String 891 | 892 | """The ID of an object""" 893 | id: ID! 894 | } 895 | 896 | """A connection to a list of items.""" 897 | type StarshipFilmsConnection { 898 | """Information to aid in pagination.""" 899 | pageInfo: PageInfo! 900 | 901 | """A list of edges.""" 902 | edges: [StarshipFilmsEdge] 903 | 904 | """ 905 | A count of the total number of objects in this connection, ignoring pagination. 906 | This allows a client to fetch the first five objects by passing "5" as the 907 | argument to "first", then fetch the total count so it could display "5 of 83", 908 | for example. 909 | """ 910 | totalCount: Int 911 | 912 | """ 913 | A list of all of the objects returned in the connection. This is a convenience 914 | field provided for quickly exploring the API; rather than querying for 915 | "{ edges { node } }" when no edge data is needed, this field can be be used 916 | instead. Note that when clients like Relay need to fetch the "cursor" field on 917 | the edge to enable efficient pagination, this shortcut cannot be used, and the 918 | full "{ edges { node } }" version should be used instead. 919 | """ 920 | films: [Film] 921 | } 922 | 923 | """An edge in a connection.""" 924 | type StarshipFilmsEdge { 925 | """The item at the end of the edge""" 926 | node: Film 927 | 928 | """A cursor for use in pagination""" 929 | cursor: String! 930 | } 931 | 932 | """A connection to a list of items.""" 933 | type StarshipPilotsConnection { 934 | """Information to aid in pagination.""" 935 | pageInfo: PageInfo! 936 | 937 | """A list of edges.""" 938 | edges: [StarshipPilotsEdge] 939 | 940 | """ 941 | A count of the total number of objects in this connection, ignoring pagination. 942 | This allows a client to fetch the first five objects by passing "5" as the 943 | argument to "first", then fetch the total count so it could display "5 of 83", 944 | for example. 945 | """ 946 | totalCount: Int 947 | 948 | """ 949 | A list of all of the objects returned in the connection. This is a convenience 950 | field provided for quickly exploring the API; rather than querying for 951 | "{ edges { node } }" when no edge data is needed, this field can be be used 952 | instead. Note that when clients like Relay need to fetch the "cursor" field on 953 | the edge to enable efficient pagination, this shortcut cannot be used, and the 954 | full "{ edges { node } }" version should be used instead. 955 | """ 956 | pilots: [Person] 957 | } 958 | 959 | """An edge in a connection.""" 960 | type StarshipPilotsEdge { 961 | """The item at the end of the edge""" 962 | node: Person 963 | 964 | """A cursor for use in pagination""" 965 | cursor: String! 966 | } 967 | 968 | """A connection to a list of items.""" 969 | type StarshipsConnection { 970 | """Information to aid in pagination.""" 971 | pageInfo: PageInfo! 972 | 973 | """A list of edges.""" 974 | edges: [StarshipsEdge] 975 | 976 | """ 977 | A count of the total number of objects in this connection, ignoring pagination. 978 | This allows a client to fetch the first five objects by passing "5" as the 979 | argument to "first", then fetch the total count so it could display "5 of 83", 980 | for example. 981 | """ 982 | totalCount: Int 983 | 984 | """ 985 | A list of all of the objects returned in the connection. This is a convenience 986 | field provided for quickly exploring the API; rather than querying for 987 | "{ edges { node } }" when no edge data is needed, this field can be be used 988 | instead. Note that when clients like Relay need to fetch the "cursor" field on 989 | the edge to enable efficient pagination, this shortcut cannot be used, and the 990 | full "{ edges { node } }" version should be used instead. 991 | """ 992 | starships: [Starship] 993 | } 994 | 995 | """An edge in a connection.""" 996 | type StarshipsEdge { 997 | """The item at the end of the edge""" 998 | node: Starship 999 | 1000 | """A cursor for use in pagination""" 1001 | cursor: String! 1002 | } 1003 | 1004 | """A single transport craft that does not have hyperdrive capability""" 1005 | type Vehicle implements Node { 1006 | """ 1007 | The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder 1008 | bike". 1009 | """ 1010 | name: String 1011 | 1012 | """ 1013 | The model or official name of this vehicle. Such as "All-Terrain Attack 1014 | Transport". 1015 | """ 1016 | model: String 1017 | 1018 | """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" 1019 | vehicleClass: String 1020 | 1021 | """The manufacturers of this vehicle.""" 1022 | manufacturers: [String] 1023 | 1024 | """The cost of this vehicle new, in Galactic Credits.""" 1025 | costInCredits: Float 1026 | 1027 | """The length of this vehicle in meters.""" 1028 | length: Float 1029 | 1030 | """The number of personnel needed to run or pilot this vehicle.""" 1031 | crew: String 1032 | 1033 | """The number of non-essential people this vehicle can transport.""" 1034 | passengers: String 1035 | 1036 | """The maximum speed of this vehicle in atmosphere.""" 1037 | maxAtmospheringSpeed: Int 1038 | 1039 | """The maximum number of kilograms that this vehicle can transport.""" 1040 | cargoCapacity: Float 1041 | 1042 | """ 1043 | The maximum length of time that this vehicle can provide consumables for its 1044 | entire crew without having to resupply. 1045 | """ 1046 | consumables: String 1047 | pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection 1048 | filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection 1049 | 1050 | """The ISO 8601 date format of the time that this resource was created.""" 1051 | created: String 1052 | 1053 | """The ISO 8601 date format of the time that this resource was edited.""" 1054 | edited: String 1055 | 1056 | """The ID of an object""" 1057 | id: ID! 1058 | } 1059 | 1060 | """A connection to a list of items.""" 1061 | type VehicleFilmsConnection { 1062 | """Information to aid in pagination.""" 1063 | pageInfo: PageInfo! 1064 | 1065 | """A list of edges.""" 1066 | edges: [VehicleFilmsEdge] 1067 | 1068 | """ 1069 | A count of the total number of objects in this connection, ignoring pagination. 1070 | This allows a client to fetch the first five objects by passing "5" as the 1071 | argument to "first", then fetch the total count so it could display "5 of 83", 1072 | for example. 1073 | """ 1074 | totalCount: Int 1075 | 1076 | """ 1077 | A list of all of the objects returned in the connection. This is a convenience 1078 | field provided for quickly exploring the API; rather than querying for 1079 | "{ edges { node } }" when no edge data is needed, this field can be be used 1080 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1081 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1082 | full "{ edges { node } }" version should be used instead. 1083 | """ 1084 | films: [Film] 1085 | } 1086 | 1087 | """An edge in a connection.""" 1088 | type VehicleFilmsEdge { 1089 | """The item at the end of the edge""" 1090 | node: Film 1091 | 1092 | """A cursor for use in pagination""" 1093 | cursor: String! 1094 | } 1095 | 1096 | """A connection to a list of items.""" 1097 | type VehiclePilotsConnection { 1098 | """Information to aid in pagination.""" 1099 | pageInfo: PageInfo! 1100 | 1101 | """A list of edges.""" 1102 | edges: [VehiclePilotsEdge] 1103 | 1104 | """ 1105 | A count of the total number of objects in this connection, ignoring pagination. 1106 | This allows a client to fetch the first five objects by passing "5" as the 1107 | argument to "first", then fetch the total count so it could display "5 of 83", 1108 | for example. 1109 | """ 1110 | totalCount: Int 1111 | 1112 | """ 1113 | A list of all of the objects returned in the connection. This is a convenience 1114 | field provided for quickly exploring the API; rather than querying for 1115 | "{ edges { node } }" when no edge data is needed, this field can be be used 1116 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1117 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1118 | full "{ edges { node } }" version should be used instead. 1119 | """ 1120 | pilots: [Person] 1121 | } 1122 | 1123 | """An edge in a connection.""" 1124 | type VehiclePilotsEdge { 1125 | """The item at the end of the edge""" 1126 | node: Person 1127 | 1128 | """A cursor for use in pagination""" 1129 | cursor: String! 1130 | } 1131 | 1132 | """A connection to a list of items.""" 1133 | type VehiclesConnection { 1134 | """Information to aid in pagination.""" 1135 | pageInfo: PageInfo! 1136 | 1137 | """A list of edges.""" 1138 | edges: [VehiclesEdge] 1139 | 1140 | """ 1141 | A count of the total number of objects in this connection, ignoring pagination. 1142 | This allows a client to fetch the first five objects by passing "5" as the 1143 | argument to "first", then fetch the total count so it could display "5 of 83", 1144 | for example. 1145 | """ 1146 | totalCount: Int 1147 | 1148 | """ 1149 | A list of all of the objects returned in the connection. This is a convenience 1150 | field provided for quickly exploring the API; rather than querying for 1151 | "{ edges { node } }" when no edge data is needed, this field can be be used 1152 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1153 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1154 | full "{ edges { node } }" version should be used instead. 1155 | """ 1156 | vehicles: [Vehicle] 1157 | } 1158 | 1159 | """An edge in a connection.""" 1160 | type VehiclesEdge { 1161 | """The item at the end of the edge""" 1162 | node: Vehicle 1163 | 1164 | """A cursor for use in pagination""" 1165 | cursor: String! 1166 | } 1167 | 1168 | -------------------------------------------------------------------------------- /tests/test_decomp_swapi.py: -------------------------------------------------------------------------------- 1 | from lean_schema import decomp 2 | from unittest import mock 3 | import json 4 | import os 5 | 6 | if os.path.exists("tests/swapi_schema.json"): 7 | SWAPI_SCHEMA_PATH = "tests/swapi_schema.json" 8 | elif os.path.exists("swapi_schema.json"): 9 | SWAPI_SCHEMA_PATH = "swapi_schema.json" 10 | else: 11 | raise FileNotFoundError( 12 | "SWAPI Schema File not found in local directory or ./tests!" 13 | ) 14 | 15 | 16 | def test_get_types_from_file(): 17 | G = decomp.mk_graph_from_schema(decomp.load_schema(SWAPI_SCHEMA_PATH)) 18 | types_file = {"types": ["Human", "Droid", "Starship"], "domains": []} 19 | 20 | R = decomp.get_types_from_file(G, types_file) 21 | assert len(R) == 3 22 | assert "Human" in R 23 | assert "Droid" in R 24 | assert "Starship" in R 25 | 26 | 27 | @mock.patch("sys.stdin") 28 | @mock.patch("builtins.print") 29 | def test_main(print_mock, stdin_mock): 30 | stdin_mock.fileno = lambda: 2 31 | stdin_mock.read = lambda: json.dumps({"types": ["Human"]}) 32 | args = [SWAPI_SCHEMA_PATH, "--log-level=DEBUG"] 33 | subschema = decomp.main(args) 34 | subgraph = decomp.mk_graph_from_schema(subschema) 35 | assert print_mock.call_count == 1 36 | assert "Human" in subgraph 37 | -------------------------------------------------------------------------------- /tests/test_get_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from lean_schema.get_types import main 4 | from unittest import mock 5 | import lean_schema 6 | -------------------------------------------------------------------------------- /tests/test_post_process.py: -------------------------------------------------------------------------------- 1 | from lean_schema import post_process 2 | import unittest 3 | import os 4 | import shutil 5 | 6 | TEST_SRC_DIR = './test-src-dir' 7 | TEST_DST_DIR = './test-dst-dir' 8 | TEST_UNMATCHED_DIR = './test-unmatched-dir' 9 | 10 | class TestPostProcess(unittest.TestCase): 11 | 12 | def tearDown(self): 13 | shutil.rmtree(TEST_SRC_DIR, ignore_errors=True) 14 | shutil.rmtree(TEST_DST_DIR, ignore_errors=True) 15 | shutil.rmtree(TEST_UNMATCHED_DIR, ignore_errors=True) 16 | 17 | def test_main_fails_without_src_dir(self): 18 | with self.assertRaises(SystemExit) as se: 19 | post_process.main([]) 20 | assert se.exception.code == 2 21 | 22 | def test_main_fails_without_dst_dir(self): 23 | with self.assertRaises(SystemExit) as se: 24 | post_process.main(["./test-src-dir"]) 25 | assert se.exception.code == 2 26 | 27 | def test_main_does_nothing_if_copy_is_false(self): 28 | with self.assertRaises(SystemExit) as se: 29 | post_process.main(["./test-src-dir", "./test-dst-dir", "--copy-codegen-files=false"]) 30 | assert se.exception.code == 0 31 | 32 | def test_main_exits_if_src_dir_does_not_exist(self): 33 | os.mkdir(TEST_DST_DIR) 34 | 35 | with self.assertRaises(SystemExit) as se: 36 | post_process.main([ 37 | "./test-src-dir-does-not-exist", 38 | TEST_DST_DIR 39 | ]) 40 | assert se.exception.code == 1 41 | 42 | shutil.rmtree(TEST_DST_DIR) 43 | 44 | def test_main_exits_if_dst_dir_does_not_exist(self): 45 | os.mkdir(TEST_SRC_DIR) 46 | 47 | with self.assertRaises(SystemExit) as se: 48 | post_process.main([ 49 | TEST_SRC_DIR, 50 | "./test-dst-dir-does-not-exist" 51 | ]) 52 | assert se.exception.code == 1 53 | 54 | shutil.rmtree(TEST_SRC_DIR) 55 | 56 | def test_main_exits_with_invalid_copy_unmatched_files_flag(self): 57 | os.mkdir(TEST_SRC_DIR) 58 | os.mkdir(TEST_DST_DIR) 59 | 60 | with self.assertRaises(SystemExit) as se: 61 | post_process.main([ 62 | "--copy-unmatched-files-dir", "./this-dir-does-not-exist", 63 | TEST_SRC_DIR, 64 | TEST_DST_DIR 65 | ]) 66 | assert se.exception.code == 1 67 | 68 | shutil.rmtree(TEST_SRC_DIR) 69 | shutil.rmtree(TEST_DST_DIR) 70 | 71 | def test_main_copies_unmatched_files_with_flag(self): 72 | os.mkdir(TEST_SRC_DIR) 73 | os.mkdir(TEST_DST_DIR) 74 | os.mkdir(TEST_UNMATCHED_DIR) 75 | 76 | with open('{}/1.test'.format(TEST_SRC_DIR), 'w') as f: 77 | f.write("src") 78 | 79 | post_process.main([ 80 | "--copy-unmatched-files-dir", TEST_UNMATCHED_DIR, 81 | "--src-files-extension", "test", 82 | TEST_SRC_DIR, 83 | TEST_DST_DIR 84 | ]) 85 | 86 | assert os.path.isfile('{}/1.test'.format(TEST_UNMATCHED_DIR)) 87 | assert os.path.isfile('{}/1.test'.format(TEST_DST_DIR)) == False 88 | 89 | shutil.rmtree(TEST_SRC_DIR) 90 | shutil.rmtree(TEST_DST_DIR) 91 | shutil.rmtree(TEST_UNMATCHED_DIR) 92 | 93 | def test_main_doesnt_copy_unmatched_files_without_flag(self): 94 | os.mkdir(TEST_SRC_DIR) 95 | os.mkdir(TEST_DST_DIR) 96 | 97 | with open('{}/1.test'.format(TEST_SRC_DIR), 'w') as f: 98 | f.write("src") 99 | 100 | post_process.main([ 101 | "--src-files-extension", "test", 102 | TEST_SRC_DIR, 103 | TEST_DST_DIR 104 | ]) 105 | 106 | assert os.path.isfile('{}/1.test'.format(TEST_DST_DIR)) == False 107 | 108 | shutil.rmtree(TEST_SRC_DIR) 109 | shutil.rmtree(TEST_DST_DIR) 110 | 111 | @unittest.mock.patch('builtins.print') 112 | def test_main_with_debug_outputs_more_information(self, builtins_print): 113 | os.mkdir(TEST_SRC_DIR) 114 | os.mkdir(TEST_DST_DIR) 115 | 116 | with open('{}/1.test'.format(TEST_SRC_DIR), 'w') as f: 117 | f.write("src") 118 | 119 | with open('{}/1.test'.format(TEST_DST_DIR), 'w') as f: 120 | f.write("dst") 121 | 122 | initial_call_count = builtins_print.call_count 123 | 124 | # without debug 125 | post_process.main([ 126 | "--src-files-extension", "test", 127 | TEST_SRC_DIR, 128 | TEST_DST_DIR 129 | ]) 130 | 131 | no_debug_call_count = builtins_print.call_count - initial_call_count 132 | 133 | # with debug 134 | post_process.main([ 135 | "--src-files-extension", "test", 136 | "--debug", 137 | TEST_SRC_DIR, 138 | TEST_DST_DIR 139 | ]) 140 | 141 | debug_call_count = builtins_print.call_count - no_debug_call_count 142 | 143 | assert debug_call_count > no_debug_call_count 144 | 145 | shutil.rmtree(TEST_SRC_DIR) 146 | shutil.rmtree(TEST_DST_DIR) 147 | 148 | # happy path 149 | def test_main_copies_files_correctly(self): 150 | os.mkdir(TEST_SRC_DIR) 151 | os.mkdir(TEST_DST_DIR) 152 | 153 | with open('{}/1.test'.format(TEST_SRC_DIR), 'w') as f: 154 | f.write("src") 155 | 156 | with open('{}/1.test'.format(TEST_DST_DIR), 'w') as f: 157 | f.write("dst") 158 | 159 | post_process.main([ 160 | "--src-files-extension", "test", 161 | TEST_SRC_DIR, 162 | TEST_DST_DIR 163 | ]) 164 | 165 | with open('{}/1.test'.format(TEST_DST_DIR), 'r') as f: 166 | contents = f.read() 167 | assert "src" in contents 168 | assert "dst" not in contents 169 | 170 | shutil.rmtree(TEST_SRC_DIR) 171 | shutil.rmtree(TEST_DST_DIR) 172 | 173 | if __name__ == '__main__': 174 | unittest.main() 175 | -------------------------------------------------------------------------------- /tests/tweet_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "queryType": { 4 | "name": "Query" 5 | }, 6 | "mutationType": { 7 | "name": "Mutation" 8 | }, 9 | "subscriptionType": null, 10 | "types": [ 11 | { 12 | "kind": "OBJECT", 13 | "name": "Query", 14 | "description": null, 15 | "fields": [ 16 | { 17 | "name": "Tweet", 18 | "description": null, 19 | "args": [ 20 | { 21 | "name": "id", 22 | "description": null, 23 | "type": { 24 | "kind": "NON_NULL", 25 | "name": null, 26 | "ofType": { 27 | "kind": "SCALAR", 28 | "name": "ID", 29 | "ofType": null 30 | } 31 | }, 32 | "defaultValue": null 33 | } 34 | ], 35 | "type": { 36 | "kind": "OBJECT", 37 | "name": "Tweet", 38 | "ofType": null 39 | }, 40 | "isDeprecated": false, 41 | "deprecationReason": null 42 | }, 43 | { 44 | "name": "Tweets", 45 | "description": null, 46 | "args": [ 47 | { 48 | "name": "limit", 49 | "description": null, 50 | "type": { 51 | "kind": "SCALAR", 52 | "name": "Int", 53 | "ofType": null 54 | }, 55 | "defaultValue": null 56 | }, 57 | { 58 | "name": "skip", 59 | "description": null, 60 | "type": { 61 | "kind": "SCALAR", 62 | "name": "Int", 63 | "ofType": null 64 | }, 65 | "defaultValue": null 66 | }, 67 | { 68 | "name": "sort_field", 69 | "description": null, 70 | "type": { 71 | "kind": "SCALAR", 72 | "name": "String", 73 | "ofType": null 74 | }, 75 | "defaultValue": null 76 | }, 77 | { 78 | "name": "sort_order", 79 | "description": null, 80 | "type": { 81 | "kind": "SCALAR", 82 | "name": "String", 83 | "ofType": null 84 | }, 85 | "defaultValue": null 86 | } 87 | ], 88 | "type": { 89 | "kind": "LIST", 90 | "name": null, 91 | "ofType": { 92 | "kind": "OBJECT", 93 | "name": "Tweet", 94 | "ofType": null 95 | } 96 | }, 97 | "isDeprecated": false, 98 | "deprecationReason": null 99 | }, 100 | { 101 | "name": "TweetsMeta", 102 | "description": null, 103 | "args": [], 104 | "type": { 105 | "kind": "OBJECT", 106 | "name": "Meta", 107 | "ofType": null 108 | }, 109 | "isDeprecated": false, 110 | "deprecationReason": null 111 | }, 112 | { 113 | "name": "User", 114 | "description": null, 115 | "args": [ 116 | { 117 | "name": "id", 118 | "description": null, 119 | "type": { 120 | "kind": "NON_NULL", 121 | "name": null, 122 | "ofType": { 123 | "kind": "SCALAR", 124 | "name": "ID", 125 | "ofType": null 126 | } 127 | }, 128 | "defaultValue": null 129 | } 130 | ], 131 | "type": { 132 | "kind": "OBJECT", 133 | "name": "User", 134 | "ofType": null 135 | }, 136 | "isDeprecated": false, 137 | "deprecationReason": null 138 | }, 139 | { 140 | "name": "Notifications", 141 | "description": null, 142 | "args": [ 143 | { 144 | "name": "limit", 145 | "description": null, 146 | "type": { 147 | "kind": "SCALAR", 148 | "name": "Int", 149 | "ofType": null 150 | }, 151 | "defaultValue": null 152 | } 153 | ], 154 | "type": { 155 | "kind": "LIST", 156 | "name": null, 157 | "ofType": { 158 | "kind": "OBJECT", 159 | "name": "Notification", 160 | "ofType": null 161 | } 162 | }, 163 | "isDeprecated": false, 164 | "deprecationReason": null 165 | }, 166 | { 167 | "name": "NotificationsMeta", 168 | "description": null, 169 | "args": [], 170 | "type": { 171 | "kind": "OBJECT", 172 | "name": "Meta", 173 | "ofType": null 174 | }, 175 | "isDeprecated": false, 176 | "deprecationReason": null 177 | } 178 | ], 179 | "inputFields": null, 180 | "interfaces": [], 181 | "enumValues": null, 182 | "possibleTypes": null 183 | }, 184 | { 185 | "kind": "SCALAR", 186 | "name": "ID", 187 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 188 | "fields": null, 189 | "inputFields": null, 190 | "interfaces": null, 191 | "enumValues": null, 192 | "possibleTypes": null 193 | }, 194 | { 195 | "kind": "OBJECT", 196 | "name": "Tweet", 197 | "description": null, 198 | "fields": [ 199 | { 200 | "name": "id", 201 | "description": null, 202 | "args": [], 203 | "type": { 204 | "kind": "NON_NULL", 205 | "name": null, 206 | "ofType": { 207 | "kind": "SCALAR", 208 | "name": "ID", 209 | "ofType": null 210 | } 211 | }, 212 | "isDeprecated": false, 213 | "deprecationReason": null 214 | }, 215 | { 216 | "name": "body", 217 | "description": null, 218 | "args": [], 219 | "type": { 220 | "kind": "SCALAR", 221 | "name": "String", 222 | "ofType": null 223 | }, 224 | "isDeprecated": false, 225 | "deprecationReason": null 226 | }, 227 | { 228 | "name": "date", 229 | "description": null, 230 | "args": [], 231 | "type": { 232 | "kind": "SCALAR", 233 | "name": "Date", 234 | "ofType": null 235 | }, 236 | "isDeprecated": false, 237 | "deprecationReason": null 238 | }, 239 | { 240 | "name": "Author", 241 | "description": null, 242 | "args": [], 243 | "type": { 244 | "kind": "OBJECT", 245 | "name": "User", 246 | "ofType": null 247 | }, 248 | "isDeprecated": false, 249 | "deprecationReason": null 250 | }, 251 | { 252 | "name": "Stats", 253 | "description": null, 254 | "args": [], 255 | "type": { 256 | "kind": "OBJECT", 257 | "name": "Stat", 258 | "ofType": null 259 | }, 260 | "isDeprecated": false, 261 | "deprecationReason": null 262 | } 263 | ], 264 | "inputFields": null, 265 | "interfaces": [], 266 | "enumValues": null, 267 | "possibleTypes": null 268 | }, 269 | { 270 | "kind": "SCALAR", 271 | "name": "String", 272 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 273 | "fields": null, 274 | "inputFields": null, 275 | "interfaces": null, 276 | "enumValues": null, 277 | "possibleTypes": null 278 | }, 279 | { 280 | "kind": "SCALAR", 281 | "name": "Date", 282 | "description": null, 283 | "fields": null, 284 | "inputFields": null, 285 | "interfaces": null, 286 | "enumValues": null, 287 | "possibleTypes": null 288 | }, 289 | { 290 | "kind": "OBJECT", 291 | "name": "User", 292 | "description": null, 293 | "fields": [ 294 | { 295 | "name": "id", 296 | "description": null, 297 | "args": [], 298 | "type": { 299 | "kind": "NON_NULL", 300 | "name": null, 301 | "ofType": { 302 | "kind": "SCALAR", 303 | "name": "ID", 304 | "ofType": null 305 | } 306 | }, 307 | "isDeprecated": false, 308 | "deprecationReason": null 309 | }, 310 | { 311 | "name": "username", 312 | "description": null, 313 | "args": [], 314 | "type": { 315 | "kind": "SCALAR", 316 | "name": "String", 317 | "ofType": null 318 | }, 319 | "isDeprecated": false, 320 | "deprecationReason": null 321 | }, 322 | { 323 | "name": "first_name", 324 | "description": null, 325 | "args": [], 326 | "type": { 327 | "kind": "SCALAR", 328 | "name": "String", 329 | "ofType": null 330 | }, 331 | "isDeprecated": false, 332 | "deprecationReason": null 333 | }, 334 | { 335 | "name": "last_name", 336 | "description": null, 337 | "args": [], 338 | "type": { 339 | "kind": "SCALAR", 340 | "name": "String", 341 | "ofType": null 342 | }, 343 | "isDeprecated": false, 344 | "deprecationReason": null 345 | }, 346 | { 347 | "name": "full_name", 348 | "description": null, 349 | "args": [], 350 | "type": { 351 | "kind": "SCALAR", 352 | "name": "String", 353 | "ofType": null 354 | }, 355 | "isDeprecated": false, 356 | "deprecationReason": null 357 | }, 358 | { 359 | "name": "name", 360 | "description": null, 361 | "args": [], 362 | "type": { 363 | "kind": "SCALAR", 364 | "name": "String", 365 | "ofType": null 366 | }, 367 | "isDeprecated": true, 368 | "deprecationReason": "No longer supported" 369 | }, 370 | { 371 | "name": "avatar_url", 372 | "description": null, 373 | "args": [], 374 | "type": { 375 | "kind": "SCALAR", 376 | "name": "Url", 377 | "ofType": null 378 | }, 379 | "isDeprecated": false, 380 | "deprecationReason": null 381 | } 382 | ], 383 | "inputFields": null, 384 | "interfaces": [], 385 | "enumValues": null, 386 | "possibleTypes": null 387 | }, 388 | { 389 | "kind": "SCALAR", 390 | "name": "Url", 391 | "description": null, 392 | "fields": null, 393 | "inputFields": null, 394 | "interfaces": null, 395 | "enumValues": null, 396 | "possibleTypes": null 397 | }, 398 | { 399 | "kind": "OBJECT", 400 | "name": "Stat", 401 | "description": null, 402 | "fields": [ 403 | { 404 | "name": "views", 405 | "description": null, 406 | "args": [], 407 | "type": { 408 | "kind": "SCALAR", 409 | "name": "Int", 410 | "ofType": null 411 | }, 412 | "isDeprecated": false, 413 | "deprecationReason": null 414 | }, 415 | { 416 | "name": "likes", 417 | "description": null, 418 | "args": [], 419 | "type": { 420 | "kind": "SCALAR", 421 | "name": "Int", 422 | "ofType": null 423 | }, 424 | "isDeprecated": false, 425 | "deprecationReason": null 426 | }, 427 | { 428 | "name": "retweets", 429 | "description": null, 430 | "args": [], 431 | "type": { 432 | "kind": "SCALAR", 433 | "name": "Int", 434 | "ofType": null 435 | }, 436 | "isDeprecated": false, 437 | "deprecationReason": null 438 | }, 439 | { 440 | "name": "responses", 441 | "description": null, 442 | "args": [], 443 | "type": { 444 | "kind": "SCALAR", 445 | "name": "Int", 446 | "ofType": null 447 | }, 448 | "isDeprecated": false, 449 | "deprecationReason": null 450 | } 451 | ], 452 | "inputFields": null, 453 | "interfaces": [], 454 | "enumValues": null, 455 | "possibleTypes": null 456 | }, 457 | { 458 | "kind": "SCALAR", 459 | "name": "Int", 460 | "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", 461 | "fields": null, 462 | "inputFields": null, 463 | "interfaces": null, 464 | "enumValues": null, 465 | "possibleTypes": null 466 | }, 467 | { 468 | "kind": "OBJECT", 469 | "name": "Meta", 470 | "description": null, 471 | "fields": [ 472 | { 473 | "name": "count", 474 | "description": null, 475 | "args": [], 476 | "type": { 477 | "kind": "SCALAR", 478 | "name": "Int", 479 | "ofType": null 480 | }, 481 | "isDeprecated": false, 482 | "deprecationReason": null 483 | } 484 | ], 485 | "inputFields": null, 486 | "interfaces": [], 487 | "enumValues": null, 488 | "possibleTypes": null 489 | }, 490 | { 491 | "kind": "OBJECT", 492 | "name": "Notification", 493 | "description": null, 494 | "fields": [ 495 | { 496 | "name": "id", 497 | "description": null, 498 | "args": [], 499 | "type": { 500 | "kind": "SCALAR", 501 | "name": "ID", 502 | "ofType": null 503 | }, 504 | "isDeprecated": false, 505 | "deprecationReason": null 506 | }, 507 | { 508 | "name": "date", 509 | "description": null, 510 | "args": [], 511 | "type": { 512 | "kind": "SCALAR", 513 | "name": "Date", 514 | "ofType": null 515 | }, 516 | "isDeprecated": false, 517 | "deprecationReason": null 518 | }, 519 | { 520 | "name": "type", 521 | "description": null, 522 | "args": [], 523 | "type": { 524 | "kind": "SCALAR", 525 | "name": "String", 526 | "ofType": null 527 | }, 528 | "isDeprecated": false, 529 | "deprecationReason": null 530 | } 531 | ], 532 | "inputFields": null, 533 | "interfaces": [], 534 | "enumValues": null, 535 | "possibleTypes": null 536 | }, 537 | { 538 | "kind": "OBJECT", 539 | "name": "Mutation", 540 | "description": null, 541 | "fields": [ 542 | { 543 | "name": "createTweet", 544 | "description": null, 545 | "args": [ 546 | { 547 | "name": "body", 548 | "description": null, 549 | "type": { 550 | "kind": "SCALAR", 551 | "name": "String", 552 | "ofType": null 553 | }, 554 | "defaultValue": null 555 | } 556 | ], 557 | "type": { 558 | "kind": "OBJECT", 559 | "name": "Tweet", 560 | "ofType": null 561 | }, 562 | "isDeprecated": false, 563 | "deprecationReason": null 564 | }, 565 | { 566 | "name": "deleteTweet", 567 | "description": null, 568 | "args": [ 569 | { 570 | "name": "id", 571 | "description": null, 572 | "type": { 573 | "kind": "NON_NULL", 574 | "name": null, 575 | "ofType": { 576 | "kind": "SCALAR", 577 | "name": "ID", 578 | "ofType": null 579 | } 580 | }, 581 | "defaultValue": null 582 | } 583 | ], 584 | "type": { 585 | "kind": "OBJECT", 586 | "name": "Tweet", 587 | "ofType": null 588 | }, 589 | "isDeprecated": false, 590 | "deprecationReason": null 591 | }, 592 | { 593 | "name": "markTweetRead", 594 | "description": null, 595 | "args": [ 596 | { 597 | "name": "id", 598 | "description": null, 599 | "type": { 600 | "kind": "NON_NULL", 601 | "name": null, 602 | "ofType": { 603 | "kind": "SCALAR", 604 | "name": "ID", 605 | "ofType": null 606 | } 607 | }, 608 | "defaultValue": null 609 | } 610 | ], 611 | "type": { 612 | "kind": "SCALAR", 613 | "name": "Boolean", 614 | "ofType": null 615 | }, 616 | "isDeprecated": false, 617 | "deprecationReason": null 618 | } 619 | ], 620 | "inputFields": null, 621 | "interfaces": [], 622 | "enumValues": null, 623 | "possibleTypes": null 624 | }, 625 | { 626 | "kind": "SCALAR", 627 | "name": "Boolean", 628 | "description": "The `Boolean` scalar type represents `true` or `false`.", 629 | "fields": null, 630 | "inputFields": null, 631 | "interfaces": null, 632 | "enumValues": null, 633 | "possibleTypes": null 634 | }, 635 | { 636 | "kind": "OBJECT", 637 | "name": "__Schema", 638 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 639 | "fields": [ 640 | { 641 | "name": "types", 642 | "description": "A list of all types supported by this server.", 643 | "args": [], 644 | "type": { 645 | "kind": "NON_NULL", 646 | "name": null, 647 | "ofType": { 648 | "kind": "LIST", 649 | "name": null, 650 | "ofType": { 651 | "kind": "NON_NULL", 652 | "name": null, 653 | "ofType": { 654 | "kind": "OBJECT", 655 | "name": "__Type", 656 | "ofType": null 657 | } 658 | } 659 | } 660 | }, 661 | "isDeprecated": false, 662 | "deprecationReason": null 663 | }, 664 | { 665 | "name": "queryType", 666 | "description": "The type that query operations will be rooted at.", 667 | "args": [], 668 | "type": { 669 | "kind": "NON_NULL", 670 | "name": null, 671 | "ofType": { 672 | "kind": "OBJECT", 673 | "name": "__Type", 674 | "ofType": null 675 | } 676 | }, 677 | "isDeprecated": false, 678 | "deprecationReason": null 679 | }, 680 | { 681 | "name": "mutationType", 682 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 683 | "args": [], 684 | "type": { 685 | "kind": "OBJECT", 686 | "name": "__Type", 687 | "ofType": null 688 | }, 689 | "isDeprecated": false, 690 | "deprecationReason": null 691 | }, 692 | { 693 | "name": "subscriptionType", 694 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 695 | "args": [], 696 | "type": { 697 | "kind": "OBJECT", 698 | "name": "__Type", 699 | "ofType": null 700 | }, 701 | "isDeprecated": false, 702 | "deprecationReason": null 703 | }, 704 | { 705 | "name": "directives", 706 | "description": "A list of all directives supported by this server.", 707 | "args": [], 708 | "type": { 709 | "kind": "NON_NULL", 710 | "name": null, 711 | "ofType": { 712 | "kind": "LIST", 713 | "name": null, 714 | "ofType": { 715 | "kind": "NON_NULL", 716 | "name": null, 717 | "ofType": { 718 | "kind": "OBJECT", 719 | "name": "__Directive", 720 | "ofType": null 721 | } 722 | } 723 | } 724 | }, 725 | "isDeprecated": false, 726 | "deprecationReason": null 727 | } 728 | ], 729 | "inputFields": null, 730 | "interfaces": [], 731 | "enumValues": null, 732 | "possibleTypes": null 733 | }, 734 | { 735 | "kind": "OBJECT", 736 | "name": "__Type", 737 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 738 | "fields": [ 739 | { 740 | "name": "kind", 741 | "description": null, 742 | "args": [], 743 | "type": { 744 | "kind": "NON_NULL", 745 | "name": null, 746 | "ofType": { 747 | "kind": "ENUM", 748 | "name": "__TypeKind", 749 | "ofType": null 750 | } 751 | }, 752 | "isDeprecated": false, 753 | "deprecationReason": null 754 | }, 755 | { 756 | "name": "name", 757 | "description": null, 758 | "args": [], 759 | "type": { 760 | "kind": "SCALAR", 761 | "name": "String", 762 | "ofType": null 763 | }, 764 | "isDeprecated": false, 765 | "deprecationReason": null 766 | }, 767 | { 768 | "name": "description", 769 | "description": null, 770 | "args": [], 771 | "type": { 772 | "kind": "SCALAR", 773 | "name": "String", 774 | "ofType": null 775 | }, 776 | "isDeprecated": false, 777 | "deprecationReason": null 778 | }, 779 | { 780 | "name": "fields", 781 | "description": null, 782 | "args": [ 783 | { 784 | "name": "includeDeprecated", 785 | "description": null, 786 | "type": { 787 | "kind": "SCALAR", 788 | "name": "Boolean", 789 | "ofType": null 790 | }, 791 | "defaultValue": "false" 792 | } 793 | ], 794 | "type": { 795 | "kind": "LIST", 796 | "name": null, 797 | "ofType": { 798 | "kind": "NON_NULL", 799 | "name": null, 800 | "ofType": { 801 | "kind": "OBJECT", 802 | "name": "__Field", 803 | "ofType": null 804 | } 805 | } 806 | }, 807 | "isDeprecated": false, 808 | "deprecationReason": null 809 | }, 810 | { 811 | "name": "interfaces", 812 | "description": null, 813 | "args": [], 814 | "type": { 815 | "kind": "LIST", 816 | "name": null, 817 | "ofType": { 818 | "kind": "NON_NULL", 819 | "name": null, 820 | "ofType": { 821 | "kind": "OBJECT", 822 | "name": "__Type", 823 | "ofType": null 824 | } 825 | } 826 | }, 827 | "isDeprecated": false, 828 | "deprecationReason": null 829 | }, 830 | { 831 | "name": "possibleTypes", 832 | "description": null, 833 | "args": [], 834 | "type": { 835 | "kind": "LIST", 836 | "name": null, 837 | "ofType": { 838 | "kind": "NON_NULL", 839 | "name": null, 840 | "ofType": { 841 | "kind": "OBJECT", 842 | "name": "__Type", 843 | "ofType": null 844 | } 845 | } 846 | }, 847 | "isDeprecated": false, 848 | "deprecationReason": null 849 | }, 850 | { 851 | "name": "enumValues", 852 | "description": null, 853 | "args": [ 854 | { 855 | "name": "includeDeprecated", 856 | "description": null, 857 | "type": { 858 | "kind": "SCALAR", 859 | "name": "Boolean", 860 | "ofType": null 861 | }, 862 | "defaultValue": "false" 863 | } 864 | ], 865 | "type": { 866 | "kind": "LIST", 867 | "name": null, 868 | "ofType": { 869 | "kind": "NON_NULL", 870 | "name": null, 871 | "ofType": { 872 | "kind": "OBJECT", 873 | "name": "__EnumValue", 874 | "ofType": null 875 | } 876 | } 877 | }, 878 | "isDeprecated": false, 879 | "deprecationReason": null 880 | }, 881 | { 882 | "name": "inputFields", 883 | "description": null, 884 | "args": [], 885 | "type": { 886 | "kind": "LIST", 887 | "name": null, 888 | "ofType": { 889 | "kind": "NON_NULL", 890 | "name": null, 891 | "ofType": { 892 | "kind": "OBJECT", 893 | "name": "__InputValue", 894 | "ofType": null 895 | } 896 | } 897 | }, 898 | "isDeprecated": false, 899 | "deprecationReason": null 900 | }, 901 | { 902 | "name": "ofType", 903 | "description": null, 904 | "args": [], 905 | "type": { 906 | "kind": "OBJECT", 907 | "name": "__Type", 908 | "ofType": null 909 | }, 910 | "isDeprecated": false, 911 | "deprecationReason": null 912 | } 913 | ], 914 | "inputFields": null, 915 | "interfaces": [], 916 | "enumValues": null, 917 | "possibleTypes": null 918 | }, 919 | { 920 | "kind": "ENUM", 921 | "name": "__TypeKind", 922 | "description": "An enum describing what kind of type a given `__Type` is.", 923 | "fields": null, 924 | "inputFields": null, 925 | "interfaces": null, 926 | "enumValues": [ 927 | { 928 | "name": "SCALAR", 929 | "description": "Indicates this type is a scalar.", 930 | "isDeprecated": false, 931 | "deprecationReason": null 932 | }, 933 | { 934 | "name": "OBJECT", 935 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 936 | "isDeprecated": false, 937 | "deprecationReason": null 938 | }, 939 | { 940 | "name": "INTERFACE", 941 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 942 | "isDeprecated": false, 943 | "deprecationReason": null 944 | }, 945 | { 946 | "name": "UNION", 947 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 948 | "isDeprecated": false, 949 | "deprecationReason": null 950 | }, 951 | { 952 | "name": "ENUM", 953 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 954 | "isDeprecated": false, 955 | "deprecationReason": null 956 | }, 957 | { 958 | "name": "INPUT_OBJECT", 959 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 960 | "isDeprecated": false, 961 | "deprecationReason": null 962 | }, 963 | { 964 | "name": "LIST", 965 | "description": "Indicates this type is a list. `ofType` is a valid field.", 966 | "isDeprecated": false, 967 | "deprecationReason": null 968 | }, 969 | { 970 | "name": "NON_NULL", 971 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 972 | "isDeprecated": false, 973 | "deprecationReason": null 974 | } 975 | ], 976 | "possibleTypes": null 977 | }, 978 | { 979 | "kind": "OBJECT", 980 | "name": "__Field", 981 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 982 | "fields": [ 983 | { 984 | "name": "name", 985 | "description": null, 986 | "args": [], 987 | "type": { 988 | "kind": "NON_NULL", 989 | "name": null, 990 | "ofType": { 991 | "kind": "SCALAR", 992 | "name": "String", 993 | "ofType": null 994 | } 995 | }, 996 | "isDeprecated": false, 997 | "deprecationReason": null 998 | }, 999 | { 1000 | "name": "description", 1001 | "description": null, 1002 | "args": [], 1003 | "type": { 1004 | "kind": "SCALAR", 1005 | "name": "String", 1006 | "ofType": null 1007 | }, 1008 | "isDeprecated": false, 1009 | "deprecationReason": null 1010 | }, 1011 | { 1012 | "name": "args", 1013 | "description": null, 1014 | "args": [], 1015 | "type": { 1016 | "kind": "NON_NULL", 1017 | "name": null, 1018 | "ofType": { 1019 | "kind": "LIST", 1020 | "name": null, 1021 | "ofType": { 1022 | "kind": "NON_NULL", 1023 | "name": null, 1024 | "ofType": { 1025 | "kind": "OBJECT", 1026 | "name": "__InputValue", 1027 | "ofType": null 1028 | } 1029 | } 1030 | } 1031 | }, 1032 | "isDeprecated": false, 1033 | "deprecationReason": null 1034 | }, 1035 | { 1036 | "name": "type", 1037 | "description": null, 1038 | "args": [], 1039 | "type": { 1040 | "kind": "NON_NULL", 1041 | "name": null, 1042 | "ofType": { 1043 | "kind": "OBJECT", 1044 | "name": "__Type", 1045 | "ofType": null 1046 | } 1047 | }, 1048 | "isDeprecated": false, 1049 | "deprecationReason": null 1050 | }, 1051 | { 1052 | "name": "isDeprecated", 1053 | "description": null, 1054 | "args": [], 1055 | "type": { 1056 | "kind": "NON_NULL", 1057 | "name": null, 1058 | "ofType": { 1059 | "kind": "SCALAR", 1060 | "name": "Boolean", 1061 | "ofType": null 1062 | } 1063 | }, 1064 | "isDeprecated": false, 1065 | "deprecationReason": null 1066 | }, 1067 | { 1068 | "name": "deprecationReason", 1069 | "description": null, 1070 | "args": [], 1071 | "type": { 1072 | "kind": "SCALAR", 1073 | "name": "String", 1074 | "ofType": null 1075 | }, 1076 | "isDeprecated": false, 1077 | "deprecationReason": null 1078 | } 1079 | ], 1080 | "inputFields": null, 1081 | "interfaces": [], 1082 | "enumValues": null, 1083 | "possibleTypes": null 1084 | }, 1085 | { 1086 | "kind": "OBJECT", 1087 | "name": "__InputValue", 1088 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 1089 | "fields": [ 1090 | { 1091 | "name": "name", 1092 | "description": null, 1093 | "args": [], 1094 | "type": { 1095 | "kind": "NON_NULL", 1096 | "name": null, 1097 | "ofType": { 1098 | "kind": "SCALAR", 1099 | "name": "String", 1100 | "ofType": null 1101 | } 1102 | }, 1103 | "isDeprecated": false, 1104 | "deprecationReason": null 1105 | }, 1106 | { 1107 | "name": "description", 1108 | "description": null, 1109 | "args": [], 1110 | "type": { 1111 | "kind": "SCALAR", 1112 | "name": "String", 1113 | "ofType": null 1114 | }, 1115 | "isDeprecated": false, 1116 | "deprecationReason": null 1117 | }, 1118 | { 1119 | "name": "type", 1120 | "description": null, 1121 | "args": [], 1122 | "type": { 1123 | "kind": "NON_NULL", 1124 | "name": null, 1125 | "ofType": { 1126 | "kind": "OBJECT", 1127 | "name": "__Type", 1128 | "ofType": null 1129 | } 1130 | }, 1131 | "isDeprecated": false, 1132 | "deprecationReason": null 1133 | }, 1134 | { 1135 | "name": "defaultValue", 1136 | "description": "A GraphQL-formatted string representing the default value for this input value.", 1137 | "args": [], 1138 | "type": { 1139 | "kind": "SCALAR", 1140 | "name": "String", 1141 | "ofType": null 1142 | }, 1143 | "isDeprecated": false, 1144 | "deprecationReason": null 1145 | } 1146 | ], 1147 | "inputFields": null, 1148 | "interfaces": [], 1149 | "enumValues": null, 1150 | "possibleTypes": null 1151 | }, 1152 | { 1153 | "kind": "OBJECT", 1154 | "name": "__EnumValue", 1155 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 1156 | "fields": [ 1157 | { 1158 | "name": "name", 1159 | "description": null, 1160 | "args": [], 1161 | "type": { 1162 | "kind": "NON_NULL", 1163 | "name": null, 1164 | "ofType": { 1165 | "kind": "SCALAR", 1166 | "name": "String", 1167 | "ofType": null 1168 | } 1169 | }, 1170 | "isDeprecated": false, 1171 | "deprecationReason": null 1172 | }, 1173 | { 1174 | "name": "description", 1175 | "description": null, 1176 | "args": [], 1177 | "type": { 1178 | "kind": "SCALAR", 1179 | "name": "String", 1180 | "ofType": null 1181 | }, 1182 | "isDeprecated": false, 1183 | "deprecationReason": null 1184 | }, 1185 | { 1186 | "name": "isDeprecated", 1187 | "description": null, 1188 | "args": [], 1189 | "type": { 1190 | "kind": "NON_NULL", 1191 | "name": null, 1192 | "ofType": { 1193 | "kind": "SCALAR", 1194 | "name": "Boolean", 1195 | "ofType": null 1196 | } 1197 | }, 1198 | "isDeprecated": false, 1199 | "deprecationReason": null 1200 | }, 1201 | { 1202 | "name": "deprecationReason", 1203 | "description": null, 1204 | "args": [], 1205 | "type": { 1206 | "kind": "SCALAR", 1207 | "name": "String", 1208 | "ofType": null 1209 | }, 1210 | "isDeprecated": false, 1211 | "deprecationReason": null 1212 | } 1213 | ], 1214 | "inputFields": null, 1215 | "interfaces": [], 1216 | "enumValues": null, 1217 | "possibleTypes": null 1218 | }, 1219 | { 1220 | "kind": "OBJECT", 1221 | "name": "__Directive", 1222 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 1223 | "fields": [ 1224 | { 1225 | "name": "name", 1226 | "description": null, 1227 | "args": [], 1228 | "type": { 1229 | "kind": "NON_NULL", 1230 | "name": null, 1231 | "ofType": { 1232 | "kind": "SCALAR", 1233 | "name": "String", 1234 | "ofType": null 1235 | } 1236 | }, 1237 | "isDeprecated": false, 1238 | "deprecationReason": null 1239 | }, 1240 | { 1241 | "name": "description", 1242 | "description": null, 1243 | "args": [], 1244 | "type": { 1245 | "kind": "SCALAR", 1246 | "name": "String", 1247 | "ofType": null 1248 | }, 1249 | "isDeprecated": false, 1250 | "deprecationReason": null 1251 | }, 1252 | { 1253 | "name": "locations", 1254 | "description": null, 1255 | "args": [], 1256 | "type": { 1257 | "kind": "NON_NULL", 1258 | "name": null, 1259 | "ofType": { 1260 | "kind": "LIST", 1261 | "name": null, 1262 | "ofType": { 1263 | "kind": "NON_NULL", 1264 | "name": null, 1265 | "ofType": { 1266 | "kind": "ENUM", 1267 | "name": "__DirectiveLocation", 1268 | "ofType": null 1269 | } 1270 | } 1271 | } 1272 | }, 1273 | "isDeprecated": false, 1274 | "deprecationReason": null 1275 | }, 1276 | { 1277 | "name": "args", 1278 | "description": null, 1279 | "args": [], 1280 | "type": { 1281 | "kind": "NON_NULL", 1282 | "name": null, 1283 | "ofType": { 1284 | "kind": "LIST", 1285 | "name": null, 1286 | "ofType": { 1287 | "kind": "NON_NULL", 1288 | "name": null, 1289 | "ofType": { 1290 | "kind": "OBJECT", 1291 | "name": "__InputValue", 1292 | "ofType": null 1293 | } 1294 | } 1295 | } 1296 | }, 1297 | "isDeprecated": false, 1298 | "deprecationReason": null 1299 | } 1300 | ], 1301 | "inputFields": null, 1302 | "interfaces": [], 1303 | "enumValues": null, 1304 | "possibleTypes": null 1305 | }, 1306 | { 1307 | "kind": "ENUM", 1308 | "name": "__DirectiveLocation", 1309 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 1310 | "fields": null, 1311 | "inputFields": null, 1312 | "interfaces": null, 1313 | "enumValues": [ 1314 | { 1315 | "name": "QUERY", 1316 | "description": "Location adjacent to a query operation.", 1317 | "isDeprecated": false, 1318 | "deprecationReason": null 1319 | }, 1320 | { 1321 | "name": "MUTATION", 1322 | "description": "Location adjacent to a mutation operation.", 1323 | "isDeprecated": false, 1324 | "deprecationReason": null 1325 | }, 1326 | { 1327 | "name": "SUBSCRIPTION", 1328 | "description": "Location adjacent to a subscription operation.", 1329 | "isDeprecated": false, 1330 | "deprecationReason": null 1331 | }, 1332 | { 1333 | "name": "FIELD", 1334 | "description": "Location adjacent to a field.", 1335 | "isDeprecated": false, 1336 | "deprecationReason": null 1337 | }, 1338 | { 1339 | "name": "FRAGMENT_DEFINITION", 1340 | "description": "Location adjacent to a fragment definition.", 1341 | "isDeprecated": false, 1342 | "deprecationReason": null 1343 | }, 1344 | { 1345 | "name": "FRAGMENT_SPREAD", 1346 | "description": "Location adjacent to a fragment spread.", 1347 | "isDeprecated": false, 1348 | "deprecationReason": null 1349 | }, 1350 | { 1351 | "name": "INLINE_FRAGMENT", 1352 | "description": "Location adjacent to an inline fragment.", 1353 | "isDeprecated": false, 1354 | "deprecationReason": null 1355 | }, 1356 | { 1357 | "name": "VARIABLE_DEFINITION", 1358 | "description": "Location adjacent to a variable definition.", 1359 | "isDeprecated": false, 1360 | "deprecationReason": null 1361 | }, 1362 | { 1363 | "name": "SCHEMA", 1364 | "description": "Location adjacent to a schema definition.", 1365 | "isDeprecated": false, 1366 | "deprecationReason": null 1367 | }, 1368 | { 1369 | "name": "SCALAR", 1370 | "description": "Location adjacent to a scalar definition.", 1371 | "isDeprecated": false, 1372 | "deprecationReason": null 1373 | }, 1374 | { 1375 | "name": "OBJECT", 1376 | "description": "Location adjacent to an object type definition.", 1377 | "isDeprecated": false, 1378 | "deprecationReason": null 1379 | }, 1380 | { 1381 | "name": "FIELD_DEFINITION", 1382 | "description": "Location adjacent to a field definition.", 1383 | "isDeprecated": false, 1384 | "deprecationReason": null 1385 | }, 1386 | { 1387 | "name": "ARGUMENT_DEFINITION", 1388 | "description": "Location adjacent to an argument definition.", 1389 | "isDeprecated": false, 1390 | "deprecationReason": null 1391 | }, 1392 | { 1393 | "name": "INTERFACE", 1394 | "description": "Location adjacent to an interface definition.", 1395 | "isDeprecated": false, 1396 | "deprecationReason": null 1397 | }, 1398 | { 1399 | "name": "UNION", 1400 | "description": "Location adjacent to a union definition.", 1401 | "isDeprecated": false, 1402 | "deprecationReason": null 1403 | }, 1404 | { 1405 | "name": "ENUM", 1406 | "description": "Location adjacent to an enum definition.", 1407 | "isDeprecated": false, 1408 | "deprecationReason": null 1409 | }, 1410 | { 1411 | "name": "ENUM_VALUE", 1412 | "description": "Location adjacent to an enum value definition.", 1413 | "isDeprecated": false, 1414 | "deprecationReason": null 1415 | }, 1416 | { 1417 | "name": "INPUT_OBJECT", 1418 | "description": "Location adjacent to an input object type definition.", 1419 | "isDeprecated": false, 1420 | "deprecationReason": null 1421 | }, 1422 | { 1423 | "name": "INPUT_FIELD_DEFINITION", 1424 | "description": "Location adjacent to an input object field definition.", 1425 | "isDeprecated": false, 1426 | "deprecationReason": null 1427 | } 1428 | ], 1429 | "possibleTypes": null 1430 | } 1431 | ], 1432 | "directives": [ 1433 | { 1434 | "name": "skip", 1435 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1436 | "locations": [ 1437 | "FIELD", 1438 | "FRAGMENT_SPREAD", 1439 | "INLINE_FRAGMENT" 1440 | ], 1441 | "args": [ 1442 | { 1443 | "name": "if", 1444 | "description": "Skipped when true.", 1445 | "type": { 1446 | "kind": "NON_NULL", 1447 | "name": null, 1448 | "ofType": { 1449 | "kind": "SCALAR", 1450 | "name": "Boolean", 1451 | "ofType": null 1452 | } 1453 | }, 1454 | "defaultValue": null 1455 | } 1456 | ] 1457 | }, 1458 | { 1459 | "name": "include", 1460 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1461 | "locations": [ 1462 | "FIELD", 1463 | "FRAGMENT_SPREAD", 1464 | "INLINE_FRAGMENT" 1465 | ], 1466 | "args": [ 1467 | { 1468 | "name": "if", 1469 | "description": "Included when true.", 1470 | "type": { 1471 | "kind": "NON_NULL", 1472 | "name": null, 1473 | "ofType": { 1474 | "kind": "SCALAR", 1475 | "name": "Boolean", 1476 | "ofType": null 1477 | } 1478 | }, 1479 | "defaultValue": null 1480 | } 1481 | ] 1482 | }, 1483 | { 1484 | "name": "deprecated", 1485 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1486 | "locations": [ 1487 | "FIELD_DEFINITION", 1488 | "ENUM_VALUE" 1489 | ], 1490 | "args": [ 1491 | { 1492 | "name": "reason", 1493 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).", 1494 | "type": { 1495 | "kind": "SCALAR", 1496 | "name": "String", 1497 | "ofType": null 1498 | }, 1499 | "defaultValue": "\"No longer supported\"" 1500 | } 1501 | ] 1502 | } 1503 | ] 1504 | } 1505 | } 1506 | -------------------------------------------------------------------------------- /tests/tweet_schema.sdl: -------------------------------------------------------------------------------- 1 | type Tweet { 2 | id: ID! 3 | # The tweet text. No more than 140 characters! 4 | body: String 5 | # When the tweet was published 6 | date: Date 7 | # Who published the tweet 8 | Author: User 9 | # Views, retweets, likes, etc 10 | Stats: Stat 11 | } 12 | 13 | type User { 14 | id: ID! 15 | username: String 16 | first_name: String 17 | last_name: String 18 | full_name: String 19 | name: String @deprecated 20 | avatar_url: Url 21 | } 22 | 23 | type Stat { 24 | views: Int 25 | likes: Int 26 | retweets: Int 27 | responses: Int 28 | } 29 | 30 | type Notification { 31 | id: ID 32 | date: Date 33 | type: String 34 | } 35 | 36 | type Meta { 37 | count: Int 38 | } 39 | 40 | scalar Url 41 | scalar Date 42 | 43 | type Query { 44 | Tweet(id: ID!): Tweet 45 | Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet] 46 | TweetsMeta: Meta 47 | User(id: ID!): User 48 | Notifications(limit: Int): [Notification] 49 | NotificationsMeta: Meta 50 | } 51 | 52 | type Mutation { 53 | createTweet ( 54 | body: String 55 | ): Tweet 56 | deleteTweet(id: ID!): Tweet 57 | markTweetRead(id: ID!): Boolean 58 | } 59 | 60 | -------------------------------------------------------------------------------- /tests/types.yaml: -------------------------------------------------------------------------------- 1 | types: 2 | - "CreateSales_SaleInput": 3 | depth: 2 4 | - "Network_Contact" 5 | - "Entity" 6 | 7 | domains: 8 | - "risk" 9 | --------------------------------------------------------------------------------