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 initializeDefaultArgFunctions(); 113 } 114 115 void setResolver(string first, string second, QueryResolver resolver) { 116 import std.exception : enforce; 117 if(first !in this.resolver) { 118 this.resolver[first] = QueryResolver[string].init; 119 } 120 enforce(second !in this.resolver[first]); 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(isCallable!( 152 __traits(getMember, Type, mem)) 153 ) 154 { 155 case mem: { 156 alias parNames = ParameterIdentifierTuple!( 157 __traits(getMember, Type, mem) 158 ); 159 alias parDef = ParameterDefaultValueTuple!( 160 __traits(getMember, Type, mem) 161 ); 162 163 Json ret = Json.emptyObject(); 164 static foreach(i; 0 .. parNames.length) { 165 static if(!is(parDef[i] == void)) { 166 ret[parNames[i]] = 167 serializeToJson(parDef[i]); 168 } 169 } 170 return ret; 171 } 172 } 173 } 174 default: break; 175 } 176 } 177 return Json.init; 178 } 179 180 private { 181 alias _defaultArgFn = Json function(string) @safe; 182 _defaultArgFn[string] _defaultArgFunctions; 183 184 void initializeDefaultArgFunctions() 185 { 186 import graphql.traits : execForAllTypes; 187 static void setupItems(T)(ref _defaultArgFn[string] items) { 188 items[T.stringof] = &getDefaultArgumentImpl!T; 189 } 190 execForAllTypes!(T, setupItems)(_defaultArgFunctions); 191 // add entry points 192 foreach(entryPoint; FieldNameTuple!T) { 193 _defaultArgFunctions[entryPoint] = 194 &getDefaultArgumentImpl!(typeof(__traits(getMember, T, 195 entryPoint))); 196 } 197 } 198 } 199 200 Json getDefaultArguments(string type, string field) { 201 if(auto f = type in _defaultArgFunctions) { 202 auto tmp = (*f)(field); 203 if(tmp.type != Json.Type.undefined && tmp.type != Json.Type.null_) { 204 return tmp; 205 } 206 } 207 return Json.init; 208 } 209 210 Json execute(Document doc, Json variables, ref Con context) @trusted { 211 import std.algorithm.searching : canFind, find; 212 OperationDefinition[] ops = this.getOperations(doc); 213 ExecutionContext ec; 214 215 Json ret = Json.emptyObject(); 216 ret[Constants.data] = Json.emptyObject(); 217 foreach(op; ops) { 218 Json tmp = this.executeOperation(op, variables, doc, context, ec); 219 this.executationTraceLog.logf("%s\n%s\n%s", op.ruleSelection, ret, 220 tmp); 221 if(tmp.type == Json.Type.object && Constants.data in tmp) { 222 foreach(key, value; tmp[Constants.data].byKeyValue()) { 223 if(key in ret[Constants.data]) { 224 this.executationTraceLog.logf( 225 "key %s already present", key 226 ); 227 continue; 228 } 229 ret[Constants.data][key] = value; 230 } 231 } 232 this.executationTraceLog.logf("%s", tmp); 233 if(Constants.errors in tmp && !tmp[Constants.errors].dataIsEmpty()) 234 { 235 ret[Constants.errors] = Json.emptyArray(); 236 } 237 foreach(err; tmp[Constants.errors]) { 238 ret[Constants.errors] ~= err; 239 } 240 } 241 return ret; 242 } 243 244 static OperationDefinition[] getOperations(Document doc) { 245 import std.algorithm.iteration : map; 246 return opDefRange(doc).map!(op => op.def.op).array; 247 } 248 249 Json executeOperation(OperationDefinition op, Json variables, 250 Document doc, ref Con context, ref ExecutionContext ec) 251 { 252 ec.path ~= PathElement( 253 op.name.value.empty ? "SelectionSet" : op.name.value); 254 scope(exit) { 255 ec.path.popBack(); 256 } 257 bool dirSaysToContinue = continueAfterDirectives(op.d, variables); 258 if(!dirSaysToContinue) { 259 return returnTemplate(); 260 } 261 if(op.ruleSelection == OperationDefinitionEnum.SelSet 262 || op.ot.tok.type == TokenType.query) 263 { 264 return this.executeQuery(op, variables, doc, context, ec); 265 } else if(op.ot.tok.type == TokenType.mutation) { 266 return this.executeMutation(op, variables, doc, context, ec); 267 } else if(op.ot.tok.type == TokenType.subscription) { 268 assert(false, "Subscription not supported yet"); 269 } 270 assert(false, "Unexpected"); 271 } 272 273 Json executeMutation(OperationDefinition op, Json variables, 274 Document doc, ref Con context, ref ExecutionContext ec) 275 { 276 this.executationTraceLog.log("mutation"); 277 Json tmp = this.executeSelections(op.ss.sel, 278 this.schema.member["mutationType"], Json.emptyObject(), 279 variables, doc, context, ec 280 ); 281 return tmp; 282 } 283 284 Json executeQuery(OperationDefinition op, Json variables, Document doc, 285 ref Con context, ref ExecutionContext ec) 286 { 287 this.executationTraceLog.log("query"); 288 Json tmp = this.executeSelections(op.ss.sel, 289 this.schema.member["queryType"], 290 Json.emptyObject(), variables, doc, context, ec 291 ); 292 return tmp; 293 } 294 295 Json executeSelections(Selections sel, GQLDType objectType, 296 Json objectValue, Json variables, Document doc, ref Con context, 297 ref ExecutionContext ec) 298 { 299 import graphql.traits : interfacesForType; 300 Json ret = returnTemplate(); 301 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 302 objectType.name, objectValue, variables); 303 this.executationTraceLog.logf("TN: %s", interfacesForType!(T)( 304 objectValue 305 .getWithDefault!string("data.__typename", "__typename") 306 )); 307 foreach(FieldRangeItem field; 308 fieldRangeArr( 309 sel, 310 doc, 311 interfacesForType!(T)(objectValue.getWithDefault!string( 312 "data.__typename", "__typename") 313 ), 314 variables) 315 ) 316 { 317 //Json args = getArguments(field, variables); 318 bool dirSaysToContinue = continueAfterDirectives( 319 field.f.dirs, variables); 320 321 Json rslt = dirSaysToContinue 322 ? this.executeFieldSelection(field, objectType, 323 objectValue, variables, doc, context, ec 324 ) 325 : Json.emptyObject(); 326 327 const string name = field.f.name.name.value; 328 ret.insertPayload(name, rslt); 329 } 330 return ret; 331 } 332 333 Json executeFieldSelection(FieldRangeItem field, GQLDType objectType, 334 Json objectValue, Json variables, Document doc, ref Con context, 335 ref ExecutionContext ec) 336 { 337 ec.path ~= PathElement(field.f.name.name.value); 338 scope(exit) { 339 ec.path.popBack(); 340 } 341 this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s", 342 //this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s", 343 field.name, objectType.name, objectValue, variables 344 ); 345 Json arguments = getArguments(field, variables); 346 //writefln("field %s\nobj %s\nvar %s\narg %s", field.name, objectValue, 347 // variables, arguments); 348 Json de; 349 try { 350 de = this.resolve(objectType.name, 351 field.aka.empty ? field.name : field.aka, 352 "data" in objectValue ? objectValue["data"] : objectValue, 353 arguments, context, ec 354 ); 355 } catch(GQLDExecutionException e) { 356 auto ret = Json.emptyObject(); 357 ret[Constants.data] = Json(null); 358 ret[Constants.errors] = Json.emptyArray(); 359 ret.insertError(e.msg, ec.path); 360 return ret; 361 } 362 363 auto retType = this.schema.getReturnType(objectType, 364 field.aka.empty ? field.name : field.aka 365 ); 366 if(retType is null) { 367 this.executationTraceLog.logf("ERR %s %s", objectType.name, 368 field.name 369 ); 370 Json ret = Json.emptyObject(); 371 ret[Constants.errors] = Json.emptyArray(); 372 ret.insertError(format( 373 "No return type for member '%s' of type '%s' found", 374 field.name, objectType.name 375 )); 376 return ret; 377 } 378 this.executationTraceLog.logf("retType %s, de: %s", retType.name, de); 379 return this.executeSelectionSet(field.f.ss, retType, de, variables, 380 doc, context, ec 381 ); 382 } 383 384 Json executeSelectionSet(SelectionSet ss, GQLDType objectType, 385 Json objectValue, Json variables, Document doc, ref Con context, 386 ref ExecutionContext ec) 387 { 388 Json rslt; 389 if(GQLDMap map = objectType.toMap()) { 390 this.executationTraceLog.log("MMMMMAP %s %s", map.name, ss !is null); 391 enforce(ss !is null && ss.sel !is null, format( 392 "ss null %s, ss.sel null %s", ss is null, 393 (ss is null) ? true : ss.sel is null)); 394 rslt = this.executeSelections(ss.sel, map, objectValue, variables, 395 doc, context, ec 396 ); 397 } else if(GQLDNonNull nonNullType = objectType.toNonNull()) { 398 this.executationTraceLog.logf("NonNull %s objectValue %s", 399 nonNullType.elementType.name, objectValue 400 ); 401 rslt = this.executeSelectionSet(ss, nonNullType.elementType, 402 objectValue, variables, doc, context, ec 403 ); 404 if(rslt.dataIsNull()) { 405 this.executationTraceLog.logf("%s", rslt); 406 rslt.insertError("NonNull was null", ec.path); 407 } 408 } else if(GQLDNullable nullType = objectType.toNullable()) { 409 this.executationTraceLog.logf("NNNNULLABLE %s %s", nullType.name, 410 objectValue); 411 this.executationTraceLog.logf("IIIIIS EMPTY %s objectValue %s", 412 objectValue.dataIsEmpty(), objectValue 413 ); 414 if(objectValue.dataIsEmpty()) { 415 if(objectValue.type != Json.Type.object) { 416 objectValue = Json.emptyObject(); 417 } 418 objectValue["data"] = null; 419 objectValue.remove(Constants.errors); 420 rslt = objectValue; 421 } else { 422 rslt = this.executeSelectionSet(ss, nullType.elementType, 423 objectValue, variables, doc, context, ec 424 ); 425 } 426 } else if(GQLDList list = objectType.toList()) { 427 this.executationTraceLog.logf("LLLLLIST %s objectValue %s", 428 list.name, objectValue); 429 rslt = this.executeList(ss, list, objectValue, variables, doc, 430 context, ec 431 ); 432 } else if(GQLDScalar scalar = objectType.toScalar()) { 433 rslt = objectValue; 434 } 435 436 return rslt; 437 } 438 439 private void toRun(SelectionSet ss, GQLDType elemType, Json item, 440 Json variables, ref Json ret, Document doc, ref Con context, 441 ref ExecutionContext ec) 442 @trusted 443 { 444 this.executationTraceLog.logf("ET: %s, item %s", elemType.name, 445 item 446 ); 447 Json tmp = this.executeSelectionSet(ss, elemType, item, variables, 448 doc, context, ec 449 ); 450 if(tmp.type == Json.Type.object) { 451 if("data" in tmp) { 452 ret["data"] ~= tmp["data"]; 453 } 454 foreach(err; tmp[Constants.errors]) { 455 ret[Constants.errors] ~= err; 456 } 457 } else if(!tmp.dataIsEmpty() && tmp.isScalar()) { 458 ret["data"] ~= tmp; 459 } 460 } 461 462 Json executeList(SelectionSet ss, GQLDList objectType, 463 Json objectValue, Json variables, Document doc, ref Con context, 464 ref ExecutionContext ec) 465 @trusted 466 { 467 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 468 objectType.name, objectValue, variables 469 ); 470 assert("data" in objectValue, objectValue.toString()); 471 GQLDType elemType = objectType.elementType; 472 this.executationTraceLog.logf("elemType %s", elemType); 473 Json ret = returnTemplate(); 474 ret["data"] = Json.emptyArray(); 475 if(this.options.asyncList == AsyncList.yes) { 476 Task[] tasks; 477 foreach(Json item; 478 objectValue["data"].type == Json.Type.array 479 ? objectValue["data"] 480 : Json.emptyArray() 481 ) 482 { 483 tasks ~= runTask({ 484 auto newEC = ec.dup; 485 this.toRun(ss, elemType, item, variables, ret, doc, 486 context, newEC 487 ); 488 }); 489 } 490 foreach(task; tasks) { 491 task.join(); 492 } 493 } else { 494 size_t idx; 495 foreach(Json item; 496 objectValue["data"].type == Json.Type.array 497 ? objectValue["data"] 498 : Json.emptyArray() 499 ) 500 { 501 ec.path ~= PathElement(idx); 502 ++idx; 503 scope(exit) { 504 ec.path.popBack(); 505 } 506 this.toRun(ss, elemType, item, variables, ret, doc, context, ec); 507 } 508 } 509 return ret; 510 } 511 } 512 513 import graphql.uda; 514 515 @GQLDUda(TypeKind.OBJECT) 516 private struct Query { 517 import std.datetime : DateTime; 518 519 GQLDCustomLeaf!DateTime current(); 520 } 521 522 private class Schema { 523 Query queryTyp; 524 } 525 526 unittest { 527 import graphql.schema.typeconversions; 528 import graphql.traits; 529 import std.datetime : DateTime; 530 531 alias a = collectTypes!(Schema); 532 alias exp = AliasSeq!(Schema, Query, string, long, bool, 533 GQLDCustomLeaf!(DateTime, toStringImpl)); 534 //static assert(is(a == exp), format("\n%s\n%s", a.stringof, exp.stringof)); 535 536 //pragma(msg, InheritedClasses!Schema); 537 538 //auto g = new GraphQLD!(Schema,int)(); 539 }