init - add project files
This commit is contained in:
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
DOMAIN="example.com"
|
||||||
|
HOST="localhost"
|
||||||
|
PORT="9090"
|
||||||
|
SESSION_PRIVATE_KEY="key"
|
||||||
|
IDENTITY_PRIVATE_KEY="key"
|
||||||
|
IDENTITY_DEFAULT_PASSWORD="password"
|
||||||
|
DB_SCHEMA="example"
|
||||||
|
SMTP_SERVER="server"
|
||||||
|
SMTP_PORT="587"
|
||||||
|
SMTP_REQUIRE_AUTH="true"
|
||||||
|
SMTP_DISPLAY_FROM="MaxWarden System"
|
||||||
|
SMTP_USERNAME="username"
|
||||||
|
SMTP_PASSWORD="password"
|
||||||
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
__debug*
|
||||||
|
*.pdb
|
||||||
|
./server
|
||||||
|
./metagen
|
||||||
|
./migrator
|
||||||
|
|
||||||
|
# editor stuff
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# User Uploads
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# configuration files
|
||||||
|
config.json
|
||||||
|
metagen_config.json
|
||||||
|
|
||||||
|
# build
|
||||||
|
.build
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# metaprogram files
|
||||||
|
*.metagen.go
|
||||||
|
*.metagen.css
|
||||||
|
|
||||||
|
.metagen
|
||||||
|
|
||||||
|
# jet autogenerated models/table structs
|
||||||
|
.jet/
|
||||||
|
|
||||||
|
server
|
||||||
|
!./cmd/server
|
||||||
|
|
||||||
|
#env / secrets
|
||||||
|
.env
|
||||||
|
|
||||||
|
#database
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
27
.vscode/launch.json
vendored
Normal file
27
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Metaprogram",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/metagen",
|
||||||
|
"args": ["--env=dev", "build"],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug HTTP Server",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/server",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"preLaunchTask": "Metagen: Build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
.vscode/page.code-snippets
vendored
Normal file
30
.vscode/page.code-snippets
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"prefix": "page",
|
||||||
|
"body": [
|
||||||
|
"package ${1:packagename}",
|
||||||
|
"",
|
||||||
|
"import (",
|
||||||
|
" . \"maragu.dev/gomponents\"",
|
||||||
|
" . \"maragu.dev/gomponents/html\"",
|
||||||
|
" \"net/http\"",
|
||||||
|
" \"maxwarden/middleware\"",
|
||||||
|
" . \"maxwarden/handlers/app\"",
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
"// @Identity",
|
||||||
|
"// @Protected",
|
||||||
|
"// @CookieSession",
|
||||||
|
"func ${2:Name}Page(w http.ResponseWriter, r *http.Request) {",
|
||||||
|
" identity := middleware.GetIdentity(r)",
|
||||||
|
" func() Node {",
|
||||||
|
" return AppLayout(\"Another Page\", *identity,",
|
||||||
|
" P(Text(\"This is another test page\")),",
|
||||||
|
" )",
|
||||||
|
" }().Render(w)",
|
||||||
|
"}",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"description": "Page with middleware and a basic view."
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.page": "go",
|
||||||
|
"*.component": "go"
|
||||||
|
}
|
||||||
|
}
|
||||||
51
.vscode/tasks.json
vendored
Normal file
51
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"type": "shell",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Metagen: Build",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./cmd/metagen", "--env=dev", "build"],
|
||||||
|
"group": "build",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Metagen: Migrate Up",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./cmd/metagen", "--env=dev", "migrate", "up"],
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Metagen: Migrate Down",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./cmd/metagen", "--env=dev", "migrate", "down"],
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Metagen: Migrate Goto",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./cmd/metagen", "--env=dev", "migrate", "goto", "${input:migration-number-goto}"],
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Metagen: Migrate Create",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./cmd/metagen", "--env=dev", "migrate", "create", "${input:migration-name}"],
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "migration-name",
|
||||||
|
"description": "Enter Migration Name:",
|
||||||
|
"default": "",
|
||||||
|
"type": "promptString"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "migration-number-goto",
|
||||||
|
"description": "Enter migration number to apply up to:",
|
||||||
|
"default": "",
|
||||||
|
"type": "promptString"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
4
Dockerfile
Normal file
4
Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM alpine:3.14
|
||||||
|
RUN apk add --no-cache git make musl-dev go
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2024 Max Amundsen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
25
README.md
Normal file
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Previous: A powerful web codebase.
|
||||||
|
|
||||||
|
Previous is a reference codebase that provides simple, and powerful facilities for building websites and web applications. Previous is not a framework, or library. This project is a showcase of what a web application can look like with simple procedural code, while avoiding needlessly overcomplicated abstractions.
|
||||||
|
|
||||||
|
At the end of the day, the job of a "web developer" is to dynamically generate HTML, and serve it back to a requester. Previous was built to remove the friction that is common with popular, overengineered solutions, so you can focus on getting the job done.
|
||||||
|
|
||||||
|
Previous includes all the tools you need to handle HTTP requests, interact with a database, and serve HTML.
|
||||||
|
|
||||||
|
## Why not another solution?
|
||||||
|
|
||||||
|
Other tools and frameworks sell the promise of a "good developer experience" without any efforts towards robustness, longevity, or "true" simplicity. Many of these frameworks deprecate features, change APIs regularly, and have many dependencies, forcing you to do lots of housekeeping to keep your website running.
|
||||||
|
|
||||||
|
The code that drives your website / application should be specialized to fit the problems you are trying to solve. By subscribing to a framework, you lock yourself into a set of rules that may not align with those problems. In contrast to a framework, a reference codebase provides all the tools you need to get up and running quickly, while proving the means to extend the code to fit your project.
|
||||||
|
|
||||||
|
### Primary Benefits
|
||||||
|
|
||||||
|
- Compile to a statically-linked executable - Extremely easy to deploy to your platform of choice; docker not required.
|
||||||
|
- Everything is statically type-checked by the compiler - This includes HTML components, and database queries.
|
||||||
|
- Procedural code - No esoteric template / configuration syntax required. Pages are written using pure, procedural Go functions.
|
||||||
|
- Vendored dependencies - All third party code is included in this codebase as source, and is built alongside your user-level code. This allows for robustness and futureproofing, as you are not dependent on library authors, or package manager repository maintainers.
|
||||||
|
- Simple and extendable - This project serves as a reference for your individual needs. You are expected to extend or reduce this codebase as needed. For example, if you are building a simple personal site, you may not need a database, or authentication system! Just delete those parts!
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation can be found [here](https://github.com/maxamundsen/Previous/wiki).
|
||||||
219
auth/auth.go
Normal file
219
auth/auth.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"maxwarden/config"
|
||||||
|
"maxwarden/entries"
|
||||||
|
"maxwarden/security"
|
||||||
|
"maxwarden/users"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UNAUTHORIZED_MESSAGE string = "Unauthorized access"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
UserID int32
|
||||||
|
SecurityStamp string
|
||||||
|
MasterKey string
|
||||||
|
Permissions users.Permissions
|
||||||
|
Authenticated bool
|
||||||
|
Expiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIdentity(userid int32, securityStamp string, masterKey string, rememberMe bool) *Identity {
|
||||||
|
expirationDuration := time.Duration(time.Hour * 24 * time.Duration(config.IDENTITY_COOKIE_EXPIRY_DAYS))
|
||||||
|
expiration := time.Now().Add(expirationDuration)
|
||||||
|
|
||||||
|
// hash password input
|
||||||
|
mk := security.SHA512_58(masterKey)
|
||||||
|
|
||||||
|
return &Identity{
|
||||||
|
UserID: userid,
|
||||||
|
SecurityStamp: securityStamp,
|
||||||
|
Authenticated: true,
|
||||||
|
MasterKey: mk,
|
||||||
|
Expiration: expiration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Authenticate(username string, password string) (int32, string, bool) {
|
||||||
|
// time attack partial mitigation
|
||||||
|
// adds up to 0.5 seconds to the response time
|
||||||
|
|
||||||
|
// this technically does not prevent a time attack, since there is still time variance without the randomness added.
|
||||||
|
// you could theoretically take an average of a 'valid user; incorrect password' vs 'invalid user' response times
|
||||||
|
// to figure out if a user exists, but you would need a lot of data to do that.
|
||||||
|
// this should make it *extremely* unlikely to do when paired with 'n login attempt per ip/minute/fingerprint'
|
||||||
|
// since you would need way more than `n` login attempts to collect an accurate average
|
||||||
|
|
||||||
|
// https://security.stackexchange.com/questions/96489/can-i-prevent-timing-attacks-with-random-delays/96493#96493
|
||||||
|
// https://www.reddit.com/r/PHP/comments/kn6ezp/have_you_secured_your_signup_process_against_a/
|
||||||
|
|
||||||
|
randomSeconds, _ := rand.Int(rand.Reader, big.NewInt(500))
|
||||||
|
randomDuration := time.Duration(randomSeconds.Int64()) * time.Millisecond
|
||||||
|
|
||||||
|
time.Sleep(randomDuration)
|
||||||
|
|
||||||
|
user, userErr := users.FetchByUsername(username)
|
||||||
|
|
||||||
|
if userErr != nil || user.FailedAttempts > int32(config.MAX_LOGIN_ATTEMPTS) {
|
||||||
|
// set user password to dummy password to keep timing consistent when validating password
|
||||||
|
user.Password = "$2a$14$KW5OO1wZqGGq3SrpBFj0Oema5DG8Ph7lZJvq0ECkkYBpNFom6b9vO"
|
||||||
|
security.ComparePasswords(password, user.Password)
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
result := security.ComparePasswords(password, user.Password)
|
||||||
|
|
||||||
|
if !result {
|
||||||
|
user.FailedAttempts += 1
|
||||||
|
users.Update(user)
|
||||||
|
} else {
|
||||||
|
user.FailedAttempts = 0
|
||||||
|
|
||||||
|
// seed data
|
||||||
|
if user.Data == nil || len(user.Data) == 0 {
|
||||||
|
secrets := []entries.Secret{}
|
||||||
|
|
||||||
|
// THE FUNNY THING
|
||||||
|
// for range 2500000 {
|
||||||
|
// secrets = append(secrets, entries.Secret{
|
||||||
|
// ID: security.RandBase58String(32),
|
||||||
|
// Description: "some website",
|
||||||
|
// URL: "https://example.com",
|
||||||
|
// Notes: "test notes something here i like writing notes lalalalala test test",
|
||||||
|
// Username: "username2345",
|
||||||
|
// Password: "laksjdflkjasdlfkj2934829384sldkfj",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
mk := security.SHA512_58(password)
|
||||||
|
user.Data, _ = security.EncryptDataWithKey(&secrets, mk)
|
||||||
|
}
|
||||||
|
|
||||||
|
users.Update(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.ID, user.SecurityStamp, result
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPasswordCriteria(password string) error {
|
||||||
|
if strings.TrimSpace(password) == "" {
|
||||||
|
return errors.New("Password cannot be blank.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(password) < config.PASSWORD_MIN_LENGTH {
|
||||||
|
return errors.New("Password must be at least " + strconv.Itoa(config.PASSWORD_MIN_LENGTH) + " characters long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
uppercaseCount := 0
|
||||||
|
lowercaseCount := 0
|
||||||
|
numberCount := 0
|
||||||
|
symbolCount := 0
|
||||||
|
|
||||||
|
for _, r := range password {
|
||||||
|
if unicode.IsUpper(r) {
|
||||||
|
uppercaseCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if unicode.IsLower(r) {
|
||||||
|
lowercaseCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if unicode.IsNumber(r) {
|
||||||
|
numberCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unicode.IsNumber(r) && !unicode.IsLower(r) && !unicode.IsUpper(r) {
|
||||||
|
symbolCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uppercaseCount < config.PASSWORD_REQUIRED_UPPERCASE {
|
||||||
|
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_UPPERCASE) + " uppercase character(s).")
|
||||||
|
}
|
||||||
|
|
||||||
|
if lowercaseCount < config.PASSWORD_REQUIRED_LOWERCASE {
|
||||||
|
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_LOWERCASE) + " lowercase character(s).")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numberCount < config.PASSWORD_REQUIRED_NUMBERS {
|
||||||
|
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_NUMBERS) + " number(s).")
|
||||||
|
}
|
||||||
|
|
||||||
|
if symbolCount < config.PASSWORD_REQUIRED_SYMBOLS {
|
||||||
|
return errors.New("Password must contain at least " + strconv.Itoa(config.PASSWORD_REQUIRED_SYMBOLS) + " symbol(s).")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func ChangePassword(user models.User, oldPassword string, newPassword string, passwordConfirm string, noCheck bool) (models.User, error) {
|
||||||
|
// // noCheck skips criteria validation, and confirmation validation
|
||||||
|
// if noCheck {
|
||||||
|
// if newPassword != passwordConfirm {
|
||||||
|
// log.Println("Passwords do not match")
|
||||||
|
// return user, errors.New("passwords do not match")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// critErr := CheckPasswordCriteria(newPassword)
|
||||||
|
|
||||||
|
// if critErr != nil {
|
||||||
|
// return user, critErr
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if !security.ComparePasswords(oldPassword, user.Password) {
|
||||||
|
// return user, errors.New("old password incorrect")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// newHash, hashErr := security.HashPassword(newPassword)
|
||||||
|
// if hashErr != nil {
|
||||||
|
// return user, errors.New("could not hash password")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// user.Password = newHash
|
||||||
|
|
||||||
|
// updateErr := database.UpdateUser(user)
|
||||||
|
|
||||||
|
// if updateErr != nil {
|
||||||
|
// return user, errors.New("could not update user")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return user, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // wrapper with less args for skipping validation, confirmation
|
||||||
|
// func ChangePasswordNoCheck(user models.User, newPassword string) (models.User, error) {
|
||||||
|
// return ChangePassword(user, "", newPassword, "", true)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // hard reset user password without confirmation or record.
|
||||||
|
// // should only be used for developer purposes
|
||||||
|
// func ResetPasswordNoConfirm(userid int) (models.User, error) {
|
||||||
|
// user, err := database.FetchUserById(userid)
|
||||||
|
// if err != nil {
|
||||||
|
// return user, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// hash, hashErr := security.HashPassword(config.GetConfig().IdentityDefaultPassword)
|
||||||
|
// if hashErr != nil {
|
||||||
|
// return user, hashErr
|
||||||
|
// }
|
||||||
|
|
||||||
|
// user.Password = hash
|
||||||
|
|
||||||
|
// updateErr := database.UpdateUser(user)
|
||||||
|
// if updateErr != nil {
|
||||||
|
// return user, updateErr
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return user, nil
|
||||||
|
// }
|
||||||
237
basic/basic.go
Normal file
237
basic/basic.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// General purpose "utilities" that act as my own "standard library"
|
||||||
|
package basic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPathParts(path string) []string {
|
||||||
|
trimmed := strings.TrimPrefix(path, "/")
|
||||||
|
return strings.Split(trimmed, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a tree pointer and a slice of path segments to insert.
|
||||||
|
// It looks for an existing child with the current segment name; if none is found,
|
||||||
|
// it creates a new node on the tree. Then it recurses on the remaining segments.
|
||||||
|
//
|
||||||
|
// Ex: Generate tree nodes from url segments
|
||||||
|
// `/app/examples/webpage` -> {"app", "examples", "webpage"}
|
||||||
|
// `/app/examples/hello-world` -> {"app", "examples", "hello-world"
|
||||||
|
// `/auth/login` -> {"auth", "login"}
|
||||||
|
//
|
||||||
|
// =>
|
||||||
|
//
|
||||||
|
// root {
|
||||||
|
// app {
|
||||||
|
// examples {
|
||||||
|
// webpage
|
||||||
|
// hello-world
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// auth {
|
||||||
|
// login
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This function is used in the code generation process to generate `pageinfo` structs
|
||||||
|
// from all known application page URLs.
|
||||||
|
func AddStringPartsToTree(tree *Tree, parts []string) {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, _ := range parts {
|
||||||
|
if parts[i] == "" {
|
||||||
|
parts[i] = "index"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.Children == nil {
|
||||||
|
tree.Children = new([]Tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for an existing child with the current part's name.
|
||||||
|
var child *Tree
|
||||||
|
for i := range *tree.Children {
|
||||||
|
if (*tree.Children)[i].Name == parts[0] {
|
||||||
|
child = &((*tree.Children)[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no child is found, create a new one and append it.
|
||||||
|
if child == nil {
|
||||||
|
newNode := Tree{Name: parts[0]}
|
||||||
|
*tree.Children = append(*tree.Children, newNode)
|
||||||
|
child = &((*tree.Children)[len(*tree.Children)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
AddStringPartsToTree(child, parts[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tree struct {
|
||||||
|
Name string
|
||||||
|
Children *[]Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func CapitalizeFirstLetter(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// Convert the first rune to uppercase
|
||||||
|
first := []rune(s)[0]
|
||||||
|
return string(unicode.ToUpper(first)) + s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntAbs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeURLParams(base string, params ...[2]string) string {
|
||||||
|
output := base
|
||||||
|
|
||||||
|
for i, v := range params {
|
||||||
|
if i == 0 {
|
||||||
|
output += "?" + v[0] + "=" + v[1]
|
||||||
|
} else {
|
||||||
|
output += "&" + v[0] + "=" + v[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func SnakeCaseToTitleCase(s string) string {
|
||||||
|
parts := strings.Split(s, "_")
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
parts[i] = CapitalizeFirstLetter(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToString(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTMLDateToTime(date string) time.Time {
|
||||||
|
t, _ := time.Parse("2006-01-02", date)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeToSqliteString(t time.Time) string {
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SqliteStringToTime(dateTimeStr string) time.Time {
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05.000000000",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
var t time.Time
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
t, err = time.Parse(format, dateTimeStr)
|
||||||
|
if err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeToTimeString(utcTime time.Time) string {
|
||||||
|
return utcTime.Format("03:04 PM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeToString(utcTime time.Time) string {
|
||||||
|
return utcTime.Format("01/02/06 03:04 PM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DateToString(utcTime time.Time) string {
|
||||||
|
return utcTime.Format("01/02/06")
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToDate(ds string) time.Time {
|
||||||
|
nt, err := time.Parse("01/02/06", ds)
|
||||||
|
if err != nil {
|
||||||
|
nt, _ = time.Parse("2006-01-02", ds)
|
||||||
|
}
|
||||||
|
return nt
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reverse[T comparable](s []T) {
|
||||||
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains[T comparable](s []T, e T) bool {
|
||||||
|
for _, v := range s {
|
||||||
|
if v == e {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexOf[T comparable](collection []T, el T) int {
|
||||||
|
for i, x := range collection {
|
||||||
|
if x == el {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveDuplicates[T comparable](sliceList []T) []T {
|
||||||
|
allKeys := make(map[T]bool)
|
||||||
|
list := []T{}
|
||||||
|
for _, item := range sliceList {
|
||||||
|
if _, value := allKeys[item]; !value {
|
||||||
|
allKeys[item] = true
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFirstNChars(s string, n int) string {
|
||||||
|
i := 0
|
||||||
|
for j := range s {
|
||||||
|
if i == n {
|
||||||
|
return s[:j]
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
167
cmd/metagen/css_preprocessor.go
Normal file
167
cmd/metagen/css_preprocessor.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"maxwarden/basic"
|
||||||
|
"maxwarden/security"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dedupMap = make(map[string]bool)
|
||||||
|
|
||||||
|
// Walk the FS tree and search for `.go` files containing calls to `InlineStyle()`
|
||||||
|
// Collect the inputs to each call (they must be string literals), expand shorthand macros, and
|
||||||
|
func generateInlineStyles() {
|
||||||
|
fmt.Printf("Compiling Inline Styles")
|
||||||
|
|
||||||
|
inlineStyleRegex := regexp.MustCompile("InlineStyle\\((((?:'[^']*')|(?:\"[^\"]*\")|(`(?:[^`]|[\r\n])*?`)))\\)")
|
||||||
|
|
||||||
|
// these are the directories that get scanned
|
||||||
|
var dirs = [2]string{"handlers", "ui"}
|
||||||
|
|
||||||
|
var matches []string
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
err := filepath.Walk(dir, func(pathStr string, info os.FileInfo, err error) error {
|
||||||
|
if strings.HasSuffix(info.Name(), ".go") {
|
||||||
|
pathStr = filepath.ToSlash(pathStr)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
file, err := os.Open(pathStr)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
content, contentErr := os.ReadFile(pathStr)
|
||||||
|
if contentErr != nil {
|
||||||
|
return contentErr
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(content)
|
||||||
|
|
||||||
|
// only pre-process page & component files
|
||||||
|
inlineStyleRegex.ReplaceAllStringFunc(s, func(match string) string {
|
||||||
|
submatch := inlineStyleRegex.FindStringSubmatch(match)
|
||||||
|
|
||||||
|
if len(submatch) > 1 {
|
||||||
|
submatch[1] = strings.TrimPrefix(submatch[1], "\"")
|
||||||
|
submatch[1] = strings.TrimPrefix(submatch[1], "'")
|
||||||
|
submatch[1] = strings.TrimPrefix(submatch[1], "`")
|
||||||
|
submatch[1] = strings.TrimSuffix(submatch[1], "\"")
|
||||||
|
submatch[1] = strings.TrimSuffix(submatch[1], "'")
|
||||||
|
submatch[1] = strings.TrimSuffix(submatch[1], "`")
|
||||||
|
submatch[1] = strings.ReplaceAll(submatch[1], "\n", " ")
|
||||||
|
submatch[1] = strings.ReplaceAll(submatch[1], "\t", "")
|
||||||
|
|
||||||
|
rawInput := submatch[1] // this is the input before we process it
|
||||||
|
|
||||||
|
// Skip duplicates
|
||||||
|
_, found := dedupMap[rawInput]
|
||||||
|
if !found {
|
||||||
|
cssHash, _ := security.HighwayHash58(rawInput)
|
||||||
|
cssHash = basic.GetFirstNChars(cssHash, 8)
|
||||||
|
|
||||||
|
// Expand custom css macros (see comments below for details)
|
||||||
|
submatch[1] = expandMe(submatch[1], cssHash)
|
||||||
|
submatch[1] = expandMedia(submatch[1])
|
||||||
|
submatch[1] = expandColor(submatch[1])
|
||||||
|
submatch[1] = expandSpacing(submatch[1])
|
||||||
|
|
||||||
|
matches = append(matches, submatch[1])
|
||||||
|
dedupMap[rawInput] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We aren't actually replacing anything in the src file, we just needed to iterate over the regex matches
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
handleErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputCSS := "/* " + METAGEN_AUTO_COMMENT + " */\n"
|
||||||
|
outputCSS += strings.Join(matches, "")
|
||||||
|
os.WriteFile("./wwwroot/css/style.metagen.css", []byte(outputCSS), 0664)
|
||||||
|
|
||||||
|
printStatus(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand spacing macro
|
||||||
|
// Ex:
|
||||||
|
//
|
||||||
|
// padding: $5;
|
||||||
|
//
|
||||||
|
// => padding: calc(var(--spacing) * 5);
|
||||||
|
func expandSpacing(input string) string {
|
||||||
|
re := regexp.MustCompile(`\$([0-9]+(?:\.[0-9]+)?)`)
|
||||||
|
|
||||||
|
transformed := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||||
|
if len(re.FindStringSubmatch(match)) >= 2 {
|
||||||
|
number := re.FindStringSubmatch(match)[1]
|
||||||
|
return fmt.Sprintf("calc(var(--spacing) * %s)", number)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand color macros:
|
||||||
|
func expandColor(input string) string {
|
||||||
|
re := regexp.MustCompile(`\$color\((.*?)(?:\/(0*(?:[1-9][0-9]?|100)))?\)`)
|
||||||
|
|
||||||
|
transformed := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||||
|
if len(re.FindStringSubmatch(match)) == 3 {
|
||||||
|
if re.FindStringSubmatch(match)[2] == "" {
|
||||||
|
return fmt.Sprintf("var(--color-%s)", re.FindStringSubmatch(match)[1])
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("oklch(from var(--color-%s) l c h / %s%%)", re.FindStringSubmatch(match)[1], re.FindStringSubmatch(match)[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return transformed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand shorthand media queries.
|
||||||
|
// Ex:
|
||||||
|
//
|
||||||
|
// media md { ... }
|
||||||
|
//
|
||||||
|
// => media screen and (min-width: 768px) { ... }
|
||||||
|
func expandMedia(input string) string {
|
||||||
|
input = strings.ReplaceAll(input, "$dark", "(prefers-color-scheme: dark)")
|
||||||
|
input = strings.ReplaceAll(input, "$light", "(prefers-color-scheme: light)")
|
||||||
|
input = strings.ReplaceAll(input, "$xs-", "screen and (max-width: 639px)")
|
||||||
|
input = strings.ReplaceAll(input, "$sm-", "screen and (max-width: 767px)")
|
||||||
|
input = strings.ReplaceAll(input, "$md-", "screen and (max-width: 1023px)")
|
||||||
|
input = strings.ReplaceAll(input, "$lg-", "screen and (max-width: 1279px)")
|
||||||
|
input = strings.ReplaceAll(input, "$xl-", "screen and (max-width: 1535px)")
|
||||||
|
input = strings.ReplaceAll(input, "$sm", "screen and (min-width: 640px)")
|
||||||
|
input = strings.ReplaceAll(input, "$md", "screen and (min-width: 768px)")
|
||||||
|
input = strings.ReplaceAll(input, "$lg", "screen and (min-width: 1024px)")
|
||||||
|
input = strings.ReplaceAll(input, "$xl", "screen and (min-width: 1280px)")
|
||||||
|
input = strings.ReplaceAll(input, "$xx", "screen and (min-width: 1536px)")
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand "$me" macro with inline style attribute
|
||||||
|
// Ex:
|
||||||
|
//
|
||||||
|
// $me { ... }
|
||||||
|
//
|
||||||
|
// => [__inlinecss_{REPLACEMENT_ID}] { ... }
|
||||||
|
func expandMe(input string, replacementId string) string {
|
||||||
|
return strings.ReplaceAll(input, "$me", fmt.Sprintf("[__inlinecss_%s]", replacementId))
|
||||||
|
}
|
||||||
52
cmd/metagen/generate_db_models.go
Normal file
52
cmd/metagen/generate_db_models.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compileJet() {
|
||||||
|
fmt.Printf("Compiling Jet generator")
|
||||||
|
|
||||||
|
os.Setenv("CGO_ENABLED", "1")
|
||||||
|
|
||||||
|
cmd := exec.Command("go", "build", "./cmd/jet")
|
||||||
|
cmd.Dir = "./tools/jet-2.12.0"
|
||||||
|
handleCmdOutput(cmd.CombinedOutput())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateJetModels() {
|
||||||
|
bin := ""
|
||||||
|
jetdir := ".jet"
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
bin = "./tools/jet-2.12.0/jet.exe"
|
||||||
|
} else {
|
||||||
|
bin = "./tools/jet-2.12.0/jet"
|
||||||
|
}
|
||||||
|
|
||||||
|
os.RemoveAll(jetdir)
|
||||||
|
|
||||||
|
// compile bin if not exists
|
||||||
|
if _, err := os.Stat(bin); err != nil {
|
||||||
|
compileJet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generating SQL models (jet)")
|
||||||
|
|
||||||
|
if _, err := os.Stat("passwords.db"); err != nil {
|
||||||
|
printStatus(false)
|
||||||
|
fmt.Println("\n" + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseType := "sqlite"
|
||||||
|
|
||||||
|
cmd := exec.Command(bin, "-source="+databaseType, "-dsn=file:passwords.db", "-schema=maxwarden", "-path="+jetdir)
|
||||||
|
|
||||||
|
handleCmdOutput(cmd.CombinedOutput())
|
||||||
|
printStatus(true)
|
||||||
|
}
|
||||||
29
cmd/metagen/generate_debug_info.go
Normal file
29
cmd/metagen/generate_debug_info.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// set debug constant inside the "config" package
|
||||||
|
func generateDebugConfig() {
|
||||||
|
fmt.Printf("Generating DEBUG/RELEASE config")
|
||||||
|
|
||||||
|
code := METAGEN_AUTO_COMMENT + "\npackage config\n\nconst (\n"
|
||||||
|
|
||||||
|
if envtype == ENVIRONMENT_DEV {
|
||||||
|
code += " DEBUG = true"
|
||||||
|
} else {
|
||||||
|
code += " DEBUG = false"
|
||||||
|
}
|
||||||
|
|
||||||
|
code += "\n)\n"
|
||||||
|
|
||||||
|
// open file and write code to it
|
||||||
|
in := []byte(code)
|
||||||
|
|
||||||
|
err := os.WriteFile("./config/debug.metagen.go", in, 0644)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
printStatus(true)
|
||||||
|
}
|
||||||
111
cmd/metagen/main.go
Normal file
111
cmd/metagen/main.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var envtype int // set by user with cli flag
|
||||||
|
|
||||||
|
const (
|
||||||
|
ENVIRONMENT_DEV = iota
|
||||||
|
ENVIRONMENT_STAGING = iota
|
||||||
|
ENVIRONMENT_PRODUCTION = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
const METAGEN_AUTO_COMMENT = "// @Metagen -- THIS FILE WAS AUTOGENERATED OR PREPROCESSED BY METAGEN - DO NOT EDIT BY HAND"
|
||||||
|
|
||||||
|
// metagen - code generator application
|
||||||
|
//
|
||||||
|
// Generates code for other applications, such as the server application
|
||||||
|
func main() {
|
||||||
|
env := flag.String("env", "dev", "The environment to run in: dev, staging, or production")
|
||||||
|
|
||||||
|
// Parse command-line flags
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
switch *env {
|
||||||
|
case "dev":
|
||||||
|
envtype = ENVIRONMENT_DEV
|
||||||
|
case "staging":
|
||||||
|
envtype = ENVIRONMENT_STAGING
|
||||||
|
case "production":
|
||||||
|
envtype = ENVIRONMENT_PRODUCTION
|
||||||
|
default:
|
||||||
|
fmt.Printf("Invalid environment specified: %s\n", *env)
|
||||||
|
fmt.Println("Allowed values are: dev, staging, or production")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "build-all":
|
||||||
|
preBuild()
|
||||||
|
build()
|
||||||
|
goto End
|
||||||
|
case "build":
|
||||||
|
preBuild()
|
||||||
|
goto End
|
||||||
|
case "migrate":
|
||||||
|
migrations(args)
|
||||||
|
goto End
|
||||||
|
default:
|
||||||
|
helpmsg()
|
||||||
|
goto End
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
End:
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
helpmsg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpmsg() {
|
||||||
|
fmt.Println("Usage: metagen [options...]")
|
||||||
|
fmt.Println("build :: Build dependencies, generate code, then build final executables.")
|
||||||
|
fmt.Println("migrate [up, down, goto {V}, create {migration name}] :: Deploy and create SQL migrations.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preBuild() {
|
||||||
|
if envtype == ENVIRONMENT_DEV {
|
||||||
|
fmt.Println("[DEBUG ENVIRONMENT]")
|
||||||
|
} else if envtype == ENVIRONMENT_STAGING || envtype == ENVIRONMENT_PRODUCTION {
|
||||||
|
fmt.Println("[RELEASE ENVIRONMENT]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// db creation
|
||||||
|
maybeCreateSqliteDb()
|
||||||
|
|
||||||
|
// code generation
|
||||||
|
generateInlineStyles()
|
||||||
|
generateDebugConfig()
|
||||||
|
generateJetModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
func build() {
|
||||||
|
compileServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileServer() {
|
||||||
|
var out []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fmt.Printf("Compiling Server Binary")
|
||||||
|
|
||||||
|
if envtype == ENVIRONMENT_DEV {
|
||||||
|
// include extra flags for the GC
|
||||||
|
out, err = exec.Command("go", "build", "-gcflags=all=-N -l", "./cmd/server").CombinedOutput()
|
||||||
|
} else {
|
||||||
|
out, err = exec.Command("go", "build", "./cmd/server").CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCmdOutput(out, err)
|
||||||
|
printStatus(true)
|
||||||
|
}
|
||||||
242
cmd/metagen/migrations.go
Normal file
242
cmd/metagen/migrations.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// create
|
||||||
|
func maybeCreateSqliteDb() {
|
||||||
|
if _, err := os.Stat("./passwords.db"); err != nil {
|
||||||
|
fmt.Printf("Creating new sqlite database")
|
||||||
|
err := os.WriteFile("./passwords.db", nil, 0755)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating Sqlite database.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.New(
|
||||||
|
"file://./migrations",
|
||||||
|
"sqlite3://passwords.db",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
printStatus(false)
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
mErr := m.Up()
|
||||||
|
if mErr != nil {
|
||||||
|
printStatus(false)
|
||||||
|
fmt.Println(mErr.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
printStatus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the running and creation of migrations
|
||||||
|
func migrations(args []string) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println("Usage: metagen migrate [up, down, goto {V}, create {migration name}]")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeCreateSqliteDb()
|
||||||
|
|
||||||
|
m, err := migrate.New(
|
||||||
|
"file://./migrations",
|
||||||
|
"sqlite3://passwords.db",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateNum := 0
|
||||||
|
|
||||||
|
if len(args) >= 3 && args[1] != "create" {
|
||||||
|
var parseErr error
|
||||||
|
migrateNum, parseErr = strconv.Atoi(args[2])
|
||||||
|
if parseErr != nil {
|
||||||
|
fmt.Println("Please provide a valid migration number.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.PrefetchMigrations = migrate.DefaultPrefetchMigrations
|
||||||
|
|
||||||
|
switch args[1] {
|
||||||
|
case "up":
|
||||||
|
err := m.Up()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
err := m.Down()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
case "goto":
|
||||||
|
err := m.Migrate(uint(migrateNum))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
case "create":
|
||||||
|
if len(args) < 3 {
|
||||||
|
fmt.Println("Please provide a name for the new migration.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
createCmd("./migrations", time.Now(), defaultTimeFormat, args[2], "sql", true, 7, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeFormat = "20060102150405"
|
||||||
|
defaultTimezone = "UTC"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidSequenceWidth = errors.New("Digits must be positive")
|
||||||
|
errIncompatibleSeqAndFormat = errors.New("The seq and format options are mutually exclusive")
|
||||||
|
errInvalidTimeFormat = errors.New("Time format may not be empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
func createFile(filename string) error {
|
||||||
|
// create exclusive (fails if file already exists)
|
||||||
|
// os.Create() specifies 0666 as the FileMode, so we're doing the same
|
||||||
|
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextSeqVersion(matches []string, seqDigits int) (string, error) {
|
||||||
|
if seqDigits <= 0 {
|
||||||
|
return "", errInvalidSequenceWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSeq := uint64(1)
|
||||||
|
|
||||||
|
if len(matches) > 0 {
|
||||||
|
filename := matches[len(matches)-1]
|
||||||
|
matchSeqStr := filepath.Base(filename)
|
||||||
|
idx := strings.Index(matchSeqStr, "_")
|
||||||
|
|
||||||
|
if idx < 1 { // Using 1 instead of 0 since there should be at least 1 digit
|
||||||
|
return "", fmt.Errorf("Malformed migration filename: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
matchSeqStr = matchSeqStr[0:idx]
|
||||||
|
nextSeq, err = strconv.ParseUint(matchSeqStr, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSeq++
|
||||||
|
}
|
||||||
|
|
||||||
|
version := fmt.Sprintf("%0[2]*[1]d", nextSeq, seqDigits)
|
||||||
|
|
||||||
|
if len(version) > seqDigits {
|
||||||
|
return "", fmt.Errorf("Next sequence number %s too large. At most %d digits are allowed", version, seqDigits)
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeVersion(startTime time.Time, format string) (version string, err error) {
|
||||||
|
switch format {
|
||||||
|
case "":
|
||||||
|
err = errInvalidTimeFormat
|
||||||
|
case "unix":
|
||||||
|
version = strconv.FormatInt(startTime.Unix(), 10)
|
||||||
|
case "unixNano":
|
||||||
|
version = strconv.FormatInt(startTime.UnixNano(), 10)
|
||||||
|
default:
|
||||||
|
version = startTime.Format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCmd(dir string, startTime time.Time, format string, name string, ext string, seq bool, seqDigits int, print bool) error {
|
||||||
|
if seq && format != defaultTimeFormat {
|
||||||
|
return errIncompatibleSeqAndFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var version string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
ext = "." + strings.TrimPrefix(ext, ".")
|
||||||
|
|
||||||
|
if seq {
|
||||||
|
matches, err := filepath.Glob(filepath.Join(dir, "*"+ext))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err = nextSeqVersion(matches, seqDigits)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
version, err = timeVersion(startTime, format)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
versionGlob := filepath.Join(dir, version+"_*"+ext)
|
||||||
|
matches, err := filepath.Glob(versionGlob)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches) > 0 {
|
||||||
|
return fmt.Errorf("duplicate migration version: %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, direction := range []string{"up", "down"} {
|
||||||
|
basename := fmt.Sprintf("%s_%s.%s%s", version, name, direction, ext)
|
||||||
|
filename := filepath.Join(dir, basename)
|
||||||
|
|
||||||
|
if err = createFile(filename); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if print {
|
||||||
|
absPath, _ := filepath.Abs(filename)
|
||||||
|
log.Println(absPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
114
cmd/metagen/util.go
Normal file
114
cmd/metagen/util.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
. "maxwarden/basic"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printStatus(b bool) {
|
||||||
|
var status string
|
||||||
|
|
||||||
|
if b {
|
||||||
|
status = "SUCCESS"
|
||||||
|
} else {
|
||||||
|
status = "FAILED"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("... %s\n", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCmdOutput(out []byte, err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\n%s\n", out)
|
||||||
|
fmt.Printf("%s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
printStatus(false)
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert an sqlite DSN to a file name
|
||||||
|
func parseSQLiteFilename(dsn string) (string, error) {
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simple file paths
|
||||||
|
if u.Scheme == "" {
|
||||||
|
return u.Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For more complex DSNs
|
||||||
|
if u.Scheme == "file" {
|
||||||
|
return u.Opaque, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("invalid DSN format: %s", dsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// given an ast.Decl, and destination struct, look at the struct for any
|
||||||
|
// boolean fields with the struct tag `Note`. If valid notes are found in the given Decl doc string
|
||||||
|
// set the tagged booleans to true on the input struct.
|
||||||
|
// file is the file that the decl originated from.
|
||||||
|
func parseNotesFromDocComment(decl ast.Decl, file *os.File, dest any) error {
|
||||||
|
re := regexp.MustCompile(`@(\w+)`)
|
||||||
|
|
||||||
|
var identifier string
|
||||||
|
var docstring string
|
||||||
|
|
||||||
|
var docNotes []string
|
||||||
|
var validNotes []string
|
||||||
|
|
||||||
|
// check if decl is a function
|
||||||
|
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
|
||||||
|
identifier = funcDecl.Name.Name
|
||||||
|
docstring = funcDecl.Doc.Text()
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := re.FindAllStringSubmatch(docstring, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
// match[1] contains the first capture group (the word after '@')
|
||||||
|
docNotes = append(docNotes, match[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
v := reflect.ValueOf(dest).Elem()
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("\ndestination struct must be a pointer to a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
// Look for the `Note` tag in the struct field
|
||||||
|
if field.Tag == "note:\"true\"" {
|
||||||
|
if Contains(docNotes, field.Name) {
|
||||||
|
v.Field(i).SetBool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
validNotes = append(validNotes, field.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range docNotes {
|
||||||
|
if !Contains(validNotes, v) {
|
||||||
|
return fmt.Errorf("\n`%s`: Unknown note `@%s`, Identifier: `%s`\n\tValid values are: %v", file.Name(), v, identifier, validNotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
cmd/passgen/main.go
Normal file
28
cmd/passgen/main.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maxwarden/entries"
|
||||||
|
"maxwarden/security"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 2 {
|
||||||
|
passHash, _ := security.HashPassword(os.Args[1])
|
||||||
|
|
||||||
|
testData := []entries.Secret{}
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
dummyData := entries.Secret{ID: security.RandBase58String(32), Description: "Twitter / X.com", URL: "https://x.com", Notes: "2fa is enabled for this account.", Username: "@johntwitter", Password: "##CORRECT_HORSE_BATTERY_STAPLE_51"}
|
||||||
|
testData = append(testData, dummyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
masterKey := security.SHA512_58(os.Args[1])
|
||||||
|
cryptData, _ := security.EncryptDataWithKey(&testData, masterKey)
|
||||||
|
|
||||||
|
println(passHash)
|
||||||
|
println(cryptData)
|
||||||
|
} else {
|
||||||
|
println("Please input a password as first program argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
75
config/config.go
Normal file
75
config/config.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// The config package allows an easy way to provide variable options to the
|
||||||
|
// program read at runtime during startup. These are typically settings like the
|
||||||
|
// http port, database connection string, or password.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v11"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SESSION_COOKIE_NAME = "_maxwarden_session"
|
||||||
|
SESSION_COOKIE_EXPIRY_DAYS int = 100
|
||||||
|
SESSION_COOKIE_ENTROPY int = 33
|
||||||
|
|
||||||
|
IDENTITY_COOKIE_NAME string = "_maxwarden_identity"
|
||||||
|
IDENTITY_COOKIE_EXPIRY_DAYS int = 30
|
||||||
|
IDENTITY_TOKEN_EXPIRY_DAYS int = 30
|
||||||
|
IDENTITY_COOKIE_ENTROPY int = 33
|
||||||
|
IDENTITY_LOGIN_PATH string = "/auth/login"
|
||||||
|
IDENTITY_LOGOUT_PATH string = "/auth/logout"
|
||||||
|
IDENTITY_DEFAULT_PATH string = "/app"
|
||||||
|
IDENTITY_AUTH_REDIRECT bool = true
|
||||||
|
IDENTITY_AUTH_KEY string = "CORRECT_HORSE_BATTERY_STAPLE"
|
||||||
|
|
||||||
|
// This key is NOT used for the hashing of passwords, or secure session data over the wire.
|
||||||
|
// It is ONLY used for performing quick file and string hashes, where security is not a factor.
|
||||||
|
DATA_HASH_KEY string = "01234567890123456789012345678901"
|
||||||
|
|
||||||
|
PASSWORD_MIN_LENGTH int = 8
|
||||||
|
PASSWORD_REQUIRED_UPPERCASE int = 1
|
||||||
|
PASSWORD_REQUIRED_LOWERCASE int = 1
|
||||||
|
PASSWORD_REQUIRED_NUMBERS int = 1
|
||||||
|
PASSWORD_REQUIRED_SYMBOLS int = 0
|
||||||
|
|
||||||
|
MAX_LOGIN_ATTEMPTS int = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type configuration struct {
|
||||||
|
Domain string `env:"DOMAIN"`
|
||||||
|
Host string `env:"HOST"`
|
||||||
|
Port string `env:"PORT"`
|
||||||
|
IdentityPrivateKey string `env:"IDENTITY_PRIVATE_KEY"`
|
||||||
|
IdentityDefaultPassword string `env:"IDENTITY_DEFAULT_PASSWORD"`
|
||||||
|
SessionPrivateKey string `env:"SESSION_PRIVATE_KEY"`
|
||||||
|
SmtpServer string `env:"SMTP_SERVER"`
|
||||||
|
SmtpPort string `env:"SMTP_PORT"`
|
||||||
|
SmtpUsername string `env:"SMTP_USERNAME"`
|
||||||
|
SmtpDisplayFrom string `env:"SMTP_DISPLAY_FROM"`
|
||||||
|
SmtpPassword string `env:"SMTP_PASSWORD"`
|
||||||
|
SmtpRequireAuth bool `env:"SMTP_REQUIRE_AUTH"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//mysql: "DbConnectionString": "root:PASSWORD@tcp(localhost:3306)/example?parseTime=true",
|
||||||
|
|
||||||
|
var config configuration
|
||||||
|
|
||||||
|
func GetConfig() configuration {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
// When in debug mode, set environment variables from the `.env` file directly.
|
||||||
|
// Just a developer convenience.
|
||||||
|
if DEBUG {
|
||||||
|
godotenv.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
envErr := env.Parse(&config)
|
||||||
|
if envErr != nil {
|
||||||
|
log.Fatal("Error parsing environment variables.")
|
||||||
|
}
|
||||||
|
}
|
||||||
25
database/connect.go
Normal file
25
database/connect.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The database package provides an interface between go code and a relational database.
|
||||||
|
|
||||||
|
var DB *sql.DB
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
DB, err = sql.Open("sqlite3", "file:passwords.db")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.SetMaxOpenConns(0)
|
||||||
|
// db.SetMaxIdleConns(200)
|
||||||
|
// db.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
}
|
||||||
225
database/filters.go
Normal file
225
database/filters.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/go-jet/jet/v2/sqlite"
|
||||||
|
|
||||||
|
. "maxwarden/basic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BETWEEN_LEFT_URL_KEY_PREFIX = "betweenLeft_"
|
||||||
|
BETWEEN_RIGHT_URL_KEY_PREFIX = "betweenRight_"
|
||||||
|
ORDER_BY_URL_KEY = "orderBy"
|
||||||
|
ORDER_DESC_URL_KEY = "desc"
|
||||||
|
PAGE_NUM_URL_KEY = "pageNum"
|
||||||
|
ITEMS_PER_PAGE_URL_KEY = "itemsPerPage"
|
||||||
|
SEARCH_URL_KEY_PREFIX = "search_"
|
||||||
|
FILTER_DEFAULT_MAX_ITEMS = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
COL_POS_LEFT = iota
|
||||||
|
COL_POS_RIGHT = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type ColInfo struct {
|
||||||
|
DisplayName string
|
||||||
|
DbName string
|
||||||
|
Sortable bool
|
||||||
|
DisplayPosition int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Search map[string]string
|
||||||
|
Pagination Pagination
|
||||||
|
OrderBy string
|
||||||
|
OrderDescending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Enabled bool
|
||||||
|
CurrentPage int
|
||||||
|
NextPage int
|
||||||
|
PreviousPage int
|
||||||
|
TotalPages int
|
||||||
|
TotalItems int
|
||||||
|
MaxItemsPerPage int
|
||||||
|
ItemsThisPage int
|
||||||
|
ViewRangeLower int
|
||||||
|
ViewRangeUpper int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilterFromSearch(s map[string]string) Filter {
|
||||||
|
f := Filter {
|
||||||
|
Search: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Pagination.CurrentPage <= 0 {
|
||||||
|
f.Pagination.CurrentPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFilterFromRequest(r *http.Request) Filter {
|
||||||
|
filter := Filter{}
|
||||||
|
filter.Search = make(map[string]string)
|
||||||
|
|
||||||
|
for k, v := range r.URL.Query() {
|
||||||
|
if strings.HasPrefix(k, SEARCH_URL_KEY_PREFIX) {
|
||||||
|
qValue := strings.Join(v, "")
|
||||||
|
|
||||||
|
if qValue != "" {
|
||||||
|
filter.Search[strings.TrimPrefix(k, SEARCH_URL_KEY_PREFIX)] = qValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.OrderBy = r.URL.Query().Get(ORDER_BY_URL_KEY)
|
||||||
|
filter.OrderDescending, _ = strconv.ParseBool(r.URL.Query().Get(ORDER_DESC_URL_KEY))
|
||||||
|
filter.Pagination.CurrentPage, _ = strconv.Atoi(r.URL.Query().Get(PAGE_NUM_URL_KEY))
|
||||||
|
filter.Pagination.MaxItemsPerPage, _ = strconv.Atoi(r.URL.Query().Get(ITEMS_PER_PAGE_URL_KEY))
|
||||||
|
|
||||||
|
if filter.Pagination.MaxItemsPerPage == 0 {
|
||||||
|
filter.Pagination.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Pagination.CurrentPage <= 0 {
|
||||||
|
filter.Pagination.CurrentPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryParamsFromPagenum(pageNum int, f Filter) string {
|
||||||
|
f.Pagination.CurrentPage = pageNum
|
||||||
|
|
||||||
|
return QueryParamsFromFilter(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryParamsFromOrderBy(orderBy string, direction bool, f Filter) string {
|
||||||
|
f.OrderBy = orderBy
|
||||||
|
f.OrderDescending = direction
|
||||||
|
f.Pagination.CurrentPage = 1
|
||||||
|
|
||||||
|
return QueryParamsFromFilter(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func QueryParamsFromFilter(f Filter) string {
|
||||||
|
output := fmt.Sprintf(
|
||||||
|
"?"+ORDER_BY_URL_KEY+"=%s&"+ORDER_DESC_URL_KEY+"=%t&"+PAGE_NUM_URL_KEY+"=%d&"+ITEMS_PER_PAGE_URL_KEY+"=%d",
|
||||||
|
f.OrderBy,
|
||||||
|
f.OrderDescending,
|
||||||
|
f.Pagination.CurrentPage,
|
||||||
|
f.Pagination.MaxItemsPerPage,
|
||||||
|
)
|
||||||
|
|
||||||
|
for k, v := range f.Search {
|
||||||
|
output += "&" + SEARCH_URL_KEY_PREFIX + k + "=" + v
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetColumnFromStringName(column string, cl ColumnList) (Column, bool) {
|
||||||
|
for _, col := range cl {
|
||||||
|
if col.Name() == column {
|
||||||
|
return col, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetColInfoFromJet(cl ColumnList) []ColInfo {
|
||||||
|
list := []ColInfo{}
|
||||||
|
|
||||||
|
for _, col := range cl {
|
||||||
|
newCol := ColInfo{
|
||||||
|
DisplayName: SnakeCaseToTitleCase(col.Name()),
|
||||||
|
DbName: col.Name(),
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, newCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pagination) GeneratePagination(totalItemsInSet int, itemsDisplayedThisPage int) {
|
||||||
|
p.TotalItems = totalItemsInSet
|
||||||
|
p.ItemsThisPage = itemsDisplayedThisPage
|
||||||
|
|
||||||
|
if p.MaxItemsPerPage == 0 {
|
||||||
|
p.MaxItemsPerPage = FILTER_DEFAULT_MAX_ITEMS
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxItemsPerPage == 0 {
|
||||||
|
p.TotalPages = 1
|
||||||
|
} else {
|
||||||
|
p.TotalPages = p.TotalItems / p.MaxItemsPerPage
|
||||||
|
|
||||||
|
if p.TotalItems%p.MaxItemsPerPage != 0 {
|
||||||
|
p.TotalPages++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.TotalPages == 0 {
|
||||||
|
p.TotalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.CurrentPage < 1 {
|
||||||
|
p.CurrentPage = 1
|
||||||
|
p.PreviousPage = 1
|
||||||
|
} else {
|
||||||
|
p.PreviousPage = p.CurrentPage - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.TotalItems != 0 {
|
||||||
|
p.ViewRangeLower = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + 1
|
||||||
|
} else {
|
||||||
|
p.ViewRangeLower = 0
|
||||||
|
}
|
||||||
|
p.ViewRangeUpper = p.MaxItemsPerPage*p.CurrentPage - p.MaxItemsPerPage + p.ItemsThisPage
|
||||||
|
|
||||||
|
if p.CurrentPage >= p.TotalPages {
|
||||||
|
p.CurrentPage = p.TotalPages
|
||||||
|
p.NextPage = p.TotalPages
|
||||||
|
} else {
|
||||||
|
p.NextPage = p.CurrentPage + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////
|
||||||
|
// IN-MEMORY OPERATIONS
|
||||||
|
///////////////////////////
|
||||||
|
|
||||||
|
// If you are doing filtering against in-memory structures, you can use the following helpers:
|
||||||
|
func PaginateSlice[T any](arr []T, f Filter) []T {
|
||||||
|
if f.Pagination.Enabled {
|
||||||
|
if f.Pagination.CurrentPage <= 0 {
|
||||||
|
f.Pagination.CurrentPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Pagination.MaxItemsPerPage > 0 {
|
||||||
|
offset := (f.Pagination.CurrentPage - 1) * f.Pagination.MaxItemsPerPage
|
||||||
|
limit := f.Pagination.MaxItemsPerPage
|
||||||
|
|
||||||
|
// bounds check
|
||||||
|
if offset > len(arr) {
|
||||||
|
arr = []T{}
|
||||||
|
} else if offset+limit > len(arr) {
|
||||||
|
arr = arr[offset:]
|
||||||
|
} else {
|
||||||
|
arr = arr[offset : offset+limit]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
125
entries/entries.go
Normal file
125
entries/entries.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package entries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"maxwarden/database"
|
||||||
|
"maxwarden/security"
|
||||||
|
"maxwarden/users"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
ID string
|
||||||
|
Description string
|
||||||
|
URL string
|
||||||
|
Notes string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntryFilter struct {
|
||||||
|
Filter database.Filter
|
||||||
|
MasterKey string
|
||||||
|
UserId int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func OrderByDescription(secret []Secret, desc bool) []Secret {
|
||||||
|
sort.Slice(secret, func(i, j int) bool {
|
||||||
|
if desc {
|
||||||
|
return strings.ToLower(secret[i].Description) > strings.ToLower(secret[j].Description)
|
||||||
|
} else {
|
||||||
|
return strings.ToLower(secret[i].Description) < strings.ToLower(secret[j].Description)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filter(f EntryFilter) ([]Secret, error) {
|
||||||
|
user, _ := users.FetchById(f.UserId)
|
||||||
|
|
||||||
|
// we need to do the rest in memory because the data is encrypted, so we need to decrypt the data
|
||||||
|
secrets, decErr := security.DecryptDataWithKey[[]Secret](user.Data, f.MasterKey)
|
||||||
|
if decErr != nil {
|
||||||
|
return nil, decErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets == nil {
|
||||||
|
return nil, errors.New("secrets list is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := []Secret{}
|
||||||
|
|
||||||
|
// decrypt data inside each entry retrieved
|
||||||
|
for _, v := range *secrets {
|
||||||
|
search := f.Filter.Search["description"]
|
||||||
|
if search != "" {
|
||||||
|
search = strings.ToLower(search)
|
||||||
|
searchable := strings.ToLower(v.Description)
|
||||||
|
|
||||||
|
if strings.Contains(searchable, search) {
|
||||||
|
output = append(output, v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output = append(output, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//order by
|
||||||
|
output = OrderByDescription(output, f.Filter.OrderDescending)
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
output = database.PaginateSlice(output, f.Filter)
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// linear search because that's the only option
|
||||||
|
func FetchSecretFromID(userId int32, masterKey string, secretId string) (Secret, error) {
|
||||||
|
user, _ := users.FetchById(userId)
|
||||||
|
|
||||||
|
secrets, decErr := security.DecryptDataWithKey[[]Secret](user.Data, masterKey)
|
||||||
|
if decErr != nil {
|
||||||
|
return Secret{}, decErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets == nil {
|
||||||
|
return Secret{}, errors.New("secrets are nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range *secrets {
|
||||||
|
if v.ID == secretId {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Secret{}, errors.New("no secret found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSecret(userId int32, masterKey string, secretId string) error {
|
||||||
|
user, _ := users.FetchById(userId)
|
||||||
|
|
||||||
|
secrets, decErr := security.DecryptDataWithKey[[]Secret](user.Data, masterKey)
|
||||||
|
if decErr != nil {
|
||||||
|
return decErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if secrets == nil {
|
||||||
|
return errors.New("attempt to range over null secret array")
|
||||||
|
}
|
||||||
|
|
||||||
|
output := []Secret{}
|
||||||
|
|
||||||
|
for _, v := range *secrets {
|
||||||
|
if v.ID != secretId {
|
||||||
|
output = append(output, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enc, _ := security.EncryptDataWithKey(&output, masterKey)
|
||||||
|
user.Data = enc
|
||||||
|
|
||||||
|
_, userErr := users.Update(user)
|
||||||
|
|
||||||
|
return userErr
|
||||||
|
}
|
||||||
39
go.mod
Normal file
39
go.mod
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
module maxwarden
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.31.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/btcsuite/btcutil v1.0.2
|
||||||
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
|
github.com/go-co-op/gocron/v2 v2.15.0
|
||||||
|
github.com/go-jet/jet/v2 v2.12.0
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
|
github.com/minio/highwayhash v1.0.3
|
||||||
|
maragu.dev/gomponents v1.0.0
|
||||||
|
maragu.dev/gomponents-htmx v0.6.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
111
go.sum
Normal file
111
go.sum
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
|
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
|
||||||
|
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
|
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||||
|
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
|
||||||
|
github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
|
||||||
|
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 h1:5lyLWsV+qCkoYqsKUDuycESh9DEIPVKN6iCFeL7ag50=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
|
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||||
|
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maragu.dev/gomponents v1.0.0 h1:eeLScjq4PqP1l+r5z/GC+xXZhLHXa6RWUWGW7gSfLh4=
|
||||||
|
maragu.dev/gomponents v1.0.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||||
|
maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng=
|
||||||
|
maragu.dev/gomponents-htmx v0.6.1/go.mod h1:51nXX+dTGff3usM7AJvbeOcQjzjpSycod+60CYeEP/M=
|
||||||
58
handlers/app/account.go
Normal file
58
handlers/app/account.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maxwarden/middleware"
|
||||||
|
"maxwarden/users"
|
||||||
|
|
||||||
|
. "maxwarden/ui"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity := middleware.GetIdentity(r)
|
||||||
|
session := middleware.GetSession(r)
|
||||||
|
|
||||||
|
|
||||||
|
type accountTableItem struct {
|
||||||
|
Property string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := []string {
|
||||||
|
"Property",
|
||||||
|
"Value",
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _ := users.FetchById(identity.UserID)
|
||||||
|
|
||||||
|
entries := []accountTableItem {
|
||||||
|
{ "UserId", user.ID },
|
||||||
|
{ "Username", user.Username },
|
||||||
|
{ "Name", user.Firstname + " " + user.Lastname },
|
||||||
|
{ "Email", user.Email },
|
||||||
|
{ "Last Login", user.LastLogin },
|
||||||
|
}
|
||||||
|
|
||||||
|
func() Node {
|
||||||
|
return AppLayout("Account", *identity, session,
|
||||||
|
AutoTableLite(
|
||||||
|
cols,
|
||||||
|
entries,
|
||||||
|
func(item accountTableItem) Node {
|
||||||
|
return Tr(
|
||||||
|
Td(B(Text(item.Property))),
|
||||||
|
Td(ToText(item.Value)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
AutoTableOptions{
|
||||||
|
BorderX: true,
|
||||||
|
Shadow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}().Render(w)
|
||||||
|
}
|
||||||
17
handlers/app/delete.go
Normal file
17
handlers/app/delete.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maxwarden/entries"
|
||||||
|
"maxwarden/middleware"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity := middleware.GetIdentity(r)
|
||||||
|
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
entries.DeleteSecret(identity.UserID, identity.MasterKey, id)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/app", http.StatusFound)
|
||||||
|
}
|
||||||
149
handlers/app/editor.go
Normal file
149
handlers/app/editor.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maxwarden/entries"
|
||||||
|
"maxwarden/security"
|
||||||
|
. "maxwarden/ui"
|
||||||
|
"maxwarden/users"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"maxwarden/middleware"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EDITOR_TYPE_EDIT = iota
|
||||||
|
EDITOR_TYPE_ADD = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func EditorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity := middleware.GetIdentity(r)
|
||||||
|
session := middleware.GetSession(r)
|
||||||
|
|
||||||
|
var editorType int
|
||||||
|
var title string
|
||||||
|
var btnLabel string
|
||||||
|
|
||||||
|
if r.URL.Path == "/app/editor/add" {
|
||||||
|
editorType = EDITOR_TYPE_ADD
|
||||||
|
title = "Add Credentials"
|
||||||
|
btnLabel = "Add"
|
||||||
|
} else {
|
||||||
|
editorType = EDITOR_TYPE_EDIT
|
||||||
|
title = "Edit Credentials"
|
||||||
|
btnLabel = "Save"
|
||||||
|
}
|
||||||
|
|
||||||
|
var secret entries.Secret
|
||||||
|
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
if editorType == EDITOR_TYPE_EDIT {
|
||||||
|
id := r.PathValue("id")
|
||||||
|
|
||||||
|
secret, _ = entries.FetchSecretFromID(identity.UserID, identity.MasterKey, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
r.ParseForm()
|
||||||
|
|
||||||
|
desc := r.FormValue("description")
|
||||||
|
notes := r.FormValue("notes")
|
||||||
|
username := r.FormValue("un")
|
||||||
|
password := r.FormValue("pas")
|
||||||
|
url := r.FormValue("url")
|
||||||
|
|
||||||
|
secret = entries.Secret{
|
||||||
|
Description: desc,
|
||||||
|
URL: url,
|
||||||
|
Notes: notes,
|
||||||
|
Password: password,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _ := users.FetchById(identity.UserID)
|
||||||
|
|
||||||
|
// Get current secret store
|
||||||
|
secrets, _ := security.DecryptDataWithKey[[]entries.Secret](user.Data, identity.MasterKey)
|
||||||
|
|
||||||
|
if secrets == nil {
|
||||||
|
http.Redirect(w, r, "/app", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if editorType == EDITOR_TYPE_ADD {
|
||||||
|
secret.ID = security.RandBase58String(32)
|
||||||
|
*secrets = append(*secrets, secret)
|
||||||
|
} else {
|
||||||
|
secret.ID = r.PathValue("id")
|
||||||
|
|
||||||
|
// linear search and replace
|
||||||
|
for i, v := range *secrets {
|
||||||
|
if v.ID == secret.ID {
|
||||||
|
(*secrets)[i] = secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize and encrypt modified store using master key
|
||||||
|
enc, _ := security.EncryptDataWithKey(secrets, identity.MasterKey)
|
||||||
|
|
||||||
|
user.Data = enc
|
||||||
|
|
||||||
|
users.Update(user)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/app", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLayout(title, *identity, session,
|
||||||
|
If(editorType == EDITOR_TYPE_EDIT,
|
||||||
|
Group{
|
||||||
|
Modal(
|
||||||
|
"warning_popup",
|
||||||
|
Text("Warning!"),
|
||||||
|
Text("Are you sure you want to delete this entry? This action cannot be undone."),
|
||||||
|
[]Node{
|
||||||
|
A(Href("/app/delete/" + secret.ID), ButtonUIDanger(Text("Delete"))),
|
||||||
|
ButtonUIOutline(ModalCloser(), Text("Close")),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
InlineStyle("$me { display: flex; flex-direction: row-reverse; align-items: center; }"),
|
||||||
|
ModalActuator("warning_popup", ButtonUIDanger(Text("Delete"))),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Form(
|
||||||
|
AutoComplete("off"),
|
||||||
|
Method("POST"),
|
||||||
|
FormLabel(Text("Description")),
|
||||||
|
FormInput(Type("text"), Name("description"), Value(secret.Description)),
|
||||||
|
Br(),
|
||||||
|
|
||||||
|
FormLabel(Text("Username")),
|
||||||
|
FormInput(Type("text"), Name("un"), Value(secret.Username)),
|
||||||
|
Br(),
|
||||||
|
|
||||||
|
FormLabel(Text("Password")),
|
||||||
|
FormInput(Type("password"), Name("pas"), Value(secret.Password)),
|
||||||
|
Br(),
|
||||||
|
|
||||||
|
FormLabel(Text("URL")),
|
||||||
|
FormInput(Type("text"), Name("url"), Value(secret.URL)),
|
||||||
|
Br(),
|
||||||
|
|
||||||
|
FormLabel(Text("Additional Notes")),
|
||||||
|
FormTextarea(InlineStyle("$me { height: $32; font-family: var(--font-mono); }"), Name("notes"), Text(secret.Notes)),
|
||||||
|
Br(),
|
||||||
|
|
||||||
|
Div(
|
||||||
|
InlineStyle("$me { display: flex; flex-direction: row; align-items: center; gap: $4; }"),
|
||||||
|
ButtonUISuccess(Text(btnLabel), Type("submit")),
|
||||||
|
A(Href("/app"), ButtonUIOutline(Text("Close"), Type("button"))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).Render(w)
|
||||||
|
}
|
||||||
32
handlers/app/vault.go
Normal file
32
handlers/app/vault.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "maxwarden/ui"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"maxwarden/middleware"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity := middleware.GetIdentity(r)
|
||||||
|
session := middleware.GetSession(r)
|
||||||
|
|
||||||
|
AppLayout("Credential Vault", *identity, session,
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $5;
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
A(Href("/app/editor/add"), ButtonUI(Text("+ Add Item"))),
|
||||||
|
),
|
||||||
|
HxLoad("/app/vault-hx"),
|
||||||
|
).Render(w)
|
||||||
|
}
|
||||||
91
handlers/app/vault_hx.go
Normal file
91
handlers/app/vault_hx.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "maxwarden/.jet/table"
|
||||||
|
|
||||||
|
"maxwarden/security"
|
||||||
|
. "maxwarden/ui"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"maxwarden/database"
|
||||||
|
"maxwarden/entries"
|
||||||
|
|
||||||
|
"maxwarden/middleware"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VaultHxHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
identity := middleware.GetIdentity(r)
|
||||||
|
|
||||||
|
filter := database.ParseFilterFromRequest(r)
|
||||||
|
filter.Pagination.Enabled = true
|
||||||
|
|
||||||
|
entryFilter := entries.EntryFilter{
|
||||||
|
Filter: filter,
|
||||||
|
UserId: identity.UserID,
|
||||||
|
MasterKey: identity.MasterKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch entities from filter function
|
||||||
|
// this first counts the possible items before pagination
|
||||||
|
searchFilter := entries.EntryFilter{
|
||||||
|
Filter: database.NewFilterFromSearch(filter.Search),
|
||||||
|
UserId: identity.UserID,
|
||||||
|
MasterKey: identity.MasterKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
searchItems, _ := entries.Filter(searchFilter)
|
||||||
|
|
||||||
|
// this query gets the data AFTER pagination
|
||||||
|
entryList, _ := entries.Filter(entryFilter)
|
||||||
|
|
||||||
|
// generate page numbers according to total length of data
|
||||||
|
entryFilter.Filter.Pagination.GeneratePagination(len(searchItems), len(entryList))
|
||||||
|
|
||||||
|
// Col header names and referenced database col names
|
||||||
|
cols := []database.ColInfo{
|
||||||
|
{DbName: "Description", DisplayName: "Description", Sortable: true},
|
||||||
|
{DisplayName: "Username"},
|
||||||
|
{DisplayName: "Password"},
|
||||||
|
{DisplayName: "URL"},
|
||||||
|
{DisplayName: "Action"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate HTML
|
||||||
|
elId := "order_table"
|
||||||
|
AutoTable(
|
||||||
|
elId,
|
||||||
|
r.URL.Path,
|
||||||
|
cols,
|
||||||
|
entryFilter.Filter,
|
||||||
|
entryList,
|
||||||
|
AutotableSearchGroup(
|
||||||
|
AutotableSearch(
|
||||||
|
Placeholder("Search Description..."),
|
||||||
|
BindSearch(elId, "description"),
|
||||||
|
AutoFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
func(entry entries.Secret) Node {
|
||||||
|
return Tr(
|
||||||
|
TdLeft(Text(entry.Description)),
|
||||||
|
TdLeft(Text(entry.Username)),
|
||||||
|
TdLeft(Text("********")),
|
||||||
|
TdLeft(PageLink(security.SanitizationPolicy.Sanitize(entry.URL), Text(entry.URL), false)),
|
||||||
|
TdCenter(A(Href("/app/editor/edit/" + entry.ID), ButtonUIOutline(Icon(ICON_PENCIL, 16)))),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
AutoTableOptions{
|
||||||
|
Compact: false,
|
||||||
|
Shadow: true,
|
||||||
|
Hover: false,
|
||||||
|
Alternate: false,
|
||||||
|
BorderX: true,
|
||||||
|
BorderY: false,
|
||||||
|
},
|
||||||
|
).Render(w)
|
||||||
|
}
|
||||||
218
handlers/auth/login.go
Normal file
218
handlers/auth/login.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "maxwarden/ui"
|
||||||
|
|
||||||
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"maxwarden/auth"
|
||||||
|
"maxwarden/config"
|
||||||
|
"maxwarden/middleware"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
LoginView("").Render(w)
|
||||||
|
} else if r.Method == http.MethodPost {
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
|
||||||
|
userid, securityStamp, authResult := auth.Authenticate(username, password)
|
||||||
|
|
||||||
|
if !authResult {
|
||||||
|
log.Println("Failed login attempt. Username: " + username)
|
||||||
|
LoginView("Username or password incorrect.").Render(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Successful login. Username: " + username)
|
||||||
|
|
||||||
|
// build identity info
|
||||||
|
identity := auth.NewIdentity(userid, securityStamp, password, false)
|
||||||
|
|
||||||
|
// serialize and send as cookie
|
||||||
|
middleware.PutIdentityCookie(w, r, identity)
|
||||||
|
|
||||||
|
params := r.URL.Query()
|
||||||
|
location := params.Get("redirect")
|
||||||
|
|
||||||
|
if len(params["redirect"]) > 0 {
|
||||||
|
http.Redirect(w, r, location, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPath := config.IDENTITY_DEFAULT_PATH
|
||||||
|
http.Redirect(w, r, defaultPath, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginView(errorMsg string) Node {
|
||||||
|
currentYear := time.Time.Year(time.Now())
|
||||||
|
|
||||||
|
return RootLayout("Login",
|
||||||
|
Body(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
height: 100%;
|
||||||
|
background: $color(light-grey);
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: normal;
|
||||||
|
padding-right: $6;
|
||||||
|
padding-left: $6;
|
||||||
|
padding-bottom: $5;
|
||||||
|
padding-top: $24;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media $md {
|
||||||
|
$me {
|
||||||
|
padding-right: $8;
|
||||||
|
padding-left: $8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
margin-bottom: $3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media $sm {
|
||||||
|
$me {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--container-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
A(Href("/"),
|
||||||
|
Img(
|
||||||
|
InlineStyle("$me { margin-right: auto; margin-left: auto; height: $32; width: auto; }"),
|
||||||
|
Src("/images/logo.png"),
|
||||||
|
Alt("MaxWarden"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
@media $sm {
|
||||||
|
$me {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--container-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
If(errorMsg != "",
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
margin-top: $5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media $sm {
|
||||||
|
$me {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--container-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
P(InlineStyle("$me { font-size: var(--text-sm); color: $color(red-500); }"), Text(errorMsg)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Form(InlineStyle("$me { margin-top: $5; }"), Action(""), Method("POST"), AutoComplete("off"),
|
||||||
|
H2(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
margin-top: $10;
|
||||||
|
margin-bottom: $5;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
color: $color(deep-blue);
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
Text("Secure Sign In"),
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Label(
|
||||||
|
InlineStyle("$me { display: block; font-size: var(--text-xs); font-weight: var(--font-weight-normal); color: $color(neutral-700);}"),
|
||||||
|
For("username"),
|
||||||
|
Text("Username"),
|
||||||
|
),
|
||||||
|
Div(InlineStyle("$me { margin-top: $2; }"),
|
||||||
|
FormInput(Name("username"), Type("text"), Required()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Label(
|
||||||
|
InlineStyle("$me { margin-top: $5; display: block; font-size: var(--text-xs); font-weight: var(--font-weight-normal); color: $color(neutral-700);}"),
|
||||||
|
For("password"),
|
||||||
|
Text("Master Password"),
|
||||||
|
),
|
||||||
|
Div(InlineStyle("$me { margin-top: $2; }"),
|
||||||
|
FormInput(Name("password"), Type("password"), Required()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
margin-top: $5;
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
Button(
|
||||||
|
InlineStyle(`
|
||||||
|
$me {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: $2;
|
||||||
|
padding-bottom: $2;
|
||||||
|
padding-left: $5;
|
||||||
|
padding-right: $5;
|
||||||
|
color: $color(white);
|
||||||
|
background-color: $color(deep-blue);
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
$me:hover {
|
||||||
|
background-color: $color(indigo-blue);
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
Type("submit"),
|
||||||
|
Text("Sign In"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InlineScript(`
|
||||||
|
let form = me();
|
||||||
|
let btn = me("button", me());
|
||||||
|
|
||||||
|
form.on("submit", () => { btn.innerHTML = "Authenticating..."; });
|
||||||
|
`),
|
||||||
|
),
|
||||||
|
P(
|
||||||
|
InlineStyle("$me { margin-top: $10; font-size: var(--text-sm); color: $color(neutral-500);}"),
|
||||||
|
Text("© "),
|
||||||
|
ToText(currentYear),
|
||||||
|
Text(" Max Amundsen"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
14
handlers/auth/logout.go
Normal file
14
handlers/auth/logout.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"maxwarden/config"
|
||||||
|
"maxwarden/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
middleware.DeleteIdentityCookie(w, r)
|
||||||
|
middleware.DeleteSessionCookie(w, r)
|
||||||
|
|
||||||
|
http.Redirect(w, r, config.IDENTITY_LOGIN_PATH, http.StatusFound)
|
||||||
|
}
|
||||||
37
handlers/index.go
Normal file
37
handlers/index.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "maxwarden/ui"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// serve home page if route is literally '/'
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, any unmapped route will route to '/', so make sure
|
||||||
|
// the URL is actually '/' or else 404
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
|
||||||
|
ErrorPage(http.StatusNotFound).Render(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := &httptest.ResponseRecorder{Code: http.StatusOK}
|
||||||
|
|
||||||
|
HttpFS.ServeHTTP(rr, r)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
w.WriteHeader(rr.Code)
|
||||||
|
ErrorPage(rr.Code).Render(w)
|
||||||
|
} else {
|
||||||
|
HttpFS.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
handlers/init.go
Normal file
9
handlers/init.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
var HttpFS http.Handler
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
HttpFS = http.FileServer(http.Dir("wwwroot"))
|
||||||
|
}
|
||||||
18
middleware/cors.go
Normal file
18
middleware/cors.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"maxwarden/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EnableCors(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if config.DEBUG {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "https://"+config.GetConfig().Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
175
middleware/identity.go
Normal file
175
middleware/identity.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"maxwarden/auth"
|
||||||
|
"maxwarden/config"
|
||||||
|
"maxwarden/security"
|
||||||
|
"maxwarden/users"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type identityKey struct{}
|
||||||
|
|
||||||
|
func LoadIdentity(h http.HandlerFunc, requireAuth bool) http.HandlerFunc {
|
||||||
|
loginPath := config.IDENTITY_LOGIN_PATH
|
||||||
|
logoutPath := config.IDENTITY_LOGOUT_PATH
|
||||||
|
defaultPath := config.IDENTITY_DEFAULT_PATH
|
||||||
|
redirect := config.IDENTITY_AUTH_REDIRECT
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var identity *auth.Identity
|
||||||
|
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
|
||||||
|
isToken := token != ""
|
||||||
|
|
||||||
|
// if bearer token present, use token auth, else use cookies
|
||||||
|
if isToken {
|
||||||
|
redirect = false
|
||||||
|
splitToken := strings.Split(token, "Bearer ")
|
||||||
|
|
||||||
|
if len(splitToken) >= 2 {
|
||||||
|
token = splitToken[1]
|
||||||
|
identity, _ = security.DecryptData[auth.Identity]([]byte(security.DecodeBase58(token)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if identity == nil {
|
||||||
|
blankIdentity := &auth.Identity{Authenticated: false}
|
||||||
|
|
||||||
|
if requireAuth {
|
||||||
|
http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), identityKey{}, blankIdentity)
|
||||||
|
h.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
identityCookie, err := r.Cookie(config.IDENTITY_COOKIE_NAME)
|
||||||
|
if err == nil {
|
||||||
|
identity, _ = security.DecryptData[auth.Identity]([]byte(security.DecodeBase58(identityCookie.Value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if identity == nil {
|
||||||
|
blankIdentity := &auth.Identity{Authenticated: false}
|
||||||
|
|
||||||
|
if requireAuth {
|
||||||
|
if redirect && loginPath != r.URL.Path && logoutPath != r.URL.Path {
|
||||||
|
redirectPath := loginPath + "?redirect=" + url.QueryEscape(r.URL.String())
|
||||||
|
http.Redirect(w, r, redirectPath, http.StatusFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if !redirect && loginPath != r.URL.Path {
|
||||||
|
http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), identityKey{}, blankIdentity)
|
||||||
|
h.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the current user according to the database, and validate that the security stamp hasn't changed.
|
||||||
|
// if it has, invalidate the login session.
|
||||||
|
latestUser, _ := users.FetchById(identity.UserID)
|
||||||
|
|
||||||
|
securityCheckFailed := latestUser.SecurityStamp != identity.SecurityStamp
|
||||||
|
notAuthenticated := requireAuth && !identity.Authenticated
|
||||||
|
identityExpired := identity.Expiration.Before(time.Now())
|
||||||
|
|
||||||
|
if securityCheckFailed || notAuthenticated || identityExpired {
|
||||||
|
if isToken {
|
||||||
|
http.Error(w, "Error: Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
DeleteIdentityCookie(w, r)
|
||||||
|
http.Redirect(w, r, loginPath, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if redirect && loginPath == r.URL.Path {
|
||||||
|
http.Redirect(w, r, defaultPath, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), identityKey{}, identity)
|
||||||
|
h.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIdentity(r *http.Request) *auth.Identity {
|
||||||
|
identity := r.Context().Value(identityKey{}).(*auth.Identity)
|
||||||
|
return identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutIdentityCookie(w http.ResponseWriter, r *http.Request, identity *auth.Identity) {
|
||||||
|
cookies := r.Cookies()
|
||||||
|
|
||||||
|
// calculate total bytes used by other cookies
|
||||||
|
var totalBytes int
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Name == config.IDENTITY_COOKIE_NAME {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
totalBytes += len(cookie.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cookie serializer is a better way to handle session data. they are still
|
||||||
|
// generated, validated, and read only by the server, but they are stored on the
|
||||||
|
// client in a cookie.
|
||||||
|
|
||||||
|
// For example, when a user logs into a web service, all of their auth data is
|
||||||
|
// packed into a serialized encrypted string, which is sent via a cookie. this
|
||||||
|
// cookie can be sent back to the page, decrypted, and de-serialized to retrieve
|
||||||
|
// auth information in code. this is extremely fast and cheap, since you do not
|
||||||
|
// need to store this data in a database, or even in memory.
|
||||||
|
|
||||||
|
// Of course with this approach you must be careful not to leak the encryption
|
||||||
|
// key, since it can be used to decrypt legitimate keys, and sign faulty ones.
|
||||||
|
// The key should not be checked into VCS, and be regenerated if theft is
|
||||||
|
// suspected. Resetting the key will log *everyone* out, since no sessions
|
||||||
|
// or identities will validate.
|
||||||
|
identityData, err := security.EncryptData(identity)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(identityData) + 8 // 8 additional bytes coming from somewhere ¯\_(ツ)_/¯
|
||||||
|
|
||||||
|
if length+totalBytes > 4096 {
|
||||||
|
log.Println("Attempt to generate cookie exceeding 4096 bytes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpCookie := &http.Cookie{
|
||||||
|
Name: config.IDENTITY_COOKIE_NAME,
|
||||||
|
Value: security.EncodeBase58(string(identityData)),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: r.URL.Scheme == "https",
|
||||||
|
Path: "/",
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no expiry is set, cookie defaults to clear after browser closes
|
||||||
|
|
||||||
|
http.SetCookie(w, httpCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteIdentityCookie(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: config.IDENTITY_COOKIE_NAME,
|
||||||
|
MaxAge: -1,
|
||||||
|
Expires: time.Now().Add(-100 * time.Hour),
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
82
middleware/session.go
Normal file
82
middleware/session.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"maxwarden/config"
|
||||||
|
"maxwarden/security"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sessionKey struct{}
|
||||||
|
|
||||||
|
func LoadSession(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var sessionMap map[string]interface{}
|
||||||
|
|
||||||
|
sessionCookie, err := r.Cookie(config.SESSION_COOKIE_NAME)
|
||||||
|
if err == nil {
|
||||||
|
decryptMap, _ := security.DecryptData[map[string]interface{}]([]byte(security.DecodeBase58(sessionCookie.Value)))
|
||||||
|
sessionMap = *decryptMap
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionMap == nil {
|
||||||
|
sessionMap = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), sessionKey{}, sessionMap)
|
||||||
|
h.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSession(r *http.Request) map[string]interface{} {
|
||||||
|
session := r.Context().Value(sessionKey{}).(map[string]interface{})
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutSessionCookie(w http.ResponseWriter, r *http.Request, session map[string]interface{}) {
|
||||||
|
cookies := r.Cookies()
|
||||||
|
|
||||||
|
// calculate total bytes used by other cookies
|
||||||
|
var totalBytes int
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Name == config.SESSION_COOKIE_NAME {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
totalBytes += len(cookie.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData, err := security.EncryptData(&session)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(sessionData) + 8
|
||||||
|
|
||||||
|
if length+totalBytes > 4096 {
|
||||||
|
log.Println("Attempt to generate cookie exceeding size limit for this domain")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpCookie := &http.Cookie{
|
||||||
|
Name: config.SESSION_COOKIE_NAME,
|
||||||
|
Value: security.EncodeBase58(string(sessionData)),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: r.URL.Scheme == "https",
|
||||||
|
Path: "/",
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, httpCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSessionCookie(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: config.SESSION_COOKIE_NAME,
|
||||||
|
MaxAge: -1,
|
||||||
|
Expires: time.Now().Add(-100 * time.Hour),
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
1
migrations/0000001_init.down.sql
Normal file
1
migrations/0000001_init.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "user";
|
||||||
13
migrations/0000001_init.up.sql
Normal file
13
migrations/0000001_init.up.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
"id" INTEGER NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"firstname" TEXT NOT NULL,
|
||||||
|
"lastname" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"failed_attempts" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"security_stamp" TEXT NOT NULL,
|
||||||
|
"last_login" TEXT NOT NULL,
|
||||||
|
"data" BLOB NOT NULL,
|
||||||
|
PRIMARY KEY("id")
|
||||||
|
);
|
||||||
2
migrations/0000002_seed_data.down.sql
Normal file
2
migrations/0000002_seed_data.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DELETE FROM
|
||||||
|
"user";
|
||||||
26
migrations/0000002_seed_data.up.sql
Normal file
26
migrations/0000002_seed_data.up.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
INSERT INTO
|
||||||
|
"user" (
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"password",
|
||||||
|
"failed_attempts",
|
||||||
|
"last_login",
|
||||||
|
"security_stamp",
|
||||||
|
"data"
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
"admin",
|
||||||
|
"admin@adminland.org",
|
||||||
|
"Example",
|
||||||
|
"User",
|
||||||
|
"$2a$10$YYljQ1vChy1v8piwBOK2GuTGofa09bXygVCnprX06jTJK6nqprE/u",
|
||||||
|
0,
|
||||||
|
"",
|
||||||
|
"ss",
|
||||||
|
""
|
||||||
|
);
|
||||||
246
security/crypt.go
Normal file
246
security/crypt.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"maxwarden/config"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil/base58"
|
||||||
|
|
||||||
|
"github.com/minio/highwayhash"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Encoding Wrappers
|
||||||
|
////////////////////////////////
|
||||||
|
|
||||||
|
func EncodeBase64(in string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase64(in string) string {
|
||||||
|
out, _ := base64.RawStdEncoding.DecodeString(in)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeBase58(in string) string {
|
||||||
|
return base58.Encode([]byte(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase58(in string) string {
|
||||||
|
return string(base58.Decode(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// HASH FUNCTIONS
|
||||||
|
////////////////////////////////
|
||||||
|
|
||||||
|
// Hash with SHA512 and output a Base58 string
|
||||||
|
func SHA512_58(in string) string {
|
||||||
|
hasher := sha512.New()
|
||||||
|
|
||||||
|
hasher.Write([]byte(in))
|
||||||
|
|
||||||
|
hashBytes := hasher.Sum(nil)
|
||||||
|
|
||||||
|
hashString := base58.Encode(hashBytes)
|
||||||
|
|
||||||
|
return hashString
|
||||||
|
}
|
||||||
|
|
||||||
|
func HighwayHash58(in string) (string, error) {
|
||||||
|
key := []byte(config.DATA_HASH_KEY)
|
||||||
|
|
||||||
|
hasher, err := highwayhash.New(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error generating hasher.")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher.Write([]byte(in))
|
||||||
|
|
||||||
|
hash := hasher.Sum(nil)
|
||||||
|
|
||||||
|
encodedData := base58.Encode(hash)
|
||||||
|
|
||||||
|
return encodedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HighwayHash(in string) (string, error) {
|
||||||
|
key := []byte(config.DATA_HASH_KEY)
|
||||||
|
|
||||||
|
hasher, err := highwayhash.New(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error generating hasher.")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher.Write([]byte(in))
|
||||||
|
|
||||||
|
hash := hasher.Sum(nil)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func QuickFileHash(filepath string) (string, error) {
|
||||||
|
key := []byte(config.DATA_HASH_KEY)
|
||||||
|
|
||||||
|
file, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error opening file", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hasher, err := highwayhash.New(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error generating hasher.")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(hasher, file)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error hashing file:", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := hasher.Sum(nil)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password using bcrypt
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare password with hash using bcrypt
|
||||||
|
func ComparePasswords(password string, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandBase58String(entropyBytes int) string {
|
||||||
|
b := make([]byte, entropyBytes)
|
||||||
|
rand.Read(b)
|
||||||
|
return base58.Encode(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Encryption FUNCTIONS
|
||||||
|
////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
// AES Encrypt
|
||||||
|
func EncryptSecret(data []byte, passKey string) ([]byte, error) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
copy(key, passKey)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData := gcm.Seal(nonce, nonce, data, nil)
|
||||||
|
|
||||||
|
return encryptedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES Decrypt
|
||||||
|
func DecryptSecret(encryptedData []byte, passKey string) ([]byte, error) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
copy(key, passKey)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(encryptedData) < nonceSize {
|
||||||
|
return nil, fmt.Errorf("ciphertext too short")
|
||||||
|
}
|
||||||
|
nonce, encryptedData := encryptedData[:nonceSize], encryptedData[nonceSize:]
|
||||||
|
|
||||||
|
decryptedData, err := gcm.Open(nil, nonce, encryptedData, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt data using default private key
|
||||||
|
func EncryptData[T any](data *T) ([]byte, error) {
|
||||||
|
return EncryptDataWithKey(data, config.GetConfig().IdentityPrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt data using default private key
|
||||||
|
func DecryptData[T any](data []byte) (*T, error) {
|
||||||
|
return DecryptDataWithKey[T](data, config.GetConfig().IdentityPrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptDataWithKey[T any](data *T, key string) ([]byte, error) {
|
||||||
|
// serialize
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
e := gob.NewEncoder(&b)
|
||||||
|
err := e.Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt
|
||||||
|
out, err := EncryptSecret(b.Bytes(), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptDataWithKey[T any](data []byte, key string) (*T, error) {
|
||||||
|
dest := new(T)
|
||||||
|
|
||||||
|
secret, err := DecryptSecret(data, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// de-serialized
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
b.Write(secret)
|
||||||
|
|
||||||
|
d := gob.NewDecoder(&b)
|
||||||
|
gobErr := d.Decode(dest)
|
||||||
|
if gobErr != nil {
|
||||||
|
return nil, gobErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest, nil
|
||||||
|
}
|
||||||
9
security/crypt_test.go
Normal file
9
security/crypt_test.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func _BenchmarkDecrypt(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
DecryptSecret([]byte("njniYY9+R8kAxUuoI6p+A0AvDfVwtKVKe7FU7q7eW4IlLF1v4hLF14Fwizsddqh54EjiBB2XwD6g07c2Ovd0p8AehEuZgA8vD1N+3zSKKg+ZDVsc/MS+6iNQYK+ARNYHrqreaB2qiJP260Le3YR3xDY/u7n+JN58FxNf2J1DMvBUXD812d7r3ING4TBTkzcCJFXql+TvzUdC1qnhdrz/AOBo919rP2+yodQRTgBsZPiSb0DCZ9nnuwT9t99ORwn8v3AelyzwBOcxiYSlP07WDQE45o962E+GONiA09q8lBIBV6wT5bgZ3GAOdNNJFPrhSUqhblDB8/16Z1NwhS/lHyQUyjGxwt3zsC3axVCNQ6t4AJr8wEyVnoLb"), "password")
|
||||||
|
}
|
||||||
|
}
|
||||||
9
security/init.go
Normal file
9
security/init.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import "github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
|
var SanitizationPolicy *bluemonday.Policy
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
SanitizationPolicy = bluemonday.UGCPolicy()
|
||||||
|
}
|
||||||
67
snailmail/mail.go
Normal file
67
snailmail/mail.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// This package wraps around the stdlib's smtp client implementation, allowing
|
||||||
|
// for easily sending emails. This package should be used alongside the "views"
|
||||||
|
// package, to generate an HTML template for use in an email.
|
||||||
|
package snailmail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/mail"
|
||||||
|
"net/smtp"
|
||||||
|
"maxwarden/config"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TYPE_TEXT = iota
|
||||||
|
TYPE_HTML = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
Recipients []string
|
||||||
|
Subject string
|
||||||
|
Body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMail(message Email, mailtype int) error {
|
||||||
|
recipientString := strings.Join(message.Recipients, ",")
|
||||||
|
from := mail.Address{Name: config.GetConfig().SmtpDisplayFrom, Address: config.GetConfig().SmtpUsername}
|
||||||
|
|
||||||
|
header := make(map[string]string)
|
||||||
|
header["To"] = recipientString
|
||||||
|
header["From"] = from.String()
|
||||||
|
header["Subject"] = mime.QEncoding.Encode("UTF-8", message.Subject)
|
||||||
|
header["MIME-Version"] = "1.0"
|
||||||
|
header["Content-Transfer-Encoding"] = "base64"
|
||||||
|
header["Date"] = time.Now().Format(time.RFC1123)
|
||||||
|
|
||||||
|
if mailtype == TYPE_HTML {
|
||||||
|
header["Content-Type"] = "text/html; charset=\"utf-8\""
|
||||||
|
} else {
|
||||||
|
header["Content-Type"] = "text/plain; charset=\"utf-8\""
|
||||||
|
}
|
||||||
|
|
||||||
|
email := ""
|
||||||
|
for k, v := range header {
|
||||||
|
email += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||||
|
}
|
||||||
|
email += "\r\n" + base64.StdEncoding.EncodeToString(message.Body.Bytes())
|
||||||
|
|
||||||
|
var auth smtp.Auth = nil
|
||||||
|
|
||||||
|
if config.GetConfig().SmtpRequireAuth {
|
||||||
|
auth = smtp.PlainAuth("", config.GetConfig().SmtpUsername, config.GetConfig().SmtpPassword, config.GetConfig().SmtpServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := smtp.SendMail(config.GetConfig().SmtpServer+":"+config.GetConfig().SmtpPort, auth, config.GetConfig().SmtpUsername, message.Recipients, []byte(email))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
27
tasks/example_tasks.go
Normal file
27
tasks/example_tasks.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Scheduler gocron.Scheduler
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
Scheduler, err := gocron.NewScheduler()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error scheduling tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
Scheduler.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShutdownTasks() {
|
||||||
|
err := Scheduler.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error stopping task scheduler")
|
||||||
|
}
|
||||||
|
}
|
||||||
167
tools/jet-2.12.0/.circleci/config.yml
Normal file
167
tools/jet-2.12.0/.circleci/config.yml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Golang CircleCI 2.0 configuration file
|
||||||
|
#
|
||||||
|
# Check https://circleci.com/docs/2.0/language-go/ for more details
|
||||||
|
version: 2.1
|
||||||
|
orbs:
|
||||||
|
codecov: codecov/codecov@3.1.1
|
||||||
|
jobs:
|
||||||
|
build_and_tests:
|
||||||
|
docker:
|
||||||
|
# specify the version
|
||||||
|
- image: cimg/go:1.22.8
|
||||||
|
- image: cimg/postgres:14.10
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: jet
|
||||||
|
POSTGRES_PASSWORD: jet
|
||||||
|
POSTGRES_DB: jetdb
|
||||||
|
PGPORT: 50901
|
||||||
|
|
||||||
|
- image: circleci/mysql:8.0.27
|
||||||
|
command: [ --default-authentication-plugin=mysql_native_password ]
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: jet
|
||||||
|
MYSQL_DATABASE: dvds
|
||||||
|
MYSQL_USER: jet
|
||||||
|
MYSQL_PASSWORD: jet
|
||||||
|
MYSQL_TCP_PORT: 50902
|
||||||
|
|
||||||
|
- image: circleci/mariadb:10.3
|
||||||
|
command: [ '--default-authentication-plugin=mysql_native_password', '--port=50903' ]
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: jet
|
||||||
|
MYSQL_DATABASE: dvds
|
||||||
|
MYSQL_USER: jet
|
||||||
|
MYSQL_PASSWORD: jet
|
||||||
|
|
||||||
|
- image: cockroachdb/cockroach-unstable:v23.1.0-rc.2
|
||||||
|
command: ['start-single-node', '--accept-sql-without-tls']
|
||||||
|
environment:
|
||||||
|
COCKROACH_USER: jet
|
||||||
|
COCKROACH_PASSWORD: jet
|
||||||
|
COCKROACH_DATABASE: jetdb
|
||||||
|
|
||||||
|
environment: # environment variables for the build itself
|
||||||
|
TEST_RESULTS: /tmp/test-results # path to where test results will be saved
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Submodule init
|
||||||
|
command: cd tests && make checkout-testdata
|
||||||
|
|
||||||
|
- restore_cache: # restores saved cache if no changes are detected since last run
|
||||||
|
keys:
|
||||||
|
- go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install jet generator
|
||||||
|
command: cd tests && make install-jet-gen
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Waiting for Postgres to be ready
|
||||||
|
command: |
|
||||||
|
for i in `seq 1 10`;
|
||||||
|
do
|
||||||
|
nc -z localhost 50901 && echo Success && exit 0
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo Failed waiting for Postgres && exit 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Waiting for MySQL to be ready
|
||||||
|
command: |
|
||||||
|
for i in `seq 1 10`;
|
||||||
|
do
|
||||||
|
nc -z 127.0.0.1 50902 && echo Success && exit 0
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo Failed waiting for MySQL && exit 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Waiting for MariaDB to be ready
|
||||||
|
command: |
|
||||||
|
for i in `seq 1 10`;
|
||||||
|
do
|
||||||
|
nc -z 127.0.0.1 50903 && echo Success && exit 0
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo Failed waiting for MySQL && exit 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Waiting for Cockroach to be ready
|
||||||
|
command: |
|
||||||
|
for i in `seq 1 10`;
|
||||||
|
do
|
||||||
|
nc -z localhost 26257 && echo Success && exit 0
|
||||||
|
echo -n .
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo Failed waiting for Cockroach && exit 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install MySQL CLI;
|
||||||
|
command: |
|
||||||
|
sudo apt-get --allow-releaseinfo-change update && sudo apt-get install default-mysql-client
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Create MySQL/MariaDB user and test databases
|
||||||
|
command: |
|
||||||
|
mysql -h 127.0.0.1 -P 50902 -u root -pjet -e "grant all privileges on *.* to 'jet'@'%';"
|
||||||
|
mysql -h 127.0.0.1 -P 50902 -u root -pjet -e "set global sql_mode = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';"
|
||||||
|
mysql -h 127.0.0.1 -P 50902 -u jet -pjet -e "create database test_sample"
|
||||||
|
mysql -h 127.0.0.1 -P 50902 -u jet -pjet -e "create database dvds2"
|
||||||
|
|
||||||
|
mysql -h 127.0.0.1 -P 50903 -u root -pjet -e "grant all privileges on *.* to 'jet'@'%';"
|
||||||
|
mysql -h 127.0.0.1 -P 50903 -u root -pjet -e "set global sql_mode = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';"
|
||||||
|
mysql -h 127.0.0.1 -P 50903 -u jet -pjet -e "create database test_sample"
|
||||||
|
mysql -h 127.0.0.1 -P 50903 -u jet -pjet -e "create database dvds2"
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Init databases
|
||||||
|
command: |
|
||||||
|
cd tests
|
||||||
|
go run ./init/init.go -testsuite all
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install gotestsum
|
||||||
|
command: go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
|
# to create test results report
|
||||||
|
- run: mkdir -p $TEST_RESULTS
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Running tests
|
||||||
|
command: gotestsum --junitfile $TEST_RESULTS/report.xml --format testname -- -coverprofile=cover.out -covermode=atomic -coverpkg=github.com/go-jet/jet/v2/postgres/...,github.com/go-jet/jet/v2/mysql/...,github.com/go-jet/jet/v2/sqlite/...,github.com/go-jet/jet/v2/qrm/...,github.com/go-jet/jet/v2/generator/...,github.com/go-jet/jet/v2/internal/...,github.com/go-jet/jet/v2/stmtcache/... ./...
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Running tests with statement caching enabled
|
||||||
|
command: JET_TESTS_WITH_STMT_CACHE=true go test -v ./tests/...
|
||||||
|
|
||||||
|
# run mariaDB and cockroachdb tests. No need to collect coverage, because coverage is already included with mysql and postgres tests
|
||||||
|
- run: MY_SQL_SOURCE=MariaDB go test -v ./tests/mysql/
|
||||||
|
- run: PG_SOURCE=COCKROACH_DB go test -v ./tests/postgres/
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
key: go-mod-v4-{{ checksum "go.sum" }}
|
||||||
|
paths:
|
||||||
|
- "/go/pkg/mod"
|
||||||
|
|
||||||
|
- codecov/upload:
|
||||||
|
file: cover.out
|
||||||
|
|
||||||
|
- store_artifacts: # Upload test summary for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
|
||||||
|
path: /tmp/test-results
|
||||||
|
destination: raw-test-output
|
||||||
|
|
||||||
|
- store_test_results: # Upload test results for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
|
||||||
|
path: /tmp/test-results
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build_and_test:
|
||||||
|
jobs:
|
||||||
|
- build_and_tests
|
||||||
3
tools/jet-2.12.0/.gitattributes
vendored
Normal file
3
tools/jet-2.12.0/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
*.sql linguist-detectable=false
|
||||||
|
*.json linguist-detectable=false
|
||||||
23
tools/jet-2.12.0/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
tools/jet-2.12.0/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Environment (please complete the following information):**
|
||||||
|
- OS: [e.g. linux, windows, macosx]
|
||||||
|
- Database: [e.g. postgres, mysql, sqlite]
|
||||||
|
- Database driver: [e.g. pq, pgx]
|
||||||
|
- Jet version [e.g. 2.6.0 or branch name]
|
||||||
|
|
||||||
|
**Code snippet**
|
||||||
|
Query statement and model files of interest.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
14
tools/jet-2.12.0/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
tools/jet-2.12.0/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: missing feature
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
6
tools/jet-2.12.0/.github/dependabot.yml
vendored
Normal file
6
tools/jet-2.12.0/.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
45
tools/jet-2.12.0/.github/workflows/code_scanner.yml
vendored
Normal file
45
tools/jet-2.12.0/.github/workflows/code_scanner.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Code Scanners
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
go_version: "1.22.8"
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security_scanning:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.go_version }}
|
||||||
|
cache: true
|
||||||
|
- name: Setup Tools
|
||||||
|
run: |
|
||||||
|
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||||
|
- name: Running Scan
|
||||||
|
run: gosec --exclude=G402,G304 ./...
|
||||||
|
lint_scanner:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.go_version }}
|
||||||
|
cache: true
|
||||||
|
- name: Setup Tools
|
||||||
|
run: |
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
- name: Running Scan
|
||||||
|
run: golangci-lint run --timeout=30m ./...
|
||||||
70
tools/jet-2.12.0/.github/workflows/codeql-analysis.yml
vendored
Normal file
70
tools/jet-2.12.0/.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '36 17 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
26
tools/jet-2.12.0/.gitignore
vendored
Normal file
26
tools/jet-2.12.0/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Idea
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
gen
|
||||||
|
.gentestdata
|
||||||
|
.tests/testdata/
|
||||||
|
.gen
|
||||||
|
.docker
|
||||||
|
.env
|
||||||
|
.tempTestDir
|
||||||
|
.gentestdata3
|
||||||
3
tools/jet-2.12.0/.gitmodules
vendored
Normal file
3
tools/jet-2.12.0/.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "tests/testdata"]
|
||||||
|
path = tests/testdata
|
||||||
|
url = https://github.com/go-jet/jet-test-data
|
||||||
19
tools/jet-2.12.0/.golangci.yml
Normal file
19
tools/jet-2.12.0/.golangci.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
run:
|
||||||
|
# The default concurrency value is the number of available CPU.
|
||||||
|
concurrency: 4
|
||||||
|
# Timeout for analysis, e.g. 30s, 5m.
|
||||||
|
# Default: 1m
|
||||||
|
timeout: 30m
|
||||||
|
# Exit code when at least one issue was found.
|
||||||
|
# Default: 1
|
||||||
|
issues-exit-code: 2
|
||||||
|
# Include test files or not.
|
||||||
|
# Default: true
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- tests
|
||||||
|
exclude-files:
|
||||||
|
- "_test.go"
|
||||||
|
- "testutils.go"
|
||||||
262
tools/jet-2.12.0/LICENSE
Normal file
262
tools/jet-2.12.0/LICENSE
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2019 Goran Bjelanovic
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
This product builds on various third-party components under other open source licenses.
|
||||||
|
This section summarizes those components and their licenses.
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/dropbox/godropbox
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2014 Dropbox, Inc.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
https://github.com/serenize/snaker
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2015 Serenize UG (haftungsbeschränkt)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
14
tools/jet-2.12.0/NOTICE
Normal file
14
tools/jet-2.12.0/NOTICE
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Jet
|
||||||
|
Copyright 2019 Goran Bjelanovic
|
||||||
|
|
||||||
|
|
||||||
|
This product contains a modified portion of 'godropbox' which can be obtained at:
|
||||||
|
https://github.com/dropbox/godropbox/tree/master/database/sqlbuilder (BSD-3)
|
||||||
|
|
||||||
|
|
||||||
|
This product contains a modified portion of 'snaker' which can be obtained at:
|
||||||
|
https://github.com/serenize/snaker (MIT)
|
||||||
|
|
||||||
|
|
||||||
|
This product contains `FormatTimestamp` function from 'pq' which can be obtained at:
|
||||||
|
https://github.com/lib/pq (MIT)
|
||||||
583
tools/jet-2.12.0/README.md
Normal file
583
tools/jet-2.12.0/README.md
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
# Jet
|
||||||
|
|
||||||
|
[](https://app.circleci.com/pipelines/github/go-jet/jet?branch=master)
|
||||||
|
[](https://codecov.io/gh/go-jet/jet)
|
||||||
|
[](https://goreportcard.com/report/github.com/go-jet/jet/v2)
|
||||||
|
[](http://godoc.org/github.com/go-jet/jet/v2)
|
||||||
|
[](https://github.com/go-jet/jet/releases)
|
||||||
|
|
||||||
|
Jet is a complete solution for efficient and high performance database access, consisting of type-safe SQL builder
|
||||||
|
with code generation and automatic query result data mapping.
|
||||||
|
Jet currently supports `PostgreSQL`, `MySQL`, `CockroachDB`, `MariaDB` and `SQLite`. Future releases will add support for additional databases.
|
||||||
|
|
||||||
|

|
||||||
|
Jet is the easiest, and the fastest way to write complex type-safe SQL queries as a Go code and map database query result
|
||||||
|
into complex object composition. __It is not an ORM.__
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
https://medium.com/@go.jet/jet-5f3667efa0cc
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
- [Features](#features)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Generate sql builder and model types](#generate-sql-builder-and-model-types)
|
||||||
|
- [Lets write some SQL queries in Go](#lets-write-some-sql-queries-in-go)
|
||||||
|
- [Execute query and store result](#execute-query-and-store-result)
|
||||||
|
- [Benefits](#benefits)
|
||||||
|
- [Dependencies](#dependencies)
|
||||||
|
- [Versioning](#versioning)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
1) Auto-generated type-safe SQL Builder. Statements supported:
|
||||||
|
* [SELECT](https://github.com/go-jet/jet/wiki/SELECT) `(DISTINCT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, FOR, LOCK_IN_SHARE_MODE, UNION, INTERSECT, EXCEPT, WINDOW, sub-queries)`
|
||||||
|
* [INSERT](https://github.com/go-jet/jet/wiki/INSERT) `(VALUES, MODEL, MODELS, QUERY, ON_CONFLICT/ON_DUPLICATE_KEY_UPDATE, RETURNING)`,
|
||||||
|
* [UPDATE](https://github.com/go-jet/jet/wiki/UPDATE) `(SET, MODEL, WHERE, RETURNING)`,
|
||||||
|
* [DELETE](https://github.com/go-jet/jet/wiki/DELETE) `(WHERE, ORDER_BY, LIMIT, RETURNING)`,
|
||||||
|
* [LOCK](https://github.com/go-jet/jet/wiki/LOCK) `(IN, NOWAIT)`, `(READ, WRITE)`
|
||||||
|
* [WITH](https://github.com/go-jet/jet/wiki/WITH)
|
||||||
|
|
||||||
|
2) Auto-generated Data Model types - Go types mapped to database type (table, view or enum), used to store
|
||||||
|
result of database queries. Can be combined to create complex query result destination.
|
||||||
|
3) Query execution with result mapping to arbitrary destination.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
To install Jet package, you need to install Go and set your Go workspace first.
|
||||||
|
|
||||||
|
[Go](https://golang.org/) **version 1.18+ is required**
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Use the command bellow to add jet as a dependency into `go.mod` project:
|
||||||
|
```sh
|
||||||
|
$ go get -u github.com/go-jet/jet/v2
|
||||||
|
```
|
||||||
|
|
||||||
|
Jet generator can be installed in one of the following ways:
|
||||||
|
|
||||||
|
- (Go1.16+) Install jet generator using go install:
|
||||||
|
```sh
|
||||||
|
go install github.com/go-jet/jet/v2/cmd/jet@latest
|
||||||
|
```
|
||||||
|
*Jet generator is installed to the directory named by the GOBIN environment variable,
|
||||||
|
which defaults to $GOPATH/bin or $HOME/go/bin if the GOPATH environment variable is not set.*
|
||||||
|
|
||||||
|
- Install jet generator to specific folder:
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/go-jet/jet.git
|
||||||
|
cd jet && go build -o dir_path ./cmd/jet
|
||||||
|
```
|
||||||
|
*Make sure `dir_path` folder is added to the PATH environment variable.*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
For this quick start example we will use PostgreSQL sample _'dvd rental'_ database. Full database dump can be found in
|
||||||
|
[./tests/testdata/init/postgres/dvds.sql](https://github.com/go-jet/jet-test-data/blob/master/init/postgres/dvds.sql).
|
||||||
|
Schema diagram of interest can be found [here](./examples/quick-start/diagram.png).
|
||||||
|
|
||||||
|
#### Generate SQL Builder and Model types
|
||||||
|
To generate jet SQL Builder and Data Model types from running postgres database, we need to call `jet` generator with postgres
|
||||||
|
connection parameters and destination folder path.
|
||||||
|
Assuming we are running local postgres database, with user `user`, user password `pass`, database `jetdb` and
|
||||||
|
schema `dvds` we will use this command:
|
||||||
|
```sh
|
||||||
|
jet -dsn=postgresql://user:pass@localhost:5432/jetdb?sslmode=disable -schema=dvds -path=./.gen
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
Connecting to postgres database: postgresql://user:pass@localhost:5432/jetdb?sslmode=disable
|
||||||
|
Retrieving schema information...
|
||||||
|
FOUND 15 table(s), 7 view(s), 1 enum(s)
|
||||||
|
Cleaning up destination directory...
|
||||||
|
Generating table sql builder files...
|
||||||
|
Generating view sql builder files...
|
||||||
|
Generating enum sql builder files...
|
||||||
|
Generating table model files...
|
||||||
|
Generating view model files...
|
||||||
|
Generating enum model files...
|
||||||
|
Done
|
||||||
|
```
|
||||||
|
Procedure is similar for MySQL, CockroachDB, MariaDB and SQLite. For example:
|
||||||
|
```sh
|
||||||
|
jet -source=mysql -dsn="user:pass@tcp(localhost:3306)/dbname" -path=./.gen
|
||||||
|
jet -dsn=postgres://user:pass@localhost:26257/jetdb?sslmode=disable -schema=dvds -path=./.gen #cockroachdb
|
||||||
|
jet -dsn="mariadb://user:pass@tcp(localhost:3306)/dvds" -path=./.gen # source flag can be omitted if data source appears in dsn
|
||||||
|
jet -source=sqlite -dsn="/path/to/sqlite/database/file" -schema=dvds -path=./.gen
|
||||||
|
jet -dsn="file:///path/to/sqlite/database/file" -schema=dvds -path=./.gen # sqlite database assumed for 'file' data sources
|
||||||
|
```
|
||||||
|
_*User has to have a permission to read information schema tables._
|
||||||
|
|
||||||
|
As command output suggest, Jet will:
|
||||||
|
- connect to postgres database and retrieve information about the _tables_, _views_ and _enums_ of `dvds` schema
|
||||||
|
- delete everything in schema destination folder - `./.gen/jetdb/dvds`,
|
||||||
|
- and finally generate SQL Builder and Model types for each schema table, view and enum.
|
||||||
|
|
||||||
|
|
||||||
|
Generated files folder structure will look like this:
|
||||||
|
```sh
|
||||||
|
|-- .gen # path
|
||||||
|
| -- jetdb # database name
|
||||||
|
| -- dvds # schema name
|
||||||
|
| |-- enum # sql builder package for enums
|
||||||
|
| | |-- mpaa_rating.go
|
||||||
|
| |-- table # sql builder package for tables
|
||||||
|
| |-- actor.go
|
||||||
|
| |-- address.go
|
||||||
|
| |-- category.go
|
||||||
|
| ...
|
||||||
|
| |-- view # sql builder package for views
|
||||||
|
| |-- actor_info.go
|
||||||
|
| |-- film_list.go
|
||||||
|
| ...
|
||||||
|
| |-- model # data model types for each table, view and enum
|
||||||
|
| | |-- actor.go
|
||||||
|
| | |-- address.go
|
||||||
|
| | |-- mpaa_rating.go
|
||||||
|
| | ...
|
||||||
|
```
|
||||||
|
Types from `table`, `view` and `enum` are used to write type safe SQL in Go, and `model` types are combined to store
|
||||||
|
results of the SQL queries.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Let's write some SQL queries in Go
|
||||||
|
|
||||||
|
First we need to import postgres SQLBuilder and generated packages from the previous step:
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
// dot import so go code would resemble as much as native SQL
|
||||||
|
// dot import is not mandatory
|
||||||
|
. "github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/table"
|
||||||
|
. "github.com/go-jet/jet/v2/postgres"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/model"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Let's say we want to retrieve the list of all _actors_ that acted in _films_ longer than 180 minutes, _film language_ is 'English'
|
||||||
|
and _film category_ is not 'Action'.
|
||||||
|
```golang
|
||||||
|
stmt := SELECT(
|
||||||
|
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate, // or just Actor.AllColumns
|
||||||
|
Film.AllColumns,
|
||||||
|
Language.AllColumns.Except(Language.LastUpdate), // all language columns except last_update
|
||||||
|
Category.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Actor.
|
||||||
|
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).
|
||||||
|
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).
|
||||||
|
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
|
||||||
|
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
|
||||||
|
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
|
||||||
|
).WHERE(
|
||||||
|
Language.Name.EQ(Char(20)("English")).
|
||||||
|
AND(Category.Name.NOT_EQ(Text("Action"))).
|
||||||
|
AND(Film.Length.GT(Int32(180))),
|
||||||
|
).ORDER_BY(
|
||||||
|
Actor.ActorID.ASC(),
|
||||||
|
Film.FilmID.ASC(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
_Package(dot) import is used, so the statements look as close as possible to the native SQL._
|
||||||
|
Note that every column has a type. String column `Language.Name` and `Category.Name` can be compared only with
|
||||||
|
string columns and expressions. `Actor.ActorID`, `FilmActor.ActorID`, `Film.Length` are integer columns
|
||||||
|
and can be compared only with integer columns and expressions.
|
||||||
|
|
||||||
|
__How to get a parametrized SQL query from the statement?__
|
||||||
|
```go
|
||||||
|
query, args := stmt.Sql()
|
||||||
|
```
|
||||||
|
query - parametrized query
|
||||||
|
args - query parameters
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to see `query` and `args`</summary>
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT actor.actor_id AS "actor.actor_id",
|
||||||
|
actor.first_name AS "actor.first_name",
|
||||||
|
actor.last_name AS "actor.last_name",
|
||||||
|
actor.last_update AS "actor.last_update",
|
||||||
|
film.film_id AS "film.film_id",
|
||||||
|
film.title AS "film.title",
|
||||||
|
film.description AS "film.description",
|
||||||
|
film.release_year AS "film.release_year",
|
||||||
|
film.language_id AS "film.language_id",
|
||||||
|
film.rental_duration AS "film.rental_duration",
|
||||||
|
film.rental_rate AS "film.rental_rate",
|
||||||
|
film.length AS "film.length",
|
||||||
|
film.replacement_cost AS "film.replacement_cost",
|
||||||
|
film.rating AS "film.rating",
|
||||||
|
film.last_update AS "film.last_update",
|
||||||
|
film.special_features AS "film.special_features",
|
||||||
|
film.fulltext AS "film.fulltext",
|
||||||
|
language.language_id AS "language.language_id",
|
||||||
|
language.name AS "language.name",
|
||||||
|
language.last_update AS "language.last_update",
|
||||||
|
category.category_id AS "category.category_id",
|
||||||
|
category.name AS "category.name",
|
||||||
|
category.last_update AS "category.last_update"
|
||||||
|
FROM dvds.actor
|
||||||
|
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
|
||||||
|
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
|
||||||
|
INNER JOIN dvds.language ON (language.language_id = film.language_id)
|
||||||
|
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
|
||||||
|
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
|
||||||
|
WHERE ((language.name = $1::char(20)) AND (category.name != $2::text)) AND (film.length > $3::integer)
|
||||||
|
ORDER BY actor.actor_id ASC, film.film_id ASC;
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
[English Action 180]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
__How to get debug SQL from statement?__
|
||||||
|
```go
|
||||||
|
debugSql := stmt.DebugSql()
|
||||||
|
```
|
||||||
|
debugSql - this query string can be copy-pasted to sql editor and executed. __It is not intended to be used in production. For debug purposes only!!!__
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to see debug sql</summary>
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT actor.actor_id AS "actor.actor_id",
|
||||||
|
actor.first_name AS "actor.first_name",
|
||||||
|
actor.last_name AS "actor.last_name",
|
||||||
|
actor.last_update AS "actor.last_update",
|
||||||
|
film.film_id AS "film.film_id",
|
||||||
|
film.title AS "film.title",
|
||||||
|
film.description AS "film.description",
|
||||||
|
film.release_year AS "film.release_year",
|
||||||
|
film.language_id AS "film.language_id",
|
||||||
|
film.rental_duration AS "film.rental_duration",
|
||||||
|
film.rental_rate AS "film.rental_rate",
|
||||||
|
film.length AS "film.length",
|
||||||
|
film.replacement_cost AS "film.replacement_cost",
|
||||||
|
film.rating AS "film.rating",
|
||||||
|
film.last_update AS "film.last_update",
|
||||||
|
film.special_features AS "film.special_features",
|
||||||
|
film.fulltext AS "film.fulltext",
|
||||||
|
language.language_id AS "language.language_id",
|
||||||
|
language.name AS "language.name",
|
||||||
|
language.last_update AS "language.last_update",
|
||||||
|
category.category_id AS "category.category_id",
|
||||||
|
category.name AS "category.name",
|
||||||
|
category.last_update AS "category.last_update"
|
||||||
|
FROM dvds.actor
|
||||||
|
INNER JOIN dvds.film_actor ON (actor.actor_id = film_actor.actor_id)
|
||||||
|
INNER JOIN dvds.film ON (film.film_id = film_actor.film_id)
|
||||||
|
INNER JOIN dvds.language ON (language.language_id = film.language_id)
|
||||||
|
INNER JOIN dvds.film_category ON (film_category.film_id = film.film_id)
|
||||||
|
INNER JOIN dvds.category ON (category.category_id = film_category.category_id)
|
||||||
|
WHERE ((language.name = 'English'::char(20)) AND (category.name != 'Action'::text)) AND (film.length > 180::integer)
|
||||||
|
ORDER BY actor.actor_id ASC, film.film_id ASC;
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
#### Execute query and store result
|
||||||
|
|
||||||
|
Well-formed SQL is just a first half of the job. Let's see how can we make some sense of result set returned executing
|
||||||
|
above statement. Usually this is the most complex and tedious work, but with Jet it is the easiest.
|
||||||
|
|
||||||
|
First we have to create desired structure to store query result.
|
||||||
|
This is done be combining autogenerated model types, or it can be done
|
||||||
|
by combining custom model types(see [wiki](https://github.com/go-jet/jet/wiki/Query-Result-Mapping-(QRM)#custom-model-types) for more information).
|
||||||
|
|
||||||
|
_Note that it's possible to overwrite default jet generator behavior. All the aspects of generated model and SQLBuilder types can be
|
||||||
|
tailor-made([wiki](https://github.com/go-jet/jet/wiki/Generator#generator-customization))._
|
||||||
|
|
||||||
|
Let's say this is our desired structure made of autogenerated types:
|
||||||
|
```go
|
||||||
|
var dest []struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
Films []struct {
|
||||||
|
model.Film
|
||||||
|
|
||||||
|
Language model.Language
|
||||||
|
Categories []model.Category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`Films` field is a slice because one actor can act in multiple films, and because each film belongs to one language
|
||||||
|
`Langauge` field is just a single model struct. `Film` can belong to multiple categories.
|
||||||
|
_*There is no limitation of how big or nested destination can be._
|
||||||
|
|
||||||
|
Now let's execute above statement on open database connection (or transaction) db and store result into `dest`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := stmt.Query(db, &dest)
|
||||||
|
handleError(err)
|
||||||
|
```
|
||||||
|
|
||||||
|
__And that's it.__
|
||||||
|
|
||||||
|
`dest` now contains the list of all actors(with list of films acted, where each film has information about language and list of belonging categories) that acted in films longer than 180 minutes, film language is 'English'
|
||||||
|
and film category is not 'Action'.
|
||||||
|
|
||||||
|
Lets print `dest` as a json to see:
|
||||||
|
```go
|
||||||
|
jsonText, _ := json.MarshalIndent(dest, "", "\t")
|
||||||
|
fmt.Println(string(jsonText))
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ActorID": 1,
|
||||||
|
"FirstName": "Penelope",
|
||||||
|
"LastName": "Guiness",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z",
|
||||||
|
"Films": [
|
||||||
|
{
|
||||||
|
"FilmID": 499,
|
||||||
|
"Title": "King Evolution",
|
||||||
|
"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"RentalDuration": 3,
|
||||||
|
"RentalRate": 4.99,
|
||||||
|
"Length": 184,
|
||||||
|
"ReplacementCost": 24.99,
|
||||||
|
"Rating": "NC-17",
|
||||||
|
"LastUpdate": "2013-05-26T14:50:58.951Z",
|
||||||
|
"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}",
|
||||||
|
"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7",
|
||||||
|
"Language": {
|
||||||
|
"LanguageID": 1,
|
||||||
|
"Name": "English ",
|
||||||
|
"LastUpdate": "0001-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
{
|
||||||
|
"CategoryID": 8,
|
||||||
|
"Name": "Family",
|
||||||
|
"LastUpdate": "2006-02-15T09:46:27Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 3,
|
||||||
|
"FirstName": "Ed",
|
||||||
|
"LastName": "Chase",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z",
|
||||||
|
"Films": [
|
||||||
|
{
|
||||||
|
"FilmID": 996,
|
||||||
|
"Title": "Young Language",
|
||||||
|
"Description": "A Unbelieveable Yarn of a Boat And a Database Administrator who must Meet a Boy in The First Manned Space Station",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"RentalDuration": 6,
|
||||||
|
"RentalRate": 0.99,
|
||||||
|
"Length": 183,
|
||||||
|
"ReplacementCost": 9.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"LastUpdate": "2013-05-26T14:50:58.951Z",
|
||||||
|
"SpecialFeatures": "{Trailers,\"Behind the Scenes\"}",
|
||||||
|
"Fulltext": "'administr':12 'boat':8 'boy':17 'databas':11 'first':20 'languag':2 'man':21 'meet':15 'must':14 'space':22 'station':23 'unbeliev':4 'yarn':5 'young':1",
|
||||||
|
"Language": {
|
||||||
|
"LanguageID": 1,
|
||||||
|
"Name": "English ",
|
||||||
|
"LastUpdate": "0001-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
{
|
||||||
|
"CategoryID": 6,
|
||||||
|
"Name": "Documentary",
|
||||||
|
"LastUpdate": "2006-02-15T09:46:27Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
//...(125 more items)
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
What if, we also want to have list of films per category and actors per category, where films are longer than 180 minutes, film language is 'English'
|
||||||
|
and film category is not 'Action'.
|
||||||
|
In that case we can reuse above statement `stmt`, and just change our destination:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var dest2 []struct {
|
||||||
|
model.Category
|
||||||
|
|
||||||
|
Films []model.Film
|
||||||
|
Actors []model.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stmt.Query(db, &dest2)
|
||||||
|
handleError(err)
|
||||||
|
```
|
||||||
|
<details>
|
||||||
|
<summary>Click to see `dest2` json</summary>
|
||||||
|
|
||||||
|
```js
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"CategoryID": 8,
|
||||||
|
"Name": "Family",
|
||||||
|
"LastUpdate": "2006-02-15T09:46:27Z",
|
||||||
|
"Films": [
|
||||||
|
{
|
||||||
|
"FilmID": 499,
|
||||||
|
"Title": "King Evolution",
|
||||||
|
"Description": "A Action-Packed Tale of a Boy And a Lumberjack who must Chase a Madman in A Baloon",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"RentalDuration": 3,
|
||||||
|
"RentalRate": 4.99,
|
||||||
|
"Length": 184,
|
||||||
|
"ReplacementCost": 24.99,
|
||||||
|
"Rating": "NC-17",
|
||||||
|
"LastUpdate": "2013-05-26T14:50:58.951Z",
|
||||||
|
"SpecialFeatures": "{Trailers,\"Deleted Scenes\",\"Behind the Scenes\"}",
|
||||||
|
"Fulltext": "'action':5 'action-pack':4 'baloon':21 'boy':10 'chase':16 'evolut':2 'king':1 'lumberjack':13 'madman':18 'must':15 'pack':6 'tale':7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FilmID": 50,
|
||||||
|
"Title": "Baked Cleopatra",
|
||||||
|
"Description": "A Stunning Drama of a Forensic Psychologist And a Husband who must Overcome a Waitress in A Monastery",
|
||||||
|
"ReleaseYear": 2006,
|
||||||
|
"LanguageID": 1,
|
||||||
|
"RentalDuration": 3,
|
||||||
|
"RentalRate": 2.99,
|
||||||
|
"Length": 182,
|
||||||
|
"ReplacementCost": 20.99,
|
||||||
|
"Rating": "G",
|
||||||
|
"LastUpdate": "2013-05-26T14:50:58.951Z",
|
||||||
|
"SpecialFeatures": "{Commentaries,\"Behind the Scenes\"}",
|
||||||
|
"Fulltext": "'bake':1 'cleopatra':2 'drama':5 'forens':8 'husband':12 'monasteri':20 'must':14 'overcom':15 'psychologist':9 'stun':4 'waitress':17"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Actors": [
|
||||||
|
{
|
||||||
|
"ActorID": 1,
|
||||||
|
"FirstName": "Penelope",
|
||||||
|
"LastName": "Guiness",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 20,
|
||||||
|
"FirstName": "Lucille",
|
||||||
|
"LastName": "Tracy",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 36,
|
||||||
|
"FirstName": "Burt",
|
||||||
|
"LastName": "Dukakis",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 70,
|
||||||
|
"FirstName": "Michelle",
|
||||||
|
"LastName": "Mcconaughey",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 118,
|
||||||
|
"FirstName": "Cuba",
|
||||||
|
"LastName": "Allen",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 187,
|
||||||
|
"FirstName": "Renee",
|
||||||
|
"LastName": "Ball",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 198,
|
||||||
|
"FirstName": "Mary",
|
||||||
|
"LastName": "Keitel",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
//...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Complete code example can be found at [./examples/quick-start/quick-start.go](./examples/quick-start/quick-start.go)
|
||||||
|
|
||||||
|
|
||||||
|
This example represent probably the most common use case. Detail info about additional statements, features and use cases can be
|
||||||
|
found at project [Wiki](https://github.com/go-jet/jet/wiki) page.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
What are the benefits of writing SQL in Go using Jet?
|
||||||
|
The biggest benefit is speed. Speed is being improved in 3 major areas:
|
||||||
|
|
||||||
|
##### Speed of development
|
||||||
|
|
||||||
|
Writing SQL queries is faster and easier, as developers will have help of SQL code completion and SQL type safety directly from Go code.
|
||||||
|
Automatic scan to arbitrary structure removes a lot of headache and boilerplate code needed to structure database query result.
|
||||||
|
|
||||||
|
##### Speed of execution
|
||||||
|
|
||||||
|
While ORM libraries can introduce significant performance penalties due to number of round-trips to the database(N+1 query problem),
|
||||||
|
`jet` will always perform better as developers can write complex query and retrieve result with a single database call.
|
||||||
|
Thus handler time lost on latency between server and database can be constant. Handler execution will be proportional
|
||||||
|
only to the query complexity and the number of rows returned from database.
|
||||||
|
|
||||||
|
With Jet, it is even possible to join the whole database and store the whole structured result in one database call.
|
||||||
|
This is exactly what is being done in one of the tests: [TestJoinEverything](https://github.com/go-jet/jet/blob/6706f4b228f51cf810129f57ba90bbdb60b85fe7/tests/postgres/chinook_db_test.go#L187).
|
||||||
|
The whole test database is joined and query result(~10,000 rows) is stored in a structured variable in less than 0.5s.
|
||||||
|
|
||||||
|
##### How quickly bugs are found
|
||||||
|
|
||||||
|
The most expensive bugs are the one discovered on the production, and the least expensive are those found during development.
|
||||||
|
With automatically generated type safe SQL, not only queries are written faster but bugs are found sooner.
|
||||||
|
Let's return to quick start example, and take closer look at a line:
|
||||||
|
```go
|
||||||
|
AND(Film.Length.GT(Int32(180))),
|
||||||
|
```
|
||||||
|
Let's say someone changes column `length` to `duration` from `film` table. The next go build will fail at that line, and
|
||||||
|
the bug will be caught at compile time.
|
||||||
|
|
||||||
|
Let's say someone changes the type of `length` column to some non-integer type. Build will also fail at the same line
|
||||||
|
because integer columns and expressions can be only compared to other integer columns and expressions.
|
||||||
|
|
||||||
|
Build will also fail if someone removes `length` column from `film` table. `Film` field will be omitted from SQL Builder and Model types,
|
||||||
|
next time `jet` generator is run.
|
||||||
|
|
||||||
|
Without Jet these bugs will have to be either caught by tests or by manual testing.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
At the moment Jet dependence only of:
|
||||||
|
- `github.com/lib/pq` _(Used by jet generator to read `PostgreSQL` database information)_
|
||||||
|
- `github.com/go-sql-driver/mysql` _(Used by jet generator to read `MySQL` and `MariaDB` database information)_
|
||||||
|
- `github.com/mattn/go-sqlite3` _(Used by jet generator to read `SQLite` database information)_
|
||||||
|
- `github.com/google/uuid` _(Used in data model files and for debug purposes)_
|
||||||
|
|
||||||
|
To run the tests, additional dependencies are required:
|
||||||
|
- `github.com/pkg/profile`
|
||||||
|
- `github.com/stretchr/testify`
|
||||||
|
- `github.com/google/go-cmp`
|
||||||
|
- `github.com/jackc/pgx/v4`
|
||||||
|
- `github.com/shopspring/decimal`
|
||||||
|
- `github.com/volatiletech/null/v8`
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
[SemVer](http://semver.org/) is used for versioning. For the versions available, take a look at the [releases](https://github.com/go-jet/jet/releases).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright 2019-2024 Goran Bjelanovic
|
||||||
|
Licensed under the Apache License, Version 2.0.
|
||||||
1
tools/jet-2.12.0/_config.yml
Normal file
1
tools/jet-2.12.0/_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-tactile
|
||||||
290
tools/jet-2.12.0/cmd/jet/main.go
Normal file
290
tools/jet-2.12.0/cmd/jet/main.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate sh -c "printf 'package main\n\nconst version = \"'%s'\"\n' $(git describe --tags --abbrev=0) > version.go"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/errfmt"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/strslice"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
sqlitegen "github.com/go-jet/jet/v2/generator/sqlite"
|
||||||
|
"github.com/go-jet/jet/v2/generator/template"
|
||||||
|
"github.com/go-jet/jet/v2/internal/jet"
|
||||||
|
"github.com/go-jet/jet/v2/mysql"
|
||||||
|
postgres2 "github.com/go-jet/jet/v2/postgres"
|
||||||
|
"github.com/go-jet/jet/v2/sqlite"
|
||||||
|
|
||||||
|
mysqlgen "github.com/go-jet/jet/v2/generator/mysql"
|
||||||
|
postgresgen "github.com/go-jet/jet/v2/generator/postgres"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
source string
|
||||||
|
|
||||||
|
dsn string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
user string
|
||||||
|
password string
|
||||||
|
sslmode string
|
||||||
|
params string
|
||||||
|
dbName string
|
||||||
|
schemaName string
|
||||||
|
|
||||||
|
ignoreTables string
|
||||||
|
ignoreViews string
|
||||||
|
ignoreEnums string
|
||||||
|
|
||||||
|
destDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&source, "source", "", "Database system name (postgres, mysql, cockroachdb, mariadb or sqlite)")
|
||||||
|
|
||||||
|
flag.StringVar(&dsn, "dsn", "", `Data source name. Unified format for connecting to database.
|
||||||
|
PostgreSQL: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||||
|
Example:
|
||||||
|
postgresql://user:pass@localhost:5432/dbname
|
||||||
|
MySQL: https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html
|
||||||
|
Example:
|
||||||
|
mysql://jet:jet@tcp(localhost:3306)/dvds
|
||||||
|
SQLite: https://www.sqlite.org/c3ref/open.html#urifilenameexamples
|
||||||
|
Example:
|
||||||
|
file://path/to/database/file`)
|
||||||
|
flag.StringVar(&host, "host", "", "Database host path. Used only if dsn is not set. (Example: localhost)")
|
||||||
|
flag.IntVar(&port, "port", 0, "Database port. Used only if dsn is not set.")
|
||||||
|
flag.StringVar(&user, "user", "", "Database user. Used only if dsn is not set.")
|
||||||
|
flag.StringVar(&password, "password", "", "The user’s password. Used only if dsn is not set.")
|
||||||
|
flag.StringVar(&dbName, "dbname", "", "Database name. Used only if dsn is not set.")
|
||||||
|
flag.StringVar(&schemaName, "schema", "public", `Database schema name. (default "public")(PostgreSQL only)`)
|
||||||
|
flag.StringVar(¶ms, "params", "", "Additional connection string parameters(optional). Used only if dsn is not set.")
|
||||||
|
flag.StringVar(&sslmode, "sslmode", "disable", `Whether or not to use SSL. Used only if dsn is not set. (optional)(default "disable")(PostgreSQL only)`)
|
||||||
|
flag.StringVar(&ignoreTables, "ignore-tables", "", `Comma-separated list of tables to ignore`)
|
||||||
|
flag.StringVar(&ignoreViews, "ignore-views", "", `Comma-separated list of views to ignore`)
|
||||||
|
flag.StringVar(&ignoreEnums, "ignore-enums", "", `Comma-separated list of enums to ignore`)
|
||||||
|
|
||||||
|
flag.StringVar(&destDir, "path", "", "Destination dir for files generated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = usage
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if dsn == "" && (source == "" || host == "" || port == 0 || user == "" || dbName == "") {
|
||||||
|
printErrorAndExit("ERROR: required flag(s) missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
source := getSource()
|
||||||
|
ignoreTablesList := parseList(ignoreTables)
|
||||||
|
ignoreViewsList := parseList(ignoreViews)
|
||||||
|
ignoreEnumsList := parseList(ignoreEnums)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch source {
|
||||||
|
case "postgresql", "postgres", "cockroachdb", "cockroach":
|
||||||
|
generatorTemplate := genTemplate(postgres2.Dialect, ignoreTablesList, ignoreViewsList, ignoreEnumsList)
|
||||||
|
|
||||||
|
if dsn != "" {
|
||||||
|
err = postgresgen.GenerateDSN(dsn, schemaName, destDir, generatorTemplate)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn := postgresgen.DBConnection{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
User: user,
|
||||||
|
Password: password,
|
||||||
|
SslMode: sslmode,
|
||||||
|
Params: params,
|
||||||
|
|
||||||
|
DBName: dbName,
|
||||||
|
SchemaName: schemaName,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = postgresgen.Generate(
|
||||||
|
destDir,
|
||||||
|
dbConn,
|
||||||
|
generatorTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
|
case "mysql", "mysqlx", "mariadb":
|
||||||
|
generatorTemplate := genTemplate(mysql.Dialect, ignoreTablesList, ignoreViewsList, ignoreEnumsList)
|
||||||
|
|
||||||
|
if dsn != "" {
|
||||||
|
err = mysqlgen.GenerateDSN(dsn, destDir, generatorTemplate)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn := mysqlgen.DBConnection{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
User: user,
|
||||||
|
Password: password,
|
||||||
|
Params: params,
|
||||||
|
DBName: dbName,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mysqlgen.Generate(
|
||||||
|
destDir,
|
||||||
|
dbConn,
|
||||||
|
generatorTemplate,
|
||||||
|
)
|
||||||
|
case "sqlite":
|
||||||
|
if dsn == "" {
|
||||||
|
printErrorAndExit("ERROR: required -dsn flag missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sqlitegen.GenerateDSN(
|
||||||
|
dsn,
|
||||||
|
destDir,
|
||||||
|
genTemplate(sqlite.Dialect, ignoreTablesList, ignoreViewsList, ignoreEnumsList),
|
||||||
|
)
|
||||||
|
|
||||||
|
case "":
|
||||||
|
printErrorAndExit("ERROR: required -source or -dsn flag missing.")
|
||||||
|
|
||||||
|
default:
|
||||||
|
printErrorAndExit("ERROR: unknown data source " + source + ". Only postgres, mysql, mariadb and sqlite are supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(errfmt.Trace(err))
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Println("Jet generator", version)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Usage:")
|
||||||
|
|
||||||
|
order := []string{
|
||||||
|
"source", "dsn", "host", "port", "user", "password", "dbname", "schema", "params", "sslmode",
|
||||||
|
"path",
|
||||||
|
"ignore-tables", "ignore-views", "ignore-enums",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range order {
|
||||||
|
flagEntry := flag.CommandLine.Lookup(name)
|
||||||
|
fmt.Printf(" -%s\n", flagEntry.Name)
|
||||||
|
fmt.Printf("\t%s\n", flagEntry.Usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(`Example commands:
|
||||||
|
|
||||||
|
$ jet -dsn=postgresql://jet:jet@localhost:5432/jetdb?sslmode=disable -schema=dvds -path=./gen
|
||||||
|
$ jet -dsn=postgres://jet:jet@localhost:26257/jetdb?sslmode=disable -schema=dvds -path=./gen #cockroachdb
|
||||||
|
$ jet -source=postgres -dsn="user=jet password=jet host=localhost port=5432 dbname=jetdb" -schema=dvds -path=./gen
|
||||||
|
$ jet -source=mysql -host=localhost -port=3306 -user=jet -password=jet -dbname=jetdb -path=./gen
|
||||||
|
$ jet -source=sqlite -dsn="file://path/to/sqlite/database/file" -path=./gen
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printErrorAndExit(error string) {
|
||||||
|
fmt.Println("\n", error)
|
||||||
|
fmt.Println()
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSource() string {
|
||||||
|
if source != "" {
|
||||||
|
return strings.TrimSpace(strings.ToLower(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectSchema(dsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectSchema(dsn string) string {
|
||||||
|
match := strings.SplitN(dsn, "://", 2)
|
||||||
|
if len(match) < 2 { // not found
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := match[0]
|
||||||
|
|
||||||
|
if protocol == "file" {
|
||||||
|
return "sqlite"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(match[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseList(list string) []string {
|
||||||
|
ret := strings.Split(list, ",")
|
||||||
|
|
||||||
|
for i := 0; i < len(ret); i++ {
|
||||||
|
ret[i] = strings.ToLower(strings.TrimSpace(ret[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func genTemplate(dialect jet.Dialect, ignoreTables []string, ignoreViews []string, ignoreEnums []string) template.Template {
|
||||||
|
|
||||||
|
shouldSkipTable := func(table metadata.Table) bool {
|
||||||
|
return strslice.Contains(ignoreTables, strings.ToLower(table.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldSkipView := func(view metadata.Table) bool {
|
||||||
|
return strslice.Contains(ignoreViews, strings.ToLower(view.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldSkipEnum := func(enum metadata.Enum) bool {
|
||||||
|
return strslice.Contains(ignoreEnums, strings.ToLower(enum.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.Default(dialect).
|
||||||
|
UseSchema(func(schemaMetaData metadata.Schema) template.Schema {
|
||||||
|
return template.DefaultSchema(schemaMetaData).
|
||||||
|
UseModel(template.DefaultModel().
|
||||||
|
UseTable(func(table metadata.Table) template.TableModel {
|
||||||
|
if shouldSkipTable(table) {
|
||||||
|
return template.TableModel{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultTableModel(table)
|
||||||
|
}).
|
||||||
|
UseView(func(view metadata.Table) template.ViewModel {
|
||||||
|
if shouldSkipView(view) {
|
||||||
|
return template.ViewModel{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultViewModel(view)
|
||||||
|
}).
|
||||||
|
UseEnum(func(enum metadata.Enum) template.EnumModel {
|
||||||
|
if shouldSkipEnum(enum) {
|
||||||
|
return template.EnumModel{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultEnumModel(enum)
|
||||||
|
}),
|
||||||
|
).
|
||||||
|
UseSQLBuilder(template.DefaultSQLBuilder().
|
||||||
|
UseTable(func(table metadata.Table) template.TableSQLBuilder {
|
||||||
|
if shouldSkipTable(table) {
|
||||||
|
return template.TableSQLBuilder{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultTableSQLBuilder(table)
|
||||||
|
}).
|
||||||
|
UseView(func(table metadata.Table) template.ViewSQLBuilder {
|
||||||
|
if shouldSkipView(table) {
|
||||||
|
return template.ViewSQLBuilder{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultViewSQLBuilder(table)
|
||||||
|
}).
|
||||||
|
UseEnum(func(enum metadata.Enum) template.EnumSQLBuilder {
|
||||||
|
if shouldSkipEnum(enum) {
|
||||||
|
return template.EnumSQLBuilder{Skip: true}
|
||||||
|
}
|
||||||
|
return template.DefaultEnumSQLBuilder(enum)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
3
tools/jet-2.12.0/cmd/jet/version.go
Normal file
3
tools/jet-2.12.0/cmd/jet/version.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const version = "v2.11.1"
|
||||||
160
tools/jet-2.12.0/doc.go
Normal file
160
tools/jet-2.12.0/doc.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
Package jet is a complete solution for efficient and high performance database access, consisting of type-safe SQL builder
|
||||||
|
with code generation and automatic query result data mapping.
|
||||||
|
Jet currently supports PostgreSQL, MySQL, MariaDB and SQLite. Future releases will add support for additional databases.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Use the command bellow to add jet as a dependency into go.mod project:
|
||||||
|
|
||||||
|
$ go get -u github.com/go-jet/jet/v2
|
||||||
|
|
||||||
|
Jet generator can be installed in one of the following ways:
|
||||||
|
|
||||||
|
1. (Go1.16+) Install jet generator using go install:
|
||||||
|
go install github.com/go-jet/jet/v2/cmd/jet@latest
|
||||||
|
|
||||||
|
2. Install jet generator to GOPATH/bin folder:
|
||||||
|
cd $GOPATH/src/ && GO111MODULE=off go get -u github.com/go-jet/jet/cmd/jet
|
||||||
|
|
||||||
|
3. Install jet generator into specific folder:
|
||||||
|
git clone https://github.com/go-jet/jet.git
|
||||||
|
cd jet && go build -o dir_path ./cmd/jet
|
||||||
|
|
||||||
|
Make sure that the destination folder is added to the PATH environment variable.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Jet requires already defined database schema(with tables, enums etc), so that jet generator can generate SQL Builder
|
||||||
|
and Model files. File generation is very fast, and can be added as every pre-build step.
|
||||||
|
Sample command:
|
||||||
|
|
||||||
|
jet -dsn=postgresql://user:pass@localhost:5432/jetdb -schema=dvds -path=./.gen
|
||||||
|
|
||||||
|
Before we can write SQL queries in Go, we need to import generated SQL builder and model types:
|
||||||
|
|
||||||
|
import . "some_path/.gen/jetdb/dvds/table"
|
||||||
|
import "some_path/.gen/jetdb/dvds/model"
|
||||||
|
|
||||||
|
To write postgres SQL queries we import:
|
||||||
|
|
||||||
|
. "github.com/go-jet/jet/v2/postgres" // Dot import is used so that Go code resemble as much as native SQL. It is not mandatory.
|
||||||
|
|
||||||
|
Then we can write the SQL query:
|
||||||
|
|
||||||
|
// sub-query
|
||||||
|
rRatingFilms :=
|
||||||
|
SELECT(
|
||||||
|
Film.FilmID,
|
||||||
|
Film.Title,
|
||||||
|
Film.Rating,
|
||||||
|
).FROM(
|
||||||
|
Film,
|
||||||
|
).WHERE(
|
||||||
|
Film.Rating.EQ(enum.FilmRating.R),
|
||||||
|
).AsTable("rFilms")
|
||||||
|
|
||||||
|
// export column from sub-query
|
||||||
|
rFilmID := Film.FilmID.From(rRatingFilms)
|
||||||
|
|
||||||
|
// main-query
|
||||||
|
stmt :=
|
||||||
|
SELECT(
|
||||||
|
Actor.AllColumns,
|
||||||
|
FilmActor.AllColumns,
|
||||||
|
rRatingFilms.AllColumns(),
|
||||||
|
).FROM(
|
||||||
|
rRatingFilms.
|
||||||
|
INNER_JOIN(FilmActor, FilmActor.FilmID.EQ(rFilmID)).
|
||||||
|
INNER_JOIN(Actor, Actor.ActorID.EQ(FilmActor.ActorID)
|
||||||
|
).ORDER_BY(
|
||||||
|
rFilmID,
|
||||||
|
Actor.ActorID,
|
||||||
|
)
|
||||||
|
|
||||||
|
Now we can run the statement and store the result into desired destination:
|
||||||
|
|
||||||
|
var dest []struct {
|
||||||
|
model.Film
|
||||||
|
|
||||||
|
Actors []model.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stmt.Query(db, &dest)
|
||||||
|
|
||||||
|
We can print a statement to see SQL query and arguments sent to postgres server:
|
||||||
|
|
||||||
|
fmt.Println(stmt.Sql())
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
SELECT "rFilms"."film.film_id" AS "film.film_id",
|
||||||
|
"rFilms"."film.title" AS "film.title",
|
||||||
|
"rFilms"."film.rating" AS "film.rating",
|
||||||
|
actor.actor_id AS "actor.actor_id",
|
||||||
|
actor.first_name AS "actor.first_name",
|
||||||
|
actor.last_name AS "actor.last_name",
|
||||||
|
actor.last_update AS "actor.last_update",
|
||||||
|
film_actor.actor_id AS "film_actor.actor_id",
|
||||||
|
film_actor.film_id AS "film_actor.film_id",
|
||||||
|
film_actor.last_update AS "film_actor.last_update"
|
||||||
|
FROM (
|
||||||
|
SELECT film.film_id AS "film.film_id",
|
||||||
|
film.title AS "film.title",
|
||||||
|
film.rating AS "film.rating"
|
||||||
|
FROM dvds.film
|
||||||
|
WHERE film.rating = 'R'
|
||||||
|
) AS "rFilms"
|
||||||
|
INNER JOIN dvds.film_actor ON (film_actor.film_id = "rFilms"."film.film_id")
|
||||||
|
INNER JOIN dvds.actor ON (film_actor.actor_id = actor.actor_id)
|
||||||
|
WHERE "rFilms"."film.film_id" < $1
|
||||||
|
ORDER BY "rFilms"."film.film_id" ASC, actor.actor_id ASC;
|
||||||
|
[50]
|
||||||
|
|
||||||
|
If we print destination as json, we'll get:
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"FilmID": 8,
|
||||||
|
"Title": "Airport Pollock",
|
||||||
|
"Rating": "R",
|
||||||
|
"Actors": [
|
||||||
|
{
|
||||||
|
"ActorID": 55,
|
||||||
|
"FirstName": "Fay",
|
||||||
|
"LastName": "Kilmer",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 96,
|
||||||
|
"FirstName": "Gene",
|
||||||
|
"LastName": "Willis",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"FilmID": 17,
|
||||||
|
"Title": "Alone Trip",
|
||||||
|
"Actors": [
|
||||||
|
{
|
||||||
|
"ActorID": 3,
|
||||||
|
"FirstName": "Ed",
|
||||||
|
"LastName": "Chase",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ActorID": 12,
|
||||||
|
"FirstName": "Karl",
|
||||||
|
"LastName": "Berry",
|
||||||
|
"LastUpdate": "2013-05-26T14:47:57.62Z"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
Detail info about all statements, features and use cases can be
|
||||||
|
found at project wiki page - https://github.com/go-jet/jet/wiki.
|
||||||
|
*/
|
||||||
|
package jet
|
||||||
12
tools/jet-2.12.0/examples/quick-start/README.md
Normal file
12
tools/jet-2.12.0/examples/quick-start/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
# Quick start example
|
||||||
|
|
||||||
|
This package contains sample usage for Jet framework.
|
||||||
|
|
||||||
|
Jet generated files of interest are in `./gen` folder.
|
||||||
|
|
||||||
|
`quick-start.go` - contains code explained at main [README.md](../../README.md#quick-start),
|
||||||
|
with a difference of redirecting json output to files(`dest.json` and `dest2.json`) rather then to a
|
||||||
|
standard output.
|
||||||
|
|
||||||
|
`./gen`, `dest.json` and `dest2.json` - added into git for presentation purposes.
|
||||||
6067
tools/jet-2.12.0/examples/quick-start/dest.json
Normal file
6067
tools/jet-2.12.0/examples/quick-start/dest.json
Normal file
File diff suppressed because it is too large
Load Diff
1769
tools/jet-2.12.0/examples/quick-start/dest2.json
Normal file
1769
tools/jet-2.12.0/examples/quick-start/dest2.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
tools/jet-2.12.0/examples/quick-start/diagram.png
Normal file
BIN
tools/jet-2.12.0/examples/quick-start/diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
118
tools/jet-2.12.0/examples/quick-start/quick-start.go
Normal file
118
tools/jet-2.12.0/examples/quick-start/quick-start.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
|
// dot import so that jet go code would resemble as much as native SQL
|
||||||
|
// dot import is not mandatory
|
||||||
|
. "github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/table"
|
||||||
|
. "github.com/go-jet/jet/v2/postgres"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/examples/quick-start/.gen/jetdb/dvds/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
host = "localhost"
|
||||||
|
port = 5432
|
||||||
|
user = "jet"
|
||||||
|
password = "jet"
|
||||||
|
dbName = "jetdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Connect to database
|
||||||
|
var connectString = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbName)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", connectString)
|
||||||
|
panicOnError(err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Write query
|
||||||
|
stmt := SELECT(
|
||||||
|
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,
|
||||||
|
Film.AllColumns,
|
||||||
|
Language.AllColumns.Except(Language.LastUpdate),
|
||||||
|
Category.AllColumns,
|
||||||
|
).FROM(
|
||||||
|
Actor.
|
||||||
|
INNER_JOIN(FilmActor, Actor.ActorID.EQ(FilmActor.ActorID)).
|
||||||
|
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).
|
||||||
|
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
|
||||||
|
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID)).
|
||||||
|
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
|
||||||
|
).WHERE(
|
||||||
|
Language.Name.EQ(Char(20)("English")).
|
||||||
|
AND(Category.Name.NOT_EQ(Text("Action"))).
|
||||||
|
AND(Film.Length.GT(Int(180))),
|
||||||
|
).ORDER_BY(
|
||||||
|
Actor.ActorID.ASC(),
|
||||||
|
Film.FilmID.ASC(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute query and store result
|
||||||
|
var dest []struct {
|
||||||
|
model.Actor
|
||||||
|
|
||||||
|
Films []struct {
|
||||||
|
model.Film
|
||||||
|
|
||||||
|
Language model.Language
|
||||||
|
Categories []model.Category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stmt.Query(db, &dest)
|
||||||
|
panicOnError(err)
|
||||||
|
|
||||||
|
printStatementInfo(stmt)
|
||||||
|
jsonSave("./dest.json", dest)
|
||||||
|
|
||||||
|
// New Destination
|
||||||
|
|
||||||
|
var dest2 []struct {
|
||||||
|
model.Category
|
||||||
|
|
||||||
|
Films []model.Film
|
||||||
|
Actors []model.Actor
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stmt.Query(db, &dest2)
|
||||||
|
panicOnError(err)
|
||||||
|
|
||||||
|
jsonSave("./dest2.json", dest2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonSave(path string, v interface{}) {
|
||||||
|
jsonText, _ := json.MarshalIndent(v, "", "\t")
|
||||||
|
|
||||||
|
err := os.WriteFile(path, jsonText, 0600)
|
||||||
|
|
||||||
|
panicOnError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatementInfo(stmt SelectStatement) {
|
||||||
|
query, args := stmt.Sql()
|
||||||
|
|
||||||
|
fmt.Println("Parameterized query: ")
|
||||||
|
fmt.Println("==============================")
|
||||||
|
fmt.Println(query)
|
||||||
|
fmt.Println("Arguments: ")
|
||||||
|
fmt.Println(args)
|
||||||
|
|
||||||
|
debugSQL := stmt.DebugSql()
|
||||||
|
|
||||||
|
fmt.Println("\n\nDebug sql: ")
|
||||||
|
fmt.Println("==============================")
|
||||||
|
fmt.Println(debugSQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func panicOnError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
tools/jet-2.12.0/generator/metadata/column_meta_data.go
Normal file
31
tools/jet-2.12.0/generator/metadata/column_meta_data.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
// Column struct
|
||||||
|
type Column struct {
|
||||||
|
Name string `sql:"primary_key"`
|
||||||
|
IsPrimaryKey bool
|
||||||
|
IsNullable bool
|
||||||
|
IsGenerated bool
|
||||||
|
HasDefault bool
|
||||||
|
DataType DataType
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataTypeKind is database type kind(base, enum, user-defined, array)
|
||||||
|
type DataTypeKind string
|
||||||
|
|
||||||
|
// DataTypeKind possible values
|
||||||
|
const (
|
||||||
|
BaseType DataTypeKind = "base"
|
||||||
|
EnumType DataTypeKind = "enum"
|
||||||
|
UserDefinedType DataTypeKind = "user-defined"
|
||||||
|
ArrayType DataTypeKind = "array"
|
||||||
|
RangeType DataTypeKind = "range"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataType contains information about column data type
|
||||||
|
type DataType struct {
|
||||||
|
Name string
|
||||||
|
Kind DataTypeKind
|
||||||
|
IsUnsigned bool
|
||||||
|
}
|
||||||
51
tools/jet-2.12.0/generator/metadata/dialect_query_set.go
Normal file
51
tools/jet-2.12.0/generator/metadata/dialect_query_set.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TableType is type of database table(view or base)
|
||||||
|
type TableType string
|
||||||
|
|
||||||
|
// SQL table types
|
||||||
|
const (
|
||||||
|
BaseTable TableType = "BASE TABLE"
|
||||||
|
ViewTable TableType = "VIEW"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DialectQuerySet is set of methods necessary to retrieve dialect metadata information
|
||||||
|
type DialectQuerySet interface {
|
||||||
|
GetTablesMetaData(db *sql.DB, schemaName string, tableType TableType) ([]Table, error)
|
||||||
|
GetEnumsMetaData(db *sql.DB, schemaName string) ([]Enum, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchema retrieves Schema information from database
|
||||||
|
func GetSchema(db *sql.DB, querySet DialectQuerySet, schemaName string) (Schema, error) {
|
||||||
|
tablesMetaData, err := querySet.GetTablesMetaData(db, schemaName, BaseTable)
|
||||||
|
if err != nil {
|
||||||
|
return Schema{}, fmt.Errorf("failed to get %s tables metadata: %w", schemaName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMetaData, err := querySet.GetTablesMetaData(db, schemaName, ViewTable)
|
||||||
|
if err != nil {
|
||||||
|
return Schema{}, fmt.Errorf("failed to get %s view metadata: %w", schemaName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enumsMetaData, err := querySet.GetEnumsMetaData(db, schemaName)
|
||||||
|
if err != nil {
|
||||||
|
return Schema{}, fmt.Errorf("failed to get %s enum metadata: %w", schemaName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := Schema{
|
||||||
|
Name: schemaName,
|
||||||
|
TablesMetaData: tablesMetaData,
|
||||||
|
ViewsMetaData: viewMetaData,
|
||||||
|
EnumsMetaData: enumsMetaData,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(" FOUND", len(ret.TablesMetaData), "table(s),", len(ret.ViewsMetaData), "view(s),",
|
||||||
|
len(ret.EnumsMetaData), "enum(s)")
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
8
tools/jet-2.12.0/generator/metadata/enum_meta_data.go
Normal file
8
tools/jet-2.12.0/generator/metadata/enum_meta_data.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
// Enum metadata struct
|
||||||
|
type Enum struct {
|
||||||
|
Name string `sql:"primary_key"`
|
||||||
|
Comment string
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
14
tools/jet-2.12.0/generator/metadata/schema_meta_data.go
Normal file
14
tools/jet-2.12.0/generator/metadata/schema_meta_data.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
// Schema struct
|
||||||
|
type Schema struct {
|
||||||
|
Name string
|
||||||
|
TablesMetaData []Table
|
||||||
|
ViewsMetaData []Table
|
||||||
|
EnumsMetaData []Enum
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if schema info does not contain any table, views or enums metadata
|
||||||
|
func (s Schema) IsEmpty() bool {
|
||||||
|
return len(s.TablesMetaData) == 0 && len(s.ViewsMetaData) == 0 && len(s.EnumsMetaData) == 0
|
||||||
|
}
|
||||||
23
tools/jet-2.12.0/generator/metadata/table_meta_data.go
Normal file
23
tools/jet-2.12.0/generator/metadata/table_meta_data.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
// Table metadata struct
|
||||||
|
type Table struct {
|
||||||
|
Name string `sql:"primary_key"`
|
||||||
|
Comment string
|
||||||
|
Columns []Column
|
||||||
|
}
|
||||||
|
|
||||||
|
// MutableColumns returns list of mutable columns for table
|
||||||
|
func (t Table) MutableColumns() []Column {
|
||||||
|
var ret []Column
|
||||||
|
|
||||||
|
for _, column := range t.Columns {
|
||||||
|
if column.IsPrimaryKey || column.IsGenerated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
118
tools/jet-2.12.0/generator/mysql/mysql_generator.go
Normal file
118
tools/jet-2.12.0/generator/mysql/mysql_generator.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/generator/template"
|
||||||
|
"github.com/go-jet/jet/v2/mysql"
|
||||||
|
mysqldr "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mysqlMaxConns = 10
|
||||||
|
|
||||||
|
// DBConnection contains MySQL connection details
|
||||||
|
type DBConnection struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Params string
|
||||||
|
|
||||||
|
DBName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates jet files at destination dir from database connection details
|
||||||
|
func Generate(destDir string, dbConn DBConnection, generatorTemplate ...template.Template) error {
|
||||||
|
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbConn.User, dbConn.Password, dbConn.Host, dbConn.Port, dbConn.DBName)
|
||||||
|
if dbConn.Params != "" {
|
||||||
|
connectionString += "?" + dbConn.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := openConnection(connectionString)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open db connection: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = generate(db, dbConn.DBName, destDir, generatorTemplate...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDSN opens connection via DSN string and does everything what Generate does.
|
||||||
|
func GenerateDSN(dsn, destDir string, templates ...template.Template) error {
|
||||||
|
// Special case for go mysql driver. It does not understand schema,
|
||||||
|
// so we need to trim it before passing to generator
|
||||||
|
// https://github.com/go-sql-driver/mysql#dsn-data-source-name
|
||||||
|
idx := strings.Index(dsn, "://")
|
||||||
|
if idx != -1 {
|
||||||
|
dsn = dsn[idx+len("://"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := mysqldr.ParseDSN(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse DSN: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.DBName == "" {
|
||||||
|
return errors.New("database name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := openConnection(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open db connection: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = generate(db, cfg.DBName, destDir, templates...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openConnection(connectionString string) (*sql.DB, error) {
|
||||||
|
fmt.Println("Connecting to MySQL database...")
|
||||||
|
db, err := sql.Open("mysql", connectionString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open mysql connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(mysqlMaxConns)
|
||||||
|
db.SetMaxIdleConns(mysqlMaxConns)
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(db *sql.DB, dbName, destDir string, templates ...template.Template) error {
|
||||||
|
fmt.Println("Retrieving database information...")
|
||||||
|
// No schemas in MySQL
|
||||||
|
schemaMetaData, err := metadata.GetSchema(db, &mySqlQuerySet{}, dbName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get '%s' database metadata: %w", dbName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
genTemplate := template.Default(mysql.Dialect)
|
||||||
|
if len(templates) > 0 {
|
||||||
|
genTemplate = templates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = template.ProcessSchema(destDir, schemaMetaData, genTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process '%s' database: %w", schemaMetaData.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
91
tools/jet-2.12.0/generator/mysql/query_set.go
Normal file
91
tools/jet-2.12.0/generator/mysql/query_set.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mySqlQuerySet is dialect query set for MySQL
|
||||||
|
type mySqlQuerySet struct{}
|
||||||
|
|
||||||
|
func (m mySqlQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
t.table_name as "table.name",
|
||||||
|
col.COLUMN_NAME AS "column.Name",
|
||||||
|
col.COLUMN_DEFAULT IS NOT NULL as "column.HasDefault",
|
||||||
|
col.IS_NULLABLE = "YES" AS "column.IsNullable",
|
||||||
|
col.COLUMN_COMMENT AS "column.Comment",
|
||||||
|
COALESCE(pk.IsPrimaryKey, 0) AS "column.IsPrimaryKey",
|
||||||
|
IF (col.COLUMN_TYPE = 'tinyint(1)',
|
||||||
|
'boolean',
|
||||||
|
IF (col.DATA_TYPE = 'enum',
|
||||||
|
CONCAT(col.TABLE_NAME, '_', col.COLUMN_NAME),
|
||||||
|
col.DATA_TYPE)
|
||||||
|
) AS "dataType.Name",
|
||||||
|
IF (col.DATA_TYPE = 'enum', 'enum', 'base') AS "dataType.Kind",
|
||||||
|
col.COLUMN_TYPE LIKE '%unsigned%' AS "dataType.IsUnsigned"
|
||||||
|
FROM INFORMATION_SCHEMA.tables AS t
|
||||||
|
INNER JOIN
|
||||||
|
information_schema.columns AS col
|
||||||
|
ON t.table_schema = col.table_schema AND t.table_name = col.table_name
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT k.column_name, 1 AS IsPrimaryKey, k.table_name
|
||||||
|
FROM information_schema.table_constraints t
|
||||||
|
JOIN information_schema.key_column_usage k USING(constraint_name, table_schema, table_name)
|
||||||
|
WHERE t.table_schema = ?
|
||||||
|
AND t.constraint_type = 'PRIMARY KEY'
|
||||||
|
) AS pk ON col.COLUMN_NAME = pk.column_name AND col.table_name = pk.table_name
|
||||||
|
WHERE t.table_schema = ?
|
||||||
|
AND t.table_type = ?
|
||||||
|
ORDER BY
|
||||||
|
t.table_name,
|
||||||
|
col.ordinal_position;
|
||||||
|
`
|
||||||
|
|
||||||
|
var tables []metadata.Table
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, schemaName, tableType}, &tables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query column meta data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mySqlQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
|
||||||
|
query := `
|
||||||
|
SELECT (CASE c.DATA_TYPE WHEN 'enum' then CONCAT(c.TABLE_NAME, '_', c.COLUMN_NAME) ELSE '' END ) as "name",
|
||||||
|
SUBSTRING(c.COLUMN_TYPE,5) as "values"
|
||||||
|
FROM information_schema.columns as c
|
||||||
|
INNER JOIN information_schema.tables as t on (t.table_schema = c.table_schema AND t.table_name = c.table_name)
|
||||||
|
WHERE c.table_schema = ? AND DATA_TYPE = 'enum';
|
||||||
|
`
|
||||||
|
var queryResult []struct {
|
||||||
|
Name string
|
||||||
|
Values string
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName}, &queryResult)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query enums meta data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []metadata.Enum
|
||||||
|
|
||||||
|
for _, result := range queryResult {
|
||||||
|
enumValues := strings.Replace(result.Values[1:len(result.Values)-1], "'", "", -1)
|
||||||
|
|
||||||
|
ret = append(ret, metadata.Enum{
|
||||||
|
Name: result.Name,
|
||||||
|
Values: strings.Split(enumValues, ","),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
93
tools/jet-2.12.0/generator/postgres/postgres_generator.go
Normal file
93
tools/jet-2.12.0/generator/postgres/postgres_generator.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/generator/template"
|
||||||
|
"github.com/go-jet/jet/v2/postgres"
|
||||||
|
"github.com/jackc/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBConnection contains postgres connection details
|
||||||
|
type DBConnection struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
SslMode string
|
||||||
|
Params string
|
||||||
|
|
||||||
|
DBName string
|
||||||
|
SchemaName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates jet files at destination dir from database connection details
|
||||||
|
func Generate(destDir string, dbConn DBConnection, genTemplate ...template.Template) (err error) {
|
||||||
|
dsn := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=%s",
|
||||||
|
url.PathEscape(dbConn.User),
|
||||||
|
url.PathEscape(dbConn.Password),
|
||||||
|
dbConn.Host,
|
||||||
|
strconv.Itoa(dbConn.Port),
|
||||||
|
url.PathEscape(dbConn.DBName),
|
||||||
|
dbConn.SslMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
return GenerateDSN(dsn, dbConn.SchemaName, destDir, genTemplate...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDSN generates jet files using dsn connection string
|
||||||
|
func GenerateDSN(dsn, schema, destDir string, templates ...template.Template) error {
|
||||||
|
cfg, err := pgconn.ParseConfig(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse config: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.Database == "" {
|
||||||
|
return fmt.Errorf("database name is required")
|
||||||
|
}
|
||||||
|
db, err := openConnection(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open db connection: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
fmt.Println("Retrieving schema information...")
|
||||||
|
generatorTemplate := template.Default(postgres.Dialect)
|
||||||
|
if len(templates) > 0 {
|
||||||
|
generatorTemplate = templates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaMetadata, err := metadata.GetSchema(db, &postgresQuerySet{}, schema)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get '%s' schema metadata: %w", schema, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath := path.Join(destDir, cfg.Database)
|
||||||
|
|
||||||
|
err = template.ProcessSchema(dirPath, schemaMetadata, generatorTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate schema %s: %d", schemaMetadata.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openConnection(dsn string) (*sql.DB, error) {
|
||||||
|
fmt.Println("Connecting to postgres database...")
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open db connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
121
tools/jet-2.12.0/generator/postgres/query_set.go
Normal file
121
tools/jet-2.12.0/generator/postgres/query_set.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// postgresQuerySet is dialect query set for PostgreSQL
|
||||||
|
type postgresQuerySet struct{}
|
||||||
|
|
||||||
|
func (p postgresQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
|
||||||
|
query := `
|
||||||
|
SELECT table_name as "table.name", obj_description((quote_ident(table_schema)||'.'||quote_ident(table_name))::regclass) as "table.comment"
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = $1 and table_type = $2
|
||||||
|
ORDER BY table_name;
|
||||||
|
`
|
||||||
|
var tables []metadata.Table
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableType}, &tables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query %s metadata: %w", tableType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add materialized views separately, because materialized views are not part of standard information schema
|
||||||
|
if tableType == metadata.ViewTable {
|
||||||
|
matViewQuery := `
|
||||||
|
select matviewname as "table.name"
|
||||||
|
from pg_matviews
|
||||||
|
where schemaname = $1;
|
||||||
|
`
|
||||||
|
var matViews []metadata.Table
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, matViewQuery, []interface{}{schemaName}, &matViews)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query materialized view metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tables = append(tables, matViews...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].Columns, err = getColumnsMetaData(db, schemaName, tables[i].Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query %s columns metadata: %w", tableType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
|
||||||
|
query := `
|
||||||
|
select
|
||||||
|
attr.attname as "column.Name",
|
||||||
|
col_description(attr.attrelid, attr.attnum) as "column.Comment",
|
||||||
|
exists(
|
||||||
|
select 1
|
||||||
|
from pg_catalog.pg_index indx
|
||||||
|
where attr.attrelid = indx.indrelid and attr.attnum = any(indx.indkey) and indx.indisprimary
|
||||||
|
) as "column.IsPrimaryKey",
|
||||||
|
not attr.attnotnull as "column.isNullable",
|
||||||
|
attr.attgenerated = 's' as "column.isGenerated",
|
||||||
|
attr.atthasdef as "column.hasDefault",
|
||||||
|
(case
|
||||||
|
when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base'
|
||||||
|
when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array'
|
||||||
|
when tp.typtype = 'd' then 'base'
|
||||||
|
when tp.typtype = 'e' then 'enum'
|
||||||
|
when tp.typtype = 'r' then 'range'
|
||||||
|
end) as "dataType.Kind",
|
||||||
|
(case when tp.typtype = 'd' then (select pg_type.typname from pg_catalog.pg_type where pg_type.oid = tp.typbasetype)
|
||||||
|
when tp.typcategory = 'A' then pg_catalog.format_type(attr.atttypid, attr.atttypmod)
|
||||||
|
else tp.typname
|
||||||
|
end) as "dataType.Name",
|
||||||
|
false as "dataType.isUnsigned"
|
||||||
|
from pg_catalog.pg_attribute as attr
|
||||||
|
join pg_catalog.pg_class as cls on cls.oid = attr.attrelid
|
||||||
|
join pg_catalog.pg_namespace as ns on ns.oid = cls.relnamespace
|
||||||
|
join pg_catalog.pg_type as tp on tp.oid = attr.atttypid
|
||||||
|
where
|
||||||
|
ns.nspname = $1 and
|
||||||
|
cls.relname = $2 and
|
||||||
|
not attr.attisdropped and
|
||||||
|
attr.attnum > 0
|
||||||
|
order by
|
||||||
|
attr.attnum;
|
||||||
|
`
|
||||||
|
var columns []metadata.Column
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableName}, &columns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query '%s' columns metadata: %w", tableName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p postgresQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
|
||||||
|
query := `
|
||||||
|
SELECT t.typname as "enum.name",
|
||||||
|
obj_description(t.oid) as "enum.comment",
|
||||||
|
e.enumlabel as "values"
|
||||||
|
FROM pg_catalog.pg_type t
|
||||||
|
JOIN pg_catalog.pg_enum e on t.oid = e.enumtypid
|
||||||
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE n.nspname = $1
|
||||||
|
ORDER BY n.nspname, t.typname, e.enumsortorder;`
|
||||||
|
|
||||||
|
var result []metadata.Enum
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName}, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query enums metadata for schema '%s': %w", schemaName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
122
tools/jet-2.12.0/generator/sqlite/query_set.go
Normal file
122
tools/jet-2.12.0/generator/sqlite/query_set.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/semantic"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sqliteQuerySet is dialect query set for SQLite
|
||||||
|
type sqliteQuerySet struct{}
|
||||||
|
|
||||||
|
func (p sqliteQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
|
||||||
|
query := `
|
||||||
|
SELECT name as "table.name"
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type=? AND name != 'sqlite_sequence'
|
||||||
|
ORDER BY name;
|
||||||
|
`
|
||||||
|
sqlTableType := "table"
|
||||||
|
|
||||||
|
if tableType == metadata.ViewTable {
|
||||||
|
sqlTableType = "view"
|
||||||
|
}
|
||||||
|
|
||||||
|
var tables []metadata.Table
|
||||||
|
|
||||||
|
_, err := qrm.Query(context.Background(), db, query, []interface{}{sqlTableType}, &tables)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query %s metadata: %w", schemaName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tables {
|
||||||
|
tables[i].Columns, err = p.GetTableColumnsMetaData(db, schemaName, tables[i].Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query column metadata: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTableInfoQuery(db *sql.DB) (string, error) {
|
||||||
|
var version string
|
||||||
|
err := db.QueryRow("select sqlite_version();").Scan(&version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get sqlite version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteVersion, err := semantic.VersionFromString(version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("can't parse sqlite version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generated columns were added in version 3.26.0
|
||||||
|
if sqliteVersion.Lt(semantic.Version{Major: 3, Minor: 26, Patch: 0}) {
|
||||||
|
return `select * from pragma_table_info(?);`, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return `select * from pragma_table_xinfo(?);`, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p sqliteQuerySet) GetTableColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
|
||||||
|
|
||||||
|
tableInfoQuery, err := getTableInfoQuery(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnInfos []struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
NotNull int32
|
||||||
|
DfltValue string
|
||||||
|
Pk int32
|
||||||
|
Hidden int32
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = qrm.Query(context.Background(), db, tableInfoQuery, []interface{}{tableName}, &columnInfos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query '%s' column metadata: %w", tableName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns []metadata.Column
|
||||||
|
|
||||||
|
for _, columnInfo := range columnInfos {
|
||||||
|
columnType := strings.TrimSuffix(getColumnType(columnInfo.Type), " GENERATED ALWAYS")
|
||||||
|
isGenerated := columnInfo.Hidden == 2 || columnInfo.Hidden == 3 // stored or virtual column
|
||||||
|
hasDefault := columnInfo.DfltValue != ""
|
||||||
|
|
||||||
|
columns = append(columns, metadata.Column{
|
||||||
|
Name: columnInfo.Name,
|
||||||
|
IsPrimaryKey: columnInfo.Pk != 0,
|
||||||
|
IsNullable: columnInfo.NotNull != 1,
|
||||||
|
IsGenerated: isGenerated,
|
||||||
|
HasDefault: hasDefault,
|
||||||
|
DataType: metadata.DataType{
|
||||||
|
Name: columnType,
|
||||||
|
Kind: metadata.BaseType,
|
||||||
|
IsUnsigned: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// will convert VARCHAR(10) -> VARCHAR, etc...
|
||||||
|
func getColumnType(columnType string) string {
|
||||||
|
return strings.TrimSpace(strings.Split(columnType, "(")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p sqliteQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
37
tools/jet-2.12.0/generator/sqlite/sqlite_generator.go
Normal file
37
tools/jet-2.12.0/generator/sqlite/sqlite_generator.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/generator/template"
|
||||||
|
"github.com/go-jet/jet/v2/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateDSN generates jet files using dsn connection string
|
||||||
|
func GenerateDSN(dsn, destDir string, templates ...template.Template) error {
|
||||||
|
db, err := sql.Open("sqlite3", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open sqlite connection: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
fmt.Println("Retrieving schema information...")
|
||||||
|
|
||||||
|
generatorTemplate := template.Default(sqlite.Dialect)
|
||||||
|
if len(templates) > 0 {
|
||||||
|
generatorTemplate = templates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaMetadata, err := metadata.GetSchema(db, &sqliteQuerySet{}, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query database metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = template.ProcessSchema(destDir, schemaMetadata, generatorTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process database %s: %w", schemaMetadata.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
196
tools/jet-2.12.0/generator/template/file_templates.go
Normal file
196
tools/jet-2.12.0/generator/template/file_templates.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
var autoGenWarningTemplate = `
|
||||||
|
//
|
||||||
|
// Code generated by go-jet DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// WARNING: Changes to this file may cause incorrect behavior
|
||||||
|
// and will be lost if the code is regenerated
|
||||||
|
//
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
var tableSQLBuilderTemplate = `
|
||||||
|
{{define "column-list" -}}
|
||||||
|
{{- range $i, $c := . }}
|
||||||
|
{{- $field := columnField $c}}
|
||||||
|
{{- if gt $i 0 }}, {{end}}{{$field.Name}}Column
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
package {{package}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/{{dialect.PackageName}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
var {{tableTemplate.InstanceName}} = new{{tableTemplate.TypeName}}("{{schemaName}}", "{{.Name}}", "{{tableTemplate.DefaultAlias}}")
|
||||||
|
|
||||||
|
{{golangComment .Comment}}
|
||||||
|
type {{structImplName}} struct {
|
||||||
|
{{dialect.PackageName}}.Table
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
{{- range $i, $c := .Columns}}
|
||||||
|
{{- $field := columnField $c}}
|
||||||
|
{{$field.Name}} {{dialect.PackageName}}.Column{{$field.Type}} {{golangComment .Comment}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
AllColumns {{dialect.PackageName}}.ColumnList
|
||||||
|
MutableColumns {{dialect.PackageName}}.ColumnList
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{tableTemplate.TypeName}} struct {
|
||||||
|
{{structImplName}}
|
||||||
|
|
||||||
|
{{toUpper insertedRowAlias}} {{structImplName}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AS creates new {{tableTemplate.TypeName}} with assigned alias
|
||||||
|
func (a {{tableTemplate.TypeName}}) AS(alias string) *{{tableTemplate.TypeName}} {
|
||||||
|
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName(), alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema creates new {{tableTemplate.TypeName}} with assigned schema name
|
||||||
|
func (a {{tableTemplate.TypeName}}) FromSchema(schemaName string) *{{tableTemplate.TypeName}} {
|
||||||
|
return new{{tableTemplate.TypeName}}(schemaName, a.TableName(), a.Alias())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix creates new {{tableTemplate.TypeName}} with assigned table prefix
|
||||||
|
func (a {{tableTemplate.TypeName}}) WithPrefix(prefix string) *{{tableTemplate.TypeName}} {
|
||||||
|
return new{{tableTemplate.TypeName}}(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSuffix creates new {{tableTemplate.TypeName}} with assigned table suffix
|
||||||
|
func (a {{tableTemplate.TypeName}}) WithSuffix(suffix string) *{{tableTemplate.TypeName}} {
|
||||||
|
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func new{{tableTemplate.TypeName}}(schemaName, tableName, alias string) *{{tableTemplate.TypeName}} {
|
||||||
|
return &{{tableTemplate.TypeName}}{
|
||||||
|
{{structImplName}}: new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias),
|
||||||
|
{{toUpper insertedRowAlias}}: new{{tableTemplate.TypeName}}Impl("", "{{insertedRowAlias}}", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias string) {{structImplName}} {
|
||||||
|
var (
|
||||||
|
{{- range $i, $c := .Columns}}
|
||||||
|
{{- $field := columnField $c}}
|
||||||
|
{{$field.Name}}Column = {{dialect.PackageName}}.{{$field.Type}}Column("{{$c.Name}}")
|
||||||
|
{{- end}}
|
||||||
|
allColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .Columns}} }
|
||||||
|
mutableColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .MutableColumns}} }
|
||||||
|
)
|
||||||
|
|
||||||
|
return {{structImplName}}{
|
||||||
|
Table: {{dialect.PackageName}}.NewTable(schemaName, tableName, alias, allColumns...),
|
||||||
|
|
||||||
|
//Columns
|
||||||
|
{{- range $i, $c := .Columns}}
|
||||||
|
{{- $field := columnField $c}}
|
||||||
|
{{$field.Name}}: {{$field.Name}}Column,
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
AllColumns: allColumns,
|
||||||
|
MutableColumns: mutableColumns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var tableSqlBuilderSetSchemaTemplate = `package {{package}}
|
||||||
|
|
||||||
|
// UseSchema sets a new schema name for all generated {{type}} SQL builder types. It is recommended to invoke
|
||||||
|
// this method only once at the beginning of the program.
|
||||||
|
func UseSchema(schema string) {
|
||||||
|
{{- range .}}
|
||||||
|
{{ .InstanceName }} = {{ .InstanceName }}.FromSchema(schema)
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var tableModelFileTemplate = `package {{package}}
|
||||||
|
|
||||||
|
{{ with modelImports }}
|
||||||
|
import (
|
||||||
|
{{- range .}}
|
||||||
|
"{{.}}"
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{$modelTableTemplate := tableTemplate}}
|
||||||
|
{{golangComment .Comment}}
|
||||||
|
type {{$modelTableTemplate.TypeName}} struct {
|
||||||
|
{{- range .Columns}}
|
||||||
|
{{- $field := structField .}}
|
||||||
|
{{$field.Name}} {{$field.Type.Name}} ` + "{{$field.TagsString}}" + ` {{golangComment .Comment}}
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
var enumSQLBuilderTemplate = `package {{package}}
|
||||||
|
|
||||||
|
import "github.com/go-jet/jet/v2/{{dialect.PackageName}}"
|
||||||
|
|
||||||
|
{{golangComment .Comment}}
|
||||||
|
var {{enumTemplate.InstanceName}} = &struct {
|
||||||
|
{{- range $index, $value := .Values}}
|
||||||
|
{{enumValueName $value}} {{dialect.PackageName}}.StringExpression
|
||||||
|
{{- end}}
|
||||||
|
} {
|
||||||
|
{{- range $index, $value := .Values}}
|
||||||
|
{{enumValueName $value}}: {{dialect.PackageName}}.NewEnumValue("{{$value}}"),
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var enumModelTemplate = `package {{package}}
|
||||||
|
{{- $enumTemplate := enumTemplate}}
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
{{golangComment .Comment}}
|
||||||
|
type {{$enumTemplate.TypeName}} string
|
||||||
|
|
||||||
|
const (
|
||||||
|
{{- range $_, $value := .Values}}
|
||||||
|
{{valueName $value}} {{$enumTemplate.TypeName}} = "{{$value}}"
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
|
||||||
|
var {{$enumTemplate.TypeName}}AllValues = []{{$enumTemplate.TypeName}} {
|
||||||
|
{{- range $_, $value := .Values}}
|
||||||
|
{{valueName $value}},
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *{{$enumTemplate.TypeName}}) Scan(value interface{}) error {
|
||||||
|
var enumValue string
|
||||||
|
switch val := value.(type) {
|
||||||
|
case string:
|
||||||
|
enumValue = val
|
||||||
|
case []byte:
|
||||||
|
enumValue = string(val)
|
||||||
|
default:
|
||||||
|
return errors.New("jet: Invalid scan value for AllTypesEnum enum. Enum value has to be of type string or []byte")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch enumValue {
|
||||||
|
{{- range $_, $value := .Values}}
|
||||||
|
case "{{$value}}":
|
||||||
|
*e = {{valueName $value}}
|
||||||
|
{{- end}}
|
||||||
|
default:
|
||||||
|
return errors.New("jet: Invalid scan value '" + enumValue + "' for {{$enumTemplate.TypeName}} enum")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e {{$enumTemplate.TypeName}}) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
13
tools/jet-2.12.0/generator/template/format.go
Normal file
13
tools/jet-2.12.0/generator/template/format.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// Returns the provided string as golang comment without ascii control characters
|
||||||
|
func formatGolangComment(comment string) string {
|
||||||
|
if len(comment) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format as colang comment and remove ascii control characters from string
|
||||||
|
return "// " + regexp.MustCompile(`[[:cntrl:]]+`).ReplaceAllString(comment, "")
|
||||||
|
}
|
||||||
27
tools/jet-2.12.0/generator/template/format_test.go
Normal file
27
tools/jet-2.12.0/generator/template/format_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_formatGolangComment(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
comment string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "Empty string", args: args{comment: ""}, want: ""},
|
||||||
|
{name: "Non-empty string", args: args{comment: "This is a comment"}, want: "// This is a comment"},
|
||||||
|
{name: "String with control characters", args: args{comment: "This is a comment with control characters \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f and text after"}, want: "// This is a comment with control characters and text after"},
|
||||||
|
{name: "String with escape characters", args: args{comment: "This is a comment with escape characters \n\r\t and text after"}, want: "// This is a comment with escape characters and text after"},
|
||||||
|
{name: "String with unicode characters", args: args{comment: "This is a comment with unicode characters ₲鬼佬℧⇄↻ and text after"}, want: "// This is a comment with unicode characters ₲鬼佬℧⇄↻ and text after"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := formatGolangComment(tt.args.comment); got != tt.want {
|
||||||
|
t.Errorf("formatGoLangComment() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
60
tools/jet-2.12.0/generator/template/generator_template.go
Normal file
60
tools/jet-2.12.0/generator/template/generator_template.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/internal/jet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Template is generator template used for file generation
|
||||||
|
type Template struct {
|
||||||
|
Dialect jet.Dialect
|
||||||
|
Schema func(schemaMetaData metadata.Schema) Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default is default generator template implementation
|
||||||
|
func Default(dialect jet.Dialect) Template {
|
||||||
|
return Template{
|
||||||
|
Dialect: dialect,
|
||||||
|
Schema: DefaultSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSchema replaces current schema generate function with a new implementation and returns new generator template
|
||||||
|
func (t Template) UseSchema(schemaFunc func(schemaMetaData metadata.Schema) Schema) Template {
|
||||||
|
t.Schema = schemaFunc
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema is schema generator template used to generate schema(model and sql builder) files
|
||||||
|
type Schema struct {
|
||||||
|
Path string
|
||||||
|
Model Model
|
||||||
|
SQLBuilder SQLBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePath replaces path and returns new schema template
|
||||||
|
func (s Schema) UsePath(path string) Schema {
|
||||||
|
s.Path = path
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseModel returns new schema template with replaced template for model files generation
|
||||||
|
func (s Schema) UseModel(model Model) Schema {
|
||||||
|
s.Model = model
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSQLBuilder returns new schema with replaced template for sql builder files generation
|
||||||
|
func (s Schema) UseSQLBuilder(sqlBuilder SQLBuilder) Schema {
|
||||||
|
s.SQLBuilder = sqlBuilder
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSchema returns default schema template implementation
|
||||||
|
func DefaultSchema(schemaMetaData metadata.Schema) Schema {
|
||||||
|
return Schema{
|
||||||
|
Path: schemaMetaData.Name,
|
||||||
|
Model: DefaultModel(),
|
||||||
|
SQLBuilder: DefaultSQLBuilder(),
|
||||||
|
}
|
||||||
|
}
|
||||||
340
tools/jet-2.12.0/generator/template/model_template.go
Normal file
340
tools/jet-2.12.0/generator/template/model_template.go
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Model is template for model files generation
|
||||||
|
type Model struct {
|
||||||
|
Skip bool
|
||||||
|
Path string
|
||||||
|
Table func(table metadata.Table) TableModel
|
||||||
|
View func(table metadata.Table) ViewModel
|
||||||
|
Enum func(enum metadata.Enum) EnumModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageName returns package name of model types
|
||||||
|
func (m Model) PackageName() string {
|
||||||
|
return path.Base(m.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePath returns new Model template with replaced file path
|
||||||
|
func (m Model) UsePath(path string) Model {
|
||||||
|
m.Path = path
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTable returns new Model template with replaced template for table model files generation
|
||||||
|
func (m Model) UseTable(tableModelFunc func(table metadata.Table) TableModel) Model {
|
||||||
|
m.Table = tableModelFunc
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseView returns new Model template with replaced template for view model files generation
|
||||||
|
func (m Model) UseView(tableModelFunc func(table metadata.Table) TableModel) Model {
|
||||||
|
m.View = tableModelFunc
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseEnum returns new Model template with replaced template for enum model files generation
|
||||||
|
func (m Model) UseEnum(enumFunc func(enumMetaData metadata.Enum) EnumModel) Model {
|
||||||
|
m.Enum = enumFunc
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultModel returns default Model template implementation
|
||||||
|
func DefaultModel() Model {
|
||||||
|
return Model{
|
||||||
|
Skip: false,
|
||||||
|
Path: "/model",
|
||||||
|
Table: DefaultTableModel,
|
||||||
|
View: DefaultViewModel,
|
||||||
|
Enum: DefaultEnumModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableModel is template for table model files generation
|
||||||
|
type TableModel struct {
|
||||||
|
Skip bool
|
||||||
|
FileName string
|
||||||
|
TypeName string
|
||||||
|
Field func(columnMetaData metadata.Column) TableModelField
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewModel is template for view model files generation
|
||||||
|
type ViewModel = TableModel
|
||||||
|
|
||||||
|
// DefaultViewModel is default view template implementation
|
||||||
|
var DefaultViewModel = DefaultTableModel
|
||||||
|
|
||||||
|
// DefaultTableModel is default table template implementation
|
||||||
|
func DefaultTableModel(tableMetaData metadata.Table) TableModel {
|
||||||
|
return TableModel{
|
||||||
|
FileName: dbidentifier.ToGoFileName(tableMetaData.Name),
|
||||||
|
TypeName: dbidentifier.ToGoIdentifier(tableMetaData.Name),
|
||||||
|
Field: DefaultTableModelField,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFileName returns new TableModel with new file name set
|
||||||
|
func (t TableModel) UseFileName(fileName string) TableModel {
|
||||||
|
t.FileName = fileName
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTypeName returns new TableModel with new type name set
|
||||||
|
func (t TableModel) UseTypeName(typeName string) TableModel {
|
||||||
|
t.TypeName = typeName
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseField returns new TableModel with new TableModelField template function
|
||||||
|
func (t TableModel) UseField(structFieldFunc func(columnMetaData metadata.Column) TableModelField) TableModel {
|
||||||
|
t.Field = structFieldFunc
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTableModelImports(modelType TableModel, tableMetaData metadata.Table) []string {
|
||||||
|
importPaths := map[string]bool{}
|
||||||
|
for _, columnMetaData := range tableMetaData.Columns {
|
||||||
|
field := modelType.Field(columnMetaData)
|
||||||
|
importPath := field.Type.ImportPath
|
||||||
|
|
||||||
|
if importPath != "" {
|
||||||
|
importPaths[importPath] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []string
|
||||||
|
for importPath := range importPaths {
|
||||||
|
ret = append(ret, importPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumModel is template for enum model files generation
|
||||||
|
type EnumModel struct {
|
||||||
|
Skip bool
|
||||||
|
FileName string
|
||||||
|
TypeName string
|
||||||
|
ValueName func(value string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFileName returns new EnumModel with new file name set
|
||||||
|
func (em EnumModel) UseFileName(fileName string) EnumModel {
|
||||||
|
em.FileName = fileName
|
||||||
|
return em
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTypeName returns new EnumModel with new type name set
|
||||||
|
func (em EnumModel) UseTypeName(typeName string) EnumModel {
|
||||||
|
em.TypeName = typeName
|
||||||
|
return em
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEnumModel returns default implementation for EnumModel
|
||||||
|
func DefaultEnumModel(enumMetaData metadata.Enum) EnumModel {
|
||||||
|
typeName := dbidentifier.ToGoIdentifier(enumMetaData.Name)
|
||||||
|
|
||||||
|
return EnumModel{
|
||||||
|
FileName: dbidentifier.ToGoFileName(enumMetaData.Name),
|
||||||
|
TypeName: typeName,
|
||||||
|
ValueName: func(value string) string {
|
||||||
|
return typeName + "_" + dbidentifier.ToGoIdentifier(value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableModelField is template for table model field generation
|
||||||
|
type TableModelField struct {
|
||||||
|
Name string
|
||||||
|
Type Type
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTableModelField returns default TableModelField implementation
|
||||||
|
func DefaultTableModelField(columnMetaData metadata.Column) TableModelField {
|
||||||
|
var tags []string
|
||||||
|
|
||||||
|
if columnMetaData.IsPrimaryKey {
|
||||||
|
tags = append(tags, `sql:"primary_key"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TableModelField{
|
||||||
|
Name: dbidentifier.ToGoIdentifier(columnMetaData.Name),
|
||||||
|
Type: getType(columnMetaData),
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseType returns new TypeModelField with a new field type set
|
||||||
|
func (f TableModelField) UseType(t Type) TableModelField {
|
||||||
|
f.Type = t
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseName returns new TableModelField implementation with new field name set
|
||||||
|
func (f TableModelField) UseName(name string) TableModelField {
|
||||||
|
f.Name = name
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTags returns new TableModelField implementation with additional tags added.
|
||||||
|
func (f TableModelField) UseTags(tags ...string) TableModelField {
|
||||||
|
f.Tags = append(f.Tags, tags...)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagsString returns tags string representation
|
||||||
|
func (f TableModelField) TagsString() string {
|
||||||
|
if len(f.Tags) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("`%s`", strings.Join(f.Tags, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type represents type of the struct field
|
||||||
|
type Type struct {
|
||||||
|
ImportPath string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewType creates new type for dummy object
|
||||||
|
func NewType(dummyObject interface{}) Type {
|
||||||
|
return Type{
|
||||||
|
ImportPath: getImportPath(dummyObject),
|
||||||
|
Name: getTypeName(dummyObject),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTypeName(t interface{}) string {
|
||||||
|
typeStr := reflect.TypeOf(t).String()
|
||||||
|
typeStr = strings.Replace(typeStr, "[]uint8", "[]byte", -1)
|
||||||
|
|
||||||
|
return typeStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImportPath(dummyData interface{}) string {
|
||||||
|
dataType := reflect.TypeOf(dummyData)
|
||||||
|
if dataType.Kind() == reflect.Ptr {
|
||||||
|
return dataType.Elem().PkgPath()
|
||||||
|
}
|
||||||
|
return dataType.PkgPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getType(columnMetadata metadata.Column) Type {
|
||||||
|
userDefinedType := getUserDefinedType(columnMetadata)
|
||||||
|
|
||||||
|
if userDefinedType != "" {
|
||||||
|
if columnMetadata.IsNullable {
|
||||||
|
return Type{Name: "*" + userDefinedType}
|
||||||
|
}
|
||||||
|
return Type{Name: userDefinedType}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewType(getGoType(columnMetadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserDefinedType(column metadata.Column) string {
|
||||||
|
switch column.DataType.Kind {
|
||||||
|
case metadata.EnumType:
|
||||||
|
return dbidentifier.ToGoIdentifier(column.DataType.Name)
|
||||||
|
case metadata.UserDefinedType, metadata.ArrayType:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGoType(column metadata.Column) interface{} {
|
||||||
|
defaultGoType := toGoType(column)
|
||||||
|
|
||||||
|
if column.IsNullable {
|
||||||
|
return reflect.New(reflect.TypeOf(defaultGoType)).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultGoType
|
||||||
|
}
|
||||||
|
|
||||||
|
// toGoType returns model type for column info.
|
||||||
|
func toGoType(column metadata.Column) interface{} {
|
||||||
|
switch strings.ToLower(column.DataType.Name) {
|
||||||
|
case "user-defined", "enum":
|
||||||
|
return ""
|
||||||
|
case "boolean", "bool":
|
||||||
|
return false
|
||||||
|
case "tinyint":
|
||||||
|
if column.DataType.IsUnsigned {
|
||||||
|
return uint8(0)
|
||||||
|
}
|
||||||
|
return int8(0)
|
||||||
|
case "smallint", "int2",
|
||||||
|
"year":
|
||||||
|
if column.DataType.IsUnsigned {
|
||||||
|
return uint16(0)
|
||||||
|
}
|
||||||
|
return int16(0)
|
||||||
|
case "integer", "int4",
|
||||||
|
"mediumint", "int": //MySQL
|
||||||
|
if column.DataType.IsUnsigned {
|
||||||
|
return uint32(0)
|
||||||
|
}
|
||||||
|
return int32(0)
|
||||||
|
case "bigint", "int8":
|
||||||
|
if column.DataType.IsUnsigned {
|
||||||
|
return uint64(0)
|
||||||
|
}
|
||||||
|
return int64(0)
|
||||||
|
case "date",
|
||||||
|
"timestamp without time zone", "timestamp",
|
||||||
|
"timestamp with time zone", "timestamptz",
|
||||||
|
"time without time zone", "time",
|
||||||
|
"time with time zone", "timetz",
|
||||||
|
"datetime": // MySQL
|
||||||
|
return time.Time{}
|
||||||
|
case "bytea",
|
||||||
|
"binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob": //MySQL
|
||||||
|
return []byte("")
|
||||||
|
case "text",
|
||||||
|
"character", "bpchar",
|
||||||
|
"character varying", "varchar", "nvarchar",
|
||||||
|
"tsvector", "bit", "bit varying", "varbit",
|
||||||
|
"money", "json", "jsonb",
|
||||||
|
"xml", "point", "interval", "line", "array",
|
||||||
|
"char", "tinytext", "mediumtext", "longtext": // MySQL
|
||||||
|
return ""
|
||||||
|
case "real", "float4":
|
||||||
|
return float32(0.0)
|
||||||
|
case "numeric", "decimal",
|
||||||
|
"double precision", "float8", "float",
|
||||||
|
"double": // MySQL
|
||||||
|
return float64(0.0)
|
||||||
|
case "uuid":
|
||||||
|
return uuid.UUID{}
|
||||||
|
case "daterange":
|
||||||
|
return pgtype.Daterange{}
|
||||||
|
case "tsrange":
|
||||||
|
return pgtype.Tsrange{}
|
||||||
|
case "tstzrange":
|
||||||
|
return pgtype.Tstzrange{}
|
||||||
|
case "int4range":
|
||||||
|
return pgtype.Int4range{}
|
||||||
|
case "int8range":
|
||||||
|
return pgtype.Int8range{}
|
||||||
|
case "numrange":
|
||||||
|
return pgtype.Numrange{}
|
||||||
|
default:
|
||||||
|
fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
45
tools/jet-2.12.0/generator/template/model_template_test.go
Normal file
45
tools/jet-2.12.0/generator/template/model_template_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_TableModelField(t *testing.T) {
|
||||||
|
require.Equal(t, DefaultTableModelField(metadata.Column{
|
||||||
|
Name: "col_name",
|
||||||
|
IsPrimaryKey: true,
|
||||||
|
IsNullable: true,
|
||||||
|
DataType: metadata.DataType{
|
||||||
|
Name: "smallint",
|
||||||
|
Kind: "base",
|
||||||
|
IsUnsigned: true,
|
||||||
|
},
|
||||||
|
}), TableModelField{
|
||||||
|
Name: "ColName",
|
||||||
|
Type: Type{
|
||||||
|
ImportPath: "",
|
||||||
|
Name: "*uint16",
|
||||||
|
},
|
||||||
|
Tags: []string{"sql:\"primary_key\""},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, DefaultTableModelField(metadata.Column{
|
||||||
|
Name: "time_column_1",
|
||||||
|
IsPrimaryKey: false,
|
||||||
|
IsNullable: true,
|
||||||
|
DataType: metadata.DataType{
|
||||||
|
Name: "timestamp with time zone",
|
||||||
|
Kind: "base",
|
||||||
|
IsUnsigned: false,
|
||||||
|
},
|
||||||
|
}), TableModelField{
|
||||||
|
Name: "TimeColumn1",
|
||||||
|
Type: Type{
|
||||||
|
ImportPath: "time",
|
||||||
|
Name: "*time.Time",
|
||||||
|
},
|
||||||
|
Tags: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
382
tools/jet-2.12.0/generator/template/process.go
Normal file
382
tools/jet-2.12.0/generator/template/process.go
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/filesys"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/internal/jet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessSchema will process schema metadata and constructs go files using generator Template
|
||||||
|
func ProcessSchema(dirPath string, schemaMetaData metadata.Schema, generatorTemplate Template) error {
|
||||||
|
if schemaMetaData.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaTemplate := generatorTemplate.Schema(schemaMetaData)
|
||||||
|
schemaPath := path.Join(dirPath, schemaTemplate.Path)
|
||||||
|
|
||||||
|
fmt.Println("Destination directory:", schemaPath)
|
||||||
|
fmt.Println("Cleaning up destination directory...")
|
||||||
|
err := filesys.RemoveDir(schemaPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to cleanup generated files")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processModel(schemaPath, schemaMetaData, schemaTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate model types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processSQLBuilder(schemaPath, generatorTemplate.Dialect, schemaMetaData, schemaTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate sql builder types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processModel(dirPath string, schemaMetaData metadata.Schema, schemaTemplate Schema) error {
|
||||||
|
modelTemplate := schemaTemplate.Model
|
||||||
|
|
||||||
|
if modelTemplate.Skip {
|
||||||
|
fmt.Println("Skipping the generation of model types.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
modelDirPath := path.Join(dirPath, modelTemplate.Path)
|
||||||
|
|
||||||
|
err := filesys.EnsureDirPathExist(modelDirPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("destination dir path does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processTableModels("table", modelDirPath, schemaMetaData.TablesMetaData, modelTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate table model types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processTableModels("view", modelDirPath, schemaMetaData.ViewsMetaData, modelTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate view model types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processEnumModels(modelDirPath, schemaMetaData.EnumsMetaData, modelTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process enum types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processSQLBuilder(dirPath string, dialect jet.Dialect, schemaMetaData metadata.Schema, schemaTemplate Schema) error {
|
||||||
|
sqlBuilderTemplate := schemaTemplate.SQLBuilder
|
||||||
|
|
||||||
|
if sqlBuilderTemplate.Skip {
|
||||||
|
fmt.Println("Skipping the generation of SQL Builder types.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlBuilderPath := path.Join(dirPath, sqlBuilderTemplate.Path)
|
||||||
|
|
||||||
|
err := processTableSQLBuilder("table", sqlBuilderPath, dialect, schemaMetaData, schemaMetaData.TablesMetaData, sqlBuilderTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process table sql builder types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processTableSQLBuilder("view", sqlBuilderPath, dialect, schemaMetaData, schemaMetaData.ViewsMetaData, sqlBuilderTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process view sql builder types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = processEnumSQLBuilder(sqlBuilderPath, dialect, schemaMetaData.EnumsMetaData, sqlBuilderTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process enum types: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEnumSQLBuilder(dirPath string, dialect jet.Dialect, enumsMetaData []metadata.Enum, sqlBuilder SQLBuilder) error {
|
||||||
|
if len(enumsMetaData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generating enum sql builder files\n")
|
||||||
|
|
||||||
|
for _, enumMetaData := range enumsMetaData {
|
||||||
|
enumTemplate := sqlBuilder.Enum(enumMetaData)
|
||||||
|
|
||||||
|
if enumTemplate.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
enumSQLBuilderPath := path.Join(dirPath, enumTemplate.Path)
|
||||||
|
|
||||||
|
err := filesys.EnsureDirPathExist(enumSQLBuilderPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create enum sql builder directory - %s: %w", enumSQLBuilderPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := generateTemplate(
|
||||||
|
autoGenWarningTemplate+enumSQLBuilderTemplate,
|
||||||
|
enumMetaData,
|
||||||
|
template.FuncMap{
|
||||||
|
"package": func() string {
|
||||||
|
return enumTemplate.PackageName()
|
||||||
|
},
|
||||||
|
"dialect": func() jet.Dialect {
|
||||||
|
return dialect
|
||||||
|
},
|
||||||
|
"enumTemplate": func() EnumSQLBuilder {
|
||||||
|
return enumTemplate
|
||||||
|
},
|
||||||
|
"enumValueName": func(enumValue string) string {
|
||||||
|
return enumTemplate.ValueName(enumValue)
|
||||||
|
},
|
||||||
|
"golangComment": formatGolangComment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generete enum type %s: %w", enumTemplate.FileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesys.FormatAndSaveGoFile(enumSQLBuilderPath, enumTemplate.FileName, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to format and save '%s' enum type : %w", enumTemplate.FileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTableSQLBuilder(fileTypes, dirPath string,
|
||||||
|
dialect jet.Dialect,
|
||||||
|
schemaMetaData metadata.Schema,
|
||||||
|
tablesMetaData []metadata.Table,
|
||||||
|
sqlBuilderTemplate SQLBuilder) error {
|
||||||
|
|
||||||
|
if len(tablesMetaData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Generating %s sql builder files\n", fileTypes)
|
||||||
|
|
||||||
|
var generatedBuilders []TableSQLBuilder
|
||||||
|
|
||||||
|
for _, tableMetaData := range tablesMetaData {
|
||||||
|
var tableSQLBuilder TableSQLBuilder
|
||||||
|
|
||||||
|
if fileTypes == "view" {
|
||||||
|
tableSQLBuilder = sqlBuilderTemplate.View(tableMetaData)
|
||||||
|
} else {
|
||||||
|
tableSQLBuilder = sqlBuilderTemplate.Table(tableMetaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tableSQLBuilder.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilder.Path)
|
||||||
|
|
||||||
|
err := filesys.EnsureDirPathExist(tableSQLBuilderPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create table sql builder directory - %s: %w", tableSQLBuilderPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := generateTemplate(
|
||||||
|
autoGenWarningTemplate+tableSQLBuilderTemplate,
|
||||||
|
tableMetaData,
|
||||||
|
template.FuncMap{
|
||||||
|
"package": func() string {
|
||||||
|
return tableSQLBuilder.PackageName()
|
||||||
|
},
|
||||||
|
"dialect": func() jet.Dialect {
|
||||||
|
return dialect
|
||||||
|
},
|
||||||
|
"schemaName": func() string {
|
||||||
|
return schemaMetaData.Name
|
||||||
|
},
|
||||||
|
"tableTemplate": func() TableSQLBuilder {
|
||||||
|
return tableSQLBuilder
|
||||||
|
},
|
||||||
|
"structImplName": func() string { // postgres only
|
||||||
|
structName := tableSQLBuilder.TypeName
|
||||||
|
return string(strings.ToLower(structName)[0]) + structName[1:]
|
||||||
|
},
|
||||||
|
"columnField": func(columnMetaData metadata.Column) TableSQLBuilderColumn {
|
||||||
|
return tableSQLBuilder.Column(columnMetaData)
|
||||||
|
},
|
||||||
|
"toUpper": strings.ToUpper,
|
||||||
|
"insertedRowAlias": func() string {
|
||||||
|
return insertedRowAlias(dialect)
|
||||||
|
},
|
||||||
|
"golangComment": formatGolangComment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate table sql builder type %s: %w", tableSQLBuilder.TypeName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesys.FormatAndSaveGoFile(tableSQLBuilderPath, tableSQLBuilder.FileName, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to format and save generated sql builder type '%s': %w", tableSQLBuilder.FileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedBuilders = append(generatedBuilders, tableSQLBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := generateUseSchemaFunc(dirPath, fileTypes, generatedBuilders)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate UseSchema function")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUseSchemaFunc(dirPath, fileTypes string, builders []TableSQLBuilder) error {
|
||||||
|
if len(builders) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := generateTemplate(
|
||||||
|
autoGenWarningTemplate+tableSqlBuilderSetSchemaTemplate,
|
||||||
|
builders,
|
||||||
|
template.FuncMap{
|
||||||
|
"package": func() string { return builders[0].PackageName() },
|
||||||
|
"type": func() string { return fileTypes },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate use schema template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath := path.Join(dirPath, builders[0].Path)
|
||||||
|
fileName := fileTypes + "_use_schema"
|
||||||
|
|
||||||
|
err = filesys.FormatAndSaveGoFile(basePath, fileName, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save %s file: %w", fileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertedRowAlias(dialect jet.Dialect) string {
|
||||||
|
if dialect.Name() == "MySQL" {
|
||||||
|
return "new"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "excluded"
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTableModels(fileTypes, modelDirPath string, tablesMetaData []metadata.Table, modelTemplate Model) error {
|
||||||
|
if len(tablesMetaData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf("Generating %s model files...\n", fileTypes)
|
||||||
|
|
||||||
|
for _, tableMetaData := range tablesMetaData {
|
||||||
|
var tableTemplate TableModel
|
||||||
|
|
||||||
|
if fileTypes == "table" {
|
||||||
|
tableTemplate = modelTemplate.Table(tableMetaData)
|
||||||
|
} else {
|
||||||
|
tableTemplate = modelTemplate.View(tableMetaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tableTemplate.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := generateTemplate(
|
||||||
|
autoGenWarningTemplate+tableModelFileTemplate,
|
||||||
|
tableMetaData,
|
||||||
|
template.FuncMap{
|
||||||
|
"package": func() string {
|
||||||
|
return modelTemplate.PackageName()
|
||||||
|
},
|
||||||
|
"modelImports": func() []string {
|
||||||
|
return getTableModelImports(tableTemplate, tableMetaData)
|
||||||
|
},
|
||||||
|
"tableTemplate": func() TableModel {
|
||||||
|
return tableTemplate
|
||||||
|
},
|
||||||
|
"structField": func(columnMetaData metadata.Column) TableModelField {
|
||||||
|
return tableTemplate.Field(columnMetaData)
|
||||||
|
},
|
||||||
|
"golangComment": formatGolangComment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate model type '%s': %w", tableMetaData.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesys.FormatAndSaveGoFile(modelDirPath, tableTemplate.FileName, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save '%s' model type: %w", tableTemplate.FileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEnumModels(modelDir string, enumsMetaData []metadata.Enum, modelTemplate Model) error {
|
||||||
|
if len(enumsMetaData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Print("Generating enum model files...\n")
|
||||||
|
|
||||||
|
for _, enumMetaData := range enumsMetaData {
|
||||||
|
enumTemplate := modelTemplate.Enum(enumMetaData)
|
||||||
|
|
||||||
|
if enumTemplate.Skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := generateTemplate(
|
||||||
|
autoGenWarningTemplate+enumModelTemplate,
|
||||||
|
enumMetaData,
|
||||||
|
template.FuncMap{
|
||||||
|
"package": func() string {
|
||||||
|
return modelTemplate.PackageName()
|
||||||
|
},
|
||||||
|
"enumTemplate": func() EnumModel {
|
||||||
|
return enumTemplate
|
||||||
|
},
|
||||||
|
"valueName": func(value string) string {
|
||||||
|
return enumTemplate.ValueName(value)
|
||||||
|
},
|
||||||
|
"golangComment": formatGolangComment,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate enum type '%s': %w", enumMetaData.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesys.FormatAndSaveGoFile(modelDir, enumTemplate.FileName, text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save '%s' enum type: %w", enumTemplate.FileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTemplate(templateText string, templateData interface{}, funcMap template.FuncMap) ([]byte, error) {
|
||||||
|
t, err := template.New("sqlBuilderTableTemplate").Funcs(funcMap).Parse(templateText)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := t.Execute(&buf, templateData); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
259
tools/jet-2.12.0/generator/template/sql_builder_template.go
Normal file
259
tools/jet-2.12.0/generator/template/sql_builder_template.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLBuilder is template for generating sql builder files
|
||||||
|
type SQLBuilder struct {
|
||||||
|
Skip bool
|
||||||
|
Path string
|
||||||
|
Table func(table metadata.Table) TableSQLBuilder
|
||||||
|
View func(view metadata.Table) TableSQLBuilder
|
||||||
|
Enum func(enum metadata.Enum) EnumSQLBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSQLBuilder returns default SQLBuilder implementation
|
||||||
|
func DefaultSQLBuilder() SQLBuilder {
|
||||||
|
return SQLBuilder{
|
||||||
|
Path: "",
|
||||||
|
Table: DefaultTableSQLBuilder,
|
||||||
|
View: DefaultViewSQLBuilder,
|
||||||
|
Enum: DefaultEnumSQLBuilder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePath returns new SQLBuilder with new relative path set
|
||||||
|
func (sb SQLBuilder) UsePath(path string) SQLBuilder {
|
||||||
|
sb.Path = path
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTable returns new SQLBuilder with new TableSQLBuilder template function set
|
||||||
|
func (sb SQLBuilder) UseTable(tableFunc func(table metadata.Table) TableSQLBuilder) SQLBuilder {
|
||||||
|
sb.Table = tableFunc
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseView returns new SQLBuilder with new ViewSQLBuilder template function set
|
||||||
|
func (sb SQLBuilder) UseView(viewFunc func(table metadata.Table) ViewSQLBuilder) SQLBuilder {
|
||||||
|
sb.View = viewFunc
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseEnum returns new SQLBuilder with new EnumSQLBuilder template function set
|
||||||
|
func (sb SQLBuilder) UseEnum(enumFunc func(enum metadata.Enum) EnumSQLBuilder) SQLBuilder {
|
||||||
|
sb.Enum = enumFunc
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableSQLBuilder is template for generating table SQLBuilder files
|
||||||
|
type TableSQLBuilder struct {
|
||||||
|
Skip bool
|
||||||
|
Path string
|
||||||
|
FileName string
|
||||||
|
InstanceName string
|
||||||
|
TypeName string
|
||||||
|
DefaultAlias string
|
||||||
|
Column func(columnMetaData metadata.Column) TableSQLBuilderColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewSQLBuilder is template for generating view SQLBuilder files
|
||||||
|
type ViewSQLBuilder = TableSQLBuilder
|
||||||
|
|
||||||
|
// DefaultTableSQLBuilder returns default implementation for TableSQLBuilder
|
||||||
|
func DefaultTableSQLBuilder(tableMetaData metadata.Table) TableSQLBuilder {
|
||||||
|
tableNameGoIdentifier := dbidentifier.ToGoIdentifier(tableMetaData.Name)
|
||||||
|
|
||||||
|
return TableSQLBuilder{
|
||||||
|
Path: "/table",
|
||||||
|
FileName: dbidentifier.ToGoFileName(tableMetaData.Name),
|
||||||
|
InstanceName: tableNameGoIdentifier,
|
||||||
|
TypeName: tableNameGoIdentifier + "Table",
|
||||||
|
DefaultAlias: "",
|
||||||
|
Column: DefaultTableSQLBuilderColumn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultViewSQLBuilder returns default implementation for ViewSQLBuilder
|
||||||
|
func DefaultViewSQLBuilder(viewMetaData metadata.Table) ViewSQLBuilder {
|
||||||
|
tableSQLBuilder := DefaultTableSQLBuilder(viewMetaData)
|
||||||
|
tableSQLBuilder.Path = "/view"
|
||||||
|
return tableSQLBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageName returns package name of table sql builder types
|
||||||
|
func (tb TableSQLBuilder) PackageName() string {
|
||||||
|
return path.Base(tb.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePath returns new TableSQLBuilder with new relative path set
|
||||||
|
func (tb TableSQLBuilder) UsePath(path string) TableSQLBuilder {
|
||||||
|
tb.Path = path
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFileName returns new TableSQLBuilder with new file name set
|
||||||
|
func (tb TableSQLBuilder) UseFileName(name string) TableSQLBuilder {
|
||||||
|
tb.FileName = name
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseInstanceName returns new TableSQLBuilder with new instance name set
|
||||||
|
func (tb TableSQLBuilder) UseInstanceName(name string) TableSQLBuilder {
|
||||||
|
tb.InstanceName = name
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTypeName returns new TableSQLBuilder with new type name set
|
||||||
|
func (tb TableSQLBuilder) UseTypeName(name string) TableSQLBuilder {
|
||||||
|
tb.TypeName = name
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseDefaultAlias returns new TableSQLBuilder with new default alias set
|
||||||
|
func (tb TableSQLBuilder) UseDefaultAlias(defaultAlias string) TableSQLBuilder {
|
||||||
|
tb.DefaultAlias = defaultAlias
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseColumn returns new TableSQLBuilder with new column template function set
|
||||||
|
func (tb TableSQLBuilder) UseColumn(columnsFunc func(column metadata.Column) TableSQLBuilderColumn) TableSQLBuilder {
|
||||||
|
tb.Column = columnsFunc
|
||||||
|
return tb
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableSQLBuilderColumn is template for table sql builder column
|
||||||
|
type TableSQLBuilderColumn struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
var reservedKeywords = []string{"TableName", "Table", "SchemaName", "Alias", "AllColumns", "MutableColumns"}
|
||||||
|
|
||||||
|
func renameIfReserved(name string) string {
|
||||||
|
if slices.Contains(reservedKeywords, name) {
|
||||||
|
return name + "_"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTableSQLBuilderColumn returns default implementation of TableSQLBuilderColumn
|
||||||
|
func DefaultTableSQLBuilderColumn(columnMetaData metadata.Column) TableSQLBuilderColumn {
|
||||||
|
return TableSQLBuilderColumn{
|
||||||
|
Name: renameIfReserved(dbidentifier.ToGoIdentifier(columnMetaData.Name)),
|
||||||
|
Type: getSqlBuilderColumnType(columnMetaData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSqlBuilderColumnType returns type of jet sql builder column
|
||||||
|
func getSqlBuilderColumnType(columnMetaData metadata.Column) string {
|
||||||
|
if columnMetaData.DataType.Kind != metadata.BaseType &&
|
||||||
|
columnMetaData.DataType.Kind != metadata.RangeType {
|
||||||
|
return "String"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(columnMetaData.DataType.Name) {
|
||||||
|
case "boolean", "bool":
|
||||||
|
return "Bool"
|
||||||
|
case "smallint", "integer", "bigint", "int2", "int4", "int8",
|
||||||
|
"tinyint", "mediumint", "int", "year": //MySQL
|
||||||
|
return "Integer"
|
||||||
|
case "date":
|
||||||
|
return "Date"
|
||||||
|
case "timestamp without time zone",
|
||||||
|
"timestamp", "datetime": //MySQL:
|
||||||
|
return "Timestamp"
|
||||||
|
case "timestamp with time zone", "timestamptz":
|
||||||
|
return "Timestampz"
|
||||||
|
case "time without time zone",
|
||||||
|
"time": //MySQL
|
||||||
|
return "Time"
|
||||||
|
case "time with time zone", "timetz":
|
||||||
|
return "Timez"
|
||||||
|
case "interval":
|
||||||
|
return "Interval"
|
||||||
|
case "user-defined", "enum", "text", "character", "character varying", "bytea", "uuid",
|
||||||
|
"tsvector", "bit", "bit varying", "money", "json", "jsonb", "xml", "point", "line", "ARRAY",
|
||||||
|
"char", "varchar", "nvarchar", "binary", "varbinary", "bpchar", "varbit",
|
||||||
|
"tinyblob", "blob", "mediumblob", "longblob", "tinytext", "mediumtext", "longtext": // MySQL
|
||||||
|
return "String"
|
||||||
|
case "real", "numeric", "decimal", "double precision", "float", "float4", "float8",
|
||||||
|
"double": // MySQL
|
||||||
|
return "Float"
|
||||||
|
case "daterange":
|
||||||
|
return "DateRange"
|
||||||
|
case "tsrange":
|
||||||
|
return "TimestampRange"
|
||||||
|
case "tstzrange":
|
||||||
|
return "TimestampzRange"
|
||||||
|
case "int4range":
|
||||||
|
return "Int4Range"
|
||||||
|
case "int8range":
|
||||||
|
return "Int8Range"
|
||||||
|
case "numrange":
|
||||||
|
return "NumericRange"
|
||||||
|
default:
|
||||||
|
fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
|
||||||
|
return "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumSQLBuilder is template for generating enum SQLBuilder files
|
||||||
|
type EnumSQLBuilder struct {
|
||||||
|
Skip bool
|
||||||
|
Path string
|
||||||
|
FileName string
|
||||||
|
InstanceName string
|
||||||
|
ValueName func(enumValue string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEnumSQLBuilder returns default implementation of EnumSQLBuilder
|
||||||
|
func DefaultEnumSQLBuilder(enumMetaData metadata.Enum) EnumSQLBuilder {
|
||||||
|
return EnumSQLBuilder{
|
||||||
|
Path: "/enum",
|
||||||
|
FileName: dbidentifier.ToGoFileName(enumMetaData.Name),
|
||||||
|
InstanceName: dbidentifier.ToGoIdentifier(enumMetaData.Name),
|
||||||
|
ValueName: func(enumValue string) string {
|
||||||
|
return defaultEnumValueName(enumMetaData.Name, enumValue)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageName returns enum sql builder package name
|
||||||
|
func (e EnumSQLBuilder) PackageName() string {
|
||||||
|
return path.Base(e.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsePath returns new EnumSQLBuilder with new path set
|
||||||
|
func (e EnumSQLBuilder) UsePath(path string) EnumSQLBuilder {
|
||||||
|
e.Path = path
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFileName returns new EnumSQLBuilder with new file name set
|
||||||
|
func (e EnumSQLBuilder) UseFileName(name string) EnumSQLBuilder {
|
||||||
|
e.FileName = name
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseInstanceName returns new EnumSQLBuilder with instance name set
|
||||||
|
func (e EnumSQLBuilder) UseInstanceName(name string) EnumSQLBuilder {
|
||||||
|
e.InstanceName = name
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultEnumValueName(enumName, enumValue string) string {
|
||||||
|
enumValueName := dbidentifier.ToGoIdentifier(enumValue)
|
||||||
|
if !unicode.IsLetter([]rune(enumValueName)[0]) {
|
||||||
|
return dbidentifier.ToGoIdentifier(enumName) + enumValueName
|
||||||
|
}
|
||||||
|
|
||||||
|
return enumValueName
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-jet/jet/v2/generator/metadata"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToGoEnumValueIdentifier(t *testing.T) {
|
||||||
|
require.Equal(t, defaultEnumValueName("enum_name", "enum_value"), "EnumValue")
|
||||||
|
require.Equal(t, defaultEnumValueName("NumEnum", "100"), "NumEnum100")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnRenameReserved(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
col string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{col: "TableName", want: "TableName_"},
|
||||||
|
{col: "Table", want: "Table_"},
|
||||||
|
{col: "SchemaName", want: "SchemaName_"},
|
||||||
|
{col: "Alias", want: "Alias_"},
|
||||||
|
{col: "AllColumns", want: "AllColumns_"},
|
||||||
|
{col: "MutableColumns", want: "MutableColumns_"},
|
||||||
|
{col: "OtherColumn", want: "OtherColumn"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.col, func(t *testing.T) {
|
||||||
|
builder := DefaultTableSQLBuilderColumn(metadata.Column{
|
||||||
|
Name: tt.col,
|
||||||
|
})
|
||||||
|
require.Equal(t, builder.Name, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
46
tools/jet-2.12.0/go.mod
Normal file
46
tools/jet-2.12.0/go.mod
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
module github.com/go-jet/jet/v2
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
// used by jet generator
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgconn v1.14.3
|
||||||
|
github.com/jackc/pgtype v1.14.4
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
)
|
||||||
|
|
||||||
|
// used in tests
|
||||||
|
require (
|
||||||
|
github.com/google/go-cmp v0.6.0
|
||||||
|
github.com/pkg/profile v1.7.0
|
||||||
|
github.com/shopspring/decimal v1.4.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
github.com/volatiletech/null/v8 v8.1.2
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
|
github.com/friendsofgo/errors v0.9.2 // indirect
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/volatiletech/inflect v0.0.1 // indirect
|
||||||
|
github.com/volatiletech/randomize v0.0.1 // indirect
|
||||||
|
github.com/volatiletech/strmangle v0.0.1 // indirect
|
||||||
|
golang.org/x/crypto v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
254
tools/jet-2.12.0/go.sum
Normal file
254
tools/jet-2.12.0/go.sum
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
|
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
|
||||||
|
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||||
|
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
|
||||||
|
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||||
|
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
|
github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8=
|
||||||
|
github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
|
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
|
||||||
|
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
|
||||||
|
github.com/volatiletech/null/v8 v8.1.2 h1:kiTiX1PpwvuugKwfvUNX/SU/5A2KGZMXfGD0DUHdKEI=
|
||||||
|
github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
|
||||||
|
github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk=
|
||||||
|
github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY=
|
||||||
|
github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk=
|
||||||
|
github.com/volatiletech/strmangle v0.0.1/go.mod h1:F6RA6IkB5vq0yTG4GQ0UsbbRcl3ni9P76i+JrTBKFFg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
|
||||||
|
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
|
||||||
|
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
42
tools/jet-2.12.0/internal/3rdparty/pq/format_timestamp.go
vendored
Normal file
42
tools/jet-2.12.0/internal/3rdparty/pq/format_timestamp.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package pq
|
||||||
|
|
||||||
|
// Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatTimestamp formats t into Postgres' text format for timestamps. From: github.com/lib/pq
|
||||||
|
func FormatTimestamp(t time.Time) []byte {
|
||||||
|
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the
|
||||||
|
// minus sign preferred by Go.
|
||||||
|
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
|
||||||
|
bc := false
|
||||||
|
if t.Year() <= 0 {
|
||||||
|
// flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11"
|
||||||
|
t = t.AddDate((-t.Year())*2+1, 0, 0)
|
||||||
|
bc = true
|
||||||
|
}
|
||||||
|
b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00"))
|
||||||
|
|
||||||
|
_, offset := t.Zone()
|
||||||
|
offset = offset % 60
|
||||||
|
if offset != 0 {
|
||||||
|
// RFC3339Nano already printed the minus sign
|
||||||
|
if offset < 0 {
|
||||||
|
offset = -offset
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, ':')
|
||||||
|
if offset < 10 {
|
||||||
|
b = append(b, '0')
|
||||||
|
}
|
||||||
|
b = strconv.AppendInt(b, int64(offset), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc {
|
||||||
|
b = append(b, " BC"...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
39
tools/jet-2.12.0/internal/3rdparty/pq/format_timestamp_test.go
vendored
Normal file
39
tools/jet-2.12.0/internal/3rdparty/pq/format_timestamp_test.go
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package pq
|
||||||
|
|
||||||
|
// Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var formatTimeTests = []struct {
|
||||||
|
time time.Time
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{time.Time{}, "0001-01-01 00:00:00Z"},
|
||||||
|
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03 04:05:06.123456789Z"},
|
||||||
|
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03 04:05:06.123456789+02:00"},
|
||||||
|
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03 04:05:06.123456789-06:00"},
|
||||||
|
{time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03 04:05:06-07:30:09"},
|
||||||
|
|
||||||
|
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03 04:05:06.123456789Z"},
|
||||||
|
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03 04:05:06.123456789+02:00"},
|
||||||
|
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03 04:05:06.123456789-06:00"},
|
||||||
|
|
||||||
|
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03 04:05:06.123456789Z BC"},
|
||||||
|
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03 04:05:06.123456789+02:00 BC"},
|
||||||
|
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03 04:05:06.123456789-06:00 BC"},
|
||||||
|
|
||||||
|
{time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03 04:05:06-07:30:09"},
|
||||||
|
{time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03 04:05:06-07:30:09 BC"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatTs(t *testing.T) {
|
||||||
|
for i, tt := range formatTimeTests {
|
||||||
|
val := string(FormatTimestamp(tt.time))
|
||||||
|
if val != tt.expected {
|
||||||
|
t.Errorf("%d: incorrect time format %q, want %q", i, val, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
tools/jet-2.12.0/internal/3rdparty/snaker/snaker.go
vendored
Normal file
119
tools/jet-2.12.0/internal/3rdparty/snaker/snaker.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package snaker
|
||||||
|
|
||||||
|
// Package snaker provides methods to convert CamelCase names to snake_case and back.
|
||||||
|
// It considers the list of allowed initialsms used by github.com/golang/lint/golint (e.g. ID or HTTP)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SnakeToCamel returns a string converted from snake case to uppercase
|
||||||
|
func SnakeToCamel(s string, firstLetterUppercase ...bool) string {
|
||||||
|
upperCase := true
|
||||||
|
if len(firstLetterUppercase) > 0 {
|
||||||
|
upperCase = firstLetterUppercase[0]
|
||||||
|
}
|
||||||
|
return snakeToCamel(s, upperCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snakeToCamel(s string, upperCase bool) string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
var result string
|
||||||
|
|
||||||
|
words := strings.Split(s, "_")
|
||||||
|
|
||||||
|
for i, word := range words {
|
||||||
|
if exception := snakeToCamelExceptions[word]; len(exception) > 0 {
|
||||||
|
result += exception
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if upperCase || i > 0 {
|
||||||
|
if upper := strings.ToUpper(word); commonInitialisms[upper] {
|
||||||
|
result += upper
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if upperCase || i > 0 {
|
||||||
|
result += camelizeWord(word, len(words) > 1)
|
||||||
|
} else {
|
||||||
|
result += word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func camelizeWord(word string, force bool) string {
|
||||||
|
runes := []rune(word)
|
||||||
|
|
||||||
|
for i, r := range runes {
|
||||||
|
if i == 0 {
|
||||||
|
runes[i] = unicode.ToUpper(r)
|
||||||
|
} else {
|
||||||
|
if !force && unicode.IsLower(r) { // already camelCase
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
runes[i] = unicode.ToLower(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commonInitialisms, taken from
|
||||||
|
// https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
|
||||||
|
var commonInitialisms = map[string]bool{
|
||||||
|
"ACL": true,
|
||||||
|
"API": true,
|
||||||
|
"ASCII": true,
|
||||||
|
"CPU": true,
|
||||||
|
"CSS": true,
|
||||||
|
"DNS": true,
|
||||||
|
"EOF": true,
|
||||||
|
"ETA": true,
|
||||||
|
"GPU": true,
|
||||||
|
"GUID": true,
|
||||||
|
"HTML": true,
|
||||||
|
"HTTP": true,
|
||||||
|
"HTTPS": true,
|
||||||
|
"ID": true,
|
||||||
|
"IP": true,
|
||||||
|
"JSON": true,
|
||||||
|
"LHS": true,
|
||||||
|
"OS": true,
|
||||||
|
"QPS": true,
|
||||||
|
"RAM": true,
|
||||||
|
"RHS": true,
|
||||||
|
"RPC": true,
|
||||||
|
"SLA": true,
|
||||||
|
"SMTP": true,
|
||||||
|
"SQL": true,
|
||||||
|
"SSH": true,
|
||||||
|
"TCP": true,
|
||||||
|
"TLS": true,
|
||||||
|
"TTL": true,
|
||||||
|
"UDP": true,
|
||||||
|
"UI": true,
|
||||||
|
"UID": true,
|
||||||
|
"UUID": true,
|
||||||
|
"URI": true,
|
||||||
|
"URL": true,
|
||||||
|
"UTF8": true,
|
||||||
|
"VM": true,
|
||||||
|
"XML": true,
|
||||||
|
"XMPP": true,
|
||||||
|
"XSRF": true,
|
||||||
|
"XSS": true,
|
||||||
|
"OAuth": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// add exceptions here for things that are not automatically convertable
|
||||||
|
var snakeToCamelExceptions = map[string]string{
|
||||||
|
"oauth": "OAuth",
|
||||||
|
}
|
||||||
16
tools/jet-2.12.0/internal/3rdparty/snaker/snaker_test.go
vendored
Normal file
16
tools/jet-2.12.0/internal/3rdparty/snaker/snaker_test.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package snaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSnakeToCamel(t *testing.T) {
|
||||||
|
require.Equal(t, SnakeToCamel(""), "")
|
||||||
|
require.Equal(t, SnakeToCamel("potato_"), "Potato")
|
||||||
|
require.Equal(t, SnakeToCamel("this_has_to_be_uppercased"), "ThisHasToBeUppercased")
|
||||||
|
require.Equal(t, SnakeToCamel("this_is_an_id"), "ThisIsAnID")
|
||||||
|
require.Equal(t, SnakeToCamel("this_is_an_identifier"), "ThisIsAnIdentifier")
|
||||||
|
require.Equal(t, SnakeToCamel("id"), "ID")
|
||||||
|
require.Equal(t, SnakeToCamel("oauth_client"), "OAuthClient")
|
||||||
|
}
|
||||||
32
tools/jet-2.12.0/internal/jet/alias.go
Normal file
32
tools/jet-2.12.0/internal/jet/alias.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
type alias struct {
|
||||||
|
expression Expression
|
||||||
|
alias string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAlias(expression Expression, aliasName string) Projection {
|
||||||
|
return &alias{
|
||||||
|
expression: expression,
|
||||||
|
alias: aliasName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alias) fromImpl(subQuery SelectTable) Projection {
|
||||||
|
// if alias is in the form "table.column", we break it into two parts so that ProjectionList.As(newAlias) can
|
||||||
|
// overwrite tableName with a new alias. This method is called only for exporting aliased custom columns.
|
||||||
|
// Generated columns have default aliasing.
|
||||||
|
tableName, columnName := extractTableAndColumnName(a.alias)
|
||||||
|
|
||||||
|
column := NewColumnImpl(columnName, tableName, nil)
|
||||||
|
column.subQuery = subQuery
|
||||||
|
|
||||||
|
return &column
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alias) serializeForProjection(statement StatementType, out *SQLBuilder) {
|
||||||
|
a.expression.serialize(statement, out)
|
||||||
|
|
||||||
|
out.WriteString("AS")
|
||||||
|
out.WriteAlias(a.alias)
|
||||||
|
}
|
||||||
115
tools/jet-2.12.0/internal/jet/bool_expression.go
Normal file
115
tools/jet-2.12.0/internal/jet/bool_expression.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
// BoolExpression interface
|
||||||
|
type BoolExpression interface {
|
||||||
|
Expression
|
||||||
|
|
||||||
|
// Check if this expression is equal to rhs
|
||||||
|
EQ(rhs BoolExpression) BoolExpression
|
||||||
|
// Check if this expression is not equal to rhs
|
||||||
|
NOT_EQ(rhs BoolExpression) BoolExpression
|
||||||
|
// Check if this expression is distinct to rhs
|
||||||
|
IS_DISTINCT_FROM(rhs BoolExpression) BoolExpression
|
||||||
|
// Check if this expression is not distinct to rhs
|
||||||
|
IS_NOT_DISTINCT_FROM(rhs BoolExpression) BoolExpression
|
||||||
|
|
||||||
|
// Check if this expression is true
|
||||||
|
IS_TRUE() BoolExpression
|
||||||
|
// Check if this expression is not true
|
||||||
|
IS_NOT_TRUE() BoolExpression
|
||||||
|
// Check if this expression is false
|
||||||
|
IS_FALSE() BoolExpression
|
||||||
|
// Check if this expression is not false
|
||||||
|
IS_NOT_FALSE() BoolExpression
|
||||||
|
// Check if this expression is unknown
|
||||||
|
IS_UNKNOWN() BoolExpression
|
||||||
|
// Check if this expression is not unknown
|
||||||
|
IS_NOT_UNKNOWN() BoolExpression
|
||||||
|
|
||||||
|
// expression AND operator rhs
|
||||||
|
AND(rhs BoolExpression) BoolExpression
|
||||||
|
// expression OR operator rhs
|
||||||
|
OR(rhs BoolExpression) BoolExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolInterfaceImpl struct {
|
||||||
|
parent BoolExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) EQ(expression BoolExpression) BoolExpression {
|
||||||
|
return Eq(b.parent, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) NOT_EQ(expression BoolExpression) BoolExpression {
|
||||||
|
return NotEq(b.parent, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_DISTINCT_FROM(rhs BoolExpression) BoolExpression {
|
||||||
|
return IsDistinctFrom(b.parent, rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_NOT_DISTINCT_FROM(rhs BoolExpression) BoolExpression {
|
||||||
|
return IsNotDistinctFrom(b.parent, rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) AND(expression BoolExpression) BoolExpression {
|
||||||
|
return newBinaryBoolOperatorExpression(b.parent, expression, "AND")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) OR(expression BoolExpression) BoolExpression {
|
||||||
|
return newBinaryBoolOperatorExpression(b.parent, expression, "OR")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_TRUE() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS TRUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_NOT_TRUE() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS NOT TRUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_FALSE() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS FALSE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_NOT_FALSE() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS NOT FALSE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_UNKNOWN() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS UNKNOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *boolInterfaceImpl) IS_NOT_UNKNOWN() BoolExpression {
|
||||||
|
return newPostfixBoolOperatorExpression(b.parent, "IS NOT UNKNOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBinaryBoolOperatorExpression(lhs, rhs Expression, operator string, additionalParams ...Expression) BoolExpression {
|
||||||
|
return BoolExp(NewBinaryOperatorExpression(lhs, rhs, operator, additionalParams...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrefixBoolOperatorExpression(expression Expression, operator string) BoolExpression {
|
||||||
|
return BoolExp(newPrefixOperatorExpression(expression, operator))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPostfixBoolOperatorExpression(expression Expression, operator string) BoolExpression {
|
||||||
|
return BoolExp(newPostfixOperatorExpression(expression, operator))
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolExpressionWrapper struct {
|
||||||
|
boolInterfaceImpl
|
||||||
|
Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBoolExpressionWrap(expression Expression) BoolExpression {
|
||||||
|
boolExpressionWrap := boolExpressionWrapper{Expression: expression}
|
||||||
|
boolExpressionWrap.boolInterfaceImpl.parent = &boolExpressionWrap
|
||||||
|
return &boolExpressionWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolExp is bool expression wrapper around arbitrary expression.
|
||||||
|
// Allows go compiler to see any expression as bool expression.
|
||||||
|
// Does not add sql cast to generated sql builder output.
|
||||||
|
func BoolExp(expression Expression) BoolExpression {
|
||||||
|
return newBoolExpressionWrap(expression)
|
||||||
|
}
|
||||||
76
tools/jet-2.12.0/internal/jet/bool_expression_test.go
Normal file
76
tools/jet-2.12.0/internal/jet/bool_expression_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoolExpressionEQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.EQ(table2ColBool), "(table1.col_bool = table2.col_bool)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionNOT_EQ(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.NOT_EQ(table2ColBool), "(table1.col_bool != table2.col_bool)")
|
||||||
|
assertClauseSerialize(t, table1ColBool.NOT_EQ(Bool(true)), "(table1.col_bool != $1)", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS DISTINCT FROM table2.col_bool)")
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS DISTINCT FROM $1)", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_NOT_DISTINCT_FROM(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(table2ColBool), "(table1.col_bool IS NOT DISTINCT FROM table2.col_bool)")
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_DISTINCT_FROM(Bool(false)), "(table1.col_bool IS NOT DISTINCT FROM $1)", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_TRUE(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_TRUE(), "table1.col_bool IS TRUE")
|
||||||
|
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE(),
|
||||||
|
`($1 = table1.col_int) IS TRUE`, int64(2))
|
||||||
|
assertClauseSerialize(t, (Int(2).EQ(table1ColInt)).IS_TRUE().AND(Int(4).EQ(table2ColInt)),
|
||||||
|
`(($1 = table1.col_int) IS TRUE AND ($2 = table2.col_int))`, int64(2), int64(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_NOT_TRUE(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_TRUE(), "table1.col_bool IS NOT TRUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_FALSE(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_FALSE(), "table1.col_bool IS FALSE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_NOT_FALSE(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_FALSE(), "table1.col_bool IS NOT FALSE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_UNKNOWN(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_UNKNOWN(), "table1.col_bool IS UNKNOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExpressionIS_NOT_UNKNOWN(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, table1ColBool.IS_NOT_UNKNOWN(), "table1.col_bool IS NOT UNKNOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBinaryBoolExpression(t *testing.T) {
|
||||||
|
boolExpression := Int(2).EQ(Int(3))
|
||||||
|
|
||||||
|
assertClauseSerialize(t, boolExpression, "($1 = $2)", int64(2), int64(3))
|
||||||
|
|
||||||
|
assertProjectionSerialize(t, boolExpression, "$1 = $2", int64(2), int64(3))
|
||||||
|
assertProjectionSerialize(t, boolExpression.AS("alias_eq_expression"),
|
||||||
|
`($1 = $2) AS "alias_eq_expression"`, int64(2), int64(3))
|
||||||
|
assertClauseSerialize(t, boolExpression.AND(Int(4).EQ(Int(5))),
|
||||||
|
"(($1 = $2) AND ($3 = $4))", int64(2), int64(3), int64(4), int64(5))
|
||||||
|
assertClauseSerialize(t, boolExpression.OR(Int(4).EQ(Int(5))),
|
||||||
|
"(($1 = $2) OR ($3 = $4))", int64(2), int64(3), int64(4), int64(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolLiteral(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, Bool(true), "$1", true)
|
||||||
|
assertClauseSerialize(t, Bool(false), "$1", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolExp(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, BoolExp(String("true")), "$1", "true")
|
||||||
|
assertClauseSerialize(t, BoolExp(String("true")).IS_TRUE(), "$1 IS TRUE", "true")
|
||||||
|
}
|
||||||
53
tools/jet-2.12.0/internal/jet/cast.go
Normal file
53
tools/jet-2.12.0/internal/jet/cast.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
// Cast interface
|
||||||
|
type Cast interface {
|
||||||
|
AS(castType string) Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
type castImpl struct {
|
||||||
|
expression Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCastImpl creates new generic cast
|
||||||
|
func NewCastImpl(expression Expression) Cast {
|
||||||
|
castImpl := castImpl{
|
||||||
|
expression: expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &castImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *castImpl) AS(castType string) Expression {
|
||||||
|
castExp := &castExpression{
|
||||||
|
expression: b.expression,
|
||||||
|
cast: string(castType),
|
||||||
|
}
|
||||||
|
|
||||||
|
castExp.ExpressionInterfaceImpl.Parent = castExp
|
||||||
|
|
||||||
|
return castExp
|
||||||
|
}
|
||||||
|
|
||||||
|
type castExpression struct {
|
||||||
|
ExpressionInterfaceImpl
|
||||||
|
|
||||||
|
expression Expression
|
||||||
|
cast string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *castExpression) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
|
||||||
|
expression := b.expression
|
||||||
|
castType := b.cast
|
||||||
|
|
||||||
|
if castOverride := out.Dialect.OperatorSerializeOverride("CAST"); castOverride != nil {
|
||||||
|
castOverride(expression, String(castType))(statement, out, FallTrough(options)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("CAST(")
|
||||||
|
expression.serialize(statement, out, FallTrough(options)...)
|
||||||
|
out.WriteString("AS")
|
||||||
|
out.WriteString(castType + ")")
|
||||||
|
}
|
||||||
11
tools/jet-2.12.0/internal/jet/cast_test.go
Normal file
11
tools/jet-2.12.0/internal/jet/cast_test.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCastAS(t *testing.T) {
|
||||||
|
assertClauseSerialize(t, NewCastImpl(Int(1)).AS("boolean"), "CAST($1 AS boolean)", int64(1))
|
||||||
|
assertClauseSerialize(t, NewCastImpl(table2Col3).AS("real"), "CAST(table2.col3 AS real)")
|
||||||
|
assertClauseSerialize(t, NewCastImpl(table2Col3.ADD(table2Col3)).AS("integer"), "CAST((table2.col3 + table2.col3) AS integer)")
|
||||||
|
}
|
||||||
672
tools/jet-2.12.0/internal/jet/clause.go
Normal file
672
tools/jet-2.12.0/internal/jet/clause.go
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
package jet
|
||||||
|
|
||||||
|
import "github.com/go-jet/jet/v2/internal/utils/is"
|
||||||
|
|
||||||
|
// Clause interface
|
||||||
|
type Clause interface {
|
||||||
|
Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseWithProjections interface
|
||||||
|
type ClauseWithProjections interface {
|
||||||
|
Clause
|
||||||
|
|
||||||
|
Projections() ProjectionList
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptimizerHint provides a way to optimize query execution per-statement basis
|
||||||
|
type OptimizerHint string
|
||||||
|
|
||||||
|
type optimizerHints []OptimizerHint
|
||||||
|
|
||||||
|
func (o optimizerHints) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(o) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("/*+")
|
||||||
|
for i, hint := range o {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteByte(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString(string(hint))
|
||||||
|
}
|
||||||
|
out.WriteString("*/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseSelect struct
|
||||||
|
type ClauseSelect struct {
|
||||||
|
Distinct bool
|
||||||
|
DistinctOnColumns []ColumnExpression
|
||||||
|
ProjectionList []Projection
|
||||||
|
|
||||||
|
// MySQL only
|
||||||
|
OptimizerHints optimizerHints
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projections returns list of projections for select clause
|
||||||
|
func (s *ClauseSelect) Projections() ProjectionList {
|
||||||
|
return s.ProjectionList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("SELECT")
|
||||||
|
s.OptimizerHints.Serialize(statementType, out, options...)
|
||||||
|
|
||||||
|
if s.Distinct {
|
||||||
|
out.WriteString("DISTINCT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.DistinctOnColumns) > 0 {
|
||||||
|
out.WriteString("ON (")
|
||||||
|
SerializeColumnExpressions(s.DistinctOnColumns, statementType, out)
|
||||||
|
out.WriteByte(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.ProjectionList) == 0 {
|
||||||
|
panic("jet: SELECT clause has to have at least one projection")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteProjections(statementType, s.ProjectionList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseFrom struct
|
||||||
|
type ClauseFrom struct {
|
||||||
|
Name string
|
||||||
|
Tables []Serializer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (f *ClauseFrom) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(f.Tables) == 0 { // SELECT statement does not have to have FROM clause
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.NewLine()
|
||||||
|
if f.Name != "" {
|
||||||
|
out.WriteString(f.Name)
|
||||||
|
} else {
|
||||||
|
out.WriteString("FROM")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.IncreaseIdent()
|
||||||
|
for i, table := range f.Tables {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(",")
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
table.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
out.DecreaseIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseWhere struct
|
||||||
|
type ClauseWhere struct {
|
||||||
|
Condition BoolExpression
|
||||||
|
Mandatory bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (c *ClauseWhere) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if c.Condition == nil {
|
||||||
|
if c.Mandatory {
|
||||||
|
panic("jet: WHERE clause not set")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !contains(options, SkipNewLine) {
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
out.WriteString("WHERE")
|
||||||
|
|
||||||
|
out.IncreaseIdent(6)
|
||||||
|
c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...)
|
||||||
|
out.DecreaseIdent(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseGroupBy struct
|
||||||
|
type ClauseGroupBy struct {
|
||||||
|
List []GroupByClause
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (c *ClauseGroupBy) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(c.List) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("GROUP BY")
|
||||||
|
|
||||||
|
out.IncreaseIdent()
|
||||||
|
|
||||||
|
for i, c := range c.List {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == nil {
|
||||||
|
panic("jet: nil clause in GROUP BY list")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.serializeForGroupBy(statementType, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.DecreaseIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseHaving struct
|
||||||
|
type ClauseHaving struct {
|
||||||
|
Condition BoolExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (c *ClauseHaving) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if c.Condition == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("HAVING")
|
||||||
|
|
||||||
|
out.IncreaseIdent()
|
||||||
|
c.Condition.serialize(statementType, out, NoWrap.WithFallTrough(options)...)
|
||||||
|
out.DecreaseIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseOrderBy struct
|
||||||
|
type ClauseOrderBy struct {
|
||||||
|
List []OrderByClause
|
||||||
|
SkipNewLine bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (o *ClauseOrderBy) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if o.List == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.SkipNewLine {
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
out.WriteString("ORDER BY")
|
||||||
|
|
||||||
|
out.IncreaseIdent()
|
||||||
|
|
||||||
|
for i, value := range o.List {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
value.serializeForOrderBy(statementType, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.DecreaseIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseLimit struct
|
||||||
|
type ClauseLimit struct {
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (l *ClauseLimit) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if l.Count >= 0 {
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("LIMIT")
|
||||||
|
out.insertParametrizedArgument(l.Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseOffset struct
|
||||||
|
type ClauseOffset struct {
|
||||||
|
Count IntegerExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (o *ClauseOffset) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if is.Nil(o.Count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("OFFSET")
|
||||||
|
o.Count.serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseFetch struct
|
||||||
|
type ClauseFetch struct {
|
||||||
|
Count IntegerExpression
|
||||||
|
WithTies bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes ClauseFetch into sql builder output
|
||||||
|
func (o *ClauseFetch) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if is.Nil(o.Count) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("FETCH FIRST")
|
||||||
|
o.Count.serialize(statementType, out, options...)
|
||||||
|
|
||||||
|
if o.WithTies {
|
||||||
|
out.WriteString("ROWS WITH TIES")
|
||||||
|
} else {
|
||||||
|
out.WriteString("ROWS ONLY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseFor struct
|
||||||
|
type ClauseFor struct {
|
||||||
|
Lock RowLock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (f *ClauseFor) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if f.Lock == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("FOR")
|
||||||
|
f.Lock.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseSetStmtOperator struct
|
||||||
|
type ClauseSetStmtOperator struct {
|
||||||
|
Operator string
|
||||||
|
All bool
|
||||||
|
Selects []SerializerStatement
|
||||||
|
OrderBy ClauseOrderBy
|
||||||
|
Limit ClauseLimit
|
||||||
|
Offset ClauseOffset
|
||||||
|
SkipSelectWrap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projections returns set of projections for ClauseSetStmtOperator
|
||||||
|
func (s *ClauseSetStmtOperator) Projections() ProjectionList {
|
||||||
|
if len(s.Selects) > 0 {
|
||||||
|
return s.Selects[0].projections()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (s *ClauseSetStmtOperator) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(s.Selects) < 2 {
|
||||||
|
panic("jet: UNION Statement must contain at least two SELECT statements")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, selectStmt := range s.Selects {
|
||||||
|
out.NewLine()
|
||||||
|
if i > 0 {
|
||||||
|
if s.SkipSelectWrap {
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString(s.Operator)
|
||||||
|
|
||||||
|
if s.All {
|
||||||
|
out.WriteString("ALL")
|
||||||
|
}
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectStmt == nil {
|
||||||
|
panic("jet: select statement of '" + s.Operator + "' is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.SkipSelectWrap {
|
||||||
|
options = append(FallTrough(options), NoWrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectStmt.serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.OrderBy.Serialize(statementType, out)
|
||||||
|
s.Limit.Serialize(statementType, out)
|
||||||
|
s.Offset.Serialize(statementType, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseUpdate struct
|
||||||
|
type ClauseUpdate struct {
|
||||||
|
Table SerializerTable
|
||||||
|
|
||||||
|
// MySQL only
|
||||||
|
OptimizerHints optimizerHints
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (u *ClauseUpdate) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("UPDATE")
|
||||||
|
u.OptimizerHints.Serialize(statementType, out, options...)
|
||||||
|
|
||||||
|
if is.Nil(u.Table) {
|
||||||
|
panic("jet: table to update is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Table.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClause struct
|
||||||
|
type SetClause struct {
|
||||||
|
Columns []Column
|
||||||
|
Values []Serializer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (s *SetClause) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(s.Values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("SET")
|
||||||
|
|
||||||
|
if len(s.Columns) != len(s.Values) {
|
||||||
|
panic("jet: mismatch in numbers of columns and values for SET clause")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.IncreaseIdent(4)
|
||||||
|
for i, column := range s.Columns {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(",")
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
if column == nil {
|
||||||
|
panic("jet: nil column in columns list for SET clause")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteIdentifier(column.Name())
|
||||||
|
|
||||||
|
out.WriteString(" = ")
|
||||||
|
|
||||||
|
s.Values[i].serialize(UpdateStatementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
out.DecreaseIdent(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseInsert struct
|
||||||
|
type ClauseInsert struct {
|
||||||
|
Table SerializerTable
|
||||||
|
Columns []Column
|
||||||
|
|
||||||
|
// MySQL only
|
||||||
|
OptimizerHints optimizerHints
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumns gets list of columns for insert
|
||||||
|
func (i *ClauseInsert) GetColumns() []Column {
|
||||||
|
if len(i.Columns) > 0 {
|
||||||
|
return i.Columns
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.Table.columns()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (i *ClauseInsert) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if is.Nil(i.Table) {
|
||||||
|
panic("jet: table is nil for INSERT clause")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("INSERT")
|
||||||
|
i.OptimizerHints.Serialize(statementType, out, options...)
|
||||||
|
out.WriteString("INTO")
|
||||||
|
|
||||||
|
i.Table.serialize(statementType, out)
|
||||||
|
|
||||||
|
if len(i.Columns) > 0 {
|
||||||
|
out.WriteString("(")
|
||||||
|
|
||||||
|
SerializeColumnNames(i.Columns, out)
|
||||||
|
|
||||||
|
out.WriteString(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseValuesQuery struct
|
||||||
|
type ClauseValuesQuery struct {
|
||||||
|
ClauseValues
|
||||||
|
ClauseQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (v *ClauseValuesQuery) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(v.Rows) > 0 && v.Query != nil {
|
||||||
|
panic("jet: VALUES or QUERY has to be specified for INSERT statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
v.ClauseValues.Serialize(statementType, out, FallTrough(options)...)
|
||||||
|
v.ClauseQuery.Serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseValues struct
|
||||||
|
type ClauseValues struct {
|
||||||
|
Rows [][]Serializer
|
||||||
|
As string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (v *ClauseValues) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(v.Rows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("VALUES")
|
||||||
|
|
||||||
|
for rowIndex, row := range v.Rows {
|
||||||
|
if rowIndex > 0 {
|
||||||
|
out.WriteString(",")
|
||||||
|
out.NewLine()
|
||||||
|
} else {
|
||||||
|
out.IncreaseIdent(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("(")
|
||||||
|
|
||||||
|
SerializeClauseList(statementType, row, out)
|
||||||
|
|
||||||
|
out.WriteByte(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.As) > 0 {
|
||||||
|
out.WriteString("AS")
|
||||||
|
out.WriteIdentifier(v.As)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.DecreaseIdent(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseQuery struct
|
||||||
|
type ClauseQuery struct {
|
||||||
|
Query SerializerStatement
|
||||||
|
SkipSelectWrap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (v *ClauseQuery) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if v.Query == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.SkipSelectWrap {
|
||||||
|
options = append(FallTrough(options), NoWrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Query.serialize(statementType, out, options...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseDelete struct
|
||||||
|
type ClauseDelete struct {
|
||||||
|
Table SerializerTable
|
||||||
|
|
||||||
|
// MySQL only
|
||||||
|
OptimizerHints optimizerHints
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (d *ClauseDelete) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("DELETE")
|
||||||
|
d.OptimizerHints.Serialize(statementType, out, options...)
|
||||||
|
out.WriteString("FROM")
|
||||||
|
d.Table.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseStatementBegin struct
|
||||||
|
type ClauseStatementBegin struct {
|
||||||
|
Name string
|
||||||
|
Tables []SerializerTable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (d *ClauseStatementBegin) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString(d.Name)
|
||||||
|
|
||||||
|
for i, table := range d.Tables {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
table.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseOptional struct
|
||||||
|
type ClauseOptional struct {
|
||||||
|
Name string
|
||||||
|
Show bool
|
||||||
|
InNewLine bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (d *ClauseOptional) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if !d.Show {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.InNewLine {
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
out.WriteString(d.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseIn struct
|
||||||
|
type ClauseIn struct {
|
||||||
|
LockMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (i *ClauseIn) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if i.LockMode == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("IN")
|
||||||
|
out.WriteString(string(i.LockMode))
|
||||||
|
out.WriteString("MODE")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowDefinition struct
|
||||||
|
type WindowDefinition struct {
|
||||||
|
Name string
|
||||||
|
Window Window
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseWindow struct
|
||||||
|
type ClauseWindow struct {
|
||||||
|
Definitions []WindowDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes clause into SQLBuilder
|
||||||
|
func (i *ClauseWindow) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(i.Definitions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("WINDOW")
|
||||||
|
|
||||||
|
for i, def := range i.Definitions {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(", ")
|
||||||
|
}
|
||||||
|
out.WriteString(def.Name)
|
||||||
|
out.WriteString("AS")
|
||||||
|
if def.Window == nil {
|
||||||
|
out.WriteString("()")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
def.Window.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPair clause
|
||||||
|
type SetPair struct {
|
||||||
|
Column ColumnSerializer
|
||||||
|
Value Serializer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClauseNew clause
|
||||||
|
type SetClauseNew []ColumnAssigment
|
||||||
|
|
||||||
|
// Serialize for SetClauseNew
|
||||||
|
func (s SetClauseNew) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("SET")
|
||||||
|
out.IncreaseIdent(4)
|
||||||
|
|
||||||
|
for i, assigment := range s {
|
||||||
|
if i > 0 {
|
||||||
|
out.WriteString(",")
|
||||||
|
out.NewLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
assigment.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.DecreaseIdent(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeywordClause type
|
||||||
|
type KeywordClause struct {
|
||||||
|
Keyword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize for KeywordClause
|
||||||
|
func (k KeywordClause) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
k.serialize(statementType, out, FallTrough(options)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClauseReturning type
|
||||||
|
type ClauseReturning struct {
|
||||||
|
ProjectionList []Projection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize for ClauseReturning
|
||||||
|
func (r *ClauseReturning) Serialize(statementType StatementType, out *SQLBuilder, options ...SerializeOption) {
|
||||||
|
if len(r.ProjectionList) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.NewLine()
|
||||||
|
out.WriteString("RETURNING")
|
||||||
|
out.IncreaseIdent()
|
||||||
|
out.WriteProjections(statementType, r.ProjectionList)
|
||||||
|
out.DecreaseIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projections for ClauseReturning
|
||||||
|
func (r ClauseReturning) Projections() ProjectionList {
|
||||||
|
return r.ProjectionList
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user