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