mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-11 19:04:35 +00:00
DB service (#132)
This commit is contained in:
108
db/handler/db.go
Normal file
108
db/handler/db.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
db "github.com/micro/services/db/proto"
|
||||
gorm2 "github.com/micro/services/pkg/gorm"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string
|
||||
Data datatypes.JSON `json:"data"`
|
||||
}
|
||||
|
||||
type Db struct {
|
||||
gorm2.Helper
|
||||
}
|
||||
|
||||
// Call is a single request handler called via client.Call or the generated client code
|
||||
func (e *Db) Create(ctx context.Context, req *db.CreateRequest, rsp *db.CreateResponse) error {
|
||||
db, err := e.GetDBConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
err = json.Unmarshal([]byte(req.Record), &m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := m["ID"].(string); !ok {
|
||||
m["ID"] = uuid.New().String()
|
||||
}
|
||||
bs, _ := json.Marshal(m)
|
||||
return db.Table(req.Table).Create(Record{
|
||||
ID: m["ID"].(string),
|
||||
Data: bs,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (e *Db) Update(ctx context.Context, req *db.UpdateRequest, rsp *db.UpdateResponse) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Db) Read(ctx context.Context, req *db.ReadRequest, rsp *db.ReadResponse) error {
|
||||
recs := []Record{}
|
||||
queries, err := Parse(req.Query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := e.GetDBConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db = db.Table(req.Table)
|
||||
for _, query := range queries {
|
||||
typ := "text"
|
||||
switch query.Value.(type) {
|
||||
case int64:
|
||||
typ = "int"
|
||||
case bool:
|
||||
typ = "boolean"
|
||||
}
|
||||
op := ""
|
||||
switch query.Op {
|
||||
case itemEquals:
|
||||
op = "="
|
||||
case itemGreaterThan:
|
||||
op = ">"
|
||||
case itemGreaterThanEquals:
|
||||
op = ">="
|
||||
case itemLessThan:
|
||||
op = "<"
|
||||
case itemLessThanEquals:
|
||||
op = "<="
|
||||
case itemNotEquals:
|
||||
op = "!="
|
||||
}
|
||||
db = db.Where(fmt.Sprintf("(data ->> '%v')::%v %v ?", query.Field, typ, op), query.Value)
|
||||
}
|
||||
err = db.Find(&recs).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret := []map[string]interface{}{}
|
||||
for _, rec := range recs {
|
||||
m, err := rec.Data.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ma := map[string]interface{}{}
|
||||
json.Unmarshal(m, &ma)
|
||||
ma["ID"] = rec.ID
|
||||
ret = append(ret, ma)
|
||||
}
|
||||
bs, _ := json.Marshal(ret)
|
||||
rsp.Records = string(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Db) Delete(ctx context.Context, req *db.DeleteRequest, rsp *db.DeleteResponse) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
134
db/handler/parse.go
Normal file
134
db/handler/parse.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/crufter/lexer"
|
||||
)
|
||||
|
||||
var quoteEscape = fmt.Sprint(0x10FFFF)
|
||||
|
||||
const (
|
||||
itemIgnore = iota
|
||||
|
||||
// nouns
|
||||
itemAnd
|
||||
itemInt
|
||||
itemFieldName
|
||||
itemString
|
||||
itemBoolTrue
|
||||
itemBoolFalse
|
||||
|
||||
// ops
|
||||
itemEquals
|
||||
itemNotEquals
|
||||
itemLessThan
|
||||
itemGreaterThan
|
||||
itemLessThanEquals
|
||||
itemGreaterThanEquals
|
||||
)
|
||||
|
||||
var opToString = map[int]string{
|
||||
itemEquals: "==",
|
||||
itemNotEquals: "!=",
|
||||
itemLessThan: "<",
|
||||
itemGreaterThan: ">",
|
||||
itemLessThanEquals: "<=",
|
||||
itemGreaterThanEquals: ">=",
|
||||
}
|
||||
|
||||
var expressions = []lexer.TokenExpr{
|
||||
{`[ ]+`, itemIgnore}, // Whitespace
|
||||
{`==`, itemEquals},
|
||||
{`!=`, itemNotEquals},
|
||||
{`false`, itemBoolFalse},
|
||||
{`true`, itemBoolTrue},
|
||||
{`and`, itemAnd},
|
||||
{`<=`, itemLessThanEquals},
|
||||
{`>=`, itemGreaterThanEquals},
|
||||
{`<`, itemLessThan},
|
||||
{`>`, itemGreaterThan},
|
||||
{`[0-9]+`, itemInt},
|
||||
{`"(?:[^"\\]|\\.)*"`, itemString},
|
||||
{`[\<\>\!\=\+\-\|\&\*\/A-Za-z][A-Za-z0-9_]*`, itemFieldName},
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Field string
|
||||
Op int
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func Parse(q string) ([]Query, error) {
|
||||
if strings.Contains(q, quoteEscape) {
|
||||
return nil, errors.New("query contains illegal max rune")
|
||||
}
|
||||
q = strings.Replace(q, `""`, quoteEscape, -1)
|
||||
tokens, err := lexer.Lex(q, expressions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries := []Query{}
|
||||
current := Query{}
|
||||
for i, token := range tokens {
|
||||
// and tokens should trigger a query
|
||||
// save and reset
|
||||
if token.Typ == itemAnd {
|
||||
queries = append(queries, current)
|
||||
current = Query{}
|
||||
continue
|
||||
}
|
||||
|
||||
// is an op
|
||||
if token.Typ >= itemEquals {
|
||||
current.Op = token.Typ
|
||||
continue
|
||||
}
|
||||
|
||||
// is a value
|
||||
switch token.Typ {
|
||||
case itemFieldName:
|
||||
current.Field = token.Text
|
||||
case itemString:
|
||||
switch current.Op {
|
||||
case itemEquals, itemNotEquals:
|
||||
default:
|
||||
return nil, fmt.Errorf("operator '%v' can't be used with strings", opToString[token.Typ])
|
||||
}
|
||||
|
||||
if len(token.Text) < 2 {
|
||||
return nil, fmt.Errorf("string literal too short: '%v'", token.Text)
|
||||
}
|
||||
current.Value = strings.Replace(token.Text[1:len(token.Text)-1], quoteEscape, `"`, -1)
|
||||
case itemBoolTrue:
|
||||
switch current.Op {
|
||||
case itemEquals, itemNotEquals:
|
||||
default:
|
||||
return nil, fmt.Errorf("operator '%v' can't be used with bools", opToString[token.Typ])
|
||||
}
|
||||
current.Value = true
|
||||
case itemBoolFalse:
|
||||
switch current.Op {
|
||||
case itemEquals, itemNotEquals:
|
||||
default:
|
||||
return nil, fmt.Errorf("operator '%v' can't be used with bools", opToString[token.Typ])
|
||||
}
|
||||
current.Value = false
|
||||
case itemInt:
|
||||
num, err := strconv.ParseInt(token.Text, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current.Value = num
|
||||
}
|
||||
|
||||
// if we are at last position, save last query
|
||||
if i == len(tokens)-1 {
|
||||
queries = append(queries, current)
|
||||
}
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
141
db/handler/parse_test.go
Normal file
141
db/handler/parse_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/crufter/lexer"
|
||||
)
|
||||
|
||||
func TestLexing(t *testing.T) {
|
||||
tokens, err := lexer.Lex("a == 12", expressions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tokens) != 3 {
|
||||
t.Fatal(tokens)
|
||||
}
|
||||
if tokens[0].Typ != itemFieldName || tokens[1].Typ != itemEquals || tokens[2].Typ != itemInt {
|
||||
t.Fatal(tokens)
|
||||
}
|
||||
|
||||
tokens, err = lexer.Lex(`a == 12 and name != "nandos"`, expressions)
|
||||
if tokens[0].Typ != itemFieldName ||
|
||||
tokens[1].Typ != itemEquals ||
|
||||
tokens[2].Typ != itemInt ||
|
||||
tokens[3].Typ != itemAnd ||
|
||||
tokens[4].Typ != itemFieldName ||
|
||||
tokens[5].Typ != itemNotEquals ||
|
||||
tokens[6].Typ != itemString {
|
||||
t.Fatal(tokens)
|
||||
}
|
||||
}
|
||||
|
||||
type tCase struct {
|
||||
Q string
|
||||
E []Query
|
||||
Err error
|
||||
}
|
||||
|
||||
func TestParsing(t *testing.T) {
|
||||
tCases := []tCase{
|
||||
tCase{
|
||||
Q: `a == 12 and name != "nandos"`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(12),
|
||||
Op: itemEquals,
|
||||
},
|
||||
Query{
|
||||
Field: "name",
|
||||
Value: "nandos",
|
||||
Op: itemNotEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
// test escaping quotes
|
||||
tCase{
|
||||
Q: `a == 12 and name != "He said ""yes""!"`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(12),
|
||||
Op: itemEquals,
|
||||
},
|
||||
Query{
|
||||
Field: "name",
|
||||
Value: `He said "yes"!`,
|
||||
Op: itemNotEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
tCase{
|
||||
Q: `a == false and b == true`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: false,
|
||||
Op: itemEquals,
|
||||
},
|
||||
Query{
|
||||
Field: "b",
|
||||
Value: true,
|
||||
Op: itemEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
// a < 20
|
||||
tCase{
|
||||
Q: `a < 20`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(20),
|
||||
Op: itemLessThan,
|
||||
},
|
||||
},
|
||||
},
|
||||
tCase{
|
||||
Q: `a <= 20`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(20),
|
||||
Op: itemLessThanEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
tCase{
|
||||
Q: `a > 20`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(20),
|
||||
Op: itemGreaterThan,
|
||||
},
|
||||
},
|
||||
},
|
||||
tCase{
|
||||
Q: `a >= 20`,
|
||||
E: []Query{
|
||||
Query{
|
||||
Field: "a",
|
||||
Value: int64(20),
|
||||
Op: itemGreaterThanEquals,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tCase := range tCases {
|
||||
fmt.Println("Parsing", tCase.Q)
|
||||
qs, err := Parse(tCase.Q)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(qs, tCase.E) {
|
||||
t.Fatal("Expected", tCase.E, "got", qs)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user