Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: bin/teach-la-go-backend
167 changes: 17 additions & 150 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# teach-la-go-backend

Hey there! This is the repo for our **experimental** Go Backend, which we're using for our online editor. Eventually, the goal of this project is to replace [the current Express-based backend](https://github.com/uclaacm/TeachLAJSBackend), bringing it up to feature parity and using all the benefits that Go provides!
Hey there! This is the repo for our Go Backend, which we're using for our online editor.

If you're on the TeachLA Slack, feel free to @leo with any questions. Thanks!
If you're on the TeachLA Slack, feel free to pop into the #go-backend channel with any and all questions. Thanks!

# Developer Setup

Expand All @@ -11,11 +11,13 @@ Here's what you need and how to **build** the project:
* [Go](https://golang.org/)

```sh
git clone [email protected]:uclaacm/teach-la-go-backend.git
# alternatively, using HTTPS:
go get github.com/uclaacm/teach-la-go-backend
# alternatively, using git:
# git clone [email protected]:uclaacm/teach-la-go-backend.git
# OR
# git clone https://github.com/uclaacm/teach-la-go-backend.git

cd teach-la-go-backend
cd $GOPATH/src/github.com/uclaacm/teach-la-go-backend

# set up git pre-commit hook
chmod +x hooks/pre-commit
Expand All @@ -32,13 +34,17 @@ go build
./teach-la-go-backend

```
If you try running the server at this point (with `./teach-la-go-backend`), you'll probably get a message like this: `could not find firebase config file! Did you set your CFGPATH variable? stat : no such file or directory`. To **run** the project, one needs to be able to interact with the TeachLA Firebase through service account credentials (usually a single JSON file). These can be obtained during a TeachLA dev team meeting, or by messaging the #go-backend channel on the TLA Slack.
If you try running the server at this point (with `./teach-la-go-backend`), you'll probably get a message like this: `...no $PORT environment variable provided.`. To **run** the project, one needs to set the port to run the backend on and have the ability to interact with the TeachLA Firebase through service account credentials, which are provided via the `$TLACFG` environment variable. These can be obtained during a TeachLA dev team meeting, or by messaging the #go-backend channel on the TLA Slack.

Once acquired, set the variables:

Once acquired, save the JSON file in the root directory. **It is recommended that you chnage the file extension to `.env` so `gitignore` will prevent it from being accidentally uploaded to the public repo**. Once you have done that, specify the location of your credentials by setting the environment variable `$CFGPATH`:
```
export CFGPATH=./secret.env
export PORT=8081
export TLACFG='my secret stuff'
```

**It is recommended that you put these commands in a `*.env` file to avoid having to run these manually.** To set your environment variables through the file, simply run `source MYFILENAME.env`.

You can now run the server you built!

## Testing
Expand All @@ -52,149 +58,10 @@ go test ./...
# run a specific test
go test ./server_test.go

# run tests with log output
# run tests with verbose output
go test -v ./server_test.go
```

With this, you can build, test, and run the actual backend. If you'd like to get working, you can stop reading here. Otherwise, you can scan through the documentation below.

# About the backend

Response codes we use:

Event | `net/http` Constant | Response Code
---|---|:-:
Nominal | `http.StatusOK` | `200`
Bad request | `http.StatusBadRequest` | `400`
Missing resource | `http.StatusNotFound` | `404`
Something unexpected happened server side | `http.StatusInternalServerError` | `500`

## What files do what
* Descriptions of document types can be found in `db/user.go` and `db/program.go`.
* Endpoint functionality is divided up into `db/userManagement.go` and `db/programManagement.go`.
* All tests are of the form `fileToTestName_test.go`.
* Middleware that handles certain request-specific options prior to handing off the request to the default handler can be found in `middleware/`.
* `hooks/` harbors our git pre-commit hooks that enforce coding style.

## Endpoints

### `GET /programs/:id`, `GET /userData/:id`

Get an `User` or `Program` document with UID `:id` in JSON form.

Example nominal response:

```json
// example /programs/ response
// response code 200
{
"code": "def howdy():\n print('hi')\n",
"dateCreated": "2019-12-14T19:14:08.457733Z",
"language": "python",
"name": "Program name",
"thumbnail": 0
}

// example /userData/ response
// response code 200
{
"displayName": "Joe Bruin",
"photoName": "",
"mostRecentProgram": "PROGHASH",
"programs": [
"HASH0",
"HASH1",
"HASH2"
],
"classes": null
}
```

### `PUT /programs/:id`, `PUT /userData/:id`

Updates the user or program document with uid `:id` with the data provided in the request body. The `/programs/` endpoint takes an array of `Program`s in the request body, while the `/userData/` endpoint takes one or more `User` fields. These endpoints, actually, **aren't properly implemented yet**. Here's what the requests should look like:

Example Request:

```json
// sample user request body
{
"displayName": "TLA Dev Team"
}

// sample program request body
{
"HASH0": {
"name": "my updated program name",
"code": "print('here\'s my new code for the program!')"
},
"HASH1": {
"name": "another program, this time just updating the name."
}
}
```

Example nominal response: `200`

### `POST /programs/`

Creates a new program document associated to a user with information as supplied through the request body:

```json
{
"uid": "my cool user ID!",
"name": "my neato processing program!",
"language": "processing",
"thumbnail": 25
}
```

Example nominal response:

```json
// response code: 200
{
"displayName": "J Bruin",
"photoName": "",
"mostRecentProgram": "",
"programs": [
"HASH0",
"HASH1",
"HASH2"
],
"classes": null
}
```

### `POST /userData/`

Creates a new `User` document with the default programs. There are no special requirements for the request body.

Example nominal response:

```json
// response code: 200
{
"displayName": "J Bruin",
"photoName": "",
"mostRecentProgram": "",
"programs": [
"HASH0",
"HASH1",
"HASH2"
],
"classes": null
}
```

### `DELETE /programs/:id`

Delete the program with uid `:id` from the user with uid `:uid`, as provided in the request body.

```json
{
"uid": "my cool program ID"
}
```
## Documentation

Example nominal response: `200`
For a formal description of the endpoints for our backend, you can scan through the documentation provided in the repository's website link on GitHub.
4 changes: 2 additions & 2 deletions db/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"time"

"../tools/log"
"github.com/uclaacm/teach-la-go-backend/tools/log"
)

const (
Expand Down Expand Up @@ -90,7 +90,7 @@ func defaultProgram(languageCode int) (defaultProg Program) {
//defaultProg.UID = uid
defaultProg.Code = defaultCode
defaultProg.Language, _ = LanguageName(languageCode)
defaultProg.DateCreated = time.Now().UTC()
defaultProg.DateCreated = time.Now().UTC().String()

return
}
Expand Down
69 changes: 44 additions & 25 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package db

import (
"context"
"fmt"
"os"

"cloud.google.com/go/firestore"
Expand All @@ -15,32 +16,25 @@ type DB struct {
*firestore.Client
}

// OpenFromCreds returns a pointer to a database client based on
// JSON credentials pointed to by the provided path.
// OpenFromEnv returns a pointer to a database client based on
// JSON credentials given by the environment variable.
// Returns an error if it fails at any point.
func OpenFromCreds(ctx context.Context, path string) (*DB, error) {
// check, using os.Stat(), that the file exists. If it does not exist,
// then fail.
if _, err := os.Stat(path); err != nil {
return nil, err
func OpenFromEnv(ctx context.Context) (*DB, error) {
cfg := os.Getenv("TLACFG")
if cfg == "" {
return nil, fmt.Errorf("no $TLACFG environment variable provided")
}

// set up the app through which our client will be
// acquired.
opt := option.WithCredentialsFile(path)
opt := option.WithCredentialsJSON([]byte(cfg))
app, err := firebase.NewApp(ctx, nil, opt)

// acquire the firestore client, fail if we cannot.
client, err := app.Firestore(ctx)
return &DB{Client: client}, err
}

// OpenFromEnv calls OpenFromCreds with the path
// provided by your environment variable $CFGPATH.
func OpenFromEnv(ctx context.Context) (*DB, error) {
return OpenFromCreds(ctx, os.Getenv(DefaultEnvVar))
}

// CreateUser creates the default user and program documents,
// then returns the object for said User.
func (d *DB) CreateUser(ctx context.Context) (*User, error) {
Expand Down Expand Up @@ -90,16 +84,25 @@ func (d *DB) UpdateUser(ctx context.Context, uid string, u *User) error {

// DeleteProgramFromUser takes a uid and a pid,
// and deletes the pid from the User with the given uid
func (d *DB) DeleteProgramFromUser(ctx context.Context, uid string, pid string) error {

//get the user doc
func (d *DB) DeleteProgramFromUser(ctx context.Context, uid string, pid string) (err error) {
doc := d.Collection(UsersPath).Doc(uid)

_, err := doc.Update(ctx, []firestore.Update{
var ud *firestore.DocumentSnapshot
u := User{}
if ud, err = doc.Get(ctx); err == nil {
if err = ud.DataTo(&u); err != nil {
return
}
} else {
return
}

_, err = doc.Update(ctx, []firestore.Update{
{Path: "mostRecentProgram", Value: int(-1)},
{Path: "programs", Value: firestore.ArrayRemove(pid)},
})

return err
return
}

// AddProgramToUser takes a uid and a pid,
Expand Down Expand Up @@ -142,14 +145,30 @@ func (d *DB) GetProgram(ctx context.Context, uid string) (*Program, error) {
return p, doc.DataTo(p)
}

// UpdateProgram updates the program with the given uid to match
// the program provided as an argument.
// UpdateProgram updates the program with given pid belonging to the
// user with given uid to match the *Program provided as an argument.
// An error is returned if any issues are encountered.
func (d *DB) UpdateProgram(ctx context.Context, uid string, p *Program) error {
doc := d.Collection(ProgramsPath).Doc(uid)
func (d *DB) UpdateProgram(ctx context.Context, uid string, pid string, progUpdate *Program) error {
userSnapshot, err := d.Collection(UsersPath).Doc(uid).Get(ctx)
if err != nil {
return err
}

_, err := doc.Update(ctx, p.ToFirestoreUpdate())
return err
// validate that the program belongs to the user.
u := User{}
if err := userSnapshot.DataTo(&u); err != nil {
return err
}

for _, userPID := range u.Programs {
if pid == userPID {
// found the program, proceed
_, err := d.Collection(ProgramsPath).Doc(pid).Update(ctx, progUpdate.ToFirestoreUpdate())
return err
}
}

return fmt.Errorf("pid does not belong to user")
}

// DeleteProgram deletes the program with the given uid. An error
Expand Down
14 changes: 6 additions & 8 deletions db/program.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package db

import (
"time"

"cloud.google.com/go/firestore"
)

// Program is a representation of a program document.
type Program struct {
Code string `firestore:"code" json:"code"`
DateCreated time.Time `firestore:"dateCreated" json:"dateCreated"`
Language string `firestore:"language" json:"language"`
Name string `firestore:"name" json:"name"`
Thumbnail int64 `firestore:"thumbnail" json:"thumbnail"`
UID string `json:"uid"`
Code string `firestore:"code" json:"code"`
DateCreated string `firestore:"dateCreated" json:"dateCreated"`
Language string `firestore:"language" json:"language"`
Name string `firestore:"name" json:"name"`
Thumbnail int `firestore:"thumbnail" json:"thumbnail"`
UID string `json:"uid"`
// PID string `json:"pid"`
}

Expand Down
Loading