├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── package.json └── src ├── __tests__ ├── enum.js └── index.js ├── enum.js ├── fragment.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.1.0 4 | 1 Nov 2016 5 | 6 | - [#6](https://github.com/kadirahq/graphqlify/pull/6) - Implement `graphqlify.query` and `graphqlify.mutation` functions 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphqlify 2 | 3 | This module helps you build GraphQL queries using plain JavaScript objects. This can be useful when you need to programmatically build GraphQL queries. Install the module from npm to get started. 4 | 5 | ``` 6 | npm i -S graphqlify 7 | ``` 8 | 9 | ## Example 10 | 11 | **GraphQL** 12 | 13 | ``` 14 | { 15 | teamFourStar { 16 | members { 17 | memberName 18 | } 19 | saiyans: members(type: SAIYAJIN, minPower: 9000) { 20 | memberName 21 | powerLevel 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | **JavaScript** 28 | 29 | ```js 30 | import graphqlify, {Enum} from 'graphqlify'; 31 | 32 | const string = graphqlify({ 33 | teamFourStar: { 34 | fields: { 35 | members: { 36 | fields: {memberName: {}} 37 | }, 38 | saiyans: { 39 | field: 'members', 40 | params: {type: Enum('SAIYAJIN'), minPower: 9000}, 41 | fields: {memberName: {}, powerLevel: {}} 42 | } 43 | } 44 | } 45 | }); 46 | ``` 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphqlify", 3 | "version": "1.1.0", 4 | "description": "Build GraphQL queries with JavaScript", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepublish": "nofat make", 8 | "test": "nofat test && nofat lint" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/kadirahq/graphqlify.git" 13 | }, 14 | "keywords": [ 15 | "graphql" 16 | ], 17 | "author": "Kadira Inc.", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/kadirahq/graphqlify/issues" 21 | }, 22 | "homepage": "https://github.com/kadirahq/graphqlify#readme", 23 | "devDependencies": { 24 | "nofat": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/enum.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {default as Enum, _enum} from '../enum'; 3 | 4 | describe('Enum', function () { 5 | it('should store the name', function () { 6 | const e = Enum('foo'); 7 | expect(e).to.be.an.instanceof(_enum); 8 | expect(e.name).to.equal('foo'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | import {expect} from 'chai'; 4 | import graphqlify, {Enum, Fragment} from '../'; 5 | 6 | describe('graphqlify', function () { 7 | it('should encode a simple field', function () { 8 | const out = graphqlify({a: 1}); 9 | expect(out).to.equal('{a}'); 10 | }); 11 | 12 | it('should encode multiple fields', function () { 13 | const out = graphqlify({a: 1, b: true, c: {}, d: null}); 14 | expect(out).to.equal('{a,b,c}'); 15 | }); 16 | 17 | it('should encode field with a label', function () { 18 | const out = graphqlify({a: {field: 'b'}}); 19 | expect(out).to.equal('{a:b}'); 20 | }); 21 | 22 | it('should encode a field with nested fields', function () { 23 | const out = graphqlify({a: {fields: {b: {fields: {c: 1}}}}}); 24 | expect(out).to.equal('{a{b{c}}}'); 25 | }); 26 | 27 | it('should encode field with boolean parameter', function () { 28 | const out = graphqlify({a: {params: {b: false}}}); 29 | expect(out).to.equal('{a(b:false)}'); 30 | }); 31 | 32 | it('should encode field with number parameter', function () { 33 | const out = graphqlify({a: {params: {b: 12.34}}}); 34 | expect(out).to.equal('{a(b:12.34)}'); 35 | }); 36 | 37 | it('should encode field with string parameter', function () { 38 | const out = graphqlify({a: {params: {b: 'c'}}}); 39 | expect(out).to.equal('{a(b:"c")}'); 40 | }); 41 | 42 | it('should encode field with enum parameter', function () { 43 | const out = graphqlify({a: {params: {b: Enum('c')}}}); 44 | expect(out).to.equal('{a(b:c)}'); 45 | }); 46 | 47 | it('should encode field with object parameter', function () { 48 | const out = graphqlify({a: {params: {b: {c: 'd'}}}}); 49 | expect(out).to.equal('{a(b:{c:"d"})}'); 50 | }); 51 | 52 | it('should encode field with array parameter', function () { 53 | const out = graphqlify({a: {params: {b: [ 'c', 'd' ]}}}); 54 | expect(out).to.equal('{a(b:["c","d"])}'); 55 | }); 56 | 57 | it('should encode a field with params and nested fields', function () { 58 | const out = graphqlify({a: {params: {b: 'c'}, fields: {d: 1}}}); 59 | expect(out).to.equal('{a(b:"c"){d}}'); 60 | }); 61 | 62 | it('should encode a field with a fragment', function () { 63 | const frag = Fragment({ 64 | name: 'fragname', 65 | type: 'FragType', 66 | fields: {b: 1}, 67 | }); 68 | const out = graphqlify({a: {fragments: [ frag ]}}); 69 | expect(out).to.equal('{a{...fragname}},fragment fragname on FragType{b}'); 70 | }); 71 | 72 | it('should encode a field with 2 fragments', function () { 73 | const frag1 = Fragment({ 74 | name: 'fragname1', 75 | type: 'FragType1', 76 | fields: {b: 1}, 77 | }); 78 | const frag2 = Fragment({ 79 | name: 'fragname2', 80 | type: 'FragType2', 81 | fields: {c: 1}, 82 | }); 83 | const out = graphqlify({a: {fragments: [ frag1, frag2 ]}}); 84 | expect(out).to.equal('{a{...fragname1,...fragname2}},fragment fragname1 on FragType1{b},fragment fragname2 on FragType2{c}'); 85 | }); 86 | 87 | it('should encode a field with nested fragments', function () { 88 | const frag2 = Fragment({ 89 | name: 'fragname2', 90 | type: 'FragType2', 91 | fields: {b: 1}, 92 | }); 93 | const frag1 = Fragment({ 94 | name: 'fragname1', 95 | type: 'FragType1', 96 | fragments: [ frag2 ], 97 | }); 98 | const out = graphqlify({a: {fragments: [ frag1 ]}}); 99 | expect(out).to.equal('{a{...fragname1}},fragment fragname1 on FragType1{...fragname2},fragment fragname2 on FragType2{b}'); 100 | }); 101 | }); 102 | 103 | describe('query', function () { 104 | it('should encode a graphql query', function () { 105 | const out = graphqlify.query({a: 1}); 106 | expect(out).to.equal('query{a}'); 107 | }); 108 | 109 | it('should encode a named graphql query', function () { 110 | const out = graphqlify.query('q1', {a: 1}); 111 | expect(out).to.equal('query q1{a}'); 112 | }); 113 | }); 114 | 115 | describe('mutation', function () { 116 | it('should encode a graphql mutation', function () { 117 | const out = graphqlify.mutation({a: 1}); 118 | expect(out).to.equal('mutation{a}'); 119 | }); 120 | 121 | it('should encode a named graphql mutation', function () { 122 | const out = graphqlify.mutation('m1', {a: 1}); 123 | expect(out).to.equal('mutation m1{a}'); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/enum.js: -------------------------------------------------------------------------------- 1 | export default function Enum(name) { 2 | return new _enum(name); 3 | } 4 | 5 | export class _enum { 6 | constructor(name) { 7 | this.name = name; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/fragment.js: -------------------------------------------------------------------------------- 1 | export default function Fragment(params) { 2 | return new _fragment(params); 3 | } 4 | 5 | export class _fragment { 6 | constructor(params) { 7 | this.name = params.name; 8 | this.type = params.type; 9 | this.fields = params.fields; 10 | this.fragments = params.fragments; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {_enum} from './enum'; 2 | // import {_fragment} from './fragment'; 3 | 4 | export {default as Enum} from './enum'; 5 | export {default as Fragment} from './fragment'; 6 | 7 | // Encodes a graphql query 8 | const graphqlify = function (fields) { 9 | return encodeOperation('', fields); 10 | }; 11 | 12 | // Encodes a graphql query 13 | graphqlify.query = function (_nameOrFields, _fieldsOrNil) { 14 | return encodeOperation('query', _nameOrFields, _fieldsOrNil); 15 | }; 16 | 17 | // Encodes a graphql mutation 18 | graphqlify.mutation = function (_nameOrFields, _fieldsOrNil) { 19 | return encodeOperation('mutation', _nameOrFields, _fieldsOrNil); 20 | }; 21 | 22 | // default export graphqlify 23 | export default graphqlify; 24 | 25 | // Encodes a graphql operation and fragments 26 | // The output is a complete graphql query. 27 | // 28 | // {a: {fields: {b: 1}}} => '{a{b}}' 29 | // 'mutation', {a: {fields: {b: 1}}} => 'mutation{a{b}}' 30 | // 31 | function encodeOperation(type, _nameOrFields, _fieldsOrNil) { 32 | let name = _nameOrFields; 33 | let fields = _fieldsOrNil; 34 | if (!_fieldsOrNil && typeof _nameOrFields === 'object') { 35 | name = null; 36 | fields = _nameOrFields; 37 | } 38 | 39 | const parts = []; 40 | 41 | // stringifying the main query object 42 | const fieldset = encodeFieldset(fields, null); 43 | 44 | if (name) { 45 | parts.push(`${type} ${name}${fieldset}`); 46 | } else { 47 | parts.push(`${type}${fieldset}`); 48 | } 49 | 50 | const fragments = findFragments(fields); 51 | if (fragments.length) { 52 | parts.push(encodeFragments(fragments)); 53 | } 54 | 55 | return parts.join(','); 56 | } 57 | 58 | // TODO add function description 59 | function findFragments(fields) { 60 | const fragments = Object.keys(fields) 61 | .filter(key => fields[key] && typeof fields[key] === 'object') 62 | .map(key => findFieldFragments(fields[key])) 63 | .reduce((a, b) => a.concat(b), []); 64 | return Array.from(new Set(fragments)); 65 | } 66 | 67 | // TODO add function description 68 | function findFieldFragments(field) { 69 | let fragments = []; 70 | if (field.fragments) { 71 | fragments = fragments.concat(field.fragments); 72 | field.fragments.forEach(frag => { 73 | const fragFragments = findFragFragments(frag); 74 | fragments = fragments.concat(fragFragments); 75 | }); 76 | } 77 | if (field.fields) { 78 | fragments = fragments.concat(findFragments(field.fields)); 79 | } 80 | return fragments; 81 | } 82 | 83 | // TODO add function description 84 | function findFragFragments(frag) { 85 | let fragments = []; 86 | if (frag.fragments) { 87 | fragments = fragments.concat(frag.fragments); 88 | frag.fragments.forEach(nestedFrag => { 89 | const fragFragments = findFragFragments(nestedFrag); 90 | fragments = fragments.concat(fragFragments); 91 | }); 92 | } 93 | if (frag.fields) { 94 | fragments = fragments.concat(findFragments(frag.fields)); 95 | } 96 | return fragments; 97 | } 98 | 99 | // TODO add function description 100 | function encodeFragments(fragments) { 101 | return fragments.map(f => encodeFragment(f)).join(','); 102 | } 103 | 104 | // TODO add function description 105 | function encodeFragment(fragment) { 106 | const fieldset = encodeFieldset(fragment.fields, fragment.fragments); 107 | return `fragment ${fragment.name} on ${fragment.type}${fieldset}`; 108 | } 109 | 110 | // Encodes a group of fields and fragments 111 | // The output is a piece of a graphql query. 112 | // 113 | // {a: 1, b: true, c: {}} => '{a,b,c}' 114 | // {a: {fields: {b: 1}}} => '{a{b}}' 115 | // 116 | function encodeFieldset(fields, fragments) { 117 | const parts = []; 118 | if (fields) { 119 | parts.push(encodeFields(fields)); 120 | } 121 | if (fragments) { 122 | fragments.forEach(f => parts.push(`...${f.name}`)); 123 | } 124 | return `{${parts.join(',')}}`; 125 | } 126 | 127 | // Encodes a set of fields and nested fields. 128 | // The output is a piece of a graphql query. 129 | // 130 | // {a: 1, b: true, c: {}} => 'a,b,c' 131 | // {a: {fields: {b: 1}}} => 'a{b}' 132 | // 133 | function encodeFields(fields) { 134 | if (!fields || typeof fields !== 'object') { 135 | throw new Error(`fields cannot be "${fields}"`); 136 | } 137 | 138 | const encoded = Object.keys(fields).filter(function (key) { 139 | return fields.hasOwnProperty(key) && fields[key]; 140 | }).map(function (key) { 141 | return encodeField(key, fields[key]); 142 | }); 143 | 144 | if (encoded.length === 0) { 145 | throw new Error(`fields cannot be empty`); 146 | } 147 | 148 | return encoded.join(','); 149 | } 150 | 151 | // Encode a single field and nested fields. 152 | // The output is a piece of a graphql query. 153 | // 154 | // ('a', 1) => 'a' 155 | // ('a', {field: 'aa'}) => 'a:aa' 156 | // ('a', {params: {b: 10}}) => 'a(b:10)' 157 | // ('a', {fields: {b: 10}}) => 'a{b}' 158 | // 159 | function encodeField(key, val) { 160 | if (typeof val !== 'object') { 161 | return key; 162 | } 163 | 164 | const parts = [ key ]; 165 | 166 | if (val.field) { 167 | parts.push(`:${val.field}`); 168 | } 169 | if (val.params) { 170 | parts.push(encodeParams(val.params)); 171 | } 172 | if (val.fields || val.fragments) { 173 | parts.push(encodeFieldset(val.fields, val.fragments)); 174 | } 175 | 176 | return parts.join(''); 177 | } 178 | 179 | // Encodes a map of field parameters. 180 | // 181 | // {a: 1, b: true} => '(a:1,b:true)' 182 | // {a: ['b', 'c']} => '(a:["b","c"])' 183 | // {a: {b: 'c'}} => '(a:{b:"c"})' 184 | // 185 | function encodeParams(params) { 186 | const encoded = encodeParamsMap(params); 187 | if (encoded.length === 0) { 188 | throw new Error(`params cannot be empty`); 189 | } 190 | 191 | return `(${encoded.join(',')})`; 192 | } 193 | 194 | // Encodes an object type field parameter. 195 | // 196 | // {a: {b: {c: 10}}} => '{a:{b:{c:10}}}' 197 | // {a: {b: false}} => '{a:{b:false}}' 198 | // 199 | function encodeParamsObject(params) { 200 | const encoded = encodeParamsMap(params); 201 | return `{${encoded.join(',')}}`; 202 | } 203 | 204 | // Encodes an array type field parameter. 205 | // 206 | // [1, 2, 3] => '[1,2,3]' 207 | // [ {a: 1}, {a: 2} ] => '[{a:1},{a:2}]' 208 | // 209 | function encodeParamsArray(array) { 210 | const encoded = array.map(encodeParamValue); 211 | return `[${encoded.join(',')}]`; 212 | } 213 | 214 | // Encodes a map of field parameters. 215 | // 216 | // {a: 1, b: true} => 'a:1,b:true' 217 | // {a: ['b', 'c']} => 'a:["b","c"]' 218 | // {a: {b: 'c'}} => 'a:{b:"c"}' 219 | // 220 | function encodeParamsMap(params) { 221 | if (!params || typeof params !== 'object') { 222 | throw new Error(`params cannot be "${params}"`); 223 | } 224 | 225 | const keys = Object.keys(params).filter(function (key) { 226 | const val = params[key]; 227 | return params.hasOwnProperty(key) && 228 | val !== undefined && 229 | val !== null && 230 | !Number.isNaN(val); 231 | }); 232 | 233 | return keys.map(key => encodeParam(key, params[key])); 234 | } 235 | 236 | // Encodes a single parameter 237 | // 238 | // ('a', 1) => 'a:1' 239 | // 240 | function encodeParam(key, val) { 241 | return `${key}:${encodeParamValue(val)}`; 242 | } 243 | 244 | // Encodes parameter value 245 | // 246 | // 'a' => '"a"' 247 | // Enum('a') => 'a' 248 | // 249 | function encodeParamValue(value) { 250 | if (Array.isArray(value)) { 251 | return encodeParamsArray(value); 252 | } 253 | if (value instanceof _enum) { 254 | return value.name; 255 | } 256 | if (typeof value === 'object') { 257 | return encodeParamsObject(value); 258 | } 259 | if (typeof value === 'string') { 260 | return JSON.stringify(value); 261 | } 262 | if (typeof value === 'number') { 263 | return String(value); 264 | } 265 | if (typeof value === 'boolean') { 266 | return value; 267 | } 268 | 269 | throw new Error(`unsupported param type "${typeof value}"`); 270 | } 271 | --------------------------------------------------------------------------------