FEAT: Init Discovery Service
Build, Push and Deploy Discovery / build-push-deploy (push) Successful in 1m45s
Deploy Infrastructure / pulumi-up (push) Successful in 2s

This commit is contained in:
FahdShalhoub
2026-08-27 22:03:41 +03:00
parent 0cfd89f200
commit 98950aba20
20 changed files with 719 additions and 6 deletions
+23
View File
@@ -0,0 +1,23 @@
// Package api holds the request and response bodies of the HTTP API — the
// wire contract, kept apart from the handlers that serve it. The swaggo
// annotations on the handlers reference these types by name
// (api.HealthResponse and so on), so renaming one changes the generated spec.
package api
// HealthResponse is the body of GET /health.
type HealthResponse struct {
Status string `json:"status" example:"ok"`
}
// ProblemDetails is an error body in the RFC 9457 "Problem Details for HTTP
// APIs" format. Type stays "about:blank" — the value RFC 9457 defines for
// problems with no dedicated documentation URI. Title is a short summary that
// stays identical for every occurrence of the same problem, so clients can
// branch on it; Detail explains this particular occurrence and is the only
// member that varies with request data.
type ProblemDetails struct {
Type string `json:"type" example:"about:blank"`
Title string `json:"title" example:"Something Went Wrong Loading The Catalogue"`
Status int `json:"status" example:"500"`
Detail string `json:"detail,omitempty" example:"The catalogue could not be read from the database. This is a server-side fault and the request was not processed; retrying in a few moments may succeed."`
}
+30
View File
@@ -0,0 +1,30 @@
package db
import (
"context"
"database/sql"
"fmt"
"log"
)
func CreateDBConnection(connectionString string) *sql.DB {
sqlDB, err := sql.Open("postgres", connectionString)
if err != nil {
panic(fmt.Errorf("db: cannot open connection: %w", err))
}
return sqlDB
}
func CloseConnection(SQLDB *sql.DB) {
err := SQLDB.Close()
if err != nil {
log.Fatalf("Error Closing DB Connection: %s", err)
}
}
func AssertSuccessfulConnection(ctx context.Context, SQLDB *sql.DB) {
if err := SQLDB.PingContext(ctx); err != nil {
panic(fmt.Errorf("db: cannot connect: %w", err))
}
}
+38
View File
@@ -0,0 +1,38 @@
package db
import (
"database/sql"
"embed"
"errors"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func Migrate(sqlDB *sql.DB) error {
driver, err := postgres.WithInstance(sqlDB, &postgres.Config{})
if err != nil {
return fmt.Errorf("db: creating migration driver: %w", err)
}
source, err := iofs.New(migrationsFS, "migrations")
if err != nil {
return fmt.Errorf("db: loading embedded migrations: %w", err)
}
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
if err != nil {
return fmt.Errorf("db: initializing migrator: %w", err)
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("db: applying migrations: %w", err)
}
return nil
}
@@ -0,0 +1,2 @@
-- Nothing to undo: 0001 creates no objects.
SELECT 1;
@@ -0,0 +1,6 @@
-- Discovery's schema starts here. The service owns its own database — the
-- `discovery` role and same-named database provisioned by newServiceDatabase
-- in infrastructure/main.go — and shares no tables with cms, so nothing is
-- created yet. The file exists because internal/db/migrate.go embeds
-- migrations/*.sql: with no migration at all the package would not compile.
SELECT 1;
@@ -0,0 +1,7 @@
// Package repositories holds every SQL statement the service runs. A
// repository is a struct carrying the *sql.DB opened by db.CreateDBConnection
// and is assigned to a package-level var in main, the way cms wires VideoRepo
// and CatagoriesRepo, so handlers call through something substitutable.
//
// Empty until discovery has tables to read.
package repositories
+41
View File
@@ -0,0 +1,41 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"thamanyah/discovery/internal/api"
)
// Health is the liveness probe the ALB target group polls.
//
// @Summary Health check
// @Description Reports that the service is up and serving. Used as the ALB target group health check; it does not verify the database connection.
// @Tags system
// @Produce json
// @Success 200 {object} api.HealthResponse
// @Router /health [get]
func Health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, api.HealthResponse{Status: "ok"})
}
func writeJSON(w http.ResponseWriter, status int, body any) {
writeJSONContent(w, "application/json", status, body)
}
func writeProblem(w http.ResponseWriter, status int, title, detail string) {
writeJSONContent(w, "application/problem+json", status, api.ProblemDetails{
Type: "about:blank",
Title: title,
Status: status,
Detail: detail,
})
}
func writeJSONContent(w http.ResponseWriter, contentType string, status int, body any) {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(body); err != nil {
log.Printf("Something Went Wrong Writing The Response: %s", err)
}
}
+8
View File
@@ -0,0 +1,8 @@
// Package models holds the domain types the rest of the service passes
// around, the way cms holds Video and Category: plain Go structs with no JSON
// tags and no SQL, so that renaming a field here never moves the published
// API (that lives in internal/api) and never rewrites a query (that lives in
// internal/db/repositories).
//
// Empty until discovery has a domain to model.
package models