├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── apis ├── apis_test.go ├── artist.go ├── artist_test.go ├── auth.go ├── auth_test.go ├── util.go └── util_test.go ├── app ├── config.go ├── init.go ├── logger.go ├── scope.go ├── transactional.go └── version.go ├── config ├── app.yaml └── errors.yaml ├── daos ├── artist.go ├── artist_test.go └── daos_test.go ├── errors ├── api_error.go ├── api_error_test.go ├── errors.go ├── errors_test.go ├── template.go └── template_test.go ├── glide.yaml ├── models ├── artist.go └── identity.go ├── server.go ├── services ├── artist.go └── artist_test.go ├── testdata ├── README.md ├── db.sql └── init.go └── util ├── paginated_list.go └── paginated_list_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | glide.lock 27 | coverage.out 28 | coverage-all.out 29 | build 30 | server 31 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | - 1.7 6 | - tip 7 | 8 | cache: 9 | directories: 10 | - vendor 11 | 12 | services: 13 | - postgresql 14 | 15 | install: 16 | - go get golang.org/x/tools/cmd/cover 17 | - go get github.com/mattn/goveralls 18 | - go get github.com/axw/gocov/gocov 19 | - go get -u github.com/Masterminds/glide 20 | - glide up -s -u 21 | 22 | before_script: 23 | - psql -U postgres -c 'CREATE DATABASE go_restful;'; 24 | 25 | script: 26 | - make test 27 | - $HOME/gopath/bin/goveralls -service=travis-ci 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Qiang Xue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAIN_VERSION:=$(shell git describe --abbrev=0 --tags || echo "0.1") 2 | VERSION:=${MAIN_VERSION}\#$(shell git log -n 1 --pretty=format:"%h") 3 | PACKAGES:=$(shell go list ./... | sed -n '1!p' | grep -v /vendor/) 4 | LDFLAGS:=-ldflags "-X github.com/qiangxue/golang-restful-starter-kit/app.Version=${VERSION}" 5 | 6 | default: run 7 | 8 | test: 9 | go test -p=1 -cover -covermode=count ${PACKAGES} 10 | 11 | cover: 12 | echo "mode: count" > coverage-all.out 13 | $(foreach pkg,$(PACKAGES), \ 14 | echo ${pkg}; \ 15 | go test -p=1 -cover -covermode=count -coverprofile=coverage.out ${pkg}; \ 16 | tail -n +2 coverage.out >> coverage-all.out;) 17 | go tool cover -html=coverage-all.out 18 | 19 | run: 20 | go run ${LDFLAGS} server.go 21 | 22 | build: clean 23 | go build ${LDFLAGS} -a -o server server.go 24 | 25 | clean: 26 | rm -rf server coverage.out coverage-all.out 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go RESTful Application Starter Kit 2 | 3 | [![GoDoc](https://godoc.org/github.com/qiangxue/golang-restful-starter-kit?status.png)](http://godoc.org/github.com/qiangxue/golang-restful-starter-kit) 4 | [![Build Status](https://travis-ci.org/qiangxue/golang-restful-starter-kit.svg?branch=master)](https://travis-ci.org/qiangxue/golang-restful-starter-kit) 5 | [![Coverage Status](https://coveralls.io/repos/github/qiangxue/golang-restful-starter-kit/badge.svg?branch=master)](https://coveralls.io/github/qiangxue/golang-restful-starter-kit?branch=master) 6 | [![Go Report](https://goreportcard.com/badge/github.com/qiangxue/golang-restful-starter-kit)](https://goreportcard.com/report/github.com/qiangxue/golang-restful-starter-kit) 7 | 8 | This starter kit is designed to get you up and running with a project structure optimal for developing 9 | RESTful services in Go. The kit promotes the best practices that follow the [SOLID principles](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) 10 | and encourage writing clear and idiomatic Go code. 11 | 12 | The kit provides the following features right out of the box 13 | 14 | * RESTful endpoints in the widely accepted format 15 | * Standard CRUD operations of a database table 16 | * JWT-based authentication 17 | * Application configuration via environment variable and configuration file 18 | * Structured logging with contextual information 19 | * Panic handling and proper error response generation 20 | * Automatic DB transaction handling 21 | * Data validation 22 | * Full test coverage 23 | 24 | The kit uses the following Go packages which can be easily replaced with your own favorite ones 25 | since their usages are mostly localized and abstracted. 26 | 27 | * Routing framework: [ozzo-routing](https://github.com/go-ozzo/ozzo-routing) 28 | * Database: [ozzo-dbx](https://github.com/go-ozzo/ozzo-dbx) 29 | * Data validation: [ozzo-validation](https://github.com/go-ozzo/ozzo-validation) 30 | * Logging: [logrus](https://github.com/Sirupsen/logrus) 31 | * Configuration: [viper](https://github.com/spf13/viper) 32 | * Dependency management: [glide](https://github.com/Masterminds/glide) 33 | * Testing: [testify](https://github.com/stretchr/testify) 34 | 35 | 36 | ## Getting Started 37 | 38 | If this is your first time encountering Go, please follow [the instructions](https://golang.org/doc/install) to 39 | install Go on your computer. The kit requires Go 1.5 or above. 40 | 41 | After installing Go, run the following commands to download and install this starter kit: 42 | 43 | ```shell 44 | # install the starter kit 45 | go get github.com/qiangxue/golang-restful-starter-kit 46 | 47 | # install glide (a vendoring and dependency management tool), if you don't have it yet 48 | go get -u github.com/Masterminds/glide 49 | 50 | # fetch the dependent packages 51 | cd $GOPATH/qiangxue/golang-restful-starter-kit 52 | glide up -u -s 53 | ``` 54 | 55 | Next, create a PostgreSQL database named `go_restful` and execute the SQL statements given in the file `data/db.sql`. 56 | The starter kit uses the following default database connection information: 57 | * server address: `127.0.0.1` (local machine) 58 | * server port: `5432` 59 | * database name: `go_restful` 60 | * username: `postgres` 61 | * password: `postgres` 62 | 63 | If your connection is different from the above, you may modify the configuration file `config/app.yaml`, or 64 | define an environment variable named `RESTFUL_DSN` like the following: 65 | 66 | ``` 67 | postgres://:@:/ 68 | ``` 69 | 70 | For more details about specifying a PostgreSQL DSN, please refer to [the documentation](https://godoc.org/github.com/lib/pq). 71 | 72 | Now you can build and run the application by running the following command under the 73 | `$GOPATH/qiangxue/golang-restful-starter-kit` directory: 74 | 75 | ```shell 76 | go run server.go 77 | ``` 78 | 79 | or simply the following if you have the `make` tool: 80 | 81 | ```shell 82 | make 83 | ``` 84 | 85 | The application runs as an HTTP server at port 8080. It provides the following RESTful endpoints: 86 | 87 | * `GET /ping`: a ping service mainly provided for health check purpose 88 | * `POST /v1/auth`: authenticate a user 89 | * `GET /v1/artists`: returns a paginated list of the artists 90 | * `GET /v1/artists/:id`: returns the detailed information of an artist 91 | * `POST /v1/artists`: creates a new artist 92 | * `PUT /v1/artists/:id`: updates an existing artist 93 | * `DELETE /v1/artists/:id`: deletes an artist 94 | 95 | For example, if you access the URL `http://localhost:8080/ping` in a browser, you should see the browser 96 | displays something like `OK v0.1#bc41dce`. 97 | 98 | If you have `cURL` or some API client tools (e.g. Postman), you may try the following more complex scenarios: 99 | 100 | ```shell 101 | # authenticate the user via: POST /v1/auth 102 | curl -X POST -H "Content-Type: application/json" -d '{"username": "demo", "password": "pass"}' http://localhost:8080/v1/auth 103 | # should return a JWT token like: {"token":"...JWT token here..."} 104 | 105 | # with the above JWT token, access the artist resources, such as: GET /v1/artists 106 | curl -X GET -H "Authorization: Bearer ...JWT token here..." http://localhost:8080/v1/artists 107 | # should return a list of artist records in the JSON format 108 | ``` 109 | 110 | ## Next Steps 111 | 112 | In this section, we will describe the steps you may take to make use of this starter kit in a real project. 113 | You may jump to the [Project Structure](#project-structure) section if you mainly want to learn about 114 | the project structure and the recommended practices. 115 | 116 | ### Renaming the Project 117 | 118 | To use the starter kit as a starting point of a real project whose package name is something like 119 | `github.com/abc/xyz`, take the following steps: 120 | 121 | * move the directory `$GOPATH/github.com/qiangxue/golang-restful-starter-kit` to `$GOPATH/github.com/abc/xyz` 122 | * do a global replacement of the string `github.com/qiangxue/golang-restful-starter-kit` in all of 123 | project files with the string `github.com/abc/xyz` 124 | 125 | ### Implementing CRUD of Another Table 126 | 127 | To implement the CRUD APIs of another database table (assuming it is named as `album`), 128 | you will need to develop the following files which are similar to the `artist.go` file in each folder: 129 | 130 | * `models/album.go`: contains the data structure representing a row in the new table. 131 | * `services/album.go`: contains the business logic that implements the CRUD operations. 132 | * `daos/album.go`: contains the DAO (Data Access Object) layer that interacts with the database table. 133 | * `apis/album.go`: contains the API layer that wires up the HTTP routes with the corresponding service APIs. 134 | 135 | Then, wire them up by modifying the `serveResources()` function in the `server.go` file. 136 | 137 | ### Implementing a non-CRUD API 138 | 139 | * If the API uses a request/response structure that is different from a database model, 140 | define the request/response model(s) in the `models` package. 141 | * In the `services` package create a service type that should contain the main service logic for the API. 142 | If the service logic is very complex or there are multiple related APIs, you may create 143 | a package under `services` to host them. 144 | * If the API needs to interact with the database or other persistent storage, create 145 | a DAO type in the `daos` package. Otherwise, the DAO type can be skipped. 146 | * In the `apis` package, define the HTTP route and the corresponding API handler. 147 | * Finally, modify the `serveResources()` function in the `server.go` file to wire up the new API. 148 | 149 | ## Project Structure 150 | 151 | This starter kit divides the whole project into four main packages: 152 | 153 | * `models`: contains the data structures used for communication between different layers. 154 | * `services`: contains the main business logic of the application. 155 | * `daos`: contains the DAO (Data Access Object) layer that interacts with persistent storage. 156 | * `apis`: contains the API layer that wires up the HTTP routes with the corresponding service APIs. 157 | 158 | [Dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) 159 | is followed to make these packages independent of each other and thus easier to test and maintain. 160 | 161 | The rest of the packages in the kit are used globally: 162 | 163 | * `app`: contains routing middlewares and application-level configurations 164 | * `errors`: contains error representation and handling 165 | * `util`: contains utility code 166 | 167 | The main entry of the application is in the `server.go` file. It does the following work: 168 | 169 | * load external configuration 170 | * establish database connection 171 | * instantiate components and inject dependencies 172 | * start the HTTP server 173 | -------------------------------------------------------------------------------- /apis/apis_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/go-ozzo/ozzo-routing" 11 | "github.com/go-ozzo/ozzo-routing/content" 12 | "github.com/qiangxue/golang-restful-starter-kit/app" 13 | "github.com/qiangxue/golang-restful-starter-kit/testdata" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type apiTestCase struct { 18 | tag string 19 | method string 20 | url string 21 | body string 22 | status int 23 | response string 24 | } 25 | 26 | var router *routing.Router 27 | 28 | func init() { 29 | logger := logrus.New() 30 | logger.Level = logrus.PanicLevel 31 | 32 | router = routing.New() 33 | 34 | router.Use( 35 | app.Init(logger), 36 | content.TypeNegotiator(content.JSON), 37 | app.Transactional(testdata.DB), 38 | ) 39 | } 40 | 41 | func testAPI(method, URL, body string) *httptest.ResponseRecorder { 42 | req, _ := http.NewRequest(method, URL, bytes.NewBufferString(body)) 43 | req.Header.Set("Content-Type", "application/json") 44 | res := httptest.NewRecorder() 45 | router.ServeHTTP(res, req) 46 | return res 47 | } 48 | 49 | func runAPITests(t *testing.T, tests []apiTestCase) { 50 | for _, test := range tests { 51 | res := testAPI(test.method, test.url, test.body) 52 | assert.Equal(t, test.status, res.Code, test.tag) 53 | if test.response != "" { 54 | assert.JSONEq(t, test.response, res.Body.String(), test.tag) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apis/artist.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/go-ozzo/ozzo-routing" 7 | "github.com/qiangxue/golang-restful-starter-kit/app" 8 | "github.com/qiangxue/golang-restful-starter-kit/models" 9 | ) 10 | 11 | type ( 12 | // artistService specifies the interface for the artist service needed by artistResource. 13 | artistService interface { 14 | Get(rs app.RequestScope, id int) (*models.Artist, error) 15 | Query(rs app.RequestScope, offset, limit int) ([]models.Artist, error) 16 | Count(rs app.RequestScope) (int, error) 17 | Create(rs app.RequestScope, model *models.Artist) (*models.Artist, error) 18 | Update(rs app.RequestScope, id int, model *models.Artist) (*models.Artist, error) 19 | Delete(rs app.RequestScope, id int) (*models.Artist, error) 20 | } 21 | 22 | // artistResource defines the handlers for the CRUD APIs. 23 | artistResource struct { 24 | service artistService 25 | } 26 | ) 27 | 28 | // ServeArtist sets up the routing of artist endpoints and the corresponding handlers. 29 | func ServeArtistResource(rg *routing.RouteGroup, service artistService) { 30 | r := &artistResource{service} 31 | rg.Get("/artists/", r.get) 32 | rg.Get("/artists", r.query) 33 | rg.Post("/artists", r.create) 34 | rg.Put("/artists/", r.update) 35 | rg.Delete("/artists/", r.delete) 36 | } 37 | 38 | func (r *artistResource) get(c *routing.Context) error { 39 | id, err := strconv.Atoi(c.Param("id")) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | response, err := r.service.Get(app.GetRequestScope(c), id) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return c.Write(response) 50 | } 51 | 52 | func (r *artistResource) query(c *routing.Context) error { 53 | rs := app.GetRequestScope(c) 54 | count, err := r.service.Count(rs) 55 | if err != nil { 56 | return err 57 | } 58 | paginatedList := getPaginatedListFromRequest(c, count) 59 | items, err := r.service.Query(app.GetRequestScope(c), paginatedList.Offset(), paginatedList.Limit()) 60 | if err != nil { 61 | return err 62 | } 63 | paginatedList.Items = items 64 | return c.Write(paginatedList) 65 | } 66 | 67 | func (r *artistResource) create(c *routing.Context) error { 68 | var model models.Artist 69 | if err := c.Read(&model); err != nil { 70 | return err 71 | } 72 | response, err := r.service.Create(app.GetRequestScope(c), &model) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | return c.Write(response) 78 | } 79 | 80 | func (r *artistResource) update(c *routing.Context) error { 81 | id, err := strconv.Atoi(c.Param("id")) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | rs := app.GetRequestScope(c) 87 | 88 | model, err := r.service.Get(rs, id) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | if err := c.Read(model); err != nil { 94 | return err 95 | } 96 | 97 | response, err := r.service.Update(rs, id, model) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return c.Write(response) 103 | } 104 | 105 | func (r *artistResource) delete(c *routing.Context) error { 106 | id, err := strconv.Atoi(c.Param("id")) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | response, err := r.service.Delete(app.GetRequestScope(c), id) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return c.Write(response) 117 | } 118 | -------------------------------------------------------------------------------- /apis/artist_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/qiangxue/golang-restful-starter-kit/daos" 8 | "github.com/qiangxue/golang-restful-starter-kit/services" 9 | "github.com/qiangxue/golang-restful-starter-kit/testdata" 10 | ) 11 | 12 | func TestArtist(t *testing.T) { 13 | testdata.ResetDB() 14 | ServeArtistResource(&router.RouteGroup, services.NewArtistService(daos.NewArtistDAO())) 15 | 16 | notFoundError := `{"error_code":"NOT_FOUND", "message":"NOT_FOUND"}` 17 | nameRequiredError := `{"error_code":"INVALID_DATA","message":"INVALID_DATA","details":[{"field":"Name","error":"cannot be blank"}]}` 18 | 19 | runAPITests(t, []apiTestCase{ 20 | {"t1 - get an artist", "GET", "/artists/2", "", http.StatusOK, `{"id":2,"name":"Accept"}`}, 21 | {"t2 - get a nonexisting artist", "GET", "/artists/99999", "", http.StatusNotFound, notFoundError}, 22 | {"t3 - create an artist", "POST", "/artists", `{"name":"Qiang"}`, http.StatusOK, `{"id": 276, "name":"Qiang"}`}, 23 | {"t4 - create an artist with validation error", "POST", "/artists", `{"name":""}`, http.StatusBadRequest, nameRequiredError}, 24 | {"t5 - update an artist", "PUT", "/artists/2", `{"name":"Qiang"}`, http.StatusOK, `{"id": 2, "name":"Qiang"}`}, 25 | {"t6 - update an artist with validation error", "PUT", "/artists/2", `{"name":""}`, http.StatusBadRequest, nameRequiredError}, 26 | {"t7 - update a nonexisting artist", "PUT", "/artists/99999", "{}", http.StatusNotFound, notFoundError}, 27 | {"t8 - delete an artist", "DELETE", "/artists/2", ``, http.StatusOK, `{"id": 2, "name":"Qiang"}`}, 28 | {"t9 - delete a nonexisting artist", "DELETE", "/artists/99999", "", http.StatusNotFound, notFoundError}, 29 | {"t10 - get a list of artists", "GET", "/artists?page=3&per_page=2", "", http.StatusOK, `{"page":3,"per_page":2,"page_count":138,"total_count":275,"items":[{"id":6,"name":"Antônio Carlos Jobim"},{"id":7,"name":"Apocalyptica"}]}`}, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /apis/auth.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/go-ozzo/ozzo-routing" 8 | "github.com/go-ozzo/ozzo-routing/auth" 9 | "github.com/qiangxue/golang-restful-starter-kit/errors" 10 | "github.com/qiangxue/golang-restful-starter-kit/models" 11 | ) 12 | 13 | type Credential struct { 14 | Username string `json:"username"` 15 | Password string `json:"password"` 16 | } 17 | 18 | func Auth(signingKey string) routing.Handler { 19 | return func(c *routing.Context) error { 20 | var credential Credential 21 | if err := c.Read(&credential); err != nil { 22 | return errors.Unauthorized(err.Error()) 23 | } 24 | 25 | identity := authenticate(credential) 26 | if identity == nil { 27 | return errors.Unauthorized("invalid credential") 28 | } 29 | 30 | token, err := auth.NewJWT(jwt.MapClaims{ 31 | "id": identity.GetID(), 32 | "name": identity.GetName(), 33 | "exp": time.Now().Add(time.Hour * 72).Unix(), 34 | }, signingKey) 35 | if err != nil { 36 | return errors.Unauthorized(err.Error()) 37 | } 38 | 39 | return c.Write(map[string]string{ 40 | "token": token, 41 | }) 42 | } 43 | } 44 | 45 | func authenticate(c Credential) models.Identity { 46 | if c.Username == "demo" && c.Password == "pass" { 47 | return &models.User{ID: "100", Name: "demo"} 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /apis/auth_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "testing" 5 | "net/http" 6 | ) 7 | 8 | func TestAuth(t *testing.T) { 9 | router.Post("/auth", Auth("secret")) 10 | runAPITests(t, []apiTestCase{ 11 | {"t1 - successful login", "POST", "/auth", `{"username":"demo", "password":"pass"}`, http.StatusOK, ""}, 12 | {"t2 - unsuccessful login", "POST", "/auth", `{"username":"demo", "password":"bad"}`, http.StatusUnauthorized, ""}, 13 | }) 14 | } -------------------------------------------------------------------------------- /apis/util.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/go-ozzo/ozzo-routing" 7 | "github.com/qiangxue/golang-restful-starter-kit/util" 8 | ) 9 | 10 | const ( 11 | DEFAULT_PAGE_SIZE int = 100 12 | MAX_PAGE_SIZE int = 1000 13 | ) 14 | 15 | func getPaginatedListFromRequest(c *routing.Context, count int) *util.PaginatedList { 16 | page := parseInt(c.Query("page"), 1) 17 | perPage := parseInt(c.Query("per_page"), DEFAULT_PAGE_SIZE) 18 | if perPage <= 0 { 19 | perPage = DEFAULT_PAGE_SIZE 20 | } 21 | if perPage > MAX_PAGE_SIZE { 22 | perPage = MAX_PAGE_SIZE 23 | } 24 | return util.NewPaginatedList(page, perPage, count) 25 | } 26 | 27 | func parseInt(value string, defaultValue int) int { 28 | if value == "" { 29 | return defaultValue 30 | } 31 | if result, err := strconv.Atoi(value); err == nil { 32 | return result 33 | } 34 | return defaultValue 35 | } 36 | -------------------------------------------------------------------------------- /apis/util_test.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/go-ozzo/ozzo-routing" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_getPaginatedListFromRequest(t *testing.T) { 13 | tests := []struct { 14 | Tag string 15 | Page, PerPage int 16 | ExpPage, ExpPerPage int 17 | }{ 18 | {"t1", 1, 10, 1, 10}, 19 | {"t2", -1, -1, 1, DEFAULT_PAGE_SIZE}, 20 | {"t2", 0, 0, 1, DEFAULT_PAGE_SIZE}, 21 | {"t3", 2, MAX_PAGE_SIZE + 1, 2, MAX_PAGE_SIZE}, 22 | } 23 | for _, test := range tests { 24 | url := "http://www.example.com/search?foo=1" 25 | if test.Page >= 0 { 26 | url = fmt.Sprintf("%s&page=%v", url, test.Page) 27 | } 28 | if test.PerPage >= 0 { 29 | url = fmt.Sprintf("%s&per_page=%v", url, test.PerPage) 30 | } 31 | req, _ := http.NewRequest("GET", url, nil) 32 | c := routing.NewContext(nil, req) 33 | pl := getPaginatedListFromRequest(c, 100000) 34 | assert.Equal(t, test.ExpPage, pl.Page) 35 | assert.Equal(t, test.ExpPerPage, pl.PerPage) 36 | } 37 | } 38 | 39 | func Test_parseInt(t *testing.T) { 40 | assert.Equal(t, 123, parseInt("123", 1)) 41 | assert.Equal(t, 1, parseInt("a123", 1)) 42 | assert.Equal(t, 1, parseInt("", 1)) 43 | } 44 | -------------------------------------------------------------------------------- /app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/viper" 7 | "github.com/go-ozzo/ozzo-validation" 8 | ) 9 | 10 | var Config appConfig 11 | 12 | type appConfig struct { 13 | ErrorFile string `mapstructure:"error_file"` 14 | ServerPort int `mapstructure:"server_port"` 15 | DSN string `mapstructure:"dsn"` 16 | JWTSigningKey string `mapstructure:"jwt_signing_key"` 17 | JWTVerificationKey string `mapstructure:"jwt_verification_key"` 18 | } 19 | 20 | func (config *appConfig) Validate() error { 21 | return validation.StructRules{}. 22 | Add("ServerPort", validation.Required). 23 | Add("DSN", validation.Required). 24 | Add("JWTSigningKey", validation.Required). 25 | Add("JWTVerificationKey", validation.Required). 26 | Validate(config) 27 | } 28 | 29 | func LoadConfig(configPaths ...string) error { 30 | v := viper.New() 31 | v.AutomaticEnv() 32 | v.SetDefault("error_file", "config/errors.yaml") 33 | v.SetDefault("server_port", 8080) 34 | v.SetEnvPrefix("restful") 35 | v.SetConfigType("yaml") 36 | v.SetConfigName("app") 37 | for _, path := range configPaths { 38 | v.AddConfigPath(path) 39 | } 40 | if err := v.ReadInConfig(); err != nil { 41 | return fmt.Errorf("Failed to read the configuration file: %s", err) 42 | } 43 | if err := v.Unmarshal(&Config); err != nil { 44 | return err 45 | } 46 | return Config.Validate() 47 | } 48 | -------------------------------------------------------------------------------- /app/init.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/go-ozzo/ozzo-routing" 11 | "github.com/go-ozzo/ozzo-routing/access" 12 | "github.com/go-ozzo/ozzo-routing/fault" 13 | "github.com/go-ozzo/ozzo-validation" 14 | "github.com/qiangxue/golang-restful-starter-kit/errors" 15 | ) 16 | 17 | // Init returns a middleware that prepares the request context and processing environment. 18 | // The middleware will populate RequestContext, handle possible panics and errors from the processing 19 | // handlers, and add an access log entry. 20 | func Init(logger *logrus.Logger) routing.Handler { 21 | return func(rc *routing.Context) error { 22 | now := time.Now() 23 | 24 | rc.Response = &access.LogResponseWriter{rc.Response, http.StatusOK, 0} 25 | 26 | ac := newRequestScope(now, logger, rc.Request) 27 | rc.Set("Context", ac) 28 | 29 | fault.Recovery(ac.Errorf, convertError)(rc) 30 | logAccess(rc, ac.Infof, ac.Now()) 31 | 32 | return nil 33 | } 34 | } 35 | 36 | // GetRequestScope returns the RequestScope of the current request. 37 | func GetRequestScope(c *routing.Context) RequestScope { 38 | return c.Get("Context").(RequestScope) 39 | } 40 | 41 | // logAccess logs a message describing the current request. 42 | func logAccess(c *routing.Context, logFunc access.LogFunc, start time.Time) { 43 | rw := c.Response.(*access.LogResponseWriter) 44 | elapsed := float64(time.Now().Sub(start).Nanoseconds()) / 1e6 45 | requestLine := fmt.Sprintf("%s %s %s", c.Request.Method, c.Request.URL.Path, c.Request.Proto) 46 | logFunc(`[%.3fms] %s %d %d`, elapsed, requestLine, rw.Status, rw.BytesWritten) 47 | } 48 | 49 | // convertError converts an error into an APIError so that it can be properly sent to the response. 50 | // You may need to customize this method by adding conversion logic for more error types. 51 | func convertError(c *routing.Context, err error) error { 52 | if err == sql.ErrNoRows { 53 | return errors.NotFound("the requested resource") 54 | } 55 | switch err.(type) { 56 | case *errors.APIError: 57 | return err 58 | case validation.Errors: 59 | return errors.InvalidData(err.(validation.Errors)) 60 | case routing.HTTPError: 61 | switch err.(routing.HTTPError).StatusCode() { 62 | case http.StatusUnauthorized: 63 | return errors.Unauthorized(err.Error()) 64 | case http.StatusNotFound: 65 | return errors.NotFound("the requested resource") 66 | } 67 | } 68 | return errors.InternalServerError(err) 69 | } 70 | -------------------------------------------------------------------------------- /app/logger.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/Sirupsen/logrus" 4 | 5 | // Logger defines the logger interface that is exposed via RequestScope. 6 | type Logger interface { 7 | Debugf(format string, args ...interface{}) 8 | Infof(format string, args ...interface{}) 9 | Warnf(format string, args ...interface{}) 10 | Errorf(format string, args ...interface{}) 11 | Debug(args ...interface{}) 12 | Info(args ...interface{}) 13 | Warn(args ...interface{}) 14 | Error(args ...interface{}) 15 | } 16 | 17 | // logger wraps logrus.Logger so that it can log messages sharing a common set of fields. 18 | type logger struct { 19 | logger *logrus.Logger 20 | fields logrus.Fields 21 | } 22 | 23 | // NewLogger creates a logger object with the specified logrus.Logger and the fields that should be added to every message. 24 | func NewLogger(l *logrus.Logger, fields logrus.Fields) Logger { 25 | return &logger{ 26 | logger: l, 27 | fields: fields, 28 | } 29 | } 30 | 31 | func (l *logger) Debugf(format string, args ...interface{}) { 32 | l.tagged().Debugf(format, args...) 33 | } 34 | 35 | func (l *logger) Infof(format string, args ...interface{}) { 36 | l.tagged().Infof(format, args...) 37 | } 38 | 39 | func (l *logger) Warnf(format string, args ...interface{}) { 40 | l.tagged().Warnf(format, args...) 41 | } 42 | 43 | func (l *logger) Errorf(format string, args ...interface{}) { 44 | l.tagged().Errorf(format, args...) 45 | } 46 | 47 | func (l *logger) Debug(args ...interface{}) { 48 | l.tagged().Debug(args...) 49 | } 50 | 51 | func (l *logger) Info(args ...interface{}) { 52 | l.tagged().Info(args...) 53 | } 54 | 55 | func (l *logger) Warn(args ...interface{}) { 56 | l.tagged().Warn(args...) 57 | } 58 | 59 | func (l *logger) Error(args ...interface{}) { 60 | l.tagged().Error(args...) 61 | } 62 | 63 | func (l *logger) tagged() *logrus.Entry { 64 | return l.logger.WithFields(l.fields) 65 | } 66 | -------------------------------------------------------------------------------- /app/scope.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/go-ozzo/ozzo-dbx" 9 | ) 10 | 11 | // RequestScope contains the application-specific information that are carried around in a request. 12 | type RequestScope interface { 13 | Logger 14 | // UserID returns the ID of the user for the current request 15 | UserID() string 16 | // Tx returns the currently active database transaction that can be used for DB query purpose 17 | Tx() *dbx.Tx 18 | // SetTx sets the database transaction 19 | SetTx(tx *dbx.Tx) 20 | // Rollback returns a value indicating whether the current database transaction should be rolled back 21 | Rollback() bool 22 | // SetRollback sets a value indicating whether the current database transaction should be rolled back 23 | SetRollback(bool) 24 | // Now returns the timestamp representing the time when the request is being processed 25 | Now() time.Time 26 | } 27 | 28 | type requestScope struct { 29 | Logger // the logger tagged with the current request information 30 | now time.Time // the time when the request is being processed 31 | userID string // an ID identifying the current user 32 | rollback bool // whether to roll back the current transaction 33 | tx *dbx.Tx // the currently active transaction 34 | } 35 | 36 | func (rs *requestScope) UserID() string { 37 | return rs.userID 38 | } 39 | 40 | func (rs *requestScope) Tx() *dbx.Tx { 41 | return rs.tx 42 | } 43 | 44 | func (rs *requestScope) SetTx(tx *dbx.Tx) { 45 | rs.tx = tx 46 | } 47 | 48 | func (rs *requestScope) Rollback() bool { 49 | return rs.rollback 50 | } 51 | 52 | func (rs *requestScope) SetRollback(v bool) { 53 | rs.rollback = v 54 | } 55 | 56 | func (rs *requestScope) Now() time.Time { 57 | return rs.now 58 | } 59 | 60 | // newRequestScope creates a new RequestScope with the current request information. 61 | func newRequestScope(now time.Time, logger *logrus.Logger, request *http.Request) RequestScope { 62 | userID := authenticate(request) 63 | return &requestScope{ 64 | Logger: NewLogger(logger, logrus.Fields{ 65 | "UserID": userID, 66 | }), 67 | now: now, 68 | userID: userID, 69 | } 70 | } 71 | 72 | // authenticate authenticates the current user. 73 | // The default implementation simply returns the value of the X-User-Id HTTP header, which assumes 74 | // the authentication has been done by an API gateway. 75 | // If this is not the case, you may customize this method with the actual authentication logic. 76 | func authenticate(request *http.Request) string { 77 | return request.Header.Get("X-User-Id") 78 | } 79 | -------------------------------------------------------------------------------- /app/transactional.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/go-ozzo/ozzo-dbx" 5 | "github.com/go-ozzo/ozzo-routing" 6 | "github.com/go-ozzo/ozzo-routing/fault" 7 | ) 8 | 9 | // Transactional returns a handler that encloses the nested handlers with a DB transaction. 10 | // If a nested handler returns an error or a panic happens, it will rollback the transaction. 11 | // Otherwise it will commit the transaction after the nested handlers finish execution. 12 | // By calling app.Context.SetRollback(true), you may also explicitly request to rollback the transaction. 13 | func Transactional(db *dbx.DB) routing.Handler { 14 | return func(c *routing.Context) error { 15 | tx, err := db.Begin() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | rs := GetRequestScope(c) 21 | rs.SetTx(tx) 22 | 23 | err = fault.PanicHandler(rs.Errorf)(c) 24 | 25 | var e error 26 | if err != nil || rs.Rollback() { 27 | // rollback if a handler returns an error or rollback is explicitly requested 28 | e = tx.Rollback() 29 | } else { 30 | e = tx.Commit() 31 | } 32 | 33 | if e != nil { 34 | if err == nil { 35 | // the error will be logged by an error handler 36 | return e 37 | } 38 | // log the tx error only 39 | rs.Error(e) 40 | } 41 | 42 | return err 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | // Version specifies the current version of the application. 4 | // The value of this variable is replaced with the latest git tag 5 | // by "make" while building or running the application. 6 | var Version = "1.0" 7 | -------------------------------------------------------------------------------- /config/app.yaml: -------------------------------------------------------------------------------- 1 | # The Data Source Name for the database 2 | # Make sure you override this in production with the environment variable: RESTFUL_DSN 3 | dsn: "postgres://postgres:postgres@127.0.0.1:5432/go_restful?sslmode=disable" 4 | 5 | # These are secret keys used for JWT signing and verification. 6 | # Make sure you override these keys in production by the following environment variables: 7 | # RESTFUL_JWT_VERIFICATION_KEY 8 | # RESTFUL_JWT_SIGNING_KEY 9 | jwt_verification_key: "QfCAH04Cob7b71QCqy738vw5XGSnFZ9d" 10 | jwt_signing_key: "QfCAH04Cob7b71QCqy738vw5XGSnFZ9d" -------------------------------------------------------------------------------- /config/errors.yaml: -------------------------------------------------------------------------------- 1 | INTERNAL_SERVER_ERROR: 2 | message: "We have encountered an internal server error." 3 | developer_message: "Internal server error: {error}" 4 | 5 | NOT_FOUND: 6 | message: "{resource} was not found." 7 | 8 | UNAUTHORIZED: 9 | message: "Authentication failed." 10 | developer_message: "Authentication failed: {error}" 11 | 12 | INVALID_DATA: 13 | message: "There is some problem with the data you submitted. See \"details\" for more information." 14 | -------------------------------------------------------------------------------- /daos/artist.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "github.com/qiangxue/golang-restful-starter-kit/app" 5 | "github.com/qiangxue/golang-restful-starter-kit/models" 6 | ) 7 | 8 | // ArtistDAO persists artist data in database 9 | type ArtistDAO struct{} 10 | 11 | // NewArtistDAO creates a new ArtistDAO 12 | func NewArtistDAO() *ArtistDAO { 13 | return &ArtistDAO{} 14 | } 15 | 16 | // Get reads the artist with the specified ID from the database. 17 | func (dao *ArtistDAO) Get(rs app.RequestScope, id int) (*models.Artist, error) { 18 | var artist models.Artist 19 | err := rs.Tx().Select().Model(id, &artist) 20 | return &artist, err 21 | } 22 | 23 | // Create saves a new artist record in the database. 24 | // The Artist.Id field will be populated with an automatically generated ID upon successful saving. 25 | func (dao *ArtistDAO) Create(rs app.RequestScope, artist *models.Artist) error { 26 | artist.Id = 0 27 | return rs.Tx().Model(artist).Insert() 28 | } 29 | 30 | // Update saves the changes to an artist in the database. 31 | func (dao *ArtistDAO) Update(rs app.RequestScope, id int, artist *models.Artist) error { 32 | if _, err := dao.Get(rs, id); err != nil { 33 | return err 34 | } 35 | artist.Id = id 36 | return rs.Tx().Model(artist).Exclude("Id").Update() 37 | } 38 | 39 | // Delete deletes an artist with the specified ID from the database. 40 | func (dao *ArtistDAO) Delete(rs app.RequestScope, id int) error { 41 | artist, err := dao.Get(rs, id) 42 | if err != nil { 43 | return err 44 | } 45 | return rs.Tx().Model(artist).Delete() 46 | } 47 | 48 | // Count returns the number of the artist records in the database. 49 | func (dao *ArtistDAO) Count(rs app.RequestScope) (int, error) { 50 | var count int 51 | err := rs.Tx().Select("COUNT(*)").From("artist").Row(&count) 52 | return count, err 53 | } 54 | 55 | // Query retrieves the artist records with the specified offset and limit from the database. 56 | func (dao *ArtistDAO) Query(rs app.RequestScope, offset, limit int) ([]models.Artist, error) { 57 | artists := []models.Artist{} 58 | err := rs.Tx().Select().OrderBy("id").Offset(int64(offset)).Limit(int64(limit)).All(&artists) 59 | return artists, err 60 | } 61 | -------------------------------------------------------------------------------- /daos/artist_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/qiangxue/golang-restful-starter-kit/app" 7 | "github.com/qiangxue/golang-restful-starter-kit/models" 8 | "github.com/qiangxue/golang-restful-starter-kit/testdata" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestArtistDAO(t *testing.T) { 13 | db := testdata.ResetDB() 14 | dao := NewArtistDAO() 15 | 16 | { 17 | // Get 18 | testDBCall(db, func(rs app.RequestScope) { 19 | artist, err := dao.Get(rs, 2) 20 | assert.Nil(t, err) 21 | if assert.NotNil(t, artist) { 22 | assert.Equal(t, 2, artist.Id) 23 | } 24 | }) 25 | } 26 | 27 | { 28 | // Create 29 | testDBCall(db, func(rs app.RequestScope) { 30 | artist := &models.Artist{ 31 | Id: 1000, 32 | Name: "tester", 33 | } 34 | err := dao.Create(rs, artist) 35 | assert.Nil(t, err) 36 | assert.NotEqual(t, 1000, artist.Id) 37 | assert.NotZero(t, artist.Id) 38 | }) 39 | } 40 | 41 | { 42 | // Update 43 | testDBCall(db, func(rs app.RequestScope) { 44 | artist := &models.Artist{ 45 | Id: 2, 46 | Name: "tester", 47 | } 48 | err := dao.Update(rs, artist.Id, artist) 49 | assert.Nil(t, rs.Tx().Commit()) 50 | assert.Nil(t, err) 51 | }) 52 | } 53 | 54 | { 55 | // Update with error 56 | testDBCall(db, func(rs app.RequestScope) { 57 | artist := &models.Artist{ 58 | Id: 2, 59 | Name: "tester", 60 | } 61 | err := dao.Update(rs, 99999, artist) 62 | assert.Nil(t, rs.Tx().Commit()) 63 | assert.NotNil(t, err) 64 | }) 65 | } 66 | 67 | { 68 | // Delete 69 | testDBCall(db, func(rs app.RequestScope) { 70 | err := dao.Delete(rs, 2) 71 | assert.Nil(t, rs.Tx().Commit()) 72 | assert.Nil(t, err) 73 | }) 74 | } 75 | 76 | { 77 | // Delete with error 78 | testDBCall(db, func(rs app.RequestScope) { 79 | err := dao.Delete(rs, 99999) 80 | assert.Nil(t, rs.Tx().Commit()) 81 | assert.NotNil(t, err) 82 | }) 83 | } 84 | 85 | { 86 | // Query 87 | testDBCall(db, func(rs app.RequestScope) { 88 | artists, err := dao.Query(rs, 1, 3) 89 | assert.Nil(t, rs.Tx().Commit()) 90 | assert.Nil(t, err) 91 | assert.Equal(t, 3, len(artists)) 92 | }) 93 | } 94 | 95 | { 96 | // Count 97 | testDBCall(db, func(rs app.RequestScope) { 98 | count, err := dao.Count(rs) 99 | assert.Nil(t, rs.Tx().Commit()) 100 | assert.Nil(t, err) 101 | assert.NotZero(t, count) 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /daos/daos_test.go: -------------------------------------------------------------------------------- 1 | package daos 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-ozzo/ozzo-dbx" 7 | "github.com/qiangxue/golang-restful-starter-kit/app" 8 | ) 9 | 10 | func testDBCall(db *dbx.DB, f func(rs app.RequestScope)) { 11 | rs := mockRequestScope(db) 12 | 13 | defer func() { 14 | rs.Tx().Rollback() 15 | }() 16 | 17 | f(rs) 18 | } 19 | 20 | type requestScope struct { 21 | app.Logger 22 | tx *dbx.Tx 23 | } 24 | 25 | func mockRequestScope(db *dbx.DB) app.RequestScope { 26 | tx, _ := db.Begin() 27 | return &requestScope{ 28 | tx: tx, 29 | } 30 | } 31 | 32 | func (rs *requestScope) UserID() string { 33 | return "tester" 34 | } 35 | 36 | func (rs *requestScope) Tx() *dbx.Tx { 37 | return rs.tx 38 | } 39 | 40 | func (rs *requestScope) SetTx(tx *dbx.Tx) { 41 | rs.tx = tx 42 | } 43 | 44 | func (rs *requestScope) Rollback() bool { 45 | return false 46 | } 47 | 48 | func (rs *requestScope) SetRollback(v bool) { 49 | } 50 | 51 | func (rs *requestScope) Now() time.Time { 52 | return time.Now() 53 | } 54 | -------------------------------------------------------------------------------- /errors/api_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // APIError represents an error that can be sent in an error response. 4 | type APIError struct { 5 | // Status represents the HTTP status code 6 | Status int `json:"-"` 7 | // ErrorCode is the code uniquely identifying an error 8 | ErrorCode string `json:"error_code"` 9 | // Message is the error message that may be displayed to end users 10 | Message string `json:"message"` 11 | // DeveloperMessage is the error message that is mainly meant for developers 12 | DeveloperMessage string `json:"developer_message,omitempty"` 13 | // Details specifies the additional error information 14 | Details interface{} `json:"details,omitempty"` 15 | } 16 | 17 | // Error returns the error message. 18 | func (e APIError) Error() string { 19 | return e.Message 20 | } 21 | 22 | // StatusCode returns the HTTP status code. 23 | func (e APIError) StatusCode() int { 24 | return e.Status 25 | } 26 | -------------------------------------------------------------------------------- /errors/api_error_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAPIError_Error(t *testing.T) { 10 | e := APIError{ 11 | Message: "abc", 12 | } 13 | assert.Equal(t, "abc", e.Error()) 14 | } 15 | 16 | func TestAPIError_StatusCode(t *testing.T) { 17 | e := APIError{ 18 | Status: 400, 19 | } 20 | assert.Equal(t, 400, e.StatusCode()) 21 | } 22 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | 7 | "github.com/go-ozzo/ozzo-validation" 8 | ) 9 | 10 | type validationError struct { 11 | Field string `json:"field"` 12 | Error string `json:"error"` 13 | } 14 | 15 | // InternalServerError creates a new API error representing an internal server error (HTTP 500) 16 | func InternalServerError(err error) *APIError { 17 | return NewAPIError(http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", Params{"error": err.Error()}) 18 | } 19 | 20 | // NotFound creates a new API error representing a resource-not-found error (HTTP 404) 21 | func NotFound(resource string) *APIError { 22 | return NewAPIError(http.StatusNotFound, "NOT_FOUND", Params{"resource": resource}) 23 | } 24 | 25 | // Unauthorized creates a new API error representing an authentication failure (HTTP 401) 26 | func Unauthorized(err string) *APIError { 27 | return NewAPIError(http.StatusUnauthorized, "UNAUTHORIZED", Params{"error": err}) 28 | } 29 | 30 | // InvalidData converts a data validation error into an API error (HTTP 400) 31 | func InvalidData(errs validation.Errors) *APIError { 32 | result := []validationError{} 33 | fields := []string{} 34 | for field := range errs { 35 | fields = append(fields, field) 36 | } 37 | sort.Strings(fields) 38 | for _, field := range fields { 39 | err := errs[field] 40 | result = append(result, validationError{ 41 | Field: field, 42 | Error: err.Error(), 43 | }) 44 | } 45 | 46 | err := NewAPIError(http.StatusBadRequest, "INVALID_DATA", nil) 47 | err.Details = result 48 | 49 | return err 50 | } 51 | -------------------------------------------------------------------------------- /errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | errs "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/go-ozzo/ozzo-validation" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInternalServerError(t *testing.T) { 13 | assert.Equal(t, http.StatusInternalServerError, InternalServerError(errs.New("")).Status) 14 | } 15 | 16 | func TestUnauthorized(t *testing.T) { 17 | assert.Equal(t, http.StatusUnauthorized, Unauthorized("t").Status) 18 | } 19 | 20 | func TestInvalidData(t *testing.T) { 21 | err := InvalidData(validation.Errors{ 22 | "abc": errs.New("1"), 23 | "xyz": errs.New("2"), 24 | }) 25 | assert.Equal(t, http.StatusBadRequest, err.Status) 26 | assert.NotNil(t, err.Details) 27 | } 28 | 29 | func TestNotFound(t *testing.T) { 30 | assert.Equal(t, http.StatusNotFound, NotFound("abc").Status) 31 | } 32 | -------------------------------------------------------------------------------- /errors/template.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type ( 12 | // Params is used to replace placeholders in an error template with the corresponding values. 13 | Params map[string]interface{} 14 | 15 | errorTemplate struct { 16 | Message string `yaml:"message"` 17 | DeveloperMessage string `yaml:"developer_message"` 18 | } 19 | ) 20 | 21 | var templates map[string]errorTemplate 22 | 23 | // LoadMessages reads a YAML file containing error templates. 24 | func LoadMessages(file string) error { 25 | bytes, err := ioutil.ReadFile(file) 26 | if err != nil { 27 | return err 28 | } 29 | templates = map[string]errorTemplate{} 30 | return yaml.Unmarshal(bytes, &templates) 31 | } 32 | 33 | // NewAPIError creates a new APIError with the given HTTP status code, error code, and parameters for replacing placeholders in the error template. 34 | // The param can be nil, indicating there is no need for placeholder replacement. 35 | func NewAPIError(status int, code string, params Params) *APIError { 36 | err := &APIError{ 37 | Status: status, 38 | ErrorCode: code, 39 | Message: code, 40 | } 41 | 42 | if template, ok := templates[code]; ok { 43 | err.Message = template.getMessage(params) 44 | err.DeveloperMessage = template.getDeveloperMessage(params) 45 | } 46 | 47 | return err 48 | } 49 | 50 | // getMessage returns the error message by replacing placeholders in the error template with the actual parameters. 51 | func (e errorTemplate) getMessage(params Params) string { 52 | return replacePlaceholders(e.Message, params) 53 | } 54 | 55 | // getDeveloperMessage returns the developer message by replacing placeholders in the error template with the actual parameters. 56 | func (e errorTemplate) getDeveloperMessage(params Params) string { 57 | return replacePlaceholders(e.DeveloperMessage, params) 58 | } 59 | 60 | func replacePlaceholders(message string, params Params) string { 61 | if len(message) == 0 { 62 | return "" 63 | } 64 | for key, value := range params { 65 | message = strings.Replace(message, "{"+key+"}", fmt.Sprint(value), -1) 66 | } 67 | return message 68 | } 69 | -------------------------------------------------------------------------------- /errors/template_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "net/http" 8 | ) 9 | 10 | const MESSAGE_FILE = "../config/errors.yaml" 11 | 12 | func TestNewAPIError(t *testing.T) { 13 | defer func() { 14 | templates = nil 15 | }() 16 | 17 | assert.Nil(t, LoadMessages(MESSAGE_FILE)) 18 | 19 | e := NewAPIError(http.StatusContinue, "xyz", nil) 20 | assert.Equal(t, http.StatusContinue, e.Status) 21 | assert.Equal(t, "xyz", e.Message) 22 | 23 | e = NewAPIError(http.StatusNotFound, "NOT_FOUND", nil) 24 | assert.Equal(t, http.StatusNotFound, e.Status) 25 | assert.NotEqual(t, "NOT_FOUND", e.Message) 26 | } 27 | 28 | func TestLoadMessages(t *testing.T) { 29 | defer func() { 30 | templates = nil 31 | }() 32 | 33 | assert.Nil(t, LoadMessages(MESSAGE_FILE)) 34 | assert.NotNil(t, LoadMessages("xyz")) 35 | } 36 | 37 | func Test_replacePlaceholders(t *testing.T) { 38 | message := replacePlaceholders("abc", nil) 39 | assert.Equal(t, "abc", message) 40 | 41 | message = replacePlaceholders("abc", Params{"abc": 1}) 42 | assert.Equal(t, "abc", message) 43 | 44 | message = replacePlaceholders("{abc}", Params{"abc": 1}) 45 | assert.Equal(t, "1", message) 46 | 47 | message = replacePlaceholders("123 {abc} xyz {abc} d {xyz}", Params{"abc": 1, "xyz": "t"}) 48 | assert.Equal(t, "123 1 xyz 1 d t", message) 49 | } 50 | 51 | func Test_errorTemplate_newAPIError(t *testing.T) { 52 | 53 | } 54 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/qiangxue/golang-restful-starter-kit 2 | import: 3 | - package: github.com/Sirupsen/logrus 4 | version: ^0.10 5 | - package: github.com/lib/pq 6 | version: 4dd446efc17690bc53e154025146f73203b18309 7 | - package: github.com/go-ozzo/ozzo-routing 8 | version: ^1.2 9 | - package: github.com/go-ozzo/ozzo-validation 10 | version: ^2.0 11 | - package: github.com/spf13/viper 12 | version: 346299ea79e446ebdddb834371ceba2e5926b732 13 | - package: gopkg.in/yaml.v2 14 | - package: github.com/stretchr/testify 15 | version: ^1.1 16 | - package: github.com/dgrijalva/jwt-go 17 | version: ^3.0 -------------------------------------------------------------------------------- /models/artist.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/go-ozzo/ozzo-validation" 4 | 5 | // Artist represents an artist record. 6 | type Artist struct { 7 | Id int `json:"id" db:"id"` 8 | Name string `json:"name" db:"name"` 9 | } 10 | 11 | // Validate validates the Artist fields. 12 | func (m Artist) Validate(attrs ...string) error { 13 | return validation.StructRules{}. 14 | Add("Name", validation.Required, validation.Length(0, 120)). 15 | Validate(m, attrs...) 16 | } 17 | -------------------------------------------------------------------------------- /models/identity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Identity interface { 4 | GetID() string 5 | GetName() string 6 | } 7 | 8 | type User struct { 9 | ID string 10 | Name string 11 | } 12 | 13 | func (u User) GetID() string { 14 | return u.ID 15 | } 16 | 17 | func (u User) GetName() string { 18 | return u.Name 19 | } 20 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/go-ozzo/ozzo-dbx" 9 | "github.com/go-ozzo/ozzo-routing" 10 | "github.com/go-ozzo/ozzo-routing/auth" 11 | "github.com/go-ozzo/ozzo-routing/content" 12 | "github.com/go-ozzo/ozzo-routing/cors" 13 | _ "github.com/lib/pq" 14 | "github.com/qiangxue/golang-restful-starter-kit/apis" 15 | "github.com/qiangxue/golang-restful-starter-kit/app" 16 | "github.com/qiangxue/golang-restful-starter-kit/daos" 17 | "github.com/qiangxue/golang-restful-starter-kit/errors" 18 | "github.com/qiangxue/golang-restful-starter-kit/services" 19 | ) 20 | 21 | func main() { 22 | // load application configurations 23 | if err := app.LoadConfig("./config"); err != nil { 24 | panic(fmt.Errorf("Invalid application configuration: %s", err)) 25 | } 26 | 27 | // load error messages 28 | if err := errors.LoadMessages(app.Config.ErrorFile); err != nil { 29 | panic(fmt.Errorf("Failed to read the error message file: %s", err)) 30 | } 31 | 32 | // create the logger 33 | logger := logrus.New() 34 | 35 | // connect to the database 36 | db, err := dbx.MustOpen("postgres", app.Config.DSN) 37 | if err != nil { 38 | panic(err) 39 | } 40 | db.LogFunc = logger.Infof 41 | 42 | // wire up API routing 43 | http.Handle("/", buildRouter(logger, db)) 44 | 45 | // start the server 46 | address := fmt.Sprintf(":%v", app.Config.ServerPort) 47 | logger.Infof("server %v is started at %v\n", app.Version, address) 48 | panic(http.ListenAndServe(address, nil)) 49 | } 50 | 51 | func buildRouter(logger *logrus.Logger, db *dbx.DB) *routing.Router { 52 | router := routing.New() 53 | 54 | router.To("GET,HEAD", "/ping", func(c *routing.Context) error { 55 | return c.Write("OK " + app.Version) 56 | }) 57 | 58 | router.Use( 59 | app.Init(logger), 60 | content.TypeNegotiator(content.JSON), 61 | cors.Handler(cors.Options{ 62 | AllowOrigins: "*", 63 | AllowHeaders: "*", 64 | AllowMethods: "*", 65 | }), 66 | app.Transactional(db), 67 | ) 68 | 69 | rg := router.Group("/v1") 70 | 71 | rg.Post("/auth", apis.Auth(app.Config.JWTSigningKey)) 72 | rg.Use(auth.JWT(app.Config.JWTVerificationKey)) 73 | 74 | artistDAO := daos.NewArtistDAO() 75 | apis.ServeArtistResource(rg, services.NewArtistService(artistDAO)) 76 | 77 | // wire up more resource APIs here 78 | 79 | return router 80 | } 81 | -------------------------------------------------------------------------------- /services/artist.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/qiangxue/golang-restful-starter-kit/app" 5 | "github.com/qiangxue/golang-restful-starter-kit/models" 6 | ) 7 | 8 | // artistDAO specifies the interface of the artist DAO needed by ArtistService. 9 | type artistDAO interface { 10 | // Get returns the artist with the specified the artist ID. 11 | Get(rs app.RequestScope, id int) (*models.Artist, error) 12 | // Count returns the number of artists. 13 | Count(rs app.RequestScope) (int, error) 14 | // Query returns the list of the artists with the given offset and limit. 15 | Query(rs app.RequestScope, offset, limit int) ([]models.Artist, error) 16 | // Create saves a new artist in the storage. 17 | Create(rs app.RequestScope, artist *models.Artist) error 18 | // Update updates the artist with the given ID in the storage. 19 | Update(rs app.RequestScope, id int, artist *models.Artist) error 20 | // Delete removes the artist with the given ID from the storage. 21 | Delete(rs app.RequestScope, id int) error 22 | } 23 | 24 | // ArtistService provides services related with artists. 25 | type ArtistService struct { 26 | dao artistDAO 27 | } 28 | 29 | // NewArtistService creates a new ArtistService with the given artist DAO. 30 | func NewArtistService(dao artistDAO) *ArtistService { 31 | return &ArtistService{dao} 32 | } 33 | 34 | // Get returns the artist with the specified the artist ID. 35 | func (s *ArtistService) Get(rs app.RequestScope, id int) (*models.Artist, error) { 36 | return s.dao.Get(rs, id) 37 | } 38 | 39 | // Create creates a new artist. 40 | func (s *ArtistService) Create(rs app.RequestScope, model *models.Artist) (*models.Artist, error) { 41 | if err := model.Validate(); err != nil { 42 | return nil, err 43 | } 44 | if err := s.dao.Create(rs, model); err != nil { 45 | return nil, err 46 | } 47 | return s.dao.Get(rs, model.Id) 48 | } 49 | 50 | // Update updates the artist with the specified ID. 51 | func (s *ArtistService) Update(rs app.RequestScope, id int, model *models.Artist) (*models.Artist, error) { 52 | if err := model.Validate(); err != nil { 53 | return nil, err 54 | } 55 | if err := s.dao.Update(rs, id, model); err != nil { 56 | return nil, err 57 | } 58 | return s.dao.Get(rs, id) 59 | } 60 | 61 | // Delete deletes the artist with the specified ID. 62 | func (s *ArtistService) Delete(rs app.RequestScope, id int) (*models.Artist, error) { 63 | artist, err := s.dao.Get(rs, id) 64 | if err != nil { 65 | return nil, err 66 | } 67 | err = s.dao.Delete(rs, id) 68 | return artist, err 69 | } 70 | 71 | // Count returns the number of artists. 72 | func (s *ArtistService) Count(rs app.RequestScope) (int, error) { 73 | return s.dao.Count(rs) 74 | } 75 | 76 | // Query returns the artists with the specified offset and limit. 77 | func (s *ArtistService) Query(rs app.RequestScope, offset, limit int) ([]models.Artist, error) { 78 | return s.dao.Query(rs, offset, limit) 79 | } 80 | -------------------------------------------------------------------------------- /services/artist_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/qiangxue/golang-restful-starter-kit/app" 8 | "github.com/qiangxue/golang-restful-starter-kit/models" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewArtistService(t *testing.T) { 13 | dao := newMockArtistDAO() 14 | s := NewArtistService(dao) 15 | assert.Equal(t, dao, s.dao) 16 | } 17 | 18 | func TestArtistService_Get(t *testing.T) { 19 | s := NewArtistService(newMockArtistDAO()) 20 | artist, err := s.Get(nil, 1) 21 | if assert.Nil(t, err) && assert.NotNil(t, artist) { 22 | assert.Equal(t, "aaa", artist.Name) 23 | } 24 | 25 | artist, err = s.Get(nil, 100) 26 | assert.NotNil(t, err) 27 | } 28 | 29 | func TestArtistService_Create(t *testing.T) { 30 | s := NewArtistService(newMockArtistDAO()) 31 | artist, err := s.Create(nil, &models.Artist{ 32 | Name: "ddd", 33 | }) 34 | if assert.Nil(t, err) && assert.NotNil(t, artist) { 35 | assert.Equal(t, 4, artist.Id) 36 | assert.Equal(t, "ddd", artist.Name) 37 | } 38 | 39 | // dao error 40 | _, err = s.Create(nil, &models.Artist{ 41 | Id: 100, 42 | Name: "ddd", 43 | }) 44 | assert.NotNil(t, err) 45 | 46 | // validation error 47 | _, err = s.Create(nil, &models.Artist{ 48 | Name: "", 49 | }) 50 | assert.NotNil(t, err) 51 | } 52 | 53 | func TestArtistService_Update(t *testing.T) { 54 | s := NewArtistService(newMockArtistDAO()) 55 | artist, err := s.Update(nil, 2, &models.Artist{ 56 | Name: "ddd", 57 | }) 58 | if assert.Nil(t, err) && assert.NotNil(t, artist) { 59 | assert.Equal(t, 2, artist.Id) 60 | assert.Equal(t, "ddd", artist.Name) 61 | } 62 | 63 | // dao error 64 | _, err = s.Update(nil, 100, &models.Artist{ 65 | Name: "ddd", 66 | }) 67 | assert.NotNil(t, err) 68 | 69 | // validation error 70 | _, err = s.Update(nil, 2, &models.Artist{ 71 | Name: "", 72 | }) 73 | assert.NotNil(t, err) 74 | } 75 | 76 | func TestArtistService_Delete(t *testing.T) { 77 | s := NewArtistService(newMockArtistDAO()) 78 | artist, err := s.Delete(nil, 2) 79 | if assert.Nil(t, err) && assert.NotNil(t, artist) { 80 | assert.Equal(t, 2, artist.Id) 81 | assert.Equal(t, "bbb", artist.Name) 82 | } 83 | 84 | _, err = s.Delete(nil, 2) 85 | assert.NotNil(t, err) 86 | } 87 | 88 | func TestArtistService_Query(t *testing.T) { 89 | s := NewArtistService(newMockArtistDAO()) 90 | result, err := s.Query(nil, 1, 2) 91 | if assert.Nil(t, err) { 92 | assert.Equal(t, 2, len(result)) 93 | } 94 | } 95 | 96 | func newMockArtistDAO() artistDAO { 97 | return &mockArtistDAO{ 98 | records: []models.Artist{ 99 | {Id: 1, Name: "aaa"}, 100 | {Id: 2, Name: "bbb"}, 101 | {Id: 3, Name: "ccc"}, 102 | }, 103 | } 104 | } 105 | 106 | type mockArtistDAO struct { 107 | records []models.Artist 108 | } 109 | 110 | func (m *mockArtistDAO) Get(rs app.RequestScope, id int) (*models.Artist, error) { 111 | for _, record := range m.records { 112 | if record.Id == id { 113 | return &record, nil 114 | } 115 | } 116 | return nil, errors.New("not found") 117 | } 118 | 119 | func (m *mockArtistDAO) Query(rs app.RequestScope, offset, limit int) ([]models.Artist, error) { 120 | return m.records[offset : offset+limit], nil 121 | } 122 | 123 | func (m *mockArtistDAO) Count(rs app.RequestScope) (int, error) { 124 | return len(m.records), nil 125 | } 126 | 127 | func (m *mockArtistDAO) Create(rs app.RequestScope, artist *models.Artist) error { 128 | if artist.Id != 0 { 129 | return errors.New("Id cannot be set") 130 | } 131 | artist.Id = len(m.records) + 1 132 | m.records = append(m.records, *artist) 133 | return nil 134 | } 135 | 136 | func (m *mockArtistDAO) Update(rs app.RequestScope, id int, artist *models.Artist) error { 137 | artist.Id = id 138 | for i, record := range m.records { 139 | if record.Id == id { 140 | m.records[i] = *artist 141 | return nil 142 | } 143 | } 144 | return errors.New("not found") 145 | } 146 | 147 | func (m *mockArtistDAO) Delete(rs app.RequestScope, id int) error { 148 | for i, record := range m.records { 149 | if record.Id == id { 150 | m.records = append(m.records[:i], m.records[i+1:]...) 151 | return nil 152 | } 153 | } 154 | return errors.New("not found") 155 | } 156 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | The data in `db.sql` is extracted from the [Chinook Database](https://chinookdatabase.codeplex.com/) which 2 | requires the following license terms: 3 | 4 | Copyright (c) 2008-2014 Luis Rocha 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 9 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 13 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 15 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /testdata/db.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS album; 2 | DROP TABLE IF EXISTS artist; 3 | 4 | CREATE TABLE artist 5 | ( 6 | id SERIAL PRIMARY KEY, 7 | name VARCHAR(120) 8 | ); 9 | 10 | CREATE TABLE album 11 | ( 12 | id SERIAL PRIMARY KEY, 13 | title VARCHAR(160) NOT NULL, 14 | artist_id INTEGER NOT NULL, 15 | FOREIGN KEY (artist_id) REFERENCES artist (id) ON DELETE CASCADE 16 | ); 17 | 18 | INSERT INTO artist (name) VALUES ('AC/DC'); 19 | INSERT INTO artist (name) VALUES ('Accept'); 20 | INSERT INTO artist (name) VALUES ('Aerosmith'); 21 | INSERT INTO artist (name) VALUES ('Alanis Morissette'); 22 | INSERT INTO artist (name) VALUES ('Alice In Chains'); 23 | INSERT INTO artist (name) VALUES ('Antônio Carlos Jobim'); 24 | INSERT INTO artist (name) VALUES ('Apocalyptica'); 25 | INSERT INTO artist (name) VALUES ('Audioslave'); 26 | INSERT INTO artist (name) VALUES ('BackBeat'); 27 | INSERT INTO artist (name) VALUES ('Billy Cobham'); 28 | INSERT INTO artist (name) VALUES ('Black Label Society'); 29 | INSERT INTO artist (name) VALUES ('Black Sabbath'); 30 | INSERT INTO artist (name) VALUES ('Body Count'); 31 | INSERT INTO artist (name) VALUES ('Bruce Dickinson'); 32 | INSERT INTO artist (name) VALUES ('Buddy Guy'); 33 | INSERT INTO artist (name) VALUES ('Caetano Veloso'); 34 | INSERT INTO artist (name) VALUES ('Chico Buarque'); 35 | INSERT INTO artist (name) VALUES ('Chico Science & Nação Zumbi'); 36 | INSERT INTO artist (name) VALUES ('Cidade Negra'); 37 | INSERT INTO artist (name) VALUES ('Cláudio Zoli'); 38 | INSERT INTO artist (name) VALUES ('Various Artists'); 39 | INSERT INTO artist (name) VALUES ('Led Zeppelin'); 40 | INSERT INTO artist (name) VALUES ('Frank Zappa & Captain Beefheart'); 41 | INSERT INTO artist (name) VALUES ('Marcos Valle'); 42 | INSERT INTO artist (name) VALUES ('Milton Nascimento & Bebeto'); 43 | INSERT INTO artist (name) VALUES ('Azymuth'); 44 | INSERT INTO artist (name) VALUES ('Gilberto Gil'); 45 | INSERT INTO artist (name) VALUES ('João Gilberto'); 46 | INSERT INTO artist (name) VALUES ('Bebel Gilberto'); 47 | INSERT INTO artist (name) VALUES ('Jorge Vercilo'); 48 | INSERT INTO artist (name) VALUES ('Baby Consuelo'); 49 | INSERT INTO artist (name) VALUES ('Ney Matogrosso'); 50 | INSERT INTO artist (name) VALUES ('Luiz Melodia'); 51 | INSERT INTO artist (name) VALUES ('Nando Reis'); 52 | INSERT INTO artist (name) VALUES ('Pedro Luís & A Parede'); 53 | INSERT INTO artist (name) VALUES ('O Rappa'); 54 | INSERT INTO artist (name) VALUES ('Ed Motta'); 55 | INSERT INTO artist (name) VALUES ('Banda Black Rio'); 56 | INSERT INTO artist (name) VALUES ('Fernanda Porto'); 57 | INSERT INTO artist (name) VALUES ('Os Cariocas'); 58 | INSERT INTO artist (name) VALUES ('Elis Regina'); 59 | INSERT INTO artist (name) VALUES ('Milton Nascimento'); 60 | INSERT INTO artist (name) VALUES ('A Cor Do Som'); 61 | INSERT INTO artist (name) VALUES ('Kid Abelha'); 62 | INSERT INTO artist (name) VALUES ('Sandra De Sá'); 63 | INSERT INTO artist (name) VALUES ('Jorge Ben'); 64 | INSERT INTO artist (name) VALUES ('Hermeto Pascoal'); 65 | INSERT INTO artist (name) VALUES ('Barão Vermelho'); 66 | INSERT INTO artist (name) VALUES ('Edson, DJ Marky & DJ Patife Featuring Fernanda Porto'); 67 | INSERT INTO artist (name) VALUES ('Metallica'); 68 | INSERT INTO artist (name) VALUES ('Queen'); 69 | INSERT INTO artist (name) VALUES ('Kiss'); 70 | INSERT INTO artist (name) VALUES ('Spyro Gyra'); 71 | INSERT INTO artist (name) VALUES ('Green Day'); 72 | INSERT INTO artist (name) VALUES ('David Coverdale'); 73 | INSERT INTO artist (name) VALUES ('Gonzaguinha'); 74 | INSERT INTO artist (name) VALUES ('Os Mutantes'); 75 | INSERT INTO artist (name) VALUES ('Deep Purple'); 76 | INSERT INTO artist (name) VALUES ('Santana'); 77 | INSERT INTO artist (name) VALUES ('Santana Feat. Dave Matthews'); 78 | INSERT INTO artist (name) VALUES ('Santana Feat. Everlast'); 79 | INSERT INTO artist (name) VALUES ('Santana Feat. Rob Thomas'); 80 | INSERT INTO artist (name) VALUES ('Santana Feat. Lauryn Hill & Cee-Lo'); 81 | INSERT INTO artist (name) VALUES ('Santana Feat. The Project G&B'); 82 | INSERT INTO artist (name) VALUES ('Santana Feat. Maná'); 83 | INSERT INTO artist (name) VALUES ('Santana Feat. Eagle-Eye Cherry'); 84 | INSERT INTO artist (name) VALUES ('Santana Feat. Eric Clapton'); 85 | INSERT INTO artist (name) VALUES ('Miles Davis'); 86 | INSERT INTO artist (name) VALUES ('Gene Krupa'); 87 | INSERT INTO artist (name) VALUES ('Toquinho & Vinícius'); 88 | INSERT INTO artist (name) VALUES ('Vinícius De Moraes & Baden Powell'); 89 | INSERT INTO artist (name) VALUES ('Vinícius De Moraes'); 90 | INSERT INTO artist (name) VALUES ('Vinícius E Qurteto Em Cy'); 91 | INSERT INTO artist (name) VALUES ('Vinícius E Odette Lara'); 92 | INSERT INTO artist (name) VALUES ('Vinicius, Toquinho & Quarteto Em Cy'); 93 | INSERT INTO artist (name) VALUES ('Creedence Clearwater Revival'); 94 | INSERT INTO artist (name) VALUES ('Cássia Eller'); 95 | INSERT INTO artist (name) VALUES ('Def Leppard'); 96 | INSERT INTO artist (name) VALUES ('Dennis Chambers'); 97 | INSERT INTO artist (name) VALUES ('Djavan'); 98 | INSERT INTO artist (name) VALUES ('Eric Clapton'); 99 | INSERT INTO artist (name) VALUES ('Faith No More'); 100 | INSERT INTO artist (name) VALUES ('Falamansa'); 101 | INSERT INTO artist (name) VALUES ('Foo Fighters'); 102 | INSERT INTO artist (name) VALUES ('Frank Sinatra'); 103 | INSERT INTO artist (name) VALUES ('Funk Como Le Gusta'); 104 | INSERT INTO artist (name) VALUES ('Godsmack'); 105 | INSERT INTO artist (name) VALUES ('Guns N'' Roses'); 106 | INSERT INTO artist (name) VALUES ('Incognito'); 107 | INSERT INTO artist (name) VALUES ('Iron Maiden'); 108 | INSERT INTO artist (name) VALUES ('James Brown'); 109 | INSERT INTO artist (name) VALUES ('Jamiroquai'); 110 | INSERT INTO artist (name) VALUES ('JET'); 111 | INSERT INTO artist (name) VALUES ('Jimi Hendrix'); 112 | INSERT INTO artist (name) VALUES ('Joe Satriani'); 113 | INSERT INTO artist (name) VALUES ('Jota Quest'); 114 | INSERT INTO artist (name) VALUES ('João Suplicy'); 115 | INSERT INTO artist (name) VALUES ('Judas Priest'); 116 | INSERT INTO artist (name) VALUES ('Legião Urbana'); 117 | INSERT INTO artist (name) VALUES ('Lenny Kravitz'); 118 | INSERT INTO artist (name) VALUES ('Lulu Santos'); 119 | INSERT INTO artist (name) VALUES ('Marillion'); 120 | INSERT INTO artist (name) VALUES ('Marisa Monte'); 121 | INSERT INTO artist (name) VALUES ('Marvin Gaye'); 122 | INSERT INTO artist (name) VALUES ('Men At Work'); 123 | INSERT INTO artist (name) VALUES ('Motörhead'); 124 | INSERT INTO artist (name) VALUES ('Motörhead & Girlschool'); 125 | INSERT INTO artist (name) VALUES ('Mônica Marianno'); 126 | INSERT INTO artist (name) VALUES ('Mötley Crüe'); 127 | INSERT INTO artist (name) VALUES ('Nirvana'); 128 | INSERT INTO artist (name) VALUES ('O Terço'); 129 | INSERT INTO artist (name) VALUES ('Olodum'); 130 | INSERT INTO artist (name) VALUES ('Os Paralamas Do Sucesso'); 131 | INSERT INTO artist (name) VALUES ('Ozzy Osbourne'); 132 | INSERT INTO artist (name) VALUES ('Page & Plant'); 133 | INSERT INTO artist (name) VALUES ('Passengers'); 134 | INSERT INTO artist (name) VALUES ('Paul D''Ianno'); 135 | INSERT INTO artist (name) VALUES ('Pearl Jam'); 136 | INSERT INTO artist (name) VALUES ('Peter Tosh'); 137 | INSERT INTO artist (name) VALUES ('Pink Floyd'); 138 | INSERT INTO artist (name) VALUES ('Planet Hemp'); 139 | INSERT INTO artist (name) VALUES ('R.E.M. Feat. Kate Pearson'); 140 | INSERT INTO artist (name) VALUES ('R.E.M. Feat. KRS-One'); 141 | INSERT INTO artist (name) VALUES ('R.E.M.'); 142 | INSERT INTO artist (name) VALUES ('Raimundos'); 143 | INSERT INTO artist (name) VALUES ('Raul Seixas'); 144 | INSERT INTO artist (name) VALUES ('Red Hot Chili Peppers'); 145 | INSERT INTO artist (name) VALUES ('Rush'); 146 | INSERT INTO artist (name) VALUES ('Simply Red'); 147 | INSERT INTO artist (name) VALUES ('Skank'); 148 | INSERT INTO artist (name) VALUES ('Smashing Pumpkins'); 149 | INSERT INTO artist (name) VALUES ('Soundgarden'); 150 | INSERT INTO artist (name) VALUES ('Stevie Ray Vaughan & Double Trouble'); 151 | INSERT INTO artist (name) VALUES ('Stone Temple Pilots'); 152 | INSERT INTO artist (name) VALUES ('System Of A Down'); 153 | INSERT INTO artist (name) VALUES ('Terry Bozzio, Tony Levin & Steve Stevens'); 154 | INSERT INTO artist (name) VALUES ('The Black Crowes'); 155 | INSERT INTO artist (name) VALUES ('The Clash'); 156 | INSERT INTO artist (name) VALUES ('The Cult'); 157 | INSERT INTO artist (name) VALUES ('The Doors'); 158 | INSERT INTO artist (name) VALUES ('The Police'); 159 | INSERT INTO artist (name) VALUES ('The Rolling Stones'); 160 | INSERT INTO artist (name) VALUES ('The Tea Party'); 161 | INSERT INTO artist (name) VALUES ('The Who'); 162 | INSERT INTO artist (name) VALUES ('Tim Maia'); 163 | INSERT INTO artist (name) VALUES ('Titãs'); 164 | INSERT INTO artist (name) VALUES ('Battlestar Galactica'); 165 | INSERT INTO artist (name) VALUES ('Heroes'); 166 | INSERT INTO artist (name) VALUES ('Lost'); 167 | INSERT INTO artist (name) VALUES ('U2'); 168 | INSERT INTO artist (name) VALUES ('UB40'); 169 | INSERT INTO artist (name) VALUES ('Van Halen'); 170 | INSERT INTO artist (name) VALUES ('Velvet Revolver'); 171 | INSERT INTO artist (name) VALUES ('Whitesnake'); 172 | INSERT INTO artist (name) VALUES ('Zeca Pagodinho'); 173 | INSERT INTO artist (name) VALUES ('The Office'); 174 | INSERT INTO artist (name) VALUES ('Dread Zeppelin'); 175 | INSERT INTO artist (name) VALUES ('Battlestar Galactica (Classic)'); 176 | INSERT INTO artist (name) VALUES ('Aquaman'); 177 | INSERT INTO artist (name) VALUES ('Christina Aguilera featuring BigElf'); 178 | INSERT INTO artist (name) VALUES ('Aerosmith & Sierra Leone''s Refugee Allstars'); 179 | INSERT INTO artist (name) VALUES ('Los Lonely Boys'); 180 | INSERT INTO artist (name) VALUES ('Corinne Bailey Rae'); 181 | INSERT INTO artist (name) VALUES ('Dhani Harrison & Jakob Dylan'); 182 | INSERT INTO artist (name) VALUES ('Jackson Browne'); 183 | INSERT INTO artist (name) VALUES ('Avril Lavigne'); 184 | INSERT INTO artist (name) VALUES ('Big & Rich'); 185 | INSERT INTO artist (name) VALUES ('Youssou N''Dour'); 186 | INSERT INTO artist (name) VALUES ('Black Eyed Peas'); 187 | INSERT INTO artist (name) VALUES ('Jack Johnson'); 188 | INSERT INTO artist (name) VALUES ('Ben Harper'); 189 | INSERT INTO artist (name) VALUES ('Snow Patrol'); 190 | INSERT INTO artist (name) VALUES ('Matisyahu'); 191 | INSERT INTO artist (name) VALUES ('The Postal Service'); 192 | INSERT INTO artist (name) VALUES ('Jaguares'); 193 | INSERT INTO artist (name) VALUES ('The Flaming Lips'); 194 | INSERT INTO artist (name) VALUES ('Jack''s Mannequin & Mick Fleetwood'); 195 | INSERT INTO artist (name) VALUES ('Regina Spektor'); 196 | INSERT INTO artist (name) VALUES ('Scorpions'); 197 | INSERT INTO artist (name) VALUES ('House Of Pain'); 198 | INSERT INTO artist (name) VALUES ('Xis'); 199 | INSERT INTO artist (name) VALUES ('Nega Gizza'); 200 | INSERT INTO artist (name) VALUES ('Gustavo & Andres Veiga & Salazar'); 201 | INSERT INTO artist (name) VALUES ('Rodox'); 202 | INSERT INTO artist (name) VALUES ('Charlie Brown Jr.'); 203 | INSERT INTO artist (name) VALUES ('Pedro Luís E A Parede'); 204 | INSERT INTO artist (name) VALUES ('Los Hermanos'); 205 | INSERT INTO artist (name) VALUES ('Mundo Livre S/A'); 206 | INSERT INTO artist (name) VALUES ('Otto'); 207 | INSERT INTO artist (name) VALUES ('Instituto'); 208 | INSERT INTO artist (name) VALUES ('Nação Zumbi'); 209 | INSERT INTO artist (name) VALUES ('DJ Dolores & Orchestra Santa Massa'); 210 | INSERT INTO artist (name) VALUES ('Seu Jorge'); 211 | INSERT INTO artist (name) VALUES ('Sabotage E Instituto'); 212 | INSERT INTO artist (name) VALUES ('Stereo Maracana'); 213 | INSERT INTO artist (name) VALUES ('Cake'); 214 | INSERT INTO artist (name) VALUES ('Aisha Duo'); 215 | INSERT INTO artist (name) VALUES ('Habib Koité and Bamada'); 216 | INSERT INTO artist (name) VALUES ('Karsh Kale'); 217 | INSERT INTO artist (name) VALUES ('The Posies'); 218 | INSERT INTO artist (name) VALUES ('Luciana Souza/Romero Lubambo'); 219 | INSERT INTO artist (name) VALUES ('Aaron Goldberg'); 220 | INSERT INTO artist (name) VALUES ('Nicolaus Esterhazy Sinfonia'); 221 | INSERT INTO artist (name) VALUES ('Temple of the Dog'); 222 | INSERT INTO artist (name) VALUES ('Chris Cornell'); 223 | INSERT INTO artist (name) VALUES ('Alberto Turco & Nova Schola Gregoriana'); 224 | INSERT INTO artist (name) VALUES ('Richard Marlow & The Choir of Trinity College, Cambridge'); 225 | INSERT INTO artist (name) VALUES ('English Concert & Trevor Pinnock'); 226 | INSERT INTO artist (name) VALUES ('Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker'); 227 | INSERT INTO artist (name) VALUES ('Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer'); 228 | INSERT INTO artist (name) VALUES ('Wilhelm Kempff'); 229 | INSERT INTO artist (name) VALUES ('Yo-Yo Ma'); 230 | INSERT INTO artist (name) VALUES ('Scholars Baroque Ensemble'); 231 | INSERT INTO artist (name) VALUES ('Academy of St. Martin in the Fields & Sir Neville Marriner'); 232 | INSERT INTO artist (name) VALUES ('Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner'); 233 | INSERT INTO artist (name) VALUES ('Berliner Philharmoniker, Claudio Abbado & Sabine Meyer'); 234 | INSERT INTO artist (name) VALUES ('Royal Philharmonic Orchestra & Sir Thomas Beecham'); 235 | INSERT INTO artist (name) VALUES ('Orchestre Révolutionnaire et Romantique & John Eliot Gardiner'); 236 | INSERT INTO artist (name) VALUES ('Britten Sinfonia, Ivor Bolton & Lesley Garrett'); 237 | INSERT INTO artist (name) VALUES ('Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti'); 238 | INSERT INTO artist (name) VALUES ('Sir Georg Solti & Wiener Philharmoniker'); 239 | INSERT INTO artist (name) VALUES ('Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair'); 240 | INSERT INTO artist (name) VALUES ('London Symphony Orchestra & Sir Charles Mackerras'); 241 | INSERT INTO artist (name) VALUES ('Barry Wordsworth & BBC Concert Orchestra'); 242 | INSERT INTO artist (name) VALUES ('Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker'); 243 | INSERT INTO artist (name) VALUES ('Eugene Ormandy'); 244 | INSERT INTO artist (name) VALUES ('Luciano Pavarotti'); 245 | INSERT INTO artist (name) VALUES ('Leonard Bernstein & New York Philharmonic'); 246 | INSERT INTO artist (name) VALUES ('Boston Symphony Orchestra & Seiji Ozawa'); 247 | INSERT INTO artist (name) VALUES ('Aaron Copland & London Symphony Orchestra'); 248 | INSERT INTO artist (name) VALUES ('Ton Koopman'); 249 | INSERT INTO artist (name) VALUES ('Sergei Prokofiev & Yuri Temirkanov'); 250 | INSERT INTO artist (name) VALUES ('Chicago Symphony Orchestra & Fritz Reiner'); 251 | INSERT INTO artist (name) VALUES ('Orchestra of The Age of Enlightenment'); 252 | INSERT INTO artist (name) VALUES ('Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra'); 253 | INSERT INTO artist (name) VALUES ('James Levine'); 254 | INSERT INTO artist (name) VALUES ('Berliner Philharmoniker & Hans Rosbaud'); 255 | INSERT INTO artist (name) VALUES ('Maurizio Pollini'); 256 | INSERT INTO artist (name) VALUES ('Academy of St. Martin in the Fields, Sir Neville Marriner & William Bennett'); 257 | INSERT INTO artist (name) VALUES ('Gustav Mahler'); 258 | INSERT INTO artist (name) VALUES ('Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos'); 259 | INSERT INTO artist (name) VALUES ('Edo de Waart & San Francisco Symphony'); 260 | INSERT INTO artist (name) VALUES ('Antal Doráti & London Symphony Orchestra'); 261 | INSERT INTO artist (name) VALUES ('Choir Of Westminster Abbey & Simon Preston'); 262 | INSERT INTO artist (name) VALUES ('Michael Tilson Thomas & San Francisco Symphony'); 263 | INSERT INTO artist (name) VALUES ('Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker'); 264 | INSERT INTO artist (name) VALUES ('The King''s Singers'); 265 | INSERT INTO artist (name) VALUES ('Berliner Philharmoniker & Herbert Von Karajan'); 266 | INSERT INTO artist (name) VALUES ('Sir Georg Solti, Sumi Jo & Wiener Philharmoniker'); 267 | INSERT INTO artist (name) VALUES ('Christopher O''Riley'); 268 | INSERT INTO artist (name) VALUES ('Fretwork'); 269 | INSERT INTO artist (name) VALUES ('Amy Winehouse'); 270 | INSERT INTO artist (name) VALUES ('Calexico'); 271 | INSERT INTO artist (name) VALUES ('Otto Klemperer & Philharmonia Orchestra'); 272 | INSERT INTO artist (name) VALUES ('Yehudi Menuhin'); 273 | INSERT INTO artist (name) VALUES ('Philharmonia Orchestra & Sir Neville Marriner'); 274 | INSERT INTO artist (name) VALUES ('Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart'); 275 | INSERT INTO artist (name) VALUES ('Les Arts Florissants & William Christie'); 276 | INSERT INTO artist (name) VALUES ('The 12 Cellists of The Berlin Philharmonic'); 277 | INSERT INTO artist (name) VALUES ('Adrian Leaper & Doreen de Feis'); 278 | INSERT INTO artist (name) VALUES ('Roger Norrington, London Classical Players'); 279 | INSERT INTO artist (name) VALUES ('Charles Dutoit & L''Orchestre Symphonique de Montréal'); 280 | INSERT INTO artist (name) VALUES ('Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir'); 281 | INSERT INTO artist (name) VALUES ('Kent Nagano and Orchestre de l''Opéra de Lyon'); 282 | INSERT INTO artist (name) VALUES ('Julian Bream'); 283 | INSERT INTO artist (name) VALUES ('Martin Roscoe'); 284 | INSERT INTO artist (name) VALUES ('Göteborgs Symfoniker & Neeme Järvi'); 285 | INSERT INTO artist (name) VALUES ('Itzhak Perlman'); 286 | INSERT INTO artist (name) VALUES ('Michele Campanella'); 287 | INSERT INTO artist (name) VALUES ('Gerald Moore'); 288 | INSERT INTO artist (name) VALUES ('Mela Tenenbaum, Pro Musica Prague & Richard Kapp'); 289 | INSERT INTO artist (name) VALUES ('Emerson String Quartet'); 290 | INSERT INTO artist (name) VALUES ('C. Monteverdi, Nigel Rogers - Chiaroscuro, London Baroque, London Cornett & Sackbu'); 291 | INSERT INTO artist (name) VALUES ('Nash Ensemble'); 292 | INSERT INTO artist (name) VALUES ('Philip Glass Ensemble'); 293 | 294 | INSERT INTO album (title, artist_id) VALUES ('For Those About To Rock We Salute You', 1); 295 | INSERT INTO album (title, artist_id) VALUES ('Balls to the Wall', 2); 296 | INSERT INTO album (title, artist_id) VALUES ('Restless and Wild', 2); 297 | INSERT INTO album (title, artist_id) VALUES ('Let There Be Rock', 1); 298 | INSERT INTO album (title, artist_id) VALUES ('Big Ones', 3); 299 | INSERT INTO album (title, artist_id) VALUES ('Jagged Little Pill', 4); 300 | INSERT INTO album (title, artist_id) VALUES ('Facelift', 5); 301 | INSERT INTO album (title, artist_id) VALUES ('Warner 25 Anos', 6); 302 | INSERT INTO album (title, artist_id) VALUES ('Plays Metallica By Four Cellos', 7); 303 | INSERT INTO album (title, artist_id) VALUES ('Audioslave', 8); 304 | INSERT INTO album (title, artist_id) VALUES ('Out Of Exile', 8); 305 | INSERT INTO album (title, artist_id) VALUES ('BackBeat Soundtrack', 9); 306 | INSERT INTO album (title, artist_id) VALUES ('The Best Of Billy Cobham', 10); 307 | INSERT INTO album (title, artist_id) VALUES ('Alcohol Fueled Brewtality Live! [Disc 1]', 11); 308 | INSERT INTO album (title, artist_id) VALUES ('Alcohol Fueled Brewtality Live! [Disc 2]', 11); 309 | INSERT INTO album (title, artist_id) VALUES ('Black Sabbath', 12); 310 | INSERT INTO album (title, artist_id) VALUES ('Black Sabbath Vol. 4 (Remaster)', 12); 311 | INSERT INTO album (title, artist_id) VALUES ('Body Count', 13); 312 | INSERT INTO album (title, artist_id) VALUES ('Chemical Wedding', 14); 313 | INSERT INTO album (title, artist_id) VALUES ('The Best Of Buddy Guy - The Millenium Collection', 15); 314 | INSERT INTO album (title, artist_id) VALUES ('Prenda Minha', 16); 315 | INSERT INTO album (title, artist_id) VALUES ('Sozinho Remix Ao Vivo', 16); 316 | INSERT INTO album (title, artist_id) VALUES ('Minha Historia', 17); 317 | INSERT INTO album (title, artist_id) VALUES ('Afrociberdelia', 18); 318 | INSERT INTO album (title, artist_id) VALUES ('Da Lama Ao Caos', 18); 319 | INSERT INTO album (title, artist_id) VALUES ('Acústico MTV [Live]', 19); 320 | INSERT INTO album (title, artist_id) VALUES ('Cidade Negra - Hits', 19); 321 | INSERT INTO album (title, artist_id) VALUES ('Na Pista', 20); 322 | INSERT INTO album (title, artist_id) VALUES ('Axé Bahia 2001', 21); 323 | INSERT INTO album (title, artist_id) VALUES ('BBC Sessions [Disc 1] [Live]', 22); 324 | INSERT INTO album (title, artist_id) VALUES ('Bongo Fury', 23); 325 | INSERT INTO album (title, artist_id) VALUES ('Carnaval 2001', 21); 326 | INSERT INTO album (title, artist_id) VALUES ('Chill: Brazil (Disc 1)', 24); 327 | INSERT INTO album (title, artist_id) VALUES ('Chill: Brazil (Disc 2)', 6); 328 | INSERT INTO album (title, artist_id) VALUES ('Garage Inc. (Disc 1)', 50); 329 | INSERT INTO album (title, artist_id) VALUES ('Greatest Hits II', 51); 330 | INSERT INTO album (title, artist_id) VALUES ('Greatest Kiss', 52); 331 | INSERT INTO album (title, artist_id) VALUES ('Heart of the Night', 53); 332 | INSERT INTO album (title, artist_id) VALUES ('International Superhits', 54); 333 | INSERT INTO album (title, artist_id) VALUES ('Into The Light', 55); 334 | INSERT INTO album (title, artist_id) VALUES ('Meus Momentos', 56); 335 | INSERT INTO album (title, artist_id) VALUES ('Minha História', 57); 336 | INSERT INTO album (title, artist_id) VALUES ('MK III The Final Concerts [Disc 1]', 58); 337 | INSERT INTO album (title, artist_id) VALUES ('Physical Graffiti [Disc 1]', 22); 338 | INSERT INTO album (title, artist_id) VALUES ('Sambas De Enredo 2001', 21); 339 | INSERT INTO album (title, artist_id) VALUES ('Supernatural', 59); 340 | INSERT INTO album (title, artist_id) VALUES ('The Best of Ed Motta', 37); 341 | INSERT INTO album (title, artist_id) VALUES ('The Essential Miles Davis [Disc 1]', 68); 342 | INSERT INTO album (title, artist_id) VALUES ('The Essential Miles Davis [Disc 2]', 68); 343 | INSERT INTO album (title, artist_id) VALUES ('The Final Concerts (Disc 2)', 58); 344 | INSERT INTO album (title, artist_id) VALUES ('Up An'' Atom', 69); 345 | INSERT INTO album (title, artist_id) VALUES ('Vinícius De Moraes - Sem Limite', 70); 346 | INSERT INTO album (title, artist_id) VALUES ('Vozes do MPB', 21); 347 | INSERT INTO album (title, artist_id) VALUES ('Chronicle, Vol. 1', 76); 348 | INSERT INTO album (title, artist_id) VALUES ('Chronicle, Vol. 2', 76); 349 | INSERT INTO album (title, artist_id) VALUES ('Cássia Eller - Coleção Sem Limite [Disc 2]', 77); 350 | INSERT INTO album (title, artist_id) VALUES ('Cássia Eller - Sem Limite [Disc 1]', 77); 351 | INSERT INTO album (title, artist_id) VALUES ('Come Taste The Band', 58); 352 | INSERT INTO album (title, artist_id) VALUES ('Deep Purple In Rock', 58); 353 | INSERT INTO album (title, artist_id) VALUES ('Fireball', 58); 354 | INSERT INTO album (title, artist_id) VALUES ('Knocking at Your Back Door: The Best Of Deep Purple in the 80''s', 58); 355 | INSERT INTO album (title, artist_id) VALUES ('Machine Head', 58); 356 | INSERT INTO album (title, artist_id) VALUES ('Purpendicular', 58); 357 | INSERT INTO album (title, artist_id) VALUES ('Slaves And Masters', 58); 358 | INSERT INTO album (title, artist_id) VALUES ('Stormbringer', 58); 359 | INSERT INTO album (title, artist_id) VALUES ('The Battle Rages On', 58); 360 | INSERT INTO album (title, artist_id) VALUES ('Vault: Def Leppard''s Greatest Hits', 78); 361 | INSERT INTO album (title, artist_id) VALUES ('Outbreak', 79); 362 | INSERT INTO album (title, artist_id) VALUES ('Djavan Ao Vivo - Vol. 02', 80); 363 | INSERT INTO album (title, artist_id) VALUES ('Djavan Ao Vivo - Vol. 1', 80); 364 | INSERT INTO album (title, artist_id) VALUES ('Elis Regina-Minha História', 41); 365 | INSERT INTO album (title, artist_id) VALUES ('The Cream Of Clapton', 81); 366 | INSERT INTO album (title, artist_id) VALUES ('Unplugged', 81); 367 | INSERT INTO album (title, artist_id) VALUES ('Album Of The Year', 82); 368 | INSERT INTO album (title, artist_id) VALUES ('Angel Dust', 82); 369 | INSERT INTO album (title, artist_id) VALUES ('King For A Day Fool For A Lifetime', 82); 370 | INSERT INTO album (title, artist_id) VALUES ('The Real Thing', 82); 371 | INSERT INTO album (title, artist_id) VALUES ('Deixa Entrar', 83); 372 | INSERT INTO album (title, artist_id) VALUES ('In Your Honor [Disc 1]', 84); 373 | INSERT INTO album (title, artist_id) VALUES ('In Your Honor [Disc 2]', 84); 374 | INSERT INTO album (title, artist_id) VALUES ('One By One', 84); 375 | INSERT INTO album (title, artist_id) VALUES ('The Colour And The Shape', 84); 376 | INSERT INTO album (title, artist_id) VALUES ('My Way: The Best Of Frank Sinatra [Disc 1]', 85); 377 | INSERT INTO album (title, artist_id) VALUES ('Roda De Funk', 86); 378 | INSERT INTO album (title, artist_id) VALUES ('As Canções de Eu Tu Eles', 27); 379 | INSERT INTO album (title, artist_id) VALUES ('Quanta Gente Veio Ver (Live)', 27); 380 | INSERT INTO album (title, artist_id) VALUES ('Quanta Gente Veio ver--Bônus De Carnaval', 27); 381 | INSERT INTO album (title, artist_id) VALUES ('Faceless', 87); 382 | INSERT INTO album (title, artist_id) VALUES ('American Idiot', 54); 383 | INSERT INTO album (title, artist_id) VALUES ('Appetite for Destruction', 88); 384 | INSERT INTO album (title, artist_id) VALUES ('Use Your Illusion I', 88); 385 | INSERT INTO album (title, artist_id) VALUES ('Use Your Illusion II', 88); 386 | INSERT INTO album (title, artist_id) VALUES ('Blue Moods', 89); 387 | INSERT INTO album (title, artist_id) VALUES ('A Matter of Life and Death', 90); 388 | INSERT INTO album (title, artist_id) VALUES ('A Real Dead One', 90); 389 | INSERT INTO album (title, artist_id) VALUES ('A Real Live One', 90); 390 | INSERT INTO album (title, artist_id) VALUES ('Brave New World', 90); 391 | INSERT INTO album (title, artist_id) VALUES ('Dance Of Death', 90); 392 | INSERT INTO album (title, artist_id) VALUES ('Fear Of The Dark', 90); 393 | INSERT INTO album (title, artist_id) VALUES ('Iron Maiden', 90); 394 | INSERT INTO album (title, artist_id) VALUES ('Killers', 90); 395 | INSERT INTO album (title, artist_id) VALUES ('Live After Death', 90); 396 | INSERT INTO album (title, artist_id) VALUES ('Live At Donington 1992 (Disc 1)', 90); 397 | INSERT INTO album (title, artist_id) VALUES ('Live At Donington 1992 (Disc 2)', 90); 398 | INSERT INTO album (title, artist_id) VALUES ('No Prayer For The Dying', 90); 399 | INSERT INTO album (title, artist_id) VALUES ('Piece Of Mind', 90); 400 | INSERT INTO album (title, artist_id) VALUES ('Powerslave', 90); 401 | INSERT INTO album (title, artist_id) VALUES ('Rock In Rio [CD1]', 90); 402 | INSERT INTO album (title, artist_id) VALUES ('Rock In Rio [CD2]', 90); 403 | INSERT INTO album (title, artist_id) VALUES ('Seventh Son of a Seventh Son', 90); 404 | INSERT INTO album (title, artist_id) VALUES ('Somewhere in Time', 90); 405 | INSERT INTO album (title, artist_id) VALUES ('The Number of The Beast', 90); 406 | INSERT INTO album (title, artist_id) VALUES ('The X Factor', 90); 407 | INSERT INTO album (title, artist_id) VALUES ('Virtual XI', 90); 408 | INSERT INTO album (title, artist_id) VALUES ('Sex Machine', 91); 409 | INSERT INTO album (title, artist_id) VALUES ('Emergency On Planet Earth', 92); 410 | INSERT INTO album (title, artist_id) VALUES ('Synkronized', 92); 411 | INSERT INTO album (title, artist_id) VALUES ('The Return Of The Space Cowboy', 92); 412 | INSERT INTO album (title, artist_id) VALUES ('Get Born', 93); 413 | INSERT INTO album (title, artist_id) VALUES ('Are You Experienced?', 94); 414 | INSERT INTO album (title, artist_id) VALUES ('Surfing with the Alien (Remastered)', 95); 415 | INSERT INTO album (title, artist_id) VALUES ('Jorge Ben Jor 25 Anos', 46); 416 | INSERT INTO album (title, artist_id) VALUES ('Jota Quest-1995', 96); 417 | INSERT INTO album (title, artist_id) VALUES ('Cafezinho', 97); 418 | INSERT INTO album (title, artist_id) VALUES ('Living After Midnight', 98); 419 | INSERT INTO album (title, artist_id) VALUES ('Unplugged [Live]', 52); 420 | INSERT INTO album (title, artist_id) VALUES ('BBC Sessions [Disc 2] [Live]', 22); 421 | INSERT INTO album (title, artist_id) VALUES ('Coda', 22); 422 | INSERT INTO album (title, artist_id) VALUES ('Houses Of The Holy', 22); 423 | INSERT INTO album (title, artist_id) VALUES ('In Through The Out Door', 22); 424 | INSERT INTO album (title, artist_id) VALUES ('IV', 22); 425 | INSERT INTO album (title, artist_id) VALUES ('Led Zeppelin I', 22); 426 | INSERT INTO album (title, artist_id) VALUES ('Led Zeppelin II', 22); 427 | INSERT INTO album (title, artist_id) VALUES ('Led Zeppelin III', 22); 428 | INSERT INTO album (title, artist_id) VALUES ('Physical Graffiti [Disc 2]', 22); 429 | INSERT INTO album (title, artist_id) VALUES ('Presence', 22); 430 | INSERT INTO album (title, artist_id) VALUES ('The Song Remains The Same (Disc 1)', 22); 431 | INSERT INTO album (title, artist_id) VALUES ('The Song Remains The Same (Disc 2)', 22); 432 | INSERT INTO album (title, artist_id) VALUES ('A TempestadeTempestade Ou O Livro Dos Dias', 99); 433 | INSERT INTO album (title, artist_id) VALUES ('Mais Do Mesmo', 99); 434 | INSERT INTO album (title, artist_id) VALUES ('Greatest Hits', 100); 435 | INSERT INTO album (title, artist_id) VALUES ('Lulu Santos - RCA 100 Anos De Música - Álbum 01', 101); 436 | INSERT INTO album (title, artist_id) VALUES ('Lulu Santos - RCA 100 Anos De Música - Álbum 02', 101); 437 | INSERT INTO album (title, artist_id) VALUES ('Misplaced Childhood', 102); 438 | INSERT INTO album (title, artist_id) VALUES ('Barulhinho Bom', 103); 439 | INSERT INTO album (title, artist_id) VALUES ('Seek And Shall Find: More Of The Best (1963-1981)', 104); 440 | INSERT INTO album (title, artist_id) VALUES ('The Best Of Men At Work', 105); 441 | INSERT INTO album (title, artist_id) VALUES ('Black Album', 50); 442 | INSERT INTO album (title, artist_id) VALUES ('Garage Inc. (Disc 2)', 50); 443 | INSERT INTO album (title, artist_id) VALUES ('Kill ''Em All', 50); 444 | INSERT INTO album (title, artist_id) VALUES ('Load', 50); 445 | INSERT INTO album (title, artist_id) VALUES ('Master Of Puppets', 50); 446 | INSERT INTO album (title, artist_id) VALUES ('ReLoad', 50); 447 | INSERT INTO album (title, artist_id) VALUES ('Ride The Lightning', 50); 448 | INSERT INTO album (title, artist_id) VALUES ('St. Anger', 50); 449 | INSERT INTO album (title, artist_id) VALUES ('...And Justice For All', 50); 450 | INSERT INTO album (title, artist_id) VALUES ('Miles Ahead', 68); 451 | INSERT INTO album (title, artist_id) VALUES ('Milton Nascimento Ao Vivo', 42); 452 | INSERT INTO album (title, artist_id) VALUES ('Minas', 42); 453 | INSERT INTO album (title, artist_id) VALUES ('Ace Of Spades', 106); 454 | INSERT INTO album (title, artist_id) VALUES ('Demorou...', 108); 455 | INSERT INTO album (title, artist_id) VALUES ('Motley Crue Greatest Hits', 109); 456 | INSERT INTO album (title, artist_id) VALUES ('From The Muddy Banks Of The Wishkah [Live]', 110); 457 | INSERT INTO album (title, artist_id) VALUES ('Nevermind', 110); 458 | INSERT INTO album (title, artist_id) VALUES ('Compositores', 111); 459 | INSERT INTO album (title, artist_id) VALUES ('Olodum', 112); 460 | INSERT INTO album (title, artist_id) VALUES ('Acústico MTV', 113); 461 | INSERT INTO album (title, artist_id) VALUES ('Arquivo II', 113); 462 | INSERT INTO album (title, artist_id) VALUES ('Arquivo Os Paralamas Do Sucesso', 113); 463 | INSERT INTO album (title, artist_id) VALUES ('Bark at the Moon (Remastered)', 114); 464 | INSERT INTO album (title, artist_id) VALUES ('Blizzard of Ozz', 114); 465 | INSERT INTO album (title, artist_id) VALUES ('Diary of a Madman (Remastered)', 114); 466 | INSERT INTO album (title, artist_id) VALUES ('No More Tears (Remastered)', 114); 467 | INSERT INTO album (title, artist_id) VALUES ('Tribute', 114); 468 | INSERT INTO album (title, artist_id) VALUES ('Walking Into Clarksdale', 115); 469 | INSERT INTO album (title, artist_id) VALUES ('Original Soundtracks 1', 116); 470 | INSERT INTO album (title, artist_id) VALUES ('The Beast Live', 117); 471 | INSERT INTO album (title, artist_id) VALUES ('Live On Two Legs [Live]', 118); 472 | INSERT INTO album (title, artist_id) VALUES ('Pearl Jam', 118); 473 | INSERT INTO album (title, artist_id) VALUES ('Riot Act', 118); 474 | INSERT INTO album (title, artist_id) VALUES ('Ten', 118); 475 | INSERT INTO album (title, artist_id) VALUES ('Vs.', 118); 476 | INSERT INTO album (title, artist_id) VALUES ('Dark Side Of The Moon', 120); 477 | INSERT INTO album (title, artist_id) VALUES ('Os Cães Ladram Mas A Caravana Não Pára', 121); 478 | INSERT INTO album (title, artist_id) VALUES ('Greatest Hits I', 51); 479 | INSERT INTO album (title, artist_id) VALUES ('News Of The World', 51); 480 | INSERT INTO album (title, artist_id) VALUES ('Out Of Time', 122); 481 | INSERT INTO album (title, artist_id) VALUES ('Green', 124); 482 | INSERT INTO album (title, artist_id) VALUES ('New Adventures In Hi-Fi', 124); 483 | INSERT INTO album (title, artist_id) VALUES ('The Best Of R.E.M.: The IRS Years', 124); 484 | INSERT INTO album (title, artist_id) VALUES ('Cesta Básica', 125); 485 | INSERT INTO album (title, artist_id) VALUES ('Raul Seixas', 126); 486 | INSERT INTO album (title, artist_id) VALUES ('Blood Sugar Sex Magik', 127); 487 | INSERT INTO album (title, artist_id) VALUES ('By The Way', 127); 488 | INSERT INTO album (title, artist_id) VALUES ('Californication', 127); 489 | INSERT INTO album (title, artist_id) VALUES ('Retrospective I (1974-1980)', 128); 490 | INSERT INTO album (title, artist_id) VALUES ('Santana - As Years Go By', 59); 491 | INSERT INTO album (title, artist_id) VALUES ('Santana Live', 59); 492 | INSERT INTO album (title, artist_id) VALUES ('Maquinarama', 130); 493 | INSERT INTO album (title, artist_id) VALUES ('O Samba Poconé', 130); 494 | INSERT INTO album (title, artist_id) VALUES ('Judas 0: B-Sides and Rarities', 131); 495 | INSERT INTO album (title, artist_id) VALUES ('Rotten Apples: Greatest Hits', 131); 496 | INSERT INTO album (title, artist_id) VALUES ('A-Sides', 132); 497 | INSERT INTO album (title, artist_id) VALUES ('Morning Dance', 53); 498 | INSERT INTO album (title, artist_id) VALUES ('In Step', 133); 499 | INSERT INTO album (title, artist_id) VALUES ('Core', 134); 500 | INSERT INTO album (title, artist_id) VALUES ('Mezmerize', 135); 501 | INSERT INTO album (title, artist_id) VALUES ('[1997] Black Light Syndrome', 136); 502 | INSERT INTO album (title, artist_id) VALUES ('Live [Disc 1]', 137); 503 | INSERT INTO album (title, artist_id) VALUES ('Live [Disc 2]', 137); 504 | INSERT INTO album (title, artist_id) VALUES ('The Singles', 138); 505 | INSERT INTO album (title, artist_id) VALUES ('Beyond Good And Evil', 139); 506 | INSERT INTO album (title, artist_id) VALUES ('Pure Cult: The Best Of The Cult (For Rockers, Ravers, Lovers & Sinners) [UK]', 139); 507 | INSERT INTO album (title, artist_id) VALUES ('The Doors', 140); 508 | INSERT INTO album (title, artist_id) VALUES ('The Police Greatest Hits', 141); 509 | INSERT INTO album (title, artist_id) VALUES ('Hot Rocks, 1964-1971 (Disc 1)', 142); 510 | INSERT INTO album (title, artist_id) VALUES ('No Security', 142); 511 | INSERT INTO album (title, artist_id) VALUES ('Voodoo Lounge', 142); 512 | INSERT INTO album (title, artist_id) VALUES ('Tangents', 143); 513 | INSERT INTO album (title, artist_id) VALUES ('Transmission', 143); 514 | INSERT INTO album (title, artist_id) VALUES ('My Generation - The Very Best Of The Who', 144); 515 | INSERT INTO album (title, artist_id) VALUES ('Serie Sem Limite (Disc 1)', 145); 516 | INSERT INTO album (title, artist_id) VALUES ('Serie Sem Limite (Disc 2)', 145); 517 | INSERT INTO album (title, artist_id) VALUES ('Acústico', 146); 518 | INSERT INTO album (title, artist_id) VALUES ('Volume Dois', 146); 519 | INSERT INTO album (title, artist_id) VALUES ('Battlestar Galactica: The Story So Far', 147); 520 | INSERT INTO album (title, artist_id) VALUES ('Battlestar Galactica, Season 3', 147); 521 | INSERT INTO album (title, artist_id) VALUES ('Heroes, Season 1', 148); 522 | INSERT INTO album (title, artist_id) VALUES ('Lost, Season 3', 149); 523 | INSERT INTO album (title, artist_id) VALUES ('Lost, Season 1', 149); 524 | INSERT INTO album (title, artist_id) VALUES ('Lost, Season 2', 149); 525 | INSERT INTO album (title, artist_id) VALUES ('Achtung Baby', 150); 526 | INSERT INTO album (title, artist_id) VALUES ('All That You Can''t Leave Behind', 150); 527 | INSERT INTO album (title, artist_id) VALUES ('B-Sides 1980-1990', 150); 528 | INSERT INTO album (title, artist_id) VALUES ('How To Dismantle An Atomic Bomb', 150); 529 | INSERT INTO album (title, artist_id) VALUES ('Pop', 150); 530 | INSERT INTO album (title, artist_id) VALUES ('Rattle And Hum', 150); 531 | INSERT INTO album (title, artist_id) VALUES ('The Best Of 1980-1990', 150); 532 | INSERT INTO album (title, artist_id) VALUES ('War', 150); 533 | INSERT INTO album (title, artist_id) VALUES ('Zooropa', 150); 534 | INSERT INTO album (title, artist_id) VALUES ('UB40 The Best Of - Volume Two [UK]', 151); 535 | INSERT INTO album (title, artist_id) VALUES ('Diver Down', 152); 536 | INSERT INTO album (title, artist_id) VALUES ('The Best Of Van Halen, Vol. I', 152); 537 | INSERT INTO album (title, artist_id) VALUES ('Van Halen', 152); 538 | INSERT INTO album (title, artist_id) VALUES ('Van Halen III', 152); 539 | INSERT INTO album (title, artist_id) VALUES ('Contraband', 153); 540 | INSERT INTO album (title, artist_id) VALUES ('Vinicius De Moraes', 72); 541 | INSERT INTO album (title, artist_id) VALUES ('Ao Vivo [IMPORT]', 155); 542 | INSERT INTO album (title, artist_id) VALUES ('The Office, Season 1', 156); 543 | INSERT INTO album (title, artist_id) VALUES ('The Office, Season 2', 156); 544 | INSERT INTO album (title, artist_id) VALUES ('The Office, Season 3', 156); 545 | INSERT INTO album (title, artist_id) VALUES ('Un-Led-Ed', 157); 546 | INSERT INTO album (title, artist_id) VALUES ('Battlestar Galactica (Classic), Season 1', 158); 547 | INSERT INTO album (title, artist_id) VALUES ('Aquaman', 159); 548 | INSERT INTO album (title, artist_id) VALUES ('Instant Karma: The Amnesty International Campaign to Save Darfur', 150); 549 | INSERT INTO album (title, artist_id) VALUES ('Speak of the Devil', 114); 550 | INSERT INTO album (title, artist_id) VALUES ('20th Century Masters - The Millennium Collection: The Best of Scorpions', 179); 551 | INSERT INTO album (title, artist_id) VALUES ('House of Pain', 180); 552 | INSERT INTO album (title, artist_id) VALUES ('Radio Brasil (O Som da Jovem Vanguarda) - Seleccao de Henrique Amaro', 36); 553 | INSERT INTO album (title, artist_id) VALUES ('Cake: B-Sides and Rarities', 196); 554 | INSERT INTO album (title, artist_id) VALUES ('LOST, Season 4', 149); 555 | INSERT INTO album (title, artist_id) VALUES ('Quiet Songs', 197); 556 | INSERT INTO album (title, artist_id) VALUES ('Muso Ko', 198); 557 | INSERT INTO album (title, artist_id) VALUES ('Realize', 199); 558 | INSERT INTO album (title, artist_id) VALUES ('Every Kind of Light', 200); 559 | INSERT INTO album (title, artist_id) VALUES ('Duos II', 201); 560 | INSERT INTO album (title, artist_id) VALUES ('Worlds', 202); 561 | INSERT INTO album (title, artist_id) VALUES ('The Best of Beethoven', 203); 562 | INSERT INTO album (title, artist_id) VALUES ('Temple of the Dog', 204); 563 | INSERT INTO album (title, artist_id) VALUES ('Carry On', 205); 564 | INSERT INTO album (title, artist_id) VALUES ('Revelations', 8); 565 | INSERT INTO album (title, artist_id) VALUES ('Adorate Deum: Gregorian Chant from the Proper of the Mass', 206); 566 | INSERT INTO album (title, artist_id) VALUES ('Allegri: Miserere', 207); 567 | INSERT INTO album (title, artist_id) VALUES ('Pachelbel: Canon & Gigue', 208); 568 | INSERT INTO album (title, artist_id) VALUES ('Vivaldi: The Four Seasons', 209); 569 | INSERT INTO album (title, artist_id) VALUES ('Bach: Violin Concertos', 210); 570 | INSERT INTO album (title, artist_id) VALUES ('Bach: Goldberg Variations', 211); 571 | INSERT INTO album (title, artist_id) VALUES ('Bach: The Cello Suites', 212); 572 | INSERT INTO album (title, artist_id) VALUES ('Handel: The Messiah (Highlights)', 213); 573 | INSERT INTO album (title, artist_id) VALUES ('The World of Classical Favourites', 214); 574 | INSERT INTO album (title, artist_id) VALUES ('Sir Neville Marriner: A Celebration', 215); 575 | INSERT INTO album (title, artist_id) VALUES ('Mozart: Wind Concertos', 216); 576 | INSERT INTO album (title, artist_id) VALUES ('Haydn: Symphonies 99 - 104', 217); 577 | INSERT INTO album (title, artist_id) VALUES ('Beethoven: Symhonies Nos. 5 & 6', 218); 578 | INSERT INTO album (title, artist_id) VALUES ('A Soprano Inspired', 219); 579 | INSERT INTO album (title, artist_id) VALUES ('Great Opera Choruses', 220); 580 | INSERT INTO album (title, artist_id) VALUES ('Wagner: Favourite Overtures', 221); 581 | INSERT INTO album (title, artist_id) VALUES ('Fauré: Requiem, Ravel: Pavane & Others', 222); 582 | INSERT INTO album (title, artist_id) VALUES ('Tchaikovsky: The Nutcracker', 223); 583 | INSERT INTO album (title, artist_id) VALUES ('The Last Night of the Proms', 224); 584 | INSERT INTO album (title, artist_id) VALUES ('Puccini: Madama Butterfly - Highlights', 225); 585 | INSERT INTO album (title, artist_id) VALUES ('Holst: The Planets, Op. 32 & Vaughan Williams: Fantasies', 226); 586 | INSERT INTO album (title, artist_id) VALUES ('Pavarotti''s Opera Made Easy', 227); 587 | INSERT INTO album (title, artist_id) VALUES ('Great Performances - Barber''s Adagio and Other Romantic Favorites for Strings', 228); 588 | INSERT INTO album (title, artist_id) VALUES ('Carmina Burana', 229); 589 | INSERT INTO album (title, artist_id) VALUES ('A Copland Celebration, Vol. I', 230); 590 | INSERT INTO album (title, artist_id) VALUES ('Bach: Toccata & Fugue in D Minor', 231); 591 | INSERT INTO album (title, artist_id) VALUES ('Prokofiev: Symphony No.1', 232); 592 | INSERT INTO album (title, artist_id) VALUES ('Scheherazade', 233); 593 | INSERT INTO album (title, artist_id) VALUES ('Bach: The Brandenburg Concertos', 234); 594 | INSERT INTO album (title, artist_id) VALUES ('Chopin: Piano Concertos Nos. 1 & 2', 235); 595 | INSERT INTO album (title, artist_id) VALUES ('Mascagni: Cavalleria Rusticana', 236); 596 | INSERT INTO album (title, artist_id) VALUES ('Sibelius: Finlandia', 237); 597 | INSERT INTO album (title, artist_id) VALUES ('Beethoven Piano Sonatas: Moonlight & Pastorale', 238); 598 | INSERT INTO album (title, artist_id) VALUES ('Great Recordings of the Century - Mahler: Das Lied von der Erde', 240); 599 | INSERT INTO album (title, artist_id) VALUES ('Elgar: Cello Concerto & Vaughan Williams: Fantasias', 241); 600 | INSERT INTO album (title, artist_id) VALUES ('Adams, John: The Chairman Dances', 242); 601 | INSERT INTO album (title, artist_id) VALUES ('Tchaikovsky: 1812 Festival Overture, Op.49, Capriccio Italien & Beethoven: Wellington''s Victory', 243); 602 | INSERT INTO album (title, artist_id) VALUES ('Palestrina: Missa Papae Marcelli & Allegri: Miserere', 244); 603 | INSERT INTO album (title, artist_id) VALUES ('Prokofiev: Romeo & Juliet', 245); 604 | INSERT INTO album (title, artist_id) VALUES ('Strauss: Waltzes', 226); 605 | INSERT INTO album (title, artist_id) VALUES ('Berlioz: Symphonie Fantastique', 245); 606 | INSERT INTO album (title, artist_id) VALUES ('Bizet: Carmen Highlights', 246); 607 | INSERT INTO album (title, artist_id) VALUES ('English Renaissance', 247); 608 | INSERT INTO album (title, artist_id) VALUES ('Handel: Music for the Royal Fireworks (Original Version 1749)', 208); 609 | INSERT INTO album (title, artist_id) VALUES ('Grieg: Peer Gynt Suites & Sibelius: Pelléas et Mélisande', 248); 610 | INSERT INTO album (title, artist_id) VALUES ('Mozart Gala: Famous Arias', 249); 611 | INSERT INTO album (title, artist_id) VALUES ('SCRIABIN: Vers la flamme', 250); 612 | INSERT INTO album (title, artist_id) VALUES ('Armada: Music from the Courts of England and Spain', 251); 613 | INSERT INTO album (title, artist_id) VALUES ('Mozart: Symphonies Nos. 40 & 41', 248); 614 | INSERT INTO album (title, artist_id) VALUES ('Back to Black', 252); 615 | INSERT INTO album (title, artist_id) VALUES ('Frank', 252); 616 | INSERT INTO album (title, artist_id) VALUES ('Carried to Dust (Bonus Track Version)', 253); 617 | INSERT INTO album (title, artist_id) VALUES ('Beethoven: Symphony No. 6 ''Pastoral'' Etc.', 254); 618 | INSERT INTO album (title, artist_id) VALUES ('Bartok: Violin & Viola Concertos', 255); 619 | INSERT INTO album (title, artist_id) VALUES ('Mendelssohn: A Midsummer Night''s Dream', 256); 620 | INSERT INTO album (title, artist_id) VALUES ('Bach: Orchestral Suites Nos. 1 - 4', 257); 621 | INSERT INTO album (title, artist_id) VALUES ('Charpentier: Divertissements, Airs & Concerts', 258); 622 | INSERT INTO album (title, artist_id) VALUES ('South American Getaway', 259); 623 | INSERT INTO album (title, artist_id) VALUES ('Górecki: Symphony No. 3', 260); 624 | INSERT INTO album (title, artist_id) VALUES ('Purcell: The Fairy Queen', 261); 625 | INSERT INTO album (title, artist_id) VALUES ('The Ultimate Relexation Album', 262); 626 | INSERT INTO album (title, artist_id) VALUES ('Purcell: Music for the Queen Mary', 263); 627 | INSERT INTO album (title, artist_id) VALUES ('Weill: The Seven Deadly Sins', 264); 628 | INSERT INTO album (title, artist_id) VALUES ('J.S. Bach: Chaconne, Suite in E Minor, Partita in E Major & Prelude, Fugue and Allegro', 265); 629 | INSERT INTO album (title, artist_id) VALUES ('Prokofiev: Symphony No.5 & Stravinksy: Le Sacre Du Printemps', 248); 630 | INSERT INTO album (title, artist_id) VALUES ('Szymanowski: Piano Works, Vol. 1', 266); 631 | INSERT INTO album (title, artist_id) VALUES ('Nielsen: The Six Symphonies', 267); 632 | INSERT INTO album (title, artist_id) VALUES ('Great Recordings of the Century: Paganini''s 24 Caprices', 268); 633 | INSERT INTO album (title, artist_id) VALUES ('Liszt - 12 Études D''Execution Transcendante', 269); 634 | INSERT INTO album (title, artist_id) VALUES ('Great Recordings of the Century - Shubert: Schwanengesang, 4 Lieder', 270); 635 | INSERT INTO album (title, artist_id) VALUES ('Locatelli: Concertos for Violin, Strings and Continuo, Vol. 3', 271); 636 | INSERT INTO album (title, artist_id) VALUES ('Respighi:Pines of Rome', 226); 637 | INSERT INTO album (title, artist_id) VALUES ('Schubert: The Late String Quartets & String Quintet (3 CD''s)', 272); 638 | INSERT INTO album (title, artist_id) VALUES ('Monteverdi: L''Orfeo', 273); 639 | INSERT INTO album (title, artist_id) VALUES ('Mozart: Chamber Music', 274); 640 | INSERT INTO album (title, artist_id) VALUES ('Koyaanisqatsi (Soundtrack from the Motion Picture)', 275); 641 | -------------------------------------------------------------------------------- /testdata/init.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/go-ozzo/ozzo-dbx" 10 | _ "github.com/lib/pq" // initialize posgresql for test 11 | "github.com/qiangxue/golang-restful-starter-kit/app" 12 | ) 13 | 14 | var ( 15 | DB *dbx.DB 16 | ) 17 | 18 | func init() { 19 | // the test may be started from the home directory or a subdirectory 20 | err := app.LoadConfig("./config", "../config") 21 | if err != nil { 22 | panic(err) 23 | } 24 | DB, err = dbx.MustOpen("postgres", app.Config.DSN) 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | // ResetDB re-create the database schema and re-populate the initial data using the SQL statements in db.sql. 31 | // This method is mainly used in tests. 32 | func ResetDB() *dbx.DB { 33 | if err := runSQLFile(DB, getSQLFile()); err != nil { 34 | panic(fmt.Errorf("Error while initializing test database: %s", err)) 35 | } 36 | return DB 37 | } 38 | 39 | func getSQLFile() string { 40 | if _, err := os.Stat("testdata/db.sql"); err == nil { 41 | return "testdata/db.sql" 42 | } 43 | return "../testdata/db.sql" 44 | } 45 | 46 | func runSQLFile(db *dbx.DB, file string) error { 47 | s, err := ioutil.ReadFile(file) 48 | if err != nil { 49 | return err 50 | } 51 | lines := strings.Split(string(s), ";") 52 | for _, line := range lines { 53 | line = strings.TrimSpace(line) 54 | if line == "" { 55 | continue 56 | } 57 | if _, err := db.NewQuery(line).Execute(); err != nil { 58 | fmt.Println(line) 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /util/paginated_list.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // PaginatedList represents a paginated list of data items. 9 | type PaginatedList struct { 10 | Page int `json:"page"` 11 | PerPage int `json:"per_page"` 12 | PageCount int `json:"page_count"` 13 | TotalCount int `json:"total_count"` 14 | Items interface{} `json:"items"` 15 | } 16 | 17 | // NewPaginatedList creates a new Paginated instance. 18 | // The page parameter is 1-based and refers to the current page index/number. 19 | // The perPage parameter refers to the number of items on each page. 20 | // And the total parameter specifies the total number of data items. 21 | // If total is less than 0, it means total is unknown. 22 | func NewPaginatedList(page, perPage, total int) *PaginatedList { 23 | if perPage < 1 { 24 | perPage = 100 25 | } 26 | pageCount := -1 27 | if total >= 0 { 28 | pageCount = (total + perPage - 1) / perPage 29 | if page > pageCount { 30 | page = pageCount 31 | } 32 | } 33 | if page < 1 { 34 | page = 1 35 | } 36 | 37 | return &PaginatedList{ 38 | Page: page, 39 | PerPage: perPage, 40 | TotalCount: total, 41 | PageCount: pageCount, 42 | } 43 | } 44 | 45 | // Offset returns the OFFSET value that can be used in a SQL statement. 46 | func (p *PaginatedList) Offset() int { 47 | return (p.Page - 1) * p.PerPage 48 | } 49 | 50 | // Limit returns the LIMIT value that can be used in a SQL statement. 51 | func (p *PaginatedList) Limit() int { 52 | return p.PerPage 53 | } 54 | 55 | // BuildLinkHeader returns an HTTP header containing the links about the pagination. 56 | func (p *PaginatedList) BuildLinkHeader(baseUrl string, defaultPerPage int) string { 57 | links := p.BuildLinks(baseUrl, defaultPerPage) 58 | header := "" 59 | if links[0] != "" { 60 | header += fmt.Sprintf("<%v>; rel=\"first\", ", links[0]) 61 | header += fmt.Sprintf("<%v>; rel=\"prev\"", links[1]) 62 | } 63 | if links[2] != "" { 64 | if header != "" { 65 | header += ", " 66 | } 67 | header += fmt.Sprintf("<%v>; rel=\"next\"", links[2]) 68 | if links[3] != "" { 69 | header += fmt.Sprintf(", <%v>; rel=\"last\"", links[3]) 70 | } 71 | } 72 | return header 73 | } 74 | 75 | // BuildLinks returns the first, prev, next, and last links corresponding to the pagination. 76 | // A link could be an empty string if it is not needed. 77 | // For example, if the pagination is at the first page, then both first and prev links 78 | // will be empty. 79 | func (p *PaginatedList) BuildLinks(baseUrl string, defaultPerPage int) [4]string { 80 | var links [4]string 81 | pageCount := p.PageCount 82 | page := p.Page 83 | if pageCount >= 0 && page > pageCount { 84 | page = pageCount 85 | } 86 | if strings.Contains(baseUrl, "?") { 87 | baseUrl += "&" 88 | } else { 89 | baseUrl += "?" 90 | } 91 | if page > 1 { 92 | links[0] = fmt.Sprintf("%vpage=%v", baseUrl, 1) 93 | links[1] = fmt.Sprintf("%vpage=%v", baseUrl, page-1) 94 | } 95 | if pageCount >= 0 && page < pageCount { 96 | links[2] = fmt.Sprintf("%vpage=%v", baseUrl, page+1) 97 | links[3] = fmt.Sprintf("%vpage=%v", baseUrl, pageCount) 98 | } else if pageCount < 0 { 99 | links[2] = fmt.Sprintf("%vpage=%v", baseUrl, page+1) 100 | } 101 | if perPage := p.PerPage; perPage != defaultPerPage { 102 | for i := 0; i < 4; i++ { 103 | if links[i] != "" { 104 | links[i] += fmt.Sprintf("&per_page=%v", perPage) 105 | } 106 | } 107 | } 108 | 109 | return links 110 | } 111 | -------------------------------------------------------------------------------- /util/paginated_list_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewPaginatedList(t *testing.T) { 10 | tests := []struct { 11 | tag string 12 | page, perPage, total int 13 | expectedPage, expectedPerPage, expectedTotal, pageCount, offset, limit int 14 | }{ 15 | // varying page 16 | {"t1", 1, 20, 50, 1, 20, 50, 3, 0, 20}, 17 | {"t2", 2, 20, 50, 2, 20, 50, 3, 20, 20}, 18 | {"t3", 3, 20, 50, 3, 20, 50, 3, 40, 20}, 19 | {"t4", 4, 20, 50, 3, 20, 50, 3, 40, 20}, 20 | {"t5", 0, 20, 50, 1, 20, 50, 3, 0, 20}, 21 | 22 | // varying perPage 23 | {"t6", 1, 0, 50, 1, 100, 50, 1, 0, 100}, 24 | {"t7", 1, -1, 50, 1, 100, 50, 1, 0, 100}, 25 | {"t8", 1, 100, 50, 1, 100, 50, 1, 0, 100}, 26 | 27 | // varying total 28 | {"t9", 1, 20, 0, 1, 20, 0, 0, 0, 20}, 29 | {"t10", 1, 20, -1, 1, 20, -1, -1, 0, 20}, 30 | } 31 | 32 | for _, test := range tests { 33 | p := NewPaginatedList(test.page, test.perPage, test.total) 34 | assert.Equal(t, test.expectedPage, p.Page, test.tag) 35 | assert.Equal(t, test.expectedPerPage, p.PerPage, test.tag) 36 | assert.Equal(t, test.expectedTotal, p.TotalCount, test.tag) 37 | assert.Equal(t, test.pageCount, p.PageCount, test.tag) 38 | assert.Equal(t, test.offset, p.Offset(), test.tag) 39 | assert.Equal(t, test.limit, p.Limit(), test.tag) 40 | } 41 | } 42 | 43 | func TestPaginatedList_LinkHeader(t *testing.T) { 44 | baseUrl := "/tokens" 45 | defaultPerPage := 10 46 | tests := []struct { 47 | tag string 48 | page, perPage, total int 49 | header string 50 | }{ 51 | {"t1", 1, 20, 50, "; rel=\"next\", ; rel=\"last\""}, 52 | {"t2", 2, 20, 50, "; rel=\"first\", ; rel=\"prev\", ; rel=\"next\", ; rel=\"last\""}, 53 | {"t3", 3, 20, 50, "; rel=\"first\", ; rel=\"prev\""}, 54 | {"t4", 0, 20, 50, "; rel=\"next\", ; rel=\"last\""}, 55 | {"t5", 4, 20, 50, "; rel=\"first\", ; rel=\"prev\""}, 56 | {"t6", 1, 20, 0, ""}, 57 | } 58 | for _, test := range tests { 59 | p := NewPaginatedList(test.page, test.perPage, test.total) 60 | assert.Equal(t, test.header, p.BuildLinkHeader(baseUrl, defaultPerPage), test.tag) 61 | } 62 | 63 | baseUrl = "/tokens?from=10" 64 | p := NewPaginatedList(1, 20, 50) 65 | assert.Equal(t, "; rel=\"next\", ; rel=\"last\"", p.BuildLinkHeader(baseUrl, defaultPerPage)) 66 | } 67 | --------------------------------------------------------------------------------