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.format : format; 8 9 import vibe.data.json; 10 11 import graphql.builder; 12 import graphql.ast; 13 import graphql.helper; 14 import graphql.tokenmodule; 15 import graphql.argumentextractor; 16 import graphql.schema.types; 17 import graphql.schema.resolver; 18 19 @safe: 20 21 struct DefaultContext { 22 } 23 24 class GraphQLD(T, QContext = DefaultContext) { 25 alias Con = QContext; 26 alias QueryResolver = Json delegate(string name, Json parent, 27 Json args, ref Con context) @safe; 28 29 alias Schema = GQLDSchema!(T); 30 31 Schema schema; 32 33 // the logger to use 34 Logger executationTraceLog; 35 Logger defaultResolverLog; 36 Logger resolverLog; 37 38 // [Type][field] 39 QueryResolver[string][string] resolver; 40 QueryResolver defaultResolver; 41 42 this() { 43 this.schema = toSchema!(T)(); 44 this.executationTraceLog = new MultiLogger(LogLevel.off); 45 this.defaultResolverLog = new MultiLogger(LogLevel.off); 46 this.resolverLog = new MultiLogger(LogLevel.off); 47 48 this.defaultResolver = delegate(string name, Json parent, Json args, 49 ref Con context) 50 { 51 import std.format; 52 this.defaultResolverLog.logf("name: %s, parent: %s, args: %s", 53 name, parent, args 54 ); 55 Json ret = Json.emptyObject(); 56 if(parent.type != Json.Type.null_ && name in parent) { 57 ret["data"] = Json.emptyObject(); 58 ret["data"] = parent[name]; 59 } else { 60 ret["error"] = Json.emptyArray(); 61 ret["error"] = Json(format("no field name '%s' found", 62 name) 63 ); 64 } 65 this.defaultResolverLog.logf("default ret %s", ret); 66 return ret; 67 }; 68 69 setDefaultSchemaResolver(this); 70 writeln(this.schema.toString()); 71 } 72 73 void setResolver(string first, string second, QueryResolver resolver) { 74 if(first !in this.resolver) { 75 this.resolver[first] = QueryResolver[string].init; 76 } 77 this.resolver[first][second] = resolver; 78 } 79 80 Json resolve(string type, string field, Json parent, Json args, 81 ref Con context) 82 { 83 Json defaultArgs = this.getDefaultArguments(type, field); 84 Json joinedArgs = joinJson(args, defaultArgs); 85 this.resolverLog.logf("%s %s %s %s %s %s", type, field, 86 defaultArgs, parent, args, joinedArgs 87 ); 88 if(type !in this.resolver) { 89 return defaultResolver(field, parent, joinedArgs, context); 90 } else if(field !in this.resolver[type]) { 91 return defaultResolver(field, parent, joinedArgs, context); 92 } else { 93 return this.resolver[type][field](field, parent, joinedArgs, 94 context 95 ); 96 } 97 } 98 99 Json getDefaultArgumentImpl(string typename, Type)(string type, 100 string field) 101 { 102 static if(isAggregateType!Type) { 103 if(typename == type) { 104 switch(field) { 105 static foreach(mem; __traits(allMembers, Type)) { 106 static if(isCallable!( 107 __traits(getMember, Type, mem)) 108 ) 109 { 110 case mem: { 111 alias parNames = ParameterIdentifierTuple!( 112 __traits(getMember, Type, mem) 113 ); 114 alias parDef = ParameterDefaultValueTuple!( 115 __traits(getMember, Type, mem) 116 ); 117 118 Json ret = Json.emptyObject(); 119 static foreach(i; 0 .. parNames.length) { 120 static if(!is(parDef[i] == void)) { 121 ret[parNames[i]] = 122 serializeToJson(parDef[i]); 123 } 124 } 125 return ret; 126 } 127 } 128 } 129 default: break; 130 } 131 } 132 } 133 return Json.init; 134 } 135 136 Json getDefaultArguments(string type, string field) { 137 import graphql.traits : collectTypes; 138 switch(type) { 139 static foreach(Type; collectTypes!(T)) {{ 140 case Type.stringof: { 141 Json tmp = getDefaultArgumentImpl!(Type.stringof, Type)( 142 type, field 143 ); 144 if(tmp.type != Json.Type.undefined 145 && tmp.type != Json.Type.null_) 146 { 147 return tmp; 148 } 149 } 150 }} 151 default: {} 152 } 153 // entryPoint == ["query", "mutation", "subscription"]; 154 switch(type) { 155 static foreach(entryPoint; FieldNameTuple!T) {{ 156 case entryPoint: { 157 Json tmp = getDefaultArgumentImpl!(entryPoint, 158 typeof(__traits(getMember, T, entryPoint))) 159 (type, field); 160 if(tmp.type != Json.Type.undefined 161 && tmp.type != Json.Type.null_) 162 { 163 return tmp; 164 } 165 } 166 }} 167 default: break; 168 } 169 defaultRet: 170 return Json.init; 171 } 172 173 Json execute(Document doc, Json variables, ref Con context) @trusted { 174 import std.algorithm.searching : canFind, find; 175 OperationDefinition[] ops = this.getOperations(doc); 176 177 auto selSet = ops 178 .find!(op => op.ruleSelection == OperationDefinitionEnum.SelSet); 179 if(!selSet.empty) { 180 if(ops.length > 1) { 181 throw new Exception( 182 "If SelectionSet the number of Operations must be 1" 183 ); 184 } 185 return this.executeOperation(selSet.front, variables, doc, context); 186 } 187 188 Json ret = returnTemplate(); 189 foreach(op; ops) { 190 Json tmp = this.executeOperation(op, variables, doc, context); 191 this.executationTraceLog.logf("%s\n%s", ret, tmp); 192 if(canFind([OperationDefinitionEnum.OT_N, 193 OperationDefinitionEnum.OT_N_D, 194 OperationDefinitionEnum.OT_N_V, 195 OperationDefinitionEnum.OT_N_VD], op.ruleSelection)) 196 { 197 if(tmp.type == Json.Type.object && "data" in tmp) { 198 foreach(key, value; tmp["data"].byKeyValue()) { 199 if(key in ret["data"]) { 200 this.executationTraceLog.logf( 201 "key %s already present", key 202 ); 203 continue; 204 } 205 ret["data"][key] = value; 206 } 207 } 208 foreach(err; tmp["error"]) { 209 ret["error"] ~= err; 210 } 211 } 212 } 213 return ret; 214 } 215 216 static OperationDefinition[] getOperations(Document doc) { 217 import std.algorithm : map; 218 return opDefRange(doc).map!(op => op.def.op).array; 219 } 220 221 Json executeOperation(OperationDefinition op, Json variables, 222 Document doc, ref Con context) 223 { 224 if(op.ruleSelection == OperationDefinitionEnum.SelSet 225 || op.ot.tok.type == TokenType.query) 226 { 227 return this.executeQuery(op, variables, doc, context); 228 } else if(op.ot.tok.type == TokenType.mutation) { 229 return this.executeMutation(op, variables, doc, context); 230 } else if(op.ot.tok.type == TokenType.subscription) { 231 assert(false, "Subscription not supported yet"); 232 } 233 assert(false, "Unexpected"); 234 } 235 236 Json executeMutation(OperationDefinition op, Json variables, 237 Document doc, ref Con context) 238 { 239 log("mutation"); 240 Json tmp = this.executeSelections(op.ss.sel, 241 this.schema.member["mutationType"], Json.emptyObject(), 242 variables, doc, context 243 ); 244 return tmp; 245 } 246 247 Json executeQuery(OperationDefinition op, Json variables, Document doc, 248 ref Con context) 249 { 250 log("query"); 251 Json tmp = this.executeSelections(op.ss.sel, 252 this.schema.member["queryType"], 253 Json.emptyObject(), variables, doc, context 254 ); 255 return tmp; 256 } 257 258 Json executeSelections(Selections sel, GQLDType objectType, 259 Json objectValue, Json variables, Document doc, ref Con context) 260 { 261 import graphql.traits : interfacesForType; 262 Json ret = returnTemplate(); 263 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 264 objectType.name, objectValue, variables); 265 this.executationTraceLog.logf("TN: %s", interfacesForType!(T)( 266 objectValue 267 .getWithDefault!string("data.__typename", "__typename") 268 )); 269 foreach(FieldRangeItem field; 270 fieldRangeArr(sel, doc, interfacesForType!(T)(objectValue 271 .getWithDefault!string("data.__typename", "__typename"))) 272 ) 273 { 274 Json rslt = this.executeFieldSelection(field, objectType, 275 objectValue, variables, doc, context 276 ); 277 ret.insertPayload(field.name, rslt); 278 } 279 return ret; 280 } 281 282 Json executeFieldSelection(FieldRangeItem field, GQLDType objectType, 283 Json objectValue, Json variables, Document doc, ref Con context) 284 { 285 this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s", 286 field.name, objectType.name, objectValue, variables 287 ); 288 Json arguments = getArguments(field, variables); 289 Json de = this.resolve(objectType.name, field.name, 290 "data" in objectValue ? objectValue["data"] : objectValue, 291 arguments, context 292 ); 293 auto retType = this.schema.getReturnType(objectType, field.name); 294 if(retType is null) { 295 this.executationTraceLog.logf("ERR %s %s", objectType.name, 296 field.name 297 ); 298 Json ret = Json.emptyObject(); 299 ret["error"] = Json.emptyArray(); 300 ret["error"] ~= Json(format( 301 "No return type for member '%s' of type '%s' found", 302 field.name, objectType.name 303 )); 304 return ret; 305 } 306 this.executationTraceLog.logf("retType %s, de: %s", retType.name, de); 307 return this.executeSelectionSet(field.f.ss, retType, de, arguments, 308 doc, context 309 ); 310 } 311 312 Json executeSelectionSet(SelectionSet ss, GQLDType objectType, 313 Json objectValue, Json variables, Document doc, ref Con context) 314 { 315 Json rslt; 316 if(GQLDMap map = objectType.toMap()) { 317 this.executationTraceLog.logf("map %s %s", map.name, ss !is null); 318 rslt = this.executeSelections(ss.sel, map, objectValue, variables, 319 doc, context 320 ); 321 } else if(GQLDNonNull nonNullType = objectType.toNonNull()) { 322 this.executationTraceLog.logf("NonNull %s", 323 nonNullType.elementType.name 324 ); 325 rslt = this.executeSelectionSet(ss, nonNullType.elementType, 326 objectValue, variables, doc, context 327 ); 328 if(rslt.dataIsNull()) { 329 this.executationTraceLog.logf("%s", rslt); 330 rslt["error"] ~= Json("NonNull was null"); 331 } 332 } else if(GQLDNullable nullType = objectType.toNullable()) { 333 this.executationTraceLog.logf("nullable %s", nullType.name); 334 rslt = this.executeSelectionSet(ss, nullType.elementType, 335 objectValue, variables, doc, context 336 ); 337 this.executationTraceLog.logf("IIIIIS EMPTY %s rslt %s", 338 rslt.dataIsEmpty(), rslt 339 ); 340 if(rslt.dataIsEmpty()) { 341 rslt["data"] = null; 342 rslt.remove("error"); 343 } else { 344 } 345 } else if(GQLDList list = objectType.toList()) { 346 this.executationTraceLog.logf("list %s", list.name); 347 rslt = this.executeList(ss, list, objectValue, variables, doc, 348 context 349 ); 350 } else if(GQLDScalar scalar = objectType.toScalar()) { 351 rslt = objectValue; 352 } 353 354 return rslt; 355 } 356 357 Json executeList(SelectionSet ss, GQLDList objectType, 358 Json objectValue, Json variables, Document doc, ref Con context) 359 @trusted 360 { 361 this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s", 362 objectType.name, objectValue, variables 363 ); 364 assert("data" in objectValue, objectValue.toString()); 365 auto elemType = objectType.elementType; 366 this.executationTraceLog.logf("elemType %s", elemType); 367 Json ret = returnTemplate(); 368 ret["data"] = Json.emptyArray(); 369 foreach(Json item; 370 objectValue["data"].type == Json.Type.array 371 ? objectValue["data"] 372 : Json.emptyArray() 373 ) 374 { 375 this.executationTraceLog.logf("ET: %s, item %s", elemType.name, 376 item 377 ); 378 Json tmp = this.executeSelectionSet(ss, elemType, item, variables, 379 doc, context 380 ); 381 if(tmp.type == Json.Type.object) { 382 if("data" in tmp) { 383 ret["data"] ~= tmp["data"]; 384 } 385 foreach(err; tmp["error"]) { 386 ret["error"] ~= err; 387 } 388 } else if(!tmp.dataIsEmpty() && tmp.isScalar()) { 389 ret["data"] ~= tmp; 390 } 391 } 392 return ret; 393 } 394 395 Json getArguments(FieldRangeItem item, Json variables) { 396 auto ae = new ArgumentExtractor(variables); 397 ae.accept(cast(const(Field))item.f); 398 return ae.arguments; 399 } 400 }