diff --git a/app/v1/manage_user.go b/app/v1/manage_user.go index 637f641..13e07e1 100644 --- a/app/v1/manage_user.go +++ b/app/v1/manage_user.go @@ -44,5 +44,5 @@ LEFT JOIN users_stats ON users.id=users_stats.id WHERE users.id=? LIMIT 1` - return userPuts(md, md.DB.QueryRowx(query, data.UserID)) + return userPutsSingle(md, md.DB.QueryRowx(query, data.UserID)) } diff --git a/app/v1/user.go b/app/v1/user.go index 5feca21..01d0335 100644 --- a/app/v1/user.go +++ b/app/v1/user.go @@ -22,33 +22,36 @@ type userData struct { Country string `json:"country"` } -// UsersGET is the API handler for GET /users -func UsersGET(md common.MethodData) common.CodeMessager { - shouldRet, whereClause, param := whereClauseUser(md, "users") - if shouldRet != nil { - return *shouldRet - } - - query := ` -SELECT users.id, users.username, register_datetime, privileges, +const userFields = `users.id, users.username, register_datetime, privileges, latest_activity, users_stats.username_aka, users_stats.country FROM users INNER JOIN users_stats ON users.id=users_stats.id +` + +// UsersGET is the API handler for GET /users +func UsersGET(md common.MethodData) common.CodeMessager { + shouldRet, whereClause, param := whereClauseUser(md, "users") + if shouldRet != nil { + return userPutsMulti(md) + } + + query := ` +SELECT ` + userFields + ` WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + ` LIMIT 1` - return userPuts(md, md.DB.QueryRowx(query, param)) + return userPutsSingle(md, md.DB.QueryRowx(query, param)) } -type userPutsUserData struct { +type userPutsSingleUserData struct { common.ResponseBase userData } -func userPuts(md common.MethodData, row *sqlx.Row) common.CodeMessager { +func userPutsSingle(md common.MethodData, row *sqlx.Row) common.CodeMessager { var err error - var user userPutsUserData + var user userPutsSingleUserData err = row.StructScan(&user.userData) switch { @@ -63,6 +66,62 @@ func userPuts(md common.MethodData, row *sqlx.Row) common.CodeMessager { return user } +type userPutsMultiUserData struct { + common.ResponseBase + Users []userData `json:"users"` +} + +func userPutsMulti(md common.MethodData) common.CodeMessager { + q := md.C.Request.URL.Query() + + // query composition + wh := common. + Where("users.username = ?", md.Query("nname")). + Where("users.id = ?", md.Query("iid")). + Where("users.privileges = ?", md.Query("privileges")). + Where("users.privileges & ? > 0", md.Query("has_privileges")). + Where("users_stats.country = ?", md.Query("country")). + Where("users_stats.username_aka = ?", md.Query("name_aka")). + In("users.id", q["ids"]...). + In("users.username", q["names"]...). + In("users_stats.username_aka", q["names_aka"]...). + In("users_stats.country", q["countries"]...) + query := "" + + "SELECT " + userFields + wh.ClauseSafe() + " AND " + md.User.OnlyUserPublic(true) + + " " + common.Sort(md, common.SortConfiguration{ + Allowed: []string{ + "id", + "username", + "privileges", + "donor_expire", + "latest_activity", + "silence_end", + }, + Default: "id ASC", + Table: "users", + }) + + " " + common.Paginate(md.Query("p"), md.Query("l"), 100) + + // query execution + rows, err := md.DB.Queryx(query, wh.Params...) + if err != nil { + md.Err(err) + return Err500 + } + var r userPutsMultiUserData + for rows.Next() { + var u userData + err := rows.StructScan(&u) + if err != nil { + md.Err(err) + continue + } + r.Users = append(r.Users, u) + } + r.Code = 200 + return r +} + // UserSelfGET is a shortcut for /users/id/self. (/users/self) func UserSelfGET(md common.MethodData) common.CodeMessager { md.C.Request.URL.RawQuery = "id=self&" + md.C.Request.URL.RawQuery diff --git a/common/where.go b/common/where.go index 96e2bc2..60e09be 100644 --- a/common/where.go +++ b/common/where.go @@ -15,17 +15,58 @@ func (w *WhereClause) Where(clause, passedParam string, allowedValues ...string) if len(allowedValues) != 0 && !contains(allowedValues, passedParam) { return w } - // checks passed, if string is empty add "WHERE" + w.addWhere() + w.Clause += clause + w.Params = append(w.Params, passedParam) + return w +} + +func (w *WhereClause) addWhere() { + // if string is empty add "WHERE", else add AND if w.Clause == "" { w.Clause += "WHERE " } else { w.Clause += " AND " } - w.Clause += clause - w.Params = append(w.Params, passedParam) +} + +// In generates an IN clause. +// initial is the initial part, e.g. "users.id". +// Fields are the possible values. +// Sample output: users.id IN ('1', '2', '3') +func (w *WhereClause) In(initial string, fields ...string) *WhereClause { + if len(fields) == 0 { + return w + } + w.addWhere() + w.Clause += initial + " IN (" + generateQuestionMarks(len(fields)) + ")" + fieldsInterfaced := make([]interface{}, 0, len(fields)) + for _, i := range fields { + fieldsInterfaced = append(fieldsInterfaced, interface{}(i)) + } + w.Params = append(w.Params, fieldsInterfaced...) return w } +func generateQuestionMarks(x int) (qm string) { + for i := 0; i < x-1; i++ { + qm += "?, " + } + if x > 0 { + qm += "?" + } + return qm +} + +// ClauseSafe returns the clause, always containing something. If w.Clause is +// empty, it returns "WHERE 1". +func (w *WhereClause) ClauseSafe() string { + if w.Clause == "" { + return "WHERE 1" + } + return w.Clause +} + // Where is the same as WhereClause.Where, but creates a new WhereClause. func Where(clause, passedParam string, allowedValues ...string) *WhereClause { w := new(WhereClause) diff --git a/common/where_test.go b/common/where_test.go new file mode 100644 index 0000000..4dbfc2e --- /dev/null +++ b/common/where_test.go @@ -0,0 +1,96 @@ +package common + +import ( + "reflect" + "testing" +) + +func Test_generateQuestionMarks(t *testing.T) { + type args struct { + x int + } + tests := []struct { + name string + args args + wantQm string + }{ + {"-1", args{-1}, ""}, + {"0", args{0}, ""}, + {"1", args{1}, "?"}, + {"2", args{2}, "?, ?"}, + } + for _, tt := range tests { + if gotQm := generateQuestionMarks(tt.args.x); gotQm != tt.wantQm { + t.Errorf("%q. generateQuestionMarks() = %v, want %v", tt.name, gotQm, tt.wantQm) + } + } +} + +func TestWhereClause_In(t *testing.T) { + type args struct { + initial string + fields []interface{} + } + tests := []struct { + name string + fields *WhereClause + args args + want *WhereClause + }{ + { + "simple", + &WhereClause{}, + args{"users.id", []interface{}{"1", "2", "3"}}, + &WhereClause{"WHERE users.id IN (?, ?, ?)", []interface{}{"1", "2", "3"}}, + }, + { + "withExisting", + Where("users.username = ?", "Howl").Where("users.xd > ?", "6"), + args{"users.id", []interface{}{"1"}}, + &WhereClause{ + "WHERE users.username = ? AND users.xd > ? AND users.id IN (?)", + []interface{}{"Howl", "6", "1"}, + }, + }, + } + for _, tt := range tests { + w := tt.fields + if got := w.In(tt.args.initial, tt.args.fields...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("%q. WhereClause.In() = %v, want %v", tt.name, got, tt.want) + } + } +} + +func TestWhere(t *testing.T) { + type args struct { + clause string + passedParam string + allowedValues []string + } + tests := []struct { + name string + args args + want *WhereClause + }{ + { + "simple", + args{"users.id = ?", "5", nil}, + &WhereClause{"WHERE users.id = ?", []interface{}{"5"}}, + }, + { + "allowed", + args{"users.id = ?", "5", []string{"1", "3", "5"}}, + &WhereClause{"WHERE users.id = ?", []interface{}{"5"}}, + }, + { + "notAllowed", + args{"users.id = ?", "5", []string{"0"}}, + &WhereClause{}, + }, + } + for _, tt := range tests { + if got := Where(tt.args.clause, tt.args.passedParam, tt.args.allowedValues...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("%q. Where() = %#v, want %#v", tt.name, got, tt.want) + } + } +}