1 module scorpion.repository; 2 3 import std.algorithm : map; 4 import std.conv : to; 5 import std..string : startsWith, join; 6 import std.traits : ReturnType, Parameters, ParameterIdentifierTuple, hasUDA, getUDAs; 7 8 import shark : Database, PrimaryKey; 9 10 import scorpion.entity : Entity, ExtendEntity; 11 12 /** 13 * Base interface for repositories. Every repository that implements 14 * this interface will be extended by scorpion and its methods implemented. 15 * Every method in the interface must have either the attribute @Select, 16 * @Insert, @Update or @Remove that indicates which action the repository 17 * will perform using the database. 18 * Example: 19 * --- 20 * @Entity("example") 21 * class Example { 22 * 23 * @PrimaryKey 24 * Integer a; 25 * 26 * String b; 27 * 28 * Time c; 29 * 30 * } 31 * 32 * interface ExampleRepository : Repository!Example { 33 * 34 * @Select 35 * Example[] selectAll(); 36 * 37 * @Insert 38 * void insert(Example); 39 * 40 * @Update 41 * void update(Example); 42 * 43 * @Remove 44 * void remove(Example); 45 * 46 * } 47 * --- 48 */ 49 interface Repository(T) {} 50 51 /** 52 * Indicates that one or more entities will be selected from 53 * the database. The return type of a method marked with the 54 * @Select atribute can be either T or T[], where T is the entity 55 * specified in the repository's declaration. 56 * Example: 57 * --- 58 * interface ExampleRepository : Repository!Example { 59 * 60 * @Select 61 * T[] selectAll(); 62 * 63 * @Select 64 * T selectOne(Integer id); 65 * 66 * } 67 * --- 68 */ 69 enum Select; 70 71 /** 72 * Indicates that a newly created entity will be inserted in 73 * the database. If the entity has one or more primary keys 74 * their values will be updated. 75 * Example: 76 * --- 77 * interface ExampleRepository : Repository!Example { 78 * 79 * @Insert 80 * void insert(Example entity); 81 * 82 * } 83 * --- 84 */ 85 enum Insert; 86 87 /** 88 * Indicates that an existing entity will be updated. 89 * The updated fields can be specified with the `Fields` 90 * attribute; if the attributes is not present all fields 91 * except the primary keys will be updated. 92 * If the entity does not have any primary key the `Where` 93 * attribute must also be present. 94 * Example: 95 * --- 96 * interface ExampleRepository : Repository!Example { 97 * 98 * @Update 99 * void update(Example entity); 100 * 101 * } 102 * --- 103 */ 104 enum Update; 105 106 /** 107 * Deletes one or more entities from the database. 108 * If the entity does not have any primary key the 109 * `Where` attribute must also be present. 110 * Example: 111 * --- 112 * interface ExampleRepository : Repository!Example { 113 * 114 * @Remove 115 * void remove(Example entity); 116 * 117 * } 118 * --- 119 * The name of the attribute id `Remove` and not 120 * `Delete` due to a conflict with the `Delete` HTTP 121 * method name and the fact that `delete` is a reserved 122 * keyword in D, thus methods could not be called simply 123 * `delete`. 124 */ 125 enum Remove; 126 127 /** 128 * Adds a `where` clause to the query. This attribute can be 129 * used with query that select, update and remove entities. 130 * It is possible to use method's arguments in the `where` 131 * clause by adding the dollar sign plus the index (starting 132 * from 0, obviously) of the variable. 133 * Example: 134 * --- 135 * interface ExampleRepository : Repository!Example { 136 * 137 * @Select 138 * @Where("b=$0") 139 * Example[] selectByB(string b); 140 * 141 * @Update 142 * @Where("b=$0 or b=$1") 143 * void update(string b0, string b1); 144 * 145 * } 146 * --- 147 */ 148 struct Where { string clause; } 149 150 /** 151 * Specifies in which order the result of a select query 152 * should be returned. 153 * It is possible to add one or more `OrderBy` attributes 154 * to the same method with different field names. 155 * Example: 156 * --- 157 * interface ExampleRepository : Repository!Example { 158 * 159 * @OrderBy("b") 160 * @OrderBy("a", OrderBy.desc) 161 * Example[] select(); 162 * 163 * } 164 * --- 165 */ 166 alias OrderBy = Database.Clause.Order.Field; 167 168 /** 169 * Specifies that the result of a select query should be 170 * returned randomly ordered. This action is usually performed 171 * directly from the database. 172 * It's not possible to use any other order attribute associated 173 * with this one. 174 */ 175 enum OrderByRandom; 176 177 /** 178 * Limits the number of results returned by a select query. 179 * It is possible, like in the `Where` attribute, to specify 180 * a value using the arguments by adding the dollar sign and 181 * the argument's index in the method. 182 * Example: 183 * --- 184 * interface ExampleRepository : Repository!Example { 185 * 186 * @Limit(5) 187 * Example[] select5(); 188 * 189 * @Limit("$0", "$1") 190 * Example[] selectInRange(size_t lowerLimit, size_t upperLimit); 191 * 192 * } 193 * --- 194 */ 195 struct Limit { 196 197 string lower, upper; 198 199 this(L, U)(L lower, U upper) if(__traits(compiles, to!string(lower)) && __traits(compiles, to!string(upper))) { 200 this.lower = lower.to!string; 201 this.upper = upper.to!string; 202 } 203 204 this(U)(U upper) if(__traits(compiles, to!string(upper))) { 205 this("0", upper); 206 } 207 208 } 209 210 /** 211 * Indicates which field(s) to update when updating an entity. 212 * This is useful when having an entity with a field that holds 213 * big data and and a small field that is updated frequently. 214 * By adding the `Fields` attribute the big data is not sent to 215 * the database and updated every time the small field is, saving 216 * time. 217 * Example: 218 * --- 219 * interface ExampleRepository : Repository!Example { 220 * 221 * @Update 222 * @Fields("b") 223 * void updateB(Example entity); 224 * 225 * @Update 226 * @Fields("c") 227 * void updateC(Example entity); 228 * 229 * } 230 * --- 231 */ 232 struct Fields { 233 234 string[] fields; 235 236 this(string[] fields...) { 237 this.fields = fields; 238 } 239 240 } 241 242 class DatabaseRepository(T:Repository!R, R) : T { 243 244 private enum __table = getUDAs!(R, Entity)[0].name; 245 private alias E = ExtendEntity!(R, __table); 246 247 private Database _database; 248 249 public this(Database database) { 250 _database = database; 251 } 252 253 mixin(extendInterface!(T, R)); 254 255 } 256 257 private string extendInterface(T, E)() { 258 string ret = ""; 259 foreach(i, immutable member; __traits(allMembers, T)) { 260 alias M = __traits(getMember, T, member); 261 alias R = ReturnType!M; 262 ret ~= "override ReturnType!(__traits(getMember, T, `" ~ member ~ "`)) " ~ member ~ "(Parameters!(__traits(getMember, T, `" ~ member ~ "`)) args){"; 263 static if(hasUDA!(M, Where)) { 264 enum where = true; 265 ret ~= "auto where = Database.Clause.Where.prepare!(E, `" ~ getUDAs!(M, Where)[0].clause ~ "`).build(args);"; 266 } else { 267 enum where = false; 268 ret ~= "enum where = Database.Clause.Where.init;"; 269 } 270 static if(hasUDA!(M, OrderByRandom)) { 271 ret ~= "enum order = Database.Clause.Order.random;"; 272 } else static if(hasUDA!(M, OrderBy)) { 273 ret ~= "enum order = Database.Clause.Order("; 274 foreach(order ; getUDAs!(M, OrderBy)) { 275 //TODO convert member name 276 ret ~= "Database.Clause.Order.Field(`" ~ order.name ~ "`, Database.Clause.Order.Field." ~ (order._asc ? "asc" : "desc") ~ "),"; 277 } 278 ret ~= ");"; 279 } else { 280 ret ~= "enum order = Database.Clause.Order.init;"; 281 } 282 static if(hasUDA!(M, Limit)) { 283 immutable limit = getUDAs!(__traits(getMember, T, member), Limit)[0]; 284 ret ~= "auto limit = Database.Clause.Limit(" ~ convert(limit.lower) ~ "," ~ convert(limit.upper) ~ ");"; 285 } else { 286 ret ~= "enum limit = Database.Clause.Limit.init;"; 287 } 288 static if(hasUDA!(M, Select)) { 289 static if(is(R == E)) { 290 ret ~= "return _database.selectOne!E(Database.Select(where, order, limit));"; 291 } else { 292 ret ~= "R[] ret; foreach(entity ; _database.select!E(Database.Select(where, order, limit))){ ret ~= entity; } return ret;"; 293 } 294 } else static if(hasUDA!(M, Insert)) { 295 static assert(is(R == void)); 296 ret ~= "_database.insert(new E(args[0]));"; 297 } else static if(hasUDA!(M, Update)) { 298 static if(hasUDA!(M, Fields)) enum fields = getUDAs!(M, Fields).fields; 299 else enum fields = members!E; 300 static if(where) { 301 ret ~= "_database.update!([" ~ fields.map!(str => '"' ~ str ~ '"').join(",") ~ "])(new E(args[0]), where);"; 302 } else { 303 ret ~= "_database.update!([" ~ fields.map!(str => '"' ~ str ~ '"').join(",") ~ "])(new E(args[0]));"; 304 } 305 } else static if(hasUDA!(M, Remove)) { 306 static if(Parameters!M.length == 1 && is(Parameters!M[0] == E)) { 307 ret ~= "_database.del(new E(args[0]));"; 308 } else { 309 ret ~= "_database.del(__table, where);"; 310 } 311 } else { 312 static assert(0, "Cannot implement method " ~ member ~ " because it's missing either @Select, @Insert, @Update or @Remove"); 313 } 314 ret ~= "}"; 315 } 316 return ret; 317 } 318 319 private string convert(string str) { 320 if(str.length && str[0] == '$') return "args[" ~ str[1..$] ~ "]"; 321 else return str; 322 } 323 324 // copied from shark.database.getEntityMembers 325 private string[] members(T)() { 326 string[] ret; 327 foreach(immutable member ; __traits(allMembers, T)) { 328 static if(!is(typeof(__traits(getMember, T, member)) == function) && __traits(compiles, mixin("new T()." ~ member ~ "=T." ~ member ~ ".init")) && !hasUDA!(__traits(getMember, T, member), PrimaryKey)) { 329 ret ~= member; 330 } 331 } 332 return ret; 333 }