In the previous part of the series we created the required interface for you invoice generation/management web application.
In this post we are going to provide and implementation of the CompanyRepository
.
In our requirements in the first part of the series we decided that we are going to use SQLite as our DBMS. Let's implement together the integration with SQLite.
Implementing the CompanyRepository
We are going to use GORM to interact with the database. However, we are going to ensure that our database models and code will be decoupled so we can switch ORM/driver or even database in the future.
We are going to work in the sqlite package.
Let's first create a branch
git checkout -b company-repo
and then install the dependencies:
go get gorm.io/gorm
go get gorm.io/driver/sqlite
go get gorm.io/datatypes"
then in our sqlite/sqlite.go
add
package sqlite
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func SetupDB(path string) (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
return nil, err
}
err = db.AutoMigrate(
&dbcompany{},
)
if err != nil {
return nil, err
}
return db, nil
}
Explanation:
SetupDB function creates a connection to the SQLite database and runs the migrations, meaning it creates the required tables if not exists or modifies them according to our schema.
and then create a file sqlite/company.go
package sqlite
import (
"context"
"invoicehub"
"gorm.io/datatypes"
"gorm.io/gorm"
)
var _ invoicehub.CompanyRepository = (*companyRepository)(nil)
type dbcompany struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"type:text"`
Address datatypes.JSONType[invoicehub.Address]
Email string `gorm:"type:text"`
TaxID string `gorm:"type:text"`
VatID string `gorm:"type:text"`
BankAccounts datatypes.JSONSlice[invoicehub.BankAccount]
}
type companyRepository struct {
db *gorm.DB
}
func NewCompanyRepository(db *gorm.DB) invoicehub.CompanyRepository {
return &companyRepository{db: db}
}
func (r *companyRepository) Create(ctx context.Context, company *invoicehub.Company) (int, error) {
// Convert invoicehub.Company to dbcompany
dbCompany := dbcompany{
Name: company.Name,
Address: datatypes.NewJSONType(company.Address),
Email: company.Email,
TaxID: company.TaxID,
VatID: company.VatID,
BankAccounts: datatypes.NewJSONSlice(company.BankAccounts),
}
// Save to database
if err := r.db.WithContext(ctx).Create(&dbCompany).Error; err != nil {
return 0, err
}
company.ID = dbCompany.ID
return company.ID, nil
}
func (r *companyRepository) Update(ctx context.Context, company *invoicehub.Company) error {
var existingCompany dbcompany
if err := r.db.WithContext(ctx).First(&existingCompany, company.ID).Error; err != nil {
return err
}
existingCompany.Name = company.Name
existingCompany.Address = datatypes.NewJSONType(company.Address)
existingCompany.Email = company.Email
existingCompany.TaxID = company.TaxID
existingCompany.VatID = company.VatID
existingCompany.BankAccounts = datatypes.NewJSONSlice(company.BankAccounts)
if err := r.db.WithContext(ctx).Save(&existingCompany).Error; err != nil {
return err
}
return nil
}
func (r *companyRepository) Get(ctx context.Context, id int) (invoicehub.Company, error) {
var dbCompany dbcompany
if err := r.db.WithContext(ctx).First(&dbCompany, id).Error; err != nil {
return invoicehub.Company{}, err
}
company := invoicehub.Company{
ID: dbCompany.ID,
Name: dbCompany.Name,
Address: dbCompany.Address.Data(),
Email: dbCompany.Email,
TaxID: dbCompany.TaxID,
VatID: dbCompany.VatID,
BankAccounts: dbCompany.BankAccounts,
}
return company, nil
}
❓why I used a dbcompany structs that basically reassembles our invoicehub.Company struct
We want our repository implementation to be decoupled with the domain Entity. The reason is flexibility to change in the future. We don't want our domain Entity to include any gorm related code (see the struct tags).
Writing unit tests
We have to verify that our implementation works. Let's write a unit test for that.
For unit test I like to use the testify package
go get github.com/stretchr/testify
Create a file sqlite/company_test.go
.
package sqlite_test
import (
"context"
"invoicehub"
"invoicehub/sqlite"
"testing"
"github.com/stretchr/testify/require"
)
func Test_companyRepository(t *testing.T) {
db, err := sqlite.SetupDB(":memory:")
require.NoError(t, err)
repo := sqlite.NewCompanyRepository(db)
ctx := context.Background()
company := invoicehub.Company{
Name: "Example Ltd",
Email: "info@example.com",
TaxID: "123456789",
VatID: "987654321",
Address: invoicehub.Address{
Street: "123 Example St",
City: "Limassol",
PostalCode: "12345",
Country: "CY",
},
BankAccounts: []invoicehub.BankAccount{
{
BankName: "Example Bank",
AccountNumber: "1234567890",
IBAN: "EX12345678901234567890",
BIC: "EXBIC123",
},
},
}
// Create a new company
id, err := repo.Create(ctx, &company)
require.NoError(t, err)
require.NotZero(t, id)
require.Equal(t, id, company.ID)
// Get the company
company2, err := repo.Get(ctx, id)
require.NoError(t, err)
require.Equal(t, company, company2)
// Update the company
company.Name = "Example Ltd 2"
err = repo.Update(ctx, &company)
require.NoError(t, err)
updatedCompany, err := repo.Get(ctx, id)
require.NoError(t, err)
require.Equal(t, company, updatedCompany)
// not found
_, err = repo.Get(ctx, 999)
require.Error(t, err)
}
💥 We cover just the basics test cases so we have some confidence that it works.
Let's run the tests
go test -v ./...
and you should get something like:
Pass the dependency in our main.go
Open cmd/main.go and do the following changes:
Pay attention, that I set a default path that will work with our docker setup
Also add to the .gitignore
the data.sqlite3
file.
echo "data.sqlite3" >> .gitignore
Now let's test our docker setup.
make dev
notice the created file:
Let's open the file with an sqlite client. On Linux run:
Let's now commit our code so far
As usual you can find the code so far in the relate github branch .
Deployment
We need to setup a new environment variable in our production .env
.
Login to your VPS and add the line:
FH_DB_PATH=invoices.sqlite3
Now you can go back to your machine and run
make deploy
After deployment check that the invoices.sqlite3 file was created .
Conclusion
In this tutorial, we successfully implemented the CompanyRepository
using GORM and SQLite, ensuring our code remains flexible and decoupled from the domain entity. We also wrote unit tests to verify our implementation and made necessary adjustments to our main.go
to integrate the repository. Finally, we configured our production environment and verified the deployment.
In the next article we are going to implement the InvoiceRepository
.
❤️ Please follow me on X or LinkedIn
If you have any questions please reach out to me via a comment or a DM in X.
Find the rest of the tutorials in this series here