Initial add backend stuff

This commit is contained in:
2026-07-08 15:45:16 -04:00
commit a7964f9410
89 changed files with 25924 additions and 0 deletions

18
cmd/bundle/main.go Normal file
View File

@@ -0,0 +1,18 @@
package main
// Thin CLI wrapper around kjol/bundler. The bundler wires its own Go-native
// Solid JSX compiler (see bundler.Build), so this wrapper carries no build logic.
import (
"fmt"
"os"
"kjol/bundler"
)
func main() {
if err := bundler.Build(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

129
cmd/loc/main.go Normal file
View File

@@ -0,0 +1,129 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/hhatto/gocloc"
)
func main() {
out, err := exec.Command("git", "ls-files").Output()
if err != nil {
fmt.Fprintf(os.Stderr, "git ls-files: %v\n", err)
os.Exit(1)
}
allFiles := strings.Split(strings.TrimSpace(string(out)), "\n")
var files []string
for _, f := range allFiles {
f = strings.TrimSpace(f)
if f == "" {
continue
}
norm := filepath.ToSlash(f)
if strings.HasPrefix(norm, "vendor/") || strings.Contains(norm, "/vendor/") {
continue
}
files = append(files, f)
}
opts := gocloc.NewClocOptions()
langs := gocloc.NewDefinedLanguages()
// gocloc maps "TypeScript" instead of "ts" in its Exts table
extAliases := map[string]string{
"ts": "TypeScript",
}
total := gocloc.NewLanguage("TOTAL", []string{}, [][]string{{"", ""}})
languages := make(map[string]*gocloc.Language)
clocFiles := make(map[string]*gocloc.ClocFile)
for _, file := range files {
file = strings.TrimSpace(file)
if file == "" {
continue
}
ext := filepath.Ext(file)
if ext == "" {
continue
}
ext = ext[1:]
langName, ok := gocloc.Exts[ext]
if !ok {
langName, ok = extAliases[ext]
if !ok {
continue
}
}
def := langs.Langs[langName]
if def == nil {
continue
}
cf := gocloc.AnalyzeFile(file, def, opts)
cf.Lang = langName
clocFiles[file] = cf
if _, exists := languages[langName]; !exists {
languages[langName] = gocloc.NewLanguage(def.Name, []string{}, [][]string{{"", ""}})
}
lang := languages[langName]
lang.Files = append(lang.Files, file)
lang.Code += cf.Code
lang.Comments += cf.Comments
lang.Blanks += cf.Blanks
total.Code += cf.Code
total.Comments += cf.Comments
total.Blanks += cf.Blanks
}
type row struct {
Name string
Files int
Code int32
Comments int32
Blanks int32
}
var rows []row
for name, lang := range languages {
rows = append(rows, row{
Name: name,
Files: len(lang.Files),
Code: lang.Code,
Comments: lang.Comments,
Blanks: lang.Blanks,
})
}
sort.Slice(rows, func(i, j int) bool {
return rows[i].Code > rows[j].Code
})
divider := "-------------------------------------------------------------------------------"
fmt.Println(divider)
fmt.Printf("%-25s %10s %10s %10s %10s\n", "Language", "Files", "Code", "Comment", "Blank")
fmt.Println(divider)
for _, r := range rows {
fmt.Printf("%-25s %10d %10d %10d %10d\n", r.Name, r.Files, r.Code, r.Comments, r.Blanks)
}
fmt.Println(divider)
fmt.Printf("%-25s %10d %10d %10d %10d\n",
"Total",
len(clocFiles),
total.Code,
total.Comments,
total.Blanks,
)
fmt.Println(divider)
}

380
cmd/migrate/database.go Normal file
View File

@@ -0,0 +1,380 @@
package main
import (
"context"
"database/sql"
"fmt"
"hash/crc32"
"os"
"strconv"
"strings"
"github.com/lib/pq"
)
const (
nilVersion int = -1
migrationsTable = "schema_migrations"
advisoryLockIDSalt uint = 1486364155
)
type migrateDB struct {
conn *sql.Conn
db *sql.DB
schemaName string
dbName string
lockID string
isLocked bool
}
func openDatabase(connStr string, schemaName string) (*migrateDB, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
conn, err := db.Conn(context.Background())
if err != nil {
db.Close()
return nil, fmt.Errorf("failed to acquire connection: %w", err)
}
mdb := &migrateDB{
conn: conn,
db: db,
}
// get actual database name
if err := conn.QueryRowContext(context.Background(), "SELECT current_database()").Scan(&mdb.dbName); err != nil {
mdb.close()
return nil, fmt.Errorf("failed to get database name: %w", err)
}
// determine schema name
if schemaName != "" {
mdb.schemaName = schemaName
} else {
if err := conn.QueryRowContext(context.Background(), "SELECT current_schema()").Scan(&mdb.schemaName); err != nil {
mdb.close()
return nil, fmt.Errorf("failed to get schema name: %w", err)
}
}
mdb.lockID = generateAdvisoryLockID(mdb.dbName, mdb.schemaName)
// ensure the schema_migrations table exists
if err := mdb.ensureVersionTable(); err != nil {
mdb.close()
return nil, err
}
return mdb, nil
}
func (d *migrateDB) close() error {
var connErr, dbErr error
if d.conn != nil {
connErr = d.conn.Close()
}
if d.db != nil {
dbErr = d.db.Close()
}
if connErr != nil {
return connErr
}
return dbErr
}
func (d *migrateDB) ensureVersionTable() error {
if err := d.lock(); err != nil {
return err
}
defer d.unlock()
// check if table already exists
var count int
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1`
if err := d.conn.QueryRowContext(context.Background(), query, d.schemaName, migrationsTable).Scan(&count); err != nil {
return fmt.Errorf("failed to check for migrations table: %w", err)
}
if count > 0 {
return nil
}
// create the table
createQuery := `CREATE TABLE IF NOT EXISTS ` +
pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(migrationsTable) +
` (version bigint NOT NULL PRIMARY KEY, dirty boolean NOT NULL)`
if _, err := d.conn.ExecContext(context.Background(), createQuery); err != nil {
return fmt.Errorf("failed to create migrations table: %w", err)
}
return nil
}
// generateAdvisoryLockID replicates golang-migrate's lock ID generation.
// Internally: strings.Join(append([]string{schemaName, tableName}, databaseName), "\x00")
// Result: "schemaName\x00tableName\x00databaseName"
// Then: CRC32(result) * 1486364155
func generateAdvisoryLockID(dbName, schemaName string) string {
combined := strings.Join([]string{schemaName, migrationsTable, dbName}, "\x00")
sum := crc32.ChecksumIEEE([]byte(combined))
sum = sum * uint32(advisoryLockIDSalt)
return fmt.Sprint(sum)
}
func (d *migrateDB) lock() error {
if d.isLocked {
return fmt.Errorf("database already locked")
}
query := `SELECT pg_advisory_lock($1)`
if _, err := d.conn.ExecContext(context.Background(), query, d.lockID); err != nil {
return fmt.Errorf("failed to acquire advisory lock: %w", err)
}
d.isLocked = true
return nil
}
func (d *migrateDB) unlock() error {
if !d.isLocked {
return nil
}
query := `SELECT pg_advisory_unlock($1)`
if _, err := d.conn.ExecContext(context.Background(), query, d.lockID); err != nil {
return fmt.Errorf("failed to release advisory lock: %w", err)
}
d.isLocked = false
return nil
}
func (d *migrateDB) version() (int, bool, error) {
query := `SELECT version, dirty FROM ` +
pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(migrationsTable) +
` LIMIT 1`
var version int
var dirty bool
err := d.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return nilVersion, false, nil
case err != nil:
if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == "undefined_table" {
return nilVersion, false, nil
}
}
return 0, false, fmt.Errorf("failed to get migration version: %w", err)
default:
return version, dirty, nil
}
}
func (d *migrateDB) setVersion(version int, dirty bool) error {
tx, err := d.conn.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
truncateQuery := `TRUNCATE ` +
pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(migrationsTable)
if _, err := tx.Exec(truncateQuery); err != nil {
tx.Rollback()
return fmt.Errorf("failed to truncate migrations table: %w", err)
}
// re-write the schema version for nil dirty versions to prevent
// empty schema version for failed down migration on the first migration
if version >= 0 || (version == nilVersion && dirty) {
insertQuery := `INSERT INTO ` +
pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(migrationsTable) +
` (version, dirty) VALUES ($1, $2)`
if _, err := tx.Exec(insertQuery, version, dirty); err != nil {
tx.Rollback()
return fmt.Errorf("failed to insert migration version: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit version update: %w", err)
}
return nil
}
func (d *migrateDB) run(filePath string) error {
content, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read migration file %s: %w", filePath, err)
}
query := string(content)
if strings.TrimSpace(query) == "" {
return nil
}
if _, err := d.conn.ExecContext(context.Background(), query); err != nil {
if pgErr, ok := err.(*pq.Error); ok {
message := fmt.Sprintf("migration failed: %s", pgErr.Message)
if pgErr.Position != "" {
if pos, parseErr := strconv.ParseUint(pgErr.Position, 10, 64); parseErr == nil {
line, col, ok := computeLineFromPos(query, int(pos))
if ok {
message = fmt.Sprintf("%s (line %d, column %d)", message, line, col)
}
}
}
if pgErr.Detail != "" {
message = fmt.Sprintf("%s, %s", message, pgErr.Detail)
}
return fmt.Errorf("%s", message)
}
return fmt.Errorf("migration failed: %w", err)
}
return nil
}
func (d *migrateDB) drop() error {
// Drop tables.
tableQuery := `SELECT table_name FROM information_schema.tables WHERE table_schema=$1 AND table_type='BASE TABLE'`
rows, err := d.conn.QueryContext(context.Background(), tableQuery, d.schemaName)
if err != nil {
return fmt.Errorf("failed to query tables: %w", err)
}
defer rows.Close()
var tableNames []string
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
return fmt.Errorf("failed to scan table name: %w", err)
}
if len(tableName) > 0 {
tableNames = append(tableNames, tableName)
}
}
if err := rows.Err(); err != nil {
return fmt.Errorf("failed to iterate tables: %w", err)
}
for _, t := range tableNames {
dropQuery := `DROP TABLE IF EXISTS ` + pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(t) + ` CASCADE`
if _, err := d.conn.ExecContext(context.Background(), dropQuery); err != nil {
return fmt.Errorf("failed to drop table %s: %w", t, err)
}
}
// Drop views.
viewQuery := `SELECT table_name FROM information_schema.views WHERE table_schema=$1`
viewRows, err := d.conn.QueryContext(context.Background(), viewQuery, d.schemaName)
if err != nil {
return fmt.Errorf("failed to query views: %w", err)
}
defer viewRows.Close()
var viewNames []string
for viewRows.Next() {
var viewName string
if err := viewRows.Scan(&viewName); err != nil {
return fmt.Errorf("failed to scan view name: %w", err)
}
viewNames = append(viewNames, viewName)
}
if err := viewRows.Err(); err != nil {
return fmt.Errorf("failed to iterate views: %w", err)
}
for _, v := range viewNames {
dropQuery := `DROP VIEW IF EXISTS ` + pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(v) + ` CASCADE`
if _, err := d.conn.ExecContext(context.Background(), dropQuery); err != nil {
return fmt.Errorf("failed to drop view %s: %w", v, err)
}
}
// Drop custom types (enums, composites, domains, ranges). typtype 'b' (base)
// already excludes Postgres' auto-generated array types, so no further
// filtering by name is needed.
typeQuery := `
SELECT t.typname
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = $1
AND t.typtype IN ('e', 'c', 'd', 'r')
AND NOT EXISTS (
SELECT 1 FROM pg_class c
WHERE c.reltype = t.oid AND c.relkind IN ('r', 'v', 'm', 'f', 'p')
)
`
typeRows, err := d.conn.QueryContext(context.Background(), typeQuery, d.schemaName)
if err != nil {
return fmt.Errorf("failed to query types: %w", err)
}
defer typeRows.Close()
var typeNames []string
for typeRows.Next() {
var typeName string
if err := typeRows.Scan(&typeName); err != nil {
return fmt.Errorf("failed to scan type name: %w", err)
}
typeNames = append(typeNames, typeName)
}
if err := typeRows.Err(); err != nil {
return fmt.Errorf("failed to iterate types: %w", err)
}
for _, tn := range typeNames {
dropQuery := `DROP TYPE IF EXISTS ` + pq.QuoteIdentifier(d.schemaName) + `.` + pq.QuoteIdentifier(tn) + ` CASCADE`
if _, err := d.conn.ExecContext(context.Background(), dropQuery); err != nil {
return fmt.Errorf("failed to drop type %s: %w", tn, err)
}
}
return nil
}
func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
s = strings.ReplaceAll(s, "\r\n", "\n")
runes := []rune(s)
if pos > len(runes) {
return 0, 0, false
}
sel := runes[:pos]
line = uint(runesCount(sel, '\n') + 1)
col = uint(pos - 1 - runesLastIndex(sel, '\n'))
return line, col, true
}
func runesCount(input []rune, target rune) int {
var count int
for _, r := range input {
if r == target {
count++
}
}
return count
}
func runesLastIndex(input []rune, target rune) int {
for i := len(input) - 1; i >= 0; i-- {
if input[i] == target {
return i
}
}
return -1
}

260
cmd/migrate/engine.go Normal file
View File

@@ -0,0 +1,260 @@
package main
import (
"errors"
"fmt"
"os"
)
var (
errNoChange = errors.New("no change")
)
type engine struct {
source *migrationSource
db *migrateDB
}
func newEngine(migrationsDir string, connStr string, schemaName string) (*engine, error) {
source, err := newMigrationSource(migrationsDir)
if err != nil {
return nil, err
}
db, err := openDatabase(connStr, schemaName)
if err != nil {
return nil, err
}
return &engine{source: source, db: db}, nil
}
func (e *engine) close() error {
return e.db.close()
}
func (e *engine) up() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
// determine where to start
var version uint
if curVersion == nilVersion {
first, err := e.source.first()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errNoChange
}
return err
}
version = first
} else {
next, err := e.source.next(uint(curVersion))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errNoChange
}
return err
}
version = next
}
// apply from starting version forward
for {
if err := e.applyUp(version); err != nil {
return err
}
next, err := e.source.next(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil // done, applied all
}
return err
}
version = next
}
}
func (e *engine) down() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
if curVersion == nilVersion {
return errNoChange
}
// determine target version after rollback
targetVersion := nilVersion
prev, err := e.source.prev(uint(curVersion))
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
// at the first migration, target is nilVersion (-1)
} else {
targetVersion = int(prev)
}
return e.applyDown(uint(curVersion), targetVersion)
}
func (e *engine) goTo(target uint) error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
curVersion, dirty, err := e.db.version()
if err != nil {
return err
}
if dirty {
return fmt.Errorf("Dirty database version %d. Fix and force version.", curVersion)
}
if !e.source.versionExists(target) {
return fmt.Errorf("version %d not found in migration source", target)
}
intTarget := int(target)
if intTarget == curVersion {
return errNoChange
}
if intTarget > curVersion {
// going up
var version uint
if curVersion == nilVersion {
first, err := e.source.first()
if err != nil {
return err
}
version = first
} else {
next, err := e.source.next(uint(curVersion))
if err != nil {
return err
}
version = next
}
for {
if err := e.applyUp(version); err != nil {
return err
}
if version == target {
return nil
}
next, err := e.source.next(version)
if err != nil {
return err
}
version = next
}
}
// going down
version := uint(curVersion)
for int(version) > intTarget {
prev, err := e.source.prev(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// we're at the first version, can't go lower
return fmt.Errorf("cannot migrate down from version %d to %d", version, target)
}
return err
}
if err := e.applyDown(version, int(prev)); err != nil {
return err
}
version = prev
}
return nil
}
func (e *engine) force(version int) error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
return e.db.setVersion(version, false)
}
func (e *engine) dropAll() error {
if err := e.db.lock(); err != nil {
return err
}
defer e.db.unlock()
return e.db.drop()
}
// applyUp applies a single up migration for the given version.
func (e *engine) applyUp(version uint) error {
// mark dirty before execution
if err := e.db.setVersion(int(version), true); err != nil {
return err
}
filePath, err := e.source.readUp(version)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// no up file for this version, just set clean
return e.db.setVersion(int(version), false)
}
return err
}
if err := e.db.run(filePath); err != nil {
return err
}
// mark clean after successful execution
return e.db.setVersion(int(version), false)
}
// applyDown applies a single down migration from fromVersion, setting targetVersion afterward.
func (e *engine) applyDown(fromVersion uint, targetVersion int) error {
// mark dirty with target version before execution
if err := e.db.setVersion(targetVersion, true); err != nil {
return err
}
filePath, err := e.source.readDown(fromVersion)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// no down file for this version, just set clean
return e.db.setVersion(targetVersion, false)
}
return err
}
if err := e.db.run(filePath); err != nil {
return err
}
// mark clean after successful execution
return e.db.setVersion(targetVersion, false)
}

273
cmd/migrate/main.go Normal file
View File

@@ -0,0 +1,273 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"kjol/config"
"kjol/dbutil"
_ "time/tzdata"
_ "github.com/lib/pq"
)
func invalidInput() {
fmt.Println("Usage: [up, down, drop, goto {V}, new {migration name}]")
os.Exit(1)
}
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 newCmd(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
}
func main() {
envfile := flag.String("env-file", "", "Load environment variables from this file instead of the implicit ./.env. Real environment variables always take precedence.")
dir := flag.String("dir", "./migrations", "Directory containing migration files.")
flag.Parse()
var dbc struct {
Username string `env:"DATABASE_USERNAME"`
Password string `env:"DATABASE_PASSWORD"`
Host string `env:"DATABASE_HOST"`
Port int `env:"DATABASE_PORT"`
Name string `env:"DATABASE_NAME"`
Schema string `env:"DATABASE_SCHEMA"`
SSLMode string `env:"DATABASE_SSL_MODE"`
}
if err := config.Load(*envfile, &dbc); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
connectionString := dbutil.BuildConnectionString(dbutil.ConnConfig{
Username: dbc.Username,
Password: dbc.Password,
Host: dbc.Host,
Port: dbc.Port,
Name: dbc.Name,
Schema: dbc.Schema,
SSLMode: dbc.SSLMode,
})
// parse CLI args and do actions
args := flag.Args()
if len(args) < 1 {
invalidInput()
}
// handle "new" command early — it doesn't need a database connection
if args[0] == "new" {
if len(args) < 2 {
fmt.Println("Please provide a name for the new migration.")
os.Exit(1)
}
newCmd(*dir, time.Now(), defaultTimeFormat, args[1], "sql", true, 7, true)
return
}
eng, err := newEngine(*dir, connectionString, dbc.Schema)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
defer eng.close()
migrateNum := 0
if len(args) >= 2 {
var parseErr error
migrateNum, parseErr = strconv.Atoi(args[1])
if parseErr != nil {
fmt.Println("Please provide a valid migration number.")
os.Exit(1)
}
}
switch args[0] {
case "up":
if err := eng.up(); err != nil {
if errors.Is(err, errNoChange) {
fmt.Println("No change")
os.Exit(0)
}
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println("Database successfully migrated to latest version")
case "down":
if err := eng.down(); err != nil {
if errors.Is(err, errNoChange) {
fmt.Println("No change")
os.Exit(0)
}
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println("Database successfully migrated to previous version")
case "goto":
if err := eng.goTo(uint(migrateNum)); err != nil {
if errors.Is(err, errNoChange) {
fmt.Println("No change")
os.Exit(0)
}
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Printf("Database successfully migrated to version `%d`\n", migrateNum)
case "drop":
if err := eng.dropAll(); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println("Database tables, views, and types successfully dropped")
default:
invalidInput()
}
}

151
cmd/migrate/source.go Normal file
View File

@@ -0,0 +1,151 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
)
type direction string
const (
dirUp direction = "up"
dirDown direction = "down"
)
type migrationFile struct {
Version uint
Identifier string
Direction direction
Filename string // e.g. "0000001_INITIAL_CREATE.up.sql"
}
type migrationSource struct {
dir string
index []uint // sorted unique version numbers
migrations map[uint]map[direction]*migrationFile // version -> direction -> file
}
// matches: 0000001_INITIAL_CREATE.up.sql
var migrationRegex = regexp.MustCompile(`^([0-9]+)_(.*)\.(up|down)\.(.*)$`)
func newMigrationSource(dir string) (*migrationSource, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("failed to read migrations directory: %w", err)
}
s := &migrationSource{
dir: dir,
migrations: make(map[uint]map[direction]*migrationFile),
}
versionSet := make(map[uint]struct{})
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
m := migrationRegex.FindStringSubmatch(name)
if len(m) != 5 {
// skip non-matching filenames (same as library)
continue
}
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
if err != nil {
continue
}
version := uint(versionUint64)
dir := direction(m[3])
mf := &migrationFile{
Version: version,
Identifier: m[2],
Direction: dir,
Filename: name,
}
if _, ok := s.migrations[version]; !ok {
s.migrations[version] = make(map[direction]*migrationFile)
}
if _, exists := s.migrations[version][dir]; exists {
return nil, fmt.Errorf("duplicate migration version %d direction %s", version, dir)
}
s.migrations[version][dir] = mf
versionSet[version] = struct{}{}
}
// build sorted index
s.index = make([]uint, 0, len(versionSet))
for v := range versionSet {
s.index = append(s.index, v)
}
sort.Slice(s.index, func(i, j int) bool {
return s.index[i] < s.index[j]
})
return s, nil
}
func (s *migrationSource) first() (uint, error) {
if len(s.index) == 0 {
return 0, os.ErrNotExist
}
return s.index[0], nil
}
func (s *migrationSource) next(version uint) (uint, error) {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] > version
})
if pos >= len(s.index) {
return 0, os.ErrNotExist
}
return s.index[pos], nil
}
func (s *migrationSource) prev(version uint) (uint, error) {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] >= version
})
// pos is the index of version (or where it would be inserted)
// we want the one before it
if pos <= 0 {
return 0, os.ErrNotExist
}
return s.index[pos-1], nil
}
func (s *migrationSource) readUp(version uint) (string, error) {
if dirs, ok := s.migrations[version]; ok {
if mf, ok := dirs[dirUp]; ok {
return filepath.Join(s.dir, mf.Filename), nil
}
}
return "", os.ErrNotExist
}
func (s *migrationSource) readDown(version uint) (string, error) {
if dirs, ok := s.migrations[version]; ok {
if mf, ok := dirs[dirDown]; ok {
return filepath.Join(s.dir, mf.Filename), nil
}
}
return "", os.ErrNotExist
}
func (s *migrationSource) versionExists(version uint) bool {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] >= version
})
return pos < len(s.index) && s.index[pos] == version
}

23
cmd/passgen/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"log"
"os"
"kjol/appenv"
"kjol/security"
)
func main() {
if len(os.Args) == 2 {
passHash, _ := security.HashPassword(os.Args[1])
println(passHash)
} else {
println("Please input a password as first program argument")
}
if appenv.Environment == appenv.EnvTypeDevelopment {
log.Printf("log message")
}
}

150
cmd/typecheck/main.go Normal file
View File

@@ -0,0 +1,150 @@
package main
// Frontend TypeScript checker. Runs tsc in noEmit mode using
// tsconfig.json. Requires node on PATH; downloads the pinned TypeScript
// release on first run (no npm).
//
// go run ./cmd/typecheck
// go run ./cmd/typecheck -p tsconfig.json
import (
"archive/tar"
"compress/gzip"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
const typescriptVersion = "5.8.3"
func main() {
tsconfig := flag.String("p", "tsconfig.json", "path to tsconfig.json")
flag.Parse()
if err := runTypecheck(*tsconfig); err != nil {
fmt.Fprintf(os.Stderr, "Typecheck failed: %v\n", err)
os.Exit(1)
}
}
func runTypecheck(tsconfig string) error {
node, err := exec.LookPath("node")
if err != nil {
return fmt.Errorf("node not found on PATH (required to run tsc): %w", err)
}
tsc, err := ensureTypeScript()
if err != nil {
return err
}
if _, err := os.Stat(tsconfig); err != nil {
return fmt.Errorf("tsconfig not found: %s", tsconfig)
}
fmt.Printf("Typechecking with TypeScript %s...\n", typescriptVersion)
cmd := exec.Command(node, tsc, "--noEmit", "-p", tsconfig)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
fmt.Println("No type errors.")
return nil
}
func ensureTypeScript() (string, error) {
root, err := os.Getwd()
if err != nil {
return "", err
}
cacheDir := filepath.Join(root, "tools", ".cache", "typescript", typescriptVersion)
tscPath := filepath.Join(cacheDir, "package", "lib", "tsc.js")
if _, err := os.Stat(tscPath); err == nil {
return tscPath, nil
}
fmt.Printf("Downloading TypeScript %s...\n", typescriptVersion)
if err := downloadTypeScript(cacheDir); err != nil {
return "", err
}
if _, err := os.Stat(tscPath); err != nil {
return "", fmt.Errorf("tsc not found after download: %s", tscPath)
}
return tscPath, nil
}
func downloadTypeScript(destDir string) error {
url := fmt.Sprintf("https://registry.npmjs.org/typescript/-/typescript-%s.tgz", typescriptVersion)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("download typescript: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download typescript: HTTP %s", resp.Status)
}
if err := os.MkdirAll(destDir, 0o755); err != nil {
return err
}
return extractTGZ(resp.Body, destDir)
}
func extractTGZ(r io.Reader, destDir string) error {
gz, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("read typescript archive: %w", err)
}
defer gz.Close()
tr := tar.NewReader(gz)
cleanDest := filepath.Clean(destDir)
for {
hdr, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("read typescript archive: %w", err)
}
target := filepath.Join(destDir, filepath.FromSlash(hdr.Name))
cleanTarget := filepath.Clean(target)
if cleanTarget != cleanDest && !strings.HasPrefix(cleanTarget, cleanDest+string(os.PathSeparator)) {
return fmt.Errorf("invalid archive path: %s", hdr.Name)
}
switch hdr.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, 0o755); err != nil {
return err
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)&0o777|0o600)
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
}
}
}