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