1 module graphql.graphql; 2 3 import std.array : array, front, empty; 4 import std.stdio; 5 import std.experimental.logger; 6 import std.traits; 7 import std.meta : AliasSeq; 8 import std.format : format; 9 import std.exception : enforce; 10 11 import vibe.core.core; 12 import vibe.data.json; 13 14 import graphql.argumentextractor; 15 import graphql.ast; 16 import graphql.builder; 17 import graphql.constants; 18 import graphql.directives; 19 import graphql.helper; 20 import graphql.schema.resolver; 21 import graphql.schema.types; 22 import graphql.tokenmodule; 23 24 @safe: 25 26 enum AsyncList { 27 no, 28 yes 29 } 30 31 struct GQLDOptions { 32 AsyncList asyncList; 33 } 34 35 struct DefaultContext { 36 } 37 38 class GQLDExecutionException : Exception { 39 this(string msg, string f = __FILE__, int l = __LINE__) { 40 super(msg, f, l); 41 this.line = l; 42 } 43 } 44 45 class GraphQLD(T, QContext = DefaultContext) { 46 alias Con = QContext; 47 alias QueryResolver = Json delegate(string name, Json parent, 48 Json args, ref Con context) @safe; 49 50 alias Schema = GQLDSchema!(T); 51 immutable GQLDOptions options; 52 53 Schema schema; 54 55 // the logger to use 56 Logger executationTraceLog; 57 Logger defaultResolverLog; 58 Logger resolverLog; 59 60 // [Type][field] 61 QueryResolver[string][string] resolver; 62 QueryResolver defaultResolver; 63 64 this(GQLDOptions options = GQLDOptions.init) { 65 this.options = options; 66 this.schema = toSchema!(T)(); 67 this.executationTraceLog = new MultiLogger(LogLevel.off); 68 this.defaultResolverLog = new MultiLogger(LogLevel.off); 69 this.resolverLog = new MultiLogger(LogLevel.off); 70 71 this.defaultResolver = delegate(string name, Json parent, Json args, 72 ref Con context) 73 { 74 import std.format; 75 this.defaultResolverLog.logf("name: %s, parent: %s, args: %s", 76 name, parent, args 77 ); 78 Json ret = Json.emptyObject(); 79 if(parent.type != Json.Type.null_ && name in parent) { 80 ret["data"] = Json.emptyObject(); 81 ret["data"] = parent[name]; 82 } else { 83 ret[Constants.errors] = Json.emptyArray(); 84 ret.insertError(format( 85 "no field name '%s' found on type '%s'", 86 name, 87 parent.getWithDefault!string("__typename") 88 ) 89 ); 90 } 91 this.defaultResolverLog.logf("default ret %s", ret); 92 return ret; 93 }; 94 95 setDefaultSchemaResolver(this); 96 } 97 98 void setResolver(string first, string second, QueryResolver resolver) { 99 import std.exception : enforce; 100 if(first !in this.resolver) { 101 this.resolver[first] = QueryResolver[string].init; 102 } 103 enforce(second !in this.resolver[first]); 104 this.resolver[first][second] = resolver; 105 } 106 107 Json resolve(string type, string field, Json parent, Json args, 108 ref Con context) 109 { 110 Json defaultArgs = this.getDefaultArguments(type, field); 111 Json joinedArgs = joinJson!(JoinJsonPrecedence.a)(args, defaultArgs); 112 this.resolverLog.logf("%s %s %s %s %s %s", type, field, 113 defaultArgs, parent, args, joinedArgs 114 ); 115 if(type !in this.resolver) { 116 return defaultResolver(field, parent, joinedArgs, context); 117 } else if(field !in this.resolver[type]) { 118 return defaultResolver(field, parent, joinedArgs, context); 119 } else { 120 return this.resolver[type][field](field, parent, joinedArgs, 121 context 122 ); 123 } 124 } 125 126 Json getDefaultArgumentImpl(string typename, Type)(string type, 127 string field) 128 { 129 static if(isAggregateType!Type) { 130 if(typename == type) { 131 switch(field) { 132 static foreach(mem; __traits(allMembers, Type)) { 133 static if(isCallable!( 134 __traits(getMember, Type, mem)) 135 ) 136 { 137 case mem: { 138 alias parNames = ParameterIdentifierTuple!( 139 __traits(getMember, Type, mem) 140 ); 141 alias parDef = ParameterDefaultValueTuple!( 142 __traits(getMember, Type, mem) 143 ); 144 145 Json ret = Json.emptyObject(); 146 static foreach(i; 0 .. parNames.length) { 147 static if(!is(parDef[i] == void)) { 148 ret[parNames[i]] = 149 serializeToJson(parDef[i]); 150 } 151 } 152 return ret; 153 } 154 } 155 } 156 default: break; 157 } 158 } 159 } 160 return Json.init; 161 } 162 163 Json getDefaultArguments(string type, string field) { 164 import graphql.traits : collectTypes; 165 switch(type) { 166 static foreach(Type; collectTypes!(T)) {{ 167 case Type.stringof: { 168 Json tmp = getDefaultArgumentImpl!(Type.stringof, Type)( 169 type, field 170 ); 171 if(tmp.type != Json.Type.undefined 172 && tmp.type != Json.Type.null_) 173 { 174 return tmp; 175 } 176 } 177 }} 178 default: {} 179 } 180 // entryPoint == ["query", "mutation", "subscription"]; 181 switch(type) { 182 static foreach(entryPoint; FieldNameTuple!T) {{ 183 case entryPoint: { 184 Json tmp = getDefaultArgumentImpl!(entryPoint, 185 typeof(__traits(getMember, T, entryPoint))) 186 (type, field); 187 if(tmp.type != Json.Type.undefined 188 && tmp.type != Json.Type.null_) 189 { 190 return tmp; 191 } 192 } 193 }} 194 default: break; 195 } 196 defaultRet: 197 return Json.init; 198 } 199 200 Json execute(Document doc, Json variables, ref Con context) @trusted { 201 import std.algorithm.searching : canFind, find; 202 OperationDefinition[] ops = this.getOperations(doc); 203 204 auto selSet = ops 205 .find!(op => op.ruleSelection == OperationDefinitionEnum.SelSet); 206 if(!selSet.empty) { 207 if(ops.length > 1) { 208 throw new Exception( 209 "If SelectionSet the number of Operations must be 1" 210 ); 211 } 212 return this.executeOperation(selSet.front, variables, doc, context); 213 } 214 215 Json ret = returnTemplate(); 216 foreach(op; ops) { 217 Json tmp = this.executeOperation(op, variables, doc, context); 218 this.executationTraceLog.logf("%s\n%s\n%s", op.ruleSelection, ret, 219 tmp); 220 if(tmp.type == Json.Type.object && "data" in tmp) { 221 foreach(key, value; tmp["data"].byKeyValue()) { 222 if(key in ret["data"]) { 223 this.executationTraceLog.logf( 224 "key %s already present", key 225 ); 226 continue; 227 } 228 ret["data"][key] = value; 229 } 230 } 231 foreach(err; tmp[Constants.errors]) { 232 ret[Constants.errors] ~= err; 233 } 234 } 235 return ret; 236 } 237 238 static OperationDefinition[] getOperations(Document doc) { 239 import std.algorithm.iteration : map; 240 return opDefRange(doc).map!(op => op.def.op).array; 241 } 242 243 Json executeOperation(OperationDefinition op, Json variables, 244 Document doc, ref Con context) 245 { 246 bool dirSaysToContinue = continueAfterDirectives(op.d, variables); 247 if(!dirSaysToContinue) { 248 return returnTemplate(); 249 } 250 if(op.ruleSelection == OperationDefinitionEnum.SelSet 251 || op.ot.tok.type == TokenType.query) 252 { 253 return this.executeQuery(op, variables, doc, context); 254 } else if(op.ot.tok.type == TokenType.mutation) { 255 return this.executeMutation(op, variables, doc, context); 256 } else if(op.ot.tok.type == TokenType.subscription) { 257 assert(false, "Subscription not supported yet"); 258 } 259 assert(false, "Unexpected"); 260 } 261 262 Json executeMutation(OperationDefinition op, Json variables, 263 Document doc, ref Con context) 264 { 265 this.executationTraceLog.log("mutation"); 266 Json tmp = this.executeSelections(op.ss.sel, 267 this.schema.member["mutationType"], Json.emptyObject(), 268 variables, doc, context 269 ); 270 return tmp; 271 } 272 273 Json executeQuery(OperationDefinition op, Json variables, Document doc, 274 ref Con context) 275 { 276 this.executationTraceLog.log("query"); 277 Json tmp = this.executeSelections(op.ss.sel, 278 this.schema.member["queryType"], 279 Json.emptyObject(), variables, doc, context 280 ); 281 return tmp; 282 } 283 284 Json executeSelections(Selections sel, GQLDType objectType, 285 Json objectValue, Json variables, Document doc, ref Con context) 286 { 287 import graphql.traits : interfacesForType; 288 Json ret = returnTemplate(); 289 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 290 objectType.name, objectValue, variables); 291 this.executationTraceLog.logf("TN: %s", interfacesForType!(T)( 292 objectValue 293 .getWithDefault!string("data.__typename", "__typename") 294 )); 295 foreach(FieldRangeItem field; 296 fieldRangeArr( 297 sel, 298 doc, 299 interfacesForType!(T)(objectValue.getWithDefault!string( 300 "data.__typename", "__typename") 301 ), 302 variables) 303 ) 304 { 305 //Json args = getArguments(field, variables); 306 bool dirSaysToContinue = continueAfterDirectives( 307 field.f.dirs, variables); 308 309 Json rslt = dirSaysToContinue 310 ? this.executeFieldSelection(field, objectType, 311 objectValue, variables, doc, context 312 ) 313 : Json.emptyObject(); 314 315 ret.insertPayload(field.name, rslt); 316 } 317 return ret; 318 } 319 320 Json executeFieldSelection(FieldRangeItem field, GQLDType objectType, 321 Json objectValue, Json variables, Document doc, ref Con context) 322 { 323 this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s", 324 field.name, objectType.name, objectValue, variables 325 ); 326 Json arguments = getArguments(field, variables); 327 //writefln("var %s\narg %s", variables, arguments); 328 Json de; 329 try { 330 de = this.resolve(objectType.name, field.name, 331 "data" in objectValue ? objectValue["data"] : objectValue, 332 arguments, context 333 ); 334 } catch(GQLDExecutionException e) { 335 auto ret = Json.emptyObject(); 336 ret[Constants.errors] = Json.emptyArray(); 337 ret.insertError(e.msg); 338 return ret; 339 } 340 if(de.dataIsEmpty()) { 341 return de; 342 } 343 auto retType = this.schema.getReturnType(objectType, field.name); 344 if(retType is null) { 345 this.executationTraceLog.logf("ERR %s %s", objectType.name, 346 field.name 347 ); 348 Json ret = Json.emptyObject(); 349 ret[Constants.errors] = Json.emptyArray(); 350 ret.insertError(format( 351 "No return type for member '%s' of type '%s' found", 352 field.name, objectType.name 353 )); 354 return ret; 355 } 356 this.executationTraceLog.logf("retType %s, de: %s", retType.name, de); 357 return this.executeSelectionSet(field.f.ss, retType, de, variables, 358 doc, context 359 ); 360 } 361 362 Json executeSelectionSet(SelectionSet ss, GQLDType objectType, 363 Json objectValue, Json variables, Document doc, ref Con context) 364 { 365 Json rslt; 366 if(GQLDMap map = objectType.toMap()) { 367 this.executationTraceLog.logf("map %s %s", map.name, ss !is null); 368 enforce(ss !is null && ss.sel !is null, format( 369 "ss null %s, ss.sel null %s", ss is null, 370 (ss is null) ? true : ss.sel is null)); 371 rslt = this.executeSelections(ss.sel, map, objectValue, variables, 372 doc, context 373 ); 374 } else if(GQLDNonNull nonNullType = objectType.toNonNull()) { 375 this.executationTraceLog.logf("NonNull %s", 376 nonNullType.elementType.name 377 ); 378 rslt = this.executeSelectionSet(ss, nonNullType.elementType, 379 objectValue, variables, doc, context 380 ); 381 if(rslt.dataIsNull()) { 382 this.executationTraceLog.logf("%s", rslt); 383 rslt.insertError("NonNull was null"); 384 } 385 } else if(GQLDNullable nullType = objectType.toNullable()) { 386 this.executationTraceLog.logf("nullable %s", nullType.name); 387 rslt = this.executeSelectionSet(ss, nullType.elementType, 388 objectValue, variables, doc, context 389 ); 390 this.executationTraceLog.logf("IIIIIS EMPTY %s rslt %s", 391 rslt.dataIsEmpty(), rslt 392 ); 393 if(rslt.dataIsEmpty()) { 394 rslt["data"] = null; 395 rslt.remove(Constants.errors); 396 } 397 } else if(GQLDList list = objectType.toList()) { 398 this.executationTraceLog.logf("list %s", list.name); 399 rslt = this.executeList(ss, list, objectValue, variables, doc, 400 context 401 ); 402 } else if(GQLDScalar scalar = objectType.toScalar()) { 403 rslt = objectValue; 404 } 405 406 return rslt; 407 } 408 409 private void toRun(SelectionSet ss, GQLDType elemType, Json item, 410 Json variables, ref Json ret, Document doc, ref Con context) 411 @trusted 412 { 413 this.executationTraceLog.logf("ET: %s, item %s", elemType.name, 414 item 415 ); 416 Json tmp = this.executeSelectionSet(ss, elemType, item, variables, 417 doc, context 418 ); 419 if(tmp.type == Json.Type.object) { 420 if("data" in tmp) { 421 ret["data"] ~= tmp["data"]; 422 } 423 foreach(err; tmp[Constants.errors]) { 424 ret[Constants.errors] ~= err; 425 } 426 } else if(!tmp.dataIsEmpty() && tmp.isScalar()) { 427 ret["data"] ~= tmp; 428 } 429 } 430 431 Json executeList(SelectionSet ss, GQLDList objectType, 432 Json objectValue, Json variables, Document doc, ref Con context) 433 @trusted 434 { 435 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 436 objectType.name, objectValue, variables 437 ); 438 assert("data" in objectValue, objectValue.toString()); 439 GQLDType elemType = objectType.elementType; 440 this.executationTraceLog.logf("elemType %s", elemType); 441 Json ret = returnTemplate(); 442 ret["data"] = Json.emptyArray(); 443 if(this.options.asyncList == AsyncList.yes) { 444 Task[] tasks; 445 foreach(Json item; 446 objectValue["data"].type == Json.Type.array 447 ? objectValue["data"] 448 : Json.emptyArray() 449 ) 450 { 451 tasks ~= runTask({ 452 this.toRun(ss, elemType, item, variables, ret, doc, 453 context 454 ); 455 }); 456 } 457 foreach(task; tasks) { 458 task.join(); 459 } 460 } else { 461 foreach(Json item; 462 objectValue["data"].type == Json.Type.array 463 ? objectValue["data"] 464 : Json.emptyArray() 465 ) 466 { 467 this.toRun(ss, elemType, item, variables, ret, doc, context); 468 } 469 } 470 return ret; 471 } 472 } 473 474 import graphql.uda; 475 476 @GQLDUda(TypeKind.OBJECT) 477 private struct Query { 478 import std.datetime : DateTime; 479 480 GQLDCustomLeaf!DateTime current(); 481 } 482 483 private class Schema { 484 Query queryTyp; 485 } 486 487 unittest { 488 import graphql.schema.typeconversions; 489 import graphql.traits; 490 491 alias a = collectTypes!(Schema); 492 // static assert(is(a == AliasSeq!(Schema, Query, string, long, bool))); 493 494 //pragma(msg, InheritedClasses!Schema); 495 496 //auto g = new GraphQLD!(Schema,int)(); 497 }