sakila/internal/httpapi/films.go
siyahas 54e36528bc Add actor, film, and category APIs with SQL query support
- Generated SQL queries for actors, films, categories.
- Introduced HTTP handlers for actor, film, and category endpoints.
- Included utility functions for parsing query parameters and building URLs.
- Enabled pagination, filtering, and HAL representation for responses.
2025-09-02 16:12:30 +03:00

459 lines
14 KiB
Go

package httpapi
import (
"database/sql"
"net/url"
"strconv"
"strings"
hres "form-builder-be-go/internal/hal"
sq "form-builder-be-go/internal/sqlc"
"github.com/gin-gonic/gin"
"github.com/raff/halgo"
)
func RegisterFilmRoutes(r *gin.Engine, db *sql.DB) {
// List films
r.GET("/films", func(c *gin.Context) {
limit := parseIntDefault(c.Query("limit"), 20)
if limit > 100 {
limit = 100
}
offset := parseIntDefault(c.Query("offset"), 0)
order := c.Query("order")
expand := parseExpand(c.Query("expand"))
langName := strings.TrimSpace(c.Query("language.name"))
catName := strings.TrimSpace(c.Query("category.name"))
q := sq.New(db)
var (
films []sq.Film
total int64
err error
)
if langName != "" {
films, err = q.ListFilmsByLanguageName(c, sq.ListFilmsByLanguageNameParams{UPPER: langName, Limit: int64(limit), Offset: int64(offset)})
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
total, err = q.CountFilmsByLanguageName(c, langName)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
} else if catName != "" {
films, err = q.ListFilmsByCategoryName(c, sq.ListFilmsByCategoryNameParams{UPPER: catName, Limit: int64(limit), Offset: int64(offset)})
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
total, err = q.CountFilmsByCategoryName(c, catName)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
} else {
films, err = q.ListFilms(c, sq.ListFilmsParams{Limit: int64(limit), Offset: int64(offset)})
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
total, err = q.CountFilms(c)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
}
selfURL := buildURL("/films", limit, offset, order, c.Query("expand"))
if langName != "" {
sep := "?"
if strings.Contains(selfURL, "?") {
sep = "&"
}
selfURL = selfURL + sep + "language.name=" + url.QueryEscape(langName)
}
if catName != "" {
sep := "?"
if strings.Contains(selfURL, "?") {
sep = "&"
}
selfURL = selfURL + sep + "category.name=" + url.QueryEscape(catName)
}
res := hres.New(selfURL)
res.AddCurie()
res.AddLink("self", halgo.Link{Href: selfURL})
if int64(offset+limit) < total {
nextURL := buildURL("/films", limit, offset+limit, order, c.Query("expand"))
if langName != "" {
sep := "?"
if strings.Contains(nextURL, "?") {
sep = "&"
}
nextURL += sep + "language.name=" + url.QueryEscape(langName)
}
if catName != "" {
sep := "?"
if strings.Contains(nextURL, "?") {
sep = "&"
}
nextURL += sep + "category.name=" + url.QueryEscape(catName)
}
res.AddLink("next", halgo.Link{Href: nextURL})
}
if offset > 0 {
prev := offset - limit
if prev < 0 {
prev = 0
}
prevURL := buildURL("/films", limit, prev, order, c.Query("expand"))
if langName != "" {
sep := "?"
if strings.Contains(prevURL, "?") {
sep = "&"
}
prevURL += sep + "language.name=" + url.QueryEscape(langName)
}
if catName != "" {
sep := "?"
if strings.Contains(prevURL, "?") {
sep = "&"
}
prevURL += sep + "category.name=" + url.QueryEscape(catName)
}
res.AddLink("prev", halgo.Link{Href: prevURL})
}
var embedded []interface{}
for _, f := range films {
itemURL := "/films/" + strconv.FormatInt(f.FilmID, 10)
relActorsURL := itemURL + "/actors"
item := hres.New(itemURL)
item.SetState(f)
item.AddLink("self", halgo.Link{Href: itemURL})
item.AddLink("sakila:actors", halgo.Link{Href: relActorsURL})
// language relations
item.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)})
if f.OriginalLanguageID.Valid {
item.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)})
}
// optional embeds
if expand["language"] {
lang, err := q.GetLanguage(c, f.LanguageID)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10))
lr.SetState(lang)
lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)})
item.Embed("language", lr)
}
if expand["original_language"] && f.OriginalLanguageID.Valid {
olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10))
olr.SetState(olang)
olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)})
item.Embed("original_language", olr)
}
if expand["actors"] {
actors, err := q.ListActorsByFilm(c, f.FilmID)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
var actorEmbeds []interface{}
for _, a := range actors {
actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10)
ar := hres.New(actorURL)
ar.SetState(a)
ar.AddLink("self", halgo.Link{Href: actorURL})
ar.AddLink("films", halgo.Link{Href: actorURL + "/films"})
actorEmbeds = append(actorEmbeds, ar)
}
item.Embed("actors", actorEmbeds)
}
embedded = append(embedded, item)
}
res.Embed("films", embedded)
res.SetState(gin.H{"count": len(films), "total": total, "limit": limit, "offset": offset})
c.Header("Content-Type", "application/hal+json")
c.JSON(200, res)
})
// Get film
r.GET("/films/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(400, gin.H{"error": "invalid id"})
return
}
expand := parseExpand(c.Query("expand"))
q := sq.New(db)
f, err := q.GetFilm(c, id)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(500, gin.H{"error": err.Error()})
return
}
self := "/films/" + strconv.FormatInt(f.FilmID, 10)
relActors := self + "/actors"
res := hres.New(self)
res.AddCurie()
res.SetState(f)
res.AddLink("self", halgo.Link{Href: self})
res.AddLink("sakila:actors", halgo.Link{Href: relActors})
// language relations
res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.LanguageID, 10)})
if f.OriginalLanguageID.Valid {
res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(f.OriginalLanguageID.Int64, 10)})
}
// optional embeds
if expand["language"] {
lang, err := q.GetLanguage(c, f.LanguageID)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
lr := hres.New("/languages/" + strconv.FormatInt(lang.LanguageID, 10))
lr.SetState(lang)
lr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(lang.LanguageID, 10)})
res.Embed("language", lr)
}
if expand["original_language"] && f.OriginalLanguageID.Valid {
olang, err := q.GetLanguage(c, f.OriginalLanguageID.Int64)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
olr := hres.New("/languages/" + strconv.FormatInt(olang.LanguageID, 10))
olr.SetState(olang)
olr.AddLink("self", halgo.Link{Href: "/languages/" + strconv.FormatInt(olang.LanguageID, 10)})
res.Embed("original_language", olr)
}
if expand["actors"] {
actors, err := q.ListActorsByFilm(c, f.FilmID)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
var actorEmbeds []interface{}
for _, a := range actors {
actorURL := "/actors/" + strconv.FormatInt(int64(a.ActorID), 10)
ar := hres.New(actorURL)
ar.SetState(a)
ar.AddLink("self", halgo.Link{Href: actorURL})
ar.AddLink("films", halgo.Link{Href: actorURL + "/films"})
actorEmbeds = append(actorEmbeds, ar)
}
res.Embed("actors", actorEmbeds)
}
c.Header("Content-Type", "application/hal+json")
c.JSON(200, res)
})
/*
// Create film
r.POST("/films", func(c *gin.Context) {
var in struct {
FilmID *int64 `json:"film_id"`
Title string `json:"title"`
Description *string `json:"description"`
ReleaseYear *string `json:"release_year"`
LanguageID int64 `json:"language_id"`
OriginalLangID *int64 `json:"original_language_id"`
RentalDuration *int64 `json:"rental_duration"`
RentalRate *float64 `json:"rental_rate"`
Length *int64 `json:"length"`
ReplacementCost *float64 `json:"replacement_cost"`
Rating *string `json:"rating"`
SpecialFeatures *string `json:"special_features"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if strings.TrimSpace(in.Title) == "" || in.LanguageID == 0 {
c.JSON(400, gin.H{"error": "title and language_id are required"})
return
}
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
if in.RentalDuration != nil {
f.RentalDuration = *in.RentalDuration
} else {
f.RentalDuration = 3
}
if in.RentalRate != nil {
f.RentalRate = *in.RentalRate
} else {
f.RentalRate = 4.99
}
if in.Length != nil {
f.Length = in.Length
}
if in.ReplacementCost != nil {
f.ReplacementCost = *in.ReplacementCost
} else {
f.ReplacementCost = 19.99
}
if in.Rating != nil {
f.Rating = in.Rating
}
if in.SpecialFeatures != nil {
f.SpecialFeatures = in.SpecialFeatures
}
if in.FilmID != nil {
f.FilmID = *in.FilmID
}
created, err := sq.CreateFilm(c, db, &f)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
self := "/films/" + strconv.FormatInt(created.FilmID, 10)
res := hres.New(self)
res.AddCurie()
res.SetState(created)
res.AddLink("self", halgo.Link{Href: self})
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
c.Header("Content-Type", "application/hal+json")
c.Header("Location", self)
c.JSON(201, res)
})
// Replace film
r.PUT("/films/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(400, gin.H{"error": "invalid id"})
return
}
var in struct {
Title string `json:"title"`
Description *string `json:"description"`
ReleaseYear *string `json:"release_year"`
LanguageID int64 `json:"language_id"`
OriginalLangID *int64 `json:"original_language_id"`
RentalDuration *int64 `json:"rental_duration"`
RentalRate *float64 `json:"rental_rate"`
Length *int64 `json:"length"`
ReplacementCost *float64 `json:"replacement_cost"`
Rating *string `json:"rating"`
SpecialFeatures *string `json:"special_features"`
}
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
if in.RentalDuration != nil {
f.RentalDuration = *in.RentalDuration
} else {
f.RentalDuration = 3
}
if in.RentalRate != nil {
f.RentalRate = *in.RentalRate
} else {
f.RentalRate = 4.99
}
if in.Length != nil {
f.Length = in.Length
}
if in.ReplacementCost != nil {
f.ReplacementCost = *in.ReplacementCost
} else {
f.ReplacementCost = 19.99
}
if in.Rating != nil {
f.Rating = in.Rating
}
if in.SpecialFeatures != nil {
f.SpecialFeatures = in.SpecialFeatures
}
updated, err := sq.UpdateFilmPut(c, db, id, f)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(400, gin.H{"error": err.Error()})
return
}
self := "/films/" + strconv.FormatInt(updated.FilmID, 10)
res := hres.New(self)
res.AddCurie()
res.SetState(updated)
res.AddLink("self", halgo.Link{Href: self})
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
c.Header("Content-Type", "application/hal+json")
c.JSON(200, res)
})
// Patch film
r.PATCH("/films/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(400, gin.H{"error": "invalid id"})
return
}
var in struct {
Title *string `json:"title"`
Description *string `json:"description"`
ReleaseYear *string `json:"release_year"`
LanguageID *int64 `json:"language_id"`
OriginalLangID *int64 `json:"original_language_id"`
RentalDuration *int64 `json:"rental_duration"`
RentalRate *float64 `json:"rental_rate"`
Length *int64 `json:"length"`
ReplacementCost *float64 `json:"replacement_cost"`
Rating *string `json:"rating"`
SpecialFeatures *string `json:"special_features"`
}
patch := sq.FilmPatch{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID, RentalDuration: in.RentalDuration, RentalRate: in.RentalRate, Length: in.Length, ReplacementCost: in.ReplacementCost, Rating: in.Rating, SpecialFeatures: in.SpecialFeatures}
updated, err := sq.PatchFilm(c, db, id, patch)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(400, gin.H{"error": err.Error()})
return
}
self := "/films/" + strconv.FormatInt(updated.FilmID, 10)
res := hres.New(self)
res.AddCurie()
res.SetState(updated)
res.AddLink("self", halgo.Link{Href: self})
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
c.Header("Content-Type", "application/hal+json")
c.JSON(200, res)
})
// Delete film
r.DELETE("/films/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(400, gin.H{"error": "invalid id"})
return
}
if err := sq.DeleteFilm(c, db, id); err != nil {
if err == sql.ErrNoRows {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.Status(204)
})
*/
}