├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.ts.snap └── index.ts ├── package.json ├── src └── index.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10 7 | steps: 8 | - checkout 9 | - run: yarn install --frozen-lockfile 10 | - run: yarn lint 11 | - run: yarn compile 12 | - run: yarn coverage 13 | - run: yarn test:prettier 14 | - run: yarn codecov 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | package.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/birkir/graphql-mst/e01a59a3b8b8713cacbe793afbadb0a1092de948/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2019-present, Birkir Gudjonsson 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm downloads](https://img.shields.io/npm/dt/graphql-mst.svg)](https://www.npmjs.com/package/graphql-mst) 2 | [![npm](https://img.shields.io/npm/v/graphql-mst.svg?maxAge=2592000)](https://www.npmjs.com/package/graphql-mst) 3 | [![codecov](https://codecov.io/gh/birkir/graphql-mst/branch/master/graph/badge.svg)](https://codecov.io/gh/birkir/graphql-mst) 4 | [![CircleCI](https://circleci.com/gh/birkir/graphql-mst.svg?style=shield)](https://circleci.com/gh/birkir/graphql-mst) 5 | [![MIT license](https://img.shields.io/github/license/birkir/graphql-mst.svg)](https://opensource.org/licenses/MIT) 6 | 7 | # graphql-mst 8 | 9 | Convert GraphQL Schema to mobx-state-tree models. 10 | 11 | See demos in [tests folder](https://github.com/birkir/graphql-mst/blob/master/__tests__/index.ts) 12 | 13 | ### Installing 14 | 15 | ```bash 16 | yarn add graphql-mst 17 | # or 18 | npm install graphql-mst 19 | ``` 20 | 21 | ### Usage 22 | 23 | ```ts 24 | import { generateFromSchema } from 'graphql-mst'; 25 | 26 | const schema = ` 27 | type Foo { 28 | a: String 29 | b: Int 30 | } 31 | type Bar { 32 | c: [Foo] 33 | } 34 | `; 35 | 36 | const { Foo, Bar } = generateFromSchema(schema); 37 | 38 | const foo = Foo.create({ 39 | a: 'Hello', 40 | b: 10, 41 | }); 42 | 43 | const bar = Bar.create({ 44 | c: [foo, { a: 'World', b: 20 }], 45 | }); 46 | ``` 47 | 48 | #### Identifiers 49 | 50 | ```ts 51 | const schema = ` 52 | type Foo { 53 | userId: ID! 54 | fooId: ID! 55 | } 56 | `; 57 | 58 | const config = { 59 | Foo: { 60 | identifier: 'fooId', // this will be used as identifier for model 'Foo' 61 | }, 62 | }; 63 | 64 | const { Foo } = generateFromSchema(schema, config); 65 | 66 | const lookup = types 67 | .model({ items: types.map(Foo) }) 68 | .actions(self => ({ add: item => self.items.put(item) })) 69 | .create({ items: {} }); 70 | 71 | lookup.put({ userId: 10, fooId: 1 }); 72 | lookup.put({ userId: 20, fooId: 2 }); 73 | 74 | lookup.items.get(1); // { userId: 10, fooId: 1 } 75 | lookup.items.get(2); // { userId: 20, fooId: 2 } 76 | ``` 77 | 78 | ### TODO and thoughts 79 | 80 | - Configure map type instead of array type 81 | - Default values for arguments as `types.optional` 82 | - reference types? 83 | - Date scalar? Custom scalar? 84 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`graphql-mst should convert complex type 1`] = ` 4 | Object { 5 | "a": "string", 6 | "b": "id", 7 | "c": 1, 8 | "d": 1.1, 9 | "e": Array [ 10 | "one", 11 | "two", 12 | ], 13 | "f": Object { 14 | "foo": "bar", 15 | }, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IAnyType, 3 | IModelType, 4 | isFrozenType, 5 | ISimpleType, 6 | isModelType, 7 | types, 8 | UnionStringArray, 9 | } from 'mobx-state-tree'; 10 | import { generateFromSchema } from '../src/index'; 11 | 12 | describe('graphql-mst', () => { 13 | it('should export a generateFromSchema function', () => { 14 | expect(typeof generateFromSchema).toBe('function'); 15 | }); 16 | 17 | it('should parse schema', () => { 18 | const schema = ` 19 | type Test { 20 | foo: String! 21 | } 22 | `; 23 | 24 | const { Test } = generateFromSchema(schema); 25 | expect(Test).toBeTruthy(); 26 | expect(isModelType(Test)).toBe(true); 27 | }); 28 | 29 | it('should throw on syntax error', () => { 30 | try { 31 | generateFromSchema(''); 32 | throw new Error(); 33 | } catch (err) { 34 | expect(err.message).toContain('Syntax Error'); 35 | } 36 | }); 37 | 38 | it('should convert scalars to frozen', () => { 39 | const schema = ` 40 | scalar JSON 41 | type Test { 42 | foo: JSON 43 | } 44 | `; 45 | const { Test } = generateFromSchema(schema); 46 | expect(isFrozenType(Test.properties.foo)).toBe(true); 47 | }); 48 | 49 | it('should convert string properties', () => { 50 | const schema = ` 51 | type Test { 52 | foo: String 53 | } 54 | `; 55 | const result = generateFromSchema(schema); 56 | const Test: IModelType<{ foo: IAnyType }, {}> = result.Test; 57 | expect(Test.properties.foo).toBeTruthy(); 58 | expect(typeof Test.properties.foo).toBe(typeof types.string); 59 | }); 60 | 61 | it('should convert enum properties', () => { 62 | const schema = ` 63 | enum TestEnum { 64 | FOO 65 | BAR 66 | } 67 | type Test { 68 | foo: TestEnum 69 | } 70 | `; 71 | const { Test, TestEnum } = generateFromSchema(schema) as { 72 | Test: any; 73 | TestEnum: ISimpleType>; 74 | }; 75 | expect(TestEnum.name).toBe('TestEnum'); 76 | expect(TestEnum.create('FOO')).toBe('FOO'); 77 | try { 78 | expect(TestEnum.create('BOO' as any)).toBe('BOO'); 79 | throw new Error(); 80 | } catch (err) { 81 | expect(err.message).toContain('Error while converting'); 82 | } 83 | expect(typeof (Test as IModelType<{ foo: typeof TestEnum }, {}>).properties.foo).toBe( 84 | typeof TestEnum 85 | ); 86 | }); 87 | 88 | it('should convert unions', () => { 89 | const schema = ` 90 | type Foo { foo: String } 91 | type Bar { bar: String } 92 | union FooBar = Foo | Bar 93 | type Test { 94 | baz: FooBar 95 | } 96 | `; 97 | 98 | const result = generateFromSchema(schema); 99 | const Foo: IModelType<{ foo: IAnyType }, {}> = result.Foo; 100 | const Bar: IModelType<{ bar: IAnyType }, {}> = result.Bar; 101 | const Test: IModelType<{ baz: IAnyType }, {}> = result.Test; 102 | 103 | expect(Test.create({ baz: Foo.create({ foo: 'foo' }) })).toEqual({ 104 | baz: { foo: 'foo' }, 105 | }); 106 | 107 | expect(Test.create({ baz: Bar.create({ bar: 'bar' }) })).toEqual({ 108 | baz: { bar: 'bar' }, 109 | }); 110 | }); 111 | 112 | it('should convert complex type', () => { 113 | const schema = ` 114 | type Foo { foo: String } 115 | type Test { 116 | a: String 117 | b: ID 118 | c: Int 119 | d: Float 120 | e: [String] 121 | f: Foo 122 | } 123 | `; 124 | const result = generateFromSchema(schema); 125 | const test = result.Test.create({ 126 | a: 'string', 127 | b: 'id', 128 | c: 1, 129 | d: 1.1, 130 | e: ['one', 'two'], 131 | f: { 132 | foo: 'bar', 133 | }, 134 | }); 135 | expect(test).toMatchSnapshot(); 136 | }); 137 | 138 | it('should convert arrays', () => { 139 | const schema = ` 140 | type Test { 141 | a: [String] 142 | b: [String!] 143 | c: [String]! 144 | d: [String!]! 145 | e: [[String!]]! 146 | } 147 | `; 148 | const { Test } = generateFromSchema(schema); 149 | expect(Test.properties.a.name).toEqual('((string | null)[] | null)'); 150 | expect(Test.properties.b.name).toEqual('(string[] | null)'); 151 | expect(Test.properties.c.name).toEqual('(string | null)[]'); 152 | expect(Test.properties.d.name).toEqual('string[]'); 153 | expect(Test.properties.e.name).toEqual('string[][]'); 154 | }); 155 | 156 | it('should convert interfaces', () => { 157 | const schema = ` 158 | interface Demo { 159 | foo: String! 160 | } 161 | type Test implements Demo { 162 | bar: String! 163 | } 164 | `; 165 | const { Test } = generateFromSchema(schema); 166 | expect(Test.properties.foo.name).toBe('string'); 167 | expect(Test.properties.bar.name).toBe('string'); 168 | }); 169 | 170 | it('should convert input types', () => { 171 | const schema = ` 172 | input TestInput { 173 | foo: String! 174 | } 175 | type Test { 176 | bar: TestInput 177 | } 178 | `; 179 | const { Test, TestInput } = generateFromSchema(schema); 180 | expect(TestInput.properties.foo.name).toBe('string'); 181 | expect(Test.properties.bar.name).toBe('(TestInput | null)'); 182 | }); 183 | 184 | it('should handle ID types', () => { 185 | let Test; 186 | const schema = ` 187 | type Test { 188 | foo: ID! 189 | bar: ID 190 | } 191 | `; 192 | Test = generateFromSchema(schema).Test; 193 | expect(Test.properties.foo.name).toBe('identifier'); 194 | 195 | Test = generateFromSchema(schema, { Test: { identifier: 'bar' } }).Test; 196 | expect(Test.properties.foo.name).toBe('string'); 197 | }); 198 | 199 | it('should handle ID types in interfaces', () => { 200 | const schema = ` 201 | interface TestInterface { 202 | foo: ID! 203 | } 204 | type Test implements TestInterface { 205 | bar: ID! 206 | } 207 | `; 208 | const { Test } = generateFromSchema(schema); 209 | expect(Test.properties.foo.name).toBe('string'); 210 | expect(Test.properties.bar.name).toBe('identifier'); 211 | 212 | const lookup = types 213 | .model({ 214 | foos: types.map(Test), 215 | }) 216 | .actions(self => ({ add: item => self.foos.put(item) })) 217 | .create({ foos: {} }); 218 | 219 | lookup.add({ foo: '10', bar: '1' }); 220 | lookup.add({ foo: '20', bar: '2' }); 221 | 222 | expect(lookup.foos.get('1')).toEqual({ foo: '10', bar: '1' }); 223 | expect(lookup.foos.get('2')).toEqual({ foo: '20', bar: '2' }); 224 | }); 225 | }); 226 | 227 | // type Foo { 228 | // foo: String 229 | // } 230 | 231 | // type Bar { 232 | // bar: String 233 | // } 234 | 235 | // union TestUnion = Foo | Bar 236 | 237 | // enum Baz { 238 | // FOO 239 | // BAR 240 | // } 241 | 242 | // interface Character { 243 | // id: ID! 244 | // name: String! 245 | // } 246 | 247 | // type Human implements Character { 248 | // id: ID! 249 | // name: String! 250 | // totalCredits: Int 251 | // } 252 | 253 | // type Droid implements Character { 254 | // id: ID! 255 | // bleh: ID! 256 | // primaryFunction: String 257 | // } 258 | 259 | // type Demo { 260 | // a: String! 261 | // aa: String 262 | // b: [String!]! 263 | // bb: [String!] 264 | // bbb: [String]! 265 | // bbbb: [String] 266 | // bbbbb: [[[String!]!]!]! 267 | // c: TestUnion 268 | // d: [Foo] 269 | // e: Bar 270 | // f: Baz 271 | // g: Droid 272 | // h(yo: UserInput): Human 273 | // u: UserInput 274 | // } 275 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-mst", 3 | "version": "0.2.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf {lib,node_modules,reports,yarn.lock}", 8 | "codecov": "codecov", 9 | "compile": "tsc", 10 | "coverage": "yarn testonly --coverage", 11 | "lint": "tslint -p .", 12 | "prepublishOnly": "yarn compile", 13 | "prettier": "prettier --write '**/*.{json,md,js,jsx,ts,tsx}'", 14 | "test:prettier": "prettier --list-different '**/*.{json,md,js,jsx,ts,tsx}'", 15 | "test:watch": "jest --watch", 16 | "test": "yarn lint && yarn compile && yarn coverage && yarn test:prettier", 17 | "testonly": "jest --runInBand" 18 | }, 19 | "author": "Birkir Gudjonsson { 21 | const builtSchema = buildSchema(source); 22 | const context = schemaToTemplateContext(builtSchema); 23 | 24 | const cache = new Map(); 25 | 26 | const mapField = (field: Field) => { 27 | let type = fieldMap[field.type]; 28 | 29 | if (!type) { 30 | if (field.isType) { 31 | const fieldType = context.types.find(t => t.name === field.type); 32 | if (fieldType) { 33 | type = mapType(fieldType); 34 | } 35 | } else if (field.isInputType) { 36 | const fieldType = context.inputTypes.find(t => t.name === field.type); 37 | if (fieldType) { 38 | type = mapType(fieldType); 39 | } 40 | } else if (field.isUnion) { 41 | const fieldUnion = context.unions.find(t => t.name === field.type); 42 | if (fieldUnion) { 43 | type = mapUnion(fieldUnion); 44 | } 45 | } else if (field.isEnum) { 46 | const fieldEnum = context.enums.find(t => t.name === field.type); 47 | if (fieldEnum) { 48 | type = mapEnum(fieldEnum); 49 | } 50 | } else if (field.isScalar) { 51 | type = types.frozen(); 52 | } 53 | } 54 | 55 | if (type) { 56 | if (field.isArray) { 57 | if (field.isNullableArray) { 58 | type = types.maybeNull(type); 59 | } 60 | 61 | for (let i = 0; i < field.dimensionOfArray; i++) { 62 | type = types.array(type); 63 | } 64 | } 65 | 66 | // @todo submit PR to get the defaultValue 67 | // if (field.hasDefaultValue && (field as any).defaultValue) { 68 | // type = types.optional(type, (field as any).defaultValue); 69 | // } else 70 | if (!field.isRequired) { 71 | type = types.maybeNull(type); 72 | } 73 | 74 | return type; 75 | } 76 | }; 77 | 78 | function isType(node: any): node is Type { 79 | return typeof node.interfaces !== 'undefined'; 80 | } 81 | 82 | function isInterface(node: any): node is Interface { 83 | return typeof node.implementingTypes !== 'undefined'; 84 | } 85 | 86 | const mapEnum = (type: Enum) => { 87 | if (cache.has(type.name)) { 88 | return cache.get(type.name); 89 | } 90 | 91 | const result = types.enumeration(type.name, type.values.map(value => value.name)); 92 | 93 | cache.set(type.name, result); 94 | 95 | return result; 96 | }; 97 | 98 | const mapUnion = (type: Union) => { 99 | if (cache.has(type.name)) { 100 | return cache.get(type.name); 101 | } 102 | 103 | const unions = type.possibleTypes 104 | .map((typeName: any) => context.types.find((n: any) => n.name === typeName)) 105 | .map(unionType => { 106 | if (isType(unionType)) { 107 | return mapType(unionType); 108 | } 109 | }) 110 | .filter(interfaceType => !!interfaceType); 111 | 112 | const result = types.union(...unions); 113 | 114 | cache.set(type.name, result); 115 | 116 | return result; 117 | }; 118 | 119 | const mapType = (type: Type | Interface | Union, config?: Config) => { 120 | if (!config) { 121 | config = typeConfig[type.name] || { identifier: undefined }; 122 | } 123 | 124 | if (cache.has(type.name)) { 125 | return cache.get(type.name); 126 | } 127 | 128 | if (isType(type) || isInterface(type)) { 129 | let result = types.model( 130 | type.name, 131 | type.fields.reduce((acc, field) => { 132 | const hasID = Object.values(acc).find((n: ISimpleType) => n.name === 'identifier'); 133 | if (field.type === 'ID') { 134 | if ( 135 | (typeof config!.identifier === 'undefined' || field.name === config!.identifier) && 136 | !hasID 137 | ) { 138 | acc[field.name] = types.identifier; 139 | } else { 140 | acc[field.name] = field.isRequired ? types.string : types.maybeNull(types.string); 141 | } 142 | } else { 143 | const fieldType = mapField(field); 144 | if (fieldType) { 145 | acc[field.name] = fieldType; 146 | } 147 | } 148 | 149 | return acc; 150 | }, {}) 151 | ); 152 | 153 | const hasIdentifier = !!(result as any).identifierAttribute; 154 | 155 | if (isType(type)) { 156 | const compositions = type.interfaces 157 | .map((interfaceName: any) => 158 | context.interfaces.find((n: any) => n.name === interfaceName) 159 | ) 160 | .map(interfaceType => { 161 | if (isInterface(interfaceType)) { 162 | return mapType(interfaceType, { identifier: hasIdentifier ? null : undefined }); 163 | } 164 | }) 165 | .filter(interfaceType => !!interfaceType); 166 | 167 | if (compositions.length) { 168 | result = (types.compose as any)(...compositions, result).named(type.name); 169 | } 170 | } 171 | 172 | cache.set(type.name, result); 173 | 174 | return result; 175 | } 176 | }; 177 | 178 | context.inputTypes.forEach(type => mapType(type)); 179 | context.types.forEach(type => mapType(type)); 180 | context.unions.forEach(type => mapUnion(type)); 181 | context.enums.forEach(type => mapEnum(type)); 182 | 183 | return Array.from(cache.entries()).reduce((acc, [key, value]) => { 184 | acc[key] = value; 185 | return acc; 186 | }, {}); 187 | }; 188 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "moduleResolution": "node", 5 | "jsx": "preserve", 6 | "noUnusedLocals": true, 7 | "pretty": true, 8 | "esModuleInterop": true, 9 | "strictNullChecks": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es6", 12 | "module": "commonjs", 13 | "declaration": true, 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "lib": ["dom", "es6", "es2015", "es2017", "esnext"] 19 | }, 20 | "include": ["./src"], 21 | "exclude": ["node_modules", "**/*.spec.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "completed-docs": false, 5 | "interface-name": false, 6 | "missing-jsdoc": false, 7 | "no-relative-imports": false, 8 | "no-reserved-keywords": false, 9 | "no-unsafe-any": false, 10 | "object-literal-sort-keys": false, 11 | "quotemark": [true, "single", "jsx-double"], 12 | "strict-boolean-expressions": false, 13 | "typedef": false 14 | }, 15 | "linterOptions": { 16 | "exclude": ["node_modules", "packages/*/lib/*"] 17 | } 18 | } 19 | --------------------------------------------------------------------------------