Repository: prest/prest
Pull request #745 (2022-12-02 17:26:55)

Add return status code support for plugin functions
Add return status code support for plugin functions
related to #687



pull-request-quantifier[bot]: ### ![](https://img.shields.io/static/v1?label=Quantified&message=Extra%20Small&color=green)

This PR has `36` quantified lines of changes. In general, a change size of upto `200` lines is ideal for the best PR experience!

------

<details >
    <summary display="inline"> <strong>Quantification details</strong></summary>
    <p />

```
Label      : Extra Small
Size       : +31 -5
Percentile : 14.4%

Total files changed: 3

Change summary by file extension:
.go : +31 -5
```
> Change counts above are quantified counts, based on the [PullRequestQuantifier customizations](https://github.com/microsoft/PullRequestQuantifier/blob/main/docs/prquantifier-yaml.md).
    
</details>


<details>
    <summary display="inline"> <strong>Why proper sizing of changes matters</strong> </summary>
    <p/>
    <p/>

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

- Fast and predictable releases to production:
  - Optimal size changes are more likely to be reviewed faster with fewer
iterations.
  - Similarity in low PR complexity drives similar review times.
- Review quality is likely higher as complexity is lower:
  - Bugs are more likely to be detected.
  - Code inconsistencies are more likely to be detected.
- Knowledge sharing is improved within the participants:
  - Small portions can be assimilated better.
- Better engineering practices are exercised:
  - Solving big problems by dividing them in well contained, smaller problems.
  - Exercising separation of concerns within the code changes.

#### What can I do to optimize my changes

- Use the PullRequestQuantifier to quantify your PR accurately
  - Create a context profile for your repo using the [context generator](https://github.com/microsoft/PullRequestQuantifier/releases)
  - Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the `Excluded` section from your `prquantifier.yaml` context profile. 
  - Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your `prquantifier.yaml` context profile.
  - Only use the labels that matter to you, [see context specification](./docs/prquantifier-yaml.md) to customize your `prquantifier.yaml` context profile.
- Change your engineering behaviors
  - For PRs that fall outside of the desired spectrum, review the details and check if:
    - Your PR could be split in smaller, self-contained PRs instead
    - Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

#### How to interpret the change counts in git diff output

- One line was added: `+1 -0`
- One line was deleted: `+0 -1`
- One line was modified: `+1 -1` (git diff doesn't know about modified, it will
interpret that line like one addition plus one deletion)
- Change percentiles: Change characteristics (addition, deletion, modification)
of this PR in relation to all other PRs within the repository.

</details>

<p />

------

Was this comment helpful? <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoic29kLWxvbCIsIlJlcG9zaXRvcnlMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0IiwiUHVsbFJlcXVlc3RMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0L3B1bGwvNzQ1IiwiRXZlbnRUeXBlIjoiVGh1bWJzVXAifQ==&amp;anonymous=True" target="_blank" title="Thumbs up"><strong>:thumbsup:</strong></a> <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoic29kLWxvbCIsIlJlcG9zaXRvcnlMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0IiwiUHVsbFJlcXVlc3RMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0L3B1bGwvNzQ1IiwiRXZlbnRUeXBlIjoiTmV1dHJhbCJ9&amp;anonymous=True" target="_blank" title="Neutral"><strong>&nbsp;:ok_hand:</strong></a> <a href="https://pullrequestquantifierfeedback.azurewebsites.net/feedback?payload=eyJBdXRob3JOYW1lIjoic29kLWxvbCIsIlJlcG9zaXRvcnlMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0IiwiUHVsbFJlcXVlc3RMaW5rIjoiaHR0cHM6Ly9naXRodWIuY29tL3ByZXN0L3ByZXN0L3B1bGwvNzQ1IiwiRXZlbnRUeXBlIjoiVGh1bWJzRG93biJ9&amp;anonymous=True" target="_blank" title="Thumbs down"><strong>&nbsp;:thumbsdown:</strong></a> (<a href="MAILTO:prquantifier@microsoft.com" title="Mail to prquantifier@microsoft.com">Email</a>)
[Customize PullRequestQuantifier](https://github.com/microsoft/PullRequestQuantifier/blob/main/docs/prquantifier-yaml.md) for this repository.


arxdsilva: Please fix tests (:

Patch:
diff --git a/Makefile b/Makefile
index e56b85c..0cb8376 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,7 @@
+DOCKER_COMPOSE?=docker-compose
+
 PHONY: \
 	build_test_image \
 
 build_test_image:
-	docker-compose -f docker-compose.yml  run --rm postgres -d
+	$(DOCKER_COMPOSE) -f docker-compose.yml  run --rm postgres -d
diff --git a/README.md b/README.md
index 68c72eb..043c90f 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
 [![GoDoc](https://godoc.org/github.com/prest/prest?status.png)](https://godoc.org/github.com/prest/prest)
 [![Go Report Card](https://goreportcard.com/badge/github.com/prest/prest)](https://goreportcard.com/report/github.com/prest/prest)
 [![codecov](https://codecov.io/gh/prest/prest/branch/main/graph/badge.svg?token=eVD9urwIEv)](https://codecov.io/gh/prest/prest)
-[![SourceLevel](https://app.sourcelevel.io/github/prest/-/prest.svg)](https://app.sourcelevel.io/github/prest/-/prest)
 [![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/prestd.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/prestd)
 [![Slack](https://img.shields.io/badge/slack-prestd-blueviolet.svg?logo=slack)](http://slack.prestd.com/)
 
diff --git a/adapters/postgres/errors.go b/adapters/postgres/errors.go
index c5793ad..b502073 100644
--- a/adapters/postgres/errors.go
+++ b/adapters/postgres/errors.go
@@ -11,6 +11,6 @@ var (
 	ErrInvalidOperator         = errors.New("invalid operator")
 	ErrInvalidGroupFn          = errors.New("invalid group function")
 	// ErrBodyEmpty err throw when body is empty
-	ErrBodyEmpty               = errors.New("body is empty")
-	ErrEmptyOrInvalidSlice     = errors.New("empty or invalid slice")
+	ErrBodyEmpty           = errors.New("body is empty")
+	ErrEmptyOrInvalidSlice = errors.New("empty or invalid slice")
 )
diff --git a/adapters/postgres/postgres.go b/adapters/postgres/postgres.go
index 3ae3c56..8a4fd10 100644
--- a/adapters/postgres/postgres.go
+++ b/adapters/postgres/postgres.go
@@ -162,14 +162,14 @@ func chkInvalidIdentifier(identifer ...string) bool {
 		}
 
 		ivalSplit := strings.Split(ival, ".")
-		if len(ivalSplit) == 2 && len(ivalSplit[len(ivalSplit) - 1]) > 63 {
+		if len(ivalSplit) == 2 && len(ivalSplit[len(ivalSplit)-1]) > 63 {
 			return true
 		}
 
 		if !strings.Contains(ival, ".") && len(ival) > 63 {
 			return true
 		}
-		
+
 		count := 0
 		for _, v := range ival {
 			if !unicode.IsLetter(v) &&
@@ -316,25 +316,25 @@ func (adapter *Postgres) ReturningByRequest(r *http.Request) (returningSyntax st
 }
 
 func sliceToJSONList(ifaceSlice interface{}) (returnValue string, err error) {
-    v := reflect.ValueOf(ifaceSlice)
+	v := reflect.ValueOf(ifaceSlice)
 
-	if v.Kind() == 0 {
+	if v.Kind() == reflect.Invalid {
 		return "[]", ErrEmptyOrInvalidSlice
 	}
 
 	value := make([]string, 0)
-	
-    for i := 0; i < v.Len(); i++ {
-        val := v.Index(i).Interface()
-		switch val.(type){
-			case int, float64:
-				newVal := fmt.Sprint(val)
-				value = append(value, newVal)
-			default:
-				newVal := fmt.Sprintf(`"%s"`, val)
-				value = append(value, newVal)
+
+	for i := 0; i < v.Len(); i++ {
+		val := v.Index(i).Interface()
+		switch val.(type) {
+		case int, float64:
+			newVal := fmt.Sprint(val)
+			value = append(value, newVal)
+		default:
+			newVal := fmt.Sprintf(`"%s"`, val)
+			value = append(value, newVal)
 		}
-    }
+	}
 	returnValue = fmt.Sprintf(`[%v]`, strings.Join(value, ", "))
 	return
 }
@@ -361,23 +361,23 @@ func (adapter *Postgres) SetByRequest(r *http.Request, initialPlaceholderID int)
 		key = fmt.Sprintf(`"%s"`, strings.Join(keys, `"."`))
 		fields = append(fields, fmt.Sprintf(`%s=$%d`, key, initialPlaceholderID))
 
-		switch reflect.ValueOf(value).Kind(){
-			case reflect.Interface:
-				values = append(values, formatters.FormatArray(value))
-			case reflect.Map:
-				jsonData, err := json.Marshal(value)
-				if err != nil {
-					log.Errorln(err)
-				}
-				values = append(values, string(jsonData))
-			case reflect.Slice:
-				value, err = sliceToJSONList(value)
-				if err != nil {
-					log.Errorln(err)
-				}
-				values = append(values, value)
-			default:
-				values = append(values, value)
+		switch reflect.ValueOf(value).Kind() {
+		case reflect.Interface:
+			values = append(values, formatters.FormatArray(value))
+		case reflect.Map:
+			jsonData, err := json.Marshal(value)
+			if err != nil {
+				log.Errorln(err)
+			}
+			values = append(values, string(jsonData))
+		case reflect.Slice:
+			value, err = sliceToJSONList(value)
+			if err != nil {
+				log.Errorln(err)
+			}
+			values = append(values, value)
+		default:
+			values = append(values, value)
 		}
 		initialPlaceholderID++
 	}
@@ -1296,6 +1296,15 @@ func GetQueryOperator(op string) (string, error) {
 		return "NOT LIKE", nil
 	case "nilike":
 		return "NOT ILIKE", nil
+	// ltree features
+	case "ltreelanc":
+		return "@>", nil
+	case "ltreerdesc":
+		return "<@", nil
+	case "ltreematch":
+		return "~", nil
+	case "ltreematchtxt":
+		return "@", nil
 	}
 
 	return "", ErrInvalidOperator
diff --git a/config/config.go b/config/config.go
index 7edb03a..df2c34e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -44,6 +44,14 @@ type AccessConf struct {
 	Tables      []TablesConf
 }
 
+// ExposeConf (expose data) information
+type ExposeConf struct {
+	Enabled         bool
+	DatabaseListing bool
+	SchemaListing   bool
+	TableListing    bool
+}
+
 // Prest basic config
 type Prest struct {
 	AuthEnabled          bool
@@ -78,6 +86,7 @@ type Prest struct {
 	MigrationsPath       string
 	QueriesPath          string
 	AccessConf           AccessConf
+	ExposeConf           ExposeConf
 	CORSAllowOrigin      []string
 	CORSAllowHeaders     []string
 	CORSAllowMethods     []string
@@ -147,6 +156,10 @@ func viperCfg() {
 	viper.SetDefault("cache.storagepath", "./")
 	viper.SetDefault("cache.sufixfile", ".cache.prestd.db")
 	viper.SetDefault("pluginpath", "./lib")
+	viper.SetDefault("expose.enabled", false)
+	viper.SetDefault("expose.tables", true)
+	viper.SetDefault("expose.schemas", true)
+	viper.SetDefault("expose.databases", true)
 
 	hDir, err := homedir.Dir()
 	if err != nil {
@@ -231,6 +244,10 @@ func Parse(cfg *Prest) (err error) {
 	cfg.Cache.Time = viper.GetInt("cache.time")
 	cfg.Cache.StoragePath = viper.GetString("cache.storagepath")
 	cfg.Cache.SufixFile = viper.GetString("cache.sufixfile")
+	cfg.ExposeConf.Enabled = viper.GetBool("expose.enabled")
+	cfg.ExposeConf.TableListing = viper.GetBool("expose.tables")
+	cfg.ExposeConf.SchemaListing = viper.GetBool("expose.schemas")
+	cfg.ExposeConf.DatabaseListing = viper.GetBool("expose.databases")
 	var cacheendpoints []CacheEndpoint
 	err = viper.UnmarshalKey("cache.endpoints", &cacheendpoints)
 	if err != nil {
diff --git a/docs/api-reference/parameters.md b/docs/api-reference/parameters.md
index a8c2054..be70017 100644
--- a/docs/api-reference/parameters.md
+++ b/docs/api-reference/parameters.md
@@ -75,3 +75,7 @@ Uses these operators in various filter applications
 | `$ilike` | Matches _case-insensitive_ always cover the entire string |
 | `$nlike` | Matches results without the entire string |
 | `$nilike` | Matches _case-insensitive_ results without the entire string |
+| `$ltreelanc` | Is left argument an ancestor of right (or equal)? |
+| `$ltreerdesc` | Is left argument a descendant of right (or equal)? |
+| `$ltreematch` | Does ltree match lquery? |
+| `$ltreematchtxt` | Does ltree match ltxtquery? |
diff --git a/docs/api-reference/plugins.md b/docs/api-reference/plugins.md
index befe399..0165dc6 100644
--- a/docs/api-reference/plugins.md
+++ b/docs/api-reference/plugins.md
@@ -104,6 +104,23 @@ func GETHelloHandler() (ret string) {
  ret = string(respJSON)
  return
 }
+
+// GETHelloHandler plugin
+// same function as GETHelloHandler, but this time we can return status code.
+func GETHelloWithStatusHandler() (ret string, code int) {
+	resp := Response{
+		HTTPVars: HTTPVars,
+		URLQuery: URLQuery,
+		MSG:      "Hello plugin caller!",
+	}
+	respJSON, err := json.Marshal(resp)
+	if err != nil {
+		return
+	}
+	ret = string(respJSON)
+	code = http.StatusAccepted
+	return
+}
 ```
 
 **Request:**
diff --git a/docs/deployment/server-configuration.md b/docs/deployment/server-configuration.md
index 7e3acf7..16101ed 100644
--- a/docs/deployment/server-configuration.md
+++ b/docs/deployment/server-configuration.md
@@ -83,6 +83,12 @@ mode = "disable"
 sslcert = "./PATH"
 sslkey = "./PATH"
 sslrootcert = "./PATH"
+
+[expose]
+enabled = true
+databases = true
+schemas = true
+tables = true
 ```
 
 ## Authorization
@@ -145,6 +151,48 @@ password = "password"
 
 > to validate all endpoints with generated jwt token must be activated jwt option
 
+## Expose Data
+
+The expose data setting enables you to configure if you want users to be able to reach listing endpoints, such as:
+
+ - /databases
+ - /schemas
+ - /tables
+
+
+An example of a configuration file disabling all listings:
+
+```toml
+# previous toml content
+[expose]
+    enabled = true
+```
+
+If you want to disable just the database listing:
+
+```toml
+# previous toml content
+[expose]
+    databases = true
+```
+
+| Name | Description |
+| --- | --- |
+| enabled | Set this as `true` if you want to **disable** all listing endpoints available. |
+| databases | Set this as `false` if you want to **disable** *databases* listing endpoints only. |
+| schemas | Set this as `false` if you want to **disable** *schemas* listing endpoints only. |
+| tables | Set this as `false` if you want to **disable** *tables* listing endpoints only. |
+
+### Default values for Exposure Settings
+
+| Name | Default Value |
+| --- | --- |
+| enabled | `false` |
+| databases | `true` |
+| schemas | `true` |
+| tables | `true` |
+
+
 ## SSL
 
 There is 4 options to set on ssl mode:
diff --git a/go.mod b/go.mod
index 34d64a0..20bee7e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,12 +8,12 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/gosidekick/migration/v3 v3.0.0
 	github.com/jmoiron/sqlx v1.3.5
-	github.com/lib/pq v1.10.6
+	github.com/lib/pq v1.10.7
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
-	github.com/rs/cors v1.8.2
-	github.com/spf13/cobra v1.6.0
-	github.com/spf13/viper v1.13.0
+	github.com/rs/cors v1.8.3
+	github.com/spf13/cobra v1.6.1
+	github.com/spf13/viper v1.14.0
 	github.com/stretchr/testify v1.8.1
 	github.com/structy/log v0.0.0-20220126185905-65895d7017d5
 	github.com/tidwall/buntdb v1.2.10
@@ -23,7 +23,7 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
@@ -31,7 +31,7 @@ require (
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/spf13/afero v1.8.2 // indirect
+	github.com/spf13/afero v1.9.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
@@ -43,10 +43,10 @@ require (
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/tidwall/rtred v0.1.2 // indirect
 	github.com/tidwall/tinyqueue v0.1.1 // indirect
-	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
-	golang.org/x/text v0.3.7 // indirect
-	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
+	golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
+	golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
+	golang.org/x/text v0.4.0 // indirect
+	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index e4acec5..650ae9a 100644
--- a/go.sum
+++ b/go.sum
@@ -62,8 +62,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
-github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -106,7 +106,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -153,8 +153,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
-github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
+github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
 github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -176,24 +176,24 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
-github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
+github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
-github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
+github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
+github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
-github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
-github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
+github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
+github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -248,8 +248,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -368,9 +368,8 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -378,8 +377,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -434,8 +433,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/lib/src/hello.go b/lib/src/hello.go
index 7b82e18..5c33909 100644
--- a/lib/src/hello.go
+++ b/lib/src/hello.go
@@ -5,6 +5,7 @@ package main
 
 import (
 	"encoding/json"
+	"net/http"
 )
 
 var (
@@ -41,3 +42,18 @@ func GETHelloHandler() (ret string) {
 	ret = string(respJSON)
 	return
 }
+
+func GETHelloWithStatusHandler() (ret string, code int) {
+	resp := Response{
+		HTTPVars: HTTPVars,
+		URLQuery: URLQuery,
+		MSG:      "Hello plugin caller!",
+	}
+	respJSON, err := json.Marshal(resp)
+	if err != nil {
+		return
+	}
+	ret = string(respJSON)
+	code = http.StatusAccepted
+	return
+}
diff --git a/middlewares/config.go b/middlewares/config.go
index 12f0bdc..3ca3bea 100644
--- a/middlewares/config.go
+++ b/middlewares/config.go
@@ -42,6 +42,9 @@ func initApp() {
 		if config.PrestConf.Cache.Enabled {
 			MiddlewareStack = append(MiddlewareStack, CacheMiddleware())
 		}
+		if config.PrestConf.ExposeConf.Enabled {
+			MiddlewareStack = append(MiddlewareStack, ExposureMiddleware())
+		}
 	}
 	app = negroni.New(MiddlewareStack...)
 }
diff --git a/middlewares/middlewares.go b/middlewares/middlewares.go
index d79030b..9a23e5c 100644
--- a/middlewares/middlewares.go
+++ b/middlewares/middlewares.go
@@ -159,3 +159,27 @@ func Cors(origin []string, headers []string) negroni.Handler {
 		next(w, r)
 	})
 }
+
+func ExposureMiddleware() negroni.Handler {
+	return negroni.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request, next http.HandlerFunc) {
+		url := rq.URL.Path
+		exposeConf := config.PrestConf.ExposeConf
+
+		if strings.HasPrefix(url, "/databases") && !exposeConf.DatabaseListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		if strings.HasPrefix(url, "/tables") && !exposeConf.TableListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		if strings.HasPrefix(url, "/schemas") && !exposeConf.SchemaListing {
+			http.Error(rw, "unauthorized listing", http.StatusUnauthorized)
+			return
+		}
+
+		next(rw, rq)
+	})
+}
diff --git a/plugins/plugins.go b/plugins/plugins.go
index b67a6ac..23021af 100644
--- a/plugins/plugins.go
+++ b/plugins/plugins.go
@@ -18,11 +18,17 @@ type LoadedPlugin struct {
 	Plugin *plugin.Plugin
 }
 
+// PluginFuncReturn structure for holding return value and status of plugin function.
+type PluginFuncReturn struct {
+	ReturnJson string
+	StatusCode int
+}
+
 // loadedFunc global variable to control plugins loaded, blocking duplicate loading
 var loadedFunc = map[string]LoadedPlugin{}
 
 // loadFunc private func to load and exec OS Library
-func loadFunc(fileName, funcName string, r *http.Request) (ret string, err error) {
+func loadFunc(fileName, funcName string, r *http.Request) (ret PluginFuncReturn, err error) {
 	libPath := filepath.Join(config.PrestConf.PluginPath, fmt.Sprintf("%s.so", fileName))
 	loadedPlugin := loadedFunc[libPath]
 	p := loadedPlugin.Plugin
@@ -61,9 +67,25 @@ func loadFunc(fileName, funcName string, r *http.Request) (ret string, err error
 	if err != nil {
 		return
 	}
-	// Exec (call) function name, return string
-	ret = f.(func() string)()
-	log.Println("ret plugin:", ret)
+	// Exec (call) function name, return string (In case which return status code does not matter)
+	function, ok := f.(func() string)
+
+	if !ok {
+		// It is probable that plugin function return not only json but also status code.
+		function := f.(func() (string, int))
+		retJson, code := function()
+		ret.ReturnJson = retJson
+		ret.StatusCode = code
+
+		log.Printf("ret plugin(status %d): %s\n", code, ret.ReturnJson)
+	} else {
+		retJson := function()
+		ret.ReturnJson = retJson
+		ret.StatusCode = -1
+
+		log.Println("ret plugin:", ret.ReturnJson)
+	}
+
 	return
 }
 
@@ -79,7 +101,12 @@ func HandlerPlugin(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	// Cache arrow if enabled
-	cache.BuntSet(r.URL.String(), ret)
+	cache.BuntSet(r.URL.String(), ret.ReturnJson)
+
 	//nolint
-	w.Write([]byte(ret))
+	if ret.StatusCode != -1 {
+		w.WriteHeader(ret.StatusCode)
+	}
+
+	w.Write([]byte(ret.ReturnJson))
 }
diff --git a/router/router.go b/router/router.go
index 705549c..fc8f366 100644
--- a/router/router.go
+++ b/router/router.go
@@ -51,6 +51,7 @@ func GetRouter() *mux.Router {
 	crudRoutes.HandleFunc("/{database}/{schema}/{table}", controllers.DeleteFromTable).Methods("DELETE")
 	crudRoutes.HandleFunc("/{database}/{schema}/{table}", controllers.UpdateTable).Methods("PUT", "PATCH")
 	router.PathPrefix("/").Handler(negroni.New(
+		middlewares.ExposureMiddleware(),
 		middlewares.AccessControl(),
 		middlewares.AuthMiddleware(),
 		middlewares.CacheMiddleware(),


Test patch:
diff --git a/adapters/postgres/postgres_test.go b/adapters/postgres/postgres_test.go
index a03bf42..421a41d 100644
--- a/adapters/postgres/postgres_test.go
+++ b/adapters/postgres/postgres_test.go
@@ -37,7 +37,7 @@ func TestLoad(t *testing.T) {
 	// Only run the failing part when a specific env variable is set
 	if os.Getenv("BE_CRASHER") == "1" {
 		Load()
-		os.Setenv("PREST_PG_DATABASE", "prest-test")
+		t.Setenv("PREST_PG_DATABASE", "prest-test")
 		return
 	}
 	// Start the actual test in a different subprocess
@@ -168,6 +168,8 @@ func TestSetByRequest(t *testing.T) {
 }
 
 func TestWhereByRequest(t *testing.T) {
+	config.Load()
+	Load()
 	var testCases = []struct {
 		description    string
 		url            string
@@ -186,6 +188,10 @@ func TestWhereByRequest(t *testing.T) {
 		{"Where by request with not ilike", "/prest-test/public/test5?name=$nilike.%25vAl%25&phonenumber=123456", []string{`"name" NOT ILIKE $`, `"phonenumber" = $`, " AND "}, []string{"%vAl%", "123456"}, nil},
 		{"Where by request with multiple colunm values", "/prest-test/public/table?created_at='$gte.1997-11-03'&created_at='$lte.1997-12-05'", []string{`"created_at" >= $`, ` AND `, `"created_at" <= $`}, []string{`'1997-11-03'`, `'1997-12-05'`}, nil},
 		{"Where by request with tsquery", "/prest-test/public/test5?name:tsquery=prest", []string{`name @@ to_tsquery('prest')`}, []string{`'prest'`}, nil},
+		{"Where by request with ltree left acendent", "/prest-test/public/test5?path='$ltreelanc.Top.*'", []string{`"path" @> $`}, []string{`'Top.*'`}, nil},
+		{"Where by request with ltree right descendent", "/prest-test/public/test5?path='$ltreerdesc.Top.*'", []string{`"path" <@ $`}, []string{`'Top.*'`}, nil},
+		{"Where by request with ltree match lquery", "/prest-test/public/test5?path='$ltreematch.Top.*'", []string{`"path" ~ $`}, []string{`'Top.*'`}, nil},
+		{"Where by request with ltree match ltxtquery", "/prest-test/public/test5?path='$ltreematchtxt.Top*'", []string{`"path" @ $`}, []string{`'Top*'`}, nil},
 	}
 
 	for _, tc := range testCases {
@@ -930,6 +936,10 @@ func TestGetQueryOperator(t *testing.T) {
 		{"$ilike", "ILIKE"},
 		{"$nlike", "NOT LIKE"},
 		{"$nilike", "NOT ILIKE"},
+		{"$ltreelanc", "@>"},
+		{"$ltreerdesc", "<@"},
+		{"$ltreematch", "~"},
+		{"$ltreematchtxt", "@"},
 	}
 
 	for _, tc := range testCases {
@@ -1284,7 +1294,7 @@ func BenchmarkPrepare(b *testing.B) {
 }
 
 func TestDisableCache(t *testing.T) {
-	os.Setenv("PREST_PG_CACHE", "false")
+	t.Setenv("PREST_PG_CACHE", "false")
 	config.Load()
 	Load()
 	ClearStmt()
@@ -1296,7 +1306,6 @@ func TestDisableCache(t *testing.T) {
 	if ok {
 		t.Error("has query in cache")
 	}
-	os.Setenv("PREST_PG_CACHE", "true")
 }
 
 func TestParseBatchInsertRequest(t *testing.T) {
@@ -1987,13 +1996,13 @@ func newScannerMock(t *testing.T) (sc adapters.Scanner) {
 func TestSliceToJSONList(t *testing.T) {
 	var i interface{}
 	var testCases = []struct {
-		description    string
-		body           interface{}
-		expectedValue  string
-		err            error
+		description   string
+		body          interface{}
+		expectedValue string
+		err           error
 	}{
-		{"String slice", []string{"one","two","three"}, `["one", "two", "three"]`, nil},
-		{"Integer slice", []int{1,2,3}, `[1, 2, 3]`, nil},
+		{"String slice", []string{"one", "two", "three"}, `["one", "two", "three"]`, nil},
+		{"Integer slice", []int{1, 2, 3}, `[1, 2, 3]`, nil},
 		{"Interface error", i, `[]`, ErrEmptyOrInvalidSlice},
 	}
 
@@ -2010,4 +2019,4 @@ func TestSliceToJSONList(t *testing.T) {
 			t.Errorf("expected %s in %s", err, tc.err)
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/cache/buntdb_test.go b/cache/buntdb_test.go
index bb1c3d6..6af1a6c 100644
--- a/cache/buntdb_test.go
+++ b/cache/buntdb_test.go
@@ -2,7 +2,6 @@ package cache
 
 import (
 	"net/http/httptest"
-	"os"
 	"testing"
 
 	"github.com/prest/prest/adapters/postgres"
@@ -15,7 +14,7 @@ func init() {
 }
 
 func TestBuntGetDoesntExist(t *testing.T) {
-	os.Setenv("PREST_CACHE", "true")
+	t.Setenv("PREST_CACHE", "true")
 	config.Load()
 	w := httptest.NewRecorder()
 
diff --git a/cache/cache_test.go b/cache/cache_test.go
index ebe4528..9524d52 100644
--- a/cache/cache_test.go
+++ b/cache/cache_test.go
@@ -10,12 +10,10 @@ import (
 func init() {
 	os.Setenv("PREST_CONF", "./testdata/prest.toml")
 	os.Setenv("PREST_CACHE_ENABLED", "true")
+	os.Setenv("PREST_PG_CACHE", "true")
 	config.Load()
 }
 func TestEndpointRulesEnable(t *testing.T) {
-	os.Setenv("PREST_CONF", "./testdata/prest.toml")
-	os.Setenv("PREST_CACHE_ENABLED", "true")
-	config.Load()
 	config.PrestConf.Cache.Endpoints = append(config.PrestConf.Cache.Endpoints, config.CacheEndpoint{
 		Time:     5,
 		Endpoint: "/prest/public/test",
@@ -38,9 +36,6 @@ func TestEndpointRulesNotExist(t *testing.T) {
 }
 
 func TestEndpointRulesDisable(t *testing.T) {
-	os.Setenv("PREST_CONF", "./testdata/prest.toml")
-	os.Setenv("PREST_CACHE_ENABLED", "true")
-	config.Load()
 	config.PrestConf.Cache.Endpoints = append(config.PrestConf.Cache.Endpoints, config.CacheEndpoint{
 		Endpoint: "/prest/public/test-disable",
 		Enabled:  false,
diff --git a/config/config_test.go b/config/config_test.go
index b6572c2..9db6d01 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestLoad(t *testing.T) {
-	os.Setenv("PREST_CONF", "../testdata/prest.toml")
+	t.Setenv("PREST_CONF", "../testdata/prest.toml")
 	Load()
 	require.Greaterf(t, len(PrestConf.AccessConf.Tables), 2,
 		"expected > 2, got: %d", len(PrestConf.AccessConf.Tables))
@@ -19,78 +19,75 @@ func TestLoad(t *testing.T) {
 	}
 	require.True(t, PrestConf.AccessConf.Restrict, "expected true, but got false")
 	require.Equal(t, 60, PrestConf.HTTPTimeout)
-
-	os.Setenv("PREST_CONF", "foo/bar/prest.toml")
 }
 
 func TestParse(t *testing.T) {
-	os.Setenv("PREST_CONF", "../testdata/prest.toml")
-	viperCfg()
-	cfg := &Prest{}
-	err := Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 3000, cfg.HTTPPort)
-
-	var expected string
-	expected = os.Getenv("PREST_PG_DATABASE")
-	if len(expected) == 0 {
-		expected = "prest"
-	}
-
-	require.Equal(t, expected, cfg.PGDatabase)
-
-	os.Unsetenv("PREST_CONF")
-	os.Unsetenv("PREST_JWT_DEFAULT")
-	os.Setenv("PREST_HTTP_PORT", "4000")
-
-	viperCfg()
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 4000, cfg.HTTPPort)
-	require.True(t, cfg.EnableDefaultJWT)
-
-	os.Unsetenv("PREST_CONF")
-
-	os.Setenv("PREST_CONF", "")
-	os.Setenv("PREST_JWT_DEFAULT", "false")
-
-	viperCfg()
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 4000, cfg.HTTPPort)
-	require.False(t, cfg.EnableDefaultJWT)
-
-	os.Unsetenv("PREST_JWT_DEFAULT")
-
-	viperCfg()
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 4000, cfg.HTTPPort)
-
-	os.Unsetenv("PREST_CONF")
-	os.Unsetenv("PREST_HTTP_PORT")
-	os.Setenv("PREST_JWT_KEY", "s3cr3t")
-
-	viperCfg()
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, "s3cr3t", cfg.JWTKey)
-	require.Equal(t, "HS256", cfg.JWTAlgo)
-
-	os.Unsetenv("PREST_JWT_KEY")
-	os.Setenv("PREST_JWT_ALGO", "HS512")
-
-	viperCfg()
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, "HS512", cfg.JWTAlgo)
-
-	os.Unsetenv("PREST_JWT_ALGO")
+	t.Run("PREST_CONF", func(t *testing.T) {
+		t.Setenv("PREST_CONF", "../testdata/prest.toml")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 3000, cfg.HTTPPort)
+
+		var expected string
+		expected = os.Getenv("PREST_PG_DATABASE")
+		if len(expected) == 0 {
+			expected = "prest"
+		}
+
+		require.Equal(t, expected, cfg.PGDatabase)
+	})
+
+	t.Run("PREST_HTTP_PORT and unset PREST_JWT_DEFAULT", func(t *testing.T) {
+		t.Setenv("PREST_HTTP_PORT", "4000")
+		os.Unsetenv("PREST_JWT_DEFAULT")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 4000, cfg.HTTPPort)
+		require.True(t, cfg.EnableDefaultJWT)
+	})
+
+	t.Run("empty PREST_CONF and falsey PREST_JWT_DEFAULT", func(t *testing.T) {
+		t.Setenv("PREST_CONF", "")
+		t.Setenv("PREST_JWT_DEFAULT", "false")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 3000, cfg.HTTPPort)
+		require.False(t, cfg.EnableDefaultJWT)
+	})
+
+	t.Run("empty PREST_CONF", func(t *testing.T) {
+		t.Setenv("PREST_CONF", "")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 3000, cfg.HTTPPort)
+	})
+
+	t.Run("PREST_JWT_KEY", func(t *testing.T) {
+		t.Setenv("PREST_JWT_KEY", "s3cr3t")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, "s3cr3t", cfg.JWTKey)
+		require.Equal(t, "HS256", cfg.JWTAlgo)
+	})
+
+	t.Run("PREST_JWT_ALGO", func(t *testing.T) {
+		t.Setenv("PREST_JWT_ALGO", "HS512")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, "HS512", cfg.JWTAlgo)
+	})
 }
 
 func TestGetDefaultPrestConf(t *testing.T) {
@@ -120,59 +117,62 @@ func TestGetDefaultPrestConf(t *testing.T) {
 }
 
 func TestDatabaseURL(t *testing.T) {
-	os.Setenv("PREST_PG_URL", "postgresql://user:pass@localhost:1234/mydatabase/?sslmode=disable")
-
 	viperCfg()
-	cfg := &Prest{}
-	err := Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, "mydatabase", cfg.PGDatabase)
-	require.Equal(t, "localhost", cfg.PGHost)
-	require.Equal(t, 1234, cfg.PGPort)
-	require.Equal(t, "user", cfg.PGUser)
-	require.Equal(t, "pass", cfg.PGPass)
-	require.Equal(t, "disable", cfg.SSLMode)
-
-	os.Unsetenv("PREST_PG_URL")
-	os.Setenv("DATABASE_URL", "postgresql://cloud:cloudPass@localhost:5432/CloudDatabase/?sslmode=disable")
-
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 5432, cfg.PGPort)
-	require.Equal(t, "cloud", cfg.PGUser)
-	require.Equal(t, "cloudPass", cfg.PGPass)
-	require.Equal(t, "disable", cfg.SSLMode)
 
-	os.Unsetenv("DATABASE_URL")
+	t.Run("PREST_PG_URL", func(t *testing.T) {
+		t.Setenv("PREST_PG_URL", "postgresql://user:pass@localhost:1234/mydatabase/?sslmode=disable")
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, "mydatabase", cfg.PGDatabase)
+		require.Equal(t, "localhost", cfg.PGHost)
+		require.Equal(t, 1234, cfg.PGPort)
+		require.Equal(t, "user", cfg.PGUser)
+		require.Equal(t, "pass", cfg.PGPass)
+		require.Equal(t, "disable", cfg.SSLMode)
+	})
+
+	t.Run("DATABASE_URL", func(t *testing.T) {
+		t.Setenv("DATABASE_URL", "postgresql://cloud:cloudPass@localhost:5432/CloudDatabase/?sslmode=disable")
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 5432, cfg.PGPort)
+		require.Equal(t, "cloud", cfg.PGUser)
+		require.Equal(t, "cloudPass", cfg.PGPass)
+		require.Equal(t, "disable", cfg.SSLMode)
+	})
 }
 
 func TestHTTPPort(t *testing.T) {
-	os.Setenv("PORT", "8080")
-
 	viperCfg()
-	cfg := &Prest{}
-	err := Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 8080, cfg.HTTPPort)
-
-	// set env PREST_HTTP_PORT and PORT
-	os.Setenv("PREST_HTTP_PORT", "3000")
-
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 8080, cfg.HTTPPort)
-
-	// unset env PORT and set PREST_HTTP_PORT
-	os.Unsetenv("PORT")
-
-	cfg = &Prest{}
-	err = Parse(cfg)
-	require.NoError(t, err)
-	require.Equal(t, 3000, cfg.HTTPPort)
 
-	os.Unsetenv("PREST_HTTP_PORT")
+	t.Run("set PORT", func(t *testing.T) {
+		t.Setenv("PORT", "8080")
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 8080, cfg.HTTPPort)
+	})
+
+	t.Run("set PREST_HTTP_PORT", func(t *testing.T) {
+		t.Setenv("PREST_HTTP_PORT", "3000")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 3000, cfg.HTTPPort)
+	})
+
+	t.Run("set PORT and PREST_HTTP_PORT", func(t *testing.T) {
+		t.Setenv("PORT", "8080")
+		t.Setenv("PREST_HTTP_PORT", "3000")
+		viperCfg()
+		cfg := &Prest{}
+		err := Parse(cfg)
+		require.NoError(t, err)
+		require.Equal(t, 8080, cfg.HTTPPort)
+	})
 }
 
 func Test_parseDatabaseURL(t *testing.T) {
@@ -194,16 +194,14 @@ func Test_parseDatabaseURL(t *testing.T) {
 func Test_portFromEnv(t *testing.T) {
 	c := &Prest{}
 
-	os.Setenv("PORT", "PORT")
+	t.Setenv("PORT", "PORT")
 
 	err := portFromEnv(c)
 	require.Error(t, err)
-
-	os.Unsetenv("PORT")
 }
 
 func Test_Auth(t *testing.T) {
-	os.Setenv("PREST_CONF", "../testdata/prest.toml")
+	t.Setenv("PREST_CONF", "../testdata/prest.toml")
 
 	viperCfg()
 	cfg := &Prest{}
@@ -223,3 +221,23 @@ func Test_Auth(t *testing.T) {
 		require.Equal(t, metadata[i], v)
 	}
 }
+
+func Test_ExposeDataConfig(t *testing.T) {
+	t.Setenv("PREST_CONF", "../testdata/prest_expose.toml")
+
+	viperCfg()
+	cfg := &Prest{}
+	err := Parse(cfg)
+	require.NoError(t, err)
+	require.Equal(t, true, cfg.ExposeConf.Enabled)
+	require.Equal(t, false, cfg.ExposeConf.DatabaseListing)
+	require.Equal(t, false, cfg.ExposeConf.SchemaListing)
+	require.Equal(t, false, cfg.ExposeConf.TableListing)
+
+	metadata := []string{"first_name", "last_name", "last_login"}
+	require.Equal(t, len(metadata), len(cfg.AuthMetadata))
+
+	for i, v := range cfg.AuthMetadata {
+		require.Equal(t, metadata[i], v)
+	}
+}
diff --git a/controllers/sql_test.go b/controllers/sql_test.go
index 9bc6b67..42312f0 100644
--- a/controllers/sql_test.go
+++ b/controllers/sql_test.go
@@ -4,7 +4,6 @@ package controllers
 import (
 	"net/http"
 	"net/http/httptest"
-	"os"
 	"testing"
 
 	"github.com/gorilla/mux"
@@ -92,7 +91,7 @@ func TestRenderWithXML(t *testing.T) {
 	}{
 		{"Get schemas with COUNT clause with XML Render", "/schemas?_count=*&_renderer=xml", "GET", 200, "<objects><object><count>4</count></object></objects>"},
 	}
-	os.Setenv("PREST_DEBUG", "true")
+	t.Setenv("PREST_DEBUG", "true")
 	config.Load()
 	postgres.Load()
 	n := middlewares.GetApp()
diff --git a/middlewares/config_test.go b/middlewares/config_test.go
index 9f39471..b373055 100644
--- a/middlewares/config_test.go
+++ b/middlewares/config_test.go
@@ -5,7 +5,6 @@ import (
 	"io"
 	"net/http"
 	"net/http/httptest"
-	"os"
 	"testing"
 
 	"github.com/gorilla/mux"
@@ -77,7 +76,7 @@ func TestGetAppWithoutReorderedMiddleware(t *testing.T) {
 }
 
 func Test_Middleware_DoesntBlock_CustomRoutes(t *testing.T) {
-	os.Setenv("PREST_DEBUG", "true")
+	t.Setenv("PREST_DEBUG", "true")
 	config.Load()
 	postgres.Load()
 	app = nil
@@ -90,7 +89,7 @@ func Test_Middleware_DoesntBlock_CustomRoutes(t *testing.T) {
 		AccessControl(),
 		negroni.Wrap(crudRoutes),
 	))
-	os.Setenv("PREST_CONF", "../testdata/prest.toml")
+	t.Setenv("PREST_CONF", "../testdata/prest.toml")
 	n := GetApp()
 	n.UseHandler(r)
 
@@ -121,7 +120,6 @@ func Test_Middleware_DoesntBlock_CustomRoutes(t *testing.T) {
 	require.Contains(t, string(body), "required authorization to table")
 
 	MiddlewareStack = []negroni.Handler{}
-	os.Setenv("PREST_CONF", "")
 }
 
 func customMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
@@ -136,7 +134,7 @@ func customMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerF
 }
 
 func TestDebug(t *testing.T) {
-	os.Setenv("PREST_DEBUG", "true")
+	t.Setenv("PREST_DEBUG", "true")
 	config.Load()
 	nd := appTest()
 	serverd := httptest.NewServer(nd)
@@ -148,8 +146,8 @@ func TestDebug(t *testing.T) {
 
 func TestEnableDefaultJWT(t *testing.T) {
 	app = nil
-	os.Setenv("PREST_JWT_DEFAULT", "false")
-	os.Setenv("PREST_DEBUG", "false")
+	t.Setenv("PREST_JWT_DEFAULT", "false")
+	t.Setenv("PREST_DEBUG", "false")
 	config.Load()
 	nd := appTest()
 	serverd := httptest.NewServer(nd)
@@ -162,8 +160,8 @@ func TestEnableDefaultJWT(t *testing.T) {
 func TestJWTIsRequired(t *testing.T) {
 	MiddlewareStack = []negroni.Handler{}
 	app = nil
-	os.Setenv("PREST_JWT_DEFAULT", "true")
-	os.Setenv("PREST_DEBUG", "false")
+	t.Setenv("PREST_JWT_DEFAULT", "true")
+	t.Setenv("PREST_DEBUG", "false")
 	config.Load()
 	nd := appTestWithJwt()
 	serverd := httptest.NewServer(nd)
@@ -178,10 +176,10 @@ func TestJWTSignatureOk(t *testing.T) {
 	app = nil
 	MiddlewareStack = nil
 	bearer := "Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQHNvbWV3aGVyZS5jb20iLCJpYXQiOjE1MTc1NjM2MTYsImlzcyI6InByaXZhdGUiLCJqdGkiOiJjZWZhNzRmZS04OTRjLWZmNjMtZDgxNi00NjIwYjhjZDkyZWUiLCJvcmciOiJwcml2YXRlIiwic3ViIjoiam9obi5kb2UifQ.zLWkEd4hP4XdCD_DlRy6mgPeKwEl1dcdtx5A_jHSfmc87EsrGgNSdi8eBTzCgSU0jgV6ssTgQwzY6x4egze2xA"
-	os.Setenv("PREST_JWT_DEFAULT", "true")
-	os.Setenv("PREST_DEBUG", "false")
-	os.Setenv("PREST_JWT_KEY", "s3cr3t")
-	os.Setenv("PREST_JWT_ALGO", "HS512")
+	t.Setenv("PREST_JWT_DEFAULT", "true")
+	t.Setenv("PREST_DEBUG", "false")
+	t.Setenv("PREST_JWT_KEY", "s3cr3t")
+	t.Setenv("PREST_JWT_ALGO", "HS512")
 	config.Load()
 	nd := appTestWithJwt()
 	serverd := httptest.NewServer(nd)
@@ -201,10 +199,10 @@ func TestJWTSignatureOk(t *testing.T) {
 func TestJWTSignatureKo(t *testing.T) {
 	app = nil
 	bearer := "Bearer: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQHNvbWV3aGVyZS5jb20iLCJleHAiOjE1MjUzMzk2MTYsImlhdCI6MTUxNzU2MzYxNiwiaXNzIjoicHJpdmF0ZSIsImp0aSI6ImNlZmE3NGZlLTg5NGMtZmY2My1kODE2LTQ2MjBiOGNkOTJlZSIsIm9yZyI6InByaXZhdGUiLCJzdWIiOiJqb2huLmRvZSJ9.zGP1Xths2bK2r9FN0Gv1SzyoisO0dhRwvqrPvunGxUyU5TbkfdnTcQRJNYZzJfGILeQ9r3tbuakWm-NIoDlbbA"
-	os.Setenv("PREST_JWT_DEFAULT", "true")
-	os.Setenv("PREST_DEBUG", "false")
-	os.Setenv("PREST_JWT_KEY", "s3cr3t")
-	os.Setenv("PREST_JWT_ALGO", "HS256")
+	t.Setenv("PREST_JWT_DEFAULT", "true")
+	t.Setenv("PREST_DEBUG", "false")
+	t.Setenv("PREST_JWT_KEY", "s3cr3t")
+	t.Setenv("PREST_JWT_ALGO", "HS256")
 	config.Load()
 	nd := appTestWithJwt()
 	serverd := httptest.NewServer(nd)
@@ -251,9 +249,9 @@ func appTestWithJwt() *negroni.Negroni {
 
 func Test_CORS_Middleware(t *testing.T) {
 	MiddlewareStack = []negroni.Handler{}
-	os.Setenv("PREST_DEBUG", "true")
-	os.Setenv("PREST_CORS_ALLOWORIGIN", "*")
-	os.Setenv("PREST_CONF", "../testdata/prest.toml")
+	t.Setenv("PREST_DEBUG", "true")
+	t.Setenv("PREST_CORS_ALLOWORIGIN", "*")
+	t.Setenv("PREST_CONF", "../testdata/prest.toml")
 	config.Load()
 	app = nil
 	r := mux.NewRouter()
@@ -279,3 +277,28 @@ func Test_CORS_Middleware(t *testing.T) {
 	require.NoError(t, err)
 	require.Zero(t, len(body))
 }
+
+func TestExposeTablesMiddleware(t *testing.T) {
+	MiddlewareStack = []negroni.Handler{}
+	app = nil
+	t.Setenv("PREST_DEBUG", "true")
+	t.Setenv("PREST_CONF", "../testdata/prest_expose.toml")
+	config.Load()
+	app = nil
+	r := mux.NewRouter()
+	r.HandleFunc("/tables", controllers.GetTables).Methods("GET")
+	r.HandleFunc("/databases", controllers.GetDatabases).Methods("GET")
+	r.HandleFunc("/schemas", controllers.GetSchemas).Methods("GET")
+	n := GetApp()
+	n.UseHandler(r)
+	server := httptest.NewServer(n)
+	defer server.Close()
+	resp, _ := http.Get(server.URL + "/tables")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+
+	resp, _ = http.Get(server.URL + "/databases")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+
+	resp, _ = http.Get(server.URL + "/schemas")
+	require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
+}
diff --git a/plugins/plugins_test.go b/plugins/plugins_test.go
index 9a7b4fe..7895c94 100644
--- a/plugins/plugins_test.go
+++ b/plugins/plugins_test.go
@@ -32,8 +32,9 @@ func TestPlugins(t *testing.T) {
 		method      string
 		status      int
 	}{
-		// should be status 200 `http.StatusOK`, but ... read TODO
+		// TODO: should be status 200 `http.StatusOK`, but read
 		{"/_PLUGIN/hello/Hello request GET method", "/_PLUGIN/hello/Hello", "GET", http.StatusNotFound},
+		{"/_PLUGIN/hello/HelloWithStatus request GET method", "/_PLUGIN/hello/HelloWithStatus", "GET", http.StatusNotFound},
 		{"/_PLUGIN/hello/Hello request POST method", "/_PLUGIN/hello/Hello", "POST", http.StatusNotFound},
 	}
 
diff --git a/testdata/prest_expose.toml b/testdata/prest_expose.toml
new file mode 100644
index 0000000..21f0388
--- /dev/null
+++ b/testdata/prest_expose.toml
@@ -0,0 +1,11 @@
+[auth]
+table = "prest_users"
+username = "username"
+password = "password"
+metadata = ["first_name", "last_name", "last_login"]
+
+[expose]
+enabled = true
+tables = false
+databases = false
+schemas = false
\ No newline at end of file