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 "data" in objectValue ? objectValue["data"] : objectValue, 352 arguments, context, ec 353 ); 354 } catch(GQLDExecutionException e) { 355 auto ret = Json.emptyObject(); 356 ret[Constants.data] = Json(null); 357 ret[Constants.errors] = Json.emptyArray(); 358 ret.insertError(e.msg, ec.path); 359 return ret; 360 } 361 362 auto retType = this.schema.getReturnType(objectType, 363 field.aka.empty ? field.name : field.aka 364 ); 365 if(retType is null) { 366 this.executationTraceLog.logf("ERR %s %s", objectType.name, 367 field.name 368 ); 369 Json ret = Json.emptyObject(); 370 ret[Constants.errors] = Json.emptyArray(); 371 ret.insertError(format( 372 "No return type for member '%s' of type '%s' found", 373 field.name, objectType.name 374 )); 375 return ret; 376 } 377 this.executationTraceLog.logf("retType %s, de: %s", retType.name, de); 378 return this.executeSelectionSet(field.f.ss, retType, de, variables, 379 doc, context, ec 380 ); 381 } 382 383 Json executeSelectionSet(SelectionSet ss, GQLDType objectType, 384 Json objectValue, Json variables, Document doc, ref Con context, 385 ref ExecutionContext ec) 386 { 387 Json rslt; 388 if(GQLDMap map = objectType.toMap()) { 389 this.executationTraceLog.log("MMMMMAP %s %s", map.name, ss !is null); 390 enforce(ss !is null && ss.sel !is null, format( 391 "ss null %s, ss.sel null %s", ss is null, 392 (ss is null) ? true : ss.sel is null)); 393 rslt = this.executeSelections(ss.sel, map, objectValue, variables, 394 doc, context, ec 395 ); 396 } else if(GQLDNonNull nonNullType = objectType.toNonNull()) { 397 this.executationTraceLog.logf("NonNull %s objectValue %s", 398 nonNullType.elementType.name, objectValue 399 ); 400 rslt = this.executeSelectionSet(ss, nonNullType.elementType, 401 objectValue, variables, doc, context, ec 402 ); 403 if(rslt.dataIsNull()) { 404 this.executationTraceLog.logf("%s", rslt); 405 rslt.insertError("NonNull was null", ec.path); 406 } 407 } else if(GQLDNullable nullType = objectType.toNullable()) { 408 this.executationTraceLog.logf("NNNNULLABLE %s %s", nullType.name, 409 objectValue); 410 this.executationTraceLog.logf("IIIIIS EMPTY %s objectValue %s", 411 objectValue.dataIsEmpty(), objectValue 412 ); 413 if(objectValue.type == Json.Type.null_ || 414 (objectValue.type == Json.Type.object && 415 ((nullType.elementType.toList && "data" !in objectValue) 416 || objectValue.dataIsNull))) { 417 if(objectValue.type != Json.Type.object) { 418 objectValue = Json.emptyObject(); 419 } 420 objectValue["data"] = null; 421 objectValue.remove(Constants.errors); 422 rslt = objectValue; 423 } else { 424 rslt = this.executeSelectionSet(ss, nullType.elementType, 425 objectValue, variables, doc, context, ec 426 ); 427 } 428 } else if(GQLDList list = objectType.toList()) { 429 this.executationTraceLog.logf("LLLLLIST %s objectValue %s", 430 list.name, objectValue); 431 rslt = this.executeList(ss, list, objectValue, variables, doc, 432 context, ec 433 ); 434 } else if(GQLDScalar scalar = objectType.toScalar()) { 435 rslt = objectValue; 436 } 437 438 return rslt; 439 } 440 441 private void toRun(SelectionSet ss, GQLDType elemType, Json item, 442 Json variables, ref Json ret, Document doc, ref Con context, 443 ref ExecutionContext ec) 444 @trusted 445 { 446 this.executationTraceLog.logf("ET: %s, item %s", elemType.name, 447 item 448 ); 449 Json tmp = this.executeSelectionSet(ss, elemType, item, variables, 450 doc, context, ec 451 ); 452 if(tmp.type == Json.Type.object) { 453 if("data" in tmp) { 454 ret["data"] ~= tmp["data"]; 455 } 456 foreach(err; tmp[Constants.errors]) { 457 ret[Constants.errors] ~= err; 458 } 459 } else if(!tmp.dataIsEmpty() && tmp.isScalar()) { 460 ret["data"] ~= tmp; 461 } 462 } 463 464 Json executeList(SelectionSet ss, GQLDList objectType, 465 Json objectValue, Json variables, Document doc, ref Con context, 466 ref ExecutionContext ec) 467 @trusted 468 { 469 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 470 objectType.name, objectValue, variables 471 ); 472 assert("data" in objectValue, objectValue.toString()); 473 GQLDType elemType = objectType.elementType; 474 this.executationTraceLog.logf("elemType %s", elemType); 475 Json ret = returnTemplate(); 476 ret["data"] = Json.emptyArray(); 477 if(this.options.asyncList == AsyncList.yes) { 478 Task[] tasks; 479 foreach(Json item; 480 objectValue["data"].type == Json.Type.array 481 ? objectValue["data"] 482 : Json.emptyArray() 483 ) 484 { 485 tasks ~= runTask({ 486 () nothrow { 487 try { 488 auto newEC = ec.dup; 489 this.toRun(ss, elemType, item, variables, ret, doc, 490 context, newEC 491 ); 492 } catch(Exception e) { 493 try { 494 this.executationTraceLog.errorf("Error in task %s" 495 , e.toString()); 496 } catch(Exception f) { 497 } 498 } 499 }(); 500 }); 501 } 502 foreach(task; tasks) { 503 task.join(); 504 } 505 } else { 506 size_t idx; 507 foreach(Json item; 508 objectValue["data"].type == Json.Type.array 509 ? objectValue["data"] 510 : Json.emptyArray() 511 ) 512 { 513 ec.path ~= PathElement(idx); 514 ++idx; 515 scope(exit) { 516 ec.path.popBack(); 517 } 518 this.toRun(ss, elemType, item, variables, ret, doc, context, ec); 519 } 520 } 521 return ret; 522 } 523 } 524 525 import graphql.uda; 526 import std.datetime : DateTime; 527 528 private DateTime fromStringImpl(string s) { 529 return DateTime.fromISOExtString(s); 530 } 531 532 @GQLDUda(TypeKind.OBJECT) 533 private struct Query { 534 535 GQLDCustomLeaf!(DateTime, toStringImpl, fromStringImpl) current(); 536 } 537 538 private class Schema { 539 Query queryTyp; 540 } 541 542 unittest { 543 import graphql.schema.typeconversions; 544 import graphql.traits; 545 import std.datetime : DateTime; 546 547 alias a = collectTypes!(Schema); 548 alias exp = AliasSeq!(Schema, Query, string, long, bool, 549 GQLDCustomLeaf!(DateTime, toStringImpl, fromStringImpl)); 550 //static assert(is(a == exp), format("\n%s\n%s", a.stringof, exp.stringof)); 551 552 //pragma(msg, InheritedClasses!Schema); 553 554 //auto g = new GraphQLD!(Schema,int)(); 555 }