Skip to content

Commit 5dce2a2

Browse files
authored
feat: add core backend infrastructure
- Add structured logging system with terminal + file output and rotation - Implement collections CRUD operations with validation and error handling - Add required database schema for the same - Account for possible DB connection leak during application startup
2 parents 42a73e0 + 7d67baa commit 5dce2a2

17 files changed

Lines changed: 1087 additions & 27 deletions

.air.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
root = "."
2+
testdata_dir = "testdata"
3+
tmp_dir = "tmp"
4+
5+
[build]
6+
bin = "tmp/req"
7+
args_bin = ["-verbose"]
8+
cmd = "go build -o ./tmp/req ."
9+
delay = 1000
10+
exclude_dir = ["tmp", "testdata"]
11+
include_ext = ["go"]
12+
13+
[log]
14+
time = true
15+
main_only = false
16+
17+
[misc]
18+
clean_on_exit = true
19+
20+
[screen]
21+
clear_on_rebuild = true
22+
keep_scroll = true

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ go.work.sum
2727
# Editor/IDE
2828
.vscode/
2929

30-
3130
# Some nix shit
3231
.gopath/
32+
33+
# air - go hot reloading
34+
tmp/
35+
testdata/
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- +goose Up
2+
CREATE TABLE endpoints (
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
collection_id INTEGER NOT NULL,
5+
name TEXT NOT NULL,
6+
method TEXT NOT NULL,
7+
url TEXT NOT NULL,
8+
headers TEXT DEFAULT '{}' NOT NULL,
9+
query_params TEXT DEFAULT '{}' NOT NULL,
10+
request_body TEXT DEFAULT '' NOT NULL,
11+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
12+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
13+
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
14+
);
15+
16+
-- +goose Down
17+
DROP TABLE endpoints;
18+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
CREATE TRIGGER update_collections_updated_at
4+
AFTER UPDATE ON collections
5+
FOR EACH ROW
6+
BEGIN
7+
UPDATE collections
8+
SET updated_at = CURRENT_TIMESTAMP
9+
WHERE id = NEW.id;
10+
END;
11+
-- +goose StatementEnd
12+
13+
-- +goose Down
14+
DROP TRIGGER IF EXISTS update_collections_updated_at;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- +goose Up
2+
CREATE INDEX idx_collections_name ON collections(name);
3+
4+
-- +goose Down
5+
DROP INDEX IF EXISTS idx_collections_name;

db/queries/collections.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
-- name: CreateCollection :one
22
INSERT INTO collections (name) VALUES (?) RETURNING *;
3+
4+
-- name: GetAllCollections :many
5+
SELECT * FROM collections;
6+
7+
-- name: UpdateCollectionName :one
8+
UPDATE collections
9+
SET name = ?
10+
WHERE id = ?
11+
RETURNING *;
12+
13+
-- name: DeleteCollection :exec
14+
DELETE FROM collections
15+
WHERE id = ?;

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.4
55
require (
66
github.com/mattn/go-sqlite3 v1.14.29
77
github.com/pressly/goose/v3 v3.24.3
8+
gopkg.in/natefinch/lumberjack.v2 v2.2.1
89
)
910

