Created
March 21, 2024 22:31
-
-
Save richhh7g/5284c94de9abc4d5f960f6dc21b4b1cc to your computer and use it in GitHub Desktop.
Go Lang - Product CRUD CLI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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