Skip to content

Commit

Permalink
wip rework
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Dec 8, 2023
1 parent 1191a5c commit 5cc4e98
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 18 deletions.
50 changes: 50 additions & 0 deletions util/sql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package util

import "strconv"

type SQLBuilder struct {
offset int
expr []string
params []any
}

func NewSQLBuilder(offset int) *SQLBuilder {
return &SQLBuilder{
offset: offset,
expr: make([]string, 0),
params: make([]any, 0),
}
}

func (b *SQLBuilder) Add(v any, expr func(int) string) {
b.expr = append(b.expr, expr(b.offset+len(b.params)))
b.params = append(b.params, v)
}

func (b *SQLBuilder) AddSlice(v []any, expr func([]int) string) {
l := len(b.params)
nums := make([]int, len(v))
for i := 0; i < len(nums); i++ {
nums[i] = b.offset + l + i
}

b.expr = append(b.expr, expr(nums))
b.params = append(b.params, v...)
}

func (b *SQLBuilder) Get() ([]string, []any) {
return b.expr, b.params
}

func SQLParam(num int) string {
return "$" + strconv.Itoa(num)
}

func SQLParams(nums []int) []string {
values := make([]string, len(nums))
for i := 0; i < len(nums); i++ {
values[i] = SQLParam(nums[i])
}

return values
}
170 changes: 152 additions & 18 deletions web/dev_application.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package web

import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"github.com/gofrs/uuid/v5"
"github.com/gw2auth/gw2auth.com-api/service"
"github.com/gw2auth/gw2auth.com-api/util"
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v4"
"net/http"
"strings"
"time"
)

Expand All @@ -26,6 +30,17 @@ type pagedResult[T any] struct {
NextToken string `json:"nextToken,omitempty"`
}

type cloudscapeQuery struct {
Tokens []cloudscapeQueryToken `json:"tokens"`
Operation string `json:"operation"`
}

type cloudscapeQueryToken struct {
PropertyKey string `json:"propertyKey"`
Operator string `json:"operator"`
Value string `json:"value"`
}

