3
0

Move messages search rest endpoint under /search/messages, add controller, fix searching by query

This commit is contained in:
Denis Arh
2018-11-01 11:07:53 +01:00
parent c0c0a88aeb
commit 52cd5d33da
14 changed files with 326 additions and 135 deletions

View File

@@ -280,7 +280,7 @@ A channel is a representation of a sequence of messages. It has meta data like c
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
| state | string | POST | Valid values: delete, restore, archive, unarchive | N/A | YES |
| state | string | POST | Valid values: delete, undelete, archive, unarchive | N/A | YES |
## Read channel details
@@ -453,21 +453,6 @@ The following event types may be sent with a message event:
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
## Search messages
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/search` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| query | string | GET | Search string to match against messages | N/A | NO |
| message_type | string | GET | Limit results to message type | N/A<br><br>Values:<br><br><ul><li>`history`</li><li>`message`</li><li>`attachment`</li><li>`media`</li> | NO |
## Pin message to channel (public bookmark)
#### Method
@@ -618,6 +603,28 @@ The following event types may be sent with a message event:
# Search entry point
## Search for messages
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/search/messages` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| inChannel | uint64 | GET | Search only in one channel | N/A | NO |
| fromUser | uint64 | GET | Search only from one user | N/A | NO |
| firstID | uint64 | GET | Paging; return newer messages only (higher id) | N/A | NO |
| lastID | uint64 | GET | Paging; return older messages only (lower id) | N/A | NO |
# Users
## Search users (Directory)

View File

@@ -387,24 +387,6 @@
]
}
},
{
"name": "search",
"path": "/search",
"method": "GET",
"title": "Search messages",
"parameters": {
"get": [
{ "type": "string", "name": "query", "required": false, "title": "Search string to match against messages" },
{
"type": "string",
"name": "message_type",
"required": false,
"title": "Limit results to message type",
"values": ["history", "message", "attachment", "media"]
}
]
}
},
{
"name": "pin",
"path": "/{messageID}/pin",
@@ -532,6 +514,37 @@
}
]
},
{
"name": "search",
"entrypoint": "search",
"authentication": ["Client ID", "Session ID"],
"path": "/search",
"method": "GET",
"title": "Search entry point",
"parameters": {
"get": [
{ "name": "query", "type": "string", "required": false, "title": "Search query" }
]
},
"apis": [
{
"method": "GET",
"name": "messages",
"path": "/messages",
"title": "Search for messages",
"parameters": {
"GET": [
{ "name": "inChannel", "type": "uint64", "required": false, "title": "Search only in one channel"},
{ "name": "fromUser", "type": "uint64", "required": false, "title": "Search only from one user"},
{ "name": "firstID", "type": "uint64", "required": false, "title": "Paging; return newer messages only (higher id)"},
{ "name": "lastID", "type": "uint64", "required": false, "title": "Paging; return older messages only (lower id)"}
]
}
}
]
},
{
"title": "Users",
"package": "sam",

View File

@@ -122,7 +122,7 @@
{
"name": "state",
"required": true,
"title": "Valid values: delete, restore, archive, unarchive",
"title": "Valid values: delete, undelete, archive, unarchive",
"type": "string"
}
]

View File

