8000 Feat autocomplete by zhangshanwen · Pull Request #447 · qor5/admin · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Feat autocomplete #447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions autocomplete/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package autocomplete

import (
"fmt"
"github.com/iancoleman/strcase"
"github.com/jinzhu/inflection"
vx "github.com/qor5/x/v3/ui/vuetifyx"
"go.uber.org/zap"
"gorm.io/gorm"
"net/http"
"reflect"
"slices"
"strings"
)

type (
Builder struct {
prefix string
db *gorm.DB
logger *zap.Logger
handler http.Handler
models []*ModelBuilder
}
)

func New() *Builder {
l, _ := zap.NewDevelopment()
return &Builder{
prefix: "",
logger: l,
}
}
func (b *Builder) NewModelBuilder(model interface{}) (mb *ModelBuilder) {
mb = &ModelBuilder{p: b, model: model}
mb.modelType = reflect.TypeOf(model)
if mb.modelType.Kind() != reflect.Ptr {
panic(fmt.Sprintf("model %#+v must be pointer", model))

Check warning on line 37 in autocomplete/builder.go

View check run for this annotation

Codecov / codecov/patch

autocomplete/builder.go#L37

Added line #L37 was not covered by tests
}
modelstr := mb.modelType.String()
modelName := modelstr[strings.LastIndex(modelstr, ".")+1:]
mb.uriName = inflection.Plural(strcase.ToKebab(modelName))
mb.columns = []string{"id"}
return
}

func (b *Builder) Model(v interface{}) (r *ModelBuilder) {
r = b.NewModelBuilder(v)
b.models = append(b.models, r)
return r
}
func (b *Builder) modelNames() (r []string) {
for _, m := range b.models {
r = append(r, m.uriName)
}
return
}
func (b *Builder) DB(v *gorm.DB) *Builder {
b.db = v
return b
}
func (b *Builder) Prefix(v string) *Builder {
b.prefix = v
return b
}
func (b *Builder) Logger(v *zap.Logger) *Builder {
b.logger = v
return b

Check warning on line 67 in autocomplete/builder.go

View check run for this annotation

Codecov / codecov/patch

autocomplete/builder.go#L65-L67

Added lines #L65 - L67 were not covered by tests
}
func (b *Builder) Build() {
mns := b.modelNames()
if len(slices.Compact(mns)) != len(mns) {
panic(fmt.Sprintf("Duplicated model names registered %v", mns))

Check warning on line 72 in autocomplete/builder.go

View check run for this annotation

Codecov / codecov/patch

autocomplete/builder.go#L72

Added line #L72 was not covered by tests
}
b.initMux()
}