type devApplicationUser struct {
UserId uuid.UUID `json:"userId"`
CreationTime time.Time `json:"creationTime"`
Expand Down Expand Up @@ -98,34 +113,60 @@ func DevApplicationEndpoint() echo.HandlerFunc {
}

func DevApplicationUsersEndpoint() echo.HandlerFunc {
const pageSize = 3
const defaultPageSize = 50

return wrapAuthenticatedHandlerFunc(func(c echo.Context, rctx RequestContext, session service.Session) error {
applicationId, err := uuid.FromString(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}

additionalSQL := "TRUE"
additionalParams := make([]any, 0)

if qJson := c.QueryParam("query"); qJson != "" {
var query cloudscapeQuery
if err = json.Unmarshal([]byte(qJson), &query); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}

// keep in sync with predefined query parameters
const paramNum = 5
if sql, params, err := translateQuery(paramNum, query); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
} else {
additionalSQL = sql
additionalParams = params
}
}

var t time.Time
var offset uint64
var pageSize uint32
var offset uint32

if nextToken := c.QueryParam("nextToken"); nextToken != "" {
if err = parseNextToken(nextToken, &t, &offset); err != nil {
if err = parseNextToken(nextToken, &t, &pageSize, &offset); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}

// in case anyone messes with the nextToken
if pageSize < 1 || pageSize > 50 || t.Before(time.Now().Add(-time.Hour)) {
return echo.NewHTTPError(http.StatusBadRequest, errors.New("pageSize or timestamp out of bounds"))
}
} else {
t = time.Now()
t = time.Now().Add(-time.Second)
pageSize = defaultPageSize
offset = 0
}

ctx := c.Request().Context()
var results []devApplicationUser
err = rctx.ExecuteTx(ctx, pgx.TxOptions{AccessMode: pgx.ReadOnly}, func(tx pgx.Tx) error {
if _, err := tx.Exec(ctx, fmt.Sprintf("SET TRANSACTION AS OF SYSTEM TIME '%s'", t.Format(time.RFC3339))); err != nil {
if _, err := tx.Exec(ctx, fmt.Sprintf("SET TRANSACTION AS OF SYSTEM TIME %d", t.UnixNano())); err != nil {
return err
}

const sql = `
sql := `
SELECT
app_account_subs.account_sub,
app_accounts.creation_time,
Expand All @@ -146,9 +187,13 @@ LEFT JOIN application_client_accounts app_client_accounts
ON app_accounts.application_id = app_client_accounts.application_id AND app_accounts.account_id = app_client_accounts.account_id
WHERE apps.id = $1
AND apps.account_id = $2
OFFSET $3 LIMIT ($4 + 1)
`
rows, err := tx.Query(ctx, sql, applicationId, session.AccountId, offset, pageSize)
sql += fmt.Sprintf("AND %s OFFSET $3 LIMIT ($4 + 1)", additionalSQL)

params := []any{applicationId, session.AccountId, offset, pageSize}
params = append(params, additionalParams...)

rows, err := tx.Query(ctx, sql, params...)
if err != nil {
return err
}
Expand All @@ -170,9 +215,9 @@ OFFSET $3 LIMIT ($4 + 1)
}

nextToken := ""
if len(results) > pageSize {
if len(results) > int(pageSize) {
results = results[:pageSize]
nextToken = buildNextToken(t, offset+pageSize)
nextToken = buildNextToken(t, pageSize, offset+pageSize)
}

return c.JSON(http.StatusOK, pagedResult[devApplicationUser]{
Expand All @@ -182,27 +227,116 @@ OFFSET $3 LIMIT ($4 + 1)
})
}

func parseNextToken(nextToken string, t *time.Time, offset *uint64) error {
b, err := base64.RawStdEncoding.DecodeString(nextToken)
func translateQuery(paramNum int, query cloudscapeQuery) (string, []any, error) {
propertyToSQL := map[string]string{
"user_id": "app_account_subs.account_sub",
"creation_time": "app_accounts.creation_time",
"client_id": "app_client_accounts.application_client_id",
"approval_status": "app_client_accounts.approval_status",
"authorized_scopes": "app_client_accounts.authorized_scopes",
}
operationToSQL := map[string]string{
"and": "AND",
"or": "OR",
}
operatorToSQL := map[string]string{
"=": "=",
"!=": "!=",
">": ">",
">=": ">=",
"<": "<",
"<=": "<=",
":": "=", // special case for authorized_scopes
"!:": "!=", // special case for authorized_scopes
}

builder := util.NewSQLBuilder(paramNum)

for _, tk := range query.Tokens {
prop, ok := propertyToSQL[tk.PropertyKey]
if !ok {
return "", []any{}, errors.New("invalid property")
}

op, ok := operatorToSQL[tk.Operator]
if !ok {
return "", []any{}, errors.New("invalid operator")
}

if tk.PropertyKey == "authorized_scopes" {
prefix := ""
suffix := ""
if strings.HasPrefix(tk.Operator, "!") {
prefix = "NOT ("
suffix = ")"
}

var values []any
if tk.Value == "" {
values = []any{}
} else {
values = make([]any, 0)
for _, v := range strings.Split(tk.Value, ",") {
values = append(values, v)
}
}

if tk.Operator == ":" || tk.Operator == "!:" {
builder.AddSlice(values, func(nums []int) string {
return fmt.Sprintf("%s%s = ANY ARRAY[ %s ]%s", prefix, prop, strings.Join(util.SQLParams(nums), ","), suffix)
})
/*
builder.Add(values, func(i int) string {
return fmt.Sprintf("%s%s = ANY(%s)%s", prefix, prop, util.SQLParam(i), suffix)
})
*/
} else {
builder.AddSlice(values, func(nums []int) string {
return fmt.Sprintf("%s%s = ARRAY[ %s ]%s", prefix, prop, strings.Join(util.SQLParams(nums), ","), suffix)
})
}
} else {
builder.Add(tk.Value, func(i int) string {
return fmt.Sprintf("%s %s %s", prop, op, util.SQLParam(i))
})
}

paramNum++
}

operation, ok := operationToSQL[query.Operation]
if !ok {
return "", []any{}, errors.New("invalid operation")
}

expr, params := builder.Get()
return strings.Join(expr, fmt.Sprintf(" %s ", operation)), params, nil
}

func parseNextToken(nextToken string, t *time.Time, pageSize *uint32, offset *uint32) error {
b, err := base64.RawURLEncoding.DecodeString(nextToken)
if err != nil {
return err
}

if len(b) < 9 {
buf := bytes.NewBuffer(b)
if buf.Len() < 9 { // 2 uint32 + at least 1 byte
return errors.New("insufficient nextToken length")
}

if err = t.UnmarshalBinary(b[:len(b)-8]); err != nil {
if err = t.UnmarshalBinary(buf.Next(buf.Len() - 8)); err != nil {
return err
}

*offset = binary.BigEndian.Uint64(b[len(b)-8:])
*pageSize = binary.BigEndian.Uint32(buf.Next(4))
*offset = binary.BigEndian.Uint32(buf.Next(4))

return nil
}

func buildNextToken(t time.Time, offset uint64) string {
func buildNextToken(t time.Time, pageSize uint32, offset uint32) string {
b, _ := t.MarshalBinary()
b = binary.BigEndian.AppendUint64(b, offset)
return base64.RawStdEncoding.EncodeToString(b)
b = binary.BigEndian.AppendUint32(b, pageSize)
b = binary.BigEndian.AppendUint32(b, offset)
return base64.RawURLEncoding.EncodeToString(b)
}

0 comments on commit 5cc4e98

Please sign in to comment.