@@ -92,34 +92,6 @@
]
}
},
{
"Name": "search",
"Method": "GET",
"Title": "Search messages",
"Path": "/search",
"Parameters": {
"get": [
{
"name": "query",
"required": false,
"title": "Search string to match against messages",
"type": "string"
},
{
"name": "message_type",
"required": false,
"title": "Limit results to message type",
"type": "string",
"values": [
"history",
"message",
"attachment",
"media"
]
}
]
}
},
{
"Name": "pin",
"Method": "POST",

View File

@@ -0,0 +1,58 @@
{
"Title": "Search entry point",
"Package": "",
"Interface": "Search",
"Struct": null,
"Parameters": {
"get": [
{
"name": "query",
"required": false,
"title": "Search query",
"type": "string"
}
]
},
"Protocol": "",
"Authentication": [
"Client ID",
"Session ID"
],
"Path": "/search",
"APIs": [
{
"Name": "messages",
"Method": "GET",
"Title": "Search for messages",
"Path": "/messages",
"Parameters": {
"GET": [
{
"name": "inChannel",
"required": false,
"title": "Search only in one channel",
"type": "uint64"
},
{
"name": "fromUser",
"required": false,
"title": "Search only from one user",
"type": "uint64"
},
{
"name": "firstID",
"required": false,
"title": "Paging; return newer messages only (higher id)",
"type": "uint64"
},
{
"name": "lastID",
"required": false,
"title": "Paging; return older messages only (lower id)",
"type": "uint64"
}
]
}
}
]
}

View File

@@ -95,7 +95,7 @@ func (r *message) FindMessages(filter *types.MessageFilter) (types.MessageSet, e
if filter.Query != "" {
sql += " AND message LIKE ?"
params = append(params, filter.Query+"%")
params = append(params, "%"+filter.Query+"%")
}
if filter.ChannelID > 0 {
@@ -103,6 +103,11 @@ func (r *message) FindMessages(filter *types.MessageFilter) (types.MessageSet, e
params = append(params, filter.ChannelID)
}
if filter.UserID > 0 {
sql += " AND rel_user = ? "
params = append(params, filter.UserID)
}
if filter.RepliesTo > 0 {
sql += " AND reply_to = ? "
params = append(params, filter.RepliesTo)

View File

@@ -31,7 +31,6 @@ type MessageAPI interface {
History(context.Context, *request.MessageHistory) (interface{}, error)
Edit(context.Context, *request.MessageEdit) (interface{}, error)
Delete(context.Context, *request.MessageDelete) (interface{}, error)
Search(context.Context, *request.MessageSearch) (interface{}, error)
Pin(context.Context, *request.MessagePin) (interface{}, error)
GetReplies(context.Context, *request.MessageGetReplies) (interface{}, error)
CreateReply(context.Context, *request.MessageCreateReply) (interface{}, error)
@@ -48,7 +47,6 @@ type Message struct {
History func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Search func(http.ResponseWriter, *http.Request)
Pin func(http.ResponseWriter, *http.Request)
GetReplies func(http.ResponseWriter, *http.Request)
CreateReply func(http.ResponseWriter, *http.Request)
@@ -89,13 +87,6 @@ func NewMessage(mh MessageAPI) *Message {
return mh.Delete(r.Context(), params)
})
},
Search: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageSearch()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.Search(r.Context(), params)
})
},
Pin: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessagePin()
@@ -163,7 +154,6 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Get("/", mh.History)
r.Put("/{messageID}", mh.Edit)
r.Delete("/{messageID}", mh.Delete)
r.Get("/search", mh.Search)
r.Post("/{messageID}/pin", mh.Pin)
r.Get("/{messageID}/replies", mh.GetReplies)
r.Post("/{messageID}/replies", mh.CreateReply)

View File

@@ -0,0 +1,57 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `search.go`, `search.util.go` or `search_test.go` to
implement your API calls, helper functions and tests. The file `search.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"github.com/go-chi/chi"
"net/http"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/sam/rest/request"
)
// Internal API interface
type SearchAPI interface {
Messages(context.Context, *request.SearchMessages) (interface{}, error)
}
// HTTP API interface
type Search struct {
Messages func(http.ResponseWriter, *http.Request)
}
func NewSearch(sh SearchAPI) *Search {
return &Search{
Messages: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSearchMessages()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return sh.Messages(r.Context(), params)
})
},
}
}
func (sh *Search) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Route("/search", func(r chi.Router) {
r.Get("/messages", sh.Messages)
})
})
}

View File

@@ -68,13 +68,6 @@ func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (inte
return nil, ctrl.svc.msg.With(ctx).Delete(r.MessageID)
}
func (ctrl *Message) Search(ctx context.Context, r *request.MessageSearch) (interface{}, error) {
return ctrl.wrapSet(ctx)(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
ChannelID: r.ChannelID,
Query: r.Query,
}))
}
func (ctrl *Message) Pin(ctx context.Context, r *request.MessagePin) (interface{}, error) {
return nil, ctrl.svc.msg.With(ctx).Pin(r.MessageID)
}

View File

@@ -221,59 +221,6 @@ func (m *MessageDelete) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageDelete()
// Message search request parameters
type MessageSearch struct {
Query string
Message_type string
ChannelID uint64 `json:",string"`
}
func NewMessageSearch() *MessageSearch {
return &MessageSearch{}
}
func (m *MessageSearch) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["query"]; ok {
m.Query = val
}
if val, ok := get["message_type"]; ok {
m.Message_type = val
}
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
return err
}
var _ RequestFiller = NewMessageSearch()
// Message pin request parameters
type MessagePin struct {
MessageID uint64 `json:",string"`

View File

@@ -0,0 +1,97 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `search.go`, `search.util.go` or `search_test.go` to
implement your API calls, helper functions and tests. The file `search.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"encoding/json"
"github.com/go-chi/chi"
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
var _ = chi.URLParam
var _ = types.JSONText{}
var _ = multipart.FileHeader{}
// Search messages request parameters
type SearchMessages struct {
InChannel uint64 `json:",string"`
FromUser uint64 `json:",string"`
FirstID uint64 `json:",string"`
LastID uint64 `json:",string"`
Query string
}
func NewSearchMessages() *SearchMessages {
return &SearchMessages{}
}
func (s *SearchMessages) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(s)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["inChannel"]; ok {
s.InChannel = parseUInt64(val)
}
if val, ok := get["fromUser"]; ok {
s.FromUser = parseUInt64(val)
}
if val, ok := get["firstID"]; ok {
s.FirstID = parseUInt64(val)
}
if val, ok := get["lastID"]; ok {
s.LastID = parseUInt64(val)
}
if val, ok := get["query"]; ok {
s.Query = val
}
return err
}
var _ RequestFiller = NewSearchMessages()