1011
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
3030
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
3131
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
3232
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
33+
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
34+
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
3335
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3436
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3537
modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package collections
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/maniac-en/req/internal/database"
9+
"github.com/maniac-en/req/internal/log"
10+
)
11+
12+
func NewCollectionsManager(q *database.Queries) *CollectionsManager {
13+
collectionsManager := CollectionsManager{
14+
DB: q,
15+
}
16+
return &collectionsManager
17+
}
18+
19+
func (c *CollectionsManager) GetAllCollections(ctx context.Context) ([]database.Collection, error) {
20+
log.Debug("fetching all collections")
21+
dbCollections, err := c.DB.GetAllCollections(ctx)
22+
if err != nil {
23+
log.Error("failed to fetch collections", "error", err)
24+
return nil, err
25+
}
26+
log.Info("fetched collections", "count", len(dbCollections))
27+
return dbCollections, nil
28+
}
29+
30+
func (c *CollectionsManager) CreateCollection(ctx context.Context, name string) (database.Collection, error) {
31+
if err := validateCollectionName(name); err != nil {
32+
log.Error("invalid collection name", "name", name, "error", err)
33+
return database.Collection{}, err
34+
}
35+
36+
log.Info("creating collection", "name", name)
37+
collection, err := c.DB.CreateCollection(ctx, name)
38+
if err != nil {
39+
log.Error("failed to create collection", "name", name, "error", err)
40+
return database.Collection{}, err
41+
}
42+
log.Info("created collection", "id", collection.ID, "name", collection.Name)
43+
return collection, nil
44+
}
45+
46+
func (c *CollectionsManager) UpdateCollectionName(ctx context.Context, name string, collectionId int) (database.Collection, error) {
47+
if err := validateCollectionName(name); err != nil {
48+
log.Error("invalid collection name", "name", name, "error", err)
49+
return database.Collection{}, err
50+
}
51+
52+
id := int64(collectionId)
53+
log.Info("updating collection name", "id", id, "new_name", name)
54+
collection, err := c.DB.UpdateCollectionName(ctx, database.UpdateCollectionNameParams{
55+
Name: name,
56+
ID: id,
57+
})
58+
if err != nil {
59+
log.Error("failed to update collection", "id", id, "name", name, "error", err)
60+
return database.Collection{}, err
61+
}
62+
log.Info("updated collection", "id", collection.ID, "name", collection.Name)
63+
return collection, nil
64+
}
65+
66+
func (c *CollectionsManager) DeleteCollection(ctx context.Context, id int) error {
67+
log.Info("deleting collection", "id", id)
68+
err := c.DB.DeleteCollection(ctx, int64(id))
69+
if err != nil {
70+
log.Error("failed to delete collection", "id", id, "error", err)
71+
return err
72+
}
73+
log.Info("deleted collection", "id", id)
74+
return nil
75+
}
76+
77+
func validateCollectionName(name string) error {
78+
name = strings.TrimSpace(name)
79+
if name == "" {
80+
return fmt.Errorf("collection name cannot be empty")
81+
}
82+
if len(name) > 100 {
83+
return fmt.Errorf("collection name cannot exceed 100 characters")
84+
}
85+
if strings.ContainsAny(name, "/\\:*?\"<>|") {
86+
return fmt.Errorf("collection name contains invalid characters")
87+
}
88+
return nil
89+
}

internal/collections/endpoints.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package collections
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/maniac-en/req/internal/database"
9+
)
10+
11+
func (c *CollectionsManager) CreateEndpoint(ctx context.Context, name, method, url string, collectionId int64) (database.Endpoint, error) {
12+
return c.DB.CreateEndpoint(ctx, database.CreateEndpointParams{
13+
Name: name,
14+
CollectionID: collectionId,
15+
Method: method,
16+
Url: url,
17+
})
18+
}
19+
20+
func (c *CollectionsManager) GetEndpoints(ctx context.Context, collectionId int64) ([]database.Endpoint, error) {
21+
return c.DB.ListEndpoints(ctx, collectionId)
22+
}
23+
24+
func (c *CollectionsManager) GetEndpoint(ctx context.Context, id int64) (database.Endpoint, error) {
25+
return c.DB.GetEndpoint(ctx, id)
26+
}
27+
28+
func (c *CollectionsManager) UpdateEndpoint(
29+
ctx context.Context,
30+
id int64,
31+
name string,
32+
method string,
33+
url string,
34+
headers string,
35+
queryParams string,
36+
requestBody string,
37+
) (database.Endpoint, error) {
38+
existingEndpoint, err := c.DB.GetEndpoint(ctx, id)
39+
if err != nil {
40+
return database.Endpoint{}, err
41+
}
42+
43+
if name != "" {
44+
existingEndpoint.Name = name
45+
}
46+
if method != "" {
47+
existingEndpoint.Method = method
48+
}
49+
if url != "" {
50+
existingEndpoint.Url = url
51+
}
52+
if headers != "" {
53+
if !json.Valid([]byte(headers)) {
54+
return database.Endpoint{}, fmt.Errorf("invalid JSON for header parameters: %s", headers)
55+
}
56+
existingEndpoint.Headers = headers
57+
}
58+
if queryParams != "" {
59+
if !json.Valid([]byte(queryParams)) {
60+
return database.Endpoint{}, fmt.Errorf("invalid JSON for query parameters: %s", queryParams)
61+
}
62+
existingEndpoint.QueryParams = queryParams
63+
}
64+
if requestBody != "" {
65+
existingEndpoint.RequestBody = requestBody
66+
}
67+
68+
return c.DB.UpdateEndpoint(ctx, database.UpdateEndpointParams{
69+
ID: existingEndpoint.ID,
70+
Name: existingEndpoint.Name,
71+
Method: existingEndpoint.Method,
72+
Url: existingEndpoint.Url,
73+
Headers: existingEndpoint.Headers,
74+
QueryParams: existingEndpoint.QueryParams,
75+
RequestBody: existingEndpoint.RequestBody,
76+
})
77+
}

0 commit comments

Comments
 (0)