This is an automated email from the ASF dual-hosted git repository.

chenjunxu pushed a commit to branch refactor
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/refactor by this push:
     new caccfb7  feat: refactor apis for existing check and other apis (#535)
caccfb7 is described below

commit caccfb75c57eb2e85466b42cf5b7a4368f1e96a8
Author: nic-chen <33000667+nic-c...@users.noreply.github.com>
AuthorDate: Fri Oct 9 16:21:49 2020 +0800

    feat: refactor apis for existing check and other apis (#535)
    
    * fix code style
    
    * fix code style
    
    * feat support query
    
    * feat: support query
    
    * change: `like` to `equal`
    
    * fix api status
    
    * feat: upstream existing check
    
    * feat: refactor api for upstream names
    
    * fix: license
    
    * test: add unit test cases
    
    * fix: update bug
    
    * test: add test cases for route
    
    * fix: unified respond format
    
    * test: remove test bug
    
    * feat: ssl existing check
    
    * fix bug: auto generate id
    
    * fix: improve consumer
    
    * fix: remove key and keys in ssl respond
    
    * fix: when list is empty, should respond an empty array
    
    * fix code style
    
    * feat: plugin orchestration
    
    * fix delete bug
    
    * fix bug
    
    * fix: keep the same request params and respond with the old format
---
 api/errno/error.go                                 |  22 +-
 api/internal/core/entity/entity.go                 |  32 ++-
 api/internal/core/entity/query.go                  | 153 +++++++++++
 api/internal/core/store/query.go                   | 151 +++++++++++
 api/internal/core/store/selector.go                | 144 +++++++++++
 api/internal/core/store/selector_test.go           | 285 +++++++++++++++++++++
 api/internal/core/store/store.go                   |  38 ++-
 api/internal/core/store/storehub.go                |  80 ++++++
 api/internal/handler/consumer/consumer.go          |  20 +-
 api/internal/handler/route/route.go                | 158 +++++++++++-
 api/internal/handler/service/service.go            |   4 +-
 api/internal/handler/ssl/ssl.go                    | 123 ++++++++-
 api/internal/handler/upstream/upstream.go          |  81 +++++-
 .../storehub.go => utils/consts/api_error.go}      |  57 ++---
 api/main.go                                        |  76 +-----
 api/route/base_test.go                             |  67 +----
 api/route/route_test.go                            | 195 ++++++++++++++
 17 files changed, 1468 insertions(+), 218 deletions(-)

diff --git a/api/errno/error.go b/api/errno/error.go
index effe063..0c18dec 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -23,8 +23,8 @@ import (
 
 type Message struct {
        Code   string
-       Msg    string
-       Status int `json:"-"`
+       Msg    string `json:"message"`
+       Status int    `json:"-"`
 }
 
 var (
@@ -128,25 +128,25 @@ func New(m Message, args ...interface{}) *ManagerError {
 
 func (e *ManagerError) Response() map[string]interface{} {
        return map[string]interface{}{
-               "code": e.Code,
-               "msg":  e.Msg,
+               "code":    e.Code,
+               "message": e.Msg,
        }
 }
 
 func (e *ManagerError) ItemResponse(data interface{}) map[string]interface{} {
        return map[string]interface{}{
-               "code": e.Code,
-               "msg":  e.Msg,
-               "data": data,
+               "code":    e.Code,
+               "message": e.Msg,
+               "data":    data,
        }
 }
 
 func (e *ManagerError) ListResponse(count, list interface{}) 
map[string]interface{} {
        return map[string]interface{}{
-               "code":  e.Code,
-               "msg":   e.Msg,
-               "count": count,
-               "list":  list,
+               "code":    e.Code,
+               "message": e.Msg,
+               "count":   count,
+               "list":    list,
        }
 }
 
diff --git a/api/internal/core/entity/entity.go 
b/api/internal/core/entity/entity.go
index 0a38c82..8a2fae4 100644
--- a/api/internal/core/entity/entity.go
+++ b/api/internal/core/entity/entity.go
@@ -44,7 +44,7 @@ type Route struct {
        RemoteAddrs     []string    `json:"remote_addrs,omitempty"`
        Vars            string      `json:"vars,omitempty"`
        FilterFunc      string      `json:"filter_func,omitempty"`
-       Script          string      `json:"script,omitempty"`
+       Script          interface{} `json:"script,omitempty"`
        Plugins         interface{} `json:"plugins,omitempty"`
        Upstream        Upstream    `json:"upstream,omitempty"`
        ServiceID       string      `json:"service_id,omitempty"`
@@ -113,6 +113,7 @@ type HealthChecker struct {
 }
 
 type Upstream struct {
+       BaseInfo
        Nodes           []Node        `json:"nodes,omitempty"`
        Retries         int           `json:"retries,omitempty"`
        Timeout         Timeout       `json:"timeout,omitempty"`
@@ -127,26 +128,38 @@ type Upstream struct {
        Name            string        `json:"name,omitempty"`
        Desc            string        `json:"desc,omitempty"`
        ServiceName     string        `json:"service_name,omitempty"`
-       ID              string        `json:"id,omitempty"`
+}
+
+type UpstreamNameResponse struct {
+       ID   string `json:"id"`
+       Name string `json:"name"`
+}
+
+func (upstream *Upstream) Parse2NameResponse() (*UpstreamNameResponse, error) {
+       nameResp := &UpstreamNameResponse{
+               ID:   upstream.ID,
+               Name: upstream.Name,
+       }
+       return nameResp, nil
 }
 
 // --- structures for upstream end  ---
 
 type Consumer struct {
-       ID       string      `json:"id"`
+       BaseInfo
        Username string      `json:"username"`
        Desc     string      `json:"desc,omitempty"`
        Plugins  interface{} `json:"plugins,omitempty"`
 }
 
 type SSL struct {
-       ID            string   `json:"id"`
+       BaseInfo
        Cert          string   `json:"cert"`
-       Key           string   `json:"key"`
+       Key           string   `json:"key,omitempty"`
        Sni           string   `json:"sni"`
        Snis          []string `json:"snis"`
        Certs         []string `json:"certs"`
-       Keys          []string `json:"keys"`
+       Keys          []string `json:"keys,omitempty"`
        ExpTime       int64    `json:"exptime"`
        Status        int      `json:"status"`
        ValidityStart int64    `json:"validity_start"`
@@ -154,7 +167,7 @@ type SSL struct {
 }
 
 type Service struct {
-       ID         string      `json:"id"`
+       BaseInfo
        Name       string      `json:"name,omitempty"`
        Desc       string      `json:"desc,omitempty"`
        Upstream   Upstream    `json:"upstream,omitempty"`
@@ -162,3 +175,8 @@ type Service struct {
        Plugins    interface{} `json:"plugins,omitempty"`
        Script     string      `json:"script,omitempty"`
 }
+
+type Script struct {
+       ID     string      `json:"id"`
+       Script interface{} `json:"script,omitempty"`
+}
diff --git a/api/internal/core/entity/query.go 
b/api/internal/core/entity/query.go
new file mode 100644
index 0000000..ea3e658
--- /dev/null
+++ b/api/internal/core/entity/query.go
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package entity
+
+import "strings"
+
+type PropertyName string
+
+const (
+       IdProperty         = "id"
+       NameProperty       = "name"
+       SniProperty        = "sni"
+       SnisProperty       = "snis"
+       CreateTimeProperty = "create_time"
+       UpdateTimeProperty = "update_time"
+)
+
+type ComparableValue interface {
+       Compare(ComparableValue) int
+       Contains(ComparableValue) bool
+}
+
+type ComparingString string
+
+func (comparing ComparingString) Compare(compared ComparableValue) int {
+       other := compared.(ComparingString)
+       return strings.Compare(string(comparing), string(other))
+}
+
+func (comparing ComparingString) Contains(compared ComparableValue) bool {
+       other := compared.(ComparingString)
+       return strings.Contains(string(comparing), string(other))
+}
+
+type ComparingStringArray []string
+
+func (comparing ComparingStringArray) Compare(compared ComparableValue) int {
+       other := compared.(ComparingString)
+       res := -1
+       for _, str := range comparing {
+               result := strings.Compare(str, string(other))
+               if result == 0 {
+                       res = 0
+                       break
+               }
+       }
+       return res
+}
+
+func (comparing ComparingStringArray) Contains(compared ComparableValue) bool {
+       other := compared.(ComparingString)
+       res := false
+       for _, str := range comparing {
+               if strings.Contains(str, string(other)) {
+                       res = true
+                       break
+               }
+       }
+       return res
+}
+
+type ComparingInt int64
+
+func int64Compare(a, b int64) int {
+       if a > b {
+               return 1
+       } else if a == b {
+               return 0
+       }
+       return -1
+}
+
+func (comparing ComparingInt) Compare(compared ComparableValue) int {
+       other := compared.(ComparingInt)
+       return int64Compare(int64(comparing), int64(other))
+}
+
+func (comparing ComparingInt) Contains(compared ComparableValue) bool {
+       return comparing.Compare(compared) == 0
+}
+
+func (info BaseInfo) GetProperty(name PropertyName) ComparableValue {
+       switch name {
+       case IdProperty:
+               return ComparingString(info.ID)
+       case CreateTimeProperty:
+               return ComparingInt(info.CreateTime)
+       case UpdateTimeProperty:
+               return ComparingInt(info.UpdateTime)
+       default:
+               return nil
+       }
+}
+
+func (route Route) GetProperty(name PropertyName) ComparableValue {
+       switch name {
+       case NameProperty:
+               return ComparingString(route.Name)
+       default:
+               return nil
+       }
+}
+
+func (upstream Upstream) GetProperty(name PropertyName) ComparableValue {
+       switch name {
+       case NameProperty:
+               return ComparingString(upstream.Name)
+       default:
+               return nil
+       }
+}
+
+func (ssl SSL) GetProperty(name PropertyName) ComparableValue {
+       switch name {
+       case SniProperty:
+               return ComparingString(ssl.Sni)
+       case SnisProperty:
+               return ComparingStringArray(ssl.Snis)
+       default:
+               return nil
+       }
+}
diff --git a/api/internal/core/store/query.go b/api/internal/core/store/query.go
new file mode 100644
index 0000000..c05a7cd
--- /dev/null
+++ b/api/internal/core/store/query.go
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package store
+
+import (
+       "github.com/apisix/manager-api/internal/core/entity"
+)
+
+type Query struct {
+       Sort       *Sort
+       Filter     *Filter
+       Pagination *Pagination
+}
+
+type Sort struct {
+       List []SortBy
+}
+
+type SortBy struct {
+       Property  entity.PropertyName
+       Ascending bool
+}
+
+var NoSort = &Sort{
+       List: []SortBy{},
+}
+
+type Filter struct {
+       List []FilterBy
+}
+
+type FilterBy struct {
+       Property entity.PropertyName
+       Value    entity.ComparableValue
+}
+
+var NoFilter = &Filter{
+       List: []FilterBy{},
+}
+
+type Pagination struct {
+       PageSize   int
+       PageNumber int
+}
+
+func NewPagination(PageSize, pageNumber int) *Pagination {
+       return &Pagination{PageSize, pageNumber}
+}
+
+func (p *Pagination) IsValid() bool {
+       return p.PageSize >= 0 && p.PageNumber >= 0
+}
+
+func (p *Pagination) IsAvailable(itemsCount, startingIndex int) bool {
+       return itemsCount > startingIndex && p.PageSize > 0
+}
+
+func (p *Pagination) Index(itemsCount int) (startIndex int, endIndex int) {
+       startIndex = p.PageSize * p.PageNumber
+       endIndex = startIndex + p.PageSize
+
+       if endIndex > itemsCount {
+               endIndex = itemsCount
+       }
+
+       return startIndex, endIndex
+}
+
+func NewQuery(sort *Sort, filter *Filter, pagination *Pagination) *Query {
+       return &Query{
+               Sort:       sort,
+               Filter:     filter,
+               Pagination: pagination,
+       }
+}
+
+func NewSort(sortRaw []string) *Sort {
+       if sortRaw == nil || len(sortRaw)%2 == 1 {
+               // Empty sort list or invalid (odd) length
+               return NoSort
+       }
+       list := []SortBy{}
+       for i := 0; i+1 < len(sortRaw); i += 2 {
+               var ascending bool
+               orderOption := sortRaw[i]
+               if orderOption == "a" {
+                       ascending = true
+               } else if orderOption == "d" {
+                       ascending = false
+               } else {
+                       return NoSort
+               }
+
+               propertyName := sortRaw[i+1]
+               sortBy := SortBy{
+                       Property:  entity.PropertyName(propertyName),
+                       Ascending: ascending,
+               }
+               list = append(list, sortBy)
+       }
+       return &Sort{
+               List: list,
+       }
+}
+
+func NewFilter(filterRaw []string) *Filter {
+       if filterRaw == nil || len(filterRaw)%2 == 1 {
+               return NoFilter
+       }
+       list := []FilterBy{}
+       for i := 0; i+1 < len(filterRaw); i += 2 {
+               propertyName := filterRaw[i]
+               propertyValue := filterRaw[i+1]
+               filterBy := FilterBy{
+                       Property: entity.PropertyName(propertyName),
+                       Value:    entity.ComparingString(propertyValue),
+               }
+               list = append(list, filterBy)
+       }
+       return &Filter{
+               List: list,
+       }
+}
diff --git a/api/internal/core/store/selector.go 
b/api/internal/core/store/selector.go
new file mode 100644
index 0000000..1fc8892
--- /dev/null
+++ b/api/internal/core/store/selector.go
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package store
+
+import (
+       "sort"
+
+       "github.com/apisix/manager-api/internal/core/entity"
+)
+
+type Row interface {
+       GetProperty(entity.PropertyName) entity.ComparableValue
+}
+
+type Selector struct {
+       List  []Row
+       Query *Query
+}
+
+func (self Selector) Len() int { return len(self.List) }
+
+func (self Selector) Swap(i, j int) {
+       self.List[i], self.List[j] = self.List[j], self.List[i]
+}
+
+func (self Selector) Less(i, j int) bool {
+       for _, sortBy := range self.Query.Sort.List {
+               a := self.List[i].GetProperty(sortBy.Property)
+               b := self.List[j].GetProperty(sortBy.Property)
+               if a == nil || b == nil {
+                       break
+               }
+               cmp := a.Compare(b)
+               if cmp == 0 {
+                       continue
+               } else {
+                       return (cmp == -1 && sortBy.Ascending) || (cmp == 1 && 
!sortBy.Ascending)
+               }
+       }
+       return false
+}
+
+func (self *Selector) Sort() *Selector {
+       sort.Sort(*self)
+       return self
+}
+
+func (self *Selector) Filter() *Selector {
+       filteredList := []Row{}
+       for _, c := range self.List {
+               matches := true
+               for _, filterBy := range self.Query.Filter.List {
+                       v := c.GetProperty(filterBy.Property)
+                       if v == nil || v.Compare(filterBy.Value) != 0 {
+                               matches = false
+                               break
+                       }
+               }
+               if matches {
+                       filteredList = append(filteredList, c)
+               }
+       }
+
+       self.List = filteredList
+       return self
+}
+
+func (self *Selector) Paginate() *Selector {
+       pagination := self.Query.Pagination
+       dataList := self.List
+       TotalSize := len(dataList)
+       startIndex, endIndex := pagination.Index(TotalSize)
+
+       if startIndex == 0 && endIndex == 0 {
+               return self
+       }
+
+       if !pagination.IsValid() {
+               self.List = []Row{}
+               return self
+       }
+
+       if startIndex > TotalSize {
+               self.List = []Row{}
+               return self
+       }
+
+       if endIndex >= TotalSize {
+               self.List = dataList[startIndex:]
+               return self
+       }
+
+       self.List = dataList[startIndex:endIndex]
+       return self
+}
+
+func NewFilterSelector(list []Row, query *Query) []Row {
+       selector := Selector{
+               List:  list,
+               Query: query,
+       }
+       filtered := selector.Filter()
+       paged := filtered.Paginate()
+       return paged.List
+}
+
+func DefaultSelector(list []Row, query *Query) ([]Row, int) {
+       selector := Selector{
+               List:  list,
+               Query: query,
+       }
+       filtered := selector.Filter()
+       filteredTotal := len(filtered.List)
+       paged := filtered.Sort().Paginate()
+       return paged.List, filteredTotal
+}
diff --git a/api/internal/core/store/selector_test.go 
b/api/internal/core/store/selector_test.go
new file mode 100644
index 0000000..cb99f42
--- /dev/null
+++ b/api/internal/core/store/selector_test.go
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Copyright 2017 The Kubernetes Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package store
+
+import (
+       "reflect"
+       "testing"
+
+       "github.com/apisix/manager-api/internal/core/entity"
+)
+
+type PaginationTestCase struct {
+       Info          string
+       Pagination    *Pagination
+       ExpectedOrder []int
+}
+
+type SortTestCase struct {
+       Info          string
+       Sort          *Sort
+       ExpectedOrder []int
+}
+
+type FilterTestCase struct {
+       Info          string
+       Filter        *Filter
+       ExpectedOrder []int
+}
+
+type TestRow struct {
+       Name       string
+       CreateTime int64
+       Id         int
+       Snis       []string
+}
+
+func (self TestRow) GetProperty(name entity.PropertyName) 
entity.ComparableValue {
+       switch name {
+       case entity.NameProperty:
+               return entity.ComparingString(self.Name)
+       case entity.SnisProperty:
+               return entity.ComparingStringArray(self.Snis)
+       case entity.CreateTimeProperty:
+               return entity.ComparingInt(self.CreateTime)
+       default:
+               return nil
+       }
+}
+
+func toRows(std []TestRow) []Row {
+       rows := make([]Row, len(std))
+       for i := range std {
+               rows[i] = std[i]
+       }
+       return rows
+}
+
+func fromRows(rows []Row) []TestRow {
+       std := make([]TestRow, len(rows))
+       for i := range std {
+               std[i] = rows[i].(TestRow)
+       }
+       return std
+}
+
+func getDataList() []Row {
+       return toRows([]TestRow{
+               {"b", 1, 1, []string{"a", "b"}},
+               {"a", 2, 2, []string{"c", "d"}},
+               {"a", 3, 3, []string{"f", "e"}},
+               {"c", 4, 4, []string{"g", "h"}},
+               {"c", 5, 5, []string{"k", "j"}},
+               {"d", 6, 6, []string{"i", "h"}},
+               {"e", 7, 7, []string{"t", "r"}},
+               {"e", 8, 8, []string{"q", "w"}},
+               {"f", 9, 9, []string{"x", "z"}},
+               {"a", 10, 10, []string{"v", "n"}},
+       })
+}
+
+func getOrder(dataList []TestRow) []int {
+       ordered := []int{}
+       for _, e := range dataList {
+               ordered = append(ordered, e.Id)
+       }
+       return ordered
+}
+
+func TestSort(t *testing.T) {
+       testCases := []SortTestCase{
+               {
+                       "no sort - do not change the original order",
+                       NoSort,
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "ascending sort by 1 property - all items sorted by 
this property",
+                       NewSort([]string{"a", "create_time"}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "descending sort by 1 property - all items sorted by 
this property",
+                       NewSort([]string{"d", "create_time"}),
+                       []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
+               },
+               {
+                       "sort by 2 properties - items should first be sorted by 
first property and later by second",
+                       NewSort([]string{"a", "name", "d", "create_time"}),
+                       []int{10, 3, 2, 1, 5, 4, 6, 8, 7, 9},
+               },
+               {
+                       "empty sort list - no sort",
+                       NewSort([]string{}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "nil - no sort",
+                       NewSort(nil),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               // Invalid arguments to the NewSortQuery
+               {
+                       "sort by few properties where at least one property 
name is invalid - no sort",
+                       NewSort([]string{"a", "INVALID_PROPERTY", "d", 
"creationTimestamp"}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "sort by few properties where at least one order option 
is invalid - no sort",
+                       NewSort([]string{"d", "name", "INVALID_ORDER", 
"creationTimestamp"}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "sort by few properties where one order tag is missing 
property - no sort",
+                       NewSort([]string{""}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "sort by few properties where one order tag is missing 
property - no sort",
+                       NewSort([]string{"d", "name", "a", "creationTimestamp", 
"a"}),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+       }
+       for _, testCase := range testCases {
+               selector := Selector{
+                       List:  getDataList(),
+                       Query: &Query{Sort: testCase.Sort},
+               }
+               sortedData := fromRows(selector.Sort().List)
+               order := getOrder(sortedData)
+               if !reflect.DeepEqual(order, testCase.ExpectedOrder) {
+                       t.Errorf(`Sort: %s. Received invalid items for %+v. Got 
%v, expected %v.`,
+                               testCase.Info, testCase.Sort, order, 
testCase.ExpectedOrder)
+               }
+       }
+
+}
+
+func TestPagination(t *testing.T) {
+       testCases := []PaginationTestCase{
+               {
+                       "no pagination - all existing elements should be 
returned",
+                       NewPagination(0, 0),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "request one item from existing page - element should 
be returned",
+                       NewPagination(1, 5),
+                       []int{6},
+               },
+               {
+                       "request one item from non existing page - no elements 
should be returned",
+                       NewPagination(1, 10),
+                       []int{},
+               },
+               {
+                       "request 2 items from existing page - 2 elements should 
be returned",
+                       NewPagination(2, 1),
+                       []int{3, 4},
+               },
+               {
+                       "request 3 items from partially existing page - last 
few existing should be returned",
+                       NewPagination(3, 3),
+                       []int{10},
+               },
+               {
+                       "request more than total number of elements from page 1 
- all existing elements should be returned",
+                       NewPagination(11, 0),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "request 3 items from non existing page - no elements 
should be returned",
+                       NewPagination(3, 4),
+                       []int{},
+               },
+               {
+                       "Invalid pagination - all elements should be returned",
+                       NewPagination(-1, 4),
+                       []int{},
+               },
+               {
+                       "Invalid pagination - all elements should be returned",
+                       NewPagination(1, -4),
+                       []int{},
+               },
+       }
+       for _, testCase := range testCases {
+               selector := Selector{
+                       List:  getDataList(),
+                       Query: &Query{Pagination: testCase.Pagination},
+               }
+               paginatedData := fromRows(selector.Paginate().List)
+               order := getOrder(paginatedData)
+               if !reflect.DeepEqual(order, testCase.ExpectedOrder) {
+                       t.Errorf(`Pagination: %s. Received invalid items for 
%+v. Got %v, expected %v.`,
+                               testCase.Info, testCase.Pagination, order, 
testCase.ExpectedOrder)
+               }
+       }
+
+}
+
+func TestFilter(t *testing.T) {
+       testCases := []FilterTestCase{
+               {
+                       "no sort - do not change the original order",
+                       NewFilter(nil),
+                       []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+               },
+               {
+                       "string filter",
+                       NewFilter([]string{"name", "a"}),
+                       []int{2, 3, 10},
+               },
+               {
+                       "string array filter",
+                       NewFilter([]string{"snis", "x"}),
+                       []int{9},
+               },
+               {
+                       "multi filter",
+                       NewFilter([]string{"snis", "t", "name", "e"}),
+                       []int{7},
+               },
+       }
+       for _, testCase := range testCases {
+               selector := Selector{
+                       List:  getDataList(),
+                       Query: &Query{Filter: testCase.Filter},
+               }
+               filteredData := fromRows(selector.Filter().List)
+               order := getOrder(filteredData)
+               if !reflect.DeepEqual(order, testCase.ExpectedOrder) {
+                       t.Errorf(`Filter: %s. Received invalid items for %+v. 
Got %v, expected %v.`,
+                               testCase.Info, testCase.Filter, order, 
testCase.ExpectedOrder)
+               }
+       }
+
+}
diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go
index 75c74a0..263a49f 100644
--- a/api/internal/core/store/store.go
+++ b/api/internal/core/store/store.go
@@ -14,19 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package store
 
 import (
        "context"
        "encoding/json"
        "fmt"
-       "github.com/apisix/manager-api/internal/core/entity"
-       "github.com/apisix/manager-api/internal/core/storage"
-       "github.com/apisix/manager-api/internal/utils"
-       "github.com/shiningrush/droplet/data"
        "log"
        "reflect"
        "time"
+
+       "github.com/shiningrush/droplet/data"
+
+       "github.com/apisix/manager-api/internal/core/entity"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/utils"
 )
 
 type Interface interface {
@@ -200,15 +203,6 @@ func (s *GenericStore) Create(ctx context.Context, obj 
interface{}) error {
                return err
        }
 
-       key := s.opt.KeyFunc(obj)
-       if key == "" {
-               return fmt.Errorf("key is required")
-       }
-       _, ok := s.cache[key]
-       if ok {
-               return fmt.Errorf("key: %s is conflicted", key)
-       }
-
        if getter, ok := obj.(entity.BaseInfoGetter); ok {
                info := getter.GetBaseInfo()
                if info.ID == "" {
@@ -218,6 +212,15 @@ func (s *GenericStore) Create(ctx context.Context, obj 
interface{}) error {
                info.UpdateTime = time.Now().Unix()
        }
 
+       key := s.opt.KeyFunc(obj)
+       if key == "" {
+               return fmt.Errorf("key is required")
+       }
+       _, ok := s.cache[key]
+       if ok {
+               return fmt.Errorf("key: %s is conflicted", key)
+       }
+
        bs, err := json.Marshal(obj)
        if err != nil {
                return fmt.Errorf("json marshal failed: %s", err)
@@ -238,13 +241,20 @@ func (s *GenericStore) Update(ctx context.Context, obj 
interface{}) error {
        if key == "" {
                return fmt.Errorf("key is required")
        }
-       _, ok := s.cache[key]
+       oldObj, ok := s.cache[key]
        if !ok {
                return fmt.Errorf("key: %s is not found", key)
        }
 
+       createTime := int64(0)
+       if oldGetter, ok := oldObj.(entity.BaseInfoGetter); ok {
+               oldInfo := oldGetter.GetBaseInfo()
+               createTime = oldInfo.CreateTime
+       }
+
        if getter, ok := obj.(entity.BaseInfoGetter); ok {
                info := getter.GetBaseInfo()
+               info.CreateTime = createTime
                info.UpdateTime = time.Now().Unix()
        }
 
diff --git a/api/internal/core/store/storehub.go 
b/api/internal/core/store/storehub.go
index 203c5bc..d07210a 100644
--- a/api/internal/core/store/storehub.go
+++ b/api/internal/core/store/storehub.go
@@ -18,6 +18,9 @@ package store
 
 import (
        "fmt"
+       "reflect"
+
+       "github.com/apisix/manager-api/internal/core/entity"
        "github.com/apisix/manager-api/internal/utils"
 )
 
@@ -29,6 +32,7 @@ const (
        HubKeyService  HubKey = "service"
        HubKeySsl      HubKey = "ssl"
        HubKeyUpstream HubKey = "upstream"
+       HubKeyScript   HubKey = "script"
 )
 
 var (
@@ -55,3 +59,79 @@ func GetStore(key HubKey) *GenericStore {
        }
        panic(fmt.Sprintf("no store with key: %s", key))
 }
+
+func InitStores() error {
+       err := InitStore(HubKeyConsumer, GenericStoreOption{
+               BasePath: "/apisix/consumers",
+               ObjType:  reflect.TypeOf(entity.Consumer{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.Consumer)
+                       return r.Username
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       err = InitStore(HubKeyRoute, GenericStoreOption{
+               BasePath: "/apisix/routes",
+               ObjType:  reflect.TypeOf(entity.Route{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.Route)
+                       return r.ID
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       err = InitStore(HubKeyService, GenericStoreOption{
+               BasePath: "/apisix/services",
+               ObjType:  reflect.TypeOf(entity.Service{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.Service)
+                       return r.ID
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       err = InitStore(HubKeySsl, GenericStoreOption{
+               BasePath: "/apisix/ssl",
+               ObjType:  reflect.TypeOf(entity.SSL{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.SSL)
+                       return r.ID
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       err = InitStore(HubKeyUpstream, GenericStoreOption{
+               BasePath: "/apisix/upstreams",
+               ObjType:  reflect.TypeOf(entity.Upstream{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.Upstream)
+                       return r.ID
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       err = InitStore(HubKeyScript, GenericStoreOption{
+               BasePath: "/apisix/scripts",
+               ObjType:  reflect.TypeOf(entity.Script{}),
+               KeyFunc: func(obj interface{}) string {
+                       r := obj.(*entity.Script)
+                       return r.ID
+               },
+       })
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
diff --git a/api/internal/handler/consumer/consumer.go 
b/api/internal/handler/consumer/consumer.go
index 71d4b03..d2533e7 100644
--- a/api/internal/handler/consumer/consumer.go
+++ b/api/internal/handler/consumer/consumer.go
@@ -17,6 +17,7 @@
 package consumer
 
 import (
+       "fmt"
        "reflect"
        "strings"
 
@@ -95,8 +96,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, 
error) {
 
 func (h *Handler) Create(c droplet.Context) (interface{}, error) {
        input := c.Input().(*entity.Consumer)
+       if input.ID != "" && input.ID != input.Username {
+               return nil, fmt.Errorf("consumer's id and username must be a 
same value")
+       }
+       input.ID = input.Username
 
-       //TODO: check duplicate username
        if err := h.consumerStore.Create(c.Context(), input); err != nil {
                return nil, err
        }
@@ -111,11 +115,21 @@ type UpdateInput struct {
 
 func (h *Handler) Update(c droplet.Context) (interface{}, error) {
        input := c.Input().(*UpdateInput)
+       if input.ID != "" && input.ID != input.Username {
+               return nil, fmt.Errorf("consumer's id and username must be a 
same value")
+       }
        input.Consumer.Username = input.Username
+       input.Consumer.ID = input.Username
 
-       //TODO: if not exists, create
        if err := h.consumerStore.Update(c.Context(), &input.Consumer); err != 
nil {
-               return nil, err
+               //if not exists, create
+               if err.Error() == fmt.Sprintf("key: %s is not found", 
input.Username) {
+                       if err := h.consumerStore.Create(c.Context(), 
&input.Consumer); err != nil {
+                               return nil, err
+                       }
+               } else {
+                       return nil, err
+               }
        }
 
        return nil, nil
diff --git a/api/internal/handler/route/route.go 
b/api/internal/handler/route/route.go
index c46ad12..45662b9 100644
--- a/api/internal/handler/route/route.go
+++ b/api/internal/handler/route/route.go
@@ -17,7 +17,10 @@
 package route
 
 import (
+       "encoding/json"
        "fmt"
+       "io/ioutil"
+       "os/exec"
        "reflect"
        "strings"
 
@@ -30,12 +33,15 @@ import (
        "github.com/apisix/manager-api/internal/core/entity"
        "github.com/apisix/manager-api/internal/core/store"
        "github.com/apisix/manager-api/internal/handler"
+       "github.com/apisix/manager-api/internal/utils"
+       "github.com/apisix/manager-api/internal/utils/consts"
 )
 
 type Handler struct {
        routeStore    store.Interface
        svcStore      store.Interface
        upstreamStore store.Interface
+       scriptStore   store.Interface
 }
 
 func NewHandler() (handler.RouteRegister, error) {
@@ -43,6 +49,7 @@ func NewHandler() (handler.RouteRegister, error) {
                routeStore:    store.GetStore(store.HubKeyRoute),
                svcStore:      store.GetStore(store.HubKeyService),
                upstreamStore: store.GetStore(store.HubKeyUpstream),
+               scriptStore:   store.GetStore(store.HubKeyScript),
        }, nil
 }
 
@@ -55,8 +62,10 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
                wrapper.InputType(reflect.TypeOf(entity.Route{}))))
        r.PUT("/apisix/admin/routes/:id", wgin.Wraps(h.Update,
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
-       r.DELETE("/apisix/admin/routes", wgin.Wraps(h.BatchDelete,
+       r.DELETE("/apisix/admin/routes/:ids", wgin.Wraps(h.BatchDelete,
                wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
+
+       r.GET("/apisix/admin/notexist/routes", consts.ErrorWrapper(Exist))
 }
 
 type GetInput struct {
@@ -70,7 +79,15 @@ func (h *Handler) Get(c droplet.Context) (interface{}, 
error) {
        if err != nil {
                return nil, err
        }
-       return r, nil
+
+       //format respond
+       route := r.(*entity.Route)
+       script, _ := h.scriptStore.Get(input.ID)
+       if script != nil {
+               route.Script = script.(*entity.Script).Script
+       }
+
+       return route, nil
 }
 
 type ListInput struct {
@@ -95,11 +112,45 @@ func (h *Handler) List(c droplet.Context) (interface{}, 
error) {
                return nil, err
        }
 
+       //format respond
+       var route *entity.Route
+       for i, item := range ret.Rows {
+               route = item.(*entity.Route)
+               script, _ := h.scriptStore.Get(route.ID)
+               if script != nil {
+                       route.Script = script.(*entity.Script).Script
+               }
+               ret.Rows[i] = route
+       }
+
        return ret, nil
 }
 
+func generateLuaCode(script map[string]interface{}) (string, error) {
+       scriptString, err := json.Marshal(script)
+       if err != nil {
+               return "", err
+       }
+
+       cmd := exec.Command("sh", "-c",
+               "cd /go/manager-api/dag-to-lua/ && lua cli.lua "+
+                       "'"+string(scriptString)+"'")
+
+       stdout, _ := cmd.StdoutPipe()
+       defer stdout.Close()
+       if err := cmd.Start(); err != nil {
+               return "", err
+       }
+
+       result, _ := ioutil.ReadAll(stdout)
+       resData := string(result)
+
+       return resData, nil
+}
+
 func (h *Handler) Create(c droplet.Context) (interface{}, error) {
        input := c.Input().(*entity.Route)
+       //check depend
        if input.ServiceID != "" {
                _, err := h.upstreamStore.Get(input.ServiceID)
                if err != nil {
@@ -119,6 +170,25 @@ func (h *Handler) Create(c droplet.Context) (interface{}, 
error) {
                }
        }
 
+       if input.Script != nil {
+               if input.ID == "" {
+                       input.ID = utils.GetFlakeUidStr()
+               }
+               script := &entity.Script{}
+               script.ID = input.ID
+               script.Script = input.Script
+               //to lua
+               var err error
+               input.Script, err = 
generateLuaCode(input.Script.(map[string]interface{}))
+               if err != nil {
+                       return nil, err
+               }
+               //save original conf
+               if err = h.scriptStore.Create(c.Context(), script); err != nil {
+                       return nil, err
+               }
+       }
+
        if err := h.routeStore.Create(c.Context(), input); err != nil {
                return nil, err
        }
@@ -135,6 +205,42 @@ func (h *Handler) Update(c droplet.Context) (interface{}, 
error) {
        input := c.Input().(*UpdateInput)
        input.Route.ID = input.ID
 
+       //check depend
+       if input.ServiceID != "" {
+               _, err := h.upstreamStore.Get(input.ServiceID)
+               if err != nil {
+                       if err == data.ErrNotFound {
+                               return nil, fmt.Errorf("service id: %s not 
found", input.ServiceID)
+                       }
+                       return nil, err
+               }
+       }
+       if input.UpstreamID != "" {
+               _, err := h.upstreamStore.Get(input.ServiceID)
+               if err != nil {
+                       if err == data.ErrNotFound {
+                               return nil, fmt.Errorf("upstream id: %s not 
found", input.UpstreamID)
+                       }
+                       return nil, err
+               }
+       }
+
+       if input.Script != nil {
+               script := entity.Script{}
+               script.ID = input.ID
+               script.Script = input.Script
+               //to lua
+               var err error
+               input.Route.Script, err = 
generateLuaCode(input.Script.(map[string]interface{}))
+               if err != nil {
+                       return nil, err
+               }
+               //save original conf
+               if err = h.scriptStore.Create(c.Context(), script); err != nil {
+                       return nil, err
+               }
+       }
+
        if err := h.routeStore.Update(c.Context(), &input.Route); err != nil {
                return nil, err
        }
@@ -143,7 +249,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, 
error) {
 }
 
 type BatchDelete struct {
-       IDs string `auto_read:"ids,query"`
+       IDs string `auto_read:"ids,path"`
 }
 
 func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
@@ -155,3 +261,49 @@ func (h *Handler) BatchDelete(c droplet.Context) 
(interface{}, error) {
 
        return nil, nil
 }
+
+type ExistInput struct {
+       Name string `auto_read:"name,query"`
+}
+
+func toRows(list *store.ListOutput) []store.Row {
+       rows := make([]store.Row, list.TotalSize)
+       for i := range list.Rows {
+               rows[i] = list.Rows[i].(*entity.Route)
+       }
+       return rows
+}
+
+func Exist(c *gin.Context) (interface{}, error) {
+       //input := c.Input().(*ExistInput)
+
+       //temporary
+       name := c.Query("name")
+       exclude := c.Query("exclude")
+       routeStore := store.GetStore(store.HubKeyRoute)
+
+       ret, err := routeStore.List(store.ListInput{
+               Predicate:  nil,
+               PageSize:   0,
+               PageNumber: 0,
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       sort := store.NewSort(nil)
+       filter := store.NewFilter([]string{"name", name})
+       pagination := store.NewPagination(0, 0)
+       query := store.NewQuery(sort, filter, pagination)
+       rows := store.NewFilterSelector(toRows(ret), query)
+
+       if len(rows) > 0 {
+               r := rows[0].(*entity.Route)
+               if r.ID != exclude {
+                       return nil, consts.InvalidParam("Route name is 
reduplicate")
+               }
+       }
+
+       return nil, nil
+}
diff --git a/api/internal/handler/service/service.go 
b/api/internal/handler/service/service.go
index ac4619a..47689ee 100644
--- a/api/internal/handler/service/service.go
+++ b/api/internal/handler/service/service.go
@@ -53,7 +53,7 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
        r.PATCH("/apisix/admin/services/:id", wgin.Wraps(h.Patch,
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
-       r.DELETE("/apisix/admin/services", wgin.Wraps(h.BatchDelete,
+       r.DELETE("/apisix/admin/services/:ids", wgin.Wraps(h.BatchDelete,
                wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
 }
 
@@ -120,7 +120,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, 
error) {
 }
 
 type BatchDelete struct {
-       IDs string `auto_read:"ids,query"`
+       IDs string `auto_read:"ids,path"`
 }
 
 func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
diff --git a/api/internal/handler/ssl/ssl.go b/api/internal/handler/ssl/ssl.go
index b7bd4bb..641ddee 100644
--- a/api/internal/handler/ssl/ssl.go
+++ b/api/internal/handler/ssl/ssl.go
@@ -19,9 +19,11 @@ package ssl
 import (
        "crypto/tls"
        "crypto/x509"
+       "encoding/json"
        "encoding/pem"
        "errors"
        "fmt"
+       "log"
        "reflect"
        "strings"
 
@@ -35,6 +37,7 @@ import (
        "github.com/apisix/manager-api/internal/core/entity"
        "github.com/apisix/manager-api/internal/core/store"
        "github.com/apisix/manager-api/internal/handler"
+       "github.com/apisix/manager-api/internal/utils/consts"
 )
 
 type Handler struct {
@@ -54,14 +57,16 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
                wrapper.InputType(reflect.TypeOf(ListInput{}))))
        r.POST("/apisix/admin/ssl", wgin.Wraps(h.Create,
                wrapper.InputType(reflect.TypeOf(entity.SSL{}))))
-       r.POST("/apisix/admin/check_ssl_cert", wgin.Wraps(h.Validate,
-               wrapper.InputType(reflect.TypeOf(entity.SSL{}))))
        r.PUT("/apisix/admin/ssl/:id", wgin.Wraps(h.Update,
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
        r.PATCH("/apisix/admin/ssl/:id", wgin.Wraps(h.Patch,
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
-       r.DELETE("/apisix/admin/ssl", wgin.Wraps(h.BatchDelete,
+       r.DELETE("/apisix/admin/ssl/:ids", wgin.Wraps(h.BatchDelete,
                wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
+       r.POST("/apisix/admin/check_ssl_cert", wgin.Wraps(h.Validate,
+               wrapper.InputType(reflect.TypeOf(entity.SSL{}))))
+
+       r.POST("/apisix/admin/check_ssl_exists", consts.ErrorWrapper(Exist))
 }
 
 type GetInput struct {
@@ -71,11 +76,17 @@ type GetInput struct {
 func (h *Handler) Get(c droplet.Context) (interface{}, error) {
        input := c.Input().(*GetInput)
 
-       r, err := h.sslStore.Get(input.ID)
+       ret, err := h.sslStore.Get(input.ID)
        if err != nil {
                return nil, err
        }
-       return r, nil
+
+       //format respond
+       ssl := ret.(*entity.SSL)
+       ssl.Key = ""
+       ssl.Keys = nil
+
+       return ssl, nil
 }
 
 type ListInput struct {
@@ -100,19 +111,31 @@ func (h *Handler) List(c droplet.Context) (interface{}, 
error) {
                return nil, err
        }
 
+       //format respond
+       var list []interface{}
+       var ssl *entity.SSL
+       for _, item := range ret.Rows {
+               ssl = item.(*entity.SSL)
+               ssl.Key = ""
+               ssl.Keys = nil
+               list = append(list, ssl)
+       }
+       if list == nil {
+               list = []interface{}{}
+       }
+       ret.Rows = list
+
        return ret, nil
 }
 
 func (h *Handler) Create(c droplet.Context) (interface{}, error) {
        input := c.Input().(*entity.SSL)
-
        ssl, err := ParseCert(input.Cert, input.Key)
        if err != nil {
                return nil, err
        }
 
        ssl.ID = input.ID
-
        if err := h.sslStore.Create(c.Context(), ssl); err != nil {
                return nil, err
        }
@@ -127,9 +150,14 @@ type UpdateInput struct {
 
 func (h *Handler) Update(c droplet.Context) (interface{}, error) {
        input := c.Input().(*UpdateInput)
-       input.SSL.ID = input.ID
+       ssl, err := ParseCert(input.Cert, input.Key)
+       if err != nil {
+               return nil, err
+       }
 
-       if err := h.sslStore.Update(c.Context(), &input.SSL); err != nil {
+       ssl.ID = input.ID
+       log.Println("ssl", ssl)
+       if err := h.sslStore.Update(c.Context(), ssl); err != nil {
                return nil, err
        }
 
@@ -177,7 +205,7 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, 
error) {
 }
 
 type BatchDelete struct {
-       Ids string `auto_read:"ids,query"`
+       Ids string `auto_read:"ids,path"`
 }
 
 func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
@@ -262,3 +290,78 @@ func (h *Handler) Validate(c droplet.Context) 
(interface{}, error) {
 
        return ssl, nil
 }
+
+type ExistInput struct {
+       Name string `auto_read:"name,query"`
+}
+
+func toRows(list *store.ListOutput) []store.Row {
+       rows := make([]store.Row, list.TotalSize)
+       for i := range list.Rows {
+               rows[i] = list.Rows[i].(*entity.SSL)
+       }
+       return rows
+}
+
+func checkValueExists(rows []store.Row, field, value string) bool {
+       selector := store.Selector{
+               List:  rows,
+               Query: &store.Query{Filter: store.NewFilter([]string{field, 
value})},
+       }
+
+       list := selector.Filter().List
+
+       return len(list) > 0
+}
+
+func checkSniExists(rows []store.Row, sni string) bool {
+       if res := checkValueExists(rows, "sni", sni); res {
+               return true
+       }
+       if res := checkValueExists(rows, "snis", sni); res {
+               return true
+       }
+       //extensive domain
+       firstDot := strings.Index(sni, ".")
+       if firstDot > 0 && sni[0:1] != "*" {
+               sni = "*" + sni[firstDot:]
+               if res := checkValueExists(rows, "sni", sni); res {
+                       return true
+               }
+               if res := checkValueExists(rows, "snis", sni); res {
+                       return true
+               }
+       }
+
+       return false
+}
+
+func Exist(c *gin.Context) (interface{}, error) {
+       //input := c.Input().(*ExistInput)
+       //temporary
+       reqBody, _ := c.GetRawData()
+       var hosts []string
+       if err := json.Unmarshal(reqBody, &hosts); err != nil {
+               return nil, err
+       }
+
+       routeStore := store.GetStore(store.HubKeySsl)
+       ret, err := routeStore.List(store.ListInput{
+               Predicate:  nil,
+               PageSize:   0,
+               PageNumber: 0,
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       for _, host := range hosts {
+               res := checkSniExists(toRows(ret), host)
+               if !res {
+                       return nil, consts.InvalidParam("SSL cert not exists 
for sni:" + host)
+               }
+       }
+
+       return nil, nil
+}
diff --git a/api/internal/handler/upstream/upstream.go 
b/api/internal/handler/upstream/upstream.go
index 9efb944..3b24ade 100644
--- a/api/internal/handler/upstream/upstream.go
+++ b/api/internal/handler/upstream/upstream.go
@@ -30,6 +30,7 @@ import (
        "github.com/apisix/manager-api/internal/core/entity"
        "github.com/apisix/manager-api/internal/core/store"
        "github.com/apisix/manager-api/internal/handler"
+       "github.com/apisix/manager-api/internal/utils/consts"
 )
 
 type Handler struct {
@@ -53,8 +54,11 @@ func (h *Handler) ApplyRoute(r *gin.Engine) {
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
        r.PATCH("/apisix/admin/upstreams/:id", wgin.Wraps(h.Patch,
                wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
-       r.DELETE("/apisix/admin/upstreams", wgin.Wraps(h.BatchDelete,
+       r.DELETE("/apisix/admin/upstreams/:ids", wgin.Wraps(h.BatchDelete,
                wrapper.InputType(reflect.TypeOf(BatchDelete{}))))
+
+       r.GET("/apisix/admin/notexist/upstreams", consts.ErrorWrapper(Exist))
+       r.GET("/apisix/admin/names/upstreams", 
consts.ErrorWrapper(listUpstreamNames))
 }
 
 type GetInput struct {
@@ -123,7 +127,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, 
error) {
 }
 
 type BatchDelete struct {
-       IDs string `auto_read:"ids,query"`
+       IDs string `auto_read:"ids,path"`
 }
 
 func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
@@ -175,3 +179,76 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, 
error) {
 
        return nil, nil
 }
+
+type ExistInput struct {
+       Name string `auto_read:"name,query"`
+}
+
+func toRows(list *store.ListOutput) []store.Row {
+       rows := make([]store.Row, list.TotalSize)
+       for i := range list.Rows {
+               rows[i] = list.Rows[i].(*entity.Upstream)
+       }
+       return rows
+}
+
+func Exist(c *gin.Context) (interface{}, error) {
+       //input := c.Input().(*ExistInput)
+
+       //temporary
+       name := c.Query("name")
+       exclude := c.Query("exclude")
+       routeStore := store.GetStore(store.HubKeyUpstream)
+
+       ret, err := routeStore.List(store.ListInput{
+               Predicate:  nil,
+               PageSize:   0,
+               PageNumber: 0,
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       sort := store.NewSort(nil)
+       filter := store.NewFilter([]string{"name", name})
+       pagination := store.NewPagination(0, 0)
+       query := store.NewQuery(sort, filter, pagination)
+       rows := store.NewFilterSelector(toRows(ret), query)
+
+       if len(rows) > 0 {
+               r := rows[0].(*entity.Upstream)
+               if r.ID != exclude {
+                       return nil, consts.InvalidParam("Upstream name is 
reduplicate")
+               }
+       }
+
+       return nil, nil
+}
+
+func listUpstreamNames(c *gin.Context) (interface{}, error) {
+       routeStore := store.GetStore(store.HubKeyUpstream)
+
+       ret, err := routeStore.List(store.ListInput{
+               Predicate:  nil,
+               PageSize:   0,
+               PageNumber: 0,
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       rows := make([]interface{}, ret.TotalSize)
+       for i := range ret.Rows {
+               row := ret.Rows[i].(*entity.Upstream)
+               rows[i], _ = row.Parse2NameResponse()
+       }
+
+       output := &store.ListOutput{
+               Rows:      rows,
+               TotalSize: ret.TotalSize,
+       }
+
+       return output, nil
+}
diff --git a/api/internal/core/store/storehub.go 
b/api/internal/utils/consts/api_error.go
similarity index 52%
copy from api/internal/core/store/storehub.go
copy to api/internal/utils/consts/api_error.go
index 203c5bc..cd9bc32 100644
--- a/api/internal/core/store/storehub.go
+++ b/api/internal/utils/consts/api_error.go
@@ -14,44 +14,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package store
+package consts
 
 import (
-       "fmt"
-       "github.com/apisix/manager-api/internal/utils"
+       "github.com/gin-gonic/gin"
+       "net/http"
 )
 
-type HubKey string
+type WrapperHandle func(c *gin.Context) (interface{}, error)
 
-const (
-       HubKeyConsumer HubKey = "consumer"
-       HubKeyRoute    HubKey = "route"
-       HubKeyService  HubKey = "service"
-       HubKeySsl      HubKey = "ssl"
-       HubKeyUpstream HubKey = "upstream"
-)
+func ErrorWrapper(handle WrapperHandle) gin.HandlerFunc {
+       return func(c *gin.Context) {
+               data, err := handle(c)
+               if err != nil {
+                       apiError := err.(*ApiError)
+                       c.JSON(apiError.Status, apiError)
+                       return
+               }
+               c.JSON(http.StatusOK, gin.H{"data": data, "code": 200, 
"message": "success"})
+       }
+}
 
-var (
-       storeHub = map[HubKey]*GenericStore{}
-)
+type ApiError struct {
+       Status  int    `json:"-"`
+       Code    int    `json:"code"`
+       Message string `json:"message"`
+}
 
-func InitStore(key HubKey, opt GenericStoreOption) error {
-       s, err := NewGenericStore(opt)
-       if err != nil {
-               return err
-       }
-       if err := s.Init(); err != nil {
-               return err
-       }
+func (err ApiError) Error() string {
+       return err.Message
+}
 
-       utils.AppendToClosers(s.Close)
-       storeHub[key] = s
-       return nil
+func InvalidParam(message string) *ApiError {
+       return &ApiError{400, 400, message}
 }
 
-func GetStore(key HubKey) *GenericStore {
-       if s, ok := storeHub[key]; ok {
-               return s
-       }
-       panic(fmt.Sprintf("no store with key: %s", key))
+func SystemError(message string) *ApiError {
+       return &ApiError{500, 500, message}
 }
diff --git a/api/main.go b/api/main.go
index 36541b7..937243e 100644
--- a/api/main.go
+++ b/api/main.go
@@ -18,20 +18,19 @@ package main
 
 import (
        "fmt"
-       "github.com/apisix/manager-api/internal/core/entity"
-       "github.com/apisix/manager-api/internal/core/storage"
-       "github.com/apisix/manager-api/internal/core/store"
-       "github.com/apisix/manager-api/internal/utils"
        "github.com/spf13/viper"
        "net/http"
-       "reflect"
        "strings"
        "time"
 
+       dlog "github.com/shiningrush/droplet/log"
+
        "github.com/apisix/manager-api/conf"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/utils"
        "github.com/apisix/manager-api/log"
        "github.com/apisix/manager-api/route"
-       dlog "github.com/shiningrush/droplet/log"
 )
 
 var logger = log.GetLogger()
@@ -44,7 +43,7 @@ func main() {
        if err := 
storage.InitETCDClient(strings.Split(viper.GetString("etcd_endpoints"), ",")); 
err != nil {
                panic(err)
        }
-       if err := initStores(); err != nil {
+       if err := store.InitStores(); err != nil {
                panic(err)
        }
 
@@ -65,66 +64,3 @@ func main() {
 
        utils.CloseAll()
 }
-
-func initStores() error {
-       err := store.InitStore(store.HubKeyConsumer, store.GenericStoreOption{
-               BasePath: "/apisix/consumers",
-               ObjType:  reflect.TypeOf(entity.Consumer{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Consumer)
-                       return r.Username
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyRoute, store.GenericStoreOption{
-               BasePath: "/apisix/routes",
-               ObjType:  reflect.TypeOf(entity.Route{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Route)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyService, store.GenericStoreOption{
-               BasePath: "/apisix/services",
-               ObjType:  reflect.TypeOf(entity.Service{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Service)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeySsl, store.GenericStoreOption{
-               BasePath: "/apisix/ssl",
-               ObjType:  reflect.TypeOf(entity.SSL{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.SSL)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyUpstream, store.GenericStoreOption{
-               BasePath: "/apisix/upstreams",
-               ObjType:  reflect.TypeOf(entity.Upstream{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Upstream)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-       return nil
-}
diff --git a/api/route/base_test.go b/api/route/base_test.go
index fdfd6f9..838f4b7 100644
--- a/api/route/base_test.go
+++ b/api/route/base_test.go
@@ -17,14 +17,12 @@
 package route
 
 import (
-       "reflect"
        "strings"
 
        "github.com/api7/apitest"
        dlog "github.com/shiningrush/droplet/log"
        "github.com/spf13/viper"
 
-       "github.com/apisix/manager-api/internal/core/entity"
        "github.com/apisix/manager-api/internal/core/storage"
        "github.com/apisix/manager-api/internal/core/store"
        "github.com/apisix/manager-api/log"
@@ -46,7 +44,7 @@ func init() {
                panic(err)
        }
 
-       if err := initStores(); err != nil {
+       if err := store.InitStores(); err != nil {
                panic(err)
        }
 
@@ -56,66 +54,3 @@ func init() {
                New().
                Handler(r)
 }
-
-func initStores() error {
-       err := store.InitStore(store.HubKeyConsumer, store.GenericStoreOption{
-               BasePath: "/apisix/consumers",
-               ObjType:  reflect.TypeOf(entity.Consumer{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Consumer)
-                       return r.Username
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyRoute, store.GenericStoreOption{
-               BasePath: "/apisix/routes",
-               ObjType:  reflect.TypeOf(entity.Route{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Route)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyService, store.GenericStoreOption{
-               BasePath: "/apisix/services",
-               ObjType:  reflect.TypeOf(entity.Service{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Service)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeySsl, store.GenericStoreOption{
-               BasePath: "/apisix/ssl",
-               ObjType:  reflect.TypeOf(entity.SSL{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.SSL)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-
-       err = store.InitStore(store.HubKeyUpstream, store.GenericStoreOption{
-               BasePath: "/apisix/upstreams",
-               ObjType:  reflect.TypeOf(entity.Upstream{}),
-               KeyFunc: func(obj interface{}) string {
-                       r := obj.(*entity.Upstream)
-                       return r.ID
-               },
-       })
-       if err != nil {
-               return err
-       }
-       return nil
-}
diff --git a/api/route/route_test.go b/api/route/route_test.go
new file mode 100644
index 0000000..5b66679
--- /dev/null
+++ b/api/route/route_test.go
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+       "net/http"
+       "testing"
+)
+
+func TestRoute(t *testing.T) {
+       // create ok
+
+       testHandler.
+               Post(uriPrefix + "/routes").
+               JSON(`{
+      "id": "11",
+                       "name": "e2e-test-route1",
+                       "desc": "route created by java sdk",
+                       "priority": 0,
+                       "methods": [
+                               "GET"
+                       ],
+                       "uris": [
+                               "/helloworld",
+                               "/hello2*"
+                       ],
+                       "hosts": [
+                               "s.com"
+                       ],
+                       "protocols": [
+                               "http",
+                               "https",
+                               "websocket"
+                       ],
+                       "redirect":{
+                               "code": 302,
+                               "uri": "/hello"
+                       },
+                       "vars": [
+                               ["arg_name", "==", "json"],
+                               ["arg_age", ">", "18"],
+                               ["arg_address", "~~", "China.*"]
+                       ],
+                       "upstream": {
+                               "type": "roundrobin",
+                               "nodes": {
+                                       "39.97.63.215:80": 100
+                               },
+                               "timeout": {
+                                       "connect":15,
+                                       "send":15,
+                                       "read":15
+                               }
+                       },
+                       "upstream_protocol": "keep",
+                       "upstream_path": {
+                               "type" : "static",
+                               "from": "",
+                               "to": "/hello"
+                       },
+                       "upstream_header": {
+                               "header_name1": "header_value1",
+                               "header_name2": "header_value2"
+                       },
+                       "plugins": {
+                               "limit-count": {
+                                       "count": 2,
+                                       "time_window": 60,
+                                       "rejected_code": 503,
+                                       "key": "remote_addr"
+                               },
+                               "prometheus": {}
+                       }
+               }`).
+               Headers(map[string]string{"Authorization": token}).
+               Expect(t).
+               Status(http.StatusOK).
+               End()
+
+       //update ok
+       testHandler.
+               Put(uriPrefix + "/routes/11").
+               JSON(`{
+      "id": "11",
+                       "name": "e2e-test-route1",
+                       "desc": "route created by java sdk",
+                       "priority": 0,
+                       "methods": [
+                               "GET"
+                       ],
+                       "uris": [
+                               "/helloworld",
+                               "/hello2*"
+                       ],
+                       "hosts": [
+                               "s.com"
+                       ],
+                       "protocols": [
+                               "http",
+                               "https",
+                               "websocket"
+                       ],
+                       "redirect":{
+                               "code": 302,
+                               "uri": "/hello"
+                       },
+                       "vars": [
+                               ["arg_name", "==", "json"],
+                               ["arg_age", ">", "18"],
+                               ["arg_address", "~~", "China.*"]
+                       ],
+                       "upstream": {
+                               "type": "roundrobin",
+                               "nodes": {
+                                       "39.97.63.215:80": 100
+                               },
+                               "timeout": {
+                                       "connect":15,
+                                       "send":15,
+                                       "read":15
+                               }
+                       },
+                       "upstream_protocol": "keep",
+                       "upstream_path": {
+                               "type" : "static",
+                               "from": "",
+                               "to": "/hello"
+                       },
+                       "upstream_header": {
+                               "header_name1": "header_value1",
+                               "header_name2": "header_value2"
+                       },
+                       "plugins": {
+                               "limit-count": {
+                                       "count": 2,
+                                       "time_window": 60,
+                                       "rejected_code": 503,
+                                       "key": "remote_addr"
+                               },
+                               "prometheus": {}
+                       }
+               }`).
+               Expect(t).
+               Status(http.StatusOK).
+               End()
+
+       //list
+       testHandler.
+               Get(uriPrefix + "/routes").
+               Headers(map[string]string{"Authorization": token}).
+               Expect(t).
+               Status(http.StatusOK).
+               End()
+
+       //not exist
+       testHandler.
+               Get(uriPrefix + "/notexist/routes").
+               //Query("name", "notexists").
+               QueryCollection(map[string][]string{"name": {"notexists"}}).
+               Headers(map[string]string{"Authorization": token}).
+               Expect(t).
+               Status(http.StatusOK).
+               End()
+
+       //existed todo: fix bug
+       //testHandler.
+       //      Get(uriPrefix + "/notexist/routes").
+       //      QueryCollection(map[string][]string{"name": {""}}).
+       //      Headers(map[string]string{"Authorization": token}).
+       //      Expect(t).
+       //      Status(http.StatusBadRequest).
+       //      End()
+
+       //delete
+       testHandler.
+               Delete(uriPrefix + "/routes/11").
+               Expect(t).
+               Status(http.StatusOK).
+               End()
+
+}

Reply via email to