Skip to content

royallthefourth/bartlett

Repository files navigation

Bartlett

GoDoc Go Report Card CircleCI codecov

Bartlett is a library that automatically generates a CRUD API for your Go web application.

Usage

Invoke Bartlett by providing a database connection, a Bartlett driver, a slice of tables, and a function that returns a userID. Bartlett will return a slice of structs with paths corresponding to your table names and a request handler for each one.

Server Setup

package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    _ "github.com/go-sql-driver/mysql"
    "github.com/royallthefourth/bartlett"
    "github.com/royallthefourth/bartlett/mariadb"
)

func indexPage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `Welcome to your Bartlett application! The interesting parts are mounted under /api`)
}

func dummyUserProvider(_ *http.Request) (interface{}, error) {
    return 0, nil // In a real application, use a closure that includes your session handler to generate a user ID. 
}

func main() {
    http.HandleFunc(`/`, indexPage)
    
    // The students table will be available from the API, but the rest of the database will not.
    tables := []bartlett.Table{
    	{
            Name: `students`,
            UserID: `student_id`, // Requests will only return rows corresponding to their ID for this table.
    	},
    }
    db, err := sql.Open("mysql", ":@/school")
    if err != nil {
        log.Fatal(err)
    }
    
    // Bartlett is not a web application.
    // Instead, it is a tool that allows you to quickly add an API to your existing application.
    b := bartlett.Bartlett{DB: db, Driver: &mariadb.MariaDB{}, Tables: tables, Users: dummyUserProvider}
    routes := b.Routes()
    for _, route := range routes {
    	http.HandleFunc(`/api` + route.Path, route.Handler) // Adds /api/students to the server.
    }
    
    log.Fatal(http.ListenAndServe(`:8080`, nil))
}

See the todo list demo application for a bigger example.

Choosing Tables

You may manually select tables to put into your API by providing a slice of bartlett.Table when you create your Bartlett struct. As a quick alternative, you may also invoke the Bartlett.probeTables() method to populate the internal table list automatically:

b := bartlett.Bartlett{DB: db, Driver: &mariadb.MariaDB{}, Users: dummyUserProvider}
b.ProbeTables(false)

ProbeTables accepts one argument to decide whether the probed tables should be writable or not. This should almost always be set to false!

Querying

SELECT

To SELECT from a table, make a GET request to its corresponding URL. For example, SELECT * FROM students; against the example above may be achieved by curl -XGET http://localhost:8080/students The result set will be emitted as a JSON array:

[
    {
        "student_id": 1,
        "age": 18,
        "grade": 85
    },
    {
        "student_id": 2,
        "age": 20,
        "grade": 91
    }
]

Note that all results are emitted as an array, even if there is only one row.

Requests may filter columns by the select= query parameter, eg /students?select=student_id,grade

WHERE

To filter on simple WHERE conditions, specify a column name as a query string parameter and the conditions as the value. For example: /students?age=eq.20 produces WHERE age = 20.

Operator SQL Note
eq =
neq !=
gt >
gte >=
lt <
lte <=
like LIKE use * in place of %
is IS eg is.true or is.null
in IN eg in."hi, there","bye"

Any of these conditions can be negated by prefixing it with not. eg /students?age=not.eq.20

ORDER BY

To order results, add order to the query: /students?order=student_id

Order by mutliple columns by separating them with ,: /students?order=age,grade

Choose ASC or DESC by appending .asc or .desc to the field name: /students?order=age.asc,grade.desc

LIMIT and OFFSET

To restrict result output, add limit. The request /students?limit=10 will return 10 results.

To add an offset, use offset in your query: /students?limit=10&offset=2 will return 10 after skipping the first 2 results.

INSERT

To write rows to a table, make a POST request to the corresponding table's URL. Your request payload may come in the form of a JSON array of rows to insert or a single value.

Inserts return an object containing an array of error messages and the IDs of all successful inserts.

To generate your own surrogate key for each row, identify in your Table struct an IDColumn. Provide a function that returns a new ID each time it's invoked. This column will be protected from tampering by users. The UserID column is also filtered out incoming POST requests.

UPDATE

To run an UPDATE query, issue a PATCH request. Set your WHERE params on the URL exactly the way you do with a SELECT. Any PATCH requests that do not have a WHERE will be rejected for your safety.

PATCH requests must include a JSON payload body with the fields to be updated and their values:

{
  "age": 71,
  "name": "Alex"
}

DELETE

To delete rows from a table, make a DELETE request to the corresponding table's URL.

You must specify at least one WHERE clause, otherwise the request will return an error. This is a design feature to prevent users from deleting everything by mistake.

Status

This project is under heavy development. Bartlett currently supports SQLite3 and MariaDB. Postgres support is planned once support for existing databases is more robust. Most data types are not yet under test and may not produce useful results. Some MariaDB types do not have a clear JSON representation. These types are marshaled as []byte.

Security

Taking user input from the web to paste into a SQL query does present some hazards. The only place where user input is placed into queries is by parameter placeholders. All other dynamic SQL strings are generated from the strings passed into the arguments at startup time, never from the URL.

To restrict access per-row, specify a column name in your Table.UserID. Tables with a UserID set will always filter according to Table.UserID = ? with the result of the userProvider function.

Prior Art

This project is inspired by Postgrest. Instead of something that runs everything on its own, though, I prefer a tool that integrates with my existing application.