diff --git a/HISTORY.md b/HISTORY.md
index a52c4e15..0605aa6c 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,9 +2,16 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
+## 5.1.0 -> 5.1.1
+Two hours after the previous update,
+
+- **More on Transactions**: By-default transaction's lifetime is 'per-call/transient' meaning that each transaction has its own scope on the context, rollbacks when `scope.Complete(notNilAndNotEmptyError)` and the rest of transictions in chain are executed as expected, from now and on you have the ability to `skip the rest of the next transictions on first failure` by simply call `scope.RequestScoped(true)`.
+
+Note: `RequestTransactionScope` renamed to ,simply, `TransactionScope`.
+
## 5.0.4 -> 5.1.0
-- **NEW (UNIQUE?) FEATURE**: Request-scoped transactions inside handler's context. Proof-of-concept example [here](https://github.com/iris-contrib/examples/tree/master/request_transactions).
+- **NEW (UNIQUE?) FEATURE**: Request-scoped transactions inside handler's context. Proof-of-concept example [here](https://github.com/iris-contrib/examples/tree/master/transactions).
## 5.0.3 -> 5.0.4
diff --git a/README.md b/README.md
index 63b9ddcb..0dcf44d1 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
-
+
@@ -920,7 +920,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
-Current: **v5.1.0**
+Current: **v5.1.1**
Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)**
@@ -929,7 +929,7 @@ Todo
------------
- [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503)
-- [x] [v5.1.0: Request-Scoped Transactions](https://github.com/iris-contrib/examples/tree/master/request_transactions), simple and elegant.
+- [x] [v5.1.0: (Request) Scoped Transactions](https://github.com/iris-contrib/examples/tree/master/transactions), simple and elegant.
Iris is a **Community-Driven** Project, waiting for your suggestions and [feature requests](https://github.com/kataras/iris/issues?utf8=%E2%9C%93&q=label%3A%22feature%20request%22)!
diff --git a/context.go b/context.go
index afd9b12f..9fc6737d 100644
--- a/context.go
+++ b/context.go
@@ -1121,20 +1121,6 @@ func (ctx *Context) MaxAge() int64 {
return -1
}
-// RequestTransactionScope is the request transaction scope of a handler's context
-// Can't say a lot here because I it will take more than 200 lines to write about.
-// You can search third-party articles or books on how Business Transaction works (it's quite simple, especialy here).
-// But I can provide you a simple example here: https://github.com/iris-contrib/examples/tree/master/request_transactions
-//
-// Note that this is unique and new
-// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
-// it's not covers all paths,
-// such as databases, this should be managed by the libraries you use to make your database connection,
-// this transaction scope is only for iris' request/response(Context).
-type RequestTransactionScope struct {
- Context *Context
-}
-
// ErrWithStatus custom error type which is useful
// to send an error containing the http status code and a reason
type ErrWithStatus struct {
@@ -1145,6 +1131,8 @@ type ErrWithStatus struct {
}
// Status sets the http status code of this error
+// if only status exists but no reason then
+// custom http error of this staus (if any) will be fired (context.EmitError)
func (err *ErrWithStatus) Status(statusCode int) *ErrWithStatus {
err.statusCode = statusCode
return err
@@ -1173,15 +1161,77 @@ func NewErrWithStatus() *ErrWithStatus {
return new(ErrWithStatus)
}
+// TransactionValidator used to register global transaction pre-validators
+type TransactionValidator interface {
+ // ValidateTransaction pre-validates transactions
+ // transaction fails if this returns an error or it's Complete has a non empty error
+ ValidateTransaction(*TransactionScope) error
+}
+
+// TransactionScope is the (request) transaction scope of a handler's context
+// Can't say a lot here because I it will take more than 200 lines to write about.
+// You can search third-party articles or books on how Business Transaction works (it's quite simple, especialy here).
+// But I can provide you a simple example here: https://github.com/iris-contrib/examples/tree/master/transactions
+//
+// Note that this is unique and new
+// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
+// it's not covers all paths,
+// such as databases, this should be managed by the libraries you use to make your database connection,
+// this transaction scope is only for iris' request & response(Context).
+type TransactionScope struct {
+ Context *Context
+ isRequestScoped bool
+ isFailure bool
+}
+
+// RequestScoped receives a boolean which determinates if other transactions depends on this.
+// If setted true then whenever this transaction is not completed succesfuly,
+// the rest of the transactions will be not executed at all.
+//
+// Defaults to false, execute all transactions on their own independently scopes.
+func (r *TransactionScope) RequestScoped(isRequestScoped bool) {
+ r.isRequestScoped = isRequestScoped
+}
+
// Complete completes the transaction
-// - if the error is not nil then the response
-// is resetting and sends an error to the client.
-// - if the error is nil then the response sent as expected.
+// rollback and send an error when:
+// 1. a not nil error AND non-empty reason AND custom type error has status code
+// 2. a not nil error AND empty reason BUT custom type error has status code
+// 3. a not nil error AND non-empty reason.
//
// The error can be a type of ErrWithStatus, create using the iris.NewErrWithStatus().
-func (r *RequestTransactionScope) Complete(err error) {
- if err != nil && err.Error() != "" {
+func (r *TransactionScope) Complete(err error) {
+ if err != nil {
+
ctx := r.Context
+ statusCode := StatusInternalServerError // default http status code if not provided
+ reason := err.Error()
+
+ if errWstatus, ok := err.(*ErrWithStatus); ok {
+ if errWstatus.statusCode > 0 {
+ // get the status code from the custom error type
+ statusCode = errWstatus.statusCode
+
+ // empty error message but status code given,
+ if reason == "" {
+ r.isFailure = true
+ // reset everything, cookies and headers and body.
+ ctx.Response.Reset()
+ // execute from custom (if any) http error (template or plain text)
+ ctx.EmitError(errWstatus.statusCode)
+ return
+ }
+ }
+ } else if reason == "" {
+ // do nothing empty reason and no status code means that this is not a failure, even if the error is not nil.
+ return
+ }
+
+ // rollback and send an error when we have:
+ // 1. a not nil error AND non-empty reason AND custom type error has status code
+ // 2. a not nil error AND empty reason BUT custom type error has status code
+ // 3. a not nil error AND non-empty reason.
+
// reset any previous response,
// except the content type we may use it to fire an error or take that from custom error type (?)
// no let's keep the custom error type as simple as possible, take that from prev attempt:
@@ -1197,39 +1247,53 @@ func (r *RequestTransactionScope) Complete(err error) {
// and anything else we tried to sent before.
ctx.Response.Reset()
- statusCode := StatusInternalServerError // default http status code if not provided
- reason := err.Error()
- shouldFireCustom := false
- if errWstatus, ok := err.(*ErrWithStatus); ok {
- statusCode = errWstatus.statusCode
- if reason == "" { // if we have custom error type with a given status but empty reason then fire from custom http error (or default)
- shouldFireCustom = true
- }
- }
- if shouldFireCustom {
- // if it's not our custom type of error nor an error with a non empty reason then we fire a default
- // or custom (EmitError) using the 500 internal server error
- ctx.EmitError(StatusInternalServerError)
- return
- }
// fire from the error or the custom error type
ctx.SetStatusCode(statusCode)
ctx.SetContentType(cType)
ctx.SetBodyString(reason)
+ r.isFailure = true
return
}
}
+// skipTransactionsContextKey set this to any value to stop executing next transactions
+// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
+const skipTransactionsContextKey = "@@IRIS_TRANSACTIONS_SKIP_@@"
+
+// SkipTransactions if called then skip the rest of the transactions
+// or all of them if called before the first transaction
+func (ctx *Context) SkipTransactions() {
+ ctx.Set(skipTransactionsContextKey, 1)
+}
+
+// TransactionsSkipped returns true if the transactions skipped or canceled at all.
+func (ctx *Context) TransactionsSkipped() bool {
+ if n, err := ctx.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
+ return true
+ }
+ return false
+}
+
// BeginTransaction starts a request scoped transaction.
//
-// See more here: https://github.com/iris-contrib/examples/tree/master/request_transactions
-func (ctx *Context) BeginTransaction(pipe func(scope *RequestTransactionScope)) {
+// See more here: https://github.com/iris-contrib/examples/tree/master/transactions
+func (ctx *Context) BeginTransaction(pipe func(scope *TransactionScope)) {
+ // do NOT begin a transaction when the previous transaction has been failed
+ // and it was requested scoped or SkipTransactions called manually
+ if ctx.TransactionsSkipped() {
+ return
+ }
// not the best way but this should be do the job if we want multiple transaction in the same handler and context.
tempCtx := *ctx // clone the temp context
- scope := &RequestTransactionScope{Context: &tempCtx}
+ scope := &TransactionScope{Context: &tempCtx}
pipe(scope) // run the context inside its scope
*ctx = *scope.Context // copy back the context
+
+ if scope.isRequestScoped && scope.isFailure {
+ ctx.SkipTransactions()
+ }
+
}
// Log logs to the iris defined logger
diff --git a/context_test.go b/context_test.go
index 002dfed3..7b12a7a0 100644
--- a/context_test.go
+++ b/context_test.go
@@ -781,15 +781,17 @@ func TestTemplatesDisabled(t *testing.T) {
e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg)
}
-func TestRequestTransactions(t *testing.T) {
+func TestTransactions(t *testing.T) {
iris.ResetDefault()
firstTransictionFailureMessage := "Error: Virtual failure!!!"
secondTransictionSuccessHTMLMessage := "