func (b *Builder) initMux() {
b.logger.Info("initializing mux for", zap.Reflect("models", b.modelNames()), zap.String("prefix", b.prefix))
mux := http.NewServeMux()

for _, m := range b.models {
path := m.JsonHref()
mux.Handle(
path,
m,
)
b.logger.Info(fmt.Sprintf("mounted url: %s\n", path))
}
b.handler = mux
}
func (b *Builder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if b.handler == nil {
b.Build()

Check warning on line 93 in autocomplete/builder.go

View check run for this annotation

Codecov / codecov/patch

autocomplete/builder.go#L93

Added line #L93 was not covered by tests
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
b.handler.ServeHTTP(w, r)
}

func NewDefaultAutocompleteDataSource(v string) *vx.AutocompleteDataSource {
return &vx.AutocompleteDataSource{
RemoteURL: v,
IsPaging: true,
ItemTitle: "title",
ItemValue: "id",
PageKey: ParamPage,
PageSizeKey: ParamPageSize,
CurrentKey: ResponseCurrent,
PagesKey: ResponsePages,
TotalKey: ResponseTotal,
SearchKey: ParamSearch,
ItemsKey: ResponseItems,
Page: 1,
PageSize: 5,

Check warning on line 115 in autocomplete/builder.go

View check run for this annotation

Codecov / codecov/patch

autocomplete/builder.go#L101-L115

Added lines #L101 - L115 were not covered by tests
}
}
184 changes: 184 additions & 0 deletions autocomplete/integration/autocomplete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package integration_test

import (
"bytes"
"encoding/json"
"github.com/qor5/admin/v3/autocomplete"
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"

"github.com/theplant/gofixtures"
"github.com/theplant/testenv"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

var TestDB *gorm.DB

func TestMain(m *testing.M) {
env, err := testenv.New().DBEnable(true).SetUp()
if err != nil {
panic(err)
}
defer env.TearDown()
TestDB = env.DB
TestDB.Logger = TestDB.Logger.LogMode(logger.Info)
m.Run()
}

var autocompleteData = gofixtures.Data(gofixtures.Sql(`
INSERT INTO public.categories (id, created_at, updated_at, deleted_at, name, path) VALUES
(1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, '1', '/1'),
(2, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, '2', '/2'),
(3, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, '3', '/3'),
(4, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, '4', '/4'),
(5, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, '5', '/5');
INSERT INTO public.users (id, created_at, updated_at, deleted_at, name, age) VALUES
(1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'k1', 20),
(2, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'k2', 21),
(3, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'b3', 22),
(4, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'b4', 23),
(5, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'b5', 24);
`, []string{"categories", "users"}))

type (
Category struct {
gorm.Model
Name string `json:"name"`
Path string `json:"path"`
}
User struct {
gorm.Model
Name string `json:"name"`
Age int `json:"age"`
}
)

func Handler(db *gorm.DB) http.Handler {
_ = db.AutoMigrate(Category{}, User{})
mux := http.NewServeMux()
b := autocomplete.New().DB(db).Prefix("/complete")
b.Model(&Category{}).Columns("id", "name", "path")
b.Model(&User{}).Columns("id", "name", "age").SQLCondition("name ilike ?")
mux.Handle("/complete/", b)
b.Build()
return mux
}

func runTest(t *testing.T, r *http.Request, handler http.Handler) *bytes.Buffer {
w := httptest.NewRecorder()
bs, _ := httputil.DumpRequest(r, true)
t.Log("======== Request ========")
t.Log(string(bs))
handler.ServeHTTP(w, r)
t.Log("======== Response ========")
t.Log(w.Header())
t.Log(w.Body.String())
return w.Body

}

func TestCategory(t *testing.T) {
handler := Handler(TestDB)
dbr, _ := TestDB.DB()
t.Log("Test Categories")
autocompleteData.TruncatePut(dbr)
var (
err error
response autocomplete.Response
)
bytes := runTest(t, httptest.NewRequest("GET", "/complete/categories", nil), handler)
if err = json.Unmarshal(bytes.Bytes(), &response); err != nil {
t.Fatalf("json unmarshal faield :%v", err)
6D4E return
}
count := 5
if response.Total != int64(count) {
t.Fatalf("except get %v but get %v", count, response.Total)
return
}
if response.Data[0]["path"] == nil {
t.Fatalf("unexcpet value")
return
}
}

func TestUser(t *testing.T) {
handler := Handler(TestDB)
dbr, _ := TestDB.DB()
t.Log("Test Users")
autocompleteData.TruncatePut(dbr)
var (
err error
response autocomplete.Response
)

bytes := runTest(t, httptest.NewRequest("GET", "/complete/users", nil), handler)
if err = json.Unmarshal(bytes.Bytes(), &response); err != nil {
t.Fatalf("json unmarshal faield :%v", err)
return
}
count := 5
if response.Total != int64(count) {
t.Fatalf("except get %v but get %v", count, response.Total)
return
}
if response.Data[0]["age"] == nil {
t.Fatalf("unexcpet value")
return
}
}

func TestUserSearch(t *testing.T) {
handler := Handler(TestDB)
dbr, _ := TestDB.DB()
t.Log("Test Users Search")
autocompleteData.TruncatePut(dbr)
var (
err error
response autocomplete.Response
)

bytes := runTest(t, httptest.NewRequest("GET", "/complete/users?search=k", nil), handler)
if err = json.Unmarshal(bytes.Bytes(), &response); err != nil {
t.Fatalf("json unmarshal faield :%v", err)
return
}
count := 2
if response.Total != int64(count) {
t.Fatalf("except get %v but get %v", count, response.Total)
return
}
if response.Data[0]["age"] == nil {
t.Fatalf("unexcpet value")
return
}
}

func TestCategoryPage(t *testing.T) {
handler := Handler(TestDB)
dbr, _ := TestDB.DB()
t.Log("Test Category Page")
autocompleteData.TruncatePut(dbr)
var (
err error
response autocomplete.Response
)

bytes := runTest(t, httptest.NewRequest("GET", "/complete/categories?page=2&pageSize=3", nil), handler)
if err = json.Unmarshal(bytes.Bytes(), &response); err != nil {
t.Fatalf("json unmarshal faield :%v", err)
return
}
count := 2
if len(response.Data) != count {
t.Fatalf("except get %v but get %v", count, len(response.Data))
return
}
if response.Data[0]["path"] == nil {
t.Fatalf("unexcpet value")
return
}
}
Loading
Loading