rename database package -> query

This commit is contained in:
2025-03-10 14:50:57 -04:00
parent 78aac7093f
commit 5dc875107b
9 changed files with 114 additions and 73 deletions

View File

@@ -5,7 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"maxwarden/config" "maxwarden/config"
"maxwarden/database" "maxwarden/query"
"maxwarden/handlers" "maxwarden/handlers"
"maxwarden/security" "maxwarden/security"
"maxwarden/tasks" "maxwarden/tasks"
@@ -22,7 +22,7 @@ func main() {
config.Init() config.Init()
security.Init() security.Init()
database.Init() query.Init()
handlers.Init() handlers.Init()
tasks.Init() tasks.Init()

View File

@@ -2,7 +2,7 @@ package entries
import ( import (
"errors" "errors"
"maxwarden/database" "maxwarden/query"
"maxwarden/security" "maxwarden/security"
"maxwarden/users" "maxwarden/users"
"sort" "sort"
@@ -24,7 +24,7 @@ type Secret struct {
} }
type EntryFilter struct { type EntryFilter struct {
Filter database.Filter Filter query.Filter
MasterKey string MasterKey string
UserId int32 UserId int32
} }
@@ -74,7 +74,7 @@ func Filter(f EntryFilter) ([]Secret, error) {
output = OrderByDescription(output, f.Filter.OrderDescending) output = OrderByDescription(output, f.Filter.OrderDescending)
// pagination // pagination
output = database.PaginateSlice(output, f.Filter) output = query.PaginateSlice(output, f.Filter)
return output, nil return output, nil
} }

View File

@@ -9,7 +9,7 @@ import (
. "maragu.dev/gomponents" . "maragu.dev/gomponents"
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
"maxwarden/database" "maxwarden/query"
"maxwarden/entries" "maxwarden/entries"
"maxwarden/middleware" "maxwarden/middleware"
@@ -20,7 +20,7 @@ import (
func VaultHxHandler(w http.ResponseWriter, r *http.Request) { func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
identity := middleware.GetIdentity(r) identity := middleware.GetIdentity(r)
filter := database.ParseFilterFromRequest(r) filter := query.ParseFilterFromRequest(r)
filter.Pagination.Enabled = true filter.Pagination.Enabled = true
entryFilter := entries.EntryFilter{ entryFilter := entries.EntryFilter{
@@ -32,7 +32,7 @@ func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
// fetch entities from filter function // fetch entities from filter function
// this first counts the possible items before pagination // this first counts the possible items before pagination
searchFilter := entries.EntryFilter{ searchFilter := entries.EntryFilter{
Filter: database.NewFilterFromSearch(filter.Search), Filter: query.NewFilterFromSearch(filter.Search),
UserId: identity.UserID, UserId: identity.UserID,
MasterKey: identity.MasterKey, MasterKey: identity.MasterKey,
} }
@@ -46,7 +46,7 @@ func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
entryFilter.Filter.Pagination.GeneratePagination(len(searchItems), len(entryList)) entryFilter.Filter.Pagination.GeneratePagination(len(searchItems), len(entryList))
// Col header names and referenced database col names // Col header names and referenced database col names
cols := []database.ColInfo{ cols := []query.ColInfo{
{DbName: "Description", DisplayName: "Description", Sortable: true}, {DbName: "Description", DisplayName: "Description", Sortable: true},
{DisplayName: "Username"}, {DisplayName: "Username"},
{DisplayName: "Password"}, {DisplayName: "Password"},

View File

@@ -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 ( import (
"database/sql" "database/sql"
@@ -11,17 +13,17 @@ import (
) )
type QueryBuilder struct { type QueryBuilder struct {
BaseSQL string // initial sql string to build query from BaseSQL string // initial sql string to build query from
Subquery bool // wraps query in parenthesis Subquery bool // wraps query in parenthesis
Single bool // returns single entity Single bool // returns single entity
Paginate bool PaginationEnabled bool
ItemsPerPage int CurrentPage int
PageNum int // index from 1 MaxItemsPerPage int
OrderBy []string OrderBy []string
OrderDescending bool OrderDescending bool
GroupBy []string GroupBy []string
Where []QueryFilter Where []QueryFilter
Setters []QuerySetter // used for insert and update compilation Setters []QuerySetter // used for insert and update compilation
} }
type QueryFilter struct { type QueryFilter struct {
@@ -119,6 +121,57 @@ func Get[T any](qb *QueryBuilder, db *sqlx.DB, manualParams ...interface{}) (T,
return entity, err 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 // internal functions
func buildWhere[T any](qb *QueryBuilder) (string, []interface{}, error) { func buildWhere[T any](qb *QueryBuilder) (string, []interface{}, error) {
@@ -277,18 +330,18 @@ func buildSelect[T any](qb *QueryBuilder) (string, []interface{}, error) {
} }
// pagination // pagination
if !qb.Single && qb.Paginate { if !qb.Single && qb.PaginationEnabled {
if qb.PageNum <= 0 { if qb.CurrentPage <= 0 {
qb.PageNum = 1 qb.CurrentPage = 1
} }
if qb.ItemsPerPage <= 0 { if qb.MaxItemsPerPage <= 0 {
qb.ItemsPerPage = 10 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) sql += fmt.Sprintf("OFFSET %d", offset)
} }

View File

@@ -1,4 +1,4 @@
package database package query
import ( import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"

View File

@@ -1,4 +1,4 @@
package database package query
import ( import (
"fmt" "fmt"

View File

@@ -3,7 +3,7 @@ package ui
import ( import (
. "maxwarden/basic" . "maxwarden/basic"
"maxwarden/database" "maxwarden/query"
. "maragu.dev/gomponents" . "maragu.dev/gomponents"
hx "maragu.dev/gomponents-htmx" hx "maragu.dev/gomponents-htmx"
@@ -20,7 +20,7 @@ const FORM_PAGINATION_SUFFIX = "_paginationForm"
func BindSearch(elId string, identifier string) Node { func BindSearch(elId string, identifier string) Node {
return Group{ return Group{
FormAttr(elId + FORM_BIND_SUFFIX), 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 // THE TABLE
// Note that "aboveTable" node is not swapped with HTMX, but "belowTable" is. // 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 { paginationButton := func(icon string, page int) Node {
return Button( return Button(
InlineStyle(` InlineStyle(`
@@ -58,7 +58,7 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat
} }
`), `),
Icon(icon, 16), Icon(icon, 16),
hx.Get(url+database.QueryParamsFromPagenum(page, f)), hx.Get(url+query.QueryParamsFromPagenum(page, f)),
hx.Swap(CSSID(tableId)), hx.Swap(CSSID(tableId)),
hx.Target(CSSID(tableId)), hx.Target(CSSID(tableId)),
hx.Select(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.Swap("outerHTML"),
hx.Target(CSSID(tableId)), hx.Target(CSSID(tableId)),
hx.Select(CSSID(tableId)), hx.Select(CSSID(tableId)),
Input(Type("hidden"), Name(database.ORDER_BY_URL_KEY), Value(f.OrderBy)), Input(Type("hidden"), Name(query.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_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.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 ---- // ---- TABLE HEADER ----
THead( THead(
Tr( Tr(
Map(cols, func(col database.ColInfo) Node { Map(cols, func(col query.ColInfo) Node {
return Th( return Th(
If(col.DisplayPosition == database.COL_POS_RIGHT, If(col.DisplayPosition == query.COL_POS_RIGHT,
InlineStyle("$me { text-align: right; }"), InlineStyle("$me { text-align: right; }"),
), ),
InlineStyle(` InlineStyle(`
@@ -147,7 +147,7 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat
), ),
If(col.Sortable, If(col.Sortable,
Group{ 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.Swap(CSSID(tableId)),
hx.Target(CSSID(tableId)), hx.Target(CSSID(tableId)),
hx.Select(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); 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; }"), 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; }"), InlineStyle("$me { flex-direction: row-reverse; }"),
), ),
Text(col.DisplayName), Text(col.DisplayName),
@@ -267,11 +267,11 @@ func AutoTable[E any](tableId string, url string, cols []database.ColInfo, f dat
hx.Swap("outerHTML"), hx.Swap("outerHTML"),
MapMapWithKey(f.Search, func(s string, v string) Node { 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(query.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_DESC_URL_KEY), Value(ToString(f.OrderDescending))),
Select( Select(
IfElse(opts.Compact, IfElse(opts.Compact,
InlineStyle("$me { padding: $2; }"), 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); 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 == 5, Selected()), Text("5"), Value("5")),
Option(If(f.Pagination.MaxItemsPerPage == 10, Selected()), Text("10"), Value("10")), Option(If(f.Pagination.MaxItemsPerPage == 10, Selected()), Text("10"), Value("10")),
Option(If(f.Pagination.MaxItemsPerPage == 25, Selected()), Text("25"), Value("25")), 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" // 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 // 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 { 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 { for _, v := range columnNames {
cols = append(cols, database.ColInfo{DisplayName: v}) cols = append(cols, query.ColInfo{DisplayName: v})
} }
return AutoTable( return AutoTable(
"", "",
"", "",
cols, cols,
database.Filter{}, query.Filter{},
entities, entities,
nil, nil,
rowComponent, rowComponent,

View File

@@ -2,7 +2,7 @@ package users
import ( import (
"database/sql" "database/sql"
"maxwarden/database" "maxwarden/query"
) )
type User struct { type User struct {
@@ -19,38 +19,38 @@ type User struct {
} }
func FetchById(id int32) (User, error) { func FetchById(id int32) (User, error) {
qb := &database.QueryBuilder{} qb := &query.QueryBuilder{}
qb.BaseSQL = "SELECT * FROM users u WHERE u.id = ?" 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) { func FetchByUsername(username string) (User, error) {
qb := &database.QueryBuilder{} qb := &query.QueryBuilder{}
qb.BaseSQL = "SELECT * FROM users u WHERE u.username = ?" 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) { func FetchSecurityStamp(userid int) (string, error) {
qb := &database.QueryBuilder{} qb := &query.QueryBuilder{}
qb.BaseSQL = "SELECT u.security_stamp FROM users u WHERE u.id = ?" 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) { func Update(user User) (sql.Result, error) {
qb := &database.QueryBuilder{} qb := &query.QueryBuilder{}
qb.BaseSQL = "UPDATE users" qb.BaseSQL = "UPDATE users"
qb.Setters = []database.QuerySetter{ qb.Setters = []query.QuerySetter{
{Column: "failed_attempts", Parameter: user.FailedAttempts}, {Column: "failed_attempts", Parameter: user.FailedAttempts},
{Column: "data", Parameter: user.Data}, {Column: "data", Parameter: user.Data},
} }
qb.Where = []database.QueryFilter{ qb.Where = []query.QueryFilter{
{Column: "id", Parameter: user.ID}, {Column: "id", Operator: query.EQ, Parameter: user.ID},
} }
return database.Update[User](qb, database.DB) return query.Update[User](qb, query.DB)
} }

12
vendor/modules.txt vendored
View File

@@ -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 # github.com/aymerick/douceur v0.2.0
## explicit ## explicit
github.com/aymerick/douceur/css github.com/aymerick/douceur/css
github.com/aymerick/douceur/parser github.com/aymerick/douceur/parser
# github.com/btcsuite/btcd v0.20.1-beta
## explicit; go 1.12
# github.com/btcsuite/btcutil v1.0.2 # github.com/btcsuite/btcutil v1.0.2
## explicit; go 1.13 ## explicit; go 1.13
github.com/btcsuite/btcutil/base58 github.com/btcsuite/btcutil/base58
@@ -72,8 +66,6 @@ github.com/joho/godotenv
github.com/jonboulle/clockwork github.com/jonboulle/clockwork
# github.com/kr/pretty v0.3.1 # github.com/kr/pretty v0.3.1
## explicit; go 1.12 ## explicit; go 1.12
# github.com/kr/text v0.2.0
## explicit
# github.com/mattn/go-sqlite3 v1.14.24 # github.com/mattn/go-sqlite3 v1.14.24
## explicit; go 1.19 ## explicit; go 1.19
github.com/mattn/go-sqlite3 github.com/mattn/go-sqlite3
@@ -90,8 +82,6 @@ github.com/pmezard/go-difflib/difflib
# github.com/robfig/cron/v3 v3.0.1 # github.com/robfig/cron/v3 v3.0.1
## explicit; go 1.12 ## explicit; go 1.12
github.com/robfig/cron/v3 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 # github.com/sethvargo/go-diceware v0.5.0
## explicit; go 1.22 ## explicit; go 1.22
github.com/sethvargo/go-diceware/diceware github.com/sethvargo/go-diceware/diceware
@@ -110,8 +100,6 @@ go.uber.org/atomic
## explicit; go 1.23.0 ## explicit; go 1.23.0
golang.org/x/crypto/bcrypt golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish 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 # golang.org/x/net v0.37.0
## explicit; go 1.23.0 ## explicit; go 1.23.0
golang.org/x/net/html golang.org/x/net/html