You've already forked openaccounting-server
mirror of
https://github.com/openaccounting/oa-server.git
synced 2025-12-09 00:50:59 +13:00
initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
config.json
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
*.csr
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2018 Open Accounting, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
7
apidoc.json
Normal file
7
apidoc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "OpenAccounting",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Open Accounting API documentation",
|
||||||
|
"title": "Open Accounting API documentation",
|
||||||
|
"url" : "https://openaccounting.io/api"
|
||||||
|
}
|
||||||
12
config.json.sample
Normal file
12
config.json.sample
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"WebUrl": "https://domain.com",
|
||||||
|
"Port": 8080,
|
||||||
|
"KeyFile": "",
|
||||||
|
"CertFile": "",
|
||||||
|
"Database": "openaccounting",
|
||||||
|
"User": "openaccounting",
|
||||||
|
"Password": "openaccounting",
|
||||||
|
"SendgridKey": "",
|
||||||
|
"SendgridEmail": "noreply@domain.com",
|
||||||
|
"SendgridSender": "Sender"
|
||||||
|
}
|
||||||
302
core/api/account.go
Normal file
302
core/api/account.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /orgs/:orgId/accounts Get Accounts by Org id
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetOrgAccounts
|
||||||
|
* @apiGroup Account
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Account.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Account was created
|
||||||
|
* @apiSuccess {Date} updated Date Account was updated
|
||||||
|
* @apiSuccess {String} name Name of the Account.
|
||||||
|
* @apiSuccess {String} parent Id of the parent Account.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
* @apiSuccess {Boolean} debitBalance True if Account has a debit balance.
|
||||||
|
* @apiSuccess {Number} balance Current Account balance in this Account's currency
|
||||||
|
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "22222222222222222222222222222222",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "Cash",
|
||||||
|
* "parent": "11111111111111111111111111111111",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* "debitBalance": true,
|
||||||
|
* "balance": 10000,
|
||||||
|
* "nativeBalance": 10000
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetOrgAccounts(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
// TODO how do we make date an optional parameter
|
||||||
|
// instead of resorting to this hack?
|
||||||
|
date := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
dateParam := r.URL.Query().Get("date")
|
||||||
|
|
||||||
|
if dateParam != "" {
|
||||||
|
dateParamNumeric, err := strconv.ParseInt(dateParam, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, "invalid date", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
date = time.Unix(0, dateParamNumeric*1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, err := model.Instance.GetAccountsWithBalances(orgId, user.Id, "", date)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /orgs/:orgId/accounts Create a new Account
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostAccount
|
||||||
|
* @apiGroup Account
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id Id 32 character hex string
|
||||||
|
* @apiParam {String} name Name of the Account.
|
||||||
|
* @apiParam {String} parent Id of the parent Account.
|
||||||
|
* @apiParam {String} currency Three letter currency code.
|
||||||
|
* @apiParam {Number} precision How many digits the currency goes out to.
|
||||||
|
* @apiParam {Boolean} debitBalance True if account has a debit balance.
|
||||||
|
* @apiParam {Number} balance Current Account balance in this Account's currency
|
||||||
|
* @apiParam {Number} nativeBalance Current Account balance in the Org's currency
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Account.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Account was created
|
||||||
|
* @apiSuccess {Date} updated Date Account was updated
|
||||||
|
* @apiSuccess {String} name Name of the Account.
|
||||||
|
* @apiSuccess {String} parent Id of the parent Account.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
* @apiSuccess {Boolean} debitBalance True if account has a debit balance.
|
||||||
|
* @apiSuccess {Number} balance Current Account balance in this Account's currency
|
||||||
|
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "22222222222222222222222222222222",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "Cash",
|
||||||
|
* "parent": "11111111111111111111111111111111",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* "debitBalance": true,
|
||||||
|
* "balance": 10000,
|
||||||
|
* "nativeBalance": 10000
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostAccount(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(content) == 0 {
|
||||||
|
rest.Error(w, "JSON payload is empty", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account := types.NewAccount()
|
||||||
|
|
||||||
|
err = json.Unmarshal(content, &account)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Maybe it's an array of accounts?
|
||||||
|
PostAccounts(w, r, content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account.OrgId = orgId
|
||||||
|
err = model.Instance.CreateAccount(account, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostAccounts(w rest.ResponseWriter, r *rest.Request, content []byte) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
accounts := make([]*types.Account, 0)
|
||||||
|
|
||||||
|
err := json.Unmarshal(content, &accounts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.OrgId = orgId
|
||||||
|
err = model.Instance.CreateAccount(account, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /orgs/:orgId/accounts/:accountId Modify an Account
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutAccount
|
||||||
|
* @apiGroup Account
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id Id 32 character hex string
|
||||||
|
* @apiParam {String} name Name of the Account.
|
||||||
|
* @apiParam {String} parent Id of the parent Account.
|
||||||
|
* @apiParam {String} currency Three letter currency code.
|
||||||
|
* @apiParam {Number} precision How many digits the currency goes out to.
|
||||||
|
* @apiParam {Boolean} debitBalance True if Account has a debit balance.
|
||||||
|
* @apiParam {Number} balance Current Account balance in this Account's currency
|
||||||
|
* @apiParam {Number} nativeBalance Current Account balance in the Org's currency
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Account.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Account was created
|
||||||
|
* @apiSuccess {Date} updated Date Account was updated
|
||||||
|
* @apiSuccess {String} name Name of the Account.
|
||||||
|
* @apiSuccess {String} parent Id of the parent Account.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
* @apiSuccess {Boolean} debitBalance True if Account has a debit balance.
|
||||||
|
* @apiSuccess {Number} balance Current Account balance in this Account's currency
|
||||||
|
* @apiSuccess {Number} nativeBalance Current Account balance in the Org's currency
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "22222222222222222222222222222222",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "Cash",
|
||||||
|
* "parent": "11111111111111111111111111111111",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* "debitBalance": true,
|
||||||
|
* "balance": 10000,
|
||||||
|
* "nativeBalance": 10000
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutAccount(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
accountId := r.PathParam("accountId")
|
||||||
|
|
||||||
|
account := types.Account{}
|
||||||
|
err := r.DecodeJsonPayload(&account)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Id = accountId
|
||||||
|
account.OrgId = orgId
|
||||||
|
|
||||||
|
err = model.Instance.UpdateAccount(&account, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&account)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /orgs/:orgId/accounts/:accountId Delete an Account
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeleteAccount
|
||||||
|
* @apiGroup Account
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeleteAccount(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
accountId := r.PathParam("accountId")
|
||||||
|
|
||||||
|
err := model.Instance.DeleteAccount(accountId, user.Id, orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
77
core/api/api.go
Normal file
77
core/api/api.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine NotAuthorizedError
|
||||||
|
*
|
||||||
|
* @apiError NotAuthorized API request does not have proper credentials
|
||||||
|
*
|
||||||
|
* @apiErrorExample Error-Response:
|
||||||
|
* HTTP/1.1 403 Not Authorized
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine InternalServerError
|
||||||
|
*
|
||||||
|
* @apiError InternalServer An internal error occurred
|
||||||
|
*
|
||||||
|
* @apiErrorExample Error-Response:
|
||||||
|
* HTTP/1.1 500 Internal Server Error
|
||||||
|
* {
|
||||||
|
* "error": "id required"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
func Init() (*rest.Api, error) {
|
||||||
|
rest.ErrorFieldName = "error"
|
||||||
|
app := rest.NewApi()
|
||||||
|
|
||||||
|
logger := &LoggerMiddleware{}
|
||||||
|
|
||||||
|
var stack = []rest.Middleware{
|
||||||
|
logger,
|
||||||
|
&rest.RecorderMiddleware{},
|
||||||
|
&rest.TimerMiddleware{},
|
||||||
|
&rest.PoweredByMiddleware{},
|
||||||
|
&rest.RecoverMiddleware{},
|
||||||
|
&rest.GzipMiddleware{},
|
||||||
|
&rest.ContentTypeCheckerMiddleware{},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Use(stack...)
|
||||||
|
|
||||||
|
app.Use(&rest.CorsMiddleware{
|
||||||
|
RejectNonCorsRequests: false,
|
||||||
|
OriginValidator: func(origin string, request *rest.Request) bool {
|
||||||
|
//return origin == "http://localhost:4200"
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||||
|
AllowedHeaders: []string{
|
||||||
|
"Accept", "Content-Type", "X-Custom-Header", "Origin", "Authorization", "Accept-Version"},
|
||||||
|
AccessControlAllowCredentials: true,
|
||||||
|
AccessControlMaxAge: 3600,
|
||||||
|
})
|
||||||
|
|
||||||
|
auth := &AuthMiddleware{
|
||||||
|
Realm: "openaccounting",
|
||||||
|
}
|
||||||
|
|
||||||
|
version := &VersionMiddleware{}
|
||||||
|
|
||||||
|
app.Use(auth)
|
||||||
|
app.Use(version)
|
||||||
|
|
||||||
|
router, err := GetRouter(auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
app.SetApp(router)
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
188
core/api/apikey.go
Normal file
188
core/api/apikey.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /apikeys Get API keys
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetApiKeys
|
||||||
|
* @apiGroup ApiKey
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the ApiKey.
|
||||||
|
* @apiSuccess {Date} inserted Date ApiKey was created
|
||||||
|
* @apiSuccess {Date} updated Date Last activity for the ApiKey
|
||||||
|
* @apiSuccess {String} userId Id of the User
|
||||||
|
* @apiSuccess {String} label Label
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "userId": "22222222222222222222222222222222",
|
||||||
|
* "label": "Shopping Cart"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetApiKeys(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
|
||||||
|
keys, err := model.Instance.GetApiKeys(user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /apikeys Create a new API key
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostApiKey
|
||||||
|
* @apiGroup ApiKey
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
*
|
||||||
|
* @apiParam {String} id 32 character hex string
|
||||||
|
* @apiParam {String} label Label
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the ApiKey.
|
||||||
|
* @apiSuccess {Date} inserted Date ApiKey was created
|
||||||
|
* @apiSuccess {Date} updated Date Last activity for the ApiKey
|
||||||
|
* @apiSuccess {String} userId Id of the User
|
||||||
|
* @apiSuccess {String} label Label
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "userId": "22222222222222222222222222222222",
|
||||||
|
* "label": "Shopping Cart"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostApiKey(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
key := &types.ApiKey{}
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(key)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key.UserId = user.Id
|
||||||
|
|
||||||
|
err = model.Instance.CreateApiKey(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /apikeys Modify an API key
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutApiKey
|
||||||
|
* @apiGroup ApiKey
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
*
|
||||||
|
* @apiParam {String} id 32 character hex string
|
||||||
|
* @apiParam {String} label Label
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the ApiKey.
|
||||||
|
* @apiSuccess {Date} inserted Date ApiKey was created
|
||||||
|
* @apiSuccess {Date} updated Date Last activity for the ApiKey
|
||||||
|
* @apiSuccess {String} userId Id of the User
|
||||||
|
* @apiSuccess {String} label Label
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "userId": "22222222222222222222222222222222",
|
||||||
|
* "label": "Shopping Cart"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutApiKey(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
key := &types.ApiKey{}
|
||||||
|
keyId := r.PathParam("apiKeyId")
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(key)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key.Id = keyId
|
||||||
|
key.UserId = user.Id
|
||||||
|
|
||||||
|
err = model.Instance.UpdateApiKey(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /apikeys/:apiKeyId Delete an API key
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeleteApiKey
|
||||||
|
* @apiGroup ApiKey
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeleteApiKey(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
id := r.PathParam("apiKeyId")
|
||||||
|
|
||||||
|
err := model.Instance.DeleteApiKey(id, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
93
core/api/auth.go
Normal file
93
core/api/auth.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/auth"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthMiddleware struct {
|
||||||
|
|
||||||
|
// Realm name to display to the user. Required.
|
||||||
|
Realm string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes AuthMiddleware implement the Middleware interface.
|
||||||
|
func (mw *AuthMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {
|
||||||
|
|
||||||
|
if mw.Realm == "" {
|
||||||
|
log.Fatal("Realm is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(writer rest.ResponseWriter, request *rest.Request) {
|
||||||
|
|
||||||
|
authHeader := request.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
request.Env["USER"] = nil
|
||||||
|
handler(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emailOrKey, password, err := mw.decodeBasicAuthHeader(authHeader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(writer, "Invalid authentication", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate via session, apikey or user
|
||||||
|
user, err := auth.Instance.Authenticate(emailOrKey, password)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
request.Env["USER"] = user
|
||||||
|
handler(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Unauthorized " + emailOrKey)
|
||||||
|
|
||||||
|
mw.unauthorized(writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *AuthMiddleware) unauthorized(writer rest.ResponseWriter) {
|
||||||
|
writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm)
|
||||||
|
rest.Error(writer, "Not Authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *AuthMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) {
|
||||||
|
|
||||||
|
parts := strings.SplitN(header, " ", 2)
|
||||||
|
if !(len(parts) == 2 && parts[0] == "Basic") {
|
||||||
|
return "", "", errors.New("Invalid authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.New("Invalid base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := strings.SplitN(string(decoded), ":", 2)
|
||||||
|
if len(creds) != 2 {
|
||||||
|
return "", "", errors.New("Invalid authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds[0], creds[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *AuthMiddleware) RequireAuth(handler rest.HandlerFunc) rest.HandlerFunc {
|
||||||
|
return func(writer rest.ResponseWriter, request *rest.Request) {
|
||||||
|
|
||||||
|
if request.Env["USER"] == nil {
|
||||||
|
mw.unauthorized(writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
core/api/logger.go
Normal file
89
core/api/logger.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoggerMiddleware struct {
|
||||||
|
Logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *LoggerMiddleware) MiddlewareFunc(h rest.HandlerFunc) rest.HandlerFunc {
|
||||||
|
|
||||||
|
// set the default Logger
|
||||||
|
if mw.Logger == nil {
|
||||||
|
mw.Logger = log.New(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
message := getIp(r)
|
||||||
|
|
||||||
|
message = message + " " + getUser(r)
|
||||||
|
message = message + " " + getTime(r)
|
||||||
|
message = message + " " + getRequest(r)
|
||||||
|
message = message + " " + getStatus(r)
|
||||||
|
message = message + " " + getBytes(r)
|
||||||
|
message = message + " " + getUserAgent(r)
|
||||||
|
|
||||||
|
mw.Logger.Print(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIp(r *rest.Request) string {
|
||||||
|
remoteAddr := r.RemoteAddr
|
||||||
|
if remoteAddr != "" {
|
||||||
|
if ip, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(r *rest.Request) string {
|
||||||
|
if r.Env["USER"] != nil {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
return user.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTime(r *rest.Request) string {
|
||||||
|
if r.Env["START_TIME"] != nil {
|
||||||
|
return r.Env["START_TIME"].(*time.Time).Format("02/Jan/2006:15:04:05 -0700")
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRequest(r *rest.Request) string {
|
||||||
|
return r.Method + " " + r.URL.RequestURI()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatus(r *rest.Request) string {
|
||||||
|
if r.Env["STATUS_CODE"] != nil {
|
||||||
|
return strconv.Itoa(r.Env["STATUS_CODE"].(int))
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBytes(r *rest.Request) string {
|
||||||
|
if r.Env["BYTES_WRITTEN"] != nil {
|
||||||
|
return strconv.FormatInt(r.Env["BYTES_WRITTEN"].(int64), 10)
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserAgent(r *rest.Request) string {
|
||||||
|
if r.UserAgent() != "" {
|
||||||
|
return r.UserAgent()
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
394
core/api/org.go
Normal file
394
core/api/org.go
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /org/:orgId Get Org by id
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetOrg
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Org was created
|
||||||
|
* @apiSuccess {Date} updated Date Org was updated
|
||||||
|
* @apiSuccess {String} name Name of the Org.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "MyOrg",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetOrg(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
org, err := model.Instance.GetOrg(orgId, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&org)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /orgs Get a User's Orgs
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetOrgs
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Org was created
|
||||||
|
* @apiSuccess {Date} updated Date Org was updated
|
||||||
|
* @apiSuccess {String} name Name of the Org.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "MyOrg",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetOrgs(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
|
||||||
|
orgs, err := model.Instance.GetOrgs(user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&orgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /orgs Create a new Org
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostOrg
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id Id 32 character hex string
|
||||||
|
* @apiParam {String} name Name of the Org.
|
||||||
|
* @apiParam {String} currency Three letter currency code.
|
||||||
|
* @apiParam {Number} precision How many digits the currency goes out to.
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Org was created
|
||||||
|
* @apiSuccess {Date} updated Date Org was updated
|
||||||
|
* @apiSuccess {String} name Name of the Org.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "MyOrg",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostOrg(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
org := types.Org{Precision: 2}
|
||||||
|
err := r.DecodeJsonPayload(&org)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.Instance.CreateOrg(&org, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&org)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /orgs/:orgId Modify an Org
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutOrg
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} name Name of the Org.
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Org.
|
||||||
|
* @apiSuccess {Date} inserted Date Org was created
|
||||||
|
* @apiSuccess {Date} updated Date Org was updated
|
||||||
|
* @apiSuccess {String} name Name of the Org.
|
||||||
|
* @apiSuccess {String} currency Three letter currency code.
|
||||||
|
* @apiSuccess {Number} precision How many digits the currency goes out to.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "name": "MyOrg",
|
||||||
|
* "currency": "USD",
|
||||||
|
* "precision": 2,
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutOrg(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
org := types.Org{}
|
||||||
|
err := r.DecodeJsonPayload(&org)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Id = orgId
|
||||||
|
|
||||||
|
err = model.Instance.UpdateOrg(&org, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&org)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /orgs/:orgId/invites Invite a user to an Org
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostInvite
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} email Email address of user
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Invite
|
||||||
|
* @apiSuccess {orgId} id Id of the Org
|
||||||
|
* @apiSuccess {Date} inserted Date Invite was created
|
||||||
|
* @apiSuccess {Date} updated Date Invite was updated/accepted
|
||||||
|
* @apiSuccess {String} email Email address of user
|
||||||
|
* @apiSuccess {String} accepted true if user has accepted
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "a1b2c3d4",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "accepted": false
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostInvite(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
invite := types.Invite{}
|
||||||
|
err := r.DecodeJsonPayload(&invite)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invite.OrgId = orgId
|
||||||
|
|
||||||
|
err = model.Instance.CreateInvite(&invite, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&invite)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /orgs/:orgId/invites/:inviteId Accept an invitation
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutInvite
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} accepted true
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Invite
|
||||||
|
* @apiSuccess {orgId} id Id of the Org
|
||||||
|
* @apiSuccess {Date} inserted Date Invite was created
|
||||||
|
* @apiSuccess {Date} updated Date Invite was updated/accepted
|
||||||
|
* @apiSuccess {String} email Email address of user
|
||||||
|
* @apiSuccess {String} accepted true if user has accepted
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "a1b2c3d4",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "accepted": true
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutInvite(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
//orgId := r.PathParam("orgId")
|
||||||
|
inviteId := r.PathParam("inviteId")
|
||||||
|
|
||||||
|
invite := types.Invite{}
|
||||||
|
err := r.DecodeJsonPayload(&invite)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invite.Id = inviteId
|
||||||
|
|
||||||
|
err = model.Instance.AcceptInvite(&invite, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&invite)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /orgs/:orgId/invites Get Org invites
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetInvites
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Invite
|
||||||
|
* @apiSuccess {orgId} id Id of the Org
|
||||||
|
* @apiSuccess {Date} inserted Date Invite was created
|
||||||
|
* @apiSuccess {Date} updated Date Invite was updated/accepted
|
||||||
|
* @apiSuccess {String} email Email address of user
|
||||||
|
* @apiSuccess {String} accepted true if user has accepted
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "a1b2c3d4",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "accepted": true
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetInvites(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
invites, err := model.Instance.GetInvites(orgId, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&invites)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /orgs/:orgId/invites/:inviteId Delete Invite
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeleteInvite
|
||||||
|
* @apiGroup Org
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeleteInvite(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
inviteId := r.PathParam("inviteId")
|
||||||
|
|
||||||
|
err := model.Instance.DeleteInvite(inviteId, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
183
core/api/price.go
Normal file
183
core/api/price.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /org/:orgId/prices Get prices nearest in time or by currency
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetPrices
|
||||||
|
* @apiGroup Price
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {Number} nearestDate Milliseconds since epoch
|
||||||
|
* @apiParam {String} currency Currency code
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Price.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} currency Currency code.
|
||||||
|
* @apiSuccess {Date} date Date of the Price.
|
||||||
|
* @apiSuccess {Date} inserted Date when Price was posted.
|
||||||
|
* @apiSuccess {Date} updated Date when Price was updated.
|
||||||
|
* @apiSuccess {Number} price Price of currency measured in native Org currency.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "currency": "EUR",
|
||||||
|
* "date": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "price": 1.16
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetPrices(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
// TODO how do we make date an optional parameter
|
||||||
|
// instead of resorting to this hack?
|
||||||
|
nearestDate := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
nearestDateParam := r.URL.Query().Get("nearestDate")
|
||||||
|
currencyParam := r.URL.Query().Get("currency")
|
||||||
|
|
||||||
|
// If currency was specified, get all prices for that currency
|
||||||
|
if currencyParam != "" {
|
||||||
|
prices, err := model.Instance.GetPricesByCurrency(orgId, currencyParam, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(prices)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if nearestDateParam != "" {
|
||||||
|
nearestDateParamNumeric, err := strconv.ParseInt(nearestDateParam, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, "invalid date", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nearestDate = time.Unix(0, nearestDateParamNumeric*1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get prices nearest in time
|
||||||
|
prices, err := model.Instance.GetPricesNearestInTime(orgId, nearestDate, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(prices)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /orgs/:orgId/prices Create a new Price
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostPrice
|
||||||
|
* @apiGroup Price
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id Id 32 character hex string.
|
||||||
|
* @apiParam {String} orgId Id of the Org.
|
||||||
|
* @apiParam {String} currency Currency code.
|
||||||
|
* @apiParam {Date} date Date of the Price.
|
||||||
|
* @apiParam {Number} price Price of currency measured in native Org currency.
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Price.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} currency Currency code.
|
||||||
|
* @apiSuccess {Date} date Date of the Price.
|
||||||
|
* @apiSuccess {Date} inserted Date when Price was posted.
|
||||||
|
* @apiSuccess {Date} updated Date when Price was updated.
|
||||||
|
* @apiSuccess {Number} price Price of currency measured in native Org currency.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "currency": "EUR",
|
||||||
|
* "date": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "price": 1.16
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostPrice(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
price := types.Price{}
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(&price)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
price.OrgId = orgId
|
||||||
|
err = model.Instance.CreatePrice(&price, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&price)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /orgs/:orgId/prices/:priceId Delete a Price
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeletePrice
|
||||||
|
* @apiGroup Price
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeletePrice(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
priceId := r.PathParam("priceId")
|
||||||
|
|
||||||
|
err := model.Instance.DeletePrice(priceId, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
48
core/api/routes.go
Normal file
48
core/api/routes.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRouter(auth *AuthMiddleware) (rest.App, error) {
|
||||||
|
return rest.MakeRouter(
|
||||||
|
rest.Get("/api/user", auth.RequireAuth(GetUser)),
|
||||||
|
rest.Put("/api/user", PutUser),
|
||||||
|
rest.Post("/api/user/verify", VerifyUser),
|
||||||
|
rest.Post("/api/user/reset-password", ResetPassword),
|
||||||
|
rest.Post("/api/users", PostUser),
|
||||||
|
rest.Post("/api/orgs", auth.RequireAuth(PostOrg)),
|
||||||
|
rest.Get("/api/orgs", auth.RequireAuth(GetOrgs)),
|
||||||
|
rest.Get("/api/orgs/:orgId", auth.RequireAuth(GetOrg)),
|
||||||
|
rest.Put("/api/orgs/:orgId", auth.RequireAuth(PutOrg)),
|
||||||
|
rest.Get("/api/orgs/:orgId/ledgers", auth.RequireAuth(GetOrgAccounts)),
|
||||||
|
rest.Post("/api/orgs/:orgId/ledgers", auth.RequireAuth(PostAccount)),
|
||||||
|
rest.Put("/api/orgs/:orgId/ledgers/:accountId", auth.RequireAuth(PutAccount)),
|
||||||
|
rest.Delete("/api/orgs/:orgId/ledgers/:accountId", auth.RequireAuth(DeleteAccount)),
|
||||||
|
rest.Get("/api/orgs/:orgId/ledgers/:accountId/transactions", auth.RequireAuth(GetTransactionsByAccount)),
|
||||||
|
rest.Get("/api/orgs/:orgId/accounts", auth.RequireAuth(GetOrgAccounts)),
|
||||||
|
rest.Post("/api/orgs/:orgId/accounts", auth.RequireAuth(PostAccount)),
|
||||||
|
rest.Put("/api/orgs/:orgId/accounts/:accountId", auth.RequireAuth(PutAccount)),
|
||||||
|
rest.Delete("/api/orgs/:orgId/accounts/:accountId", auth.RequireAuth(DeleteAccount)),
|
||||||
|
rest.Get("/api/orgs/:orgId/accounts/:accountId/transactions", auth.RequireAuth(GetTransactionsByAccount)),
|
||||||
|
rest.Get("/api/orgs/:orgId/transactions", auth.RequireAuth(GetTransactionsByOrg)),
|
||||||
|
rest.Post("/api/orgs/:orgId/transactions", auth.RequireAuth(PostTransaction)),
|
||||||
|
rest.Put("/api/orgs/:orgId/transactions/:transactionId", auth.RequireAuth(PutTransaction)),
|
||||||
|
rest.Delete("/api/orgs/:orgId/transactions/:transactionId", auth.RequireAuth(DeleteTransaction)),
|
||||||
|
rest.Get("/api/orgs/:orgId/prices", auth.RequireAuth(GetPrices)),
|
||||||
|
rest.Post("/api/orgs/:orgId/prices", auth.RequireAuth(PostPrice)),
|
||||||
|
rest.Delete("/api/orgs/:orgId/prices/:priceId", auth.RequireAuth(DeletePrice)),
|
||||||
|
rest.Get("/ws", ws.Handler),
|
||||||
|
rest.Post("/api/sessions", auth.RequireAuth(PostSession)),
|
||||||
|
rest.Delete("/api/sessions/:sessionId", auth.RequireAuth(DeleteSession)),
|
||||||
|
rest.Get("/api/apikeys", auth.RequireAuth(GetApiKeys)),
|
||||||
|
rest.Post("/api/apikeys", auth.RequireAuth(PostApiKey)),
|
||||||
|
rest.Put("/api/apikeys/:apiKeyId", auth.RequireAuth(PutApiKey)),
|
||||||
|
rest.Delete("/api/apikeys/:apiKeyId", auth.RequireAuth(DeleteApiKey)),
|
||||||
|
rest.Get("/api/orgs/:orgId/invites", auth.RequireAuth(GetInvites)),
|
||||||
|
rest.Post("/api/orgs/:orgId/invites", auth.RequireAuth(PostInvite)),
|
||||||
|
rest.Put("/api/orgs/:orgId/invites/:inviteId", auth.RequireAuth(PutInvite)),
|
||||||
|
rest.Delete("/api/orgs/:orgId/invites/:inviteId", auth.RequireAuth(DeleteInvite)),
|
||||||
|
)
|
||||||
|
}
|
||||||
87
core/api/session.go
Normal file
87
core/api/session.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /sessions Create a new Session
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostSession
|
||||||
|
* @apiGroup Session
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
*
|
||||||
|
* @apiParam {String} id 32 character hex string
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Session.
|
||||||
|
* @apiSuccess {Date} inserted Date Session was created
|
||||||
|
* @apiSuccess {Date} updated Date Last activity for the Session
|
||||||
|
* @apiSuccess {String} userId Id of the User
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "userId": "22222222222222222222222222222222"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostSession(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
session := &types.Session{}
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(session)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.UserId = user.Id
|
||||||
|
|
||||||
|
err = model.Instance.CreateSession(session)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /sessions/:sessionId Log out of a Session
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeleteSession
|
||||||
|
* @apiGroup Session
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeleteSession(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
sessionId := r.PathParam("sessionId")
|
||||||
|
|
||||||
|
err := model.Instance.DeleteSession(sessionId, user.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
362
core/api/transaction.go
Normal file
362
core/api/transaction.go
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /orgs/:orgId/accounts/:accountId/transactions Get Transactions by Account Id
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetAccountTransactions
|
||||||
|
* @apiGroup Transaction
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Transaction.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} userId Id of the User who created the Transaction.
|
||||||
|
* @apiSuccess {Date} date Date of the Transaction
|
||||||
|
* @apiSuccess {Date} inserted Date Transaction was created
|
||||||
|
* @apiSuccess {Date} updated Date Transaction was updated
|
||||||
|
* @apiSuccess {String} description Description of Transaction
|
||||||
|
* @apiSuccess {String} data Extra data field
|
||||||
|
* @apiSuccess {Object[]} splits Array of Transaction Splits
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "userId": "11111111111111111111111111111111",
|
||||||
|
* "date": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "inserted": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "updated": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "description": "Treat friend to lunch",
|
||||||
|
* "data:": "{\"key\": \"value\"}",
|
||||||
|
* "splits": [
|
||||||
|
* {
|
||||||
|
* "accountId": "11111111111111111111111111111111",
|
||||||
|
* "amount": -2000,
|
||||||
|
* "nativeAmount": -2000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "22222222222222222222222222222222",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "33333333333333333333333333333333",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetTransactionsByAccount(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
accountId := r.PathParam("accountId")
|
||||||
|
|
||||||
|
queryOptions, err := types.QueryOptionsFromURLQuery(r.URL.Query())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, "invalid query options", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sTxs, err := model.Instance.GetTransactionsByAccount(orgId, user.Id, accountId, queryOptions)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&sTxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /orgs/:orgId/transactions Get Transactions by Org Id
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetOrgTransactions
|
||||||
|
* @apiGroup Transaction
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Transaction.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} userId Id of the User who created the Transaction.
|
||||||
|
* @apiSuccess {Date} date Date of the Transaction
|
||||||
|
* @apiSuccess {Date} inserted Date Transaction was created
|
||||||
|
* @apiSuccess {Date} updated Date Transaction was updated
|
||||||
|
* @apiSuccess {String} description Description of Transaction
|
||||||
|
* @apiSuccess {String} data Extra data field
|
||||||
|
* @apiSuccess {Object[]} splits Array of Transaction Splits
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "userId": "11111111111111111111111111111111",
|
||||||
|
* "date": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "inserted": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "updated": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "description": "Treat friend to lunch",
|
||||||
|
* "data:": "{\"key\": \"value\"}",
|
||||||
|
* "splits": [
|
||||||
|
* {
|
||||||
|
* "accountId": "11111111111111111111111111111111",
|
||||||
|
* "amount": -2000,
|
||||||
|
* "nativeAmount": -2000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "22222222222222222222222222222222",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "33333333333333333333333333333333",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetTransactionsByOrg(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
queryOptions, err := types.QueryOptionsFromURLQuery(r.URL.Query())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, "invalid query options", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sTxs, err := model.Instance.GetTransactionsByOrg(orgId, user.Id, queryOptions)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(&sTxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /orgs/:orgId/transactions Create a new Transaction
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostTransaction
|
||||||
|
* @apiGroup Transaction
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id Id 32 character hex string
|
||||||
|
* @apiParam {Date} date Date of the Transaction
|
||||||
|
* @apiParam {String} description Description of Transaction
|
||||||
|
* @apiParam {String} data Extra data field
|
||||||
|
* @apiParam {Object[]} splits Array of Transaction Splits. nativeAmounts must add up to 0.
|
||||||
|
* @apiParam {String} splits.accountId Id of Account
|
||||||
|
* @apiParam {Number} splits.amount Amount of split in Account currency
|
||||||
|
* @apiParam {Number} splits.nativeAmount Amount of split in Org currency
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Transaction.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} userId Id of the User who created the Transaction.
|
||||||
|
* @apiSuccess {Date} date Date of the Transaction
|
||||||
|
* @apiSuccess {Date} inserted Date Transaction was created
|
||||||
|
* @apiSuccess {Date} updated Date Transaction was updated
|
||||||
|
* @apiSuccess {String} description Description of Transaction
|
||||||
|
* @apiSuccess {String} data Extra data field
|
||||||
|
* @apiSuccess {Object[]} splits Array of Transaction Splits
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "userId": "11111111111111111111111111111111",
|
||||||
|
* "date": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "inserted": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "updated": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "description": "Treat friend to lunch",
|
||||||
|
* "data:": "{\"key\": \"value\"}",
|
||||||
|
* "splits": [
|
||||||
|
* {
|
||||||
|
* "accountId": "11111111111111111111111111111111",
|
||||||
|
* "amount": -2000,
|
||||||
|
* "nativeAmount": -2000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "22222222222222222222222222222222",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "33333333333333333333333333333333",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostTransaction(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
|
||||||
|
sTx := types.Transaction{}
|
||||||
|
err := r.DecodeJsonPayload(&sTx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sTx.OrgId = orgId
|
||||||
|
sTx.UserId = user.Id
|
||||||
|
|
||||||
|
err = model.Instance.CreateTransaction(&sTx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(sTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /orgs/:orgId/transactions/:transactionId Modify a Transaction
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutTransaction
|
||||||
|
* @apiGroup Transaction
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id 32 character hex string
|
||||||
|
* @apiParam {Date} date Date of the Transaction
|
||||||
|
* @apiParam {String} description Description of Transaction
|
||||||
|
* @apiParam {String} data Extra data field
|
||||||
|
* @apiParam {Object[]} splits Array of Transaction Splits. nativeAmounts must add up to 0.
|
||||||
|
* @apiParam {String} splits.accountId Id of Account
|
||||||
|
* @apiParam {Number} splits.amount Amount of split in Account currency
|
||||||
|
* @apiParam {Number} splits.nativeAmount Amount of split in Org currency
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the Transaction.
|
||||||
|
* @apiSuccess {String} orgId Id of the Org.
|
||||||
|
* @apiSuccess {String} userId Id of the User who created the Transaction.
|
||||||
|
* @apiSuccess {Date} date Date of the Transaction
|
||||||
|
* @apiSuccess {Date} inserted Date Transaction was created
|
||||||
|
* @apiSuccess {Date} updated Date Transaction was updated
|
||||||
|
* @apiSuccess {String} description Description of Transaction
|
||||||
|
* @apiSuccess {String} data Extra data field
|
||||||
|
* @apiSuccess {Object[]} splits Array of Transaction Splits
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "orgId": "11111111111111111111111111111111",
|
||||||
|
* "userId": "11111111111111111111111111111111",
|
||||||
|
* "date": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "inserted": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "updated": "2018-06-08T20:12:29.720Z",
|
||||||
|
* "description": "Treat friend to lunch",
|
||||||
|
* "data:": "{\"key\": \"value\"}",
|
||||||
|
* "splits": [
|
||||||
|
* {
|
||||||
|
* "accountId": "11111111111111111111111111111111",
|
||||||
|
* "amount": -2000,
|
||||||
|
* "nativeAmount": -2000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "22222222222222222222222222222222",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "accountId": "33333333333333333333333333333333",
|
||||||
|
* "amount": 1000,
|
||||||
|
* "nativeAmount": 1000
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutTransaction(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
transactionId := r.PathParam("transactionId")
|
||||||
|
|
||||||
|
sTx := types.Transaction{}
|
||||||
|
err := r.DecodeJsonPayload(&sTx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sTx.OrgId = orgId
|
||||||
|
sTx.UserId = user.Id
|
||||||
|
|
||||||
|
err = model.Instance.UpdateTransaction(transactionId, &sTx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(sTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /orgs/:orgId/transactions/:transactionId Delete a Transaction
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName DeleteTransaction
|
||||||
|
* @apiGroup Transaction
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func DeleteTransaction(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
orgId := r.PathParam("orgId")
|
||||||
|
transactionId := r.PathParam("transactionId")
|
||||||
|
|
||||||
|
err := model.Instance.DeleteTransaction(transactionId, user.Id, orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
265
core/api/user.go
Normal file
265
core/api/user.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VerifyUserParams struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmResetPasswordParams struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetPasswordParams struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /user Get Authenticated User
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName GetUser
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the User.
|
||||||
|
* @apiSuccess {Date} inserted Date User was created
|
||||||
|
* @apiSuccess {Date} updated Date User was updated
|
||||||
|
* @apiSuccess {String} firstName First name of the User.
|
||||||
|
* @apiSuccess {String} lastName Last name of the User.
|
||||||
|
* @apiSuccess {String} email Email of the User.
|
||||||
|
* @apiSuccess {Boolean} agreeToTerms Agree to terms
|
||||||
|
* @apiSuccess {Boolean} emailVerified True if email has been verified.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "firstName": "John",
|
||||||
|
* "lastName": "Doe",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "agreeToTerms": true,
|
||||||
|
* "emailVerified": true
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse NotAuthorizedError
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func GetUser(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
|
||||||
|
w.WriteJson(&user)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /users Create a new User
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PostUser
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} id 32 character hex string
|
||||||
|
* @apiParam {String} firstName First name of the User.
|
||||||
|
* @apiParam {String} lastName Last name of the User.
|
||||||
|
* @apiParam {String} email Email of the User.
|
||||||
|
* @apiParam {String} password Password of the User.
|
||||||
|
* @apiParam {Boolean} agreeToTerms True if you agree to terms
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the User.
|
||||||
|
* @apiSuccess {Date} inserted Date User was created
|
||||||
|
* @apiSuccess {Date} updated Date User was updated
|
||||||
|
* @apiSuccess {String} firstName First name of the User.
|
||||||
|
* @apiSuccess {String} lastName Last name of the User.
|
||||||
|
* @apiSuccess {String} email Email of the User.
|
||||||
|
* @apiSuccess {Boolean} emailVerified True if email has been verified.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "firstName": "John",
|
||||||
|
* "lastName": "Doe",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "agreeToTerms": true,
|
||||||
|
* "emailVerified": true
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PostUser(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
user := &types.User{}
|
||||||
|
err := r.DecodeJsonPayload(user)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.Instance.CreateUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /user Modify User
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName PutUser
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Authorization HTTP Basic Auth
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} password New password
|
||||||
|
* @apiParam {String} code Password reset code. (Instead of Authorization header)
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} id Id of the User.
|
||||||
|
* @apiSuccess {Date} inserted Date User was created
|
||||||
|
* @apiSuccess {Date} updated Date User was updated
|
||||||
|
* @apiSuccess {String} firstName First name of the User.
|
||||||
|
* @apiSuccess {String} lastName Last name of the User.
|
||||||
|
* @apiSuccess {String} email Email of the User.
|
||||||
|
* @apiSuccess {Boolean} emailVerified True if email has been verified.
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "id": "11111111111111111111111111111111",
|
||||||
|
* "inserted": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "updated": "2018-09-11T18:05:04.420Z",
|
||||||
|
* "firstName": "John",
|
||||||
|
* "lastName": "Doe",
|
||||||
|
* "email": "johndoe@email.com",
|
||||||
|
* "agreeToTerms": true,
|
||||||
|
* "emailVerified": true
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func PutUser(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
if r.Env["USER"] == nil {
|
||||||
|
// password reset
|
||||||
|
params := &ConfirmResetPasswordParams{}
|
||||||
|
err := r.DecodeJsonPayload(params)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := model.Instance.ConfirmResetPassword(params.Password, params.Code)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it's an authenticated PUT
|
||||||
|
|
||||||
|
user := r.Env["USER"].(*types.User)
|
||||||
|
|
||||||
|
newUser := &types.User{}
|
||||||
|
err := r.DecodeJsonPayload(newUser)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Password = newUser.Password
|
||||||
|
|
||||||
|
err = model.Instance.UpdateUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteJson(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /user/verify Verify user email address
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName VerifyUser
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} code Email verification code
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func VerifyUser(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
params := &VerifyUserParams{}
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(params)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.Instance.VerifyUser(params.Code)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /user/reset-password Send reset password email
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiName ResetPassword
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiHeader {String} Accept-Version ^1.0.0 semver versioning
|
||||||
|
*
|
||||||
|
* @apiParam {String} email Email address for user
|
||||||
|
*
|
||||||
|
* @apiSuccessExample Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
*
|
||||||
|
* @apiUse InternalServerError
|
||||||
|
*/
|
||||||
|
func ResetPassword(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
params := &ResetPasswordParams{}
|
||||||
|
|
||||||
|
err := r.DecodeJsonPayload(params)
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.Instance.ResetPassword(params.Email)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
48
core/api/version.go
Normal file
48
core/api/version.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VersionMiddleware struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes AuthMiddleware implement the Middleware interface.
|
||||||
|
func (mw *VersionMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc {
|
||||||
|
return func(writer rest.ResponseWriter, request *rest.Request) {
|
||||||
|
version := request.Header.Get("Accept-Version")
|
||||||
|
|
||||||
|
// Don't require version header for websockets
|
||||||
|
if request.URL.String() == "/ws" {
|
||||||
|
handler(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
rest.Error(writer, "Accept-Version header required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
constraint, err := semver.NewConstraint(version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rest.Error(writer, "Invalid version", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverVersion, _ := semver.NewVersion("1.0.0")
|
||||||
|
// Pre-release versions
|
||||||
|
compatVersion, _ := semver.NewVersion("0.1.8")
|
||||||
|
|
||||||
|
versionMatch := constraint.Check(serverVersion)
|
||||||
|
compatMatch := constraint.Check(compatVersion)
|
||||||
|
|
||||||
|
if versionMatch == false && compatMatch == false {
|
||||||
|
rest.Error(writer, "Invalid version", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
91
core/auth/auth.go
Normal file
91
core/auth/auth.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Instance Interface
|
||||||
|
|
||||||
|
type AuthService struct {
|
||||||
|
db db.Datastore
|
||||||
|
bcrypt util.Bcrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Authenticate(string, string) (*types.User, error)
|
||||||
|
AuthenticateUser(email string, password string) (*types.User, error)
|
||||||
|
AuthenticateSession(string) (*types.User, error)
|
||||||
|
AuthenticateApiKey(string) (*types.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(db db.Datastore, bcrypt util.Bcrypt) *AuthService {
|
||||||
|
authService := &AuthService{db: db, bcrypt: bcrypt}
|
||||||
|
Instance = authService
|
||||||
|
return authService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) Authenticate(emailOrKey string, password string) (*types.User, error) {
|
||||||
|
// authenticate via session, apikey or user
|
||||||
|
user, err := auth.AuthenticateSession(emailOrKey)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = auth.AuthenticateApiKey(emailOrKey)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = auth.AuthenticateUser(emailOrKey, password)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) AuthenticateUser(email string, password string) (*types.User, error) {
|
||||||
|
u, err := auth.db.GetVerifiedUserByEmail(email)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Invalid email or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = auth.bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Invalid email or password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) AuthenticateSession(id string) (*types.User, error) {
|
||||||
|
u, err := auth.db.GetUserByActiveSession(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Invalid session")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.db.UpdateSessionActivity(id)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) AuthenticateApiKey(id string) (*types.User, error) {
|
||||||
|
u, err := auth.db.GetUserByApiKey(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Access denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.db.UpdateApiKeyActivity(id)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
90
core/auth/auth_test.go
Normal file
90
core/auth/auth_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TdUser struct {
|
||||||
|
db.Datastore
|
||||||
|
testNum int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdUser) GetVerifiedUserByEmail(email string) (*types.User, error) {
|
||||||
|
switch td.testNum {
|
||||||
|
case 1:
|
||||||
|
return td.GetVerifiedUserByEmail_1(email)
|
||||||
|
case 2:
|
||||||
|
return td.GetVerifiedUserByEmail_2(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("test error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdUser) GetVerifiedUserByEmail_1(email string) (*types.User, error) {
|
||||||
|
return &types.User{
|
||||||
|
"1",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
"johndoe@email.com",
|
||||||
|
"password",
|
||||||
|
"$2a$10$KrtvADe7jwrmYIe3GXFbNupOQaPIvyOKeng5826g4VGOD47TpAisG",
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdUser) GetVerifiedUserByEmail_2(email string) (*types.User, error) {
|
||||||
|
return nil, errors.New("sql error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthenticateUser(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
email string
|
||||||
|
password string
|
||||||
|
saltedHash string
|
||||||
|
testNum int
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
email: "johndoe@email.com",
|
||||||
|
password: "password",
|
||||||
|
saltedHash: "$2a$10$KrtvADe7jwrmYIe3GXFbNupOQaPIvyOKeng5826g4VGOD47TpAisG",
|
||||||
|
testNum: 1,
|
||||||
|
},
|
||||||
|
"non-existing user": {
|
||||||
|
err: errors.New("Invalid email or password"),
|
||||||
|
email: "nouser@email.com",
|
||||||
|
password: "password",
|
||||||
|
saltedHash: "",
|
||||||
|
testNum: 2,
|
||||||
|
},
|
||||||
|
"wrong password": {
|
||||||
|
err: errors.New("Invalid email or password"),
|
||||||
|
email: "johndoe@email.com",
|
||||||
|
password: "bad",
|
||||||
|
saltedHash: "$2a$10$KrtvADe7jwrmYIe3GXFbNupOQaPIvyOKeng5826g4VGOD47TpAisG",
|
||||||
|
testNum: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
authService := NewAuthService(&TdUser{testNum: test.testNum}, new(util.StandardBcrypt))
|
||||||
|
|
||||||
|
_, err := authService.AuthenticateUser(test.email, test.password)
|
||||||
|
|
||||||
|
assert.Equal(t, err, test.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
core/mocks/Bcrypt.go
Normal file
60
core/mocks/Bcrypt.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
// Bcrypt is an autogenerated mock type for the Bcrypt type
|
||||||
|
type Bcrypt struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareHashAndPassword provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Bcrypt) CompareHashAndPassword(_a0 []byte, _a1 []byte) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateFromPassword provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Bcrypt) GenerateFromPassword(_a0 []byte, _a1 int) ([]byte, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []byte
|
||||||
|
if rf, ok := ret.Get(0).(func([]byte, int) []byte); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func([]byte, int) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultCost provides a mock function with given fields:
|
||||||
|
func (_m *Bcrypt) GetDefaultCost() int {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 int
|
||||||
|
if rf, ok := ret.Get(0).(func() int); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
970
core/mocks/Datastore.go
Normal file
970
core/mocks/Datastore.go
Normal file
@@ -0,0 +1,970 @@
|
|||||||
|
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import mock "github.com/stretchr/testify/mock"
|
||||||
|
import time "time"
|
||||||
|
import types "github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
|
||||||
|
// Datastore is an autogenerated mock type for the Datastore type
|
||||||
|
type Datastore struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptInvite provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AcceptInvite(_a0 *types.Invite, _a1 string) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Invite, string) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBalance provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddBalance(_a0 *types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBalances provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddBalances(_a0 []*types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNativeBalanceCost provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddNativeBalanceCost(_a0 *types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNativeBalanceNearestInTime provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddNativeBalanceNearestInTime(_a0 *types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNativeBalancesCost provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddNativeBalancesCost(_a0 []*types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNativeBalancesNearestInTime provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) AddNativeBalancesNearestInTime(_a0 []*types.Account, _a1 time.Time) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]*types.Account, time.Time) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrg provides a mock function with given fields: _a0, _a1, _a2
|
||||||
|
func (_m *Datastore) CreateOrg(_a0 *types.Org, _a1 string, _a2 []*types.Account) error {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Org, string, []*types.Account) error); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAccount provides a mock function with given fields: id
|
||||||
|
func (_m *Datastore) DeleteAccount(id string) error {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAndInsertTransaction provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) DeleteAndInsertTransaction(_a0 string, _a1 *types.Transaction) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, *types.Transaction) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteApiKey provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) DeleteApiKey(_a0 string, _a1 string) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteInvite provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) DeleteInvite(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePrice provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) DeletePrice(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) DeleteSession(_a0 string, _a1 string) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTransaction provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) DeleteTransaction(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) Escape(_a0 string) string {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccount provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetAccount(_a0 string) (*types.Account, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.Account
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.Account); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccountsByOrgId provides a mock function with given fields: orgId
|
||||||
|
func (_m *Datastore) GetAccountsByOrgId(orgId string) ([]*types.Account, error) {
|
||||||
|
ret := _m.Called(orgId)
|
||||||
|
|
||||||
|
var r0 []*types.Account
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []*types.Account); ok {
|
||||||
|
r0 = rf(orgId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(orgId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApiKeys provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetApiKeys(_a0 string) ([]*types.ApiKey, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []*types.ApiKey
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []*types.ApiKey); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.ApiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChildCountByAccountId provides a mock function with given fields: id
|
||||||
|
func (_m *Datastore) GetChildCountByAccountId(id string) (int64, error) {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 int64
|
||||||
|
if rf, ok := ret.Get(0).(func(string) int64); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInvite provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetInvite(_a0 string) (*types.Invite, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.Invite
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.Invite); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInvites provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetInvites(_a0 string) ([]*types.Invite, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []*types.Invite
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []*types.Invite); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrg provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) GetOrg(_a0 string, _a1 string) (*types.Org, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 *types.Org
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) *types.Org); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Org)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrgAdmins provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetOrgAdmins(_a0 string) ([]*types.User, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []*types.User
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []*types.User); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrgUserIds provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetOrgUserIds(_a0 string) ([]string, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []string
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrgs provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetOrgs(_a0 string) ([]*types.Org, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 []*types.Org
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []*types.Org); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Org)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermissionedAccountIds provides a mock function with given fields: _a0, _a1, _a2
|
||||||
|
func (_m *Datastore) GetPermissionedAccountIds(_a0 string, _a1 string, _a2 string) ([]string, error) {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
||||||
|
var r0 []string
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string, string) []string); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
|
||||||
|
r1 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPriceById provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetPriceById(_a0 string) (*types.Price, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.Price
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.Price); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPricesByCurrency provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) GetPricesByCurrency(_a0 string, _a1 string) ([]*types.Price, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []*types.Price
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) []*types.Price); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPricesNearestInTime provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) GetPricesNearestInTime(_a0 string, _a1 time.Time) ([]*types.Price, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []*types.Price
|
||||||
|
if rf, ok := ret.Get(0).(func(string, time.Time) []*types.Price); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, time.Time) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRootAccount provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetRootAccount(_a0 string) (*types.Account, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.Account
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.Account); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSplitCountByAccountId provides a mock function with given fields: id
|
||||||
|
func (_m *Datastore) GetSplitCountByAccountId(id string) (int64, error) {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 int64
|
||||||
|
if rf, ok := ret.Get(0).(func(string) int64); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionById provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetTransactionById(_a0 string) (*types.Transaction, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.Transaction
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.Transaction); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionsByAccount provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *Datastore) GetTransactionsByAccount(_a0 string, _a1 *types.QueryOptions) ([]*types.Transaction, error) {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 []*types.Transaction
|
||||||
|
if rf, ok := ret.Get(0).(func(string, *types.QueryOptions) []*types.Transaction); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, *types.QueryOptions) error); ok {
|
||||||
|
r1 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionsByOrg provides a mock function with given fields: _a0, _a1, _a2
|
||||||
|
func (_m *Datastore) GetTransactionsByOrg(_a0 string, _a1 *types.QueryOptions, _a2 []string) ([]*types.Transaction, error) {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
||||||
|
var r0 []*types.Transaction
|
||||||
|
if rf, ok := ret.Get(0).(func(string, *types.QueryOptions, []string) []*types.Transaction); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*types.Transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, *types.QueryOptions, []string) error); ok {
|
||||||
|
r1 = rf(_a0, _a1, _a2)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByActiveSession provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetUserByActiveSession(_a0 string) (*types.User, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.User
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.User); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByApiKey provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetUserByApiKey(_a0 string) (*types.User, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.User
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.User); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByResetCode provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetUserByResetCode(_a0 string) (*types.User, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.User
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.User); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerifiedUserByEmail provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) GetVerifiedUserByEmail(_a0 string) (*types.User, error) {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 *types.User
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *types.User); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertAccount provides a mock function with given fields: account
|
||||||
|
func (_m *Datastore) InsertAccount(account *types.Account) error {
|
||||||
|
ret := _m.Called(account)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Account) error); ok {
|
||||||
|
r0 = rf(account)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertApiKey provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertApiKey(_a0 *types.ApiKey) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.ApiKey) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertInvite provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertInvite(_a0 *types.Invite) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Invite) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPrice provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertPrice(_a0 *types.Price) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Price) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertSession provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertSession(_a0 *types.Session) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Session) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertTransaction provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertTransaction(_a0 *types.Transaction) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Transaction) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertUser provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) InsertUser(_a0 *types.User) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.User) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAccount provides a mock function with given fields: account
|
||||||
|
func (_m *Datastore) UpdateAccount(account *types.Account) error {
|
||||||
|
ret := _m.Called(account)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Account) error); ok {
|
||||||
|
r0 = rf(account)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateApiKey provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateApiKey(_a0 *types.ApiKey) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.ApiKey) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateApiKeyActivity provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateApiKeyActivity(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOrg provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateOrg(_a0 *types.Org) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.Org) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSessionActivity provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateSessionActivity(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateUser(_a0 *types.User) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.User) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserResetPassword provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) UpdateUserResetPassword(_a0 *types.User) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*types.User) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyUser provides a mock function with given fields: _a0
|
||||||
|
func (_m *Datastore) VerifyUser(_a0 string) error {
|
||||||
|
ret := _m.Called(_a0)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(_a0)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
377
core/model/account.go
Normal file
377
core/model/account.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/ws"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountInterface interface {
|
||||||
|
CreateAccount(account *types.Account, userId string) error
|
||||||
|
UpdateAccount(account *types.Account, userId string) error
|
||||||
|
DeleteAccount(id string, userId string, orgId string) error
|
||||||
|
GetAccounts(orgId string, userId string, tokenId string) ([]*types.Account, error)
|
||||||
|
GetAccountsWithBalances(orgId string, userId string, tokenId string, date time.Time) ([]*types.Account, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByName []*types.Account
|
||||||
|
|
||||||
|
func (a ByName) Len() int { return len(a) }
|
||||||
|
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||||
|
|
||||||
|
func (model *Model) CreateAccount(account *types.Account, userId string) (err error) {
|
||||||
|
if account.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.OrgId == "" {
|
||||||
|
return errors.New("orgId required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Name == "" {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Currency == "" {
|
||||||
|
return errors.New("currency required")
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccounts, err := model.GetAccounts(account.OrgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, account.Parent) {
|
||||||
|
return errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", account.Parent))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.InsertAccount(account)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access account
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(account.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushAccount(account, userIds, "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UpdateAccount(account *types.Account, userId string) (err error) {
|
||||||
|
if account.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.OrgId == "" {
|
||||||
|
return errors.New("orgId required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Name == "" {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Currency == "" {
|
||||||
|
return errors.New("currency required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Parent == account.Id {
|
||||||
|
return errors.New("account cannot be its own parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccounts, err := model.GetAccounts(account.OrgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, account.Parent) {
|
||||||
|
return errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", account.Parent))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.UpdateAccount(account)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.AddBalance(account, time.Now())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.AddNativeBalanceCost(account, time.Now())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access account
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(account.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushAccount(account, userIds, "update")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeleteAccount(id string, userId string, orgId string) (err error) {
|
||||||
|
// TODO make sure user is part of org
|
||||||
|
|
||||||
|
// check to make sure user has permission
|
||||||
|
userAccounts, err := model.GetAccounts(orgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, id) {
|
||||||
|
return errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't allow deleting of accounts that have transactions or child accounts
|
||||||
|
count, err := model.db.GetSplitCountByAccountId(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 0 {
|
||||||
|
return errors.New("Cannot delete an account that has transactions")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err = model.db.GetChildCountByAccountId(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 0 {
|
||||||
|
return errors.New("Cannot delete an account that has children")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := model.db.GetAccount(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.DeleteAccount(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access account
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(account.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushAccount(account, userIds, "delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getAccounts(orgId string, userId string, tokenId string, date time.Time, withBalances bool) ([]*types.Account, error) {
|
||||||
|
permissionedAccounts, err := model.db.GetPermissionedAccountIds(orgId, userId, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allAccounts []*types.Account
|
||||||
|
|
||||||
|
if withBalances == true {
|
||||||
|
allAccounts, err = model.getAllAccountsWithBalances(orgId, date)
|
||||||
|
} else {
|
||||||
|
allAccounts, err = model.getAllAccounts(orgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMap := model.makeAccountMap(allAccounts)
|
||||||
|
writeAccessMap := make(map[string]*types.Account)
|
||||||
|
readAccessMap := make(map[string]*types.Account)
|
||||||
|
|
||||||
|
for _, accountId := range permissionedAccounts {
|
||||||
|
writeAccessMap[accountId] = accountMap[accountId].Account
|
||||||
|
|
||||||
|
// parents are read only
|
||||||
|
parents := model.getParents(accountId, accountMap)
|
||||||
|
|
||||||
|
for _, parentAccount := range parents {
|
||||||
|
readAccessMap[parentAccount.Id] = parentAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
// top level accounts are initially read only unless user has permission
|
||||||
|
topLevelAccounts := model.getTopLevelAccounts(accountMap)
|
||||||
|
|
||||||
|
for _, topLevelAccount := range topLevelAccounts {
|
||||||
|
readAccessMap[topLevelAccount.Id] = topLevelAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children have write access
|
||||||
|
children := model.getChildren(accountId, accountMap)
|
||||||
|
|
||||||
|
for _, childAccount := range children {
|
||||||
|
writeAccessMap[childAccount.Id] = childAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]*types.Account, 0)
|
||||||
|
|
||||||
|
for _, account := range writeAccessMap {
|
||||||
|
filtered = append(filtered, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, account := range readAccessMap {
|
||||||
|
_, ok := writeAccessMap[id]
|
||||||
|
|
||||||
|
if ok == false {
|
||||||
|
account.ReadOnly = true
|
||||||
|
filtered = append(filtered, account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO sort by inserted
|
||||||
|
sort.Sort(ByName(filtered))
|
||||||
|
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetAccounts(orgId string, userId string, tokenId string) ([]*types.Account, error) {
|
||||||
|
return model.getAccounts(orgId, userId, tokenId, time.Time{}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetAccountsWithBalances(orgId string, userId string, tokenId string, date time.Time) ([]*types.Account, error) {
|
||||||
|
return model.getAccounts(orgId, userId, tokenId, date, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getAllAccounts(orgId string) ([]*types.Account, error) {
|
||||||
|
return model.db.GetAccountsByOrgId(orgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getAllAccountsWithBalances(orgId string, date time.Time) ([]*types.Account, error) {
|
||||||
|
accounts, err := model.db.GetAccountsByOrgId(orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.AddBalances(accounts, date)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.AddNativeBalancesCost(accounts, date)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) makeAccountMap(accounts []*types.Account) map[string]*types.AccountNode {
|
||||||
|
m := make(map[string]*types.AccountNode)
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
m[account.Id] = &types.AccountNode{
|
||||||
|
Account: account,
|
||||||
|
Parent: nil,
|
||||||
|
Children: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
m[account.Id].Parent = m[account.Parent]
|
||||||
|
|
||||||
|
if value, ok := m[account.Parent]; ok {
|
||||||
|
value.Children = append(value.Children, m[account.Id])
|
||||||
|
value.Account.HasChildren = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getChildren(parentId string, accountMap map[string]*types.AccountNode) []*types.Account {
|
||||||
|
if _, ok := accountMap[parentId]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
children := make([]*types.Account, 0)
|
||||||
|
|
||||||
|
for _, childAccountNode := range accountMap[parentId].Children {
|
||||||
|
children = append(children, childAccountNode.Account)
|
||||||
|
grandChildren := model.getChildren(childAccountNode.Account.Id, accountMap)
|
||||||
|
children = append(children, grandChildren...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getParents(accountId string, accountMap map[string]*types.AccountNode) []*types.Account {
|
||||||
|
node, ok := accountMap[accountId]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Parent == nil {
|
||||||
|
return make([]*types.Account, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
parents := model.getParents(node.Parent.Account.Id, accountMap)
|
||||||
|
return append(parents, node.Parent.Account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) accountsContainWriteAccess(accounts []*types.Account, accountId string) bool {
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.Id == accountId && !account.ReadOnly {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getAccountFromList(accounts []*types.Account, accountId string) *types.Account {
|
||||||
|
for _, account := range accounts {
|
||||||
|
if account.Id == accountId {
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getTopLevelAccounts(accountMap map[string]*types.AccountNode) []*types.Account {
|
||||||
|
accounts := make([]*types.Account, 0)
|
||||||
|
|
||||||
|
for _, node := range accountMap {
|
||||||
|
if node.Parent == nil {
|
||||||
|
accounts = append(accounts, node.Account)
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
accounts = append(accounts, child.Account)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
330
core/model/account_test.go
Normal file
330
core/model/account_test.go
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TdAccount struct {
|
||||||
|
db.Datastore
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetPermissionedAccountIds(userId string, orgId string, tokenId string) ([]string, error) {
|
||||||
|
// User has permission to only "Assets" account
|
||||||
|
return []string{"2"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetAccountsByOrgId(orgId string) ([]*types.Account, error) {
|
||||||
|
args := td.Called(orgId)
|
||||||
|
return args.Get(0).([]*types.Account), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) InsertAccount(account *types.Account) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) UpdateAccount(account *types.Account) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddBalance(account *types.Account, date time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddNativeBalanceNearestInTime(account *types.Account, date time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddNativeBalanceCost(account *types.Account, date time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddBalances(accounts []*types.Account, date time.Time) error {
|
||||||
|
balance := int64(1000)
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.Balance = &balance
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddNativeBalancesNearestInTime(accounts []*types.Account, date time.Time) error {
|
||||||
|
balance := int64(1000)
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.NativeBalance = &balance
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) AddNativeBalancesCost(accounts []*types.Account, date time.Time) error {
|
||||||
|
balance := int64(1000)
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.NativeBalance = &balance
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetSplitCountByAccountId(id string) (int64, error) {
|
||||||
|
args := td.Called(id)
|
||||||
|
return args.Get(0).(int64), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetChildCountByAccountId(id string) (int64, error) {
|
||||||
|
args := td.Called(id)
|
||||||
|
return args.Get(0).(int64), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) DeleteAccount(id string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetOrgUserIds(id string) ([]string, error) {
|
||||||
|
return []string{"1"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdAccount) GetAccount(id string) (*types.Account, error) {
|
||||||
|
return &types.Account{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestAccounts() []*types.Account {
|
||||||
|
return []*types.Account{
|
||||||
|
&types.Account{
|
||||||
|
Id: "2",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Assets",
|
||||||
|
Parent: "1",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
&types.Account{
|
||||||
|
Id: "3",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Current Assets",
|
||||||
|
Parent: "2",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
&types.Account{
|
||||||
|
Id: "1",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Root",
|
||||||
|
Parent: "",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
account *types.Account
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
account: &types.Account{
|
||||||
|
Id: "1",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Cash",
|
||||||
|
Parent: "3",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"permission error": {
|
||||||
|
err: errors.New("user does not have permission to access account 1"),
|
||||||
|
account: &types.Account{
|
||||||
|
Id: "1",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Cash",
|
||||||
|
Parent: "1",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdAccount{}
|
||||||
|
td.On("GetAccountsByOrgId", "1").Return(getTestAccounts(), nil)
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
err := model.CreateAccount(test.account, "1")
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateAccount(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
account *types.Account
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
account: &types.Account{
|
||||||
|
Id: "3",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Current Assets2",
|
||||||
|
Parent: "2",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
err: errors.New("account cannot be its own parent"),
|
||||||
|
account: &types.Account{
|
||||||
|
Id: "3",
|
||||||
|
OrgId: "1",
|
||||||
|
Name: "Current Assets",
|
||||||
|
Parent: "3",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
DebitBalance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdAccount{}
|
||||||
|
td.On("GetAccountsByOrgId", "1").Return(getTestAccounts(), nil)
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
err := model.UpdateAccount(test.account, "1")
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
td.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteAccount(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
accountId string
|
||||||
|
count int64
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
accountId: "3",
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
err: errors.New("Cannot delete an account that has transactions"),
|
||||||
|
accountId: "3",
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdAccount{}
|
||||||
|
td.On("GetAccountsByOrgId", "1").Return(getTestAccounts(), nil)
|
||||||
|
td.On("GetSplitCountByAccountId", test.accountId).Return(test.count, nil)
|
||||||
|
td.On("GetChildCountByAccountId", test.accountId).Return(test.count, nil)
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
err := model.DeleteAccount(test.accountId, "1", "1")
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
td.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAccounts(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
// "error": {
|
||||||
|
// err: errors.New("db error"),
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdAccount{}
|
||||||
|
td.On("GetAccountsByOrgId", "1").Return(getTestAccounts(), test.err)
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
accounts, err := model.GetAccounts("1", "1", "")
|
||||||
|
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
td.AssertExpectations(t)
|
||||||
|
assert.Equal(t, 3, len(accounts))
|
||||||
|
assert.Equal(t, false, accounts[0].ReadOnly)
|
||||||
|
assert.Equal(t, false, accounts[1].ReadOnly)
|
||||||
|
assert.Equal(t, true, accounts[2].ReadOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAccountsWithBalances(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
err: errors.New("db error"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdAccount{}
|
||||||
|
td.On("GetAccountsByOrgId", "1").Return(getTestAccounts(), test.err)
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
accounts, err := model.GetAccountsWithBalances("1", "1", "", time.Now())
|
||||||
|
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
td.AssertExpectations(t)
|
||||||
|
assert.Equal(t, 3, len(accounts))
|
||||||
|
assert.Equal(t, false, accounts[0].ReadOnly)
|
||||||
|
assert.Equal(t, false, accounts[1].ReadOnly)
|
||||||
|
assert.Equal(t, true, accounts[2].ReadOnly)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1000), *accounts[0].Balance)
|
||||||
|
assert.Equal(t, int64(1000), *accounts[1].Balance)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1000), *accounts[0].NativeBalance)
|
||||||
|
assert.Equal(t, int64(1000), *accounts[1].NativeBalance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
core/model/apikey.go
Normal file
37
core/model/apikey.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApiKeyInterface interface {
|
||||||
|
CreateApiKey(*types.ApiKey) error
|
||||||
|
UpdateApiKey(*types.ApiKey) error
|
||||||
|
DeleteApiKey(string, string) error
|
||||||
|
GetApiKeys(string) ([]*types.ApiKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateApiKey(key *types.ApiKey) error {
|
||||||
|
if key.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.InsertApiKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UpdateApiKey(key *types.ApiKey) error {
|
||||||
|
if key.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.UpdateApiKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeleteApiKey(id string, userId string) error {
|
||||||
|
return model.db.DeleteApiKey(id, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetApiKeys(userId string) ([]*types.ApiKey, error) {
|
||||||
|
return model.db.GetApiKeys(userId)
|
||||||
|
}
|
||||||
391
core/model/db/account.go
Normal file
391
core/model/db/account.go
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emptyAccountId = "00000000000000000000000000000000"
|
||||||
|
|
||||||
|
type AccountInterface interface {
|
||||||
|
InsertAccount(account *types.Account) error
|
||||||
|
UpdateAccount(account *types.Account) error
|
||||||
|
GetAccount(string) (*types.Account, error)
|
||||||
|
GetAccountsByOrgId(orgId string) ([]*types.Account, error)
|
||||||
|
GetPermissionedAccountIds(string, string, string) ([]string, error)
|
||||||
|
GetSplitCountByAccountId(id string) (int64, error)
|
||||||
|
GetChildCountByAccountId(id string) (int64, error)
|
||||||
|
DeleteAccount(id string) error
|
||||||
|
AddBalances([]*types.Account, time.Time) error
|
||||||
|
AddNativeBalancesCost([]*types.Account, time.Time) error
|
||||||
|
AddNativeBalancesNearestInTime([]*types.Account, time.Time) error
|
||||||
|
AddBalance(*types.Account, time.Time) error
|
||||||
|
AddNativeBalanceCost(*types.Account, time.Time) error
|
||||||
|
AddNativeBalanceNearestInTime(*types.Account, time.Time) error
|
||||||
|
GetRootAccount(string) (*types.Account, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertAccount(account *types.Account) error {
|
||||||
|
account.Inserted = time.Now()
|
||||||
|
account.Updated = account.Inserted
|
||||||
|
|
||||||
|
query := "INSERT INTO account(id,orgId,inserted,updated,name,parent,currency,`precision`,debitBalance) VALUES(UNHEX(?),UNHEX(?),?,?,?,UNHEX(?),?,?,?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
account.Id,
|
||||||
|
account.OrgId,
|
||||||
|
util.TimeToMs(account.Inserted),
|
||||||
|
util.TimeToMs(account.Updated),
|
||||||
|
account.Name,
|
||||||
|
account.Parent,
|
||||||
|
account.Currency,
|
||||||
|
account.Precision,
|
||||||
|
account.DebitBalance)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateAccount(account *types.Account) error {
|
||||||
|
account.Updated = time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE account SET updated = ?, name = ?, parent = UNHEX(?), currency = ?, `precision` = ?, debitBalance = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(account.Updated),
|
||||||
|
account.Name,
|
||||||
|
account.Parent,
|
||||||
|
account.Currency,
|
||||||
|
account.Precision,
|
||||||
|
account.DebitBalance,
|
||||||
|
account.Id)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetAccount(id string) (*types.Account, error) {
|
||||||
|
a := types.Account{}
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT LOWER(HEX(id)),LOWER(HEX(orgId)),inserted,updated,name,LOWER(HEX(parent)),currency,`precision`,debitBalance FROM account WHERE id = UNHEX(?)", id).
|
||||||
|
Scan(&a.Id, &a.OrgId, &inserted, &updated, &a.Name, &a.Parent, &a.Currency, &a.Precision, &a.DebitBalance)
|
||||||
|
|
||||||
|
if a.Parent == emptyAccountId {
|
||||||
|
a.Parent = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, errors.New("Account not found")
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
a.Inserted = util.MsToTime(inserted)
|
||||||
|
a.Updated = util.MsToTime(updated)
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetAccountsByOrgId(orgId string) ([]*types.Account, error) {
|
||||||
|
rows, err := db.Query("SELECT LOWER(HEX(id)),LOWER(HEX(orgId)),inserted,updated,name,LOWER(HEX(parent)),currency,`precision`,debitBalance FROM account WHERE orgId = UNHEX(?)", orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
accounts := make([]*types.Account, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
a := new(types.Account)
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err = rows.Scan(&a.Id, &a.OrgId, &inserted, &updated, &a.Name, &a.Parent, &a.Currency, &a.Precision, &a.DebitBalance)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Parent == emptyAccountId {
|
||||||
|
a.Parent = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Inserted = util.MsToTime(inserted)
|
||||||
|
a.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
accounts = append(accounts, a)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPermissionedAccountIds(orgId string, userId string, tokenId string) ([]string, error) {
|
||||||
|
// Get user permissions
|
||||||
|
// TODO incorporate tokens
|
||||||
|
rows, err := db.Query("SELECT LOWER(HEX(accountId)) FROM permission WHERE orgId = UNHEX(?) AND userId = UNHEX(?)", orgId, userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var permissionedAccounts []string
|
||||||
|
|
||||||
|
var id string
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
err := rows.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionedAccounts = append(permissionedAccounts, id)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissionedAccounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetSplitCountByAccountId(id string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
query := "SELECT COUNT(*) FROM split WHERE deleted = false AND accountId = UNHEX(?)"
|
||||||
|
|
||||||
|
err := db.QueryRow(query, id).Scan(&count)
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetChildCountByAccountId(id string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
query := "SELECT COUNT(*) FROM account WHERE parent = UNHEX(?)"
|
||||||
|
|
||||||
|
err := db.QueryRow(query, id).Scan(&count)
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteAccount(id string) error {
|
||||||
|
query := "DELETE FROM account WHERE id = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err := db.Exec(query, id)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddBalances(accounts []*types.Account, date time.Time) error {
|
||||||
|
// TODO optimize
|
||||||
|
ids := make([]string, len(accounts))
|
||||||
|
|
||||||
|
for i, account := range accounts {
|
||||||
|
ids[i] = "UNHEX(\"" + account.Id + "\")"
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceMap := make(map[string]*int64)
|
||||||
|
|
||||||
|
query := "SELECT LOWER(HEX(accountId)), SUM(amount) FROM split WHERE deleted = false AND accountId IN (" +
|
||||||
|
strings.Join(ids, ",") + ")" +
|
||||||
|
" AND date < ? GROUP BY accountId"
|
||||||
|
|
||||||
|
rows, err := db.Query(query, util.TimeToMs(date))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var balance int64
|
||||||
|
err := rows.Scan(&id, &balance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceMap[id] = &balance
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.Balance = balanceMap[account.Id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddNativeBalancesCost(accounts []*types.Account, date time.Time) error {
|
||||||
|
// TODO optimize
|
||||||
|
ids := make([]string, len(accounts))
|
||||||
|
|
||||||
|
for i, account := range accounts {
|
||||||
|
ids[i] = "UNHEX(\"" + account.Id + "\")"
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceMap := make(map[string]*int64)
|
||||||
|
|
||||||
|
query := "SELECT LOWER(HEX(accountId)), SUM(nativeAmount) FROM split WHERE deleted = false AND accountId IN (" +
|
||||||
|
strings.Join(ids, ",") + ")" +
|
||||||
|
" AND date < ? GROUP BY accountId"
|
||||||
|
|
||||||
|
rows, err := db.Query(query, util.TimeToMs(date))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var balance int64
|
||||||
|
err := rows.Scan(&id, &balance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
balanceMap[id] = &balance
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
account.NativeBalance = balanceMap[account.Id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddNativeBalancesNearestInTime(accounts []*types.Account, date time.Time) error {
|
||||||
|
// TODO Don't look up org currency every single time
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
err := db.AddNativeBalanceNearestInTime(account, date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddBalance(account *types.Account, date time.Time) error {
|
||||||
|
var balance sql.NullInt64
|
||||||
|
|
||||||
|
query := "SELECT SUM(amount) FROM split WHERE deleted = false AND accountId = UNHEX(?) AND date < ?"
|
||||||
|
|
||||||
|
err := db.QueryRow(query, account.Id, util.TimeToMs(date)).Scan(&balance)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Balance = &balance.Int64
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddNativeBalanceCost(account *types.Account, date time.Time) error {
|
||||||
|
var nativeBalance sql.NullInt64
|
||||||
|
|
||||||
|
query := "SELECT SUM(nativeAmount) FROM split WHERE deleted = false AND accountId = UNHEX(?) AND date < ?"
|
||||||
|
|
||||||
|
err := db.QueryRow(query, account.Id, util.TimeToMs(date)).Scan(&nativeBalance)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
account.NativeBalance = &nativeBalance.Int64
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AddNativeBalanceNearestInTime(account *types.Account, date time.Time) error {
|
||||||
|
var orgCurrency string
|
||||||
|
var orgPrecision int
|
||||||
|
|
||||||
|
query1 := "SELECT currency,`precision` FROM org WHERE id = UNHEX(?)"
|
||||||
|
|
||||||
|
err := db.QueryRow(query1, account.OrgId).Scan(&orgCurrency, &orgPrecision)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgCurrency == account.Currency {
|
||||||
|
nativeBalance := int64(*account.Balance)
|
||||||
|
account.NativeBalance = &nativeBalance
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp sql.NullInt64
|
||||||
|
var price float64
|
||||||
|
|
||||||
|
query2 := "SELECT ABS(CAST(date AS SIGNED) - ?) AS datediff, price FROM price WHERE currency = ? ORDER BY datediff ASC LIMIT 1"
|
||||||
|
|
||||||
|
err = db.QueryRow(query2, util.TimeToMs(date), account.Currency).Scan(&tmp, &price)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
nativeBalance := int64(0)
|
||||||
|
account.NativeBalance = &nativeBalance
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
precisionAdj := math.Pow(10, float64(account.Precision-orgPrecision))
|
||||||
|
nativeBalance := int64(float64(*account.Balance) * price / precisionAdj)
|
||||||
|
account.NativeBalance = &nativeBalance
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetRootAccount(orgId string) (*types.Account, error) {
|
||||||
|
a := types.Account{}
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := db.QueryRow(
|
||||||
|
"SELECT LOWER(HEX(id)),LOWER(HEX(orgId)),inserted,updated,name,LOWER(HEX(parent)),currency,`precision`,debitBalance FROM account WHERE orgId = UNHEX(?) AND parent = UNHEX(?)",
|
||||||
|
orgId,
|
||||||
|
emptyAccountId).
|
||||||
|
Scan(&a.Id, &a.OrgId, &inserted, &updated, &a.Name, &a.Parent, &a.Currency, &a.Precision, &a.DebitBalance)
|
||||||
|
|
||||||
|
a.Parent = ""
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, errors.New("Account not found")
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
a.Inserted = util.MsToTime(inserted)
|
||||||
|
a.Updated = util.MsToTime(updated)
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
132
core/model/db/apikey.go
Normal file
132
core/model/db/apikey.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApiKeyInterface interface {
|
||||||
|
InsertApiKey(*types.ApiKey) error
|
||||||
|
UpdateApiKey(*types.ApiKey) error
|
||||||
|
DeleteApiKey(string, string) error
|
||||||
|
GetApiKeys(string) ([]*types.ApiKey, error)
|
||||||
|
UpdateApiKeyActivity(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyFields = "LOWER(HEX(id)),inserted,updated,LOWER(HEX(userId)),label"
|
||||||
|
|
||||||
|
func (db *DB) InsertApiKey(key *types.ApiKey) error {
|
||||||
|
key.Inserted = time.Now()
|
||||||
|
key.Updated = key.Inserted
|
||||||
|
|
||||||
|
query := "INSERT INTO apikey(id,inserted,updated,userId,label) VALUES(UNHEX(?),?,?,UNHEX(?),?)"
|
||||||
|
res, err := db.Exec(
|
||||||
|
query,
|
||||||
|
key.Id,
|
||||||
|
util.TimeToMs(key.Inserted),
|
||||||
|
util.TimeToMs(key.Updated),
|
||||||
|
key.UserId,
|
||||||
|
key.Label,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCnt, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowCnt < 1 {
|
||||||
|
return errors.New("Unable to insert apikey into db")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateApiKey(key *types.ApiKey) error {
|
||||||
|
key.Updated = time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE apikey SET updated = ?, label = ? WHERE deleted IS NULL AND id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(key.Updated),
|
||||||
|
key.Label,
|
||||||
|
key.Id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var inserted int64
|
||||||
|
|
||||||
|
err = db.QueryRow("SELECT inserted FROM apikey WHERE id = UNHEX(?)", key.Id).Scan(&inserted)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key.Inserted = util.MsToTime(inserted)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteApiKey(id string, userId string) error {
|
||||||
|
query := "UPDATE apikey SET deleted = ? WHERE id = UNHEX(?) AND userId = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(time.Now()),
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetApiKeys(userId string) ([]*types.ApiKey, error) {
|
||||||
|
rows, err := db.Query("SELECT "+apiKeyFields+" from apikey WHERE deleted IS NULL AND userId = UNHEX(?)", userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
keys := make([]*types.ApiKey, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
k := new(types.ApiKey)
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err = rows.Scan(&k.Id, &inserted, &updated, &k.UserId, &k.Label)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
k.Inserted = util.MsToTime(inserted)
|
||||||
|
k.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateApiKeyActivity(id string) error {
|
||||||
|
query := "UPDATE apikey SET updated = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(time.Now()),
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
76
core/model/db/db.go
Normal file
76
core/model/db/db.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
*sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Datastore interface {
|
||||||
|
Escape(string) string
|
||||||
|
UserInterface
|
||||||
|
OrgInterface
|
||||||
|
AccountInterface
|
||||||
|
TransactionInterface
|
||||||
|
PriceInterface
|
||||||
|
SessionInterface
|
||||||
|
ApiKeyInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(dataSourceName string) (*DB, error) {
|
||||||
|
var err error
|
||||||
|
db, err := sql.Open("mysql", dataSourceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Escape(sql string) string {
|
||||||
|
dest := make([]byte, 0, 2*len(sql))
|
||||||
|
var escape byte
|
||||||
|
for i := 0; i < len(sql); i++ {
|
||||||
|
c := sql[i]
|
||||||
|
|
||||||
|
escape = 0
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case 0: /* Must be escaped for 'mysql' */
|
||||||
|
escape = '0'
|
||||||
|
break
|
||||||
|
case '\n': /* Must be escaped for logs */
|
||||||
|
escape = 'n'
|
||||||
|
break
|
||||||
|
case '\r':
|
||||||
|
escape = 'r'
|
||||||
|
break
|
||||||
|
case '\\':
|
||||||
|
escape = '\\'
|
||||||
|
break
|
||||||
|
case '\'':
|
||||||
|
escape = '\''
|
||||||
|
break
|
||||||
|
case '"': /* Better safe than sorry */
|
||||||
|
escape = '"'
|
||||||
|
break
|
||||||
|
case '\032': /* This gives problems on Win32 */
|
||||||
|
escape = 'Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
if escape != 0 {
|
||||||
|
dest = append(dest, '\\', escape)
|
||||||
|
} else {
|
||||||
|
dest = append(dest, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(dest)
|
||||||
|
}
|
||||||
370
core/model/db/org.go
Normal file
370
core/model/db/org.go
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrgInterface interface {
|
||||||
|
CreateOrg(*types.Org, string, []*types.Account) error
|
||||||
|
UpdateOrg(*types.Org) error
|
||||||
|
GetOrg(string, string) (*types.Org, error)
|
||||||
|
GetOrgs(string) ([]*types.Org, error)
|
||||||
|
GetOrgUserIds(string) ([]string, error)
|
||||||
|
InsertInvite(*types.Invite) error
|
||||||
|
AcceptInvite(*types.Invite, string) error
|
||||||
|
GetInvites(string) ([]*types.Invite, error)
|
||||||
|
GetInvite(string) (*types.Invite, error)
|
||||||
|
DeleteInvite(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgFields = "LOWER(HEX(o.id)),o.inserted,o.updated,o.name,o.currency,o.`precision`"
|
||||||
|
const inviteFields = "i.id,LOWER(HEX(i.orgId)),i.inserted,i.updated,i.email,i.accepted"
|
||||||
|
|
||||||
|
func (db *DB) CreateOrg(org *types.Org, userId string, accounts []*types.Account) (err error) {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
panic(p) // re-throw panic after Rollback
|
||||||
|
} else if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = tx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
org.Inserted = time.Now()
|
||||||
|
org.Updated = org.Inserted
|
||||||
|
|
||||||
|
// create org
|
||||||
|
query1 := "INSERT INTO org(id,inserted,updated,name,currency,`precision`) VALUES(UNHEX(?),?,?,?,?,?)"
|
||||||
|
|
||||||
|
res, err := tx.Exec(
|
||||||
|
query1,
|
||||||
|
org.Id,
|
||||||
|
util.TimeToMs(org.Inserted),
|
||||||
|
util.TimeToMs(org.Updated),
|
||||||
|
org.Name,
|
||||||
|
org.Currency,
|
||||||
|
org.Precision,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// associate user with org
|
||||||
|
query2 := "INSERT INTO userorg(userId,orgId,admin) VALUES(UNHEX(?),UNHEX(?), 1)"
|
||||||
|
|
||||||
|
res, err = tx.Exec(query2, userId, org.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = res.LastInsertId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create Accounts: Root, Assets, Liabilities, Equity, Income, Expenses
|
||||||
|
|
||||||
|
for _, account := range accounts {
|
||||||
|
|
||||||
|
query := "INSERT INTO account(id,orgId,inserted,updated,name,parent,currency,`precision`,debitBalance) VALUES (UNHEX(?),UNHEX(?),?,?,?,UNHEX(?),?,?,?)"
|
||||||
|
|
||||||
|
if _, err = tx.Exec(
|
||||||
|
query,
|
||||||
|
account.Id,
|
||||||
|
org.Id,
|
||||||
|
util.TimeToMs(org.Inserted),
|
||||||
|
util.TimeToMs(org.Updated),
|
||||||
|
account.Name,
|
||||||
|
account.Parent,
|
||||||
|
account.Currency,
|
||||||
|
account.Precision,
|
||||||
|
account.DebitBalance,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionId, err := util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant root permission to user
|
||||||
|
|
||||||
|
query3 := "INSERT INTO permission (id,userId,orgId,accountId,type,inserted,updated) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),UNHEX(?),?,?,?)"
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
query3,
|
||||||
|
permissionId,
|
||||||
|
userId,
|
||||||
|
org.Id,
|
||||||
|
accounts[0].Id,
|
||||||
|
0,
|
||||||
|
util.TimeToMs(org.Inserted),
|
||||||
|
util.TimeToMs(org.Updated),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateOrg(org *types.Org) error {
|
||||||
|
org.Updated = time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE org SET updated = ?, name = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(org.Updated),
|
||||||
|
org.Name,
|
||||||
|
org.Id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetOrg(orgId string, userId string) (*types.Org, error) {
|
||||||
|
var o types.Org
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT "+orgFields+" FROM org o JOIN userorg ON userorg.orgId = o.id WHERE o.id = UNHEX(?) AND userorg.userId = UNHEX(?)", orgId, userId).
|
||||||
|
Scan(&o.Id, &inserted, &updated, &o.Name, &o.Currency, &o.Precision)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, errors.New("Org not found")
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
o.Inserted = util.MsToTime(inserted)
|
||||||
|
o.Updated = util.MsToTime(updated)
|
||||||
|
return &o, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetOrgs(userId string) ([]*types.Org, error) {
|
||||||
|
rows, err := db.Query("SELECT "+orgFields+" from org o JOIN userorg ON userorg.orgId = o.id WHERE userorg.userId = UNHEX(?)", userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
orgs := make([]*types.Org, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
o := new(types.Org)
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err = rows.Scan(&o.Id, &inserted, &updated, &o.Name, &o.Currency, &o.Precision)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Inserted = util.MsToTime(inserted)
|
||||||
|
o.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
orgs = append(orgs, o)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetOrgUserIds(orgId string) ([]string, error) {
|
||||||
|
rows, err := db.Query("SELECT LOWER(HEX(userId)) FROM userorg WHERE orgId = UNHEX(?)", orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
userIds := make([]string, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var userId string
|
||||||
|
err = rows.Scan(&userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds = append(userIds, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertInvite(invite *types.Invite) error {
|
||||||
|
invite.Inserted = time.Now()
|
||||||
|
invite.Updated = invite.Inserted
|
||||||
|
|
||||||
|
query := "INSERT INTO invite(id,orgId,inserted,updated,email,accepted) VALUES(?,UNHEX(?),?,?,?,?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
invite.Id,
|
||||||
|
invite.OrgId,
|
||||||
|
util.TimeToMs(invite.Inserted),
|
||||||
|
util.TimeToMs(invite.Updated),
|
||||||
|
invite.Email,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) AcceptInvite(invite *types.Invite, userId string) error {
|
||||||
|
invite.Updated = time.Now()
|
||||||
|
|
||||||
|
// Get root account for permission
|
||||||
|
rootAccount, err := db.GetRootAccount(invite.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
panic(p) // re-throw panic after Rollback
|
||||||
|
} else if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = tx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// associate user with org
|
||||||
|
query1 := "INSERT INTO userorg(userId,orgId,admin) VALUES(UNHEX(?),UNHEX(?), 0)"
|
||||||
|
|
||||||
|
_, err = tx.Exec(query1, userId, invite.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query2 := "UPDATE invite SET accepted = 1, updated = ? WHERE id = ?"
|
||||||
|
|
||||||
|
_, err = tx.Exec(query2, util.TimeToMs(invite.Updated), invite.Id)
|
||||||
|
|
||||||
|
// Grant root permission to user
|
||||||
|
|
||||||
|
permissionId, err := util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query3 := "INSERT INTO permission (id,userId,orgId,accountId,type,inserted,updated) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),UNHEX(?),?,?,?)"
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
query3,
|
||||||
|
permissionId,
|
||||||
|
userId,
|
||||||
|
invite.OrgId,
|
||||||
|
rootAccount.Id,
|
||||||
|
0,
|
||||||
|
util.TimeToMs(invite.Updated),
|
||||||
|
util.TimeToMs(invite.Updated),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetInvites(orgId string) ([]*types.Invite, error) {
|
||||||
|
// don't include expired invoices
|
||||||
|
cutoff := util.TimeToMs(time.Now()) - 7*24*60*60*1000
|
||||||
|
|
||||||
|
rows, err := db.Query("SELECT "+inviteFields+" FROM invite i WHERE orgId = UNHEX(?) AND inserted > ?", orgId, cutoff)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
invites := make([]*types.Invite, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
i := new(types.Invite)
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err = rows.Scan(&i.Id, &i.OrgId, &inserted, &updated, &i.Email, &i.Accepted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Inserted = util.MsToTime(inserted)
|
||||||
|
i.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
invites = append(invites, i)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return invites, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetInvite(id string) (*types.Invite, error) {
|
||||||
|
var i types.Invite
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT "+inviteFields+" FROM invite i WHERE i.id = ?", id).
|
||||||
|
Scan(&i.Id, &i.OrgId, &inserted, &updated, &i.Email, &i.Accepted)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, errors.New("Invite not found")
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
i.Inserted = util.MsToTime(inserted)
|
||||||
|
i.Updated = util.MsToTime(updated)
|
||||||
|
return &i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteInvite(id string) error {
|
||||||
|
query := "DELETE FROM invite WHERE id = ?"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
156
core/model/db/price.go
Normal file
156
core/model/db/price.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PriceInterface interface {
|
||||||
|
InsertPrice(*types.Price) error
|
||||||
|
GetPriceById(string) (*types.Price, error)
|
||||||
|
DeletePrice(string) error
|
||||||
|
GetPricesNearestInTime(string, time.Time) ([]*types.Price, error)
|
||||||
|
GetPricesByCurrency(string, string) ([]*types.Price, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceFields = "LOWER(HEX(p.id)),LOWER(HEX(p.orgId)),p.currency,p.date,p.inserted,p.updated,p.price"
|
||||||
|
|
||||||
|
func (db *DB) InsertPrice(price *types.Price) error {
|
||||||
|
price.Inserted = time.Now()
|
||||||
|
price.Updated = price.Inserted
|
||||||
|
|
||||||
|
if price.Date.IsZero() {
|
||||||
|
price.Date = price.Inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "INSERT INTO price(id,orgId,currency,date,inserted,updated,price) VALUES(UNHEX(?),UNHEX(?),?,?,?,?,?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
price.Id,
|
||||||
|
price.OrgId,
|
||||||
|
price.Currency,
|
||||||
|
util.TimeToMs(price.Date),
|
||||||
|
util.TimeToMs(price.Inserted),
|
||||||
|
util.TimeToMs(price.Updated),
|
||||||
|
price.Price,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPriceById(id string) (*types.Price, error) {
|
||||||
|
var p types.Price
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT "+priceFields+" FROM price p WHERE id = UNHEX(?)", id).
|
||||||
|
Scan(&p.Id, &p.OrgId, &p.Currency, &date, &inserted, &updated, &p.Price)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, errors.New("Price not found")
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
p.Date = util.MsToTime(date)
|
||||||
|
p.Inserted = util.MsToTime(inserted)
|
||||||
|
p.Updated = util.MsToTime(updated)
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeletePrice(id string) error {
|
||||||
|
query := "DELETE FROM price WHERE id = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err := db.Exec(query, id)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPricesNearestInTime(orgId string, date time.Time) ([]*types.Price, error) {
|
||||||
|
qSelect := "SELECT " + priceFields
|
||||||
|
qFrom := " FROM price p"
|
||||||
|
qJoin := " LEFT OUTER JOIN price p2 ON p.currency = p2.currency AND p.orgId = p2.orgId AND ABS(CAST(p.date AS SIGNED) - ?) > ABS(CAST(p2.date AS SIGNED) - ?)"
|
||||||
|
qWhere := " WHERE p2.id IS NULL AND p.orgId = UNHEX(?)"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qJoin + qWhere
|
||||||
|
|
||||||
|
rows, err := db.Query(query, util.TimeToMs(date), util.TimeToMs(date), orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
prices := make([]*types.Price, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
p := new(types.Price)
|
||||||
|
err = rows.Scan(&p.Id, &p.OrgId, &p.Currency, &date, &inserted, &updated, &p.Price)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Date = util.MsToTime(date)
|
||||||
|
p.Inserted = util.MsToTime(inserted)
|
||||||
|
p.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
prices = append(prices, p)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPricesByCurrency(orgId string, currency string) ([]*types.Price, error) {
|
||||||
|
qSelect := "SELECT " + priceFields
|
||||||
|
qFrom := " FROM price p"
|
||||||
|
qWhere := " WHERE p.orgId = UNHEX(?) AND p.currency = ?"
|
||||||
|
pOrder := " ORDER BY date ASC"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qWhere + pOrder
|
||||||
|
|
||||||
|
rows, err := db.Query(query, orgId, currency)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
prices := make([]*types.Price, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
p := new(types.Price)
|
||||||
|
err = rows.Scan(&p.Id, &p.OrgId, &p.Currency, &date, &inserted, &updated, &p.Price)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Date = util.MsToTime(date)
|
||||||
|
p.Inserted = util.MsToTime(inserted)
|
||||||
|
p.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
prices = append(prices, p)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prices, nil
|
||||||
|
}
|
||||||
65
core/model/db/session.go
Normal file
65
core/model/db/session.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionInterface interface {
|
||||||
|
InsertSession(*types.Session) error
|
||||||
|
DeleteSession(string, string) error
|
||||||
|
UpdateSessionActivity(string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertSession(session *types.Session) error {
|
||||||
|
session.Inserted = time.Now()
|
||||||
|
session.Updated = session.Inserted
|
||||||
|
|
||||||
|
query := "INSERT INTO session(id,inserted,updated,userId) VALUES(UNHEX(?),?,?,UNHEX(?))"
|
||||||
|
res, err := db.Exec(
|
||||||
|
query,
|
||||||
|
session.Id,
|
||||||
|
util.TimeToMs(session.Inserted),
|
||||||
|
util.TimeToMs(session.Updated),
|
||||||
|
session.UserId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCnt, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowCnt < 1 {
|
||||||
|
return errors.New("Unable to insert session into db")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteSession(id string, userId string) error {
|
||||||
|
query := "UPDATE session SET `terminated` = ? WHERE id = UNHEX(?) AND userId = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(time.Now()),
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateSessionActivity(id string) error {
|
||||||
|
query := "UPDATE session SET updated = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(time.Now()),
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
558
core/model/db/transaction.go
Normal file
558
core/model/db/transaction.go
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const txFields = "LOWER(HEX(id)),LOWER(HEX(orgId)),LOWER(HEX(userId)),date,inserted,updated,description,data,deleted"
|
||||||
|
const splitFields = "id,LOWER(HEX(transactionId)),LOWER(HEX(accountId)),date,inserted,updated,amount,nativeAmount,deleted"
|
||||||
|
|
||||||
|
type TransactionInterface interface {
|
||||||
|
InsertTransaction(*types.Transaction) error
|
||||||
|
GetTransactionById(string) (*types.Transaction, error)
|
||||||
|
GetTransactionsByAccount(string, *types.QueryOptions) ([]*types.Transaction, error)
|
||||||
|
GetTransactionsByOrg(string, *types.QueryOptions, []string) ([]*types.Transaction, error)
|
||||||
|
DeleteTransaction(string) error
|
||||||
|
DeleteAndInsertTransaction(string, *types.Transaction) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertTransaction(transaction *types.Transaction) (err error) {
|
||||||
|
// Save to db
|
||||||
|
dbTx, err := db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
panic(p) // re-throw panic after Rollback
|
||||||
|
} else if err != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = dbTx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// save tx
|
||||||
|
query1 := "INSERT INTO transaction(id,orgId,userId,date,inserted,updated,description,data) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),?,?,?,?,?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query1,
|
||||||
|
transaction.Id,
|
||||||
|
transaction.OrgId,
|
||||||
|
transaction.UserId,
|
||||||
|
util.TimeToMs(transaction.Date),
|
||||||
|
util.TimeToMs(transaction.Inserted),
|
||||||
|
util.TimeToMs(transaction.Updated),
|
||||||
|
transaction.Description,
|
||||||
|
transaction.Data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save splits
|
||||||
|
for _, split := range transaction.Splits {
|
||||||
|
query := "INSERT INTO split(transactionId,accountId,date,inserted,updated,amount,nativeAmount) VALUES (UNHEX(?),UNHEX(?),?,?,?,?,?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query,
|
||||||
|
transaction.Id,
|
||||||
|
split.AccountId,
|
||||||
|
util.TimeToMs(transaction.Date),
|
||||||
|
util.TimeToMs(transaction.Inserted),
|
||||||
|
util.TimeToMs(transaction.Updated),
|
||||||
|
split.Amount,
|
||||||
|
split.NativeAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetTransactionById(id string) (*types.Transaction, error) {
|
||||||
|
row := db.QueryRow("SELECT "+txFields+" FROM transaction WHERE id = UNHEX(?)", id)
|
||||||
|
|
||||||
|
t, err := db.unmarshalTransaction(row)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query("SELECT "+splitFields+" FROM split WHERE transactionId = UNHEX(?) ORDER BY id", t.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Splits, err = db.unmarshalSplits(rows)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetTransactionsByAccount(accountId string, options *types.QueryOptions) ([]*types.Transaction, error) {
|
||||||
|
query := "SELECT LOWER(HEX(s.transactionId)) FROM split s"
|
||||||
|
|
||||||
|
if options.DescriptionStartsWith != "" {
|
||||||
|
query = query + " JOIN transaction t ON t.id = s.transactionId"
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query + " WHERE s.accountId = UNHEX(?)"
|
||||||
|
|
||||||
|
query = db.addOptionsToQuery(query, options)
|
||||||
|
|
||||||
|
rows, err := db.Query(query, accountId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
err = rows.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, "UNHEX(\""+id+"\")")
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return make([]*types.Transaction, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query = "SELECT " + txFields + " FROM transaction WHERE id IN (" + strings.Join(ids, ",") + ")"
|
||||||
|
|
||||||
|
query = db.addSortToQuery(query, options)
|
||||||
|
|
||||||
|
rows, err = db.Query(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions, err := db.unmarshalTransactions(rows)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionMap := make(map[string]*types.Transaction)
|
||||||
|
|
||||||
|
for _, t := range transactions {
|
||||||
|
transactionMap[t.Id] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err = db.Query("SELECT " + splitFields + " FROM split WHERE transactionId IN (" + strings.Join(ids, ",") + ") ORDER BY id")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
splits, err := db.unmarshalSplits(rows)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range splits {
|
||||||
|
transaction := transactionMap[s.TransactionId]
|
||||||
|
transaction.Splits = append(transaction.Splits, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetTransactionsByOrg(orgId string, options *types.QueryOptions, accountIds []string) ([]*types.Transaction, error) {
|
||||||
|
if len(accountIds) == 0 {
|
||||||
|
return make([]*types.Transaction, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, accountId := range accountIds {
|
||||||
|
accountIds[i] = "UNHEX(\"" + accountId + "\")"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "SELECT DISTINCT LOWER(HEX(s.transactionId)),s.date,s.inserted,s.updated FROM split s"
|
||||||
|
|
||||||
|
if options.DescriptionStartsWith != "" {
|
||||||
|
query = query + " JOIN transaction t ON t.id = s.transactionId"
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query + " WHERE s.accountId IN (" + strings.Join(accountIds, ",") + ")"
|
||||||
|
|
||||||
|
query = db.addOptionsToQuery(query, options)
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
ids := []string{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
err = rows.Scan(&id, &date, &inserted, &updated)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, "UNHEX(\""+id+"\")")
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return make([]*types.Transaction, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query = "SELECT " + txFields + " FROM transaction WHERE id IN (" + strings.Join(ids, ",") + ")"
|
||||||
|
|
||||||
|
query = db.addSortToQuery(query, options)
|
||||||
|
|
||||||
|
rows, err = db.Query(query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions, err := db.unmarshalTransactions(rows)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionMap := make(map[string]*types.Transaction)
|
||||||
|
|
||||||
|
for _, t := range transactions {
|
||||||
|
transactionMap[t.Id] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err = db.Query("SELECT " + splitFields + " FROM split WHERE transactionId IN (" + strings.Join(ids, ",") + ") ORDER BY id")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
splits, err := db.unmarshalSplits(rows)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range splits {
|
||||||
|
transaction := transactionMap[s.TransactionId]
|
||||||
|
transaction.Splits = append(transaction.Splits, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteTransaction(id string) (err error) {
|
||||||
|
dbTx, err := db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
panic(p) // re-throw panic after Rollback
|
||||||
|
} else if err != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = dbTx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
updatedTime := util.TimeToMs(time.Now())
|
||||||
|
|
||||||
|
// mark splits as deleted
|
||||||
|
|
||||||
|
query1 := "UPDATE split SET updated = ?, deleted = true WHERE transactionId = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query1,
|
||||||
|
updatedTime,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark transaction as deleted
|
||||||
|
|
||||||
|
query2 := "UPDATE transaction SET updated = ?, deleted = true WHERE id = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query2,
|
||||||
|
updatedTime,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteAndInsertTransaction(oldId string, transaction *types.Transaction) (err error) {
|
||||||
|
// Save to db
|
||||||
|
dbTx, err := db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
panic(p) // re-throw panic after Rollback
|
||||||
|
} else if err != nil {
|
||||||
|
dbTx.Rollback()
|
||||||
|
} else {
|
||||||
|
err = dbTx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
updatedTime := util.TimeToMs(transaction.Updated)
|
||||||
|
|
||||||
|
// mark splits as deleted
|
||||||
|
|
||||||
|
query1 := "UPDATE split SET updated = ?, deleted = true WHERE transactionId = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query1,
|
||||||
|
updatedTime,
|
||||||
|
oldId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark transaction as deleted
|
||||||
|
|
||||||
|
query2 := "UPDATE transaction SET updated = ?, deleted = true WHERE id = UNHEX(?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query2,
|
||||||
|
updatedTime,
|
||||||
|
oldId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save new tx
|
||||||
|
query3 := "INSERT INTO transaction(id,orgId,userId,date,inserted,updated,description,data) VALUES(UNHEX(?),UNHEX(?),UNHEX(?),?,?,?,?,?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query3,
|
||||||
|
transaction.Id,
|
||||||
|
transaction.OrgId,
|
||||||
|
transaction.UserId,
|
||||||
|
util.TimeToMs(transaction.Date),
|
||||||
|
util.TimeToMs(transaction.Inserted),
|
||||||
|
updatedTime,
|
||||||
|
transaction.Description,
|
||||||
|
transaction.Data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save splits
|
||||||
|
for _, split := range transaction.Splits {
|
||||||
|
query := "INSERT INTO split(transactionId,accountId,date,inserted,updated,amount,nativeAmount) VALUES (UNHEX(?),UNHEX(?),?,?,?,?,?)"
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(
|
||||||
|
query,
|
||||||
|
transaction.Id,
|
||||||
|
split.AccountId,
|
||||||
|
util.TimeToMs(transaction.Date),
|
||||||
|
util.TimeToMs(transaction.Inserted),
|
||||||
|
updatedTime,
|
||||||
|
split.Amount,
|
||||||
|
split.NativeAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalTransaction(row *sql.Row) (*types.Transaction, error) {
|
||||||
|
t := new(types.Transaction)
|
||||||
|
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := row.Scan(&t.Id, &t.OrgId, &t.UserId, &date, &inserted, &updated, &t.Description, &t.Data, &t.Deleted)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Date = util.MsToTime(date)
|
||||||
|
t.Inserted = util.MsToTime(inserted)
|
||||||
|
t.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalTransactions(rows *sql.Rows) ([]*types.Transaction, error) {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
transactions := make([]*types.Transaction, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
t := new(types.Transaction)
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
err := rows.Scan(&t.Id, &t.OrgId, &t.UserId, &date, &inserted, &updated, &t.Description, &t.Data, &t.Deleted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Date = util.MsToTime(date)
|
||||||
|
t.Inserted = util.MsToTime(inserted)
|
||||||
|
t.Updated = util.MsToTime(updated)
|
||||||
|
transactions = append(transactions, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Err()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalSplits(rows *sql.Rows) ([]*types.Split, error) {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
splits := make([]*types.Split, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
s := new(types.Split)
|
||||||
|
var id int64
|
||||||
|
var date int64
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
var deleted bool
|
||||||
|
err := rows.Scan(&id, &s.TransactionId, &s.AccountId, &date, &inserted, &updated, &s.Amount, &s.NativeAmount, &deleted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
splits = append(splits, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Err()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return splits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) addOptionsToQuery(query string, options *types.QueryOptions) string {
|
||||||
|
if options.IncludeDeleted != true {
|
||||||
|
query += " AND s.deleted = false"
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.SinceInserted != 0 {
|
||||||
|
query += " AND s.inserted > " + strconv.Itoa(options.SinceInserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.SinceUpdated != 0 {
|
||||||
|
query += " AND s.updated > " + strconv.Itoa(options.SinceUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.BeforeInserted != 0 {
|
||||||
|
query += " AND s.inserted < " + strconv.Itoa(options.BeforeInserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.BeforeUpdated != 0 {
|
||||||
|
query += " AND s.updated < " + strconv.Itoa(options.BeforeUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.StartDate != 0 {
|
||||||
|
query += " AND s.date >= " + strconv.Itoa(options.StartDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.EndDate != 0 {
|
||||||
|
query += " AND s.date < " + strconv.Itoa(options.EndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.DescriptionStartsWith != "" {
|
||||||
|
query += " AND t.description LIKE '" + db.Escape(options.DescriptionStartsWith) + "%'"
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Sort == "updated-asc" {
|
||||||
|
query += " ORDER BY s.updated ASC"
|
||||||
|
} else {
|
||||||
|
query += " ORDER BY s.date DESC, s.inserted DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Limit != 0 && options.Skip != 0 {
|
||||||
|
query += " LIMIT " + strconv.Itoa(options.Skip) + ", " + strconv.Itoa(options.Limit)
|
||||||
|
} else if options.Limit != 0 {
|
||||||
|
query += " LIMIT " + strconv.Itoa(options.Limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) addSortToQuery(query string, options *types.QueryOptions) string {
|
||||||
|
if options.Sort == "updated-asc" {
|
||||||
|
query += " ORDER BY updated ASC"
|
||||||
|
} else {
|
||||||
|
query += " ORDER BY date DESC, inserted DESC"
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
264
core/model/db/user.go
Normal file
264
core/model/db/user.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userFields = "LOWER(HEX(u.id)),u.inserted,u.updated,u.firstName,u.lastName,u.email,u.passwordHash,u.agreeToTerms,u.passwordReset,u.emailVerified,u.emailVerifyCode"
|
||||||
|
|
||||||
|
type UserInterface interface {
|
||||||
|
InsertUser(*types.User) error
|
||||||
|
VerifyUser(string) error
|
||||||
|
UpdateUser(*types.User) error
|
||||||
|
UpdateUserResetPassword(*types.User) error
|
||||||
|
GetVerifiedUserByEmail(string) (*types.User, error)
|
||||||
|
GetUserByActiveSession(string) (*types.User, error)
|
||||||
|
GetUserByApiKey(string) (*types.User, error)
|
||||||
|
GetUserByResetCode(string) (*types.User, error)
|
||||||
|
GetOrgAdmins(string) ([]*types.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) InsertUser(user *types.User) error {
|
||||||
|
user.Inserted = time.Now()
|
||||||
|
user.Updated = user.Inserted
|
||||||
|
user.PasswordReset = ""
|
||||||
|
|
||||||
|
query := "INSERT INTO user(id,inserted,updated,firstName,lastName,email,passwordHash,agreeToTerms,passwordReset,emailVerified,emailVerifyCode) VALUES(UNHEX(?),?,?,?,?,?,?,?,?,?,?)"
|
||||||
|
res, err := db.Exec(
|
||||||
|
query,
|
||||||
|
user.Id,
|
||||||
|
util.TimeToMs(user.Inserted),
|
||||||
|
util.TimeToMs(user.Updated),
|
||||||
|
user.FirstName,
|
||||||
|
user.LastName,
|
||||||
|
user.Email,
|
||||||
|
user.PasswordHash,
|
||||||
|
user.AgreeToTerms,
|
||||||
|
user.PasswordReset,
|
||||||
|
user.EmailVerified,
|
||||||
|
user.EmailVerifyCode,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCnt, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowCnt < 1 {
|
||||||
|
return errors.New("Unable to insert user into db")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) VerifyUser(code string) error {
|
||||||
|
query := "UPDATE user SET updated = ?, emailVerified = 1 WHERE emailVerifyCode = ?"
|
||||||
|
res, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(time.Now()),
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
|
||||||
|
count, err := res.RowsAffected()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return errors.New("Invalid code")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateUser(user *types.User) error {
|
||||||
|
user.Updated = time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE user SET updated = ?, passwordHash = ?, passwordReset = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(user.Updated),
|
||||||
|
user.PasswordHash,
|
||||||
|
"",
|
||||||
|
user.Id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateUserResetPassword(user *types.User) error {
|
||||||
|
user.Updated = time.Now()
|
||||||
|
|
||||||
|
query := "UPDATE user SET updated = ?, passwordReset = ? WHERE id = UNHEX(?)"
|
||||||
|
_, err := db.Exec(
|
||||||
|
query,
|
||||||
|
util.TimeToMs(user.Updated),
|
||||||
|
user.PasswordReset,
|
||||||
|
user.Id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetVerifiedUserByEmail(email string) (*types.User, error) {
|
||||||
|
query := "SELECT " + userFields + " FROM user u WHERE email = ? AND emailVerified = 1"
|
||||||
|
|
||||||
|
row := db.QueryRow(query, email)
|
||||||
|
u, err := db.unmarshalUser(row)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUserByActiveSession(sessionId string) (*types.User, error) {
|
||||||
|
qSelect := "SELECT " + userFields
|
||||||
|
qFrom := " FROM user u"
|
||||||
|
qJoin := " JOIN session s ON s.userId = u.id"
|
||||||
|
qWhere := " WHERE s.terminated IS NULL AND s.id = UNHEX(?)"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qJoin + qWhere
|
||||||
|
|
||||||
|
row := db.QueryRow(query, sessionId)
|
||||||
|
u, err := db.unmarshalUser(row)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUserByApiKey(keyId string) (*types.User, error) {
|
||||||
|
qSelect := "SELECT " + userFields
|
||||||
|
qFrom := " FROM user u"
|
||||||
|
qJoin := " JOIN apikey a ON a.userId = u.id"
|
||||||
|
qWhere := " WHERE a.deleted IS NULL AND a.id = UNHEX(?)"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qJoin + qWhere
|
||||||
|
|
||||||
|
row := db.QueryRow(query, keyId)
|
||||||
|
u, err := db.unmarshalUser(row)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUserByResetCode(code string) (*types.User, error) {
|
||||||
|
qSelect := "SELECT " + userFields
|
||||||
|
qFrom := " FROM user u"
|
||||||
|
qWhere := " WHERE u.passwordReset = ?"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qWhere
|
||||||
|
|
||||||
|
row := db.QueryRow(query, code)
|
||||||
|
u, err := db.unmarshalUser(row)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(u)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetOrgAdmins(orgId string) ([]*types.User, error) {
|
||||||
|
qSelect := "SELECT " + userFields
|
||||||
|
qFrom := " FROM user u"
|
||||||
|
qJoin := " JOIN userorg uo ON uo.userId = u.id"
|
||||||
|
qWhere := " WHERE uo.admin = true AND uo.orgId = UNHEX(?)"
|
||||||
|
|
||||||
|
query := qSelect + qFrom + qJoin + qWhere
|
||||||
|
|
||||||
|
rows, err := db.Query(query, orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.unmarshalUsers(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalUser(row *sql.Row) (*types.User, error) {
|
||||||
|
u := new(types.User)
|
||||||
|
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
&u.Id,
|
||||||
|
&inserted,
|
||||||
|
&updated,
|
||||||
|
&u.FirstName,
|
||||||
|
&u.LastName,
|
||||||
|
&u.Email,
|
||||||
|
&u.PasswordHash,
|
||||||
|
&u.AgreeToTerms,
|
||||||
|
&u.PasswordReset,
|
||||||
|
&u.EmailVerified,
|
||||||
|
&u.EmailVerifyCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Inserted = util.MsToTime(inserted)
|
||||||
|
u.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) unmarshalUsers(rows *sql.Rows) ([]*types.User, error) {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
users := make([]*types.User, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
u := new(types.User)
|
||||||
|
var inserted int64
|
||||||
|
var updated int64
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&u.Id,
|
||||||
|
&inserted,
|
||||||
|
&updated,
|
||||||
|
&u.FirstName,
|
||||||
|
&u.LastName,
|
||||||
|
&u.Email,
|
||||||
|
&u.PasswordHash,
|
||||||
|
&u.AgreeToTerms,
|
||||||
|
&u.PasswordReset,
|
||||||
|
&u.EmailVerified,
|
||||||
|
&u.EmailVerifyCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Inserted = util.MsToTime(inserted)
|
||||||
|
u.Updated = util.MsToTime(updated)
|
||||||
|
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Err()
|
||||||
|
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
31
core/model/model.go
Normal file
31
core/model/model.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Instance Interface
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
db db.Datastore
|
||||||
|
bcrypt util.Bcrypt
|
||||||
|
config types.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
UserInterface
|
||||||
|
OrgInterface
|
||||||
|
AccountInterface
|
||||||
|
TransactionInterface
|
||||||
|
PriceInterface
|
||||||
|
SessionInterface
|
||||||
|
ApiKeyInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(db db.Datastore, bcrypt util.Bcrypt, config types.Config) *Model {
|
||||||
|
model := &Model{db: db, bcrypt: bcrypt, config: config}
|
||||||
|
Instance = model
|
||||||
|
return model
|
||||||
|
}
|
||||||
294
core/model/org.go
Normal file
294
core/model/org.go
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrgInterface interface {
|
||||||
|
CreateOrg(*types.Org, string) error
|
||||||
|
UpdateOrg(*types.Org, string) error
|
||||||
|
GetOrg(string, string) (*types.Org, error)
|
||||||
|
GetOrgs(string) ([]*types.Org, error)
|
||||||
|
CreateInvite(*types.Invite, string) error
|
||||||
|
AcceptInvite(*types.Invite, string) error
|
||||||
|
GetInvites(string, string) ([]*types.Invite, error)
|
||||||
|
DeleteInvite(string, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateOrg(org *types.Org, userId string) error {
|
||||||
|
if org.Name == "" {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if org.Currency == "" {
|
||||||
|
return errors.New("currency required")
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts := make([]*types.Account, 6)
|
||||||
|
|
||||||
|
id, err := util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[0] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Root",
|
||||||
|
Parent: "",
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[1] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Assets",
|
||||||
|
Parent: accounts[0].Id,
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[2] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Liabilities",
|
||||||
|
Parent: accounts[0].Id,
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[3] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Equity",
|
||||||
|
Parent: accounts[0].Id,
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[4] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Income",
|
||||||
|
Parent: accounts[0].Id,
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts[5] = &types.Account{
|
||||||
|
Id: id,
|
||||||
|
Name: "Expenses",
|
||||||
|
Parent: accounts[0].Id,
|
||||||
|
Currency: org.Currency,
|
||||||
|
Precision: org.Precision,
|
||||||
|
DebitBalance: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.CreateOrg(org, userId, accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UpdateOrg(org *types.Org, userId string) error {
|
||||||
|
_, err := model.GetOrg(org.Id, userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// user doesn't have access to org
|
||||||
|
return errors.New("access denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
if org.Name == "" {
|
||||||
|
return errors.New("name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.UpdateOrg(org)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetOrg(orgId string, userId string) (*types.Org, error) {
|
||||||
|
return model.db.GetOrg(orgId, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetOrgs(userId string) ([]*types.Org, error) {
|
||||||
|
return model.db.GetOrgs(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UserBelongsToOrg(userId string, orgId string) (bool, error) {
|
||||||
|
orgs, err := model.GetOrgs(userId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
belongs := false
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
if org.Id == orgId {
|
||||||
|
belongs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return belongs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateInvite(invite *types.Invite, userId string) error {
|
||||||
|
admins, err := model.db.GetOrgAdmins(invite.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
|
||||||
|
for _, admin := range admins {
|
||||||
|
if admin.Id == userId {
|
||||||
|
isAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin == false {
|
||||||
|
return errors.New("Must be org admin to invite users")
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteId, err := util.NewInviteId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
invite.Id = inviteId
|
||||||
|
|
||||||
|
err = model.db.InsertInvite(invite)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if invite.Email != "" {
|
||||||
|
// TODO send email
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) AcceptInvite(invite *types.Invite, userId string) error {
|
||||||
|
if invite.Accepted != true {
|
||||||
|
return errors.New("accepted must be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if invite.Id == "" {
|
||||||
|
return errors.New("missing invite id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original invite
|
||||||
|
original, err := model.db.GetInvite(invite.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if original.Accepted == true {
|
||||||
|
return errors.New("invite already accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
oneWeekAfter := original.Inserted.Add(time.Hour * 24 * 7)
|
||||||
|
|
||||||
|
if time.Now().After(oneWeekAfter) == true {
|
||||||
|
return errors.New("invite has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
invite.OrgId = original.OrgId
|
||||||
|
invite.Email = original.Email
|
||||||
|
invite.Inserted = original.Inserted
|
||||||
|
|
||||||
|
return model.db.AcceptInvite(invite, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetInvites(orgId string, userId string) ([]*types.Invite, error) {
|
||||||
|
admins, err := model.db.GetOrgAdmins(orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
|
||||||
|
for _, admin := range admins {
|
||||||
|
if admin.Id == userId {
|
||||||
|
isAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin == false {
|
||||||
|
return nil, errors.New("Must be org admin to invite users")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.GetInvites(orgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeleteInvite(id string, userId string) error {
|
||||||
|
// Get original invite
|
||||||
|
invite, err := model.db.GetInvite(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure user has access
|
||||||
|
|
||||||
|
admins, err := model.db.GetOrgAdmins(invite.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
|
||||||
|
for _, admin := range admins {
|
||||||
|
if admin.Id == userId {
|
||||||
|
isAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin == false {
|
||||||
|
return errors.New("Must be org admin to delete invite")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.DeleteInvite(id)
|
||||||
|
}
|
||||||
74
core/model/org_test.go
Normal file
74
core/model/org_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TdOrg struct {
|
||||||
|
db.Datastore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdOrg) GetOrg(orgId string, userId string) (*types.Org, error) {
|
||||||
|
if userId == "1" {
|
||||||
|
return &types.Org{
|
||||||
|
Id: "1",
|
||||||
|
Name: "MyOrg",
|
||||||
|
Currency: "USD",
|
||||||
|
Precision: 2,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdOrg) UpdateOrg(org *types.Org) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateOrg(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
org *types.Org
|
||||||
|
userId string
|
||||||
|
}{
|
||||||
|
"success": {
|
||||||
|
err: nil,
|
||||||
|
org: &types.Org{
|
||||||
|
Id: "1",
|
||||||
|
Name: "MyOrg2",
|
||||||
|
},
|
||||||
|
userId: "1",
|
||||||
|
},
|
||||||
|
"access denied": {
|
||||||
|
err: errors.New("access denied"),
|
||||||
|
org: &types.Org{
|
||||||
|
Id: "1",
|
||||||
|
Name: "MyOrg2",
|
||||||
|
},
|
||||||
|
userId: "2",
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
err: errors.New("name required"),
|
||||||
|
org: &types.Org{
|
||||||
|
Id: "1",
|
||||||
|
Name: "",
|
||||||
|
},
|
||||||
|
userId: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdOrg{}
|
||||||
|
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
err := model.UpdateOrg(test.org, test.userId)
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
117
core/model/price.go
Normal file
117
core/model/price.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/ws"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PriceInterface interface {
|
||||||
|
CreatePrice(*types.Price, string) error
|
||||||
|
DeletePrice(string, string) error
|
||||||
|
GetPricesNearestInTime(string, time.Time, string) ([]*types.Price, error)
|
||||||
|
GetPricesByCurrency(string, string, string) ([]*types.Price, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreatePrice(price *types.Price, userId string) error {
|
||||||
|
belongs, err := model.UserBelongsToOrg(userId, price.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if belongs == false {
|
||||||
|
return errors.New("User does not belong to org")
|
||||||
|
}
|
||||||
|
|
||||||
|
if price.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if price.OrgId == "" {
|
||||||
|
return errors.New("orgId required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if price.Currency == "" {
|
||||||
|
return errors.New("currency required")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.InsertPrice(price)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(price.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushPrice(price, userIds, "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeletePrice(id string, userId string) error {
|
||||||
|
// Get original price
|
||||||
|
price, err := model.db.GetPriceById(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
belongs, err := model.UserBelongsToOrg(userId, price.OrgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if belongs == false {
|
||||||
|
return errors.New("User does not belong to org")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.DeletePrice(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access account
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(price.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushPrice(price, userIds, "delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetPricesNearestInTime(orgId string, date time.Time, userId string) ([]*types.Price, error) {
|
||||||
|
belongs, err := model.UserBelongsToOrg(userId, orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if belongs == false {
|
||||||
|
return nil, errors.New("User does not belong to org")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.GetPricesNearestInTime(orgId, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetPricesByCurrency(orgId string, currency string, userId string) ([]*types.Price, error) {
|
||||||
|
belongs, err := model.UserBelongsToOrg(userId, orgId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if belongs == false {
|
||||||
|
return nil, errors.New("User does not belong to org")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.GetPricesByCurrency(orgId, currency)
|
||||||
|
}
|
||||||
149
core/model/price_test.go
Normal file
149
core/model/price_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/mocks"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreatePrice(t *testing.T) {
|
||||||
|
|
||||||
|
price := types.Price{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"BTC",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
6700,
|
||||||
|
}
|
||||||
|
|
||||||
|
badPrice := types.Price{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
6700,
|
||||||
|
}
|
||||||
|
|
||||||
|
badOrg := types.Price{
|
||||||
|
"1",
|
||||||
|
"1",
|
||||||
|
"BTC",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
6700,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
price types.Price
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
price: price,
|
||||||
|
},
|
||||||
|
"with error": {
|
||||||
|
err: errors.New("currency required"),
|
||||||
|
price: badPrice,
|
||||||
|
},
|
||||||
|
"with org error": {
|
||||||
|
err: errors.New("User does not belong to org"),
|
||||||
|
price: badOrg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
price := test.price
|
||||||
|
userId := "3"
|
||||||
|
|
||||||
|
db := &mocks.Datastore{}
|
||||||
|
|
||||||
|
db.On("GetOrgs", userId).Return([]*types.Org{
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
db.On("InsertPrice", &test.price).Return(nil)
|
||||||
|
|
||||||
|
db.On("GetOrgUserIds", price.OrgId).Return([]string{userId}, nil)
|
||||||
|
|
||||||
|
model := NewModel(db, &util.StandardBcrypt{}, types.Config{})
|
||||||
|
|
||||||
|
err := model.CreatePrice(&price, userId)
|
||||||
|
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeletePrice(t *testing.T) {
|
||||||
|
|
||||||
|
price := types.Price{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"BTC",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
6700,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
userId string
|
||||||
|
price types.Price
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
price: price,
|
||||||
|
userId: "3",
|
||||||
|
},
|
||||||
|
"with org error": {
|
||||||
|
err: errors.New("User does not belong to org"),
|
||||||
|
price: price,
|
||||||
|
userId: "4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
price := test.price
|
||||||
|
|
||||||
|
db := &mocks.Datastore{}
|
||||||
|
|
||||||
|
db.On("GetPriceById", price.Id).Return(&price, nil)
|
||||||
|
|
||||||
|
db.On("GetOrgs", "3").Return([]*types.Org{
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
db.On("GetOrgs", "4").Return([]*types.Org{
|
||||||
|
{
|
||||||
|
Id: "7",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
db.On("DeletePrice", price.Id).Return(nil)
|
||||||
|
|
||||||
|
db.On("GetOrgUserIds", price.OrgId).Return([]string{test.userId}, nil)
|
||||||
|
|
||||||
|
model := NewModel(db, &util.StandardBcrypt{}, types.Config{})
|
||||||
|
|
||||||
|
err := model.DeletePrice(price.Id, test.userId)
|
||||||
|
|
||||||
|
assert.Equal(t, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
core/model/session.go
Normal file
23
core/model/session.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionInterface interface {
|
||||||
|
CreateSession(*types.Session) error
|
||||||
|
DeleteSession(string, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateSession(session *types.Session) error {
|
||||||
|
if session.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.InsertSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeleteSession(id string, userId string) error {
|
||||||
|
return model.db.DeleteSession(id, userId)
|
||||||
|
}
|
||||||
213
core/model/transaction.go
Normal file
213
core/model/transaction.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/ws"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransactionInterface interface {
|
||||||
|
CreateTransaction(*types.Transaction) error
|
||||||
|
UpdateTransaction(string, *types.Transaction) error
|
||||||
|
GetTransactionsByAccount(string, string, string, *types.QueryOptions) ([]*types.Transaction, error)
|
||||||
|
GetTransactionsByOrg(string, string, *types.QueryOptions) ([]*types.Transaction, error)
|
||||||
|
DeleteTransaction(string, string, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateTransaction(transaction *types.Transaction) (err error) {
|
||||||
|
err = model.checkSplits(transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if transaction.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Inserted = time.Now()
|
||||||
|
transaction.Updated = time.Now()
|
||||||
|
|
||||||
|
if transaction.Date.IsZero() {
|
||||||
|
transaction.Date = transaction.Inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.InsertTransaction(transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access transaction
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushTransaction(transaction, userIds, "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UpdateTransaction(oldId string, transaction *types.Transaction) (err error) {
|
||||||
|
err = model.checkSplits(transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldId == "" || transaction.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get original transaction
|
||||||
|
original, err := model.getTransactionById(oldId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Updated = time.Now()
|
||||||
|
transaction.Inserted = original.Inserted
|
||||||
|
|
||||||
|
// We used to compare splits and if they hadn't changed just do an update
|
||||||
|
// on the transaction. The problem is then the updated field gets out of sync
|
||||||
|
// between the tranaction and its splits.
|
||||||
|
// It needs to be in sync for getTransactionsByOrg() to work correctly with pagination
|
||||||
|
|
||||||
|
// Delete old transaction and insert a new one
|
||||||
|
transaction.Inserted = transaction.Updated
|
||||||
|
err = model.db.DeleteAndInsertTransaction(oldId, transaction)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access transaction
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushTransaction(original, userIds, "delete")
|
||||||
|
ws.PushTransaction(transaction, userIds, "create")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetTransactionsByAccount(orgId string, userId string, accountId string, options *types.QueryOptions) ([]*types.Transaction, error) {
|
||||||
|
userAccounts, err := model.GetAccounts(orgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, accountId) {
|
||||||
|
return nil, errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", accountId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.GetTransactionsByAccount(accountId, options)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) GetTransactionsByOrg(orgId string, userId string, options *types.QueryOptions) ([]*types.Transaction, error) {
|
||||||
|
userAccounts, err := model.GetAccounts(orgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountIds []string
|
||||||
|
for _, account := range userAccounts {
|
||||||
|
accountIds = append(accountIds, account.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.GetTransactionsByOrg(orgId, options, accountIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) DeleteTransaction(id string, userId string, orgId string) (err error) {
|
||||||
|
transaction, err := model.getTransactionById(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccounts, err := model.GetAccounts(orgId, userId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, split := range transaction.Splits {
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, split.AccountId) {
|
||||||
|
return errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", split.AccountId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.DeleteTransaction(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify web socket subscribers
|
||||||
|
// TODO only get user ids that have permission to access transaction
|
||||||
|
userIds, err2 := model.db.GetOrgUserIds(transaction.OrgId)
|
||||||
|
|
||||||
|
if err2 == nil {
|
||||||
|
ws.PushTransaction(transaction, userIds, "delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) getTransactionById(id string) (*types.Transaction, error) {
|
||||||
|
// TODO if this is made public, make a separate version that checks permission
|
||||||
|
return model.db.GetTransactionById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) checkSplits(transaction *types.Transaction) (err error) {
|
||||||
|
if len(transaction.Splits) < 2 {
|
||||||
|
return errors.New("at least 2 splits are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
org, err := model.GetOrg(transaction.OrgId, transaction.UserId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccounts, err := model.GetAccounts(transaction.OrgId, transaction.UserId, "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var amount int64 = 0
|
||||||
|
|
||||||
|
for _, split := range transaction.Splits {
|
||||||
|
if !model.accountsContainWriteAccess(userAccounts, split.AccountId) {
|
||||||
|
return errors.New(fmt.Sprintf("%s %s", "user does not have permission to access account", split.AccountId))
|
||||||
|
}
|
||||||
|
|
||||||
|
account := model.getAccountFromList(userAccounts, split.AccountId)
|
||||||
|
|
||||||
|
if account.HasChildren == true {
|
||||||
|
return errors.New("Cannot use parent account for split")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.Currency == org.Currency && split.NativeAmount != split.Amount {
|
||||||
|
return errors.New("nativeAmount must equal amount for native currency splits")
|
||||||
|
}
|
||||||
|
|
||||||
|
amount += split.NativeAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount != 0 {
|
||||||
|
return errors.New("splits must add up to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
141
core/model/transaction_test.go
Normal file
141
core/model/transaction_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TdTransaction struct {
|
||||||
|
db.Datastore
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) GetOrg(orgId string, userId string) (*types.Org, error) {
|
||||||
|
org := &types.Org{
|
||||||
|
Currency: "USD",
|
||||||
|
}
|
||||||
|
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) GetPermissionedAccountIds(userId string, orgId string, tokenId string) ([]string, error) {
|
||||||
|
return []string{"1", "2"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) GetAccountsByOrgId(orgId string) ([]*types.Account, error) {
|
||||||
|
return []*types.Account{&types.Account{Id: "1", Currency: "USD"}, &types.Account{Id: "2"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) InsertTransaction(transaction *types.Transaction) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) GetTransactionById(id string) (*types.Transaction, error) {
|
||||||
|
args := td.Called(id)
|
||||||
|
return args.Get(0).(*types.Transaction), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) UpdateTransaction(oldId string, transaction *types.Transaction) error {
|
||||||
|
args := td.Called(oldId, transaction)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdTransaction) GetOrgUserIds(id string) ([]string, error) {
|
||||||
|
return []string{"1"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTransaction(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
tx *types.Transaction
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
tx: &types.Transaction{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
"description",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
[]*types.Split{
|
||||||
|
&types.Split{"1", "1", 1000, 1000},
|
||||||
|
&types.Split{"1", "2", -1000, -1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bad split amounts": {
|
||||||
|
err: errors.New("splits must add up to 0"),
|
||||||
|
tx: &types.Transaction{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
"description",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
[]*types.Split{
|
||||||
|
&types.Split{"1", "1", 1000, 1000},
|
||||||
|
&types.Split{"1", "2", -500, -500},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"lacking permission": {
|
||||||
|
err: errors.New("user does not have permission to access account 3"),
|
||||||
|
tx: &types.Transaction{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
"description",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
[]*types.Split{
|
||||||
|
&types.Split{"1", "1", 1000, 1000},
|
||||||
|
&types.Split{"1", "3", -1000, -1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nativeAmount mismatch": {
|
||||||
|
err: errors.New("nativeAmount must equal amount for native currency splits"),
|
||||||
|
tx: &types.Transaction{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
time.Now(),
|
||||||
|
"description",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
[]*types.Split{
|
||||||
|
&types.Split{"1", "1", 1000, 500},
|
||||||
|
&types.Split{"1", "2", -1000, -500},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
td := &TdTransaction{}
|
||||||
|
model := NewModel(td, nil, types.Config{})
|
||||||
|
|
||||||
|
err := model.CreateTransaction(test.tx)
|
||||||
|
|
||||||
|
assert.Equal(t, err, test.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
core/model/types/account.go
Normal file
31
core/model/types/account.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
OrgId string `json:"orgId"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Parent string `json:"parent"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Precision int `json:"precision"`
|
||||||
|
DebitBalance bool `json:"debitBalance"`
|
||||||
|
Balance *int64 `json:"balance"`
|
||||||
|
NativeBalance *int64 `json:"nativeBalance"`
|
||||||
|
ReadOnly bool `json:"readOnly"`
|
||||||
|
HasChildren bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountNode struct {
|
||||||
|
Account *Account
|
||||||
|
Parent *AccountNode
|
||||||
|
Children []*AccountNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccount() *Account {
|
||||||
|
return &Account{Precision: 2}
|
||||||
|
}
|
||||||
15
core/model/types/apikey.go
Normal file
15
core/model/types/apikey.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApiKey struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Deleted mysql.NullTime `json:"-"` // Can we marshal this correctly?
|
||||||
|
}
|
||||||
14
core/model/types/config.go
Normal file
14
core/model/types/config.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WebUrl string
|
||||||
|
Port int
|
||||||
|
KeyFile string
|
||||||
|
CertFile string
|
||||||
|
Database string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
SendgridKey string
|
||||||
|
SendgridEmail string
|
||||||
|
SendgridSender string
|
||||||
|
}
|
||||||
14
core/model/types/invite.go
Normal file
14
core/model/types/invite.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Invite struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
OrgId string `json:"orgId"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Accepted bool `json:"accepted"`
|
||||||
|
}
|
||||||
14
core/model/types/org.go
Normal file
14
core/model/types/org.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Org struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Precision int `json:"precision"`
|
||||||
|
}
|
||||||
15
core/model/types/price.go
Normal file
15
core/model/types/price.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Price struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
OrgId string `json:"orgId"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
}
|
||||||
104
core/model/types/query_options.go
Normal file
104
core/model/types/query_options.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryOptions struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Skip int `json:"skip"`
|
||||||
|
SinceInserted int `json:"sinceInserted"`
|
||||||
|
SinceUpdated int `json:"sinceUpdated"`
|
||||||
|
BeforeInserted int `json:"beforeInserted"`
|
||||||
|
BeforeUpdated int `json:"beforeUpdated"`
|
||||||
|
StartDate int `json:"startDate"`
|
||||||
|
EndDate int `json:"endDate"`
|
||||||
|
DescriptionStartsWith string `json:"descriptionStartsWith"`
|
||||||
|
IncludeDeleted bool `json:"includeDeleted"`
|
||||||
|
Sort string `json:"string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryOptionsFromURLQuery(urlQuery url.Values) (*QueryOptions, error) {
|
||||||
|
qo := &QueryOptions{}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if urlQuery.Get("limit") != "" {
|
||||||
|
qo.Limit, err = strconv.Atoi(urlQuery.Get("limit"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("skip") != "" {
|
||||||
|
qo.Skip, err = strconv.Atoi(urlQuery.Get("skip"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("sinceInserted") != "" {
|
||||||
|
qo.SinceInserted, err = strconv.Atoi(urlQuery.Get("sinceInserted"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("sinceUpdated") != "" {
|
||||||
|
qo.SinceUpdated, err = strconv.Atoi(urlQuery.Get("sinceUpdated"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("beforeInserted") != "" {
|
||||||
|
qo.BeforeInserted, err = strconv.Atoi(urlQuery.Get("beforeInserted"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("beforeUpdated") != "" {
|
||||||
|
qo.BeforeUpdated, err = strconv.Atoi(urlQuery.Get("beforeUpdated"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("startDate") != "" {
|
||||||
|
qo.StartDate, err = strconv.Atoi(urlQuery.Get("startDate"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("endDate") != "" {
|
||||||
|
qo.EndDate, err = strconv.Atoi(urlQuery.Get("endDate"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("descriptionStartsWith") != "" {
|
||||||
|
qo.DescriptionStartsWith = urlQuery.Get("descriptionStartsWith")
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("includeDeleted") == "true" {
|
||||||
|
qo.IncludeDeleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlQuery.Get("sort") != "" {
|
||||||
|
qo.Sort = urlQuery.Get("sort")
|
||||||
|
}
|
||||||
|
|
||||||
|
return qo, nil
|
||||||
|
}
|
||||||
14
core/model/types/session.go
Normal file
14
core/model/types/session.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Terminated mysql.NullTime `json:"-"` // Can we marshal this correctly?
|
||||||
|
}
|
||||||
25
core/model/types/transaction.go
Normal file
25
core/model/types/transaction.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
OrgId string `json:"orgId"`
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
Splits []*Split `json:"splits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Split struct {
|
||||||
|
TransactionId string `json:"-"`
|
||||||
|
AccountId string `json:"accountId"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
NativeAmount int64 `json:"nativeAmount"`
|
||||||
|
}
|
||||||
20
core/model/types/user.go
Normal file
20
core/model/types/user.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Inserted time.Time `json:"inserted"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
AgreeToTerms bool `json:"agreeToTerms"`
|
||||||
|
PasswordReset string `json:"-"`
|
||||||
|
EmailVerified bool `json:"emailVerified"`
|
||||||
|
EmailVerifyCode string `json:"-"`
|
||||||
|
}
|
||||||
228
core/model/user.go
Normal file
228
core/model/user.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"github.com/sendgrid/sendgrid-go"
|
||||||
|
"github.com/sendgrid/sendgrid-go/helpers/mail"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserInterface interface {
|
||||||
|
CreateUser(user *types.User) error
|
||||||
|
VerifyUser(string) error
|
||||||
|
UpdateUser(user *types.User) error
|
||||||
|
ResetPassword(email string) error
|
||||||
|
ConfirmResetPassword(string, string) (*types.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) CreateUser(user *types.User) error {
|
||||||
|
if user.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.FirstName == "" {
|
||||||
|
return errors.New("first name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.LastName == "" {
|
||||||
|
return errors.New("last name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Email == "" {
|
||||||
|
return errors.New("email required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Password == "" {
|
||||||
|
return errors.New("password required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AgreeToTerms != true {
|
||||||
|
return errors.New("must agree to terms")
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash password
|
||||||
|
// bcrypt's function also generates a salt
|
||||||
|
|
||||||
|
passwordHash, err := model.bcrypt.GenerateFromPassword([]byte(user.Password), model.bcrypt.GetDefaultCost())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = string(passwordHash)
|
||||||
|
user.Password = ""
|
||||||
|
user.EmailVerified = false
|
||||||
|
user.EmailVerifyCode, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.InsertUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.SendVerificationEmail(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) VerifyUser(code string) error {
|
||||||
|
if code == "" {
|
||||||
|
return errors.New("code required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.db.VerifyUser(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) UpdateUser(user *types.User) error {
|
||||||
|
if user.Id == "" {
|
||||||
|
return errors.New("id required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Password == "" {
|
||||||
|
return errors.New("password required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash password
|
||||||
|
// bcrypt's function also generates a salt
|
||||||
|
|
||||||
|
passwordHash, err := model.bcrypt.GenerateFromPassword([]byte(user.Password), model.bcrypt.GetDefaultCost())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = string(passwordHash)
|
||||||
|
user.Password = ""
|
||||||
|
|
||||||
|
return model.db.UpdateUser(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) ResetPassword(email string) error {
|
||||||
|
if email == "" {
|
||||||
|
return errors.New("email required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := model.db.GetVerifiedUserByEmail(email)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Don't send back error so people can't try to find user accounts
|
||||||
|
log.Printf("Invalid email for reset password " + email)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordReset, err = util.NewGuid()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = model.db.UpdateUserResetPassword(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.SendPasswordResetEmail(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) ConfirmResetPassword(password string, code string) (*types.User, error) {
|
||||||
|
if password == "" {
|
||||||
|
return nil, errors.New("password required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("code required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := model.db.GetUserByResetCode(code)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Invalid code")
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordHash, err := model.bcrypt.GenerateFromPassword([]byte(password), model.bcrypt.GetDefaultCost())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = string(passwordHash)
|
||||||
|
user.Password = ""
|
||||||
|
|
||||||
|
err = model.db.UpdateUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) SendVerificationEmail(user *types.User) error {
|
||||||
|
log.Println("Sending verification email to " + user.Email)
|
||||||
|
|
||||||
|
link := model.config.WebUrl + "/user/verify?code=" + user.EmailVerifyCode
|
||||||
|
|
||||||
|
from := mail.NewEmail(model.config.SendgridSender, model.config.SendgridEmail)
|
||||||
|
subject := "Verify your email"
|
||||||
|
to := mail.NewEmail(user.FirstName+" "+user.LastName, user.Email)
|
||||||
|
|
||||||
|
plainTextContent := "Thank you for signing up with Open Accounting! " +
|
||||||
|
"Please click on the link below to verify your email address:\n\n" + link
|
||||||
|
htmlContent := "Thank you for signing up with Open Accounting! " +
|
||||||
|
"Please click on the link below to verify your email address:<br><br>" +
|
||||||
|
"<a href=\"" + link + "\">" + link + "</a>"
|
||||||
|
|
||||||
|
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
|
||||||
|
client := sendgrid.NewSendClient(model.config.SendgridKey)
|
||||||
|
response, err := client.Send(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(response.StatusCode)
|
||||||
|
log.Println(response.Body)
|
||||||
|
log.Println(response.Headers)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (model *Model) SendPasswordResetEmail(user *types.User) error {
|
||||||
|
log.Println("Sending password reset email to " + user.Email)
|
||||||
|
|
||||||
|
link := model.config.WebUrl + "/user/reset-password?code=" + user.PasswordReset
|
||||||
|
|
||||||
|
from := mail.NewEmail(model.config.SendgridSender, model.config.SendgridEmail)
|
||||||
|
subject := "Reset password"
|
||||||
|
to := mail.NewEmail(user.FirstName+" "+user.LastName, user.Email)
|
||||||
|
|
||||||
|
plainTextContent := "Please click the following link to reset your password:\n\n" + link +
|
||||||
|
"If you did not request to have your password reset, please ignore this email and " +
|
||||||
|
"nothing will happen."
|
||||||
|
htmlContent := "Please click the following link to reset your password:<br><br>\n" +
|
||||||
|
"<a href=\"" + link + "\">" + link + "</a><br><br>\n" +
|
||||||
|
"If you did not request to have your password reset, please ignore this email and " +
|
||||||
|
"nothing will happen."
|
||||||
|
|
||||||
|
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
|
||||||
|
client := sendgrid.NewSendClient(model.config.SendgridKey)
|
||||||
|
response, err := client.Send(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(response.StatusCode)
|
||||||
|
log.Println(response.Body)
|
||||||
|
log.Println(response.Headers)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
177
core/model/user_test.go
Normal file
177
core/model/user_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/openaccounting/oa-server/core/mocks"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TdUser struct {
|
||||||
|
db.Datastore
|
||||||
|
testNum int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdUser) InsertUser(user *types.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (td *TdUser) UpdateUser(user *types.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUser(t *testing.T) {
|
||||||
|
|
||||||
|
// Id string `json:"id"`
|
||||||
|
// Inserted time.Time `json:"inserted"`
|
||||||
|
// Updated time.Time `json:"updated"`
|
||||||
|
// FirstName string `json:"firstName"`
|
||||||
|
// LastName string `json:"lastName"`
|
||||||
|
// Email string `json:"email"`
|
||||||
|
// Password string `json:"password"`
|
||||||
|
// PasswordHash string `json:"-"`
|
||||||
|
// AgreeToTerms bool `json:"agreeToTerms"`
|
||||||
|
// PasswordReset string `json:"-"`
|
||||||
|
// EmailVerified bool `json:"emailVerified"`
|
||||||
|
// EmailVerifyCode string `json:"-"`
|
||||||
|
|
||||||
|
user := types.User{
|
||||||
|
"0",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
"johndoe@email.com",
|
||||||
|
"password",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
badUser := types.User{
|
||||||
|
"0",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
"John",
|
||||||
|
"Doe",
|
||||||
|
"",
|
||||||
|
"password",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
user types.User
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
"with error": {
|
||||||
|
err: errors.New("email required"),
|
||||||
|
user: badUser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
user := test.user
|
||||||
|
|
||||||
|
mockBcrypt := new(mocks.Bcrypt)
|
||||||
|
|
||||||
|
mockBcrypt.On("GetDefaultCost").Return(10)
|
||||||
|
|
||||||
|
mockBcrypt.On("GenerateFromPassword", []byte(user.Password), 10).
|
||||||
|
Return(make([]byte, 0), nil)
|
||||||
|
|
||||||
|
model := NewModel(&TdUser{}, mockBcrypt, types.Config{})
|
||||||
|
|
||||||
|
err := model.CreateUser(&user)
|
||||||
|
|
||||||
|
assert.Equal(t, err, test.err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
mockBcrypt.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUser(t *testing.T) {
|
||||||
|
|
||||||
|
user := types.User{
|
||||||
|
"0",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
"John2",
|
||||||
|
"Doe",
|
||||||
|
"johndoe@email.com",
|
||||||
|
"password",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
badUser := types.User{
|
||||||
|
"0",
|
||||||
|
time.Unix(0, 0),
|
||||||
|
time.Unix(0, 0),
|
||||||
|
"John2",
|
||||||
|
"Doe",
|
||||||
|
"johndoe@email.com",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
err error
|
||||||
|
user types.User
|
||||||
|
}{
|
||||||
|
"successful": {
|
||||||
|
err: nil,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
"with error": {
|
||||||
|
err: errors.New("password required"),
|
||||||
|
user: badUser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("Running test case: %s", name)
|
||||||
|
|
||||||
|
user := test.user
|
||||||
|
|
||||||
|
mockBcrypt := new(mocks.Bcrypt)
|
||||||
|
|
||||||
|
mockBcrypt.On("GetDefaultCost").Return(10)
|
||||||
|
|
||||||
|
mockBcrypt.On("GenerateFromPassword", []byte(user.Password), 10).
|
||||||
|
Return(make([]byte, 0), nil)
|
||||||
|
|
||||||
|
model := NewModel(&TdUser{}, mockBcrypt, types.Config{})
|
||||||
|
|
||||||
|
err := model.UpdateUser(&user)
|
||||||
|
|
||||||
|
assert.Equal(t, err, test.err)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
mockBcrypt.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
core/server.go
Normal file
50
core/server.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/openaccounting/oa-server/core/api"
|
||||||
|
"github.com/openaccounting/oa-server/core/auth"
|
||||||
|
"github.com/openaccounting/oa-server/core/model"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/db"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"github.com/openaccounting/oa-server/core/util"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
//"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
//filename is the path to the json config file
|
||||||
|
var config types.Config
|
||||||
|
file, err := os.Open("./config.json")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
err = decoder.Decode(&config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionString := config.User + ":" + config.Password + "@/" + config.Database
|
||||||
|
|
||||||
|
db, err := db.NewDB(connectionString)
|
||||||
|
|
||||||
|
bc := &util.StandardBcrypt{}
|
||||||
|
|
||||||
|
model.NewModel(db, bc, config)
|
||||||
|
auth.NewAuthService(db, bc)
|
||||||
|
|
||||||
|
app, err := api.Init()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(config.Port), config.CertFile, config.KeyFile, app.MakeHandler()))
|
||||||
|
}
|
||||||
26
core/util/bcrypt.go
Normal file
26
core/util/bcrypt.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bcrypt interface {
|
||||||
|
GenerateFromPassword([]byte, int) ([]byte, error)
|
||||||
|
CompareHashAndPassword([]byte, []byte) error
|
||||||
|
GetDefaultCost() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type StandardBcrypt struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *StandardBcrypt) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
|
||||||
|
return bcrypt.GenerateFromPassword(password, cost)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *StandardBcrypt) CompareHashAndPassword(hashedPassword, password []byte) error {
|
||||||
|
return bcrypt.CompareHashAndPassword(hashedPassword, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *StandardBcrypt) GetDefaultCost() int {
|
||||||
|
return bcrypt.DefaultCost
|
||||||
|
}
|
||||||
46
core/util/util.go
Normal file
46
core/util/util.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Round64(input float64) int64 {
|
||||||
|
if input < 0 {
|
||||||
|
return int64(input - 0.5)
|
||||||
|
}
|
||||||
|
return int64(input + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeToMs(date time.Time) int64 {
|
||||||
|
return date.UnixNano() / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
func MsToTime(ms int64) time.Time {
|
||||||
|
return time.Unix(0, ms*1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGuid() (string, error) {
|
||||||
|
byteArray := make([]byte, 16)
|
||||||
|
|
||||||
|
_, err := rand.Read(byteArray)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(byteArray), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInviteId() (string, error) {
|
||||||
|
byteArray := make([]byte, 4)
|
||||||
|
|
||||||
|
_, err := rand.Read(byteArray)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(byteArray), nil
|
||||||
|
}
|
||||||
341
core/ws/ws.go
Normal file
341
core/ws/ws.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package ws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
"github.com/ant0ine/go-json-rest/rest"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/openaccounting/oa-server/core/auth"
|
||||||
|
"github.com/openaccounting/oa-server/core/model/types"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "1.0.0"
|
||||||
|
|
||||||
|
//var upgrader = websocket.Upgrader{} // use default options
|
||||||
|
var txSubscriptions = make(map[string][]*websocket.Conn)
|
||||||
|
var accountSubscriptions = make(map[string][]*websocket.Conn)
|
||||||
|
var priceSubscriptions = make(map[string][]*websocket.Conn)
|
||||||
|
var userMap = make(map[*websocket.Conn]*types.User)
|
||||||
|
var sequenceNumbers = make(map[*websocket.Conn]int)
|
||||||
|
var locks = make(map[*websocket.Conn]*sync.Mutex)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
SequenceNumber int `json:"sequenceNumber"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(w rest.ResponseWriter, r *rest.Request) {
|
||||||
|
c, err := websocket.Upgrade(w.(http.ResponseWriter), r.Request, nil, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("upgrade:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceNumbers[c] = -1
|
||||||
|
locks[c] = &sync.Mutex{}
|
||||||
|
|
||||||
|
defer c.Close()
|
||||||
|
for {
|
||||||
|
mt, messageData, err := c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("readerr:", err)
|
||||||
|
// remove connection from maps
|
||||||
|
unsubscribeAll(c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt != websocket.TextMessage {
|
||||||
|
log.Println("Unsupported message type", mt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
message := Message{}
|
||||||
|
|
||||||
|
err = json.Unmarshal(messageData, &message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Could not parse message:", string(messageData))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("recv: %s", message)
|
||||||
|
|
||||||
|
// check version
|
||||||
|
err = checkVersion(message.Version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
writeMessage(c, websocket.CloseMessage, websocket.FormatCloseMessage(4001, err.Error()))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure they are authenticated
|
||||||
|
if userMap[c] == nil {
|
||||||
|
log.Println("checking message for authentication")
|
||||||
|
err = authenticate(message, c)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Authentication error " + err.Error())
|
||||||
|
writeMessage(c, websocket.CloseMessage, websocket.FormatCloseMessage(4000, err.Error()))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processMessage(message, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKey(userId string, orgId string) string {
|
||||||
|
return userId + "-" + orgId
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMessage(message Message, conn *websocket.Conn) error {
|
||||||
|
var dataString string
|
||||||
|
err := mapstructure.Decode(message.Data, &dataString)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := getKey(userMap[conn].Id, dataString)
|
||||||
|
|
||||||
|
log.Println(message.Action, message.Type, dataString)
|
||||||
|
|
||||||
|
switch message.Action {
|
||||||
|
case "subscribe":
|
||||||
|
switch message.Type {
|
||||||
|
case "transaction":
|
||||||
|
subscribe(conn, key, txSubscriptions)
|
||||||
|
case "account":
|
||||||
|
subscribe(conn, key, accountSubscriptions)
|
||||||
|
case "price":
|
||||||
|
subscribe(conn, key, priceSubscriptions)
|
||||||
|
default:
|
||||||
|
return errors.New("Unhandled message type: " + message.Type)
|
||||||
|
}
|
||||||
|
case "unsubscribe":
|
||||||
|
switch message.Type {
|
||||||
|
case "transaction":
|
||||||
|
unsubscribe(conn, key, txSubscriptions)
|
||||||
|
case "account":
|
||||||
|
unsubscribe(conn, key, accountSubscriptions)
|
||||||
|
case "price":
|
||||||
|
unsubscribe(conn, key, priceSubscriptions)
|
||||||
|
default:
|
||||||
|
return errors.New("Unhandled message type: " + message.Type)
|
||||||
|
}
|
||||||
|
case "ping":
|
||||||
|
sequenceNumbers[conn]++
|
||||||
|
response := Message{version, sequenceNumbers[conn], "pong", "pong", nil}
|
||||||
|
responseData, err := json.Marshal(response)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeMessage(conn, websocket.TextMessage, responseData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
unsubscribeAll(conn)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe(conn *websocket.Conn, key string, clientMap map[string][]*websocket.Conn) {
|
||||||
|
conns := clientMap[key]
|
||||||
|
alreadySubscribed := false
|
||||||
|
|
||||||
|
for _, c := range conns {
|
||||||
|
if conn == c {
|
||||||
|
alreadySubscribed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if alreadySubscribed == false {
|
||||||
|
clientMap[key] = append(clientMap[key], conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(conn *websocket.Conn, key string, clientMap map[string][]*websocket.Conn) {
|
||||||
|
newConns := clientMap[key][:0]
|
||||||
|
|
||||||
|
for _, c := range clientMap[key] {
|
||||||
|
if conn != c {
|
||||||
|
newConns = append(newConns, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribeAll(conn *websocket.Conn) {
|
||||||
|
for key, conns := range txSubscriptions {
|
||||||
|
newConns := conns[:0]
|
||||||
|
for _, c := range conns {
|
||||||
|
if conn != c {
|
||||||
|
newConns = append(newConns, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txSubscriptions[key] = newConns
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, conns := range accountSubscriptions {
|
||||||
|
newConns := conns[:0]
|
||||||
|
for _, c := range conns {
|
||||||
|
if conn != c {
|
||||||
|
newConns = append(newConns, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accountSubscriptions[key] = newConns
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, conns := range priceSubscriptions {
|
||||||
|
newConns := conns[:0]
|
||||||
|
for _, c := range conns {
|
||||||
|
if conn != c {
|
||||||
|
newConns = append(newConns, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
priceSubscriptions[key] = newConns
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(userMap, conn)
|
||||||
|
delete(sequenceNumbers, conn)
|
||||||
|
delete(locks, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PushTransaction(transaction *types.Transaction, userIds []string, action string) {
|
||||||
|
log.Println(txSubscriptions)
|
||||||
|
|
||||||
|
message := Message{version, -1, "transaction", action, transaction}
|
||||||
|
|
||||||
|
for _, userId := range userIds {
|
||||||
|
key := getKey(userId, transaction.OrgId)
|
||||||
|
for _, conn := range txSubscriptions[key] {
|
||||||
|
sequenceNumbers[conn]++
|
||||||
|
message.SequenceNumber = sequenceNumbers[conn]
|
||||||
|
messageData, err := json.Marshal(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("PushTransaction json error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeMessage(conn, websocket.TextMessage, messageData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot PushTransaction to client:", err)
|
||||||
|
unsubscribeAll(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PushAccount(account *types.Account, userIds []string, action string) {
|
||||||
|
message := Message{version, -1, "account", action, account}
|
||||||
|
|
||||||
|
for _, userId := range userIds {
|
||||||
|
key := getKey(userId, account.OrgId)
|
||||||
|
for _, conn := range accountSubscriptions[key] {
|
||||||
|
sequenceNumbers[conn]++
|
||||||
|
message.SequenceNumber = sequenceNumbers[conn]
|
||||||
|
messageData, err := json.Marshal(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("PushAccount error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = writeMessage(conn, websocket.TextMessage, messageData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot PushAccount to client:", err)
|
||||||
|
unsubscribeAll(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PushPrice(price *types.Price, userIds []string, action string) {
|
||||||
|
message := Message{version, -1, "price", action, price}
|
||||||
|
|
||||||
|
for _, userId := range userIds {
|
||||||
|
key := getKey(userId, price.OrgId)
|
||||||
|
for _, conn := range priceSubscriptions[key] {
|
||||||
|
sequenceNumbers[conn]++
|
||||||
|
message.SequenceNumber = sequenceNumbers[conn]
|
||||||
|
messageData, err := json.Marshal(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("PushPrice error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeMessage(conn, websocket.TextMessage, messageData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot PushPrice to client:", err)
|
||||||
|
unsubscribeAll(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate(message Message, conn *websocket.Conn) error {
|
||||||
|
var id string
|
||||||
|
err := mapstructure.Decode(message.Data, &id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.Action != "authenticate" {
|
||||||
|
return errors.New("Authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := auth.Instance.Authenticate(id, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap[conn] = user
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVersion(clientVersion string) error {
|
||||||
|
constraint, err := semver.NewConstraint(clientVersion)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Invalid version")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverVersion, _ := semver.NewVersion(version)
|
||||||
|
|
||||||
|
versionMatch := constraint.Check(serverVersion)
|
||||||
|
|
||||||
|
if versionMatch != true {
|
||||||
|
return errors.New("Invalid version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMessage(conn *websocket.Conn, messageType int, data []byte) error {
|
||||||
|
locks[conn].Lock()
|
||||||
|
defer locks[conn].Unlock()
|
||||||
|
return conn.WriteMessage(messageType, data)
|
||||||
|
}
|
||||||
5
indexes.sql
Normal file
5
indexes.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE INDEX account_orgId_index ON account (orgId);
|
||||||
|
CREATE INDEX split_accountId_index ON split (accountId);
|
||||||
|
CREATE INDEX split_transactionId_index ON split (transactionId);
|
||||||
|
CREATE INDEX split_date_index ON split (date);
|
||||||
|
CREATE INDEX split_updated_index ON split (updated);
|
||||||
4
runTests
Executable file
4
runTests
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
go test ./core/model
|
||||||
|
go test ./core/auth
|
||||||
33
schema.sql
Normal file
33
schema.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
DROP DATABASE IF EXISTS `openaccounting`;
|
||||||
|
|
||||||
|
CREATE DATABASE openaccounting CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
use openaccounting;
|
||||||
|
|
||||||
|
CREATE TABLE org (id BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, currency VARCHAR(10) NOT NULL, `precision` INT NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE user (id BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, firstName VARCHAR(50) NOT NULL, lastName VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL, passwordHash VARCHAR(100) NOT NULL, agreeToTerms BOOLEAN NOT NULL, passwordReset VARCHAR(32) NOT NULL, emailVerified BOOLEAN NOT NULL, emailVerifyCode VARCHAR(32) NOT NULL, UNIQUE(email), PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE userorg (id INT UNSIGNED NOT NULL AUTO_INCREMENT, userId BINARY(16) NOT NULL, orgId BINARY(16) NOT NULL, admin BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE token (id BINARY(16) NOT NULL, name VARCHAR(100), userOrgId INT UNSIGNED NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE account (id BINARY(16) NOT NULL, orgId BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, parent BINARY(16) NOT NULL, currency VARCHAR(10) NOT NULL, `precision` INT NOT NULL, debitBalance BOOLEAN NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE transaction (id BINARY(16) NOT NULL, orgId BINARY(16) NOT NULL, userId BINARY(16) NOT NULL, date BIGINT UNSIGNED NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, description VARCHAR(300) NOT NULL, data TEXT NOT NULL, deleted BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE split (id INT UNSIGNED NOT NULL AUTO_INCREMENT, transactionId BINARY(16) NOT NULL, accountId BINARY(16) NOT NULL, date BIGINT UNSIGNED NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, amount BIGINT NOT NULL, nativeAmount BIGINT NOT NULL, deleted BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE balance (id INT UNSIGNED NOT NULL AUTO_INCREMENT, date BIGINT UNSIGNED NOT NULL, accountId BINARY(16) NOT NULL, amount BIGINT NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE permission (id BINARY(16) NOT NULL, userId BINARY(16), tokenId BINARY(16), orgId BINARY(16) NOT NULL, accountId BINARY(16) NOT NULL, type INT UNSIGNED NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE price (id BINARY(16) NOT NULL, orgId BINARY(16) NOT NULL, currency VARCHAR(10) NOT NULL, date BIGINT UNSIGNED NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, price DOUBLE UNSIGNED NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE session (id BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, userId BINARY(16) NOT NULL, `terminated` BIGINT UNSIGNED, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE apikey (id BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, userId BINARY(16) NOT NULL, label VARCHAR(300) NOT NULL, deleted BIGINT UNSIGNED, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE invite (id VARCHAR(32) NOT NULL, orgId BINARY(16) NOT NULL, inserted BIGINT UNSIGNED NOT NULL, updated BIGINT UNSIGNED NOT NULL, email VARCHAR(100) NOT NULL, accepted BOOLEAN NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
GRANT ALL ON openaccounting.* TO 'openaccounting'@'localhost' IDENTIFIED BY 'openaccounting';
|
||||||
86
vendor/github.com/Masterminds/semver/CHANGELOG.md
generated
vendored
Normal file
86
vendor/github.com/Masterminds/semver/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 1.4.2 (2018-04-10)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
- #72: Updated the docs to point to vert for a console appliaction
|
||||||
|
- #71: Update the docs on pre-release comparator handling
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
|
||||||
|
|
||||||
|
# 1.4.1 (2018-04-02)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Fixed #64: Fix pre-release precedence issue (thanks @uudashr)
|
||||||
|
|
||||||
|
# 1.4.0 (2017-10-04)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill)
|
||||||
|
|
||||||
|
# 1.3.1 (2017-07-10)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Fixed #57: number comparisons in prerelease sometimes inaccurate
|
||||||
|
|
||||||
|
# 1.3.0 (2017-05-02)
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- #45: Added json (un)marshaling support (thanks @mh-cbon)
|
||||||
|
- Stability marker. See https://masterminds.github.io/stability/
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
- #55: The godoc icon moved from png to svg
|
||||||
|
|
||||||
|
# 1.2.3 (2017-04-03)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
|
||||||
|
|
||||||
|
# Release 1.2.2 (2016-12-13)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
|
||||||
|
|
||||||
|
# Release 1.2.1 (2016-11-28)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
|
||||||
|
properly.
|
||||||
|
|
||||||
|
# Release 1.2.0 (2016-11-04)
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- #20: Added MustParse function for versions (thanks @adamreese)
|
||||||
|
- #15: Added increment methods on versions (thanks @mh-cbon)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
|
||||||
|
might not satisfy the intended compatibility. The change here ignores pre-releases
|
||||||
|
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
|
||||||
|
constraint. For example, `^1.2.3` will ignore pre-releases while
|
||||||
|
`^1.2.3-alpha` will include them.
|
||||||
|
|
||||||
|
# Release 1.1.1 (2016-06-30)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
|
||||||
|
- Issue #8: Added benchmarks (thanks @sdboyer)
|
||||||
|
- Updated Go Report Card URL to new location
|
||||||
|
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
|
||||||
|
- Updating tagging to v[SemVer] structure for compatibility with other tools.
|
||||||
|
|
||||||
|
# Release 1.1.0 (2016-03-11)
|
||||||
|
|
||||||
|
- Issue #2: Implemented validation to provide reasons a versions failed a
|
||||||
|
constraint.
|
||||||
|
|
||||||
|
# Release 1.0.1 (2015-12-31)
|
||||||
|
|
||||||
|
- Fixed #1: * constraint failing on valid versions.
|
||||||
|
|
||||||
|
# Release 1.0.0 (2015-10-20)
|
||||||
|
|
||||||
|
- Initial release
|
||||||
20
vendor/github.com/Masterminds/semver/LICENSE.txt
generated
vendored
Normal file
20
vendor/github.com/Masterminds/semver/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
The Masterminds
|
||||||
|
Copyright (C) 2014-2015, Matt Butcher and Matt Farina
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
36
vendor/github.com/Masterminds/semver/Makefile
generated
vendored
Normal file
36
vendor/github.com/Masterminds/semver/Makefile
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
.PHONY: setup
|
||||||
|
setup:
|
||||||
|
go get -u gopkg.in/alecthomas/gometalinter.v1
|
||||||
|
gometalinter.v1 --install
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: validate lint
|
||||||
|
@echo "==> Running tests"
|
||||||
|
go test -v
|
||||||
|
|
||||||
|
.PHONY: validate
|
||||||
|
validate:
|
||||||
|
@echo "==> Running static validations"
|
||||||
|
@gometalinter.v1 \
|
||||||
|
--disable-all \
|
||||||
|
--enable deadcode \
|
||||||
|
--severity deadcode:error \
|
||||||
|
--enable gofmt \
|
||||||
|
--enable gosimple \
|
||||||
|
--enable ineffassign \
|
||||||
|
--enable misspell \
|
||||||
|
--enable vet \
|
||||||
|
--tests \
|
||||||
|
--vendor \
|
||||||
|
--deadline 60s \
|
||||||
|
./... || exit_code=1
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@echo "==> Running linters"
|
||||||
|
@gometalinter.v1 \
|
||||||
|
--disable-all \
|
||||||
|
--enable golint \
|
||||||
|
--vendor \
|
||||||
|
--deadline 60s \
|
||||||
|
./... || :
|
||||||
186
vendor/github.com/Masterminds/semver/README.md
generated
vendored
Normal file
186
vendor/github.com/Masterminds/semver/README.md
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# SemVer
|
||||||
|
|
||||||
|
The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to:
|
||||||
|
|
||||||
|
* Parse semantic versions
|
||||||
|
* Sort semantic versions
|
||||||
|
* Check if a semantic version fits within a set of constraints
|
||||||
|
* Optionally work with a `v` prefix
|
||||||
|
|
||||||
|
[](https://masterminds.github.io/stability/active.html)
|
||||||
|
[](https://travis-ci.org/Masterminds/semver) [](https://ci.appveyor.com/project/mattfarina/semver/branch/master) [](https://godoc.org/github.com/Masterminds/semver) [](https://goreportcard.com/report/github.com/Masterminds/semver)
|
||||||
|
|
||||||
|
If you are looking for a command line tool for version comparisons please see
|
||||||
|
[vert](https://github.com/Masterminds/vert) which uses this library.
|
||||||
|
|
||||||
|
## Parsing Semantic Versions
|
||||||
|
|
||||||
|
To parse a semantic version use the `NewVersion` function. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
v, err := semver.NewVersion("1.2.3-beta.1+build345")
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is an error the version wasn't parseable. The version object has methods
|
||||||
|
to get the parts of the version, compare it to other versions, convert the
|
||||||
|
version back into a string, and get the original string. For more details
|
||||||
|
please see the [documentation](https://godoc.org/github.com/Masterminds/semver).
|
||||||
|
|
||||||
|
## Sorting Semantic Versions
|
||||||
|
|
||||||
|
A set of versions can be sorted using the [`sort`](https://golang.org/pkg/sort/)
|
||||||
|
package from the standard library. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
|
||||||
|
vs := make([]*semver.Version, len(raw))
|
||||||
|
for i, r := range raw {
|
||||||
|
v, err := semver.NewVersion(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error parsing version: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vs[i] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(semver.Collection(vs))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking Version Constraints
|
||||||
|
|
||||||
|
Checking a version against version constraints is one of the most featureful
|
||||||
|
parts of the package.
|
||||||
|
|
||||||
|
```go
|
||||||
|
c, err := semver.NewConstraint(">= 1.2.3")
|
||||||
|
if err != nil {
|
||||||
|
// Handle constraint not being parseable.
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := semver.NewVersion("1.3")
|
||||||
|
if err != nil {
|
||||||
|
// Handle version not being parseable.
|
||||||
|
}
|
||||||
|
// Check if the version meets the constraints. The a variable will be true.
|
||||||
|
a := c.Check(v)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Comparisons
|
||||||
|
|
||||||
|
There are two elements to the comparisons. First, a comparison string is a list
|
||||||
|
of comma separated and comparisons. These are then separated by || separated or
|
||||||
|
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
|
||||||
|
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
|
||||||
|
greater than or equal to 4.2.3.
|
||||||
|
|
||||||
|
The basic comparisons are:
|
||||||
|
|
||||||
|
* `=`: equal (aliased to no operator)
|
||||||
|
* `!=`: not equal
|
||||||
|
* `>`: greater than
|
||||||
|
* `<`: less than
|
||||||
|
* `>=`: greater than or equal to
|
||||||
|
* `<=`: less than or equal to
|
||||||
|
|
||||||
|
## Working With Pre-release Versions
|
||||||
|
|
||||||
|
Pre-releases, for those not familiar with them, are used for software releases
|
||||||
|
prior to stable or generally available releases. Examples of pre-releases include
|
||||||
|
development, alpha, beta, and release candidate releases. A pre-release may be
|
||||||
|
a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the
|
||||||
|
order of precidence, pre-releases come before their associated releases. In this
|
||||||
|
example `1.2.3-beta.1 < 1.2.3`.
|
||||||
|
|
||||||
|
According to the Semantic Version specification pre-releases may not be
|
||||||
|
API compliant with their release counterpart. It says,
|
||||||
|
|
||||||
|
> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.
|
||||||
|
|
||||||
|
SemVer comparisons without a pre-release comparator will skip pre-release versions.
|
||||||
|
For example, `>=1.2.3` will skip pre-releases when looking at a list of releases
|
||||||
|
while `>=1.2.3-0` will evaluate and find pre-releases.
|
||||||
|
|
||||||
|
The reason for the `0` as a pre-release version in the example comparison is
|
||||||
|
because pre-releases can only contain ASCII alphanumerics and hyphens (along with
|
||||||
|
`.` separators), per the spec. Sorting happens in ASCII sort order, again per the spec. The lowest character is a `0` in ASCII sort order (see an [ASCII Table](http://www.asciitable.com/))
|
||||||
|
|
||||||
|
Understanding ASCII sort ordering is important because A-Z comes before a-z. That
|
||||||
|
means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case
|
||||||
|
sensitivity doesn't apply here. This is due to ASCII sort ordering which is what
|
||||||
|
the spec specifies.
|
||||||
|
|
||||||
|
## Hyphen Range Comparisons
|
||||||
|
|
||||||
|
There are multiple methods to handle ranges and the first is hyphens ranges.
|
||||||
|
These look like:
|
||||||
|
|
||||||
|
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
|
||||||
|
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
|
||||||
|
|
||||||
|
## Wildcards In Comparisons
|
||||||
|
|
||||||
|
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
|
||||||
|
for all comparison operators. When used on the `=` operator it falls
|
||||||
|
back to the pack level comparison (see tilde below). For example,
|
||||||
|
|
||||||
|
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||||
|
* `>= 1.2.x` is equivalent to `>= 1.2.0`
|
||||||
|
* `<= 2.x` is equivalent to `< 3`
|
||||||
|
* `*` is equivalent to `>= 0.0.0`
|
||||||
|
|
||||||
|
## Tilde Range Comparisons (Patch)
|
||||||
|
|
||||||
|
The tilde (`~`) comparison operator is for patch level ranges when a minor
|
||||||
|
version is specified and major level changes when the minor number is missing.
|
||||||
|
For example,
|
||||||
|
|
||||||
|
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
|
||||||
|
* `~1` is equivalent to `>= 1, < 2`
|
||||||
|
* `~2.3` is equivalent to `>= 2.3, < 2.4`
|
||||||
|
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||||
|
* `~1.x` is equivalent to `>= 1, < 2`
|
||||||
|
|
||||||
|
## Caret Range Comparisons (Major)
|
||||||
|
|
||||||
|
The caret (`^`) comparison operator is for major level changes. This is useful
|
||||||
|
when comparisons of API versions as a major change is API breaking. For example,
|
||||||
|
|
||||||
|
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
|
||||||
|
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
|
||||||
|
* `^2.3` is equivalent to `>= 2.3, < 3`
|
||||||
|
* `^2.x` is equivalent to `>= 2.0.0, < 3`
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
|
||||||
|
In addition to testing a version against a constraint, a version can be validated
|
||||||
|
against a constraint. When validation fails a slice of errors containing why a
|
||||||
|
version didn't meet the constraint is returned. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
|
||||||
|
if err != nil {
|
||||||
|
// Handle constraint not being parseable.
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := semver.NewVersion("1.3")
|
||||||
|
if err != nil {
|
||||||
|
// Handle version not being parseable.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate a version against a constraint.
|
||||||
|
a, msgs := c.Validate(v)
|
||||||
|
// a is false
|
||||||
|
for _, m := range msgs {
|
||||||
|
fmt.Println(m)
|
||||||
|
|
||||||
|
// Loops over the errors which would read
|
||||||
|
// "1.3 is greater than 1.2.3"
|
||||||
|
// "1.3 is less than 1.4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Contribute
|
||||||
|
|
||||||
|
If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues)
|
||||||
|
or [create a pull request](https://github.com/Masterminds/semver/pulls).
|
||||||
44
vendor/github.com/Masterminds/semver/appveyor.yml
generated
vendored
Normal file
44
vendor/github.com/Masterminds/semver/appveyor.yml
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
version: build-{build}.{branch}
|
||||||
|
|
||||||
|
clone_folder: C:\gopath\src\github.com\Masterminds\semver
|
||||||
|
shallow_clone: true
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: C:\gopath
|
||||||
|
|
||||||
|
platform:
|
||||||
|
- x64
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -u gopkg.in/alecthomas/gometalinter.v1
|
||||||
|
- set PATH=%PATH%;%GOPATH%\bin
|
||||||
|
- gometalinter.v1.exe --install
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- go install -v ./...
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- "gometalinter.v1 \
|
||||||
|
--disable-all \
|
||||||
|
--enable deadcode \
|
||||||
|
--severity deadcode:error \
|
||||||
|
--enable gofmt \
|
||||||
|
--enable gosimple \
|
||||||
|
--enable ineffassign \
|
||||||
|
--enable misspell \
|
||||||
|
--enable vet \
|
||||||
|
--tests \
|
||||||
|
--vendor \
|
||||||
|
--deadline 60s \
|
||||||
|
./... || exit_code=1"
|
||||||
|
- "gometalinter.v1 \
|
||||||
|
--disable-all \
|
||||||
|
--enable golint \
|
||||||
|
--vendor \
|
||||||
|
--deadline 60s \
|
||||||
|
./... || :"
|
||||||
|
- go test -v
|
||||||
|
|
||||||
|
deploy: off
|
||||||
24
vendor/github.com/Masterminds/semver/collection.go
generated
vendored
Normal file
24
vendor/github.com/Masterminds/semver/collection.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package semver
|
||||||
|
|
||||||
|
// Collection is a collection of Version instances and implements the sort
|
||||||
|
// interface. See the sort package for more details.
|
||||||
|
// https://golang.org/pkg/sort/
|
||||||
|
type Collection []*Version
|
||||||
|
|
||||||
|
// Len returns the length of a collection. The number of Version instances
|
||||||
|
// on the slice.
|
||||||
|
func (c Collection) Len() int {
|
||||||
|
return len(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less is needed for the sort interface to compare two Version objects on the
|
||||||
|
// slice. If checks if one is less than the other.
|
||||||
|
func (c Collection) Less(i, j int) bool {
|
||||||
|
return c[i].LessThan(c[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap is needed for the sort interface to replace the Version objects
|
||||||
|
// at two different positions in the slice.
|
||||||
|
func (c Collection) Swap(i, j int) {
|
||||||
|
c[i], c[j] = c[j], c[i]
|
||||||
|
}
|
||||||
406
vendor/github.com/Masterminds/semver/constraints.go
generated
vendored
Normal file
406
vendor/github.com/Masterminds/semver/constraints.go
generated
vendored
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
package semver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constraints is one or more constraint that a semantic version can be
|
||||||
|
// checked against.
|
||||||
|
type Constraints struct {
|
||||||
|
constraints [][]*constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConstraint returns a Constraints instance that a Version instance can
|
||||||
|
// be checked against. If there is a parse error it will be returned.
|
||||||
|
func NewConstraint(c string) (*Constraints, error) {
|
||||||
|
|
||||||
|
// Rewrite - ranges into a comparison operation.
|
||||||
|
c = rewriteRange(c)
|
||||||
|
|
||||||
|
ors := strings.Split(c, "||")
|
||||||
|
or := make([][]*constraint, len(ors))
|
||||||
|
for k, v := range ors {
|
||||||
|
cs := strings.Split(v, ",")
|
||||||
|
result := make([]*constraint, len(cs))
|
||||||
|
for i, s := range cs {
|
||||||
|
pc, err := parseConstraint(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = pc
|
||||||
|
}
|
||||||
|
or[k] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
o := &Constraints{constraints: or}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tests if a version satisfies the constraints.
|
||||||
|
func (cs Constraints) Check(v *Version) bool {
|
||||||
|
// loop over the ORs and check the inner ANDs
|
||||||
|
for _, o := range cs.constraints {
|
||||||
|
joy := true
|
||||||
|
for _, c := range o {
|
||||||
|
if !c.check(v) {
|
||||||
|
joy = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if joy {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if a version satisfies a constraint. If not a slice of
|
||||||
|
// reasons for the failure are returned in addition to a bool.
|
||||||
|
func (cs Constraints) Validate(v *Version) (bool, []error) {
|
||||||
|
// loop over the ORs and check the inner ANDs
|
||||||
|
var e []error
|
||||||
|
for _, o := range cs.constraints {
|
||||||
|
joy := true
|
||||||
|
for _, c := range o {
|
||||||
|
if !c.check(v) {
|
||||||
|
em := fmt.Errorf(c.msg, v, c.orig)
|
||||||
|
e = append(e, em)
|
||||||
|
joy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if joy {
|
||||||
|
return true, []error{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, e
|
||||||
|
}
|
||||||
|
|
||||||
|
var constraintOps map[string]cfunc
|
||||||
|
var constraintMsg map[string]string
|
||||||
|
var constraintRegex *regexp.Regexp
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
constraintOps = map[string]cfunc{
|
||||||
|
"": constraintTildeOrEqual,
|
||||||
|
"=": constraintTildeOrEqual,
|
||||||
|
"!=": constraintNotEqual,
|
||||||
|
">": constraintGreaterThan,
|
||||||
|
"<": constraintLessThan,
|
||||||
|
">=": constraintGreaterThanEqual,
|
||||||
|
"=>": constraintGreaterThanEqual,
|
||||||
|
"<=": constraintLessThanEqual,
|
||||||
|
"=<": constraintLessThanEqual,
|
||||||
|
"~": constraintTilde,
|
||||||
|
"~>": constraintTilde,
|
||||||
|
"^": constraintCaret,
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintMsg = map[string]string{
|
||||||
|
"": "%s is not equal to %s",
|
||||||
|
"=": "%s is not equal to %s",
|
||||||
|
"!=": "%s is equal to %s",
|
||||||
|
">": "%s is less than or equal to %s",
|
||||||
|
"<": "%s is greater than or equal to %s",
|
||||||
|
">=": "%s is less than %s",
|
||||||
|
"=>": "%s is less than %s",
|
||||||
|
"<=": "%s is greater than %s",
|
||||||
|
"=<": "%s is greater than %s",
|
||||||
|
"~": "%s does not have same major and minor version as %s",
|
||||||
|
"~>": "%s does not have same major and minor version as %s",
|
||||||
|
"^": "%s does not have same major version as %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := make([]string, 0, len(constraintOps))
|
||||||
|
for k := range constraintOps {
|
||||||
|
ops = append(ops, regexp.QuoteMeta(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||||
|
`^\s*(%s)\s*(%s)\s*$`,
|
||||||
|
strings.Join(ops, "|"),
|
||||||
|
cvRegex))
|
||||||
|
|
||||||
|
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
|
||||||
|
`\s*(%s)\s+-\s+(%s)\s*`,
|
||||||
|
cvRegex, cvRegex))
|
||||||
|
}
|
||||||
|
|
||||||
|
// An individual constraint
|
||||||
|
type constraint struct {
|
||||||
|
// The callback function for the restraint. It performs the logic for
|
||||||
|
// the constraint.
|
||||||
|
function cfunc
|
||||||
|
|
||||||
|
msg string
|
||||||
|
|
||||||
|
// The version used in the constraint check. For example, if a constraint
|
||||||
|
// is '<= 2.0.0' the con a version instance representing 2.0.0.
|
||||||
|
con *Version
|
||||||
|
|
||||||
|
// The original parsed version (e.g., 4.x from != 4.x)
|
||||||
|
orig string
|
||||||
|
|
||||||
|
// When an x is used as part of the version (e.g., 1.x)
|
||||||
|
minorDirty bool
|
||||||
|
dirty bool
|
||||||
|
patchDirty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a version meets the constraint
|
||||||
|
func (c *constraint) check(v *Version) bool {
|
||||||
|
return c.function(v, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cfunc func(v *Version, c *constraint) bool
|
||||||
|
|
||||||
|
func parseConstraint(c string) (*constraint, error) {
|
||||||
|
m := constraintRegex.FindStringSubmatch(c)
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("improper constraint: %s", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
ver := m[2]
|
||||||
|
orig := ver
|
||||||
|
minorDirty := false
|
||||||
|
patchDirty := false
|
||||||
|
dirty := false
|
||||||
|
if isX(m[3]) {
|
||||||
|
ver = "0.0.0"
|
||||||
|
dirty = true
|
||||||
|
} else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" {
|
||||||
|
minorDirty = true
|
||||||
|
dirty = true
|
||||||
|
ver = fmt.Sprintf("%s.0.0%s", m[3], m[6])
|
||||||
|
} else if isX(strings.TrimPrefix(m[5], ".")) {
|
||||||
|
dirty = true
|
||||||
|
patchDirty = true
|
||||||
|
ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6])
|
||||||
|
}
|
||||||
|
|
||||||
|
con, err := NewVersion(ver)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
// The constraintRegex should catch any regex parsing errors. So,
|
||||||
|
// we should never get here.
|
||||||
|
return nil, errors.New("constraint Parser Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := &constraint{
|
||||||
|
function: constraintOps[m[1]],
|
||||||
|
msg: constraintMsg[m[1]],
|
||||||
|
con: con,
|
||||||
|
orig: orig,
|
||||||
|
minorDirty: minorDirty,
|
||||||
|
patchDirty: patchDirty,
|
||||||
|
dirty: dirty,
|
||||||
|
}
|
||||||
|
return cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constraint functions
|
||||||
|
func constraintNotEqual(v *Version, c *constraint) bool {
|
||||||
|
if c.dirty {
|
||||||
|
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.con.Major() != v.Major() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.con.Minor() != v.Minor() && !c.minorDirty {
|
||||||
|
return true
|
||||||
|
} else if c.minorDirty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !v.Equal(c.con)
|
||||||
|
}
|
||||||
|
|
||||||
|
func constraintGreaterThan(v *Version, c *constraint) bool {
|
||||||
|
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Compare(c.con) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func constraintLessThan(v *Version, c *constraint) bool {
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.dirty {
|
||||||
|
return v.Compare(c.con) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Major() > c.con.Major() {
|
||||||
|
return false
|
||||||
|
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
|
||||||
|
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Compare(c.con) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func constraintLessThanEqual(v *Version, c *constraint) bool {
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.dirty {
|
||||||
|
return v.Compare(c.con) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Major() > c.con.Major() {
|
||||||
|
return false
|
||||||
|
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~*, ~>* --> >= 0.0.0 (any)
|
||||||
|
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0
|
||||||
|
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0
|
||||||
|
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0
|
||||||
|
// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0
|
||||||
|
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
|
||||||
|
func constraintTilde(v *Version, c *constraint) bool {
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.LessThan(c.con) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~0.0.0 is a special case where all constraints are accepted. It's
|
||||||
|
// equivalent to >= 0.0.0.
|
||||||
|
if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 &&
|
||||||
|
!c.minorDirty && !c.patchDirty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Major() != c.con.Major() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Minor() != c.con.Minor() && !c.minorDirty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
|
||||||
|
// it's a straight =
|
||||||
|
func constraintTildeOrEqual(v *Version, c *constraint) bool {
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dirty {
|
||||||
|
c.msg = constraintMsg["~"]
|
||||||
|
return constraintTilde(v, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Equal(c.con)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ^* --> (any)
|
||||||
|
// ^2, ^2.x, ^2.x.x --> >=2.0.0, <3.0.0
|
||||||
|
// ^2.0, ^2.0.x --> >=2.0.0, <3.0.0
|
||||||
|
// ^1.2, ^1.2.x --> >=1.2.0, <2.0.0
|
||||||
|
// ^1.2.3 --> >=1.2.3, <2.0.0
|
||||||
|
// ^1.2.0 --> >=1.2.0, <2.0.0
|
||||||
|
func constraintCaret(v *Version, c *constraint) bool {
|
||||||
|
// If there is a pre-release on the version but the constraint isn't looking
|
||||||
|
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||||
|
// more details.
|
||||||
|
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.LessThan(c.con) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Major() != c.con.Major() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var constraintRangeRegex *regexp.Regexp
|
||||||
|
|
||||||
|
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
|
||||||
|
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
|
||||||
|
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
|
||||||
|
|
||||||
|
func isX(x string) bool {
|
||||||
|
switch x {
|
||||||
|
case "x", "*", "X":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewriteRange(i string) string {
|
||||||
|
m := constraintRangeRegex.FindAllStringSubmatch(i, -1)
|
||||||
|
if m == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
o := i
|
||||||
|
for _, v := range m {
|
||||||
|
t := fmt.Sprintf(">= %s, <= %s", v[1], v[11])
|
||||||
|
o = strings.Replace(o, v[0], t, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
||||||
115
vendor/github.com/Masterminds/semver/doc.go
generated
vendored
Normal file
115
vendor/github.com/Masterminds/semver/doc.go
generated
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go.
|
||||||
|
|
||||||
|
Specifically it provides the ability to:
|
||||||
|
|
||||||
|
* Parse semantic versions
|
||||||
|
* Sort semantic versions
|
||||||
|
* Check if a semantic version fits within a set of constraints
|
||||||
|
* Optionally work with a `v` prefix
|
||||||
|
|
||||||
|
Parsing Semantic Versions
|
||||||
|
|
||||||
|
To parse a semantic version use the `NewVersion` function. For example,
|
||||||
|
|
||||||
|
v, err := semver.NewVersion("1.2.3-beta.1+build345")
|
||||||
|
|
||||||
|
If there is an error the version wasn't parseable. The version object has methods
|
||||||
|
to get the parts of the version, compare it to other versions, convert the
|
||||||
|
version back into a string, and get the original string. For more details
|
||||||
|
please see the documentation at https://godoc.org/github.com/Masterminds/semver.
|
||||||
|
|
||||||
|
Sorting Semantic Versions
|
||||||
|
|
||||||
|
A set of versions can be sorted using the `sort` package from the standard library.
|
||||||
|
For example,
|
||||||
|
|
||||||
|
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
|
||||||
|
vs := make([]*semver.Version, len(raw))
|
||||||
|
for i, r := range raw {
|
||||||
|
v, err := semver.NewVersion(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error parsing version: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vs[i] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(semver.Collection(vs))
|
||||||
|
|
||||||
|
Checking Version Constraints
|
||||||
|
|
||||||
|
Checking a version against version constraints is one of the most featureful
|
||||||
|
parts of the package.
|
||||||
|
|
||||||
|
c, err := semver.NewConstraint(">= 1.2.3")
|
||||||
|
if err != nil {
|
||||||
|
// Handle constraint not being parseable.
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := semver.NewVersion("1.3")
|
||||||
|
if err != nil {
|
||||||
|
// Handle version not being parseable.
|
||||||
|
}
|
||||||
|
// Check if the version meets the constraints. The a variable will be true.
|
||||||
|
a := c.Check(v)
|
||||||
|
|
||||||
|
Basic Comparisons
|
||||||
|
|
||||||
|
There are two elements to the comparisons. First, a comparison string is a list
|
||||||
|
of comma separated and comparisons. These are then separated by || separated or
|
||||||
|
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
|
||||||
|
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
|
||||||
|
greater than or equal to 4.2.3.
|
||||||
|
|
||||||
|
The basic comparisons are:
|
||||||
|
|
||||||
|
* `=`: equal (aliased to no operator)
|
||||||
|
* `!=`: not equal
|
||||||
|
* `>`: greater than
|
||||||
|
* `<`: less than
|
||||||
|
* `>=`: greater than or equal to
|
||||||
|
* `<=`: less than or equal to
|
||||||
|
|
||||||
|
Hyphen Range Comparisons
|
||||||
|
|
||||||
|
There are multiple methods to handle ranges and the first is hyphens ranges.
|
||||||
|
These look like:
|
||||||
|
|
||||||
|
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
|
||||||
|
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
|
||||||
|
|
||||||
|
Wildcards In Comparisons
|
||||||
|
|
||||||
|
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
|
||||||
|
for all comparison operators. When used on the `=` operator it falls
|
||||||
|
back to the pack level comparison (see tilde below). For example,
|
||||||
|
|
||||||
|
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||||
|
* `>= 1.2.x` is equivalent to `>= 1.2.0`
|
||||||
|
* `<= 2.x` is equivalent to `<= 3`
|
||||||
|
* `*` is equivalent to `>= 0.0.0`
|
||||||
|
|
||||||
|
Tilde Range Comparisons (Patch)
|
||||||
|
|
||||||
|
The tilde (`~`) comparison operator is for patch level ranges when a minor
|
||||||
|
version is specified and major level changes when the minor number is missing.
|
||||||
|
For example,
|
||||||
|
|
||||||
|
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
|
||||||
|
* `~1` is equivalent to `>= 1, < 2`
|
||||||
|
* `~2.3` is equivalent to `>= 2.3, < 2.4`
|
||||||
|
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||||
|
* `~1.x` is equivalent to `>= 1, < 2`
|
||||||
|
|
||||||
|
Caret Range Comparisons (Major)
|
||||||
|
|
||||||
|
The caret (`^`) comparison operator is for major level changes. This is useful
|
||||||
|
when comparisons of API versions as a major change is API breaking. For example,
|
||||||
|
|
||||||
|
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
|
||||||
|
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
|
||||||
|
* `^2.3` is equivalent to `>= 2.3, < 3`
|
||||||
|
* `^2.x` is equivalent to `>= 2.0.0, < 3`
|
||||||
|
*/
|
||||||
|
package semver
|
||||||
421
vendor/github.com/Masterminds/semver/version.go
generated
vendored
Normal file
421
vendor/github.com/Masterminds/semver/version.go
generated
vendored
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package semver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The compiled version of the regex created at init() is cached here so it
|
||||||
|
// only needs to be created once.
|
||||||
|
var versionRegex *regexp.Regexp
|
||||||
|
var validPrereleaseRegex *regexp.Regexp
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidSemVer is returned a version is found to be invalid when
|
||||||
|
// being parsed.
|
||||||
|
ErrInvalidSemVer = errors.New("Invalid Semantic Version")
|
||||||
|
|
||||||
|
// ErrInvalidMetadata is returned when the metadata is an invalid format
|
||||||
|
ErrInvalidMetadata = errors.New("Invalid Metadata string")
|
||||||
|
|
||||||
|
// ErrInvalidPrerelease is returned when the pre-release is an invalid format
|
||||||
|
ErrInvalidPrerelease = errors.New("Invalid Prerelease string")
|
||||||
|
)
|
||||||
|
|
||||||
|
// SemVerRegex is the regular expression used to parse a semantic version.
|
||||||
|
const SemVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
|
||||||
|
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
|
||||||
|
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
|
||||||
|
|
||||||
|
// ValidPrerelease is the regular expression which validates
|
||||||
|
// both prerelease and metadata values.
|
||||||
|
const ValidPrerelease string = `^([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*)`
|
||||||
|
|
||||||
|
// Version represents a single semantic version.
|
||||||
|
type Version struct {
|
||||||
|
major, minor, patch int64
|
||||||
|
pre string
|
||||||
|
metadata string
|
||||||
|
original string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
versionRegex = regexp.MustCompile("^" + SemVerRegex + "$")
|
||||||
|
validPrereleaseRegex = regexp.MustCompile(ValidPrerelease)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVersion parses a given version and returns an instance of Version or
|
||||||
|
// an error if unable to parse the version.
|
||||||
|
func NewVersion(v string) (*Version, error) {
|
||||||
|
m := versionRegex.FindStringSubmatch(v)
|
||||||
|
if m == nil {
|
||||||
|
return nil, ErrInvalidSemVer
|
||||||
|
}
|
||||||
|
|
||||||
|
sv := &Version{
|
||||||
|
metadata: m[8],
|
||||||
|
pre: m[5],
|
||||||
|
original: v,
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp int64
|
||||||
|
temp, err := strconv.ParseInt(m[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||||
|
}
|
||||||
|
sv.major = temp
|
||||||
|
|
||||||
|
if m[2] != "" {
|
||||||
|
temp, err = strconv.ParseInt(strings.TrimPrefix(m[2], "."), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||||
|
}
|
||||||
|
sv.minor = temp
|
||||||
|
} else {
|
||||||
|
sv.minor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if m[3] != "" {
|
||||||
|
temp, err = strconv.ParseInt(strings.TrimPrefix(m[3], "."), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||||
|
}
|
||||||
|
sv.patch = temp
|
||||||
|
} else {
|
||||||
|
sv.patch = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return sv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParse parses a given version and panics on error.
|
||||||
|
func MustParse(v string) *Version {
|
||||||
|
sv, err := NewVersion(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts a Version object to a string.
|
||||||
|
// Note, if the original version contained a leading v this version will not.
|
||||||
|
// See the Original() method to retrieve the original value. Semantic Versions
|
||||||
|
// don't contain a leading v per the spec. Instead it's optional on
|
||||||
|
// implementation.
|
||||||
|
func (v *Version) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch)
|
||||||
|
if v.pre != "" {
|
||||||
|
fmt.Fprintf(&buf, "-%s", v.pre)
|
||||||
|
}
|
||||||
|
if v.metadata != "" {
|
||||||
|
fmt.Fprintf(&buf, "+%s", v.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original returns the original value passed in to be parsed.
|
||||||
|
func (v *Version) Original() string {
|
||||||
|
return v.original
|
||||||
|
}
|
||||||
|
|
||||||
|
// Major returns the major version.
|
||||||
|
func (v *Version) Major() int64 {
|
||||||
|
return v.major
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minor returns the minor version.
|
||||||
|
func (v *Version) Minor() int64 {
|
||||||
|
return v.minor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch returns the patch version.
|
||||||
|
func (v *Version) Patch() int64 {
|
||||||
|
return v.patch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prerelease returns the pre-release version.
|
||||||
|
func (v *Version) Prerelease() string {
|
||||||
|
return v.pre
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns the metadata on the version.
|
||||||
|
func (v *Version) Metadata() string {
|
||||||
|
return v.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// originalVPrefix returns the original 'v' prefix if any.
|
||||||
|
func (v *Version) originalVPrefix() string {
|
||||||
|
|
||||||
|
// Note, only lowercase v is supported as a prefix by the parser.
|
||||||
|
if v.original != "" && v.original[:1] == "v" {
|
||||||
|
return v.original[:1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncPatch produces the next patch version.
|
||||||
|
// If the current version does not have prerelease/metadata information,
|
||||||
|
// it unsets metadata and prerelease values, increments patch number.
|
||||||
|
// If the current version has any of prerelease or metadata information,
|
||||||
|
// it unsets both values and keeps curent patch value
|
||||||
|
func (v Version) IncPatch() Version {
|
||||||
|
vNext := v
|
||||||
|
// according to http://semver.org/#spec-item-9
|
||||||
|
// Pre-release versions have a lower precedence than the associated normal version.
|
||||||
|
// according to http://semver.org/#spec-item-10
|
||||||
|
// Build metadata SHOULD be ignored when determining version precedence.
|
||||||
|
if v.pre != "" {
|
||||||
|
vNext.metadata = ""
|
||||||
|
vNext.pre = ""
|
||||||
|
} else {
|
||||||
|
vNext.metadata = ""
|
||||||
|
vNext.pre = ""
|
||||||
|
vNext.patch = v.patch + 1
|
||||||
|
}
|
||||||
|
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||||
|
return vNext
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncMinor produces the next minor version.
|
||||||
|
// Sets patch to 0.
|
||||||
|
// Increments minor number.
|
||||||
|
// Unsets metadata.
|
||||||
|
// Unsets prerelease status.
|
||||||
|
func (v Version) IncMinor() Version {
|
||||||
|
vNext := v
|
||||||
|
vNext.metadata = ""
|
||||||
|
vNext.pre = ""
|
||||||
|
vNext.patch = 0
|
||||||
|
vNext.minor = v.minor + 1
|
||||||
|
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||||
|
return vNext
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncMajor produces the next major version.
|
||||||
|
// Sets patch to 0.
|
||||||
|
// Sets minor to 0.
|
||||||
|
// Increments major number.
|
||||||
|
// Unsets metadata.
|
||||||
|
// Unsets prerelease status.
|
||||||
|
func (v Version) IncMajor() Version {
|
||||||
|
vNext := v
|
||||||
|
vNext.metadata = ""
|
||||||
|
vNext.pre = ""
|
||||||
|
vNext.patch = 0
|
||||||
|
vNext.minor = 0
|
||||||
|
vNext.major = v.major + 1
|
||||||
|
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||||
|
return vNext
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrerelease defines the prerelease value.
|
||||||
|
// Value must not include the required 'hypen' prefix.
|
||||||
|
func (v Version) SetPrerelease(prerelease string) (Version, error) {
|
||||||
|
vNext := v
|
||||||
|
if len(prerelease) > 0 && !validPrereleaseRegex.MatchString(prerelease) {
|
||||||
|
return vNext, ErrInvalidPrerelease
|
||||||
|
}
|
||||||
|
vNext.pre = prerelease
|
||||||
|
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||||
|
return vNext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetadata defines metadata value.
|
||||||
|
// Value must not include the required 'plus' prefix.
|
||||||
|
func (v Version) SetMetadata(metadata string) (Version, error) {
|
||||||
|
vNext := v
|
||||||
|
if len(metadata) > 0 && !validPrereleaseRegex.MatchString(metadata) {
|
||||||
|
return vNext, ErrInvalidMetadata
|
||||||
|
}
|
||||||
|
vNext.metadata = metadata
|
||||||
|
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||||
|
return vNext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessThan tests if one version is less than another one.
|
||||||
|
func (v *Version) LessThan(o *Version) bool {
|
||||||
|
return v.Compare(o) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GreaterThan tests if one version is greater than another one.
|
||||||
|
func (v *Version) GreaterThan(o *Version) bool {
|
||||||
|
return v.Compare(o) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal tests if two versions are equal to each other.
|
||||||
|
// Note, versions can be equal with different metadata since metadata
|
||||||
|
// is not considered part of the comparable version.
|
||||||
|
func (v *Version) Equal(o *Version) bool {
|
||||||
|
return v.Compare(o) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare compares this version to another one. It returns -1, 0, or 1 if
|
||||||
|
// the version smaller, equal, or larger than the other version.
|
||||||
|
//
|
||||||
|
// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is
|
||||||
|
// lower than the version without a prerelease.
|
||||||
|
func (v *Version) Compare(o *Version) int {
|
||||||
|
// Compare the major, minor, and patch version for differences. If a
|
||||||
|
// difference is found return the comparison.
|
||||||
|
if d := compareSegment(v.Major(), o.Major()); d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
if d := compareSegment(v.Minor(), o.Minor()); d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
if d := compareSegment(v.Patch(), o.Patch()); d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point the major, minor, and patch versions are the same.
|
||||||
|
ps := v.pre
|
||||||
|
po := o.Prerelease()
|
||||||
|
|
||||||
|
if ps == "" && po == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if ps == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if po == "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return comparePrerelease(ps, po)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements JSON.Unmarshaler interface.
|
||||||
|
func (v *Version) UnmarshalJSON(b []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temp, err := NewVersion(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.major = temp.major
|
||||||
|
v.minor = temp.minor
|
||||||
|
v.patch = temp.patch
|
||||||
|
v.pre = temp.pre
|
||||||
|
v.metadata = temp.metadata
|
||||||
|
v.original = temp.original
|
||||||
|
temp = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements JSON.Marshaler interface.
|
||||||
|
func (v *Version) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareSegment(v, o int64) int {
|
||||||
|
if v < o {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v > o {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePrerelease(v, o string) int {
|
||||||
|
|
||||||
|
// split the prelease versions by their part. The separator, per the spec,
|
||||||
|
// is a .
|
||||||
|
sparts := strings.Split(v, ".")
|
||||||
|
oparts := strings.Split(o, ".")
|
||||||
|
|
||||||
|
// Find the longer length of the parts to know how many loop iterations to
|
||||||
|
// go through.
|
||||||
|
slen := len(sparts)
|
||||||
|
olen := len(oparts)
|
||||||
|
|
||||||
|
l := slen
|
||||||
|
if olen > slen {
|
||||||
|
l = olen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over each part of the prereleases to compare the differences.
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
// Since the lentgh of the parts can be different we need to create
|
||||||
|
// a placeholder. This is to avoid out of bounds issues.
|
||||||
|
stemp := ""
|
||||||
|
if i < slen {
|
||||||
|
stemp = sparts[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
otemp := ""
|
||||||
|
if i < olen {
|
||||||
|
otemp = oparts[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
d := comparePrePart(stemp, otemp)
|
||||||
|
if d != 0 {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reaching here means two versions are of equal value but have different
|
||||||
|
// metadata (the part following a +). They are not identical in string form
|
||||||
|
// but the version comparison finds them to be equal.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePrePart(s, o string) int {
|
||||||
|
// Fastpath if they are equal
|
||||||
|
if s == o {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// When s or o are empty we can use the other in an attempt to determine
|
||||||
|
// the response.
|
||||||
|
if s == "" {
|
||||||
|
if o != "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if o == "" {
|
||||||
|
if s != "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// When comparing strings "99" is greater than "103". To handle
|
||||||
|
// cases like this we need to detect numbers and compare them.
|
||||||
|
|
||||||
|
oi, n1 := strconv.ParseInt(o, 10, 64)
|
||||||
|
si, n2 := strconv.ParseInt(s, 10, 64)
|
||||||
|
|
||||||
|
// The case where both are strings compare the strings
|
||||||
|
if n1 != nil && n2 != nil {
|
||||||
|
if s > o {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
} else if n1 != nil {
|
||||||
|
// o is a string and s is a number
|
||||||
|
return -1
|
||||||
|
} else if n2 != nil {
|
||||||
|
// s is a string and o is a number
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// Both are numbers
|
||||||
|
if si > oi {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
|
||||||
|
}
|
||||||
9
vendor/github.com/ant0ine/go-json-rest/LICENSE
generated
vendored
Normal file
9
vendor/github.com/ant0ine/go-json-rest/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Copyright (c) 2013-2016 Antoine Imbert
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
236
vendor/github.com/ant0ine/go-json-rest/rest/access_log_apache.go
generated
vendored
Normal file
236
vendor/github.com/ant0ine/go-json-rest/rest/access_log_apache.go
generated
vendored
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO Future improvements:
|
||||||
|
// * support %{strftime}t ?
|
||||||
|
// * support %{<header>}o to print headers
|
||||||
|
|
||||||
|
// AccessLogFormat defines the format of the access log record.
|
||||||
|
// This implementation is a subset of Apache mod_log_config.
|
||||||
|
// (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html)
|
||||||
|
//
|
||||||
|
// %b content length in bytes, - if 0
|
||||||
|
// %B content length in bytes
|
||||||
|
// %D response elapsed time in microseconds
|
||||||
|
// %h remote address
|
||||||
|
// %H server protocol
|
||||||
|
// %l identd logname, not supported, -
|
||||||
|
// %m http method
|
||||||
|
// %P process id
|
||||||
|
// %q query string
|
||||||
|
// %r first line of the request
|
||||||
|
// %s status code
|
||||||
|
// %S status code preceeded by a terminal color
|
||||||
|
// %t time of the request
|
||||||
|
// %T response elapsed time in seconds, 3 decimals
|
||||||
|
// %u remote user, - if missing
|
||||||
|
// %{User-Agent}i user agent, - if missing
|
||||||
|
// %{Referer}i referer, - is missing
|
||||||
|
//
|
||||||
|
// Some predefined formats are provided as contants.
|
||||||
|
type AccessLogFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CommonLogFormat is the Common Log Format (CLF).
|
||||||
|
CommonLogFormat = "%h %l %u %t \"%r\" %s %b"
|
||||||
|
|
||||||
|
// CombinedLogFormat is the NCSA extended/combined log format.
|
||||||
|
CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\""
|
||||||
|
|
||||||
|
// DefaultLogFormat is the default format, colored output and response time, convenient for development.
|
||||||
|
DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessLogApacheMiddleware produces the access log following a format inspired by Apache
|
||||||
|
// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that should be in the wrapped
|
||||||
|
// middlewares. It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares.
|
||||||
|
type AccessLogApacheMiddleware struct {
|
||||||
|
|
||||||
|
// Logger points to the logger object used by this middleware, it defaults to
|
||||||
|
// log.New(os.Stderr, "", 0).
|
||||||
|
Logger *log.Logger
|
||||||
|
|
||||||
|
// Format defines the format of the access log record. See AccessLogFormat for the details.
|
||||||
|
// It defaults to DefaultLogFormat.
|
||||||
|
Format AccessLogFormat
|
||||||
|
|
||||||
|
textTemplate *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes AccessLogApacheMiddleware implement the Middleware interface.
|
||||||
|
func (mw *AccessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
// set the default Logger
|
||||||
|
if mw.Logger == nil {
|
||||||
|
mw.Logger = log.New(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default format
|
||||||
|
if mw.Format == "" {
|
||||||
|
mw.Format = DefaultLogFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.convertFormat()
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
util := &accessLogUtil{w, r}
|
||||||
|
|
||||||
|
mw.Logger.Print(mw.executeTextTemplate(util))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var apacheAdapter = strings.NewReplacer(
|
||||||
|
"%b", "{{.BytesWritten | dashIf0}}",
|
||||||
|
"%B", "{{.BytesWritten}}",
|
||||||
|
"%D", "{{.ResponseTime | microseconds}}",
|
||||||
|
"%h", "{{.ApacheRemoteAddr}}",
|
||||||
|
"%H", "{{.R.Proto}}",
|
||||||
|
"%l", "-",
|
||||||
|
"%m", "{{.R.Method}}",
|
||||||
|
"%P", "{{.Pid}}",
|
||||||
|
"%q", "{{.ApacheQueryString}}",
|
||||||
|
"%r", "{{.R.Method}} {{.R.URL.RequestURI}} {{.R.Proto}}",
|
||||||
|
"%s", "{{.StatusCode}}",
|
||||||
|
"%S", "\033[{{.StatusCode | statusCodeColor}}m{{.StatusCode}}",
|
||||||
|
"%t", "{{if .StartTime}}{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}{{end}}",
|
||||||
|
"%T", "{{if .ResponseTime}}{{.ResponseTime.Seconds | printf \"%.3f\"}}{{end}}",
|
||||||
|
"%u", "{{.RemoteUser | dashIfEmptyStr}}",
|
||||||
|
"%{User-Agent}i", "{{.R.UserAgent | dashIfEmptyStr}}",
|
||||||
|
"%{Referer}i", "{{.R.Referer | dashIfEmptyStr}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convert the Apache access log format into a text/template
|
||||||
|
func (mw *AccessLogApacheMiddleware) convertFormat() {
|
||||||
|
|
||||||
|
tmplText := apacheAdapter.Replace(string(mw.Format))
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"dashIfEmptyStr": func(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
"dashIf0": func(value int64) string {
|
||||||
|
if value == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", value)
|
||||||
|
},
|
||||||
|
"microseconds": func(dur *time.Duration) string {
|
||||||
|
if dur != nil {
|
||||||
|
return fmt.Sprintf("%d", dur.Nanoseconds()/1000)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
"statusCodeColor": func(statusCode int) string {
|
||||||
|
if statusCode >= 400 && statusCode < 500 {
|
||||||
|
return "1;33"
|
||||||
|
} else if statusCode >= 500 {
|
||||||
|
return "0;31"
|
||||||
|
}
|
||||||
|
return "0;32"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
mw.textTemplate, err = template.New("accessLog").Funcs(funcMap).Parse(tmplText)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the text template with the data derived from the request, and return a string.
|
||||||
|
func (mw *AccessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string {
|
||||||
|
buf := bytes.NewBufferString("")
|
||||||
|
err := mw.textTemplate.Execute(buf, util)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessLogUtil provides a collection of utility functions that devrive data from the Request object.
|
||||||
|
// This object is used to provide data to the Apache Style template and the the JSON log record.
|
||||||
|
type accessLogUtil struct {
|
||||||
|
W ResponseWriter
|
||||||
|
R *Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// As stored by the auth middlewares.
|
||||||
|
func (u *accessLogUtil) RemoteUser() string {
|
||||||
|
if u.R.Env["REMOTE_USER"] != nil {
|
||||||
|
return u.R.Env["REMOTE_USER"].(string)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If qs exists then return it with a leadin "?", apache log style.
|
||||||
|
func (u *accessLogUtil) ApacheQueryString() string {
|
||||||
|
if u.R.URL.RawQuery != "" {
|
||||||
|
return "?" + u.R.URL.RawQuery
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the request entered the timer middleware.
|
||||||
|
func (u *accessLogUtil) StartTime() *time.Time {
|
||||||
|
if u.R.Env["START_TIME"] != nil {
|
||||||
|
return u.R.Env["START_TIME"].(*time.Time)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If remoteAddr is set then return is without the port number, apache log style.
|
||||||
|
func (u *accessLogUtil) ApacheRemoteAddr() string {
|
||||||
|
remoteAddr := u.R.RemoteAddr
|
||||||
|
if remoteAddr != "" {
|
||||||
|
if ip, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// As recorded by the recorder middleware.
|
||||||
|
func (u *accessLogUtil) StatusCode() int {
|
||||||
|
if u.R.Env["STATUS_CODE"] != nil {
|
||||||
|
return u.R.Env["STATUS_CODE"].(int)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// As mesured by the timer middleware.
|
||||||
|
func (u *accessLogUtil) ResponseTime() *time.Duration {
|
||||||
|
if u.R.Env["ELAPSED_TIME"] != nil {
|
||||||
|
return u.R.Env["ELAPSED_TIME"].(*time.Duration)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process id.
|
||||||
|
func (u *accessLogUtil) Pid() int {
|
||||||
|
return os.Getpid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// As recorded by the recorder middleware.
|
||||||
|
func (u *accessLogUtil) BytesWritten() int64 {
|
||||||
|
if u.R.Env["BYTES_WRITTEN"] != nil {
|
||||||
|
return u.R.Env["BYTES_WRITTEN"].(int64)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
88
vendor/github.com/ant0ine/go-json-rest/rest/access_log_json.go
generated
vendored
Normal file
88
vendor/github.com/ant0ine/go-json-rest/rest/access_log_json.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessLogJsonMiddleware produces the access log with records written as JSON. This middleware
|
||||||
|
// depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped middlewares. It
|
||||||
|
// also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares.
|
||||||
|
type AccessLogJsonMiddleware struct {
|
||||||
|
|
||||||
|
// Logger points to the logger object used by this middleware, it defaults to
|
||||||
|
// log.New(os.Stderr, "", 0).
|
||||||
|
Logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes AccessLogJsonMiddleware implement the Middleware interface.
|
||||||
|
func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
// set the default Logger
|
||||||
|
if mw.Logger == nil {
|
||||||
|
mw.Logger = log.New(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
mw.Logger.Printf("%s", makeAccessLogJsonRecord(r).asJson())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessLogJsonRecord is the data structure used by AccessLogJsonMiddleware to create the JSON
|
||||||
|
// records. (Public for documentation only, no public method uses it)
|
||||||
|
type AccessLogJsonRecord struct {
|
||||||
|
Timestamp *time.Time
|
||||||
|
StatusCode int
|
||||||
|
ResponseTime *time.Duration
|
||||||
|
HttpMethod string
|
||||||
|
RequestURI string
|
||||||
|
RemoteUser string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord {
|
||||||
|
|
||||||
|
var timestamp *time.Time
|
||||||
|
if r.Env["START_TIME"] != nil {
|
||||||
|
timestamp = r.Env["START_TIME"].(*time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusCode int
|
||||||
|
if r.Env["STATUS_CODE"] != nil {
|
||||||
|
statusCode = r.Env["STATUS_CODE"].(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseTime *time.Duration
|
||||||
|
if r.Env["ELAPSED_TIME"] != nil {
|
||||||
|
responseTime = r.Env["ELAPSED_TIME"].(*time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteUser string
|
||||||
|
if r.Env["REMOTE_USER"] != nil {
|
||||||
|
remoteUser = r.Env["REMOTE_USER"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AccessLogJsonRecord{
|
||||||
|
Timestamp: timestamp,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
ResponseTime: responseTime,
|
||||||
|
HttpMethod: r.Method,
|
||||||
|
RequestURI: r.URL.RequestURI(),
|
||||||
|
RemoteUser: remoteUser,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AccessLogJsonRecord) asJson() []byte {
|
||||||
|
b, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
83
vendor/github.com/ant0ine/go-json-rest/rest/api.go
generated
vendored
Normal file
83
vendor/github.com/ant0ine/go-json-rest/rest/api.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Api defines a stack of Middlewares and an App.
|
||||||
|
type Api struct {
|
||||||
|
stack []Middleware
|
||||||
|
app App
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApi makes a new Api object. The Middleware stack is empty, and the App is nil.
|
||||||
|
func NewApi() *Api {
|
||||||
|
return &Api{
|
||||||
|
stack: []Middleware{},
|
||||||
|
app: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use pushes one or multiple middlewares to the stack for middlewares
|
||||||
|
// maintained in the Api object.
|
||||||
|
func (api *Api) Use(middlewares ...Middleware) {
|
||||||
|
api.stack = append(api.stack, middlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetApp sets the App in the Api object.
|
||||||
|
func (api *Api) SetApp(app App) {
|
||||||
|
api.app = app
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHandler wraps all the Middlewares of the stack and the App together, and returns an
|
||||||
|
// http.Handler ready to be used. If the Middleware stack is empty the App is used directly. If the
|
||||||
|
// App is nil, a HandlerFunc that does nothing is used instead.
|
||||||
|
func (api *Api) MakeHandler() http.Handler {
|
||||||
|
var appFunc HandlerFunc
|
||||||
|
if api.app != nil {
|
||||||
|
appFunc = api.app.AppFunc()
|
||||||
|
} else {
|
||||||
|
appFunc = func(w ResponseWriter, r *Request) {}
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(
|
||||||
|
adapterFunc(
|
||||||
|
WrapMiddlewares(api.stack, appFunc),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines a stack of middlewares convenient for development. Among other things:
|
||||||
|
// console friendly logging, JSON indentation, error stack strace in the response.
|
||||||
|
var DefaultDevStack = []Middleware{
|
||||||
|
&AccessLogApacheMiddleware{},
|
||||||
|
&TimerMiddleware{},
|
||||||
|
&RecorderMiddleware{},
|
||||||
|
&PoweredByMiddleware{},
|
||||||
|
&RecoverMiddleware{
|
||||||
|
EnableResponseStackTrace: true,
|
||||||
|
},
|
||||||
|
&JsonIndentMiddleware{},
|
||||||
|
&ContentTypeCheckerMiddleware{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines a stack of middlewares convenient for production. Among other things:
|
||||||
|
// Apache CombinedLogFormat logging, gzip compression.
|
||||||
|
var DefaultProdStack = []Middleware{
|
||||||
|
&AccessLogApacheMiddleware{
|
||||||
|
Format: CombinedLogFormat,
|
||||||
|
},
|
||||||
|
&TimerMiddleware{},
|
||||||
|
&RecorderMiddleware{},
|
||||||
|
&PoweredByMiddleware{},
|
||||||
|
&RecoverMiddleware{},
|
||||||
|
&GzipMiddleware{},
|
||||||
|
&ContentTypeCheckerMiddleware{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines a stack of middlewares that should be common to most of the middleware stacks.
|
||||||
|
var DefaultCommonStack = []Middleware{
|
||||||
|
&TimerMiddleware{},
|
||||||
|
&RecorderMiddleware{},
|
||||||
|
&PoweredByMiddleware{},
|
||||||
|
&RecoverMiddleware{},
|
||||||
|
}
|
||||||
100
vendor/github.com/ant0ine/go-json-rest/rest/auth_basic.go
generated
vendored
Normal file
100
vendor/github.com/ant0ine/go-json-rest/rest/auth_basic.go
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, a 401 HTTP response
|
||||||
|
//is returned. On success, the wrapped middleware is called, and the userId is made available as
|
||||||
|
// request.Env["REMOTE_USER"].(string)
|
||||||
|
type AuthBasicMiddleware struct {
|
||||||
|
|
||||||
|
// Realm name to display to the user. Required.
|
||||||
|
Realm string
|
||||||
|
|
||||||
|
// Callback function that should perform the authentication of the user based on userId and
|
||||||
|
// password. Must return true on success, false on failure. Required.
|
||||||
|
Authenticator func(userId string, password string) bool
|
||||||
|
|
||||||
|
// Callback function that should perform the authorization of the authenticated user. Called
|
||||||
|
// only after an authentication success. Must return true on success, false on failure.
|
||||||
|
// Optional, default to success.
|
||||||
|
Authorizator func(userId string, request *Request) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes AuthBasicMiddleware implement the Middleware interface.
|
||||||
|
func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
if mw.Realm == "" {
|
||||||
|
log.Fatal("Realm is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mw.Authenticator == nil {
|
||||||
|
log.Fatal("Authenticator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mw.Authorizator == nil {
|
||||||
|
mw.Authorizator = func(userId string, request *Request) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(writer ResponseWriter, request *Request) {
|
||||||
|
|
||||||
|
authHeader := request.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
mw.unauthorized(writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
providedUserId, providedPassword, err := mw.decodeBasicAuthHeader(authHeader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
Error(writer, "Invalid authentication", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mw.Authenticator(providedUserId, providedPassword) {
|
||||||
|
mw.unauthorized(writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mw.Authorizator(providedUserId, request) {
|
||||||
|
mw.unauthorized(writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Env["REMOTE_USER"] = providedUserId
|
||||||
|
|
||||||
|
handler(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *AuthBasicMiddleware) unauthorized(writer ResponseWriter) {
|
||||||
|
writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm)
|
||||||
|
Error(writer, "Not Authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *AuthBasicMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) {
|
||||||
|
|
||||||
|
parts := strings.SplitN(header, " ", 2)
|
||||||
|
if !(len(parts) == 2 && parts[0] == "Basic") {
|
||||||
|
return "", "", errors.New("Invalid authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.New("Invalid base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := strings.SplitN(string(decoded), ":", 2)
|
||||||
|
if len(creds) != 2 {
|
||||||
|
return "", "", errors.New("Invalid authentication")
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds[0], creds[1], nil
|
||||||
|
}
|
||||||
40
vendor/github.com/ant0ine/go-json-rest/rest/content_type_checker.go
generated
vendored
Normal file
40
vendor/github.com/ant0ine/go-json-rest/rest/content_type_checker.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContentTypeCheckerMiddleware verifies the request Content-Type header and returns a
|
||||||
|
// StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. The expected
|
||||||
|
// Content-Type is 'application/json' if the content is non-null. Note: If a charset parameter
|
||||||
|
// exists, it MUST be UTF-8.
|
||||||
|
type ContentTypeCheckerMiddleware struct{}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface.
|
||||||
|
func (mw *ContentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
mediatype, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
|
charset, ok := params["charset"]
|
||||||
|
if !ok {
|
||||||
|
charset = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// per net/http doc, means that the length is known and non-null
|
||||||
|
if r.ContentLength > 0 &&
|
||||||
|
!(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") {
|
||||||
|
|
||||||
|
Error(w,
|
||||||
|
"Bad Content-Type or charset, expected 'application/json'",
|
||||||
|
http.StatusUnsupportedMediaType,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the wrapped handler
|
||||||
|
handler(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
vendor/github.com/ant0ine/go-json-rest/rest/cors.go
generated
vendored
Normal file
135
vendor/github.com/ant0ine/go-json-rest/rest/cors.go
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Possible improvements:
|
||||||
|
// If AllowedMethods["*"] then Access-Control-Allow-Methods is set to the requested methods
|
||||||
|
// If AllowedHeaderss["*"] then Access-Control-Allow-Headers is set to the requested headers
|
||||||
|
// Put some presets in AllowedHeaders
|
||||||
|
// Put some presets in AccessControlExposeHeaders
|
||||||
|
|
||||||
|
// CorsMiddleware provides a configurable CORS implementation.
|
||||||
|
type CorsMiddleware struct {
|
||||||
|
allowedMethods map[string]bool
|
||||||
|
allowedMethodsCsv string
|
||||||
|
allowedHeaders map[string]bool
|
||||||
|
allowedHeadersCsv string
|
||||||
|
|
||||||
|
// Reject non CORS requests if true. See CorsInfo.IsCors.
|
||||||
|
RejectNonCorsRequests bool
|
||||||
|
|
||||||
|
// Function excecuted for every CORS requests to validate the Origin. (Required)
|
||||||
|
// Must return true if valid, false if invalid.
|
||||||
|
// For instance: simple equality, regexp, DB lookup, ...
|
||||||
|
OriginValidator func(origin string, request *Request) bool
|
||||||
|
|
||||||
|
// List of allowed HTTP methods. Note that the comparison will be made in
|
||||||
|
// uppercase to avoid common mistakes. And that the
|
||||||
|
// Access-Control-Allow-Methods response header also uses uppercase.
|
||||||
|
// (see CorsInfo.AccessControlRequestMethod)
|
||||||
|
AllowedMethods []string
|
||||||
|
|
||||||
|
// List of allowed HTTP Headers. Note that the comparison will be made with
|
||||||
|
// noarmalized names (http.CanonicalHeaderKey). And that the response header
|
||||||
|
// also uses normalized names.
|
||||||
|
// (see CorsInfo.AccessControlRequestHeaders)
|
||||||
|
AllowedHeaders []string
|
||||||
|
|
||||||
|
// List of headers used to set the Access-Control-Expose-Headers header.
|
||||||
|
AccessControlExposeHeaders []string
|
||||||
|
|
||||||
|
// User to se the Access-Control-Allow-Credentials response header.
|
||||||
|
AccessControlAllowCredentials bool
|
||||||
|
|
||||||
|
// Used to set the Access-Control-Max-Age response header, in seconds.
|
||||||
|
AccessControlMaxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes CorsMiddleware implement the Middleware interface.
|
||||||
|
func (mw *CorsMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
// precompute as much as possible at init time
|
||||||
|
|
||||||
|
mw.allowedMethods = map[string]bool{}
|
||||||
|
normedMethods := []string{}
|
||||||
|
for _, allowedMethod := range mw.AllowedMethods {
|
||||||
|
normed := strings.ToUpper(allowedMethod)
|
||||||
|
mw.allowedMethods[normed] = true
|
||||||
|
normedMethods = append(normedMethods, normed)
|
||||||
|
}
|
||||||
|
mw.allowedMethodsCsv = strings.Join(normedMethods, ",")
|
||||||
|
|
||||||
|
mw.allowedHeaders = map[string]bool{}
|
||||||
|
normedHeaders := []string{}
|
||||||
|
for _, allowedHeader := range mw.AllowedHeaders {
|
||||||
|
normed := http.CanonicalHeaderKey(allowedHeader)
|
||||||
|
mw.allowedHeaders[normed] = true
|
||||||
|
normedHeaders = append(normedHeaders, normed)
|
||||||
|
}
|
||||||
|
mw.allowedHeadersCsv = strings.Join(normedHeaders, ",")
|
||||||
|
|
||||||
|
return func(writer ResponseWriter, request *Request) {
|
||||||
|
|
||||||
|
corsInfo := request.GetCorsInfo()
|
||||||
|
|
||||||
|
// non CORS requests
|
||||||
|
if !corsInfo.IsCors {
|
||||||
|
if mw.RejectNonCorsRequests {
|
||||||
|
Error(writer, "Non CORS request", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// continue, execute the wrapped middleware
|
||||||
|
handler(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the Origin
|
||||||
|
if mw.OriginValidator(corsInfo.Origin, request) == false {
|
||||||
|
Error(writer, "Invalid Origin", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if corsInfo.IsPreflight {
|
||||||
|
|
||||||
|
// check the request methods
|
||||||
|
if mw.allowedMethods[corsInfo.AccessControlRequestMethod] == false {
|
||||||
|
Error(writer, "Invalid Preflight Request", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the request headers
|
||||||
|
for _, requestedHeader := range corsInfo.AccessControlRequestHeaders {
|
||||||
|
if mw.allowedHeaders[requestedHeader] == false {
|
||||||
|
Error(writer, "Invalid Preflight Request", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Access-Control-Allow-Methods", mw.allowedMethodsCsv)
|
||||||
|
writer.Header().Set("Access-Control-Allow-Headers", mw.allowedHeadersCsv)
|
||||||
|
writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin)
|
||||||
|
if mw.AccessControlAllowCredentials == true {
|
||||||
|
writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
writer.Header().Set("Access-Control-Max-Age", strconv.Itoa(mw.AccessControlMaxAge))
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-preflight requests
|
||||||
|
for _, exposed := range mw.AccessControlExposeHeaders {
|
||||||
|
writer.Header().Add("Access-Control-Expose-Headers", exposed)
|
||||||
|
}
|
||||||
|
writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin)
|
||||||
|
if mw.AccessControlAllowCredentials == true {
|
||||||
|
writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
// continure, execute the wrapped middleware
|
||||||
|
handler(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
47
vendor/github.com/ant0ine/go-json-rest/rest/doc.go
generated
vendored
Normal file
47
vendor/github.com/ant0ine/go-json-rest/rest/doc.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// A quick and easy way to setup a RESTful JSON API
|
||||||
|
//
|
||||||
|
// http://ant0ine.github.io/go-json-rest/
|
||||||
|
//
|
||||||
|
// Go-Json-Rest is a thin layer on top of net/http that helps building RESTful JSON APIs easily.
|
||||||
|
// It provides fast and scalable request routing using a Trie based implementation, helpers to deal
|
||||||
|
// with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip,
|
||||||
|
// Status, ...
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "github.com/ant0ine/go-json-rest/rest"
|
||||||
|
// "log"
|
||||||
|
// "net/http"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// type User struct {
|
||||||
|
// Id string
|
||||||
|
// Name string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func GetUser(w rest.ResponseWriter, req *rest.Request) {
|
||||||
|
// user := User{
|
||||||
|
// Id: req.PathParam("id"),
|
||||||
|
// Name: "Antoine",
|
||||||
|
// }
|
||||||
|
// w.WriteJson(&user)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// api := rest.NewApi()
|
||||||
|
// api.Use(rest.DefaultDevStack...)
|
||||||
|
// router, err := rest.MakeRouter(
|
||||||
|
// rest.Get("/users/:id", GetUser),
|
||||||
|
// )
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// api.SetApp(router)
|
||||||
|
// log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
package rest
|
||||||
132
vendor/github.com/ant0ine/go-json-rest/rest/gzip.go
generated
vendored
Normal file
132
vendor/github.com/ant0ine/go-json-rest/rest/gzip.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GzipMiddleware is responsible for compressing the payload with gzip and setting the proper
|
||||||
|
// headers when supported by the client. It must be wrapped by TimerMiddleware for the
|
||||||
|
// compression time to be captured. And It must be wrapped by RecorderMiddleware for the
|
||||||
|
// compressed BYTES_WRITTEN to be captured.
|
||||||
|
type GzipMiddleware struct{}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes GzipMiddleware implement the Middleware interface.
|
||||||
|
func (mw *GzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
// gzip support enabled
|
||||||
|
canGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
|
// client accepts gzip ?
|
||||||
|
writer := &gzipResponseWriter{w, false, canGzip, nil}
|
||||||
|
defer func() {
|
||||||
|
// need to close gzip writer
|
||||||
|
if writer.gzipWriter != nil {
|
||||||
|
writer.gzipWriter.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// call the handler with the wrapped writer
|
||||||
|
h(writer, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private responseWriter intantiated by the gzip middleware.
|
||||||
|
// It encodes the payload with gzip and set the proper headers.
|
||||||
|
// It implements the following interfaces:
|
||||||
|
// ResponseWriter
|
||||||
|
// http.ResponseWriter
|
||||||
|
// http.Flusher
|
||||||
|
// http.CloseNotifier
|
||||||
|
// http.Hijacker
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
ResponseWriter
|
||||||
|
wroteHeader bool
|
||||||
|
canGzip bool
|
||||||
|
gzipWriter *gzip.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the right headers for gzip encoded responses.
|
||||||
|
func (w *gzipResponseWriter) WriteHeader(code int) {
|
||||||
|
|
||||||
|
// Always set the Vary header, even if this particular request
|
||||||
|
// is not gzipped.
|
||||||
|
w.Header().Add("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
if w.canGzip {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local Write is called.
|
||||||
|
func (w *gzipResponseWriter) WriteJson(v interface{}) error {
|
||||||
|
b, err := w.EncodeJson(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Flush.
|
||||||
|
// Provided in order to implement the http.Flusher interface.
|
||||||
|
func (w *gzipResponseWriter) Flush() {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
flusher := w.ResponseWriter.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent CloseNotify.
|
||||||
|
// Provided in order to implement the http.CloseNotifier interface.
|
||||||
|
func (w *gzipResponseWriter) CloseNotify() <-chan bool {
|
||||||
|
notifier := w.ResponseWriter.(http.CloseNotifier)
|
||||||
|
return notifier.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Hijacker interface.
|
||||||
|
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker := w.ResponseWriter.(http.Hijacker)
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and encode the payload if necessary.
|
||||||
|
// Provided in order to implement the http.ResponseWriter interface.
|
||||||
|
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := w.ResponseWriter.(http.ResponseWriter)
|
||||||
|
|
||||||
|
if w.canGzip {
|
||||||
|
// Write can be called multiple times for a given response.
|
||||||
|
// (see the streaming example:
|
||||||
|
// https://github.com/ant0ine/go-json-rest-examples/tree/master/streaming)
|
||||||
|
// The gzipWriter is instantiated only once, and flushed after
|
||||||
|
// each write.
|
||||||
|
if w.gzipWriter == nil {
|
||||||
|
w.gzipWriter = gzip.NewWriter(writer)
|
||||||
|
}
|
||||||
|
count, errW := w.gzipWriter.Write(b)
|
||||||
|
errF := w.gzipWriter.Flush()
|
||||||
|
if errW != nil {
|
||||||
|
return count, errW
|
||||||
|
}
|
||||||
|
if errF != nil {
|
||||||
|
return count, errF
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.Write(b)
|
||||||
|
}
|
||||||
53
vendor/github.com/ant0ine/go-json-rest/rest/if.go
generated
vendored
Normal file
53
vendor/github.com/ant0ine/go-json-rest/rest/if.go
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IfMiddleware evaluates at runtime a condition based on the current request, and decides to
|
||||||
|
// execute one of the other Middleware based on this boolean.
|
||||||
|
type IfMiddleware struct {
|
||||||
|
|
||||||
|
// Runtime condition that decides of the execution of IfTrue of IfFalse.
|
||||||
|
Condition func(r *Request) bool
|
||||||
|
|
||||||
|
// Middleware to run when the condition is true. Note that the middleware is initialized
|
||||||
|
// weather if will be used or not. (Optional, pass-through if not set)
|
||||||
|
IfTrue Middleware
|
||||||
|
|
||||||
|
// Middleware to run when the condition is false. Note that the middleware is initialized
|
||||||
|
// weather if will be used or not. (Optional, pass-through if not set)
|
||||||
|
IfFalse Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes TimerMiddleware implement the Middleware interface.
|
||||||
|
func (mw *IfMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
if mw.Condition == nil {
|
||||||
|
log.Fatal("IfMiddleware Condition is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ifTrueHandler HandlerFunc
|
||||||
|
if mw.IfTrue != nil {
|
||||||
|
ifTrueHandler = mw.IfTrue.MiddlewareFunc(h)
|
||||||
|
} else {
|
||||||
|
ifTrueHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
var ifFalseHandler HandlerFunc
|
||||||
|
if mw.IfFalse != nil {
|
||||||
|
ifFalseHandler = mw.IfFalse.MiddlewareFunc(h)
|
||||||
|
} else {
|
||||||
|
ifFalseHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
if mw.Condition(r) {
|
||||||
|
ifTrueHandler(w, r)
|
||||||
|
} else {
|
||||||
|
ifFalseHandler(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
113
vendor/github.com/ant0ine/go-json-rest/rest/json_indent.go
generated
vendored
Normal file
113
vendor/github.com/ant0ine/go-json-rest/rest/json_indent.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JsonIndentMiddleware provides JSON encoding with indentation.
|
||||||
|
// It could be convenient to use it during development.
|
||||||
|
// It works by "subclassing" the responseWriter provided by the wrapping middleware,
|
||||||
|
// replacing the writer.EncodeJson and writer.WriteJson implementations,
|
||||||
|
// and making the parent implementations ignored.
|
||||||
|
type JsonIndentMiddleware struct {
|
||||||
|
|
||||||
|
// prefix string, as in json.MarshalIndent
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
// indentation string, as in json.MarshalIndent
|
||||||
|
Indent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes JsonIndentMiddleware implement the Middleware interface.
|
||||||
|
func (mw *JsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
if mw.Indent == "" {
|
||||||
|
mw.Indent = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
writer := &jsonIndentResponseWriter{w, false, mw.Prefix, mw.Indent}
|
||||||
|
// call the wrapped handler
|
||||||
|
handler(writer, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private responseWriter intantiated by the middleware.
|
||||||
|
// It implements the following interfaces:
|
||||||
|
// ResponseWriter
|
||||||
|
// http.ResponseWriter
|
||||||
|
// http.Flusher
|
||||||
|
// http.CloseNotifier
|
||||||
|
// http.Hijacker
|
||||||
|
type jsonIndentResponseWriter struct {
|
||||||
|
ResponseWriter
|
||||||
|
wroteHeader bool
|
||||||
|
prefix string
|
||||||
|
indent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the parent EncodeJson to provide indentation.
|
||||||
|
func (w *jsonIndentResponseWriter) EncodeJson(v interface{}) ([]byte, error) {
|
||||||
|
b, err := json.MarshalIndent(v, w.prefix, w.indent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local EncodeJson and local Write are called.
|
||||||
|
// Does not call the parent WriteJson.
|
||||||
|
func (w *jsonIndentResponseWriter) WriteJson(v interface{}) error {
|
||||||
|
b, err := w.EncodeJson(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent WriteHeader.
|
||||||
|
func (w *jsonIndentResponseWriter) WriteHeader(code int) {
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Flush.
|
||||||
|
// Provided in order to implement the http.Flusher interface.
|
||||||
|
func (w *jsonIndentResponseWriter) Flush() {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
flusher := w.ResponseWriter.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent CloseNotify.
|
||||||
|
// Provided in order to implement the http.CloseNotifier interface.
|
||||||
|
func (w *jsonIndentResponseWriter) CloseNotify() <-chan bool {
|
||||||
|
notifier := w.ResponseWriter.(http.CloseNotifier)
|
||||||
|
return notifier.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Hijacker interface.
|
||||||
|
func (w *jsonIndentResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker := w.ResponseWriter.(http.Hijacker)
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Write.
|
||||||
|
// Provided in order to implement the http.ResponseWriter interface.
|
||||||
|
func (w *jsonIndentResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
writer := w.ResponseWriter.(http.ResponseWriter)
|
||||||
|
return writer.Write(b)
|
||||||
|
}
|
||||||
116
vendor/github.com/ant0ine/go-json-rest/rest/jsonp.go
generated
vendored
Normal file
116
vendor/github.com/ant0ine/go-json-rest/rest/jsonp.go
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JsonpMiddleware provides JSONP responses on demand, based on the presence
|
||||||
|
// of a query string argument specifying the callback name.
|
||||||
|
type JsonpMiddleware struct {
|
||||||
|
|
||||||
|
// Name of the query string parameter used to specify the
|
||||||
|
// the name of the JS callback used for the padding.
|
||||||
|
// Defaults to "callback".
|
||||||
|
CallbackNameKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc returns a HandlerFunc that implements the middleware.
|
||||||
|
func (mw *JsonpMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
if mw.CallbackNameKey == "" {
|
||||||
|
mw.CallbackNameKey = "callback"
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
callbackName := r.URL.Query().Get(mw.CallbackNameKey)
|
||||||
|
// TODO validate the callbackName ?
|
||||||
|
|
||||||
|
if callbackName != "" {
|
||||||
|
// the client request JSONP, instantiate JsonpMiddleware.
|
||||||
|
writer := &jsonpResponseWriter{w, false, callbackName}
|
||||||
|
// call the handler with the wrapped writer
|
||||||
|
h(writer, r)
|
||||||
|
} else {
|
||||||
|
// do nothing special
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private responseWriter intantiated by the JSONP middleware.
|
||||||
|
// It adds the padding to the payload and set the proper headers.
|
||||||
|
// It implements the following interfaces:
|
||||||
|
// ResponseWriter
|
||||||
|
// http.ResponseWriter
|
||||||
|
// http.Flusher
|
||||||
|
// http.CloseNotifier
|
||||||
|
// http.Hijacker
|
||||||
|
type jsonpResponseWriter struct {
|
||||||
|
ResponseWriter
|
||||||
|
wroteHeader bool
|
||||||
|
callbackName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the Content-Type to be text/javascript
|
||||||
|
func (w *jsonpResponseWriter) WriteHeader(code int) {
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
|
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local Write is called.
|
||||||
|
func (w *jsonpResponseWriter) WriteJson(v interface{}) error {
|
||||||
|
b, err := w.EncodeJson(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// JSONP security fix (http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/)
|
||||||
|
w.Header().Set("Content-Disposition", "filename=f.txt")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Write([]byte("/**/" + w.callbackName + "("))
|
||||||
|
w.Write(b)
|
||||||
|
w.Write([]byte(")"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Flush.
|
||||||
|
// Provided in order to implement the http.Flusher interface.
|
||||||
|
func (w *jsonpResponseWriter) Flush() {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
flusher := w.ResponseWriter.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent CloseNotify.
|
||||||
|
// Provided in order to implement the http.CloseNotifier interface.
|
||||||
|
func (w *jsonpResponseWriter) CloseNotify() <-chan bool {
|
||||||
|
notifier := w.ResponseWriter.(http.CloseNotifier)
|
||||||
|
return notifier.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Hijacker interface.
|
||||||
|
func (w *jsonpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker := w.ResponseWriter.(http.Hijacker)
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called.
|
||||||
|
// Provided in order to implement the http.ResponseWriter interface.
|
||||||
|
func (w *jsonpResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := w.ResponseWriter.(http.ResponseWriter)
|
||||||
|
|
||||||
|
return writer.Write(b)
|
||||||
|
}
|
||||||
72
vendor/github.com/ant0ine/go-json-rest/rest/middleware.go
generated
vendored
Normal file
72
vendor/github.com/ant0ine/go-json-rest/rest/middleware.go
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc.
|
||||||
|
type HandlerFunc func(ResponseWriter, *Request)
|
||||||
|
|
||||||
|
// App defines the interface that an object should implement to be used as an app in this framework
|
||||||
|
// stack. The App is the top element of the stack, the other elements being middlewares.
|
||||||
|
type App interface {
|
||||||
|
AppFunc() HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppSimple is an adapter type that makes it easy to write an App with a simple function.
|
||||||
|
// eg: rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { ... }))
|
||||||
|
type AppSimple HandlerFunc
|
||||||
|
|
||||||
|
// AppFunc makes AppSimple implement the App interface.
|
||||||
|
func (as AppSimple) AppFunc() HandlerFunc {
|
||||||
|
return HandlerFunc(as)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and
|
||||||
|
// be used in the middleware stack.
|
||||||
|
type Middleware interface {
|
||||||
|
MiddlewareFunc(handler HandlerFunc) HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareSimple is an adapter type that makes it easy to write a Middleware with a simple
|
||||||
|
// function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { ... }))
|
||||||
|
type MiddlewareSimple func(handler HandlerFunc) HandlerFunc
|
||||||
|
|
||||||
|
// MiddlewareFunc makes MiddlewareSimple implement the Middleware interface.
|
||||||
|
func (ms MiddlewareSimple) MiddlewareFunc(handler HandlerFunc) HandlerFunc {
|
||||||
|
return ms(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc
|
||||||
|
// ready to be executed. This can be used to wrap a set of middlewares, post routing, on a per Route
|
||||||
|
// basis.
|
||||||
|
func WrapMiddlewares(middlewares []Middleware, handler HandlerFunc) HandlerFunc {
|
||||||
|
wrapped := handler
|
||||||
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
|
wrapped = middlewares[i].MiddlewareFunc(wrapped)
|
||||||
|
}
|
||||||
|
return wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the transition between net/http and go-json-rest objects.
|
||||||
|
// It intanciates the rest.Request and rest.ResponseWriter, ...
|
||||||
|
func adapterFunc(handler HandlerFunc) http.HandlerFunc {
|
||||||
|
|
||||||
|
return func(origWriter http.ResponseWriter, origRequest *http.Request) {
|
||||||
|
|
||||||
|
// instantiate the rest objects
|
||||||
|
request := &Request{
|
||||||
|
origRequest,
|
||||||
|
nil,
|
||||||
|
map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := &responseWriter{
|
||||||
|
origWriter,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the wrapped handler
|
||||||
|
handler(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
29
vendor/github.com/ant0ine/go-json-rest/rest/powered_by.go
generated
vendored
Normal file
29
vendor/github.com/ant0ine/go-json-rest/rest/powered_by.go
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
const xPoweredByDefault = "go-json-rest"
|
||||||
|
|
||||||
|
// PoweredByMiddleware adds the "X-Powered-By" header to the HTTP response.
|
||||||
|
type PoweredByMiddleware struct {
|
||||||
|
|
||||||
|
// If specified, used as the value for the "X-Powered-By" response header.
|
||||||
|
// Defaults to "go-json-rest".
|
||||||
|
XPoweredBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes PoweredByMiddleware implement the Middleware interface.
|
||||||
|
func (mw *PoweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
poweredBy := xPoweredByDefault
|
||||||
|
if mw.XPoweredBy != "" {
|
||||||
|
poweredBy = mw.XPoweredBy
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
w.Header().Add("X-Powered-By", poweredBy)
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
100
vendor/github.com/ant0ine/go-json-rest/rest/recorder.go
generated
vendored
Normal file
100
vendor/github.com/ant0ine/go-json-rest/rest/recorder.go
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecorderMiddleware keeps a record of the HTTP status code of the response,
|
||||||
|
// and the number of bytes written.
|
||||||
|
// The result is available to the wrapping handlers as request.Env["STATUS_CODE"].(int),
|
||||||
|
// and as request.Env["BYTES_WRITTEN"].(int64)
|
||||||
|
type RecorderMiddleware struct{}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes RecorderMiddleware implement the Middleware interface.
|
||||||
|
func (mw *RecorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
writer := &recorderResponseWriter{w, 0, false, 0}
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(writer, r)
|
||||||
|
|
||||||
|
r.Env["STATUS_CODE"] = writer.statusCode
|
||||||
|
r.Env["BYTES_WRITTEN"] = writer.bytesWritten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private responseWriter intantiated by the recorder middleware.
|
||||||
|
// It keeps a record of the HTTP status code of the response.
|
||||||
|
// It implements the following interfaces:
|
||||||
|
// ResponseWriter
|
||||||
|
// http.ResponseWriter
|
||||||
|
// http.Flusher
|
||||||
|
// http.CloseNotifier
|
||||||
|
// http.Hijacker
|
||||||
|
type recorderResponseWriter struct {
|
||||||
|
ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
wroteHeader bool
|
||||||
|
bytesWritten int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the status code.
|
||||||
|
func (w *recorderResponseWriter) WriteHeader(code int) {
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
if w.wroteHeader {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.statusCode = code
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local Write is called.
|
||||||
|
func (w *recorderResponseWriter) WriteJson(v interface{}) error {
|
||||||
|
b, err := w.EncodeJson(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Flush.
|
||||||
|
// Provided in order to implement the http.Flusher interface.
|
||||||
|
func (w *recorderResponseWriter) Flush() {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
flusher := w.ResponseWriter.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent CloseNotify.
|
||||||
|
// Provided in order to implement the http.CloseNotifier interface.
|
||||||
|
func (w *recorderResponseWriter) CloseNotify() <-chan bool {
|
||||||
|
notifier := w.ResponseWriter.(http.CloseNotifier)
|
||||||
|
return notifier.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Hijacker interface.
|
||||||
|
func (w *recorderResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker := w.ResponseWriter.(http.Hijacker)
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the local WriteHeader is called, and call the parent Write.
|
||||||
|
// Provided in order to implement the http.ResponseWriter interface.
|
||||||
|
func (w *recorderResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
writer := w.ResponseWriter.(http.ResponseWriter)
|
||||||
|
written, err := writer.Write(b)
|
||||||
|
w.bytesWritten += int64(written)
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
74
vendor/github.com/ant0ine/go-json-rest/rest/recover.go
generated
vendored
Normal file
74
vendor/github.com/ant0ine/go-json-rest/rest/recover.go
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecoverMiddleware catches the panic errors that occur in the wrapped HandleFunc,
|
||||||
|
// and convert them to 500 responses.
|
||||||
|
type RecoverMiddleware struct {
|
||||||
|
|
||||||
|
// Custom logger used for logging the panic errors,
|
||||||
|
// optional, defaults to log.New(os.Stderr, "", 0)
|
||||||
|
Logger *log.Logger
|
||||||
|
|
||||||
|
// If true, the log records will be printed as JSON. Convenient for log parsing.
|
||||||
|
EnableLogAsJson bool
|
||||||
|
|
||||||
|
// If true, when a "panic" happens, the error string and the stack trace will be
|
||||||
|
// printed in the 500 response body.
|
||||||
|
EnableResponseStackTrace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes RecoverMiddleware implement the Middleware interface.
|
||||||
|
func (mw *RecoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
// set the default Logger
|
||||||
|
if mw.Logger == nil {
|
||||||
|
mw.Logger = log.New(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
// catch user code's panic, and convert to http response
|
||||||
|
defer func() {
|
||||||
|
if reco := recover(); reco != nil {
|
||||||
|
trace := debug.Stack()
|
||||||
|
|
||||||
|
// log the trace
|
||||||
|
message := fmt.Sprintf("%s\n%s", reco, trace)
|
||||||
|
mw.logError(message)
|
||||||
|
|
||||||
|
// write error response
|
||||||
|
if mw.EnableResponseStackTrace {
|
||||||
|
Error(w, message, http.StatusInternalServerError)
|
||||||
|
} else {
|
||||||
|
Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw *RecoverMiddleware) logError(message string) {
|
||||||
|
if mw.EnableLogAsJson {
|
||||||
|
record := map[string]string{
|
||||||
|
"error": message,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(&record)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
mw.Logger.Printf("%s", b)
|
||||||
|
} else {
|
||||||
|
mw.Logger.Print(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
148
vendor/github.com/ant0ine/go-json-rest/rest/request.go
generated
vendored
Normal file
148
vendor/github.com/ant0ine/go-json-rest/rest/request.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrJsonPayloadEmpty is returned when the JSON payload is empty.
|
||||||
|
ErrJsonPayloadEmpty = errors.New("JSON payload is empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request inherits from http.Request, and provides additional methods.
|
||||||
|
type Request struct {
|
||||||
|
*http.Request
|
||||||
|
|
||||||
|
// Map of parameters that have been matched in the URL Path.
|
||||||
|
PathParams map[string]string
|
||||||
|
|
||||||
|
// Environment used by middlewares to communicate.
|
||||||
|
Env map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathParam provides a convenient access to the PathParams map.
|
||||||
|
func (r *Request) PathParam(name string) string {
|
||||||
|
return r.PathParams[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeJsonPayload reads the request body and decodes the JSON using json.Unmarshal.
|
||||||
|
func (r *Request) DecodeJsonPayload(v interface{}) error {
|
||||||
|
content, err := ioutil.ReadAll(r.Body)
|
||||||
|
r.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(content) == 0 {
|
||||||
|
return ErrJsonPayloadEmpty
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(content, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseUrl returns a new URL object with the Host and Scheme taken from the request.
|
||||||
|
// (without the trailing slash in the host)
|
||||||
|
func (r *Request) BaseUrl() *url.URL {
|
||||||
|
scheme := r.URL.Scheme
|
||||||
|
if scheme == "" {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP sometimes gives the default scheme as HTTP even when used with TLS
|
||||||
|
// Check if TLS is not nil and given back https scheme
|
||||||
|
if scheme == "http" && r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := r.Host
|
||||||
|
if len(host) > 0 && host[len(host)-1] == '/' {
|
||||||
|
host = host[:len(host)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: scheme,
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UrlFor returns the URL object from UriBase with the Path set to path, and the query
|
||||||
|
// string built with queryParams.
|
||||||
|
func (r *Request) UrlFor(path string, queryParams map[string][]string) *url.URL {
|
||||||
|
baseUrl := r.BaseUrl()
|
||||||
|
baseUrl.Path = path
|
||||||
|
if queryParams != nil {
|
||||||
|
query := url.Values{}
|
||||||
|
for k, v := range queryParams {
|
||||||
|
for _, vv := range v {
|
||||||
|
query.Add(k, vv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
baseUrl.RawQuery = query.Encode()
|
||||||
|
}
|
||||||
|
return baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// CorsInfo contains the CORS request info derived from a rest.Request.
|
||||||
|
type CorsInfo struct {
|
||||||
|
IsCors bool
|
||||||
|
IsPreflight bool
|
||||||
|
Origin string
|
||||||
|
OriginUrl *url.URL
|
||||||
|
|
||||||
|
// The header value is converted to uppercase to avoid common mistakes.
|
||||||
|
AccessControlRequestMethod string
|
||||||
|
|
||||||
|
// The header values are normalized with http.CanonicalHeaderKey.
|
||||||
|
AccessControlRequestHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCorsInfo derives CorsInfo from Request.
|
||||||
|
func (r *Request) GetCorsInfo() *CorsInfo {
|
||||||
|
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
|
||||||
|
var originUrl *url.URL
|
||||||
|
var isCors bool
|
||||||
|
|
||||||
|
if origin == "" {
|
||||||
|
isCors = false
|
||||||
|
} else if origin == "null" {
|
||||||
|
isCors = true
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
originUrl, err = url.ParseRequestURI(origin)
|
||||||
|
isCors = err == nil && r.Host != originUrl.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
reqMethod := r.Header.Get("Access-Control-Request-Method")
|
||||||
|
|
||||||
|
reqHeaders := []string{}
|
||||||
|
rawReqHeaders := r.Header[http.CanonicalHeaderKey("Access-Control-Request-Headers")]
|
||||||
|
for _, rawReqHeader := range rawReqHeaders {
|
||||||
|
if len(rawReqHeader) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// net/http does not handle comma delimited headers for us
|
||||||
|
for _, reqHeader := range strings.Split(rawReqHeader, ",") {
|
||||||
|
reqHeaders = append(reqHeaders, http.CanonicalHeaderKey(strings.TrimSpace(reqHeader)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPreflight := isCors && r.Method == "OPTIONS" && reqMethod != ""
|
||||||
|
|
||||||
|
return &CorsInfo{
|
||||||
|
IsCors: isCors,
|
||||||
|
IsPreflight: isPreflight,
|
||||||
|
Origin: origin,
|
||||||
|
OriginUrl: originUrl,
|
||||||
|
AccessControlRequestMethod: strings.ToUpper(reqMethod),
|
||||||
|
AccessControlRequestHeaders: reqHeaders,
|
||||||
|
}
|
||||||
|
}
|
||||||
127
vendor/github.com/ant0ine/go-json-rest/rest/response.go
generated
vendored
Normal file
127
vendor/github.com/ant0ine/go-json-rest/rest/response.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A ResponseWriter interface dedicated to JSON HTTP response.
|
||||||
|
// Note, the responseWriter object instantiated by the framework also implements many other interfaces
|
||||||
|
// accessible by type assertion: http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker.
|
||||||
|
type ResponseWriter interface {
|
||||||
|
|
||||||
|
// Identical to the http.ResponseWriter interface
|
||||||
|
Header() http.Header
|
||||||
|
|
||||||
|
// Use EncodeJson to generate the payload, write the headers with http.StatusOK if
|
||||||
|
// they are not already written, then write the payload.
|
||||||
|
// The Content-Type header is set to "application/json", unless already specified.
|
||||||
|
WriteJson(v interface{}) error
|
||||||
|
|
||||||
|
// Encode the data structure to JSON, mainly used to wrap ResponseWriter in
|
||||||
|
// middlewares.
|
||||||
|
EncodeJson(v interface{}) ([]byte, error)
|
||||||
|
|
||||||
|
// Similar to the http.ResponseWriter interface, with additional JSON related
|
||||||
|
// headers set.
|
||||||
|
WriteHeader(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This allows to customize the field name used in the error response payload.
|
||||||
|
// It defaults to "Error" for compatibility reason, but can be changed before starting the server.
|
||||||
|
// eg: rest.ErrorFieldName = "errorMessage"
|
||||||
|
var ErrorFieldName = "Error"
|
||||||
|
|
||||||
|
// Error produces an error response in JSON with the following structure, '{"Error":"My error message"}'
|
||||||
|
// The standard plain text net/http Error helper can still be called like this:
|
||||||
|
// http.Error(w, "error message", code)
|
||||||
|
func Error(w ResponseWriter, error string, code int) {
|
||||||
|
w.WriteHeader(code)
|
||||||
|
err := w.WriteJson(map[string]string{ErrorFieldName: error})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFound produces a 404 response with the following JSON, '{"Error":"Resource not found"}'
|
||||||
|
// The standard plain text net/http NotFound helper can still be called like this:
|
||||||
|
// http.NotFound(w, r.Request)
|
||||||
|
func NotFound(w ResponseWriter, r *Request) {
|
||||||
|
Error(w, "Resource not found", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private responseWriter intantiated by the resource handler.
|
||||||
|
// It implements the following interfaces:
|
||||||
|
// ResponseWriter
|
||||||
|
// http.ResponseWriter
|
||||||
|
// http.Flusher
|
||||||
|
// http.CloseNotifier
|
||||||
|
// http.Hijacker
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
wroteHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
|
if w.Header().Get("Content-Type") == "" {
|
||||||
|
// Per spec, UTF-8 is the default, and the charset parameter should not
|
||||||
|
// be necessary. But some clients (eg: Chrome) think otherwise.
|
||||||
|
// Since json.Marshal produces UTF-8, setting the charset parameter is a
|
||||||
|
// safe option.
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) EncodeJson(v interface{}) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the object in JSON and call Write.
|
||||||
|
func (w *responseWriter) WriteJson(v interface{}) error {
|
||||||
|
b, err := w.EncodeJson(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.ResponseWriter interface.
|
||||||
|
func (w *responseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Flusher interface.
|
||||||
|
func (w *responseWriter) Flush() {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
flusher := w.ResponseWriter.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.CloseNotifier interface.
|
||||||
|
func (w *responseWriter) CloseNotify() <-chan bool {
|
||||||
|
notifier := w.ResponseWriter.(http.CloseNotifier)
|
||||||
|
return notifier.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided in order to implement the http.Hijacker interface.
|
||||||
|
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker := w.ResponseWriter.(http.Hijacker)
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
107
vendor/github.com/ant0ine/go-json-rest/rest/route.go
generated
vendored
Normal file
107
vendor/github.com/ant0ine/go-json-rest/rest/route.go
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Route defines a route as consumed by the router. It can be instantiated directly, or using one
|
||||||
|
// of the shortcut methods: rest.Get, rest.Post, rest.Put, rest.Patch and rest.Delete.
|
||||||
|
type Route struct {
|
||||||
|
|
||||||
|
// Any HTTP method. It will be used as uppercase to avoid common mistakes.
|
||||||
|
HttpMethod string
|
||||||
|
|
||||||
|
// A string like "/resource/:id.json".
|
||||||
|
// Placeholders supported are:
|
||||||
|
// :paramName that matches any char to the first '/' or '.'
|
||||||
|
// #paramName that matches any char to the first '/'
|
||||||
|
// *paramName that matches everything to the end of the string
|
||||||
|
// (placeholder names must be unique per PathExp)
|
||||||
|
PathExp string
|
||||||
|
|
||||||
|
// Code that will be executed when this route is taken.
|
||||||
|
Func HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakePath generates the path corresponding to this Route and the provided path parameters.
|
||||||
|
// This is used for reverse route resolution.
|
||||||
|
func (route *Route) MakePath(pathParams map[string]string) string {
|
||||||
|
path := route.PathExp
|
||||||
|
for paramName, paramValue := range pathParams {
|
||||||
|
paramPlaceholder := ":" + paramName
|
||||||
|
relaxedPlaceholder := "#" + paramName
|
||||||
|
splatPlaceholder := "*" + paramName
|
||||||
|
r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue, relaxedPlaceholder, paramValue)
|
||||||
|
path = r.Replace(path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head is a shortcut method that instantiates a HEAD route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"HEAD", pathExp, handlerFunc}
|
||||||
|
func Head(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "HEAD",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get is a shortcut method that instantiates a GET route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"GET", pathExp, handlerFunc}
|
||||||
|
func Get(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "GET",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post is a shortcut method that instantiates a POST route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"POST", pathExp, handlerFunc}
|
||||||
|
func Post(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "POST",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put is a shortcut method that instantiates a PUT route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"PUT", pathExp, handlerFunc}
|
||||||
|
func Put(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "PUT",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch is a shortcut method that instantiates a PATCH route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"PATCH", pathExp, handlerFunc}
|
||||||
|
func Patch(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "PATCH",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete is a shortcut method that instantiates a DELETE route. Equivalent to &Route{"DELETE", pathExp, handlerFunc}
|
||||||
|
func Delete(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "DELETE",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options is a shortcut method that instantiates an OPTIONS route. See the Route object the parameters definitions.
|
||||||
|
// Equivalent to &Route{"OPTIONS", pathExp, handlerFunc}
|
||||||
|
func Options(pathExp string, handlerFunc HandlerFunc) *Route {
|
||||||
|
return &Route{
|
||||||
|
HttpMethod: "OPTIONS",
|
||||||
|
PathExp: pathExp,
|
||||||
|
Func: handlerFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
194
vendor/github.com/ant0ine/go-json-rest/rest/router.go
generated
vendored
Normal file
194
vendor/github.com/ant0ine/go-json-rest/rest/router.go
generated
vendored
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/ant0ine/go-json-rest/rest/trie"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type router struct {
|
||||||
|
Routes []*Route
|
||||||
|
|
||||||
|
disableTrieCompression bool
|
||||||
|
index map[*Route]int
|
||||||
|
trie *trie.Trie
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRouter returns the router app. Given a set of Routes, it dispatches the request to the
|
||||||
|
// HandlerFunc of the first route that matches. The order of the Routes matters.
|
||||||
|
func MakeRouter(routes ...*Route) (App, error) {
|
||||||
|
r := &router{
|
||||||
|
Routes: routes,
|
||||||
|
}
|
||||||
|
err := r.start()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the REST routing and run the user code.
|
||||||
|
func (rt *router) AppFunc() HandlerFunc {
|
||||||
|
return func(writer ResponseWriter, request *Request) {
|
||||||
|
|
||||||
|
// find the route
|
||||||
|
route, params, pathMatched := rt.findRouteFromURL(request.Method, request.URL)
|
||||||
|
if route == nil {
|
||||||
|
|
||||||
|
if pathMatched {
|
||||||
|
// no route found, but path was matched: 405 Method Not Allowed
|
||||||
|
Error(writer, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// no route found, the path was not matched: 404 Not Found
|
||||||
|
NotFound(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// a route was found, set the PathParams
|
||||||
|
request.PathParams = params
|
||||||
|
|
||||||
|
// run the user code
|
||||||
|
handler := route.Func
|
||||||
|
handler(writer, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is run for each new request, perf is important.
|
||||||
|
func escapedPath(urlObj *url.URL) string {
|
||||||
|
// the escape method of url.URL should be public
|
||||||
|
// that would avoid this split.
|
||||||
|
parts := strings.SplitN(urlObj.RequestURI(), "?", 2)
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__")
|
||||||
|
|
||||||
|
var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#")
|
||||||
|
|
||||||
|
// This is run at init time only.
|
||||||
|
func escapedPathExp(pathExp string) (string, error) {
|
||||||
|
|
||||||
|
// PathExp validation
|
||||||
|
if pathExp == "" {
|
||||||
|
return "", errors.New("empty PathExp")
|
||||||
|
}
|
||||||
|
if pathExp[0] != '/' {
|
||||||
|
return "", errors.New("PathExp must start with /")
|
||||||
|
}
|
||||||
|
if strings.Contains(pathExp, "?") {
|
||||||
|
return "", errors.New("PathExp must not contain the query string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the right escaping
|
||||||
|
// XXX a bit hacky
|
||||||
|
|
||||||
|
pathExp = preEscape.Replace(pathExp)
|
||||||
|
|
||||||
|
urlObj, err := url.Parse(pathExp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the same escaping as find requests
|
||||||
|
pathExp = urlObj.RequestURI()
|
||||||
|
|
||||||
|
pathExp = postEscape.Replace(pathExp)
|
||||||
|
|
||||||
|
return pathExp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This validates the Routes and prepares the Trie data structure.
|
||||||
|
// It must be called once the Routes are defined and before trying to find Routes.
|
||||||
|
// The order matters, if multiple Routes match, the first defined will be used.
|
||||||
|
func (rt *router) start() error {
|
||||||
|
|
||||||
|
rt.trie = trie.New()
|
||||||
|
rt.index = map[*Route]int{}
|
||||||
|
|
||||||
|
for i, route := range rt.Routes {
|
||||||
|
|
||||||
|
// work with the PathExp urlencoded.
|
||||||
|
pathExp, err := escapedPathExp(route.PathExp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert in the Trie
|
||||||
|
err = rt.trie.AddRoute(
|
||||||
|
strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase
|
||||||
|
pathExp,
|
||||||
|
route,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// index
|
||||||
|
rt.index[route] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.disableTrieCompression == false {
|
||||||
|
rt.trie.Compress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the result that has the route defined the earliest
|
||||||
|
func (rt *router) ofFirstDefinedRoute(matches []*trie.Match) *trie.Match {
|
||||||
|
minIndex := -1
|
||||||
|
var bestMatch *trie.Match
|
||||||
|
|
||||||
|
for _, result := range matches {
|
||||||
|
route := result.Route.(*Route)
|
||||||
|
routeIndex := rt.index[route]
|
||||||
|
if minIndex == -1 || routeIndex < minIndex {
|
||||||
|
minIndex = routeIndex
|
||||||
|
bestMatch = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first matching Route and the corresponding parameters for a given URL object.
|
||||||
|
func (rt *router) findRouteFromURL(httpMethod string, urlObj *url.URL) (*Route, map[string]string, bool) {
|
||||||
|
|
||||||
|
// lookup the routes in the Trie
|
||||||
|
matches, pathMatched := rt.trie.FindRoutesAndPathMatched(
|
||||||
|
strings.ToUpper(httpMethod), // work with the httpMethod in uppercase
|
||||||
|
escapedPath(urlObj), // work with the path urlencoded
|
||||||
|
)
|
||||||
|
|
||||||
|
// short cuts
|
||||||
|
if len(matches) == 0 {
|
||||||
|
// no route found
|
||||||
|
return nil, nil, pathMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches) == 1 {
|
||||||
|
// one route found
|
||||||
|
return matches[0].Route.(*Route), matches[0].Params, pathMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiple routes found, pick the first defined
|
||||||
|
result := rt.ofFirstDefinedRoute(matches)
|
||||||
|
return result.Route.(*Route), result.Params, pathMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the url string (complete or just the path) and return the first matching Route and the corresponding parameters.
|
||||||
|
func (rt *router) findRoute(httpMethod, urlStr string) (*Route, map[string]string, bool, error) {
|
||||||
|
|
||||||
|
// parse the url
|
||||||
|
urlObj, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
route, params, pathMatched := rt.findRouteFromURL(httpMethod, urlObj)
|
||||||
|
return route, params, pathMatched, nil
|
||||||
|
}
|
||||||
129
vendor/github.com/ant0ine/go-json-rest/rest/status.go
generated
vendored
Normal file
129
vendor/github.com/ant0ine/go-json-rest/rest/status.go
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusMiddleware keeps track of various stats about the processed requests.
|
||||||
|
// It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"],
|
||||||
|
// recorderMiddleware and timerMiddleware must be in the wrapped middlewares.
|
||||||
|
type StatusMiddleware struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
start time.Time
|
||||||
|
pid int
|
||||||
|
responseCounts map[string]int
|
||||||
|
totalResponseTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes StatusMiddleware implement the Middleware interface.
|
||||||
|
func (mw *StatusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
|
||||||
|
mw.start = time.Now()
|
||||||
|
mw.pid = os.Getpid()
|
||||||
|
mw.responseCounts = map[string]int{}
|
||||||
|
mw.totalResponseTime = time.Time{}
|
||||||
|
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
if r.Env["STATUS_CODE"] == nil {
|
||||||
|
log.Fatal("StatusMiddleware: Env[\"STATUS_CODE\"] is nil, " +
|
||||||
|
"RecorderMiddleware may not be in the wrapped Middlewares.")
|
||||||
|
}
|
||||||
|
statusCode := r.Env["STATUS_CODE"].(int)
|
||||||
|
|
||||||
|
if r.Env["ELAPSED_TIME"] == nil {
|
||||||
|
log.Fatal("StatusMiddleware: Env[\"ELAPSED_TIME\"] is nil, " +
|
||||||
|
"TimerMiddleware may not be in the wrapped Middlewares.")
|
||||||
|
}
|
||||||
|
responseTime := r.Env["ELAPSED_TIME"].(*time.Duration)
|
||||||
|
|
||||||
|
mw.lock.Lock()
|
||||||
|
mw.responseCounts[fmt.Sprintf("%d", statusCode)]++
|
||||||
|
mw.totalResponseTime = mw.totalResponseTime.Add(*responseTime)
|
||||||
|
mw.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status contains stats and status information. It is returned by GetStatus.
|
||||||
|
// These information can be made available as an API endpoint, see the "status"
|
||||||
|
// example to install the following status route.
|
||||||
|
// GET /.status returns something like:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "Pid": 21732,
|
||||||
|
// "UpTime": "1m15.926272s",
|
||||||
|
// "UpTimeSec": 75.926272,
|
||||||
|
// "Time": "2013-03-04 08:00:27.152986 +0000 UTC",
|
||||||
|
// "TimeUnix": 1362384027,
|
||||||
|
// "StatusCodeCount": {
|
||||||
|
// "200": 53,
|
||||||
|
// "404": 11
|
||||||
|
// },
|
||||||
|
// "TotalCount": 64,
|
||||||
|
// "TotalResponseTime": "16.777ms",
|
||||||
|
// "TotalResponseTimeSec": 0.016777,
|
||||||
|
// "AverageResponseTime": "262.14us",
|
||||||
|
// "AverageResponseTimeSec": 0.00026214
|
||||||
|
// }
|
||||||
|
type Status struct {
|
||||||
|
Pid int
|
||||||
|
UpTime string
|
||||||
|
UpTimeSec float64
|
||||||
|
Time string
|
||||||
|
TimeUnix int64
|
||||||
|
StatusCodeCount map[string]int
|
||||||
|
TotalCount int
|
||||||
|
TotalResponseTime string
|
||||||
|
TotalResponseTimeSec float64
|
||||||
|
AverageResponseTime string
|
||||||
|
AverageResponseTimeSec float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus computes and returns a Status object based on the request informations accumulated
|
||||||
|
// since the start of the process.
|
||||||
|
func (mw *StatusMiddleware) GetStatus() *Status {
|
||||||
|
|
||||||
|
mw.lock.RLock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
uptime := now.Sub(mw.start)
|
||||||
|
|
||||||
|
totalCount := 0
|
||||||
|
for _, count := range mw.responseCounts {
|
||||||
|
totalCount += count
|
||||||
|
}
|
||||||
|
|
||||||
|
totalResponseTime := mw.totalResponseTime.Sub(time.Time{})
|
||||||
|
|
||||||
|
averageResponseTime := time.Duration(0)
|
||||||
|
if totalCount > 0 {
|
||||||
|
avgNs := int64(totalResponseTime) / int64(totalCount)
|
||||||
|
averageResponseTime = time.Duration(avgNs)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := &Status{
|
||||||
|
Pid: mw.pid,
|
||||||
|
UpTime: uptime.String(),
|
||||||
|
UpTimeSec: uptime.Seconds(),
|
||||||
|
Time: now.String(),
|
||||||
|
TimeUnix: now.Unix(),
|
||||||
|
StatusCodeCount: mw.responseCounts,
|
||||||
|
TotalCount: totalCount,
|
||||||
|
TotalResponseTime: totalResponseTime.String(),
|
||||||
|
TotalResponseTimeSec: totalResponseTime.Seconds(),
|
||||||
|
AverageResponseTime: averageResponseTime.String(),
|
||||||
|
AverageResponseTimeSec: averageResponseTime.Seconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.lock.RUnlock()
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
26
vendor/github.com/ant0ine/go-json-rest/rest/timer.go
generated
vendored
Normal file
26
vendor/github.com/ant0ine/go-json-rest/rest/timer.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimerMiddleware computes the elapsed time spent during the execution of the wrapped handler.
|
||||||
|
// The result is available to the wrapping handlers as request.Env["ELAPSED_TIME"].(*time.Duration),
|
||||||
|
// and as request.Env["START_TIME"].(*time.Time)
|
||||||
|
type TimerMiddleware struct{}
|
||||||
|
|
||||||
|
// MiddlewareFunc makes TimerMiddleware implement the Middleware interface.
|
||||||
|
func (mw *TimerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc {
|
||||||
|
return func(w ResponseWriter, r *Request) {
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
r.Env["START_TIME"] = &start
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
h(w, r)
|
||||||
|
|
||||||
|
end := time.Now()
|
||||||
|
elapsed := end.Sub(start)
|
||||||
|
r.Env["ELAPSED_TIME"] = &elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
426
vendor/github.com/ant0ine/go-json-rest/rest/trie/impl.go
generated
vendored
Normal file
426
vendor/github.com/ant0ine/go-json-rest/rest/trie/impl.go
generated
vendored
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
// Special Trie implementation for HTTP routing.
|
||||||
|
//
|
||||||
|
// This Trie implementation is designed to support strings that includes
|
||||||
|
// :param and *splat parameters. Strings that are commonly used to represent
|
||||||
|
// the Path in HTTP routing. This implementation also maintain for each Path
|
||||||
|
// a map of HTTP Methods associated with the Route.
|
||||||
|
//
|
||||||
|
// You probably don't need to use this package directly.
|
||||||
|
//
|
||||||
|
package trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func splitParam(remaining string) (string, string) {
|
||||||
|
i := 0
|
||||||
|
for len(remaining) > i && remaining[i] != '/' && remaining[i] != '.' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return remaining[:i], remaining[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitRelaxed(remaining string) (string, string) {
|
||||||
|
i := 0
|
||||||
|
for len(remaining) > i && remaining[i] != '/' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return remaining[:i], remaining[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
HttpMethodToRoute map[string]interface{}
|
||||||
|
|
||||||
|
Children map[string]*node
|
||||||
|
ChildrenKeyLen int
|
||||||
|
|
||||||
|
ParamChild *node
|
||||||
|
ParamName string
|
||||||
|
|
||||||
|
RelaxedChild *node
|
||||||
|
RelaxedName string
|
||||||
|
|
||||||
|
SplatChild *node
|
||||||
|
SplatName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParams []string) error {
|
||||||
|
|
||||||
|
if len(pathExp) == 0 {
|
||||||
|
// end of the path, leaf node, update the map
|
||||||
|
if n.HttpMethodToRoute == nil {
|
||||||
|
n.HttpMethodToRoute = map[string]interface{}{
|
||||||
|
httpMethod: route,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
if n.HttpMethodToRoute[httpMethod] != nil {
|
||||||
|
return errors.New("node.Route already set, duplicated path and method")
|
||||||
|
}
|
||||||
|
n.HttpMethodToRoute[httpMethod] = route
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := pathExp[0:1]
|
||||||
|
remaining := pathExp[1:]
|
||||||
|
var nextNode *node
|
||||||
|
|
||||||
|
if token[0] == ':' {
|
||||||
|
// :param case
|
||||||
|
var name string
|
||||||
|
name, remaining = splitParam(remaining)
|
||||||
|
|
||||||
|
// Check param name is unique
|
||||||
|
for _, e := range usedParams {
|
||||||
|
if e == name {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf("A route can't have two placeholders with the same name: %s", name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usedParams = append(usedParams, name)
|
||||||
|
|
||||||
|
if n.ParamChild == nil {
|
||||||
|
n.ParamChild = &node{}
|
||||||
|
n.ParamName = name
|
||||||
|
} else {
|
||||||
|
if n.ParamName != name {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Routes sharing a common placeholder MUST name it consistently: %s != %s",
|
||||||
|
n.ParamName,
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextNode = n.ParamChild
|
||||||
|
} else if token[0] == '#' {
|
||||||
|
// #param case
|
||||||
|
var name string
|
||||||
|
name, remaining = splitRelaxed(remaining)
|
||||||
|
|
||||||
|
// Check param name is unique
|
||||||
|
for _, e := range usedParams {
|
||||||
|
if e == name {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf("A route can't have two placeholders with the same name: %s", name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usedParams = append(usedParams, name)
|
||||||
|
|
||||||
|
if n.RelaxedChild == nil {
|
||||||
|
n.RelaxedChild = &node{}
|
||||||
|
n.RelaxedName = name
|
||||||
|
} else {
|
||||||
|
if n.RelaxedName != name {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Routes sharing a common placeholder MUST name it consistently: %s != %s",
|
||||||
|
n.RelaxedName,
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextNode = n.RelaxedChild
|
||||||
|
} else if token[0] == '*' {
|
||||||
|
// *splat case
|
||||||
|
name := remaining
|
||||||
|
remaining = ""
|
||||||
|
|
||||||
|
// Check param name is unique
|
||||||
|
for _, e := range usedParams {
|
||||||
|
if e == name {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf("A route can't have two placeholders with the same name: %s", name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.SplatChild == nil {
|
||||||
|
n.SplatChild = &node{}
|
||||||
|
n.SplatName = name
|
||||||
|
}
|
||||||
|
nextNode = n.SplatChild
|
||||||
|
} else {
|
||||||
|
// general case
|
||||||
|
if n.Children == nil {
|
||||||
|
n.Children = map[string]*node{}
|
||||||
|
n.ChildrenKeyLen = 1
|
||||||
|
}
|
||||||
|
if n.Children[token] == nil {
|
||||||
|
n.Children[token] = &node{}
|
||||||
|
}
|
||||||
|
nextNode = n.Children[token]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextNode.addRoute(httpMethod, remaining, route, usedParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) compress() {
|
||||||
|
// *splat branch
|
||||||
|
if n.SplatChild != nil {
|
||||||
|
n.SplatChild.compress()
|
||||||
|
}
|
||||||
|
// :param branch
|
||||||
|
if n.ParamChild != nil {
|
||||||
|
n.ParamChild.compress()
|
||||||
|
}
|
||||||
|
// #param branch
|
||||||
|
if n.RelaxedChild != nil {
|
||||||
|
n.RelaxedChild.compress()
|
||||||
|
}
|
||||||
|
// main branch
|
||||||
|
if len(n.Children) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// compressable ?
|
||||||
|
canCompress := true
|
||||||
|
for _, node := range n.Children {
|
||||||
|
if node.HttpMethodToRoute != nil || node.SplatChild != nil || node.ParamChild != nil || node.RelaxedChild != nil {
|
||||||
|
canCompress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// compress
|
||||||
|
if canCompress {
|
||||||
|
merged := map[string]*node{}
|
||||||
|
for key, node := range n.Children {
|
||||||
|
for gdKey, gdNode := range node.Children {
|
||||||
|
mergedKey := key + gdKey
|
||||||
|
merged[mergedKey] = gdNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.Children = merged
|
||||||
|
n.ChildrenKeyLen++
|
||||||
|
n.compress()
|
||||||
|
// continue
|
||||||
|
} else {
|
||||||
|
for _, node := range n.Children {
|
||||||
|
node.compress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printFPadding(padding int, format string, a ...interface{}) {
|
||||||
|
for i := 0; i < padding; i++ {
|
||||||
|
fmt.Print(" ")
|
||||||
|
}
|
||||||
|
fmt.Printf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private function for now
|
||||||
|
func (n *node) printDebug(level int) {
|
||||||
|
level++
|
||||||
|
// *splat branch
|
||||||
|
if n.SplatChild != nil {
|
||||||
|
printFPadding(level, "*splat\n")
|
||||||
|
n.SplatChild.printDebug(level)
|
||||||
|
}
|
||||||
|
// :param branch
|
||||||
|
if n.ParamChild != nil {
|
||||||
|
printFPadding(level, ":param\n")
|
||||||
|
n.ParamChild.printDebug(level)
|
||||||
|
}
|
||||||
|
// #param branch
|
||||||
|
if n.RelaxedChild != nil {
|
||||||
|
printFPadding(level, "#relaxed\n")
|
||||||
|
n.RelaxedChild.printDebug(level)
|
||||||
|
}
|
||||||
|
// main branch
|
||||||
|
for key, node := range n.Children {
|
||||||
|
printFPadding(level, "\"%s\"\n", key)
|
||||||
|
node.printDebug(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility for the node.findRoutes recursive method
|
||||||
|
|
||||||
|
type paramMatch struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type findContext struct {
|
||||||
|
paramStack []paramMatch
|
||||||
|
matchFunc func(httpMethod, path string, node *node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFindContext() *findContext {
|
||||||
|
return &findContext{
|
||||||
|
paramStack: []paramMatch{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *findContext) pushParams(name, value string) {
|
||||||
|
fc.paramStack = append(
|
||||||
|
fc.paramStack,
|
||||||
|
paramMatch{name, value},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *findContext) popParams() {
|
||||||
|
fc.paramStack = fc.paramStack[:len(fc.paramStack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *findContext) paramsAsMap() map[string]string {
|
||||||
|
r := map[string]string{}
|
||||||
|
for _, param := range fc.paramStack {
|
||||||
|
if r[param.name] != "" {
|
||||||
|
// this is checked at addRoute time, and should never happen.
|
||||||
|
panic(fmt.Sprintf(
|
||||||
|
"placeholder %s already found, placeholder names should be unique per route",
|
||||||
|
param.name,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
r[param.name] = param.value
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type Match struct {
|
||||||
|
// Same Route as in AddRoute
|
||||||
|
Route interface{}
|
||||||
|
// map of params matched for this result
|
||||||
|
Params map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) find(httpMethod, path string, context *findContext) {
|
||||||
|
|
||||||
|
if n.HttpMethodToRoute != nil && path == "" {
|
||||||
|
context.matchFunc(httpMethod, path, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// *splat branch
|
||||||
|
if n.SplatChild != nil {
|
||||||
|
context.pushParams(n.SplatName, path)
|
||||||
|
n.SplatChild.find(httpMethod, "", context)
|
||||||
|
context.popParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
// :param branch
|
||||||
|
if n.ParamChild != nil {
|
||||||
|
value, remaining := splitParam(path)
|
||||||
|
context.pushParams(n.ParamName, value)
|
||||||
|
n.ParamChild.find(httpMethod, remaining, context)
|
||||||
|
context.popParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
// #param branch
|
||||||
|
if n.RelaxedChild != nil {
|
||||||
|
value, remaining := splitRelaxed(path)
|
||||||
|
context.pushParams(n.RelaxedName, value)
|
||||||
|
n.RelaxedChild.find(httpMethod, remaining, context)
|
||||||
|
context.popParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
// main branch
|
||||||
|
length := n.ChildrenKeyLen
|
||||||
|
if len(path) < length {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := path[0:length]
|
||||||
|
remaining := path[length:]
|
||||||
|
if n.Children[token] != nil {
|
||||||
|
n.Children[token].find(httpMethod, remaining, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trie struct {
|
||||||
|
root *node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instanciate a Trie with an empty node as the root.
|
||||||
|
func New() *Trie {
|
||||||
|
return &Trie{
|
||||||
|
root: &node{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the route in the Trie following or creating the nodes corresponding to the path.
|
||||||
|
func (t *Trie) AddRoute(httpMethod, pathExp string, route interface{}) error {
|
||||||
|
return t.root.addRoute(httpMethod, pathExp, route, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce the size of the tree, must be done after the last AddRoute.
|
||||||
|
func (t *Trie) Compress() {
|
||||||
|
t.root.compress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private function for now.
|
||||||
|
func (t *Trie) printDebug() {
|
||||||
|
fmt.Print("<trie>\n")
|
||||||
|
t.root.printDebug(0)
|
||||||
|
fmt.Print("</trie>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a path and an http method, return all the matching routes.
|
||||||
|
func (t *Trie) FindRoutes(httpMethod, path string) []*Match {
|
||||||
|
context := newFindContext()
|
||||||
|
matches := []*Match{}
|
||||||
|
context.matchFunc = func(httpMethod, path string, node *node) {
|
||||||
|
if node.HttpMethodToRoute[httpMethod] != nil {
|
||||||
|
// path and method match, found a route !
|
||||||
|
matches = append(
|
||||||
|
matches,
|
||||||
|
&Match{
|
||||||
|
Route: node.HttpMethodToRoute[httpMethod],
|
||||||
|
Params: context.paramsAsMap(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.root.find(httpMethod, path, context)
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as FindRoutes, but return in addition a boolean indicating if the path was matched.
|
||||||
|
// Useful to return 405
|
||||||
|
func (t *Trie) FindRoutesAndPathMatched(httpMethod, path string) ([]*Match, bool) {
|
||||||
|
context := newFindContext()
|
||||||
|
pathMatched := false
|
||||||
|
matches := []*Match{}
|
||||||
|
context.matchFunc = func(httpMethod, path string, node *node) {
|
||||||
|
pathMatched = true
|
||||||
|
if node.HttpMethodToRoute[httpMethod] != nil {
|
||||||
|
// path and method match, found a route !
|
||||||
|
matches = append(
|
||||||
|
matches,
|
||||||
|
&Match{
|
||||||
|
Route: node.HttpMethodToRoute[httpMethod],
|
||||||
|
Params: context.paramsAsMap(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.root.find(httpMethod, path, context)
|
||||||
|
return matches, pathMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a path, and whatever the http method, return all the matching routes.
|
||||||
|
func (t *Trie) FindRoutesForPath(path string) []*Match {
|
||||||
|
context := newFindContext()
|
||||||
|
matches := []*Match{}
|
||||||
|
context.matchFunc = func(httpMethod, path string, node *node) {
|
||||||
|
params := context.paramsAsMap()
|
||||||
|
for _, route := range node.HttpMethodToRoute {
|
||||||
|
matches = append(
|
||||||
|
matches,
|
||||||
|
&Match{
|
||||||
|
Route: route,
|
||||||
|
Params: params,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.root.find("", path, context)
|
||||||
|
return matches
|
||||||
|
}
|
||||||
15
vendor/github.com/davecgh/go-spew/LICENSE
generated
vendored
Normal file
15
vendor/github.com/davecgh/go-spew/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
152
vendor/github.com/davecgh/go-spew/spew/bypass.go
generated
vendored
Normal file
152
vendor/github.com/davecgh/go-spew/spew/bypass.go
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||||
|
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build !js,!appengine,!safe,!disableunsafe
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = false
|
||||||
|
|
||||||
|
// ptrSize is the size of a pointer on the current arch.
|
||||||
|
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
||||||
|
// internal reflect.Value fields. These values are valid before golang
|
||||||
|
// commit ecccf07e7f9d which changed the format. The are also valid
|
||||||
|
// after commit 82f48826c6c7 which changed the format again to mirror
|
||||||
|
// the original format. Code in the init function updates these offsets
|
||||||
|
// as necessary.
|
||||||
|
offsetPtr = uintptr(ptrSize)
|
||||||
|
offsetScalar = uintptr(0)
|
||||||
|
offsetFlag = uintptr(ptrSize * 2)
|
||||||
|
|
||||||
|
// flagKindWidth and flagKindShift indicate various bits that the
|
||||||
|
// reflect package uses internally to track kind information.
|
||||||
|
//
|
||||||
|
// flagRO indicates whether or not the value field of a reflect.Value is
|
||||||
|
// read-only.
|
||||||
|
//
|
||||||
|
// flagIndir indicates whether the value field of a reflect.Value is
|
||||||
|
// the actual data or a pointer to the data.
|
||||||
|
//
|
||||||
|
// These values are valid before golang commit 90a7c3c86944 which
|
||||||
|
// changed their positions. Code in the init function updates these
|
||||||
|
// flags as necessary.
|
||||||
|
flagKindWidth = uintptr(5)
|
||||||
|
flagKindShift = uintptr(flagKindWidth - 1)
|
||||||
|
flagRO = uintptr(1 << 0)
|
||||||
|
flagIndir = uintptr(1 << 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Older versions of reflect.Value stored small integers directly in the
|
||||||
|
// ptr field (which is named val in the older versions). Versions
|
||||||
|
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
||||||
|
// scalar for this purpose which unfortunately came before the flag
|
||||||
|
// field, so the offset of the flag field is different for those
|
||||||
|
// versions.
|
||||||
|
//
|
||||||
|
// This code constructs a new reflect.Value from a known small integer
|
||||||
|
// and checks if the size of the reflect.Value struct indicates it has
|
||||||
|
// the scalar field. When it does, the offsets are updated accordingly.
|
||||||
|
vv := reflect.ValueOf(0xf00)
|
||||||
|
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
||||||
|
offsetScalar = ptrSize * 2
|
||||||
|
offsetFlag = ptrSize * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit 90a7c3c86944 changed the flag positions such that the low
|
||||||
|
// order bits are the kind. This code extracts the kind from the flags
|
||||||
|
// field and ensures it's the correct type. When it's not, the flag
|
||||||
|
// order has been changed to the newer format, so the flags are updated
|
||||||
|
// accordingly.
|
||||||
|
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
||||||
|
upfv := *(*uintptr)(upf)
|
||||||
|
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
||||||
|
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
||||||
|
flagKindShift = 0
|
||||||
|
flagRO = 1 << 5
|
||||||
|
flagIndir = 1 << 6
|
||||||
|
|
||||||
|
// Commit adf9b30e5594 modified the flags to separate the
|
||||||
|
// flagRO flag into two bits which specifies whether or not the
|
||||||
|
// field is embedded. This causes flagIndir to move over a bit
|
||||||
|
// and means that flagRO is the combination of either of the
|
||||||
|
// original flagRO bit and the new bit.
|
||||||
|
//
|
||||||
|
// This code detects the change by extracting what used to be
|
||||||
|
// the indirect bit to ensure it's set. When it's not, the flag
|
||||||
|
// order has been changed to the newer format, so the flags are
|
||||||
|
// updated accordingly.
|
||||||
|
if upfv&flagIndir == 0 {
|
||||||
|
flagRO = 3 << 5
|
||||||
|
flagIndir = 1 << 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||||
|
// the typical safety restrictions preventing access to unaddressable and
|
||||||
|
// unexported data. It works by digging the raw pointer to the underlying
|
||||||
|
// value out of the protected value and generating a new unprotected (unsafe)
|
||||||
|
// reflect.Value to it.
|
||||||
|
//
|
||||||
|
// This allows us to check for implementations of the Stringer and error
|
||||||
|
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||||
|
// inaccessible values such as unexported struct fields.
|
||||||
|
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
||||||
|
indirects := 1
|
||||||
|
vt := v.Type()
|
||||||
|
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
||||||
|
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
||||||
|
if rvf&flagIndir != 0 {
|
||||||
|
vt = reflect.PtrTo(v.Type())
|
||||||
|
indirects++
|
||||||
|
} else if offsetScalar != 0 {
|
||||||
|
// The value is in the scalar field when it's not one of the
|
||||||
|
// reference types.
|
||||||
|
switch vt.Kind() {
|
||||||
|
case reflect.Uintptr:
|
||||||
|
case reflect.Chan:
|
||||||
|
case reflect.Func:
|
||||||
|
case reflect.Map:
|
||||||
|
case reflect.Ptr:
|
||||||
|
case reflect.UnsafePointer:
|
||||||
|
default:
|
||||||
|
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
||||||
|
offsetScalar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pv := reflect.NewAt(vt, upv)
|
||||||
|
rv = pv
|
||||||
|
for i := 0; i < indirects; i++ {
|
||||||
|
rv = rv.Elem()
|
||||||
|
}
|
||||||
|
return rv
|
||||||
|
}
|
||||||
38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
generated
vendored
Normal file
38
vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||||
|
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||||
|
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||||
|
// tag is deprecated and thus should not be used.
|
||||||
|
// +build js appengine safe disableunsafe
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||||
|
// not access to the unsafe package is available.
|
||||||
|
UnsafeDisabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// unsafeReflectValue typically converts the passed reflect.Value into a one
|
||||||
|
// that bypasses the typical safety restrictions preventing access to
|
||||||
|
// unaddressable and unexported data. However, doing this relies on access to
|
||||||
|
// the unsafe package. This is a stub version which simply returns the passed
|
||||||
|
// reflect.Value when the unsafe package is not available.
|
||||||
|
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||||
|
return v
|
||||||
|
}
|
||||||
341
vendor/github.com/davecgh/go-spew/spew/common.go
generated
vendored
Normal file
341
vendor/github.com/davecgh/go-spew/spew/common.go
generated
vendored
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Some constants in the form of bytes to avoid string overhead. This mirrors
|
||||||
|
// the technique used in the fmt package.
|
||||||
|
var (
|
||||||
|
panicBytes = []byte("(PANIC=")
|
||||||
|
plusBytes = []byte("+")
|
||||||
|
iBytes = []byte("i")
|
||||||
|
trueBytes = []byte("true")
|
||||||
|
falseBytes = []byte("false")
|
||||||
|
interfaceBytes = []byte("(interface {})")
|
||||||
|
commaNewlineBytes = []byte(",\n")
|
||||||
|
newlineBytes = []byte("\n")
|
||||||
|
openBraceBytes = []byte("{")
|
||||||
|
openBraceNewlineBytes = []byte("{\n")
|
||||||
|
closeBraceBytes = []byte("}")
|
||||||
|
asteriskBytes = []byte("*")
|
||||||
|
colonBytes = []byte(":")
|
||||||
|
colonSpaceBytes = []byte(": ")
|
||||||
|
openParenBytes = []byte("(")
|
||||||
|
closeParenBytes = []byte(")")
|
||||||
|
spaceBytes = []byte(" ")
|
||||||
|
pointerChainBytes = []byte("->")
|
||||||
|
nilAngleBytes = []byte("<nil>")
|
||||||
|
maxNewlineBytes = []byte("<max depth reached>\n")
|
||||||
|
maxShortBytes = []byte("<max>")
|
||||||
|
circularBytes = []byte("<already shown>")
|
||||||
|
circularShortBytes = []byte("<shown>")
|
||||||
|
invalidAngleBytes = []byte("<invalid>")
|
||||||
|
openBracketBytes = []byte("[")
|
||||||
|
closeBracketBytes = []byte("]")
|
||||||
|
percentBytes = []byte("%")
|
||||||
|
precisionBytes = []byte(".")
|
||||||
|
openAngleBytes = []byte("<")
|
||||||
|
closeAngleBytes = []byte(">")
|
||||||
|
openMapBytes = []byte("map[")
|
||||||
|
closeMapBytes = []byte("]")
|
||||||
|
lenEqualsBytes = []byte("len=")
|
||||||
|
capEqualsBytes = []byte("cap=")
|
||||||
|
)
|
||||||
|
|
||||||
|
// hexDigits is used to map a decimal value to a hex digit.
|
||||||
|
var hexDigits = "0123456789abcdef"
|
||||||
|
|
||||||
|
// catchPanic handles any panics that might occur during the handleMethods
|
||||||
|
// calls.
|
||||||
|
func catchPanic(w io.Writer, v reflect.Value) {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
w.Write(panicBytes)
|
||||||
|
fmt.Fprintf(w, "%v", err)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMethods attempts to call the Error and String methods on the underlying
|
||||||
|
// type the passed reflect.Value represents and outputes the result to Writer w.
|
||||||
|
//
|
||||||
|
// It handles panics in any called methods by catching and displaying the error
|
||||||
|
// as the formatted value.
|
||||||
|
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
|
||||||
|
// We need an interface to check if the type implements the error or
|
||||||
|
// Stringer interface. However, the reflect package won't give us an
|
||||||
|
// interface on certain things like unexported struct fields in order
|
||||||
|
// to enforce visibility rules. We use unsafe, when it's available,
|
||||||
|
// to bypass these restrictions since this package does not mutate the
|
||||||
|
// values.
|
||||||
|
if !v.CanInterface() {
|
||||||
|
if UnsafeDisabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose whether or not to do error and Stringer interface lookups against
|
||||||
|
// the base type or a pointer to the base type depending on settings.
|
||||||
|
// Technically calling one of these methods with a pointer receiver can
|
||||||
|
// mutate the value, however, types which choose to satisify an error or
|
||||||
|
// Stringer interface with a pointer receiver should not be mutating their
|
||||||
|
// state inside these interface methods.
|
||||||
|
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
|
||||||
|
v = unsafeReflectValue(v)
|
||||||
|
}
|
||||||
|
if v.CanAddr() {
|
||||||
|
v = v.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it an error or Stringer?
|
||||||
|
switch iface := v.Interface().(type) {
|
||||||
|
case error:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(iface.Error()))
|
||||||
|
return true
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
defer catchPanic(w, v)
|
||||||
|
if cs.ContinueOnMethod {
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.Write([]byte(iface.String()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// printBool outputs a boolean value as true or false to Writer w.
|
||||||
|
func printBool(w io.Writer, val bool) {
|
||||||
|
if val {
|
||||||
|
w.Write(trueBytes)
|
||||||
|
} else {
|
||||||
|
w.Write(falseBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printInt outputs a signed integer value to Writer w.
|
||||||
|
func printInt(w io.Writer, val int64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatInt(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printUint outputs an unsigned integer value to Writer w.
|
||||||
|
func printUint(w io.Writer, val uint64, base int) {
|
||||||
|
w.Write([]byte(strconv.FormatUint(val, base)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFloat outputs a floating point value using the specified precision,
|
||||||
|
// which is expected to be 32 or 64bit, to Writer w.
|
||||||
|
func printFloat(w io.Writer, val float64, precision int) {
|
||||||
|
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// printComplex outputs a complex value using the specified float precision
|
||||||
|
// for the real and imaginary parts to Writer w.
|
||||||
|
func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||||
|
r := real(c)
|
||||||
|
w.Write(openParenBytes)
|
||||||
|
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
|
||||||
|
i := imag(c)
|
||||||
|
if i >= 0 {
|
||||||
|
w.Write(plusBytes)
|
||||||
|
}
|
||||||
|
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
|
||||||
|
w.Write(iBytes)
|
||||||
|
w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
||||||
|
// prefix to Writer w.
|
||||||
|
func printHexPtr(w io.Writer, p uintptr) {
|
||||||
|
// Null pointer.
|
||||||
|
num := uint64(p)
|
||||||
|
if num == 0 {
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
|
||||||
|
buf := make([]byte, 18)
|
||||||
|
|
||||||
|
// It's simpler to construct the hex string right to left.
|
||||||
|
base := uint64(16)
|
||||||
|
i := len(buf) - 1
|
||||||
|
for num >= base {
|
||||||
|
buf[i] = hexDigits[num%base]
|
||||||
|
num /= base
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
buf[i] = hexDigits[num]
|
||||||
|
|
||||||
|
// Add '0x' prefix.
|
||||||
|
i--
|
||||||
|
buf[i] = 'x'
|
||||||
|
i--
|
||||||
|
buf[i] = '0'
|
||||||
|
|
||||||
|
// Strip unused leading bytes.
|
||||||
|
buf = buf[i:]
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
|
||||||
|
// elements to be sorted.
|
||||||
|
type valuesSorter struct {
|
||||||
|
values []reflect.Value
|
||||||
|
strings []string // either nil or same len and values
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValuesSorter initializes a valuesSorter instance, which holds a set of
|
||||||
|
// surrogate keys on which the data should be sorted. It uses flags in
|
||||||
|
// ConfigState to decide if and how to populate those surrogate keys.
|
||||||
|
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
|
||||||
|
vs := &valuesSorter{values: values, cs: cs}
|
||||||
|
if canSortSimply(vs.values[0].Kind()) {
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
if !cs.DisableMethods {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
if !handleMethods(cs, &b, vs.values[i]) {
|
||||||
|
vs.strings = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
vs.strings[i] = b.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vs.strings == nil && cs.SpewKeys {
|
||||||
|
vs.strings = make([]string, len(values))
|
||||||
|
for i := range vs.values {
|
||||||
|
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
|
||||||
|
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
|
||||||
|
// directly, or whether it should be considered for sorting by surrogate keys
|
||||||
|
// (if the ConfigState allows it).
|
||||||
|
func canSortSimply(kind reflect.Kind) bool {
|
||||||
|
// This switch parallels valueSortLess, except for the default case.
|
||||||
|
switch kind {
|
||||||
|
case reflect.Bool:
|
||||||
|
return true
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return true
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return true
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
case reflect.String:
|
||||||
|
return true
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return true
|
||||||
|
case reflect.Array:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of values in the slice. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Len() int {
|
||||||
|
return len(s.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps the values at the passed indices. It is part of the
|
||||||
|
// sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Swap(i, j int) {
|
||||||
|
s.values[i], s.values[j] = s.values[j], s.values[i]
|
||||||
|
if s.strings != nil {
|
||||||
|
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueSortLess returns whether the first value should sort before the second
|
||||||
|
// value. It is used by valueSorter.Less as part of the sort.Interface
|
||||||
|
// implementation.
|
||||||
|
func valueSortLess(a, b reflect.Value) bool {
|
||||||
|
switch a.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return !a.Bool() && b.Bool()
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
return a.Int() < b.Int()
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return a.Float() < b.Float()
|
||||||
|
case reflect.String:
|
||||||
|
return a.String() < b.String()
|
||||||
|
case reflect.Uintptr:
|
||||||
|
return a.Uint() < b.Uint()
|
||||||
|
case reflect.Array:
|
||||||
|
// Compare the contents of both arrays.
|
||||||
|
l := a.Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
av := a.Index(i)
|
||||||
|
bv := b.Index(i)
|
||||||
|
if av.Interface() == bv.Interface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return valueSortLess(av, bv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.String() < b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less returns whether the value at index i should sort before the
|
||||||
|
// value at index j. It is part of the sort.Interface implementation.
|
||||||
|
func (s *valuesSorter) Less(i, j int) bool {
|
||||||
|
if s.strings == nil {
|
||||||
|
return valueSortLess(s.values[i], s.values[j])
|
||||||
|
}
|
||||||
|
return s.strings[i] < s.strings[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortValues is a sort function that handles both native types and any type that
|
||||||
|
// can be converted to error or Stringer. Other inputs are sorted according to
|
||||||
|
// their Value.String() value to ensure display stability.
|
||||||
|
func sortValues(values []reflect.Value, cs *ConfigState) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Sort(newValuesSorter(values, cs))
|
||||||
|
}
|
||||||
306
vendor/github.com/davecgh/go-spew/spew/config.go
generated
vendored
Normal file
306
vendor/github.com/davecgh/go-spew/spew/config.go
generated
vendored
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigState houses the configuration options used by spew to format and
|
||||||
|
// display values. There is a global instance, Config, that is used to control
|
||||||
|
// all top-level Formatter and Dump functionality. Each ConfigState instance
|
||||||
|
// provides methods equivalent to the top-level functions.
|
||||||
|
//
|
||||||
|
// The zero value for ConfigState provides no indentation. You would typically
|
||||||
|
// want to set it to a space or a tab.
|
||||||
|
//
|
||||||
|
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
|
||||||
|
// with default settings. See the documentation of NewDefaultConfig for default
|
||||||
|
// values.
|
||||||
|
type ConfigState struct {
|
||||||
|
// Indent specifies the string to use for each indentation level. The
|
||||||
|
// global config instance that all top-level functions use set this to a
|
||||||
|
// single space by default. If you would like more indentation, you might
|
||||||
|
// set this to a tab with "\t" or perhaps two spaces with " ".
|
||||||
|
Indent string
|
||||||
|
|
||||||
|
// MaxDepth controls the maximum number of levels to descend into nested
|
||||||
|
// data structures. The default, 0, means there is no limit.
|
||||||
|
//
|
||||||
|
// NOTE: Circular data structures are properly detected, so it is not
|
||||||
|
// necessary to set this value unless you specifically want to limit deeply
|
||||||
|
// nested data structures.
|
||||||
|
MaxDepth int
|
||||||
|
|
||||||
|
// DisableMethods specifies whether or not error and Stringer interfaces are
|
||||||
|
// invoked for types that implement them.
|
||||||
|
DisableMethods bool
|
||||||
|
|
||||||
|
// DisablePointerMethods specifies whether or not to check for and invoke
|
||||||
|
// error and Stringer interfaces on types which only accept a pointer
|
||||||
|
// receiver when the current type is not a pointer.
|
||||||
|
//
|
||||||
|
// NOTE: This might be an unsafe action since calling one of these methods
|
||||||
|
// with a pointer receiver could technically mutate the value, however,
|
||||||
|
// in practice, types which choose to satisify an error or Stringer
|
||||||
|
// interface with a pointer receiver should not be mutating their state
|
||||||
|
// inside these interface methods. As a result, this option relies on
|
||||||
|
// access to the unsafe package, so it will not have any effect when
|
||||||
|
// running in environments without access to the unsafe package such as
|
||||||
|
// Google App Engine or with the "safe" build tag specified.
|
||||||
|
DisablePointerMethods bool
|
||||||
|
|
||||||
|
// DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
// pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
DisablePointerAddresses bool
|
||||||
|
|
||||||
|
// DisableCapacities specifies whether to disable the printing of capacities
|
||||||
|
// for arrays, slices, maps and channels. This is useful when diffing
|
||||||
|
// data structures in tests.
|
||||||
|
DisableCapacities bool
|
||||||
|
|
||||||
|
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||||
|
// a custom error or Stringer interface is invoked. The default, false,
|
||||||
|
// means it will print the results of invoking the custom error or Stringer
|
||||||
|
// interface and return immediately instead of continuing to recurse into
|
||||||
|
// the internals of the data type.
|
||||||
|
//
|
||||||
|
// NOTE: This flag does not have any effect if method invocation is disabled
|
||||||
|
// via the DisableMethods or DisablePointerMethods options.
|
||||||
|
ContinueOnMethod bool
|
||||||
|
|
||||||
|
// SortKeys specifies map keys should be sorted before being printed. Use
|
||||||
|
// this to have a more deterministic, diffable output. Note that only
|
||||||
|
// native types (bool, int, uint, floats, uintptr and string) and types
|
||||||
|
// that support the error or Stringer interfaces (if methods are
|
||||||
|
// enabled) are supported, with other types sorted according to the
|
||||||
|
// reflect.Value.String() output which guarantees display stability.
|
||||||
|
SortKeys bool
|
||||||
|
|
||||||
|
// SpewKeys specifies that, as a last resort attempt, map keys should
|
||||||
|
// be spewed to strings and sorted by those strings. This is only
|
||||||
|
// considered if SortKeys is true.
|
||||||
|
SpewKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the active configuration of the top-level functions.
|
||||||
|
// The configuration can be changed by modifying the contents of spew.Config.
|
||||||
|
var Config = ConfigState{Indent: " "}
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the formatted string as a value that satisfies error. See NewFormatter
|
||||||
|
// for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||||
|
// the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a Formatter interface returned by c.NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
|
||||||
|
func (c *ConfigState) Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(c.convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
c.Printf, c.Println, or c.Printf.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(c, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(c, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by modifying the public members
|
||||||
|
of c. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func (c *ConfigState) Dump(a ...interface{}) {
|
||||||
|
fdump(c, os.Stdout, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func (c *ConfigState) Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(c, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a spew Formatter interface using
|
||||||
|
// the ConfigState associated with s.
|
||||||
|
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = newFormatter(c, arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||||
|
//
|
||||||
|
// Indent: " "
|
||||||
|
// MaxDepth: 0
|
||||||
|
// DisableMethods: false
|
||||||
|
// DisablePointerMethods: false
|
||||||
|
// ContinueOnMethod: false
|
||||||
|
// SortKeys: false
|
||||||
|
func NewDefaultConfig() *ConfigState {
|
||||||
|
return &ConfigState{Indent: " "}
|
||||||
|
}
|
||||||
211
vendor/github.com/davecgh/go-spew/spew/doc.go
generated
vendored
Normal file
211
vendor/github.com/davecgh/go-spew/spew/doc.go
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package spew implements a deep pretty printer for Go data structures to aid in
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
A quick overview of the additional features spew provides over the built-in
|
||||||
|
printing facilities for Go data types are as follows:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output (only when using
|
||||||
|
Dump style)
|
||||||
|
|
||||||
|
There are two different approaches spew allows for dumping Go data structures:
|
||||||
|
|
||||||
|
* Dump style which prints with newlines, customizable indentation,
|
||||||
|
and additional debug information such as types and all pointer addresses
|
||||||
|
used to indirect to the final value
|
||||||
|
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||||
|
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||||
|
similar to the default %v while providing the additional functionality
|
||||||
|
outlined above and passing unsupported format verbs such as %x and %q
|
||||||
|
along to fmt
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
|
||||||
|
This section demonstrates how to quickly get started with spew. See the
|
||||||
|
sections below for further details on formatting and configuration options.
|
||||||
|
|
||||||
|
To dump a variable with full newlines, indentation, type, and pointer
|
||||||
|
information use Dump, Fdump, or Sdump:
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Alternatively, if you would prefer to use format strings with a compacted inline
|
||||||
|
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||||
|
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||||
|
%#+v (adds types and pointer addresses):
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
Configuration Options
|
||||||
|
|
||||||
|
Configuration of spew is handled by fields in the ConfigState type. For
|
||||||
|
convenience, all of the top-level functions use a global state available
|
||||||
|
via the spew.Config global.
|
||||||
|
|
||||||
|
It is also possible to create a ConfigState instance that provides methods
|
||||||
|
equivalent to the top-level functions. This allows concurrent configuration
|
||||||
|
options. See the ConfigState documentation for more details.
|
||||||
|
|
||||||
|
The following configuration options are available:
|
||||||
|
* Indent
|
||||||
|
String to use for each indentation level for Dump functions.
|
||||||
|
It is a single space by default. A popular alternative is "\t".
|
||||||
|
|
||||||
|
* MaxDepth
|
||||||
|
Maximum number of levels to descend into nested data structures.
|
||||||
|
There is no limit by default.
|
||||||
|
|
||||||
|
* DisableMethods
|
||||||
|
Disables invocation of error and Stringer interface methods.
|
||||||
|
Method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerMethods
|
||||||
|
Disables invocation of error and Stringer interface methods on types
|
||||||
|
which only accept pointer receivers from non-pointer variables.
|
||||||
|
Pointer method invocation is enabled by default.
|
||||||
|
|
||||||
|
* DisablePointerAddresses
|
||||||
|
DisablePointerAddresses specifies whether to disable the printing of
|
||||||
|
pointer addresses. This is useful when diffing data structures in tests.
|
||||||
|
|
||||||
|
* DisableCapacities
|
||||||
|
DisableCapacities specifies whether to disable the printing of
|
||||||
|
capacities for arrays, slices, maps and channels. This is useful when
|
||||||
|
diffing data structures in tests.
|
||||||
|
|
||||||
|
* ContinueOnMethod
|
||||||
|
Enables recursion into types after invoking error and Stringer interface
|
||||||
|
methods. Recursion after method invocation is disabled by default.
|
||||||
|
|
||||||
|
* SortKeys
|
||||||
|
Specifies map keys should be sorted before being printed. Use
|
||||||
|
this to have a more deterministic, diffable output. Note that
|
||||||
|
only native types (bool, int, uint, floats, uintptr and string)
|
||||||
|
and types which implement error or Stringer interfaces are
|
||||||
|
supported with other types sorted according to the
|
||||||
|
reflect.Value.String() output which guarantees display
|
||||||
|
stability. Natural map order is used by default.
|
||||||
|
|
||||||
|
* SpewKeys
|
||||||
|
Specifies that, as a last resort attempt, map keys should be
|
||||||
|
spewed to strings and sorted by those strings. This is only
|
||||||
|
considered if SortKeys is true.
|
||||||
|
|
||||||
|
Dump Usage
|
||||||
|
|
||||||
|
Simply call spew.Dump with a list of variables you want to dump:
|
||||||
|
|
||||||
|
spew.Dump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
You may also call spew.Fdump if you would prefer to output to an arbitrary
|
||||||
|
io.Writer. For example, to dump to standard error:
|
||||||
|
|
||||||
|
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
A third option is to call spew.Sdump to get the formatted output as a string:
|
||||||
|
|
||||||
|
str := spew.Sdump(myVar1, myVar2, ...)
|
||||||
|
|
||||||
|
Sample Dump Output
|
||||||
|
|
||||||
|
See the Dump example for details on the setup of the types and variables being
|
||||||
|
shown here.
|
||||||
|
|
||||||
|
(main.Foo) {
|
||||||
|
unexportedField: (*main.Bar)(0xf84002e210)({
|
||||||
|
flag: (main.Flag) flagTwo,
|
||||||
|
data: (uintptr) <nil>
|
||||||
|
}),
|
||||||
|
ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||||
|
(string) (len=3) "one": (bool) true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||||
|
command as shown.
|
||||||
|
([]uint8) (len=32 cap=32) {
|
||||||
|
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||||
|
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||||
|
00000020 31 32 |12|
|
||||||
|
}
|
||||||
|
|
||||||
|
Custom Formatter
|
||||||
|
|
||||||
|
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||||
|
so that it integrates cleanly with standard fmt package printing functions. The
|
||||||
|
formatter is useful for inline printing of smaller data types similar to the
|
||||||
|
standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Custom Formatter Usage
|
||||||
|
|
||||||
|
The simplest way to make use of the spew custom formatter is to call one of the
|
||||||
|
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||||
|
functions have syntax you are most likely already familiar with:
|
||||||
|
|
||||||
|
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
spew.Println(myVar, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||||
|
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||||
|
|
||||||
|
See the Index for the full list convenience functions.
|
||||||
|
|
||||||
|
Sample Formatter Output
|
||||||
|
|
||||||
|
Double pointer to a uint8:
|
||||||
|
%v: <**>5
|
||||||
|
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||||
|
%#v: (**uint8)5
|
||||||
|
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||||
|
|
||||||
|
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||||
|
%v: <*>{1 <*><shown>}
|
||||||
|
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||||
|
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||||
|
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
||||||
|
|
||||||
|
See the Printf example for details on the setup of variables being shown
|
||||||
|
here.
|
||||||
|
|
||||||
|
Errors
|
||||||
|
|
||||||
|
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||||
|
detects them and handles them internally by printing the panic information
|
||||||
|
inline with the output. Since spew is intended to provide deep pretty printing
|
||||||
|
capabilities on structures, it intentionally does not return any errors.
|
||||||
|
*/
|
||||||
|
package spew
|
||||||
509
vendor/github.com/davecgh/go-spew/spew/dump.go
generated
vendored
Normal file
509
vendor/github.com/davecgh/go-spew/spew/dump.go
generated
vendored
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// uint8Type is a reflect.Type representing a uint8. It is used to
|
||||||
|
// convert cgo types to uint8 slices for hexdumping.
|
||||||
|
uint8Type = reflect.TypeOf(uint8(0))
|
||||||
|
|
||||||
|
// cCharRE is a regular expression that matches a cgo char.
|
||||||
|
// It is used to detect character arrays to hexdump them.
|
||||||
|
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
||||||
|
|
||||||
|
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||||
|
// char. It is used to detect unsigned character arrays to hexdump
|
||||||
|
// them.
|
||||||
|
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
||||||
|
|
||||||
|
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||||
|
// It is used to detect uint8_t arrays to hexdump them.
|
||||||
|
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
||||||
|
)
|
||||||
|
|
||||||
|
// dumpState contains information about the state of a dump operation.
|
||||||
|
type dumpState struct {
|
||||||
|
w io.Writer
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
ignoreNextIndent bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// indent performs indentation according to the depth level and cs.Indent
|
||||||
|
// option.
|
||||||
|
func (d *dumpState) indent() {
|
||||||
|
if d.ignoreNextIndent {
|
||||||
|
d.ignoreNextIndent = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range d.pointers {
|
||||||
|
if depth >= d.depth {
|
||||||
|
delete(d.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by dereferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d.pointers[addr] = d.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type information.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
d.w.Write([]byte(ve.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
|
||||||
|
// Display pointer information.
|
||||||
|
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
d.w.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(d.w, addr)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
switch {
|
||||||
|
case nilFound == true:
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound == true:
|
||||||
|
d.w.Write(circularBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
d.ignoreNextType = true
|
||||||
|
d.dump(ve)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
|
||||||
|
// reflection) arrays and slices are dumped in hexdump -C fashion.
|
||||||
|
func (d *dumpState) dumpSlice(v reflect.Value) {
|
||||||
|
// Determine whether this type should be hex dumped or not. Also,
|
||||||
|
// for types which should be hexdumped, try to use the underlying data
|
||||||
|
// first, then fall back to trying to convert them to a uint8 slice.
|
||||||
|
var buf []uint8
|
||||||
|
doConvert := false
|
||||||
|
doHexDump := false
|
||||||
|
numEntries := v.Len()
|
||||||
|
if numEntries > 0 {
|
||||||
|
vt := v.Index(0).Type()
|
||||||
|
vts := vt.String()
|
||||||
|
switch {
|
||||||
|
// C types that need to be converted.
|
||||||
|
case cCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUnsignedCharRE.MatchString(vts):
|
||||||
|
fallthrough
|
||||||
|
case cUint8tCharRE.MatchString(vts):
|
||||||
|
doConvert = true
|
||||||
|
|
||||||
|
// Try to use existing uint8 slices and fall back to converting
|
||||||
|
// and copying if that fails.
|
||||||
|
case vt.Kind() == reflect.Uint8:
|
||||||
|
// We need an addressable interface to convert the type
|
||||||
|
// to a byte slice. However, the reflect package won't
|
||||||
|
// give us an interface on certain things like
|
||||||
|
// unexported struct fields in order to enforce
|
||||||
|
// visibility rules. We use unsafe, when available, to
|
||||||
|
// bypass these restrictions since this package does not
|
||||||
|
// mutate the values.
|
||||||
|
vs := v
|
||||||
|
if !vs.CanInterface() || !vs.CanAddr() {
|
||||||
|
vs = unsafeReflectValue(vs)
|
||||||
|
}
|
||||||
|
if !UnsafeDisabled {
|
||||||
|
vs = vs.Slice(0, numEntries)
|
||||||
|
|
||||||
|
// Use the existing uint8 slice if it can be
|
||||||
|
// type asserted.
|
||||||
|
iface := vs.Interface()
|
||||||
|
if slice, ok := iface.([]uint8); ok {
|
||||||
|
buf = slice
|
||||||
|
doHexDump = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The underlying data needs to be converted if it can't
|
||||||
|
// be type asserted to a uint8 slice.
|
||||||
|
doConvert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy and convert the underlying type if needed.
|
||||||
|
if doConvert && vt.ConvertibleTo(uint8Type) {
|
||||||
|
// Convert and copy each element into a uint8 byte
|
||||||
|
// slice.
|
||||||
|
buf = make([]uint8, numEntries)
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
vv := v.Index(i)
|
||||||
|
buf[i] = uint8(vv.Convert(uint8Type).Uint())
|
||||||
|
}
|
||||||
|
doHexDump = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hexdump the entire slice as needed.
|
||||||
|
if doHexDump {
|
||||||
|
indent := strings.Repeat(d.cs.Indent, d.depth)
|
||||||
|
str := indent + hex.Dump(buf)
|
||||||
|
str = strings.Replace(str, "\n", "\n"+indent, -1)
|
||||||
|
str = strings.TrimRight(str, d.cs.Indent)
|
||||||
|
d.w.Write([]byte(str))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively call dump for each item.
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
d.dump(d.unpackValue(v.Index(i)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump is the main workhorse for dumping a value. It uses the passed reflect
|
||||||
|
// value to figure out what kind of object we are dealing with and formats it
|
||||||
|
// appropriately. It is a recursive function, however circular data structures
|
||||||
|
// are detected and handled properly.
|
||||||
|
func (d *dumpState) dump(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
d.w.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
d.indent()
|
||||||
|
d.dumpPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !d.ignoreNextType {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
d.w.Write([]byte(v.Type().String()))
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.ignoreNextType = false
|
||||||
|
|
||||||
|
// Display length and capacity if the built-in len and cap functions
|
||||||
|
// work with the value's kind and the len/cap itself is non-zero.
|
||||||
|
valueLen, valueCap := 0, 0
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Array, reflect.Slice, reflect.Chan:
|
||||||
|
valueLen, valueCap = v.Len(), v.Cap()
|
||||||
|
case reflect.Map, reflect.String:
|
||||||
|
valueLen = v.Len()
|
||||||
|
}
|
||||||
|
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
d.w.Write(openParenBytes)
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(lenEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueLen), 10)
|
||||||
|
}
|
||||||
|
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||||
|
if valueLen != 0 {
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
d.w.Write(capEqualsBytes)
|
||||||
|
printInt(d.w, int64(valueCap), 10)
|
||||||
|
}
|
||||||
|
d.w.Write(closeParenBytes)
|
||||||
|
d.w.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods flag
|
||||||
|
// is enabled
|
||||||
|
if !d.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(d.cs, d.w, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(d.w, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(d.w, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(d.w, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(d.w, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(d.w, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(d.w, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(d.w, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.dumpSlice(v)
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
d.w.Write([]byte(strconv.Quote(v.String())))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
d.w.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if d.cs.SortKeys {
|
||||||
|
sortValues(keys, d.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
d.dump(d.unpackValue(key))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.MapIndex(key)))
|
||||||
|
if i < (numEntries - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
d.w.Write(openBraceNewlineBytes)
|
||||||
|
d.depth++
|
||||||
|
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(maxNewlineBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
numFields := v.NumField()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
d.indent()
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
d.w.Write([]byte(vtf.Name))
|
||||||
|
d.w.Write(colonSpaceBytes)
|
||||||
|
d.ignoreNextIndent = true
|
||||||
|
d.dump(d.unpackValue(v.Field(i)))
|
||||||
|
if i < (numFields - 1) {
|
||||||
|
d.w.Write(commaNewlineBytes)
|
||||||
|
} else {
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.depth--
|
||||||
|
d.indent()
|
||||||
|
d.w.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(d.w, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(d.w, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it in case any new
|
||||||
|
// types are added.
|
||||||
|
default:
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(d.w, "%v", v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fdump is a helper function to consolidate the logic from the various public
|
||||||
|
// methods which take varying writers and config states.
|
||||||
|
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
|
||||||
|
for _, arg := range a {
|
||||||
|
if arg == nil {
|
||||||
|
w.Write(interfaceBytes)
|
||||||
|
w.Write(spaceBytes)
|
||||||
|
w.Write(nilAngleBytes)
|
||||||
|
w.Write(newlineBytes)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d := dumpState{w: w, cs: cs}
|
||||||
|
d.pointers = make(map[uintptr]int)
|
||||||
|
d.dump(reflect.ValueOf(arg))
|
||||||
|
d.w.Write(newlineBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||||
|
// exactly the same as Dump.
|
||||||
|
func Fdump(w io.Writer, a ...interface{}) {
|
||||||
|
fdump(&Config, w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||||
|
// as Dump.
|
||||||
|
func Sdump(a ...interface{}) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fdump(&Config, &buf, a...)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dump displays the passed parameters to standard out with newlines, customizable
|
||||||
|
indentation, and additional debug information such as complete types and all
|
||||||
|
pointer addresses used to indirect to the final value. It provides the
|
||||||
|
following features over the built-in printing facilities provided by the fmt
|
||||||
|
package:
|
||||||
|
|
||||||
|
* Pointers are dereferenced and followed
|
||||||
|
* Circular data structures are detected and handled properly
|
||||||
|
* Custom Stringer/error interfaces are optionally invoked, including
|
||||||
|
on unexported types
|
||||||
|
* Custom types which only implement the Stringer/error interfaces via
|
||||||
|
a pointer receiver are optionally invoked when passing non-pointer
|
||||||
|
variables
|
||||||
|
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||||
|
includes offsets, byte values in hex, and ASCII output
|
||||||
|
|
||||||
|
The configuration options are controlled by an exported package global,
|
||||||
|
spew.Config. See ConfigState for options documentation.
|
||||||
|
|
||||||
|
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||||
|
get the formatted result as a string.
|
||||||
|
*/
|
||||||
|
func Dump(a ...interface{}) {
|
||||||
|
fdump(&Config, os.Stdout, a...)
|
||||||
|
}
|
||||||
419
vendor/github.com/davecgh/go-spew/spew/format.go
generated
vendored
Normal file
419
vendor/github.com/davecgh/go-spew/spew/format.go
generated
vendored
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// supportedFlags is a list of all the character flags supported by fmt package.
|
||||||
|
const supportedFlags = "0-+# "
|
||||||
|
|
||||||
|
// formatState implements the fmt.Formatter interface and contains information
|
||||||
|
// about the state of a formatting operation. The NewFormatter function can
|
||||||
|
// be used to get a new Formatter which can be used directly as arguments
|
||||||
|
// in standard fmt package printing calls.
|
||||||
|
type formatState struct {
|
||||||
|
value interface{}
|
||||||
|
fs fmt.State
|
||||||
|
depth int
|
||||||
|
pointers map[uintptr]int
|
||||||
|
ignoreNextType bool
|
||||||
|
cs *ConfigState
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDefaultFormat recreates the original format string without precision
|
||||||
|
// and width information to pass in to fmt.Sprintf in the case of an
|
||||||
|
// unrecognized type. Unless new types are added to the language, this
|
||||||
|
// function won't ever be called.
|
||||||
|
func (f *formatState) buildDefaultFormat() (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune('v')
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// constructOrigFormat recreates the original format string including precision
|
||||||
|
// and width information to pass along to the standard fmt package. This allows
|
||||||
|
// automatic deferral of all format strings this package doesn't support.
|
||||||
|
func (f *formatState) constructOrigFormat(verb rune) (format string) {
|
||||||
|
buf := bytes.NewBuffer(percentBytes)
|
||||||
|
|
||||||
|
for _, flag := range supportedFlags {
|
||||||
|
if f.fs.Flag(int(flag)) {
|
||||||
|
buf.WriteRune(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width, ok := f.fs.Width(); ok {
|
||||||
|
buf.WriteString(strconv.Itoa(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
if precision, ok := f.fs.Precision(); ok {
|
||||||
|
buf.Write(precisionBytes)
|
||||||
|
buf.WriteString(strconv.Itoa(precision))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteRune(verb)
|
||||||
|
|
||||||
|
format = buf.String()
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackValue returns values inside of non-nil interfaces when possible and
|
||||||
|
// ensures that types for values which have been unpacked from an interface
|
||||||
|
// are displayed when the show types flag is also set.
|
||||||
|
// This is useful for data types like structs, arrays, slices, and maps which
|
||||||
|
// can contain varying types packed inside an interface.
|
||||||
|
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
if v.Kind() == reflect.Interface {
|
||||||
|
f.ignoreNextType = false
|
||||||
|
if !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatPtr handles formatting of pointers by indirecting them as necessary.
|
||||||
|
func (f *formatState) formatPtr(v reflect.Value) {
|
||||||
|
// Display nil if top level pointer is nil.
|
||||||
|
showTypes := f.fs.Flag('#')
|
||||||
|
if v.IsNil() && (!showTypes || f.ignoreNextType) {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove pointers at or below the current depth from map used to detect
|
||||||
|
// circular refs.
|
||||||
|
for k, depth := range f.pointers {
|
||||||
|
if depth >= f.depth {
|
||||||
|
delete(f.pointers, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list of all dereferenced pointers to possibly show later.
|
||||||
|
pointerChain := make([]uintptr, 0)
|
||||||
|
|
||||||
|
// Figure out how many levels of indirection there are by derferencing
|
||||||
|
// pointers and unpacking interfaces down the chain while detecting circular
|
||||||
|
// references.
|
||||||
|
nilFound := false
|
||||||
|
cycleFound := false
|
||||||
|
indirects := 0
|
||||||
|
ve := v
|
||||||
|
for ve.Kind() == reflect.Ptr {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indirects++
|
||||||
|
addr := ve.Pointer()
|
||||||
|
pointerChain = append(pointerChain, addr)
|
||||||
|
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
|
||||||
|
cycleFound = true
|
||||||
|
indirects--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
f.pointers[addr] = f.depth
|
||||||
|
|
||||||
|
ve = ve.Elem()
|
||||||
|
if ve.Kind() == reflect.Interface {
|
||||||
|
if ve.IsNil() {
|
||||||
|
nilFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ve = ve.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display type or indirection level depending on flags.
|
||||||
|
if showTypes && !f.ignoreNextType {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||||
|
f.fs.Write([]byte(ve.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
} else {
|
||||||
|
if nilFound || cycleFound {
|
||||||
|
indirects += strings.Count(ve.Type().String(), "*")
|
||||||
|
}
|
||||||
|
f.fs.Write(openAngleBytes)
|
||||||
|
f.fs.Write([]byte(strings.Repeat("*", indirects)))
|
||||||
|
f.fs.Write(closeAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display pointer information depending on flags.
|
||||||
|
if f.fs.Flag('+') && (len(pointerChain) > 0) {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
for i, addr := range pointerChain {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(pointerChainBytes)
|
||||||
|
}
|
||||||
|
printHexPtr(f.fs, addr)
|
||||||
|
}
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display dereferenced value.
|
||||||
|
switch {
|
||||||
|
case nilFound == true:
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
|
||||||
|
case cycleFound == true:
|
||||||
|
f.fs.Write(circularShortBytes)
|
||||||
|
|
||||||
|
default:
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(ve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format is the main workhorse for providing the Formatter interface. It
|
||||||
|
// uses the passed reflect value to figure out what kind of object we are
|
||||||
|
// dealing with and formats it appropriately. It is a recursive function,
|
||||||
|
// however circular data structures are detected and handled properly.
|
||||||
|
func (f *formatState) format(v reflect.Value) {
|
||||||
|
// Handle invalid reflect values immediately.
|
||||||
|
kind := v.Kind()
|
||||||
|
if kind == reflect.Invalid {
|
||||||
|
f.fs.Write(invalidAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointers specially.
|
||||||
|
if kind == reflect.Ptr {
|
||||||
|
f.formatPtr(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type information unless already handled elsewhere.
|
||||||
|
if !f.ignoreNextType && f.fs.Flag('#') {
|
||||||
|
f.fs.Write(openParenBytes)
|
||||||
|
f.fs.Write([]byte(v.Type().String()))
|
||||||
|
f.fs.Write(closeParenBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = false
|
||||||
|
|
||||||
|
// Call Stringer/error interfaces if they exist and the handle methods
|
||||||
|
// flag is enabled.
|
||||||
|
if !f.cs.DisableMethods {
|
||||||
|
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||||
|
if handled := handleMethods(f.cs, f.fs, v); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Invalid:
|
||||||
|
// Do nothing. We should never get here since invalid has already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
printBool(f.fs, v.Bool())
|
||||||
|
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||||
|
printInt(f.fs, v.Int(), 10)
|
||||||
|
|
||||||
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||||
|
printUint(f.fs, v.Uint(), 10)
|
||||||
|
|
||||||
|
case reflect.Float32:
|
||||||
|
printFloat(f.fs, v.Float(), 32)
|
||||||
|
|
||||||
|
case reflect.Float64:
|
||||||
|
printFloat(f.fs, v.Float(), 64)
|
||||||
|
|
||||||
|
case reflect.Complex64:
|
||||||
|
printComplex(f.fs, v.Complex(), 32)
|
||||||
|
|
||||||
|
case reflect.Complex128:
|
||||||
|
printComplex(f.fs, v.Complex(), 64)
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case reflect.Array:
|
||||||
|
f.fs.Write(openBracketBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
numEntries := v.Len()
|
||||||
|
for i := 0; i < numEntries; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBracketBytes)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
f.fs.Write([]byte(v.String()))
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
// The only time we should get here is for nil interfaces due to
|
||||||
|
// unpackValue calls.
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
// Do nothing. We should never get here since pointers have already
|
||||||
|
// been handled above.
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
// nil maps should be indicated as different than empty maps
|
||||||
|
if v.IsNil() {
|
||||||
|
f.fs.Write(nilAngleBytes)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fs.Write(openMapBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
keys := v.MapKeys()
|
||||||
|
if f.cs.SortKeys {
|
||||||
|
sortValues(keys, f.cs)
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(key))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
f.ignoreNextType = true
|
||||||
|
f.format(f.unpackValue(v.MapIndex(key)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeMapBytes)
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
numFields := v.NumField()
|
||||||
|
f.fs.Write(openBraceBytes)
|
||||||
|
f.depth++
|
||||||
|
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||||
|
f.fs.Write(maxShortBytes)
|
||||||
|
} else {
|
||||||
|
vt := v.Type()
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
f.fs.Write(spaceBytes)
|
||||||
|
}
|
||||||
|
vtf := vt.Field(i)
|
||||||
|
if f.fs.Flag('+') || f.fs.Flag('#') {
|
||||||
|
f.fs.Write([]byte(vtf.Name))
|
||||||
|
f.fs.Write(colonBytes)
|
||||||
|
}
|
||||||
|
f.format(f.unpackValue(v.Field(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.depth--
|
||||||
|
f.fs.Write(closeBraceBytes)
|
||||||
|
|
||||||
|
case reflect.Uintptr:
|
||||||
|
printHexPtr(f.fs, uintptr(v.Uint()))
|
||||||
|
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
printHexPtr(f.fs, v.Pointer())
|
||||||
|
|
||||||
|
// There were not any other types at the time this code was written, but
|
||||||
|
// fall back to letting the default fmt package handle it if any get added.
|
||||||
|
default:
|
||||||
|
format := f.buildDefaultFormat()
|
||||||
|
if v.CanInterface() {
|
||||||
|
fmt.Fprintf(f.fs, format, v.Interface())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f.fs, format, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
|
||||||
|
// details.
|
||||||
|
func (f *formatState) Format(fs fmt.State, verb rune) {
|
||||||
|
f.fs = fs
|
||||||
|
|
||||||
|
// Use standard formatting for verbs that are not v.
|
||||||
|
if verb != 'v' {
|
||||||
|
format := f.constructOrigFormat(verb)
|
||||||
|
fmt.Fprintf(fs, format, f.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.value == nil {
|
||||||
|
if fs.Flag('#') {
|
||||||
|
fs.Write(interfaceBytes)
|
||||||
|
}
|
||||||
|
fs.Write(nilAngleBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.format(reflect.ValueOf(f.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFormatter is a helper function to consolidate the logic from the various
|
||||||
|
// public methods which take varying config states.
|
||||||
|
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
|
||||||
|
fs := &formatState{value: v, cs: cs}
|
||||||
|
fs.pointers = make(map[uintptr]int)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||||
|
interface. As a result, it integrates cleanly with standard fmt package
|
||||||
|
printing functions. The formatter is useful for inline printing of smaller data
|
||||||
|
types similar to the standard %v format specifier.
|
||||||
|
|
||||||
|
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||||
|
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||||
|
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||||
|
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||||
|
the width and precision arguments (however they will still work on the format
|
||||||
|
specifiers not handled by the custom formatter).
|
||||||
|
|
||||||
|
Typically this function shouldn't be called directly. It is much easier to make
|
||||||
|
use of the custom formatter by calling one of the convenience functions such as
|
||||||
|
Printf, Println, or Fprintf.
|
||||||
|
*/
|
||||||
|
func NewFormatter(v interface{}) fmt.Formatter {
|
||||||
|
return newFormatter(&Config, v)
|
||||||
|
}
|
||||||
148
vendor/github.com/davecgh/go-spew/spew/spew.go
generated
vendored
Normal file
148
vendor/github.com/davecgh/go-spew/spew/spew.go
generated
vendored
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package spew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the formatted string as a value that satisfies error. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Errorf(format string, a ...interface{}) (err error) {
|
||||||
|
return fmt.Errorf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprint(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintf(w, format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Fprintln(w, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Print(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Print(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Printf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the number of bytes written and any write error encountered. See
|
||||||
|
// NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Println(a ...interface{}) (n int, err error) {
|
||||||
|
return fmt.Println(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprint(a ...interface{}) string {
|
||||||
|
return fmt.Sprint(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||||
|
// passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintf(format string, a ...interface{}) string {
|
||||||
|
return fmt.Sprintf(format, convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||||
|
// were passed with a default Formatter interface returned by NewFormatter. It
|
||||||
|
// returns the resulting string. See NewFormatter for formatting details.
|
||||||
|
//
|
||||||
|
// This function is shorthand for the following syntax:
|
||||||
|
//
|
||||||
|
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||||
|
func Sprintln(a ...interface{}) string {
|
||||||
|
return fmt.Sprintln(convertArgs(a)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||||
|
// length with each argument converted to a default spew Formatter interface.
|
||||||
|
func convertArgs(args []interface{}) (formatters []interface{}) {
|
||||||
|
formatters = make([]interface{}, len(args))
|
||||||
|
for index, arg := range args {
|
||||||
|
formatters[index] = NewFormatter(arg)
|
||||||
|
}
|
||||||
|
return formatters
|
||||||
|
}
|
||||||
80
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
80
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
|
||||||
|
|
||||||
|
# If you are submitting a patch, please add your name or the name of the
|
||||||
|
# organization which holds the copyright to this list in alphabetical order.
|
||||||
|
|
||||||
|
# Names should be added to this file as
|
||||||
|
# Name <email address>
|
||||||
|
# The email address is not required for organizations.
|
||||||
|
# Please keep the list sorted.
|
||||||
|
|
||||||
|
|
||||||
|
# Individual Persons
|
||||||
|
|
||||||
|
Aaron Hopkins <go-sql-driver at die.net>
|
||||||
|
Achille Roussel <achille.roussel at gmail.com>
|
||||||
|
Arne Hormann <arnehormann at gmail.com>
|
||||||
|
Asta Xie <xiemengjun at gmail.com>
|
||||||
|
Bulat Gaifullin <gaifullinbf at gmail.com>
|
||||||
|
Carlos Nieto <jose.carlos at menteslibres.net>
|
||||||
|
Chris Moos <chris at tech9computers.com>
|
||||||
|
Daniel Montoya <dsmontoyam at gmail.com>
|
||||||
|
Daniel Nichter <nil at codenode.com>
|
||||||
|
Daniël van Eeden <git at myname.nl>
|
||||||
|
Dave Protasowski <dprotaso at gmail.com>
|
||||||
|
DisposaBoy <disposaboy at dby.me>
|
||||||
|
Egor Smolyakov <egorsmkv at gmail.com>
|
||||||
|
Evan Shaw <evan at vendhq.com>
|
||||||
|
Frederick Mayle <frederickmayle at gmail.com>
|
||||||
|
Gustavo Kristic <gkristic at gmail.com>
|
||||||
|
Hanno Braun <mail at hannobraun.com>
|
||||||
|
Henri Yandell <flamefew at gmail.com>
|
||||||
|
Hirotaka Yamamoto <ymmt2005 at gmail.com>
|
||||||
|
ICHINOSE Shogo <shogo82148 at gmail.com>
|
||||||
|
INADA Naoki <songofacandy at gmail.com>
|
||||||
|
Jacek Szwec <szwec.jacek at gmail.com>
|
||||||
|
James Harr <james.harr at gmail.com>
|
||||||
|
Jeff Hodges <jeff at somethingsimilar.com>
|
||||||
|
Jeffrey Charles <jeffreycharles at gmail.com>
|
||||||
|
Jian Zhen <zhenjl at gmail.com>
|
||||||
|
Joshua Prunier <joshua.prunier at gmail.com>
|
||||||
|
Julien Lefevre <julien.lefevr at gmail.com>
|
||||||
|
Julien Schmidt <go-sql-driver at julienschmidt.com>
|
||||||
|
Justin Li <jli at j-li.net>
|
||||||
|
Justin Nuß <nuss.justin at gmail.com>
|
||||||
|
Kamil Dziedzic <kamil at klecza.pl>
|
||||||
|
Kevin Malachowski <kevin at chowski.com>
|
||||||
|
Lennart Rudolph <lrudolph at hmc.edu>
|
||||||
|
Leonardo YongUk Kim <dalinaum at gmail.com>
|
||||||
|
Linh Tran Tuan <linhduonggnu at gmail.com>
|
||||||
|
Lion Yang <lion at aosc.xyz>
|
||||||
|
Luca Looz <luca.looz92 at gmail.com>
|
||||||
|
Lucas Liu <extrafliu at gmail.com>
|
||||||
|
Luke Scott <luke at webconnex.com>
|
||||||
|
Maciej Zimnoch <maciej.zimnoch@codilime.com>
|
||||||
|
Michael Woolnough <michael.woolnough at gmail.com>
|
||||||
|
Nicola Peduzzi <thenikso at gmail.com>
|
||||||
|
Olivier Mengué <dolmen at cpan.org>
|
||||||
|
oscarzhao <oscarzhaosl at gmail.com>
|
||||||
|
Paul Bonser <misterpib at gmail.com>
|
||||||
|
Peter Schultz <peter.schultz at classmarkets.com>
|
||||||
|
Rebecca Chin <rchin at pivotal.io>
|
||||||
|
Runrioter Wung <runrioter at gmail.com>
|
||||||
|
Robert Russell <robert at rrbrussell.com>
|
||||||
|
Shuode Li <elemount at qq.com>
|
||||||
|
Soroush Pour <me at soroushjp.com>
|
||||||
|
Stan Putrya <root.vagner at gmail.com>
|
||||||
|
Stanley Gunawan <gunawan.stanley at gmail.com>
|
||||||
|
Xiangyu Hu <xiangyu.hu at outlook.com>
|
||||||
|
Xiaobing Jiang <s7v7nislands at gmail.com>
|
||||||
|
Xiuming Chen <cc at cxm.cc>
|
||||||
|
Zhenye Xie <xiezhenye at gmail.com>
|
||||||
|
|
||||||
|
# Organizations
|
||||||
|
|
||||||
|
Barracuda Networks, Inc.
|
||||||
|
Counting Ltd.
|
||||||
|
Google Inc.
|
||||||
|
Keybase Inc.
|
||||||
|
Pivotal Inc.
|
||||||
|
Stripe Inc.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user