View File

@@ -23,6 +23,7 @@ func MountRoutes() func(chi.Router) {
handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r)
handlers.NewTeam(Team{}.New()).MountRoutes(r)
handlers.NewUser(User{}.New()).MountRoutes(r)
handlers.NewSearch(Search{}.New()).MountRoutes(r)
})
}
}

48
sam/rest/search.go Normal file
View File

@@ -0,0 +1,48 @@
package rest
import (
"context"
"github.com/crusttech/crust/internal/payload"
"github.com/crusttech/crust/internal/payload/outgoing"
"github.com/crusttech/crust/sam/rest/request"
"github.com/crusttech/crust/sam/service"
"github.com/crusttech/crust/sam/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type Search struct {
svc struct {
msg service.MessageService
}
}
func (Search) New() *Search {
ctrl := &Search{}
ctrl.svc.msg = service.DefaultMessage
return ctrl
}
func (ctrl *Search) Messages(ctx context.Context, r *request.SearchMessages) (interface{}, error) {
return ctrl.wrapSet(ctx)(ctrl.svc.msg.With(ctx).Find(&types.MessageFilter{
Query: r.Query,
ChannelID: r.InChannel,
UserID: r.FromUser,
FirstID: r.FirstID,
LastID: r.LastID,
}))
}
func (ctrl *Search) wrapSet(ctx context.Context) func(mm types.MessageSet, err error) (*outgoing.MessageSet, error) {
return func(mm types.MessageSet, err error) (*outgoing.MessageSet, error) {
if err != nil {
return nil, err
} else {
return payload.Messages(ctx, mm), nil
}
}
}

View File

@@ -34,12 +34,15 @@ type (
// All messages that belong to a channel
ChannelID uint64
// Only messages that belong to a user
UserID uint64
// Return all replies to a single message
RepliesTo uint64
// (FirstID...LastID), for paging
//
// Include all messsages which IDs range from "first" to "last" (exclusive!)
// Include all messages which IDs range from "first" to "last" (exclusive!)
FirstID uint64
LastID uint64