UNPKG

21.4 kBPlain TextView Raw
1/**
2 * Copyright (c) 2016, John Hewson
3 * All rights reserved.
4 */
5
6/// <reference path="../typings/node.d.ts" />
7/// <reference path="../typings/es6-function.d.ts" />
8/// <reference path="../typings/graphql-types.d.ts" />
9/// <reference path="../typings/graphql-language.d.ts" />
10/// <reference path="../typings/graphql-utilities.d.ts" />
11
12import {
13 Definition,
14 OperationDefinition,
15 FragmentDefinition,
16 FragmentSpread,
17 InlineFragment,
18 SelectionSet,
19 Field,
20 Document,
21 Type,
22 parse,
23 print,
24 visit
25} from "graphql/language";
26
27import {
28 ElmFieldDecl,
29 ElmDecl,
30 ElmTypeDecl,
31 ElmParameterDecl,
32 moduleToString,
33 ElmExpr,
34 ElmFunctionDecl,
35 ElmType,
36 ElmTypeName,
37 ElmTypeRecord,
38 ElmTypeApp,
39 ElmTypeAliasDecl
40} from './elm-ast';
41
42import {
43 GraphQLSchema,
44 GraphQLNonNull,
45 GraphQLList,
46 GraphQLScalarType,
47 GraphQLEnumType,
48 GraphQLType,
49 GraphQLObjectType,
50 GraphQLInterfaceType,
51 GraphQLInputObjectType,
52 GraphQLUnionType
53} from 'graphql/type';
54
55import {
56 TypeInfo,
57 typeFromAST,
58} from 'graphql/utilities';
59
60import {
61 decoderForQuery,
62 decoderForFragment
63} from './query-to-decoder';
64
65export type GraphQLEnumMap = { [name: string]: GraphQLEnumType };
66export type GraphQLTypeMap = { [name: string]: GraphQLType };
67export type FragmentDefinitionMap = { [name: string]: FragmentDefinition };
68export type GraphQLUnionMap = { [name: string]: GraphQLUnionType };
69
70const alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
71 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
72
73export function queryToElm(graphql: string, moduleName: string, liveUrl: string, verb: string,
74 schema: GraphQLSchema): string {
75 let queryDocument = parse(graphql);
76 let [decls, expose] = translateQuery(liveUrl, queryDocument, schema, verb);
77 return moduleToString(moduleName, expose, [
78 'Task exposing (Task)',
79 'Json.Decode.Extra exposing ((|:))',
80 'Json.Decode exposing (..)',
81 'Json.Encode exposing (encode)',
82 'Http',
83 'GraphQL.Client as GraphQL exposing (Context, apply, maybeEncode, GQLError, HttpMapper)'
84 ], decls);
85}
86
87function translateQuery(uri: string, doc: Document, schema: GraphQLSchema, verb: string): [Array<ElmDecl>, Array<string>] {
88 let expose: Array<string> = [];
89 let fragmentDefinitionMap: FragmentDefinitionMap = {};
90
91 function walkQueryDocument(doc: Document, info: TypeInfo): [Array<ElmDecl>, Array<string>] {
92 let decls: Array<ElmDecl> = [];
93 // decls.push(new ElmFunctionDecl('endpointUrl', [], new ElmTypeName('String'), { expr: `"${uri}"` }));
94
95 buildFragmentDefinitionMap(doc);
96 let seenFragments: FragmentDefinitionMap = {};
97 let seenEnums: GraphQLEnumMap = {};
98 let seenUnions: GraphQLUnionMap = {};
99
100 for (let def of doc.definitions) {
101 if (def.kind == 'OperationDefinition') {
102 decls.push(...walkOperationDefinition(<OperationDefinition>def, info));
103 } else if (def.kind == 'FragmentDefinition') {
104 decls.push(...walkFragmentDefinition(<FragmentDefinition>def, info));
105 }
106 collectFragments(def, seenFragments);
107 collectEnums(def, seenEnums);
108 collectUnions(def, seenUnions);
109 }
110
111 for (let fragName in seenFragments) {
112 let frag = seenFragments[fragName];
113 let decodeFragFuncName = fragName[0].toLowerCase() + fragName.substr(1) + 'Decoder';
114 let fragTypeName = fragName[0].toUpperCase() + fragName.substr(1);
115 decls.push(new ElmFunctionDecl(
116 decodeFragFuncName, [],
117 new ElmTypeName('Decoder ' + fragTypeName),
118 decoderForFragment(frag, info, schema, fragmentDefinitionMap, seenFragments) ));
119 expose.push(decodeFragFuncName);
120 expose.push(fragTypeName);
121 }
122
123 for (let name in seenEnums) {
124 let seenEnum = seenEnums[name];
125 decls.unshift(walkEnum(seenEnum));
126 decls.push(decoderForEnum(seenEnum));
127 expose.push(seenEnum.name + "(..)");
128 }
129
130 for (let name in seenUnions) {
131 let union = seenUnions[name];
132 decls.unshift(walkUnion(union));
133 expose.push((getRootType(union)['name'] + "(..)"));
134 }
135
136 return [decls, expose];
137 }
138
139 function buildFragmentDefinitionMap(doc: Document): void {
140 visit(doc, {
141 enter: function(node) {
142 if (node.kind == 'FragmentDefinition') {
143 let def = <FragmentDefinition>node;
144 let name = def.name.value;
145 fragmentDefinitionMap[name] = def;
146 }
147 },
148 leave: function(node) {}
149 });
150 }
151
152 function collectFragments(def: Definition, fragments: FragmentDefinitionMap = {}): FragmentDefinitionMap {
153 visit(doc, {
154 enter: function(node) {
155 if (node.kind == 'FragmentSpread') {
156 let spread = <FragmentSpread>node;
157 let name = spread.name.value;
158 fragments[name] = fragmentDefinitionMap[name];
159 }
160 },
161 leave: function(node) {}
162 });
163 return fragments;
164 }
165
166 // Retrieve fragments used in the specified query definition selection set
167 function queryFragments(selectionSet: SelectionSet, fragments: FragmentDefinitionMap = {}): FragmentDefinitionMap {
168 if (selectionSet) {
169 visit(selectionSet, {
170 enter: function (node) {
171 if (node.kind == 'FragmentSpread') {
172 let spread = <FragmentSpread>node;
173 let name = spread.name.value;
174 let frag = fragments[name] = fragmentDefinitionMap[name];
175 fragments = queryFragments(frag.selectionSet, fragments);
176 }
177 },
178 leave: function (node) {
179 }
180 });
181 }
182 return fragments;
183 }
184
185 function collectUnions(def: Definition, unions: GraphQLUnionMap = {}): GraphQLUnionMap {
186 let info = new TypeInfo(schema);
187 visit(doc, {
188 enter: function(node, key, parent) {
189 if (node.kind == 'InlineFragment') {
190 let parentType = <GraphQLUnionType> info.getType();
191 unions[parentType.name] = parentType;
192 }
193 info.enter(node);
194 },
195 leave: function(node) {
196 info.leave(node);
197 }
198 });
199 return unions;
200 }
201
202 function collectEnums(def: Definition, enums: GraphQLEnumMap = {}): GraphQLEnumMap {
203 let info = new TypeInfo(schema);
204 visit(doc, {
205 enter: function(node, key, parent) {
206 info.enter(node);
207 if (node.kind == 'Field') {
208 let field = <Field>node;
209 let name = field.name.value;
210 let type = info.getType();
211 collectEnumsForType(type, enums);
212 }
213 // todo: do we need to walk into fragment spreads?
214 },
215 leave: function(node, key, parent) {
216 info.leave(node);
217 }
218 });
219 return enums;
220 }
221
222 function collectEnumsForType(type: GraphQLType, seen: GraphQLEnumMap = {}, seenTypes: GraphQLTypeMap = {}): void {
223 if (type instanceof GraphQLEnumType) {
224 seen[type.name] = type;
225 } else if (type instanceof GraphQLList) {
226 collectEnumsForType(type.ofType, seen, seenTypes);
227 } else if (type instanceof GraphQLObjectType ||
228 type instanceof GraphQLInterfaceType ||
229 type instanceof GraphQLInputObjectType) {
230 if (seenTypes[type.name]) {
231 return;
232 } else {
233 seenTypes[type.name] = type;
234 }
235 let fieldMap = type.getFields();
236 for (let fieldName in fieldMap) {
237 let field = fieldMap[fieldName];
238 collectEnumsForType(field.type, seen, seenTypes)
239 }
240 } else if (type instanceof GraphQLNonNull) {
241 collectEnumsForType(type.ofType, seen, seenTypes);
242 }
243 }
244
245 function walkEnum(enumType: GraphQLEnumType): ElmTypeDecl {
246 console.log(enumType.getValues())
247 return new ElmTypeDecl(enumType.name, enumType.getValues().map(v => v.name[0].toUpperCase() + v.name.substr(1)));
248 }
249
250 function decoderForEnum(enumType: GraphQLEnumType): ElmFunctionDecl {
251 // might need to be Maybe Episode, with None -> fail in the Decoder
252 let decoderTypeName = enumType.name[0].toUpperCase() + enumType.name.substr(1);
253 return new ElmFunctionDecl(enumType.name.toLowerCase() + 'Decoder', [], new ElmTypeName('Decoder ' + decoderTypeName),
254 { expr: 'string |> andThen (\\s ->\n' +
255 ' case s of\n' + enumType.getValues().map(v =>
256 ' "' + v.name + '" -> succeed ' + v.name[0].toUpperCase() + v.name.substr(1)).join('\n') + '\n' +
257 ' _ -> fail "Unknown ' + enumType.name + '")'
258 });
259 }
260
261 function walkUnion(union: GraphQLUnionType): ElmTypeDecl {
262 union = <GraphQLUnionType>getRootType(union)
263 let types = union.getTypes();
264 let params = types.map((t, i) => alphabet[i]).join(' ');
265 return new ElmTypeDecl(getRootType(union)['name'] + ' ' + params, types.map((t, i) => elmSafeName(t.name) + ' ' + alphabet[i]));
266 }
267
268 function walkOperationDefinition(def: OperationDefinition, info: TypeInfo): Array<ElmDecl> {
269 info.enter(def);
270 if (!info.getType()) {
271 throw new Error(`GraphQL schema does not define ${def.operation} '${def.name.value}'`);
272 }
273 if (def.operation == 'query' || def.operation == 'mutation') {
274 let decls: Array<ElmDecl> = [];
275 // Name
276 let name: string;
277 if (def.name) {
278 name = def.name.value;
279 } else {
280 name = 'AnonymousQuery';
281 }
282 let resultType = name[0].toUpperCase() + name.substr(1);
283 // todo: Directives
284 // SelectionSet
285 let [fields, spreads] = walkSelectionSet(def.selectionSet, info);
286 // todo: use spreads...
287 decls.push(new ElmTypeAliasDecl(resultType, new ElmTypeRecord(fields)))
288 // VariableDefinition
289 let parameters: Array<{name: string, type: ElmType, schemaType: GraphQLType, hasDefault:boolean}> = [];
290 if (def.variableDefinitions) {
291 for (let varDef of def.variableDefinitions) {
292 let name = varDef.variable.name.value;
293 let schemaType = typeFromAST(schema, varDef.type);
294 let type = typeToElm(schemaType);
295 parameters.push({ name, type, schemaType, hasDefault: varDef.defaultValue != null });
296 }
297 }
298 let funcName = name[0].toLowerCase() + name.substr(1);
299
300 // grabs all fragments
301 let seenFragments = collectFragments(def);
302
303 // grabs all fragment dependencies in the query
304 let qFragments = queryFragments(def.selectionSet);
305
306 let query = '';
307 for (let name in qFragments) {
308 query += print(qFragments[name]) + ' ';
309 }
310
311 query += print(def);
312 let decodeFuncName = resultType[0].toLowerCase() + resultType.substr(1) + 'Decoder';
313 expose.push(funcName);
314 expose.push(resultType);
315
316 let resultTypeName = resultType[0].toUpperCase() + resultType.substr(1);
317 let elmContextType = new ElmTypeName("Context");
318 let elmMapperType = new ElmTypeName(`HttpMapper success result`);
319 let elmToMsgType = new ElmTypeName("(result -> msg)");
320 let elmToResultMapType = new ElmTypeName(`(${resultTypeName} -> success)`);
321 let elmParamsType = new ElmTypeRecord(parameters.map(p => new ElmFieldDecl(p.name, p.type)));
322 let elmContext = new ElmParameterDecl('context', elmContextType);
323 let elmMapper = new ElmParameterDecl('mapper', elmMapperType);
324 let elmToMsg = new ElmParameterDecl('toMsg', elmToMsgType);
325 let elmToResultMap = new ElmParameterDecl('mapDecoder', elmToResultMapType);
326 let elmParams = new ElmParameterDecl('params', elmParamsType);
327 let elmParamsDecl = elmParamsType.fields.length > 0 ? [elmContext, elmParams, elmToResultMap, elmMapper, elmToMsg] : [elmContext, elmToResultMap, elmMapper, elmToMsg];
328 let methodParam = def.operation == 'query' ? `"${verb}" ` : '';
329
330 decls.push(new ElmFunctionDecl(
331 funcName, elmParamsDecl, new ElmTypeName(`Cmd msg`),
332 {
333 // we use awkward variable names to avoid naming collisions with query parameters
334 expr: `let graphQLQuery = """${query.replace(/\s+/g, ' ')}""" in\n` +
335 ` let graphQLParams =\n` +
336 ` Json.Encode.object\n` +
337 ` [ ` +
338 parameters.map(p => {
339 let encoder: string;
340 if (p.hasDefault) {
341 encoder =`case params.${p.name} of` +
342 `\n Just val -> ${encoderForInputType(p.schemaType, true)} val` +
343 `\n Nothing -> Json.Encode.null`
344 } else {
345 encoder = encoderForInputType(p.schemaType, true, 'params.' + p.name);
346 }
347 return `("${p.name}", ${encoder})`;
348 })
349 .join(`\n , `) + '\n' +
350 ` ]\n` +
351 ` in\n` +
352 ` GraphQL.${def.operation} context ${methodParam}graphQLQuery "${name}" graphQLParams (Json.Decode.map mapDecoder ${decodeFuncName}) mapper toMsg`
353 }
354 ));
355 decls.push(new ElmFunctionDecl(
356 decodeFuncName, [],
357 new ElmTypeName('Decoder ' + resultTypeName),
358 decoderForQuery(def, info, schema, fragmentDefinitionMap, seenFragments) ));
359
360 info.leave(def);
361 return decls;
362 }
363 }
364
365 function encoderForInputType(type: GraphQLType, isNonNull?: boolean, value?: string): string {
366 let encoder: string;
367
368 let isMaybe = false
369 if (type instanceof GraphQLNonNull) {
370 type = type['ofType'];
371 } else {
372 isMaybe = true;
373 }
374
375 if (type instanceof GraphQLInputObjectType) {
376 let fieldEncoders: Array<string> = [];
377 let fields = type.getFields();
378 for (let name in fields) {
379 let field = fields[name];
380 let valuePath = value + '.' + field.name;
381 fieldEncoders.push(`("${field.name}", ${encoderForInputType(field.type, false, valuePath)})`);
382 }
383 encoder = '(Json.Encode.object [' + fieldEncoders.join(`, `) + '])';
384 } else if (type instanceof GraphQLList) {
385 encoder = '(Json.Encode.list (List.map (\\x -> ' + encoderForInputType(type.ofType, true, 'x') + ') ' + value + '))';
386 } else if (type instanceof GraphQLScalarType) {
387 switch (type.name) {
388 case 'Int': encoder = 'Json.Encode.int ' + value; break;
389 case 'Float': encoder = 'Json.Encode.float ' + value; break;
390 case 'Boolean': encoder = 'Json.Encode.bool ' + value; break;
391 case 'ID':
392 case 'String': encoder = 'Json.Encode.string ' + value; break;
393 }
394 } else {
395 throw new Error('not implemented: ' + type.constructor.name);
396 }
397
398 if (isMaybe) {
399 encoder = '(maybeEncode ' + encoder + ')'
400 }
401 return encoder;
402 }
403
404 function walkFragmentDefinition(def: FragmentDefinition, info: TypeInfo): Array<ElmDecl> {
405 info.enter(def);
406
407 let name = def.name.value;
408
409 let decls: Array<ElmDecl> = [];
410 let resultType = name[0].toUpperCase() + name.substr(1);
411
412 // todo: Directives
413
414 // SelectionSet
415 let [fields, spreads] = walkSelectionSet(def.selectionSet, info);
416 let type: ElmType = new ElmTypeRecord(fields, 'a')
417 for (let spreadName of spreads) {
418 let typeName = spreadName[0].toUpperCase() + spreadName.substr(1) + '_';
419 type = new ElmTypeApp(typeName, [type]);
420 }
421
422 decls.push(new ElmTypeAliasDecl(resultType + '_', type, ['a']));
423 decls.push(new ElmTypeAliasDecl(resultType, new ElmTypeApp(resultType + '_', [new ElmTypeRecord([])])));
424
425 info.leave(def);
426 return decls;
427 }
428
429 function walkSelectionSet(selSet: SelectionSet, info: TypeInfo): [Array<ElmFieldDecl>, Array<string>, ElmType] {
430 info.enter(selSet);
431 let fields: Array<ElmFieldDecl> = [];
432 let spreads: Array<string> = [];
433
434 if (getRootType(info.getType()) instanceof GraphQLUnionType) {
435 let type = walkUnionSelectionSet(selSet, info);
436 return [[], [], type];
437 } else {
438 for (let sel of selSet.selections) {
439 if (sel.kind == 'Field') {
440 let field = <Field>sel;
441 fields.push(walkField(field, info));
442 } else if (sel.kind == 'FragmentSpread') {
443 spreads.push((<FragmentSpread>sel).name.value);
444 } else if (sel.kind == 'InlineFragment') {
445 let frag = (<InlineFragment>sel);
446 // todo: InlineFragment
447 throw new Error('not implemented: InlineFragment on ' + frag.typeCondition.name.value);
448 }
449 }
450
451 info.leave(selSet);
452 return [fields, spreads, null];
453 }
454 }
455
456 function walkUnionSelectionSet(selSet: SelectionSet, info: TypeInfo): ElmType {
457 let union = <GraphQLUnionType>getRootType(info.getType())
458
459 let typeMap: { [name: string]: ElmType } = {};
460 for (let type of union.getTypes()) {
461 typeMap[type.name] = new ElmTypeRecord([]);
462 }
463
464 for (let sel of selSet.selections) {
465 if (sel.kind == 'InlineFragment') {
466 let inline = (<InlineFragment>sel);
467
468 info.enter(inline);
469 let [fields, spreads] = walkSelectionSet(inline.selectionSet, info);
470 info.leave(inline);
471
472 // record
473 let type: ElmType = new ElmTypeRecord(fields);
474 // spreads
475 for (let spreadName of spreads) {
476 let typeName = spreadName[0].toUpperCase() + spreadName.substr(1) + '_';
477 type = new ElmTypeApp(typeName, [type]);
478 }
479
480 typeMap[inline.typeCondition.name.value] = type;
481 }
482 }
483
484 let args: Array<ElmType> = [];
485 for (let name in typeMap) {
486 args.push(typeMap[name]);
487 }
488 return new ElmTypeApp(getRootType(union)['name'], args);
489 }
490
491 function walkField(field: Field, info: TypeInfo): ElmFieldDecl {
492 info.enter(field);
493
494 let info_type = info.getType()
495 // Name
496 let name = elmSafeName(field.name.value);
497 // Alias
498 if (field.alias) {
499 name = elmSafeName(field.alias.value);
500 }
501 // todo: Arguments, such as `id: $someId`, where $someId is a variable
502 let args = field.arguments; // e.g. id: "1000"
503 // todo: Directives
504 // SelectionSet
505 if (field.selectionSet) {
506 let isMaybe = false
507 if (info_type instanceof GraphQLNonNull) {
508 info_type = info_type['ofType']
509 } else {
510 isMaybe = true
511 }
512
513 let isList = info_type instanceof GraphQLList;
514 let [fields, spreads, union] = walkSelectionSet(field.selectionSet, info);
515
516 let type: ElmType = union ? union : new ElmTypeRecord(fields);
517
518 for (let spreadName of spreads) {
519 let typeName = spreadName[0].toUpperCase() + spreadName.substr(1) + '_';
520 type = new ElmTypeApp(typeName, [type]);
521 }
522
523 if (isList) {
524 type = new ElmTypeApp('List', [type]);
525 }
526
527 if (isMaybe) {
528 type = new ElmTypeApp('Maybe', [type]);
529 }
530
531 info.leave(field);
532 return new ElmFieldDecl(name, type)
533 } else {
534 if (!info.getType()) {
535 let errorMessage = `Unknown GraphQL field: ' ${field.name.value}\n${JSON.stringify(info)}\n`
536 throw new Error('Unknown GraphQL field: ' + field.name.value);
537 }
538 let type = typeToElm(info.getType());
539 info.leave(field);
540 return new ElmFieldDecl(name, type)
541 }
542 }
543 return walkQueryDocument(doc, new TypeInfo(schema));
544}
545
546export function getRootType(type: GraphQLType): GraphQLType {
547 if (type instanceof GraphQLList){
548 return getRootType(type['ofType'])
549 } else if (type instanceof GraphQLNonNull){
550 return getRootType(type['ofType'])
551 } else {
552 return type
553 }
554}
555
556export function typeToElm(type: GraphQLType, isNonNull = false): ElmType {
557 let elmType: ElmType;
558
559 if (type instanceof GraphQLNonNull) {
560 elmType = typeToElm(type.ofType, true);
561 }
562 else if (type instanceof GraphQLScalarType) {
563 switch (type.name) {
564 case 'Int': elmType = new ElmTypeName('Int'); break;
565 case 'Float': elmType = new ElmTypeName('Float'); break;
566 case 'Boolean': elmType = new ElmTypeName('Bool'); break;
567 case 'ID':
568 case 'String': elmType = new ElmTypeName('String'); break;
569 }
570 } else if (type instanceof GraphQLEnumType) {
571 elmType = new ElmTypeName(type.name[0].toUpperCase() + type.name.substr(1));
572 } else if (type instanceof GraphQLList) {
573 elmType = new ElmTypeApp('List', [typeToElm(type.ofType, true)]);
574 } else if (type instanceof GraphQLObjectType ||
575 type instanceof GraphQLInterfaceType ||
576 type instanceof GraphQLInputObjectType) {
577 let fields: Array<ElmFieldDecl> = [];
578 let fieldMap = type.getFields();
579 for (let fieldName in fieldMap) {
580 let field = fieldMap[fieldName];
581 fields.push(new ElmFieldDecl(elmSafeName(fieldName), typeToElm(field.type)))
582 }
583 elmType = new ElmTypeRecord(fields);
584 } else if (type instanceof GraphQLNonNull) {
585 elmType = typeToElm(type.ofType, true);
586 } else {
587 throw new Error('Unexpected: ' + type.constructor.name);
588 }
589
590 if (!isNonNull && !(type instanceof GraphQLList) && !(type instanceof GraphQLNonNull)) {
591 elmType = new ElmTypeApp('Maybe', [elmType]);
592 }
593 return elmType;
594}
595
596export function elmSafeName(graphQlName: string): string {
597 switch (graphQlName) {
598 case 'type': return "type_";
599 case 'Task': return "Task_";
600 case 'List': return "List_";
601 case 'Http': return "Http_";
602 case 'GraphQL': return "GraphQL_";
603 // todo: more...
604 default: return graphQlName;
605 }
606}