From 9cd8cc8a5a97344d90d30c3b3ebad842e5ccf9f3 Mon Sep 17 00:00:00 2001
From: Jan Semmelink <jan@uafrica.com>
Date: Mon, 27 Sep 2021 15:25:59 +0200
Subject: [PATCH] Add API context to be shared in other projects

---
 api/README.md       |  25 +++++
 api/api.go          | 155 +++++++++++++++++++++++++++
 api/context.go      | 223 ++++++++++++++++++++++++++++++++++++++
 api/handler.go      |  92 ++++++++++++++++
 api/lambda.go       | 253 ++++++++++++++++++++++++++++++++++++++++++++
 api/local.go        | 101 ++++++++++++++++++
 api/router.go       |  90 ++++++++++++++++
 api/test.go         |   1 +
 go.mod              |   7 +-
 go.sum              |  16 +++
 service/context.go  |  61 +++++++++++
 service/database.go |   7 ++
 12 files changed, 1029 insertions(+), 2 deletions(-)
 create mode 100644 api/README.md
 create mode 100644 api/api.go
 create mode 100644 api/context.go
 create mode 100644 api/handler.go
 create mode 100644 api/lambda.go
 create mode 100644 api/local.go
 create mode 100644 api/router.go
 create mode 100644 api/test.go
 create mode 100644 service/context.go
 create mode 100644 service/database.go

diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..010c529
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,25 @@
+# TO TEST
+- mage run and local run
+- claims
+- ctx values, read from handler and set in handler then retrieve later in handler
+
+# TODO
+- sqs
+- cron
+- combined local service with api,sqs and cron
+- use in v3 and shiplogic
+- config for local running
+- db connection
+
+
+- when handler returns an error to indicate HTTP code, then it is wrapped and user gets:
+
+```{"message":"error from handler, because user not found HTTP(404:Not Found), because user does not exist"}``` instead of just:
+```{"message":"user does not exist"}```
+
+- api-docs not yet working here - and need to provide HTML option or per-endpoint options at least
+
+- add path parameters, e.g. /user/{user_id} - make sure it works the same way as lambda does - but we're not using it at present, I assume because the old simple map[][] router did not support it.
+- load config for local running
+- document with examples and templates
+- scheduled tasks from the db or some other scheduler from AWS?
\ No newline at end of file
diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..382bcaf
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,155 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"regexp"
+	"time"
+
+	"github.com/aws/aws-lambda-go/events"
+	"github.com/aws/aws-lambda-go/lambda"
+	"gitlab.com/uafrica/go-utils/errors"
+	"gitlab.com/uafrica/go-utils/logger"
+	"gitlab.com/uafrica/go-utils/service"
+)
+
+//New creates the API with the specified routes keys on [path][method]
+//value could be any of the handler function signatures supported by the api.Router
+//requestIDHeaderKey is defined in the response header to match the requestID from the request
+func New(requestIDHeaderKey string, routes map[string]map[string]interface{}) Api {
+	env := os.Getenv("ENVIRONMENT") //todo: support config loading for local dev and env for lambda in prod
+	if env == "" {
+		env = "dev"
+	}
+	if requestIDHeaderKey == "" {
+		requestIDHeaderKey = "request-id"
+	}
+
+	router, err := NewRouter(routes)
+	if err != nil {
+		panic(fmt.Sprintf("cannot create router: %+v", err))
+	}
+
+	return Api{
+		ILogger:            logger.New().WithFields(map[string]interface{}{"env": env}),
+		env:                env,
+		router:             router,
+		requestIDHeaderKey: requestIDHeaderKey,
+		checks:             []check{},
+		crashReporter:      defaultCrashReporter{},
+		auditor:            noAudit{},
+	}
+}
+
+type Api struct {
+	logger.ILogger     //for logging outside of context
+	env                string
+	router             Router
+	requestIDHeaderKey string
+
+	//options:
+	localPort     int //==0 for lambda, >0 for http.ListenAndServe
+	crashReporter ICrashReporter
+	cors          ICORS
+	dbConn        service.IDatabaseConnector
+	checks        []check
+	auditor       IAuditor
+}
+
+//checks are executed at the start of each request
+//to stop processing, return an error, preferably with an HTTP status code (see errors.HTTP(...))
+//all values returned will be logged and added to the context for retrieval by any handler
+//the values are added with the check's name as prefix: "<check.name>_<value.name>"
+//and cannot be changed afterwards
+type IChecker interface {
+	Check(req events.APIGatewayProxyRequest) (values map[string]interface{}, err error)
+}
+
+type check struct {
+	name    string
+	checker IChecker
+}
+
+const namePattern = `[a-z]([a-z0-9_]*[a-z0-9])*`
+
+var nameRegex = regexp.MustCompile("^" + namePattern + "$")
+
+//check name must be lowercase with optional underscores, e.g. "rate_limiter" or "claims"
+func (app Api) WithCheck(name string, checker IChecker) Api {
+	if !nameRegex.MatchString(name) {
+		panic(errors.Errorf("invalid name for check(%s)", name))
+	}
+	if checker == nil {
+		panic(errors.Errorf("check(%s) is nil", name))
+	}
+	for _, check := range app.checks {
+		if check.name == name {
+			panic(errors.Errorf("check(%s) already registered", name))
+		}
+	}
+	app.checks = append(app.checks, check{name: name, checker: checker})
+	return app
+}
+
+func (api Api) WithDb(dbConn service.IDatabaseConnector) Api {
+	api.dbConn = dbConn
+	return api
+}
+
+func (api Api) WithCORS(cors ICORS) Api {
+	api.cors = cors
+	return api
+}
+
+type ICORS interface {
+	CORS() map[string]string //return CORS headers
+}
+
+func (api Api) WithCrashReported(crashReporter ICrashReporter) Api {
+	if crashReporter != nil {
+		api.crashReporter = crashReporter
+	}
+	return api
+}
+
+type ICrashReporter interface {
+	Catch(ctx Context) //Report(method string, path string, crash interface{})
+}
+
+func (api Api) WithLocalPort(localPortPtr *int) Api {
+	if localPortPtr != nil && *localPortPtr > 0 {
+		api.localPort = *localPortPtr
+	}
+	return api
+}
+
+//run and panic on error
+func (api Api) Run() {
+	//decide local of lambda
+	if api.localPort > 0 {
+		err := http.ListenAndServe(fmt.Sprintf(":%d", api.localPort), api) //calls app.ServeHTTP() which calls app.Handler()
+		if err != nil {
+			panic(err)
+		}
+	} else {
+		lambda.Start(api.Handler) //calls app.Handler directly
+	}
+}
+
+type defaultCrashReporter struct{}
+
+func (defaultCrashReporter) Catch(ctx Context) {
+	// crash := recover()
+	// if crash != nil {
+	// 	ctx.Errorf("CRASH: (%T) %+v\n", crash, crash)
+	// }
+}
+
+type IAuditor interface {
+	Audit(startTime, endTime time.Time, values map[string]interface{})
+}
+
+type noAudit struct{}
+
+func (noAudit) Audit(startTime, endTime time.Time, values map[string]interface{}) {} //do nothing
diff --git a/api/context.go b/api/context.go
new file mode 100644
index 0000000..37f73d2
--- /dev/null
+++ b/api/context.go
@@ -0,0 +1,223 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/aws/aws-lambda-go/events"
+	"github.com/uptrace/bun"
+	"gitlab.com/uafrica/go-utils/errors"
+	"gitlab.com/uafrica/go-utils/logger"
+	"gitlab.com/uafrica/go-utils/service"
+)
+
+type IContext interface {
+	context.Context
+	logger.ILogger
+	StartTime() time.Time
+	MillisecondsSinceStart() int64
+	//DB() *bun.DB
+}
+
+type Context struct {
+	service.Context
+	Request          events.APIGatewayProxyRequest
+	RequestID        string
+	ValuesFromChecks map[string]map[string]interface{} //also in context.Value(), but cannot retrieve iteratively from there for logging...
+	DB               *bun.DB
+}
+
+// func (ctx Context) Audit(org, new interface{}, eventType types.AuditEventType) {
+// 	//call old function for now - should become part of context ONLY
+// 	audit.SaveAuditEvent(org, new, ctx.Claims, eventType, &ctx.RequestID)
+// }
+
+//todo: change to be a ctx method that defer to log so it does not have to be called explicitly
+//it should also capture metrics for the handler and automaticlaly write the audit record,
+//(but still allow for audit to be suppressed may be in some cases)
+func (ctx Context) LogAPIRequestAndResponse(res events.APIGatewayProxyResponse, err error) {
+	fields := map[string]interface{}{
+		"path":                   ctx.Request.Path,
+		"method":                 ctx.Request.HTTPMethod,
+		"status_code":            res.StatusCode,
+		"api_gateway_request_id": ctx.RequestID,
+	}
+
+	if ctx.Request.HTTPMethod == "GET" {
+		fields["req-query"] = ctx.Request.QueryStringParameters
+	}
+
+	statusOK := res.StatusCode >= 200 && res.StatusCode <= 299
+	if err != nil || !statusOK {
+		fields["error"] = err
+		fields["req-body"] = ctx.Request.Body
+		fields["req-query"] = ctx.Request.QueryStringParameters
+		fields["res-body"] = res.Body
+		for checkName, checkValues := range ctx.ValuesFromChecks {
+			for name, value := range checkValues {
+				fields[checkName+"_"+name] = value
+			}
+		}
+	}
+	ctx.WithFields(fields).Infof("Request & Response: err=%+v", err)
+}
+
+//allocate struct for params, populate it from the URL parameters then validate and return the struct
+func (ctx Context) GetRequestParams(paramsStructType reflect.Type) (interface{}, error) {
+	paramValues := map[string]interface{}{}
+	for n, v := range ctx.Request.QueryStringParameters {
+		paramValues[n] = v
+	}
+	paramsStructValuePtr := reflect.New(paramsStructType)
+	for i := 0; i < paramsStructType.NumField(); i++ {
+		f := paramsStructType.Field(i)
+		n := (strings.SplitN(f.Tag.Get("json"), ",", 2))[0]
+		if n == "" {
+			n = strings.ToLower(f.Name)
+		}
+		if n == "" || n == "-" {
+			continue
+		}
+
+		//get value(s) from query string
+		var paramStrValues []string
+		if paramStrValue, isDefined := ctx.Request.QueryStringParameters[n]; isDefined {
+			paramStrValues = []string{paramStrValue} //single value
+		} else {
+			paramStrValues = ctx.Request.MultiValueQueryStringParameters[n]
+		}
+		if len(paramStrValues) == 0 {
+			continue //param has no value specified in URL
+		}
+
+		//param is defined >=1 times in URL
+		if f.Type.Kind() == reflect.Slice {
+			//iterate over all specified values
+			for index, paramStrValue := range paramStrValues {
+				newValuePtr := reflect.New(f.Type.Elem())
+				if err := setParamFromStr(fmt.Sprintf("%s[%d]", n, index),
+					//paramsStructValuePtr.Elem().Field(i).Index(index),
+					newValuePtr.Elem(), //.Elem() to dereference
+					paramStrValue); err != nil {
+					return nil, errors.Wrapf(err, "failed to set %s[%d]=%s", n, i, paramStrValues[0])
+				}
+				paramsStructValuePtr.Elem().Field(i).Set(reflect.Append(paramsStructValuePtr.Elem().Field(i), newValuePtr.Elem()))
+			}
+		} else {
+			if len(paramStrValues) > 1 {
+				return nil, errors.Errorf("%s does not support >1 values(%v)", n, strings.Join(paramStrValues, ","))
+			}
+			//single value specified
+			if err := setParamFromStr(n, paramsStructValuePtr.Elem().Field(i), paramStrValues[0]); err != nil {
+				return nil, errors.Wrapf(err, "failed to set %s=%s", n, paramStrValues[0])
+			}
+		}
+	} //for each param struct field
+
+	if validator, ok := paramsStructValuePtr.Interface().(IValidator); ok {
+		if err := validator.Validate(); err != nil {
+			return nil, errors.Wrapf(err, "invalid params")
+		}
+	}
+
+	return paramsStructValuePtr.Elem().Interface(), nil
+}
+
+func setParamFromStr(fieldName string, fieldValue reflect.Value, paramStrValue string) error {
+	logger.Debugf("Set(%s,%v,%v,%v)", fieldName, fieldValue.Type(), fieldValue.Kind(), paramStrValue)
+	switch fieldValue.Type().Kind() {
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		//parse to int for this struct field
+		if i64, err := strconv.ParseInt(paramStrValue, 10, 64); err != nil {
+			return errors.Errorf("%s is not a number", paramStrValue)
+		} else {
+			switch fieldValue.Type().Kind() {
+			case reflect.Int:
+				fieldValue.Set(reflect.ValueOf(int(i64)))
+			case reflect.Int8:
+				fieldValue.Set(reflect.ValueOf(int8(i64)))
+			case reflect.Int16:
+				fieldValue.Set(reflect.ValueOf(int16(i64)))
+			case reflect.Int32:
+				fieldValue.Set(reflect.ValueOf(int32(i64)))
+			case reflect.Int64:
+				fieldValue.Set(reflect.ValueOf(i64))
+			}
+		}
+
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		//parse to int for this struct field
+		if u64, err := strconv.ParseUint(paramStrValue, 10, 64); err != nil {
+			return errors.Errorf("%s is not a number", paramStrValue)
+		} else {
+			switch fieldValue.Type().Kind() {
+			case reflect.Uint:
+				fieldValue.Set(reflect.ValueOf(uint(u64)))
+			case reflect.Uint8:
+				fieldValue.Set(reflect.ValueOf(uint8(u64)))
+			case reflect.Uint16:
+				fieldValue.Set(reflect.ValueOf(uint16(u64)))
+			case reflect.Uint32:
+				fieldValue.Set(reflect.ValueOf(uint32(u64)))
+			case reflect.Uint64:
+				fieldValue.Set(reflect.ValueOf(u64))
+			}
+		}
+
+	case reflect.Bool:
+		bs := strings.ToLower(paramStrValue)
+		if bs == "true" || bs == "yes" || bs == "1" {
+			fieldValue.Set(reflect.ValueOf(true))
+		}
+
+	case reflect.String:
+		fieldValue.Set(reflect.ValueOf(paramStrValue))
+
+	case reflect.Float32:
+		if f32, err := strconv.ParseFloat(paramStrValue, 32); err != nil {
+			return errors.Wrapf(err, "invalid float")
+		} else {
+			fieldValue.Set(reflect.ValueOf(float32(f32)))
+		}
+
+	case reflect.Float64:
+		if f64, err := strconv.ParseFloat(paramStrValue, 64); err != nil {
+			return errors.Wrapf(err, "invalid float")
+		} else {
+			fieldValue.Set(reflect.ValueOf(f64))
+		}
+
+	default:
+		return errors.Errorf("unsupported type %v", fieldValue.Type().Kind())
+	} //switch param struct field
+	return nil
+}
+
+func (ctx Context) GetRequestBody(requestStructType reflect.Type) (interface{}, error) {
+	requestStructValuePtr := reflect.New(requestStructType)
+	err := json.Unmarshal([]byte(ctx.Request.Body), requestStructValuePtr.Interface())
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to JSON request body")
+	}
+
+	if validator, ok := requestStructValuePtr.Interface().(IValidator); ok {
+		if err := validator.Validate(); err != nil {
+			return nil, errors.Wrapf(err, "invalid request body")
+		}
+	}
+
+	return requestStructValuePtr.Elem().Interface(), nil
+}
+
+// func (ctx Context) DB() *bun.DB {
+// 	return ctx.DB
+// }
+
+type IValidator interface {
+	Validate() error
+}
diff --git a/api/handler.go b/api/handler.go
new file mode 100644
index 0000000..7340bd1
--- /dev/null
+++ b/api/handler.go
@@ -0,0 +1,92 @@
+package api
+
+import (
+	"reflect"
+
+	"gitlab.com/uafrica/go-utils/errors"
+)
+
+type handler struct {
+	RequestParamsType reflect.Type
+	RequestBodyType   reflect.Type
+	ResponseType      reflect.Type
+	FuncValue         reflect.Value
+}
+
+func NewHandler(fnc interface{}) (handler, error) {
+	h := handler{}
+
+	fncType := reflect.TypeOf(fnc)
+	if fncType.NumIn() < 2 || fncType.NumIn() > 3 {
+		return h, errors.Errorf("takes %d args instead of (Context, Params[, Body])", fncType.NumIn())
+	}
+	if fncType.NumOut() < 1 || fncType.NumOut() > 2 {
+		return h, errors.Errorf("returns %d results instead of ([Response,] error)", fncType.NumOut())
+	}
+
+	//arg[0] must implement interface lambda_helpers.Context
+	if _, ok := reflect.New(fncType.In(0)).Interface().(IContext); !ok {
+		return h, errors.Errorf("first arg %v does not implement lambda_helpers.IContext", fncType.In(0))
+	}
+
+	//arg[1] must be a struct for params. It may be an empty struct, but
+	//all public fields require a json tag which we will use to math the URL param name
+	if err := validateStructType(fncType.In(1)); err != nil {
+		return h, errors.Errorf("second arg %v is not valid params struct type", fncType.In(1))
+	}
+	h.RequestParamsType = fncType.In(1)
+
+	//arg[2] is optional and must be a struct for request body. It may be an empty struct, but
+	//all public fields require a json tag which we will use to unmarshal the request body from JSON
+	if fncType.NumIn() >= 3 {
+		if fncType.In(2).Kind() == reflect.Slice {
+			if err := validateStructType(fncType.In(2).Elem()); err != nil {
+				return h, errors.Errorf("third arg %v is not valid body []struct type", fncType.In(2))
+			}
+		} else {
+			if err := validateStructType(fncType.In(2)); err != nil {
+				return h, errors.Errorf("third arg %v is not valid body struct type", fncType.In(2))
+			}
+		}
+
+		//todo: check special fields for claims, and see if also applies to params struct...
+		//AccountID must be int64 or *int64 with tag =???
+		//UserID must be int64 or *int64 with tag =???
+		//Username must be string with tag =???
+
+		h.RequestBodyType = fncType.In(2)
+	}
+
+	//if 2 results, first must be response struct or array of response structs that will be marshalled to JSON
+	if fncType.NumOut() > 1 {
+		if fncType.Out(0).Kind() == reflect.Slice {
+			if err := validateStructType(fncType.Out(0).Elem()); err != nil {
+				return h, errors.Errorf("first result %v is not valid response []struct type", fncType.Out(0))
+			}
+		} else {
+			if err := validateStructType(fncType.Out(0)); err != nil {
+				return h, errors.Errorf("first result %v is not valid response struct type", fncType.Out(0))
+			}
+		}
+		h.ResponseType = fncType.Out(0)
+	}
+
+	//last result must be error
+	if _, ok := reflect.New(fncType.Out(fncType.NumOut() - 1)).Interface().(*error); !ok {
+		return h, errors.Errorf("last result %v is not error type", fncType.Out(fncType.NumOut()-1))
+	}
+
+	h.FuncValue = reflect.ValueOf(fnc)
+	return h, nil
+}
+
+func validateStructType(t reflect.Type) error {
+	if t.Kind() != reflect.Struct {
+		return errors.Errorf("%v is %v, not a struct", t, t.Kind())
+	}
+	// for i := 0; i < t.NumField(); i++ {
+	// 	f := t.Field(i)
+	// 	if f.... check tags recursively... for now, not too strict ... add checks if we see issues that break the API, to help dev to fix before we deploy, or to prevent bad habits...
+	// }
+	return nil
+}
diff --git a/api/lambda.go b/api/lambda.go
new file mode 100644
index 0000000..3299fac
--- /dev/null
+++ b/api/lambda.go
@@ -0,0 +1,253 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"math/rand"
+	"net/http"
+	"reflect"
+	"time"
+
+	"github.com/aws/aws-lambda-go/events"
+	"github.com/aws/aws-lambda-go/lambdacontext"
+	"gitlab.com/uafrica/go-utils/errors"
+	"gitlab.com/uafrica/go-utils/service"
+)
+
+//this is native handler for lambda passed into lambda.Start()
+//to run locally, this is called from app.ServeHTTP()
+func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGatewayProxyRequest) (res events.APIGatewayProxyResponse, err error) {
+	res = events.APIGatewayProxyResponse{
+		StatusCode: http.StatusInternalServerError,
+		Body:       "undefined response",
+		Headers:    map[string]string{},
+	}
+
+	// Replace the proxy resource with the path, has some edge cases but works for our current API implementation
+	// Edge case being that if have path params then specify those routes explicitly
+	if apiGatewayProxyReq.Resource == "/{proxy+}" {
+		apiGatewayProxyReq.Resource = apiGatewayProxyReq.Path
+	}
+
+	//setup context
+	requestID := apiGatewayProxyReq.RequestContext.RequestID
+	if lambdaContext, ok := lambdacontext.FromContext(baseCtx); ok && lambdaContext != nil {
+		requestID = lambdaContext.AwsRequestID
+	}
+	ctx := Context{
+		Context: service.NewContext(baseCtx, map[string]interface{}{
+			"env":        api.env,
+			"request_id": requestID,
+		}),
+		Request:          apiGatewayProxyReq,
+		RequestID:        requestID,
+		ValuesFromChecks: map[string]map[string]interface{}{},
+	}
+
+	//report handler crashes
+	defer api.crashReporter.Catch(ctx)
+
+	defer func() {
+		//set CORS headers on every response
+		if api.cors != nil {
+			for n, v := range api.cors.CORS() {
+				res.Headers[n] = v
+			}
+		}
+	}()
+
+	defer func() {
+		ctx.LogAPIRequestAndResponse(res, err)
+		if err != nil {
+			if withCause, ok := err.(errors.ErrorWithCause); ok {
+				res.StatusCode = withCause.Code()
+			}
+			errorMessage := fmt.Sprintf("%c", err)
+			jsonError, _ := json.Marshal(map[string]interface{}{"message": errorMessage})
+			res.Body = string(jsonError)
+			err = nil //never pass error back to lambda or http server
+		}
+		if api.requestIDHeaderKey != "" {
+			res.Headers[api.requestIDHeaderKey] = ctx.RequestID
+		}
+		api.auditor.Audit(ctx.StartTime(), time.Now(), map[string]interface{}{
+			"request_id": ctx.RequestID,
+			"request":    ctx.Request,
+			"response":   res},
+		)
+	}()
+
+	//do checks before proceed
+	//(typical maintenance mode, rate limits, authentication/claims, ...)
+	for _, check := range api.checks {
+		var checkValues map[string]interface{}
+		checkValues, err = check.checker.Check(apiGatewayProxyReq)
+		for n, v := range checkValues {
+			ctx.Context = ctx.Context.WithValue(check.name+"_"+n, v)
+		}
+		ctx.ValuesFromChecks[check.name] = checkValues
+		if err != nil {
+			return
+		}
+	}
+
+	//Early return OPTIONS call
+	if apiGatewayProxyReq.HTTPMethod == "OPTIONS" {
+		res.StatusCode = http.StatusNoContent
+		err = nil
+		return
+	}
+
+	rand.Seed(time.Now().Unix())
+
+	if api.dbConn != nil {
+		ctx.DB, err = api.dbConn.Connect()
+		if err != nil {
+			err = errors.Wrapf(err, "failed to connect to db")
+			return
+		}
+	}
+
+	ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
+	ctx.WithFields(map[string]interface{}{
+		"http_method":                ctx.Request.HTTPMethod,
+		"path":                       ctx.Request.Path,
+		"api_gateway_request_id":     ctx.Request.RequestContext.RequestID,
+		"user_cognito_auth_provider": ctx.Request.RequestContext.Identity.CognitoAuthenticationProvider,
+		"user_arn":                   ctx.Request.RequestContext.Identity.UserArn,
+	}).Infof("Start API Handler")
+
+	//TODO:
+	// // Get claims and check the status of the user
+	// ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
+	// if err != nil {
+	// 	return events.APIGatewayProxyResponse{
+	// 		StatusCode: http.StatusBadRequest,
+	// 		Body:       fmt.Sprintf("%v\n", err),
+	// 		Headers:    utils.CorsHeaders(),
+	// 	}, nil
+	// }
+
+	// if ctx.Claims.UserID != nil {
+	// 	userStatusResponse := checkUserStatus(ctx.Claims)
+	// 	if userStatusResponse != nil {
+	// 		return *userStatusResponse, nil
+	// 	}
+	// }
+
+	// permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
+	// if !permissions.HasPermission(ctx.Claims.Role, permissionString) {
+	// 	response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString))
+	// 	if ctx.Claims.Role == nil {
+	// 		ctx.Errorf("%d :: %s: %v", *ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
+	// 	} else if ctx.Claims.RoleID == nil {
+	// 		ctx.Errorf("%s: you have no role ID", permissionString)
+	// 	}
+	// 	return response, nil
+	// }
+
+	//route on method and path
+	resourceHandler, err := api.router.Route(apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
+	if err != nil {
+		err = errors.Wrapf(err, "invalid route")
+		return
+	}
+
+	if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
+		ctx.Debugf("Calling legacy handler...")
+		return legacyHandlerFunc(apiGatewayProxyReq)
+	}
+
+	handler, ok := resourceHandler.(handler)
+	if !ok {
+		//should not get here if validateAPIEndpoints() is properly checking!
+		err = errors.HTTP(http.StatusInternalServerError, errors.Errorf("invalid handler function %T", resourceHandler), "invalid routing")
+	}
+
+	//new type of handler function
+	//allocate, populate and validate params struct
+	paramsStruct, paramsErr := ctx.GetRequestParams(handler.RequestParamsType)
+	if paramsErr != nil {
+		err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters")
+		return
+	}
+	//apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always
+	// if err = ctx.Claims.FillOnObject(ctx.Request, &paramsStruct); err != nil {
+	// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters")
+	// 	return
+	// }
+	ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)
+
+	args := []reflect.Value{
+		reflect.ValueOf(ctx),
+		reflect.ValueOf(paramsStruct),
+	}
+
+	var bodyStruct interface{}
+	if handler.RequestBodyType != nil {
+		//allocate, populate and validate request struct
+		bodyStruct, err = ctx.GetRequestBody(handler.RequestBodyType)
+		if err != nil {
+			err = errors.HTTP(http.StatusBadRequest, err, "invalid body")
+			return
+		}
+
+		//apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always
+		// if err = ctx.Claims.FillOnObject(ctx.Request, &bodyStruct); err != nil {
+		// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body")
+		// 	return
+		// }
+
+		ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
+		args = append(args, reflect.ValueOf(bodyStruct))
+	}
+
+	//call handler in a func with defer to catch potential crash
+	ctx.Debugf("calling handler")
+	var results []reflect.Value
+	results, err = func() (results []reflect.Value, err error) {
+		defer func() {
+			if crashDump := recover(); crashDump != nil {
+				err = errors.Errorf("handler function crashed: %+v", crashDump)
+				return
+			}
+		}()
+		results = handler.FuncValue.Call(args)
+		return results, nil
+	}()
+	if err != nil {
+		err = errors.Wrapf(err, "handler failed")
+		return
+	}
+
+	ctx.Debugf("handler -> results: %v", results)
+
+	//see if handler failed using last result of type error
+	lastResultValue := results[len(results)-1]
+	if !lastResultValue.IsNil() {
+		err = lastResultValue.Interface().(error)
+		if err != nil {
+			err = errors.Wrapf(err, "error from handler")
+			return
+		}
+	}
+
+	//handler succeeded, some handler does not have a response data (typically post/put/patch/delete)
+	err = nil
+	res.StatusCode = http.StatusOK
+
+	if len(results) > 1 {
+		responseStruct := results[0].Interface()
+		ctx.Debugf("Response type: %T", responseStruct)
+
+		var bodyBytes []byte
+		bodyBytes, err = json.Marshal(responseStruct)
+		if err != nil {
+			err = errors.Wrapf(err, "failed to encode response content")
+			return
+		}
+		res.Body = string(bodyBytes)
+	}
+	return
+}
diff --git a/api/local.go b/api/local.go
new file mode 100644
index 0000000..a88f1e8
--- /dev/null
+++ b/api/local.go
@@ -0,0 +1,101 @@
+package api
+
+import (
+	"bytes"
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/aws/aws-lambda-go/events"
+	"github.com/google/uuid"
+	"gitlab.com/uafrica/go-utils/errors"
+)
+
+//use this in http.ListenAndServe() to test locally
+func (api Api) ServeHTTP(httpRes http.ResponseWriter, httpReq *http.Request) {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
+	defer cancel()
+
+	req := events.APIGatewayProxyRequest{
+		Resource:                        httpReq.URL.Path, //RequestURI cannot be used - it includes the url params
+		Path:                            httpReq.URL.Path,
+		HTTPMethod:                      httpReq.Method,
+		Headers:                         map[string]string{},
+		MultiValueHeaders:               nil, // map[string][]string           `json:"multiValueHeaders"`
+		QueryStringParameters:           map[string]string{},
+		MultiValueQueryStringParameters: map[string][]string{},
+		PathParameters:                  nil, //                  map[string]string             `json:"pathParameters"`
+		StageVariables:                  nil, //                  map[string]string             `json:"stageVariables"`
+		//RequestContext:                  events.APIGatewayProxyRequestContext{}, //                  APIGatewayProxyRequestContext `json:"requestContext"`
+		//Body:            bodyString, //                            string                        `json:"body"`
+		IsBase64Encoded: false,
+	}
+
+	//copy significant headers
+	//todo: option to let app specify other list
+	for _, name := range []string{"Content-Type", "request-id"} {
+		if value := httpReq.Header.Get(name); value != "" {
+			req.Headers[name] = value
+		}
+	}
+
+	requestID := httpReq.Header.Get("request-id")
+	if requestID == "" {
+		//define a random request-id
+		requestID = uuid.New().String()
+		req.Headers["request-id"] = requestID
+	}
+
+	for n, v := range httpReq.URL.Query() {
+		if len(v) == 1 {
+			req.QueryStringParameters[n] = v[0]
+		}
+		if len(v) > 1 {
+			req.MultiValueQueryStringParameters[n] = v
+		}
+	}
+
+	req.RequestContext = events.APIGatewayProxyRequestContext{
+		AccountID:        "",                                 //        string                    `json:"accountId"`
+		ResourceID:       "",                                 //string                    `json:"resourceId"`
+		OperationName:    "",                                 //string                    `json:"operationName,omitempty"`
+		Stage:            "",                                 //string                    `json:"stage"`
+		DomainName:       "",                                 //string                    `json:"domainName"`
+		DomainPrefix:     "",                                 //string                    `json:"domainPrefix"`
+		RequestID:        requestID,                          //string                    `json:"requestId"`
+		Protocol:         "",                                 //string                    `json:"protocol"`
+		Identity:         events.APIGatewayRequestIdentity{}, // `json:"identity"`
+		ResourcePath:     "",                                 //string                    `json:"resourcePath"`
+		Authorizer:       nil,                                //map[string]interface{}    `json:"authorizer"`
+		HTTPMethod:       "",                                 //string                    `json:"httpMethod"`
+		RequestTime:      "",                                 //string                    `json:"requestTime"`
+		RequestTimeEpoch: time.Now().Unix(),                  //                     `json:"requestTimeEpoch"`
+		APIID:            "",                                 //string                    `json:"apiId"` // The API Gateway rest API Id
+	}
+
+	bodyBytes := bytes.NewBuffer(nil)
+	if _, err := bodyBytes.ReadFrom(httpReq.Body); err != nil {
+		http.Error(httpRes, errors.Wrapf(err, "failed to read body").Error(), http.StatusInternalServerError)
+		return
+	}
+	req.Body = bodyBytes.String()
+
+	res, err := api.Handler(ctx, req)
+	if err != nil {
+		http.Error(httpRes, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	for n, v := range res.Headers {
+		httpRes.Header().Set(n, v)
+	}
+	if res.StatusCode < 200 || res.StatusCode >= 300 {
+		http.Error(httpRes, res.Body, res.StatusCode)
+		return
+	}
+
+	if res.Body != "" {
+		httpRes.Header().Set("Content-Type", "application/json")
+		httpRes.Write([]byte(res.Body))
+	}
+}
diff --git a/api/router.go b/api/router.go
new file mode 100644
index 0000000..5095e9b
--- /dev/null
+++ b/api/router.go
@@ -0,0 +1,90 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/aws/aws-lambda-go/events"
+	"gitlab.com/uafrica/go-utils/errors"
+)
+
+type Router struct {
+	endpoints map[string]map[string]interface{}
+}
+
+func (r Router) Endpoints() map[string]map[string]interface{} {
+	return r.endpoints
+}
+
+func (r Router) Route(path, method string) (interface{}, error) {
+	if methods, ok := r.endpoints[path]; !ok {
+		return nil, errors.HTTP(http.StatusNotFound, errors.Errorf("%s not found", path), "unknown resource path")
+	} else {
+		if handler, ok := methods[method]; !ok {
+			return nil, errors.HTTP(http.StatusMethodNotAllowed, errors.Errorf("%s not allowed on %s", method, path), "method not allowed")
+		} else {
+			return handler, nil
+		}
+	}
+}
+
+//check that all API endpoints are correctly defined using one of the supported handler types
+//return updated endpoints with additional information
+func NewRouter(endpoints map[string]map[string]interface{}) (Router, error) {
+	countLegacy := 0
+	countHandler := 0
+	for resource, methodHandlers := range endpoints {
+		if resource == "" {
+			return Router{}, errors.Errorf("blank resource")
+		}
+		if resource == "/api-docs" {
+			return Router{}, errors.Errorf("%s may not be a defined endpoint - it is reserved", resource)
+		}
+		for method, handlerFunc := range methodHandlers {
+			switch method {
+			case "GET":
+			case "POST":
+			case "PUT":
+			case "PATCH":
+			case "DELETE":
+			default:
+				return Router{}, errors.Errorf("nvalid method:\"%s\" on resource \"%s\"", method, resource)
+			}
+			if handlerFunc == nil {
+				return Router{}, errors.Errorf("nil handler on %s %s", method, resource)
+			}
+
+			if _, ok := handlerFunc.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
+				//ok - leave as is - we support this legacyHandler
+				fmt.Printf("%10s %s: OK (legacy handler)\n", method, resource)
+				countLegacy++
+			} else {
+				handler, err := NewHandler(handlerFunc)
+				if err != nil {
+					return Router{}, errors.Wrapf(err, "%s %s has invalid handler %T", method, resource, handlerFunc)
+				}
+				//replace the endpoint value so we can quickly call this handler
+				endpoints[resource][method] = handler
+				fmt.Printf("%10s %s: OK (params: %v, request: %v)\n", method, resource, handler.RequestParamsType, handler.RequestBodyType)
+				countHandler++
+			}
+		}
+	}
+	fmt.Printf("Checked %d legacy and %d new handlers\n", countLegacy, countHandler)
+
+	//add reserved endpoint to generate documentation
+	r := Router{
+		endpoints: endpoints,
+	}
+
+	// {
+	// 	docsHandler, err := NewHandler(GETApiDocs(r)) //endpoints))
+	// 	if err != nil {
+	// 		return Router{}, errors.Wrapf(err, "failed to define handler for docs")
+	// 	}
+	// 	endpoints["/api-docs"] = map[string]interface{}{
+	// 		"GET": docsHandler,
+	// 	}
+	// }
+	return r, nil
+}
diff --git a/api/test.go b/api/test.go
new file mode 100644
index 0000000..778f64e
--- /dev/null
+++ b/api/test.go
@@ -0,0 +1 @@
+package api
diff --git a/go.mod b/go.mod
index fe6f449..aa20d27 100644
--- a/go.mod
+++ b/go.mod
@@ -10,14 +10,17 @@ require (
 )
 
 require (
+	github.com/aws/aws-lambda-go v1.26.0
 	github.com/go-pg/zerochecker v0.2.0 // indirect
+	github.com/google/uuid v1.3.0
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
+	github.com/uptrace/bun v1.0.8
 	github.com/vmihailenco/bufpool v0.1.11 // indirect
-	github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect
+	github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
 	github.com/vmihailenco/tagparser v0.1.2 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
-	golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
+	golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
 	mellium.im/sasl v0.2.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 105598b..f70e5ab 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,11 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/aws/aws-lambda-go v1.26.0 h1:6ujqBpYF7tdZcBvPIccs98SpeGfrt/UOVEiexfNIdHA=
+github.com/aws/aws-lambda-go v1.26.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -32,6 +36,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -56,6 +62,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -66,10 +74,15 @@ github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
 github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
+github.com/uptrace/bun v1.0.8 h1:uWrBnun83dfNGP//xdLAdGA3UGISQtpdSzjZHUNmZh0=
+github.com/uptrace/bun v1.0.8/go.mod h1:aL6D9vPw8DXaTQTwGrEPtUderBYXx7ShUmPfnxnqscw=
+github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
 github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
 github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
 github.com/vmihailenco/msgpack/v5 v5.3.1 h1:0i85a4dsZh8mC//wmyyTEzidDLPQfQAxZIOLtafGbFY=
 github.com/vmihailenco/msgpack/v5 v5.3.1/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc=
+github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
 github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
 github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@@ -110,6 +123,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -152,6 +167,7 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
diff --git a/service/context.go b/service/context.go
new file mode 100644
index 0000000..1f172e8
--- /dev/null
+++ b/service/context.go
@@ -0,0 +1,61 @@
+package service
+
+import (
+	"context"
+	"time"
+
+	"gitlab.com/uafrica/go-utils/logger"
+)
+
+//values: are added to context and logger
+//these values are logged for every log event in this context
+//values can be added later using with value, but won't be logged
+//	they are just for retrieval between unrelated packages, e.g.
+//	authentication may set the user_id etc... and other package may retrieve it but not change it
+type valueKey string
+
+func NewContext(base context.Context, values map[string]interface{}) Context {
+	for n, v := range values {
+		base = context.WithValue(base, valueKey(n), v)
+	}
+	return Context{
+		Context:   base,
+		Logger:    logger.New().WithFields(values),
+		startTime: time.Now(),
+	}
+}
+
+// type IContext interface {
+// 	context.Context
+// 	logger.ILogger
+// 	StartTime() time.Time
+// 	MillisecondsSinceStart() int64
+// 	ValueOrDefault(name string, defaultValue interface{}) interface{}
+// 	WithValue(name string, value interface{}) Context
+// }
+
+type Context struct {
+	context.Context
+	logger.Logger
+	startTime time.Time
+}
+
+func (ctx Context) MillisecondsSinceStart() int64 {
+	return time.Since(ctx.startTime).Milliseconds()
+}
+
+func (ctx Context) StartTime() time.Time {
+	return ctx.startTime
+}
+
+func (ctx Context) ValueOrDefault(name string, defaultValue interface{}) interface{} {
+	if value := ctx.Value(valueKey(name)); value != nil {
+		return value
+	}
+	return defaultValue
+}
+
+func (ctx Context) WithValue(name string, value interface{}) Context {
+	ctx.Context = context.WithValue(ctx.Context, valueKey(name), value)
+	return ctx
+}
diff --git a/service/database.go b/service/database.go
new file mode 100644
index 0000000..3670ee5
--- /dev/null
+++ b/service/database.go
@@ -0,0 +1,7 @@
+package service
+
+import "github.com/uptrace/bun"
+
+type IDatabaseConnector interface {
+	Connect() (*bun.DB, error)
+}
-- 
GitLab