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