Ported code over from beep v1. Wrote API documentation. Changed some POST/PATCH handlers to respond an empty body instead of just parroting the supplied body
parent
853bbe3da9
commit
e4ce8ee2d8
|
@ -0,0 +1,12 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
|
@ -0,0 +1,13 @@
|
|||
FROM golang:1.11-rc-alpine as build
|
||||
|
||||
RUN apk add --no-cache git=2.18.1-r0
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum *.go ./
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=build /src/core /core
|
||||
|
||||
ENTRYPOINT ["/core"]
|
462
README.md
462
README.md
|
@ -1,3 +1,463 @@
|
|||
# backend-core
|
||||
|
||||
Beep backend handling core relational information that is not updated often.
|
||||
Beep backend handling core relational information that is not updated often.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```
|
||||
cockroach start --insecure
|
||||
echo "create database core;" | cockroach sql --insecure
|
||||
|
||||
migrate -database cockroach://root@localhost:26257/core?sslmode=disable -source file://migrations goto 1
|
||||
go build && ./core
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Unless otherwise noted, bodies and responses are with ```Content-Type: application/json```.
|
||||
|
||||
| Contents |
|
||||
| -------- |
|
||||
| [Create User](#Create-User) |
|
||||
| [Get Users by Phone](#Get-Users-by-Phone) |
|
||||
| [Get User](#Get-User) |
|
||||
| [Create Conversation](#Create-Conversation) |
|
||||
| [Delete Conversation](#Delete-Conversation) |
|
||||
| [Update Conversation](#Update-Conversation) |
|
||||
| [Get Conversations](#Get-Conversations) |
|
||||
| [Get Conversation](#Get-Conversation) |
|
||||
| [Create Conversation Member](#Create-Conversation-Member) |
|
||||
| [Get Conversation Members](#Get-Conversation-Members) |
|
||||
| [Create Contact](#Create-Contact) |
|
||||
| [Get Contacts](#Get-Contacts) |
|
||||
|
||||
---
|
||||
|
||||
### Create User
|
||||
|
||||
```
|
||||
POST /user
|
||||
```
|
||||
|
||||
Create a new user.
|
||||
|
||||
#### Body
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| first_name | String | First name of the added user. | ✓ |
|
||||
| last_name | String | Last name of the added user. | ✓ |
|
||||
| phone_number | String | Phone number of the added user. Shouldn't be needed but makes life easier. | X |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Created user object.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<id>",
|
||||
"first_name": "<first_name>",
|
||||
"last_name": "<last_name>",
|
||||
"phone_number": "<phone_number>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Error parsing submitted body, or fields first_name or last_name have a length of 0. |
|
||||
| 500 | Error occurred inserting entry into database. |
|
||||
|
||||
---
|
||||
|
||||
### Get Users by Phone
|
||||
|
||||
```
|
||||
GET /user
|
||||
```
|
||||
|
||||
Get user(s) associated with the supplied phone number.
|
||||
|
||||
#### Querystring
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| phone_number | String | Phone number to be queried. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
List of users.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "<id>",
|
||||
"first_name": "<first_name>",
|
||||
"last_name": "<last_name>"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Supplied phone_number is absent/an invalid phone number. |
|
||||
| 500 | Error occurred retrieving entries from database. |
|
||||
|
||||
---
|
||||
|
||||
### Get User
|
||||
|
||||
```
|
||||
GET /user/:user
|
||||
```
|
||||
|
||||
Get a specific user by ID.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
User object.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<id>",
|
||||
"first_name": "<first_name>",
|
||||
"last_name": "<last_name>",
|
||||
"phone_number": "<phone_number>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 404 | User with supplied ID could not be found in database |
|
||||
| 500 | Error occurred retrieving entries from database. |
|
||||
|
||||
---
|
||||
|
||||
### Create Conversation
|
||||
|
||||
```
|
||||
POST /user/:user/conversation
|
||||
```
|
||||
|
||||
Create a new conversation for a user.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
|
||||
#### Body
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| title | String | Title of the conversation | X |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Conversation object.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<id>",
|
||||
"title": "<title>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Error occurred parsing the supplied body. |
|
||||
| 404 | User with supplied ID could not be found in database. |
|
||||
| 500 | Error occurred inserting entries into the database. |
|
||||
|
||||
---
|
||||
|
||||
### Delete Conversation
|
||||
|
||||
```
|
||||
DELETE /user/:user/conversation/:conversation
|
||||
```
|
||||
|
||||
Delete the specified conversation.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
| conversation | String | Conversation's ID. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Empty body.
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 404 | User/Conversation with supplied ID could not be found in database. |
|
||||
| 500 | Error occurred deleting entries from the database. |
|
||||
|
||||
---
|
||||
|
||||
### Update Conversation
|
||||
|
||||
```
|
||||
PATCH /user/:user/conversation/:conversation
|
||||
```
|
||||
|
||||
Update a conversation's details (mainly just title for now).
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
| conversation | String | Conversation's ID. | ✓ |
|
||||
|
||||
#### Body
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| title | String | New title of the conversation. | X |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Empty Body. (TODO: Updated conversation)
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Error occurred parsing the supplied body. |
|
||||
| 404 | User/Conversation with supplied ID could not be found in database. |
|
||||
| 500 | Error occurred updating entries in the database. |
|
||||
|
||||
---
|
||||
|
||||
### Get Conversations
|
||||
|
||||
```
|
||||
GET /user/:user/conversation
|
||||
```
|
||||
|
||||
Get the conversations of the specified user.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
List of conversations.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "<id>",
|
||||
"title": "<title>"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 500 | Error occurred updating entries in the database. |
|
||||
|
||||
---
|
||||
|
||||
### Get Conversation
|
||||
|
||||
```
|
||||
GET /user/:user/conversation/:conversation
|
||||
```
|
||||
|
||||
Get a specific conversation of a specific user.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
| conversation | String | Conversation's ID. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Conversation object.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<id>",
|
||||
"title": "<title>"
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 404 | User/Conversation with supplied ID could not be found in database. |
|
||||
| 500 | Error occurred retrieving entries from the database. |
|
||||
|
||||
---
|
||||
|
||||
### Create Conversation Member
|
||||
|
||||
```
|
||||
POST /user/:user/conversation/:conversation/member
|
||||
```
|
||||
|
||||
Add a member to the specified conversation of the specified member.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
| conversation | String | Conversation's ID. | ✓ |
|
||||
|
||||
#### Body
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | String | ID of the user to be added. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Empty body.
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Error occurred parsing the supplied body/The length of the ID supplied in the body is less than 1. |
|
||||
| 404 | User/Conversation with supplied ID could not be found in database. |
|
||||
| 500 | Error occurred updating entries in the database. |
|
||||
|
||||
---
|
||||
|
||||
### Get Conversation Members
|
||||
|
||||
```
|
||||
GET /user/:user/conversation/:conversation/member
|
||||
```
|
||||
|
||||
Get the members of the specified conversation of the specified member.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
| conversation | String | Conversation's ID. | ✓ |
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
List of user objects in conversation.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "<id>",
|
||||
"first_name": "<first_name>",
|
||||
"last_name": "<last_name>"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 500 | Error occurred retrieving entries from the database. |
|
||||
|
||||
---
|
||||
|
||||
### Create Contact
|
||||
|
||||
```
|
||||
POST /user/:user/contact
|
||||
```
|
||||
|
||||
Add a new contact for the specified user.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
|
||||
#### Body
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| id | String | New contact's ID. | ✓ |
|
||||
|
||||
#### Success Response (200 OK)
|
||||
|
||||
Empty body
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Error occurred parsing the supplied body/The length of the ID supplied in the body is less than 1 or equal to the user's ID. |
|
||||
| 500 | Error occurred updating entries in the database. |
|
||||
|
||||
---
|
||||
|
||||
### Get Contacts
|
||||
|
||||
```
|
||||
GET /user/:user/contact
|
||||
```
|
||||
|
||||
Get the contacts of the specified user.
|
||||
|
||||
#### URL Params
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| user | String | User's ID. | ✓ |
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
List of user objects in user's contacts.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "<id>",
|
||||
"first_name": "<first_name>",
|
||||
"last_name": "<last_name>"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 500 | Error occurred retrieving entries from the database. |
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
module backend/core
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.1.0 // indirect
|
||||
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d
|
||||
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
|
||||
github.com/ttacon/libphonenumber v1.0.0
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d h1:of6+TpypLAaiv4JxgH5aplBZnt0b65B4v4c8q5oy+Sk=
|
||||
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84 h1:it29sI2IM490luSc3RAhp5WuCYnc6RtbfLVAB7nmC5M=
|
||||
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
|
||||
github.com/ttacon/libphonenumber v1.0.0 h1:5DJsnAoMCC+LjJ6lQqjjf2EHiDD6KH4rVhDsMn5oXII=
|
||||
github.com/ttacon/libphonenumber v1.0.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M=
|
|
@ -0,0 +1,506 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
// Parse
|
||||
user := User{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&user)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate
|
||||
phone, err := ParsePhone(user.PhoneNumber)
|
||||
if err != nil || len(user.FirstName) < 1 || len(user.LastName) < 1 {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user.PhoneNumber = phone // shouldn't be needed but makes life easier
|
||||
|
||||
// Generate ID
|
||||
id := "u-" + RandomHex()
|
||||
user.ID = id
|
||||
|
||||
// Log
|
||||
log.Print(user)
|
||||
|
||||
// Insert
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO "user" (id, first_name, last_name, phone_number) VALUES ($1, $2, $3, $4)
|
||||
`, user.ID, user.FirstName, user.LastName, user.PhoneNumber)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *Handler) GetUsersByPhone(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
// Parse
|
||||
phone, err := ParsePhone(r.FormValue("phone_number"))
|
||||
|
||||
// Validate
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Response object
|
||||
users := make([]User, 0)
|
||||
|
||||
// Select
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, first_name, last_name FROM "user" WHERE phone_number = $1
|
||||
`, phone)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Scan
|
||||
for rows.Next() {
|
||||
user := User{}
|
||||
if err := rows.Scan(&user.ID, &user.FirstName, &user.LastName); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(users)
|
||||
}
|
||||
|
||||
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
|
||||
// Response object
|
||||
user := User{}
|
||||
|
||||
// Select
|
||||
err := h.db.QueryRow(`
|
||||
SELECT id, first_name, last_name, phone_number FROM "user" WHERE id = $1
|
||||
`, userID).Scan(&user.ID, &user.FirstName, &user.LastName, &user.PhoneNumber)
|
||||
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
conversation := Conversation{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&conversation)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate ID
|
||||
id := "c-" + RandomHex()
|
||||
conversation.ID = id
|
||||
|
||||
// Log
|
||||
log.Print(conversation)
|
||||
|
||||
// Insert
|
||||
tx, err := h.db.Begin()
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Conversation
|
||||
_, err1 := tx.Exec(`
|
||||
INSERT INTO "conversation" (id, title) VALUES ($1, $2)
|
||||
`, conversation.ID, conversation.Title)
|
||||
// First member
|
||||
_, err2 := tx.Exec(`
|
||||
INSERT INTO member ("user", "conversation") VALUES ($1, $2)
|
||||
`, userID, conversation.ID)
|
||||
if err1 != nil || err2 != nil {
|
||||
// likely 404...
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
log.Print(err1, err2)
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(conversation)
|
||||
}
|
||||
|
||||
func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
|
||||
// Response object
|
||||
conversations := make([]Conversation, 0)
|
||||
|
||||
// Select
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, title FROM "conversation"
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Scan
|
||||
for rows.Next() {
|
||||
var id, title string
|
||||
if err := rows.Scan(&id, &title); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
conversations = append(conversations, Conversation{id, title})
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(conversations)
|
||||
}
|
||||
|
||||
func (h *Handler) GetConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
conversationID := p.ByName("conversation")
|
||||
|
||||
// Response object
|
||||
conversation := Conversation{}
|
||||
|
||||
// Select
|
||||
err := h.db.QueryRow(`
|
||||
SELECT id, title FROM "conversation"
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
|
||||
`, userID, conversationID).Scan(&conversation.ID, &conversation.Title)
|
||||
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(conversation)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
conversationID := p.ByName("conversation")
|
||||
conversation := Conversation{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&conversation)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check
|
||||
var conversationID2 string
|
||||
err = h.db.QueryRow(`
|
||||
SELECT id FROM "conversation"
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
|
||||
`, userID, conversationID).Scan(&conversationID2)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update
|
||||
if len(conversation.Title) > 0 {
|
||||
_, err = h.db.Exec(`
|
||||
UPDATE "conversation"
|
||||
SET title = $2
|
||||
WHERE id = $1
|
||||
`, conversationID, conversation.Title)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(200);
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
userID := p.ByName("user")
|
||||
conversationID := p.ByName("conversation")
|
||||
|
||||
// Delete
|
||||
tx, err := h.db.Begin()
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check
|
||||
var conversationID2 string
|
||||
err = h.db.QueryRow(`
|
||||
SELECT id FROM "conversation"
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
|
||||
`, userID, conversationID).Scan(&conversationID2)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Users in Conversation
|
||||
_, err1 := tx.Exec(`
|
||||
DELETE FROM "member" WHERE "conversation" = $1
|
||||
`, conversationID)
|
||||
// Conversation
|
||||
_, err2 := tx.Exec(`
|
||||
DELETE FROM "conversation" WHERE "id" = $1
|
||||
`, conversationID)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
// likely 404...
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
log.Print(err1, err2)
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateConversationMember(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
conversationID := p.ByName("conversation")
|
||||
member := User{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&member)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate
|
||||
if len(member.ID) < 1 {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Log
|
||||
log.Print(member)
|
||||
|
||||
// Check
|
||||
var conversationID2 string
|
||||
err = h.db.QueryRow(`
|
||||
SELECT id FROM "conversation"
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
|
||||
`, userID, conversationID).Scan(&conversationID2)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO member ("user", "conversation") VALUES ($2, $1)
|
||||
`, conversationID, member.ID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
//w.Header().Set("Content-Type", "application/json")
|
||||
//json.NewEncoder(w).Encode(member)
|
||||
w.WriteHeader(200)
|
||||
}
|
||||
|
||||
func (h *Handler) GetConversationMembers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
conversationID := p.ByName("conversation")
|
||||
|
||||
// Response object
|
||||
users := make([]User, 0)
|
||||
|
||||
// Select
|
||||
rows, err := h.db.Query(`
|
||||
SELECT "user".id, "user".first_name, "user".last_name FROM "user"
|
||||
INNER JOIN member m ON "user".id = m.user
|
||||
INNER JOIN conversation ON "conversation".id = m.conversation
|
||||
INNER JOIN member
|
||||
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
|
||||
`, userID, conversationID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Scan
|
||||
for rows.Next() {
|
||||
var id, firstName, lastName string
|
||||
if err := rows.Scan(&id, &firstName, &lastName); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
users = append(users, User{ID: id, FirstName: firstName, LastName: lastName})
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(users)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateContact(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
contact := User{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&contact)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate
|
||||
if len(contact.ID) < 1 || contact.ID == userID {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Insert
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO contact ("user", contact) VALUES ($1, $2)
|
||||
`, userID, contact.ID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond
|
||||
//w.Header().Set("Content-Type", "application/json")
|
||||
//json.NewEncoder(w).Encode(contact)
|
||||
}
|
||||
|
||||
func (h *Handler) GetContacts(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Parse
|
||||
userID := p.ByName("user")
|
||||
|
||||
// Response object
|
||||
contacts := make([]User, 0)
|
||||
|
||||
// Select
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, first_name, last_name, phone_number FROM "user"
|
||||
INNER JOIN contact
|
||||
ON contact.contact = "user".id AND contact.user = $1
|
||||
`, userID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Scan
|
||||
for rows.Next() {
|
||||
var id, firstName, lastName, phone string
|
||||
if err := rows.Scan(&id, &firstName, &lastName, &phone); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
contacts = append(contacts, User{id, firstName, lastName, phone})
|
||||
}
|
||||
|
||||
// Respond
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(contacts)
|
||||
}
|
||||
|
||||
func NewHandler(db *sql.DB) *Handler {
|
||||
return &Handler{db}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
var listen string
|
||||
var postgres string
|
||||
|
||||
func main() {
|
||||
// Parse flags
|
||||
flag.StringVar(&listen, "listen", ":8080", "host and port to listen on")
|
||||
flag.StringVar(&postgres, "postgres", "postgresql://root@localhost:26257/core?sslmode=disable", "postgres string")
|
||||
flag.Parse()
|
||||
|
||||
// Open postgres
|
||||
log.Printf("connecting to postgres %s", postgres)
|
||||
db, err := sql.Open("postgres", postgres)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Handler
|
||||
h := NewHandler(db)
|
||||
|
||||
// Routes
|
||||
router := httprouter.New()
|
||||
// Users
|
||||
router.POST("/user/", h.CreateUser)
|
||||
router.GET("/user/", h.GetUsersByPhone)
|
||||
router.GET("/user/:user", h.GetUser)
|
||||
//router.PATCH("/user/:user", h.UpdateUser)
|
||||
// Conversations
|
||||
router.POST("/user/:user/conversation/", h.CreateConversation)
|
||||
router.GET("/user/:user/conversation/", h.GetConversations) // USER MEMBER CONVERSATION
|
||||
router.DELETE("/user/:user/conversation/:conversation", h.DeleteConversation)
|
||||
//router.GET("/user/:user/conversation/bymembers/", h.GetConversationsByMembers) // TODO
|
||||
router.GET("/user/:user/conversation/:conversation", h.GetConversation) // USER MEMBER CONVERSATION
|
||||
router.PATCH("/user/:user/conversation/:conversation", h.UpdateConversation) // USER MEMBER CONVERSATION ADMIN=true -> update conversation title
|
||||
//router.DELETE("/user/:user/conversation/:conversation", h.DeleteConversation) // USER MEMBER CONVERSATION -> delete membership
|
||||
router.POST("/user/:user/conversation/:conversation/member/", h.CreateConversationMember) // USER MEMBER CONVERSATION ADMIN=true -> create new membership
|
||||
router.GET("/user/:user/conversation/:conversation/member/", h.GetConversationMembers) // USER MEMBER CONVERSATION
|
||||
//router.DELETE("/user/:user/conversation/:conversation/member/:member", h.DeleteConversationMember) // USER MEMBER CONVERSATION ADMIN=true -> delete membership
|
||||
// Last heard
|
||||
//router.GET("/user/:user/lastheard/:conversation", h.GetLastheard)
|
||||
//router.PUT("/user/:user/lastheard/:conversation", h.SetLastheard)
|
||||
// Contacts
|
||||
router.POST("/user/:user/contact/", h.CreateContact)
|
||||
router.GET("/user/:user/contact/", h.GetContacts)
|
||||
//router.GET("/user/:user/contact/:contact", h.GetContact)
|
||||
//router.DELETE("/user/:user/contact/:contact", h.DeleteContact)
|
||||
//router.GET("/user/:user/contact/:contact/conversation/", h.GetContactConversations)
|
||||
|
||||
log.Printf("starting server on %s", listen)
|
||||
log.Fatal(http.ListenAndServe(listen, router))
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
CREATE TABLE "user" (
|
||||
id BYTEA PRIMARY KEY,
|
||||
first_name VARCHAR(65535),
|
||||
last_name VARCHAR(65535),
|
||||
phone_number VARCHAR(32) UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE "conversation" (
|
||||
id BYTEA PRIMARY KEY,
|
||||
title VARCHAR(65535)
|
||||
);
|
||||
|
||||
CREATE TABLE member (
|
||||
"user" BYTEA REFERENCES "user"(id),
|
||||
"conversation" BYTEA REFERENCES "conversation"(id),
|
||||
UNIQUE ("user", "conversation")
|
||||
);
|
||||
|
||||
CREATE TABLE contact (
|
||||
"user" BYTEA REFERENCES "user"(id),
|
||||
contact BYTEA REFERENCES "user"(id),
|
||||
UNIQUE ("user", contact)
|
||||
);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
type Conversation struct {
|
||||
ID string `json:"id"` // id
|
||||
Title string `json:"title"` // title
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"` // id
|
||||
FirstName string `json:"first_name"` // first_name
|
||||
LastName string `json:"last_name"` // last_name
|
||||
PhoneNumber string `json:"phone_number"` // phone_number
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/ttacon/libphonenumber"
|
||||
)
|
||||
|
||||
func RandomHex() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic("unable to generate 16 bytes of randomness")
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func ParsePhone(phone string) (string, error) {
|
||||
num, err := libphonenumber.Parse(phone, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return libphonenumber.Format(num, libphonenumber.INTERNATIONAL), nil
|
||||
}
|
Loading…
Reference in New Issue