Skip to content

Instantly share code, notes, and snippets.

@richhh7g
Created March 21, 2024 22:31
Show Gist options
  • Save richhh7g/5284c94de9abc4d5f960f6dc21b4b1cc to your computer and use it in GitHub Desktop.
Save richhh7g/5284c94de9abc4d5f960f6dc21b4b1cc to your computer and use it in GitHub Desktop.
Go Lang - Product CRUD CLI
// go run product_crud.go --action Get --product '{"id": "uuid"}'
// go run product_crud.go --action All
// go run product_crud.go --action Create --product '{"name": "Example product", "price": 1000.0}'
// go run product_crud.go --action Update --product '{"id": "uuid", "name": "Example product", "price": 1000.0}'
// go run product_crud.go --action Delete --product '{"id": "uuid"}'
package main
import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"flag"
"io"
"log"
"os"
"path/filepath"
"github.com/google/uuid"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
type Product struct {
Id string
Name string
Price float64
}
type ProductJSON struct {
Id string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
type CLIArgs struct {
Filename string
Product ProductJSON
Action ActionArg
}
type ActionArg string
const (
ActionGet ActionArg = "Get"
ActionAll ActionArg = "All"
ActionCreate ActionArg = "Create"
ActionUpdate ActionArg = "Update"
ActionDelete ActionArg = "Delete"
)
func main() {
args, err := ParseCLIArgs()
if err != nil {
log.Fatal(err)
}
db, err := ConnectSQLiteDB(&args.Filename)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = RunAction(args.Action, db, &args)
if err != nil {
log.Fatal(err)
}
}
func ParseCLIArgs() (CLIArgs, error) {
const DEFAULT_ACTION = string(ActionAll)
const DEFAULT_FILENAME = "database.sqlite3"
flag.Usage = func() {
flag.PrintDefaults()
}
actionParam := flag.String("action", DEFAULT_ACTION, `--action Get | All | Create | Update | Delete`)
productParam := flag.String("product", "", `--product '{"name": "Example product", "price": 1000.0}'`)
filenameParam := flag.String("filename", DEFAULT_FILENAME, "--filename name.extension")
flag.Parse()
isDefaultAction := ActionArg(DEFAULT_ACTION) == ActionArg(*actionParam)
if *productParam == "" && !isDefaultAction {
return CLIArgs{}, errors.New("the --product parameter cannot be empty")
}
productJson := ProductJSON{}
params := CLIArgs{
Filename: *filenameParam,
Product: productJson,
Action: ActionArg(*actionParam),
}
if isDefaultAction {
return params, nil
}
err := json.Unmarshal([]byte(*productParam), &productJson)
if err != nil {
return CLIArgs{}, errors.New("error when parsing JSON")
}
params.Product = productJson
return params, nil
}
func RunAction(action ActionArg, db *sql.DB, args *CLIArgs) error {
switch args.Action {
case ActionGet:
getFn := getAction(args.Action, actionGetFn)
product, err := getFn(db, args)
if err != nil {
return errors.New("error getting product from database")
}
var buff bytes.Buffer
err = json.NewEncoder(&buff).Encode(product)
if err != nil {
return errors.New("error encoding product to JSON")
}
io.CopyBuffer(os.Stdout, &buff, nil)
return nil
case ActionAll:
getAllFn := getAction(args.Action, actionGetAllFn)
products, err := getAllFn(db)
if err != nil {
return errors.New("error getting products from database")
}
var buff bytes.Buffer
err = json.NewEncoder(&buff).Encode(products)
if err != nil {
return errors.New("error encoding products to JSON")
}
io.CopyBuffer(os.Stdout, &buff, nil)
return nil
case ActionCreate:
createFn := getAction(args.Action, actionCreateFn)
return createFn(db, args)
case ActionUpdate:
updateFn := getAction(args.Action, actionUpdateFn)
product, err := updateFn(db, args)
if err != nil {
return err
}
var buff bytes.Buffer
err = json.NewEncoder(&buff).Encode(&product)
if err != nil {
return errors.New("error encoding product to JSON")
}
io.CopyBuffer(os.Stdout, &buff, nil)
return nil
case ActionDelete:
deleteFn := getAction(args.Action, actionDeleteFn)
return deleteFn(db, args.Product.Id)
default:
getAllFn := getAction(args.Action, actionGetAllFn)
products, err := getAllFn(db)
if err != nil {
return errors.New("error getting products from database")
}
var buff bytes.Buffer
err = json.NewEncoder(&buff).Encode(products)
if err != nil {
return errors.New("error encoding products to JSON")
}
io.CopyBuffer(os.Stdout, &buff, nil)
return nil
}
}
func getAction[TArg ActionArg, TF any](action TArg, actionFunc TF) TF {
actionsMap := map[TArg]TF{
TArg(ActionGet): actionFunc,
TArg(ActionAll): actionFunc,
TArg(ActionCreate): actionFunc,
TArg(ActionUpdate): actionFunc,
TArg(ActionDelete): actionFunc,
}
return actionsMap[action]
}
func actionGetFn(db *sql.DB, args *CLIArgs) (Product, error) {
return findProductDB(db, args.Product.Id)
}
func actionGetAllFn(db *sql.DB) ([]Product, error) {
return findAllProductsDB(db)
}
func actionCreateFn(db *sql.DB, args *CLIArgs) error {
var product Product = Product{}
productJSONB, err := json.Marshal(args.Product)
if err != nil {
return errors.New("error marshaling product JSON")
}
jsonErr := json.Unmarshal(productJSONB, &product)
if jsonErr != nil {
return errors.New("error unmarshaling product JSON")
}
product.Id = uuid.NewString()
return insertProductDB(db, &product)
}
func actionUpdateFn(db *sql.DB, args *CLIArgs) (*Product, error) {
product := Product{}
productJSONB, err := json.Marshal(args.Product)
if err != nil {
return &product, errors.New("error marshaling product JSON")
}
jsonErr := json.Unmarshal(productJSONB, &product)
if jsonErr != nil {
return &product, errors.New("error unmarshaling product JSON")
}
return updateProductDB(db, &product)
}
func actionDeleteFn(db *sql.DB, productId string) error {
return deleteProductDB(db, productId)
}
func findProductDB(db *sql.DB, productId string) (Product, error) {
const findProductQuery = "SELECT * FROM products WHERE id = $1;"
product := Product{}
stmt, err := db.Prepare(findProductQuery)
if err != nil {
return product, errors.New("error when preparing the query to find product")
}
defer stmt.Close()
err = stmt.QueryRow(productId).Scan(&product.Id, &product.Name, &product.Price)
if err != nil {
return product, errors.New("error finding product in database")
}
return product, nil
}
func findAllProductsDB(db *sql.DB) ([]Product, error) {
const findAllProductQuery = "SELECT * FROM products;"
products := []Product{}
rows, err := db.Query(findAllProductQuery)
if err != nil {
return nil, errors.New("error finding products in database")
}
defer rows.Close()
for rows.Next() {
product := Product{}
err := rows.Scan(&product.Id, &product.Name, &product.Price)
if err != nil {
return nil, errors.New("error finding products in database")
}
products = append(products, product)
}
return products, nil
}
func insertProductDB(db *sql.DB, product *Product) error {
const insertQuery = "INSERT INTO products(id, name, price) VALUES($1, $2, $3);"
if product == nil {
return errors.New("product is null, cannot insert into database")
}
stmt, err := db.Prepare(insertQuery)
if err != nil {
return errors.New("error when preparing the query to create a new product")
}
defer stmt.Close()
_, err = stmt.Exec(&product.Id, &product.Name, &product.Price)
if err != nil {
return errors.New("error when inserting product into database")
}
return nil
}
func updateProductDB(db *sql.DB, product *Product) (*Product, error) {
const updateProductQuery = "UPDATE products SET name = $1, price = $2 WHERE id = $3;"
if product == nil {
return nil, errors.New("product is null, cannot insert into database")
}
stmt, err := db.Prepare(updateProductQuery)
if err != nil {
return nil, errors.New("error when preparing the query to update product")
}
defer stmt.Close()
result, err := stmt.Exec(product.Name, product.Price, product.Id)
if err != nil {
return nil, errors.New("error when updating product into database")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, errors.New("error when getting rows affected by updating")
}
if rowsAffected == 0 {
return nil, errors.New("no rows were updated, product may not exist")
}
return product, nil
}
func deleteProductDB(db *sql.DB, productId string) error {
const deleteProductQuery = "DELETE FROM products WHERE id = $1;"
stmt, err := db.Prepare(deleteProductQuery)
if err != nil {
return errors.New("error when preparing the query to delete product")
}
defer stmt.Close()
result, err := stmt.Exec(productId)
if err != nil {
return errors.New("error when deleting product into database")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.New("error when getting rows affected by deletion")
}
if rowsAffected == 0 {
return errors.New("no rows were deleted, product may not exist")
}
return nil
}
func ConnectSQLiteDB(filename *string) (*sql.DB, error) {
const DRIVER_NAME = "sqlite3"
err := newDBFile(filename)
if err != nil {
return nil, err
}
db, err := sql.Open(DRIVER_NAME, *filename)
if err != nil {
return nil, errors.New("error connecting to database")
}
initialSchemaQuery := "CREATE TABLE IF NOT EXISTS products(id VARCHAR(255) NOT NULL PRIMARY KEY, name VARCHAR(100) NOT NULL, price REAL NOT NULL);"
_, err = db.Exec(initialSchemaQuery)
if err != nil {
return nil, errors.New("error creating initial table")
}
return db, nil
}
func newDBFile(filename *string) error {
dir, _ := os.Getwd()
filePath := filepath.Join(dir, *filename)
_, fileInfoError := os.Stat(filePath)
if !os.IsExist(fileInfoError) {
return nil
}
_, createFileError := os.Create(*filename)
if createFileError != nil {
return errors.New("error creating database file")
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment