From 5dc875107bf83253126584a0f4e2fec5b2109a5f Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Mon, 10 Mar 2025 14:50:57 -0400 Subject: [PATCH] rename database package -> query --- cmd/server/main.go | 4 +- entries/entries.go | 6 +-- handlers/app/vault_hx.go | 8 +-- {database => query}/builder.go | 91 +++++++++++++++++++++++++++------- {database => query}/connect.go | 2 +- {database => query}/filters.go | 2 +- ui/autotable.go | 38 +++++++------- users/users.go | 24 ++++----- vendor/modules.txt | 12 ----- 9 files changed, 114 insertions(+), 73 deletions(-) rename {database => query}/builder.go (78%) rename {database => query}/connect.go (96%) rename {database => query}/filters.go (99%) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3ce3fe9..417769c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -5,7 +5,7 @@ import ( "log" "net/http" "maxwarden/config" - "maxwarden/database" + "maxwarden/query" "maxwarden/handlers" "maxwarden/security" "maxwarden/tasks" @@ -22,7 +22,7 @@ func main() { config.Init() security.Init() - database.Init() + query.Init() handlers.Init() tasks.Init() diff --git a/entries/entries.go b/entries/entries.go index ec58e9a..1241497 100644 --- a/entries/entries.go +++ b/entries/entries.go @@ -2,7 +2,7 @@ package entries import ( "errors" - "maxwarden/database" + "maxwarden/query" "maxwarden/security" "maxwarden/users" "sort" @@ -24,7 +24,7 @@ type Secret struct { } type EntryFilter struct { - Filter database.Filter + Filter query.Filter MasterKey string UserId int32 } @@ -74,7 +74,7 @@ func Filter(f EntryFilter) ([]Secret, error) { output = OrderByDescription(output, f.Filter.OrderDescending) // pagination - output = database.PaginateSlice(output, f.Filter) + output = query.PaginateSlice(output, f.Filter) return output, nil } diff --git a/handlers/app/vault_hx.go b/handlers/app/vault_hx.go index d3312b9..613f05a 100644 --- a/handlers/app/vault_hx.go +++ b/handlers/app/vault_hx.go @@ -9,7 +9,7 @@ import ( . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" - "maxwarden/database" + "maxwarden/query" "maxwarden/entries" "maxwarden/middleware" @@ -20,7 +20,7 @@ import ( func VaultHxHandler(w http.ResponseWriter, r *http.Request) { identity := middleware.GetIdentity(r) - filter := database.ParseFilterFromRequest(r) + filter := query.ParseFilterFromRequest(r) filter.Pagination.Enabled = true entryFilter := entries.EntryFilter{ @@ -32,7 +32,7 @@ func VaultHxHandler(w http.ResponseWriter, r *http.Request) { // fetch entities from filter function // this first counts the possible items before pagination searchFilter := entries.EntryFilter{ - Filter: database.NewFilterFromSearch(filter.Search), + Filter: query.NewFilterFromSearch(filter.Search), UserId: identity.UserID, MasterKey: identity.MasterKey, } @@ -46,7 +46,7 @@ func VaultHxHandler(w http.ResponseWriter, r *http.Request) { entryFilter.Filter.Pagination.GeneratePagination(len(searchItems), len(entryList)) // Col header names and referenced database col names - cols := []database.ColInfo{ + cols := []query.ColInfo{ {DbName: "Description", DisplayName: "Description", Sortable: true}, {DisplayName: "Username"}, {DisplayName: "Password"}, diff --git a/database/builder.go b/query/builder.go similarity index 78% rename from database/builder.go rename to query/builder.go index be5e6aa..17226db 100644 --- a/database/builder.go +++ b/query/builder.go @@ -1,4 +1,6 @@ -package database +// This file provides the QueryBuilder API for building dynamic queries. +// API for building dynamic SQL queries from structured database filters +package query import ( "database/sql" @@ -11,17 +13,17 @@ import ( ) type QueryBuilder struct { - BaseSQL string // initial sql string to build query from - Subquery bool // wraps query in parenthesis - Single bool // returns single entity - Paginate bool - ItemsPerPage int - PageNum int // index from 1 - OrderBy []string - OrderDescending bool - GroupBy []string - Where []QueryFilter - Setters []QuerySetter // used for insert and update compilation + BaseSQL string // initial sql string to build query from + Subquery bool // wraps query in parenthesis + Single bool // returns single entity + PaginationEnabled bool + CurrentPage int + MaxItemsPerPage int + OrderBy []string + OrderDescending bool + GroupBy []string + Where []QueryFilter + Setters []QuerySetter // used for insert and update compilation } type QueryFilter struct { @@ -119,6 +121,57 @@ func Get[T any](qb *QueryBuilder, db *sqlx.DB, manualParams ...interface{}) (T, return entity, err } +// Set query builder properties from a `Filter` object +// NOTE: The Filter.Search map does not generate a QueryWhere[] on the builder +// Either create them manually for fine-grain control, or use the `SetBuilderWhereFromFilter` instead. +func SetBuilderFromFilter(qb *QueryBuilder, f Filter) { + qb.PaginationEnabled = f.Pagination.Enabled + qb.CurrentPage = f.Pagination.CurrentPage + qb.MaxItemsPerPage = f.Pagination.MaxItemsPerPage + + if f.OrderBy != "" { + qb.OrderBy = []string{f.OrderBy} + qb.OrderDescending = f.OrderDescending + } +} + +// Append to QueryBuilder.QueryWhere[] array from a Filter.Search hashmap +// NOTE: This automatically generate WHERE clause uses the LIKE operator, with the search item wrapped in wildcards. +// This is nice when you want to do a simple "search" on a column, but you probably don't want this function if you are doing +// something more specific with your search results. +// +// - Filter.Search keys map to column names +// - Filter.Search values map to SQL parameters +func SetBuilderWhereFromFilter(qb *QueryBuilder, f Filter) { + for k, v := range f.Search { + qb.Where = append(qb.Where, QueryFilter{ + Column: k, Operator: LIKE, Parameter: Wildcard(v), + }) + } +} + +// Wrap the input in SQL wildcards +func Wildcard(i interface{}) string { + v := reflect.ValueOf(i) + + output := "" + + switch v.Kind() { + case reflect.String: + output = "%" + v.String() + "%" + case reflect.Int: + output = fmt.Sprintf("%%%d%%", v.Int()) + case reflect.Float64: + output = fmt.Sprintf("%%%f%%", v.Float()) + case reflect.Bool: + output = fmt.Sprintf("%%%t%%", v.Bool()) + default: + output = fmt.Sprintf("%%%v%%", i) + } + + return output +} + // internal functions func buildWhere[T any](qb *QueryBuilder) (string, []interface{}, error) { @@ -277,18 +330,18 @@ func buildSelect[T any](qb *QueryBuilder) (string, []interface{}, error) { } // pagination - if !qb.Single && qb.Paginate { - if qb.PageNum <= 0 { - qb.PageNum = 1 + if !qb.Single && qb.PaginationEnabled { + if qb.CurrentPage <= 0 { + qb.CurrentPage = 1 } - if qb.ItemsPerPage <= 0 { - qb.ItemsPerPage = 10 + if qb.MaxItemsPerPage <= 0 { + qb.MaxItemsPerPage = 10 } - sql += fmt.Sprintf("LIMIT %d ", qb.ItemsPerPage) + sql += fmt.Sprintf("LIMIT %d ", qb.MaxItemsPerPage) - offset := (qb.PageNum - 1) * qb.ItemsPerPage + offset := (qb.CurrentPage - 1) * qb.MaxItemsPerPage sql += fmt.Sprintf("OFFSET %d", offset) } diff --git a/database/connect.go b/query/connect.go similarity index 96% rename from database/connect.go rename to query/connect.go index 2f5331d..5f2d590 100644 --- a/database/connect.go +++ b/query/connect.go @@ -1,4 +1,4 @@ -package database +package query import ( "github.com/jmoiron/sqlx" diff --git a/database/filters.go b/query/filters.go similarity index 99% rename from database/filters.go rename to query/filters.go index 1c6aff3..cac73aa 100644 --- a/database/filters.go +++ b/query/filters.go @@ -1,4 +1,4 @@ -package database +package query import ( "fmt" diff --git a/ui/autotable.go b/ui/autotable.go index e2f07f5..56b974e 100644 --- a/ui/autotable.go +++ b/ui/autotable.go @@ -3,7 +3,7 @@ package ui import ( . "maxwarden/basic" - "maxwarden/database" + "maxwarden/query" . "maragu.dev/gomponents" hx "maragu.dev/gomponents-htmx" @@ -20,7 +20,7 @@ const FORM_PAGINATION_SUFFIX = "_paginationForm" func BindSearch(elId string, identifier string) Node { return Group{ FormAttr(elId + FORM_BIND_SUFFIX), - Name(database.SEARCH_URL_KEY_PREFIX + identifier), + Name(query.SEARCH_URL_KEY_PREFIX + identifier), } } @@ -41,7 +41,7 @@ type AutoTableOptions struct { // THE TABLE // Note that "aboveTable" node is not swapped with HTMX, but "belowTable" is. -func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f database.Filter, entities []E, aboveTable Node, rowComponent func(E) Node, belowTable Node, opts AutoTableOptions) Node { +func AutoTable[E any](tableId string, url string, cols []query.ColInfo, f query.Filter, entities []E, aboveTable Node, rowComponent func(E) Node, belowTable Node, opts AutoTableOptions) Node { paginationButton := func(icon string, page int) Node { return Button( InlineStyle(` @@ -58,7 +58,7 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat } `), Icon(icon, 16), - hx.Get(url+database.QueryParamsFromPagenum(page, f)), + hx.Get(url+query.QueryParamsFromPagenum(page, f)), hx.Swap(CSSID(tableId)), hx.Target(CSSID(tableId)), hx.Select(CSSID(tableId)), @@ -83,9 +83,9 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat hx.Swap("outerHTML"), hx.Target(CSSID(tableId)), hx.Select(CSSID(tableId)), - Input(Type("hidden"), Name(database.ORDER_BY_URL_KEY), Value(f.OrderBy)), - Input(Type("hidden"), Name(database.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))), - Input(Type("hidden"), Name(database.ITEMS_PER_PAGE_URL_KEY), Value(ToString(f.Pagination.MaxItemsPerPage))), + Input(Type("hidden"), Name(query.ORDER_BY_URL_KEY), Value(f.OrderBy)), + Input(Type("hidden"), Name(query.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))), + Input(Type("hidden"), Name(query.ITEMS_PER_PAGE_URL_KEY), Value(ToString(f.Pagination.MaxItemsPerPage))), ), }, ), @@ -123,9 +123,9 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat // ---- TABLE HEADER ---- THead( Tr( - Map(cols, func(col database.ColInfo) Node { + Map(cols, func(col query.ColInfo) Node { return Th( - If(col.DisplayPosition == database.COL_POS_RIGHT, + If(col.DisplayPosition == query.COL_POS_RIGHT, InlineStyle("$me { text-align: right; }"), ), InlineStyle(` @@ -147,7 +147,7 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat ), If(col.Sortable, Group{ - hx.Get(url + database.QueryParamsFromOrderBy(col.DbName, !f.OrderDescending && (col.DbName == f.OrderBy), f)), + hx.Get(url + query.QueryParamsFromOrderBy(col.DbName, !f.OrderDescending && (col.DbName == f.OrderBy), f)), hx.Swap(CSSID(tableId)), hx.Target(CSSID(tableId)), hx.Select(CSSID(tableId)), @@ -175,10 +175,10 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat font-weight: var(--font-weight-bold); } `), - If(col.DisplayPosition == database.COL_POS_LEFT, + If(col.DisplayPosition == query.COL_POS_LEFT, InlineStyle("$me { flex-direction: row; }"), ), - If(col.DisplayPosition == database.COL_POS_RIGHT, + If(col.DisplayPosition == query.COL_POS_RIGHT, InlineStyle("$me { flex-direction: row-reverse; }"), ), Text(col.DisplayName), @@ -267,11 +267,11 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat hx.Swap("outerHTML"), MapMapWithKey(f.Search, func(s string, v string) Node { - return Input(Type("hidden"), Name(database.SEARCH_URL_KEY_PREFIX+s), Value(v)) + return Input(Type("hidden"), Name(query.SEARCH_URL_KEY_PREFIX+s), Value(v)) }), - Input(Type("hidden"), Name(database.ORDER_BY_URL_KEY), Value(f.OrderBy)), - Input(Type("hidden"), Name(database.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))), + Input(Type("hidden"), Name(query.ORDER_BY_URL_KEY), Value(f.OrderBy)), + Input(Type("hidden"), Name(query.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))), Select( IfElse(opts.Compact, InlineStyle("$me { padding: $2; }"), @@ -287,7 +287,7 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat box-shadow: var(--shadow-sm); } `), - Name(database.ITEMS_PER_PAGE_URL_KEY), + Name(query.ITEMS_PER_PAGE_URL_KEY), Option(If(f.Pagination.MaxItemsPerPage == 5, Selected()), Text("5"), Value("5")), Option(If(f.Pagination.MaxItemsPerPage == 10, Selected()), Text("10"), Value("10")), Option(If(f.Pagination.MaxItemsPerPage == 25, Selected()), Text("25"), Value("25")), @@ -355,17 +355,17 @@ func AutotableSearch(c ...Node) Node { // for simple datasets because why not. This also gives you the option to "upgrade" // to the "full" table later on, since you are using the same api func AutoTableLite[E any](columnNames []string, entities []E, rowComponent func(E) Node, opts AutoTableOptions) Node { - cols := []database.ColInfo{} + cols := []query.ColInfo{} for _, v := range columnNames { - cols = append(cols, database.ColInfo{DisplayName: v}) + cols = append(cols, query.ColInfo{DisplayName: v}) } return AutoTable( "", "", cols, - database.Filter{}, + query.Filter{}, entities, nil, rowComponent, diff --git a/users/users.go b/users/users.go index 5c4622b..75412bc 100644 --- a/users/users.go +++ b/users/users.go @@ -2,7 +2,7 @@ package users import ( "database/sql" - "maxwarden/database" + "maxwarden/query" ) type User struct { @@ -19,38 +19,38 @@ type User struct { } func FetchById(id int32) (User, error) { - qb := &database.QueryBuilder{} + qb := &query.QueryBuilder{} qb.BaseSQL = "SELECT * FROM users u WHERE u.id = ?" - return database.Get[User](qb, database.DB, id) + return query.Get[User](qb, query.DB, id) } func FetchByUsername(username string) (User, error) { - qb := &database.QueryBuilder{} + qb := &query.QueryBuilder{} qb.BaseSQL = "SELECT * FROM users u WHERE u.username = ?" - return database.Get[User](qb, database.DB, username) + return query.Get[User](qb, query.DB, username) } func FetchSecurityStamp(userid int) (string, error) { - qb := &database.QueryBuilder{} + qb := &query.QueryBuilder{} qb.BaseSQL = "SELECT u.security_stamp FROM users u WHERE u.id = ?" - return database.Get[string](qb, database.DB, userid) + return query.Get[string](qb, query.DB, userid) } func Update(user User) (sql.Result, error) { - qb := &database.QueryBuilder{} + qb := &query.QueryBuilder{} qb.BaseSQL = "UPDATE users" - qb.Setters = []database.QuerySetter{ + qb.Setters = []query.QuerySetter{ {Column: "failed_attempts", Parameter: user.FailedAttempts}, {Column: "data", Parameter: user.Data}, } - qb.Where = []database.QueryFilter{ - {Column: "id", Parameter: user.ID}, + qb.Where = []query.QueryFilter{ + {Column: "id", Operator: query.EQ, Parameter: user.ID}, } - return database.Update[User](qb, database.DB) + return query.Update[User](qb, query.DB) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9baaba1..a4762e8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,13 +1,7 @@ -# github.com/PuerkitoBio/goquery v1.10.2 -## explicit; go 1.23 -# github.com/andybalholm/cascadia v1.3.3 -## explicit; go 1.16 # github.com/aymerick/douceur v0.2.0 ## explicit github.com/aymerick/douceur/css github.com/aymerick/douceur/parser -# github.com/btcsuite/btcd v0.20.1-beta -## explicit; go 1.12 # github.com/btcsuite/btcutil v1.0.2 ## explicit; go 1.13 github.com/btcsuite/btcutil/base58 @@ -72,8 +66,6 @@ github.com/joho/godotenv github.com/jonboulle/clockwork # github.com/kr/pretty v0.3.1 ## explicit; go 1.12 -# github.com/kr/text v0.2.0 -## explicit # github.com/mattn/go-sqlite3 v1.14.24 ## explicit; go 1.19 github.com/mattn/go-sqlite3 @@ -90,8 +82,6 @@ github.com/pmezard/go-difflib/difflib # github.com/robfig/cron/v3 v3.0.1 ## explicit; go 1.12 github.com/robfig/cron/v3 -# github.com/rogpeppe/go-internal v1.12.0 -## explicit; go 1.20 # github.com/sethvargo/go-diceware v0.5.0 ## explicit; go 1.22 github.com/sethvargo/go-diceware/diceware @@ -110,8 +100,6 @@ go.uber.org/atomic ## explicit; go 1.23.0 golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish -# golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 -## explicit; go 1.23.0 # golang.org/x/net v0.37.0 ## explicit; go 1.23.0 golang.org/x/net/html