sakila/internal/httpapi/films.go
siyahas 5aa6a96b1d Add film CRUD operations and integrate Sakila database enhancement
- Implemented Create, Read, Update (PUT/PATCH), and Delete (CRUD) operations for films.
- Added SQL queries for film management (e.g., NextFilmID, InsertFilm, UpdateFilmPut, PatchFilm, and DeleteFilm).
- Updated HTTP handlers to support enhanced film operations, including validation and null handling.
- Refactored category handling logic for cleaner syntax.
- Included HTTP client test samples for film API in `films_crud_test.http`.
2025-09-08 16:28:19 +03:00

567 lines
17 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"`
OriginalLanguageID *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 == nil {
c.JSON(400, gin.H{"error": "title and language_id are required"})
return
}
q := sq.New(db)
var id int64
if in.FilmID != nil {
id = *in.FilmID
} else {
var err error
id, err = q.NextFilmID(c)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
}
toNullString := func(p *string) sql.NullString {
if p == nil {
return sql.NullString{Valid: false}
}
return sql.NullString{String: *p, Valid: true}
}
toNullInt := func(p *int64) sql.NullInt64 {
if p == nil {
return sql.NullInt64{Valid: false}
}
return sql.NullInt64{Int64: *p, Valid: true}
}
var desc interface{}
if in.Description != nil {
desc = *in.Description
} else {
desc = nil
}
rentalDuration := int64(3)
if in.RentalDuration != nil {
rentalDuration = *in.RentalDuration
}
rentalRate := 4.99
if in.RentalRate != nil {
rentalRate = *in.RentalRate
}
replacementCost := 19.99
if in.ReplacementCost != nil {
replacementCost = *in.ReplacementCost
}
length := toNullInt(in.Length)
err := q.InsertFilm(c, sq.InsertFilmParams{
FilmID: id,
Title: in.Title,
Description: desc,
ReleaseYear: toNullString(in.ReleaseYear),
LanguageID: *in.LanguageID,
OriginalLanguageID: toNullInt(in.OriginalLanguageID),
RentalDuration: rentalDuration,
RentalRate: rentalRate,
Length: length,
ReplacementCost: replacementCost,
Rating: toNullString(in.Rating),
SpecialFeatures: toNullString(in.SpecialFeatures),
})
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
created, err := q.GetFilm(c, id)
if err != nil {
c.JSON(500, 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"})
res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(created.LanguageID, 10)})
if created.OriginalLanguageID.Valid {
res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(created.OriginalLanguageID.Int64, 10)})
}
c.Header("Content-Type", "application/hal+json")
c.Header("Location", self)
c.JSON(201, res)
})
// Replace film (PUT)
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"`
OriginalLanguageID *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) == "" {
c.JSON(400, gin.H{"error": "title is required"})
return
}
q := sq.New(db)
toNullString := func(p *string) sql.NullString {
if p == nil {
return sql.NullString{Valid: false}
}
return sql.NullString{String: *p, Valid: true}
}
toNullInt := func(p *int64) sql.NullInt64 {
if p == nil {
return sql.NullInt64{Valid: false}
}
return sql.NullInt64{Int64: *p, Valid: true}
}
var desc interface{}
if in.Description != nil {
desc = *in.Description
} else {
desc = nil
}
aff, err := q.UpdateFilmPut(c, sq.UpdateFilmPutParams{
Title: in.Title,
Description: desc,
ReleaseYear: toNullString(in.ReleaseYear),
LanguageID: in.LanguageID,
OriginalLanguageID: toNullInt(in.OriginalLanguageID),
RentalDuration: in.RentalDuration,
RentalRate: in.RentalRate,
Length: toNullInt(in.Length),
ReplacementCost: in.ReplacementCost,
Rating: toNullString(in.Rating),
SpecialFeatures: toNullString(in.SpecialFeatures),
FilmID: id,
})
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if aff == 0 {
c.JSON(404, gin.H{"error": "not found"})
return
}
updated, err := q.GetFilm(c, id)
if err != nil {
c.JSON(500, 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"})
res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.LanguageID, 10)})
if updated.OriginalLanguageID.Valid {
res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.OriginalLanguageID.Int64, 10)})
}
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"`
OriginalLanguageID *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
}
q := sq.New(db)
toNullString := func(p *string) sql.NullString {
if p == nil {
return sql.NullString{Valid: false}
}
return sql.NullString{String: *p, Valid: true}
}
toNullInt := func(p *int64) sql.NullInt64 {
if p == nil {
return sql.NullInt64{Valid: false}
}
return sql.NullInt64{Int64: *p, Valid: true}
}
toNullFloat := func(p *float64) sql.NullFloat64 {
if p == nil {
return sql.NullFloat64{Valid: false}
}
return sql.NullFloat64{Float64: *p, Valid: true}
}
var desc interface{}
if in.Description != nil {
desc = *in.Description
} else {
desc = nil
}
aff, err := q.PatchFilm(c, sq.PatchFilmParams{
Title: toNullString(in.Title),
Description: desc,
ReleaseYear: toNullString(in.ReleaseYear),
LanguageID: toNullInt(in.LanguageID),
OriginalLanguageID: toNullInt(in.OriginalLanguageID),
RentalDuration: toNullInt(in.RentalDuration),
RentalRate: toNullFloat(in.RentalRate),
Length: toNullInt(in.Length),
ReplacementCost: toNullFloat(in.ReplacementCost),
Rating: toNullString(in.Rating),
SpecialFeatures: toNullString(in.SpecialFeatures),
FilmID: id,
})
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if aff == 0 {
c.JSON(404, gin.H{"error": "not found"})
return
}
updated, err := q.GetFilm(c, id)
if err != nil {
c.JSON(500, 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"})
res.AddLink("sakila:language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.LanguageID, 10)})
if updated.OriginalLanguageID.Valid {
res.AddLink("sakila:original-language", halgo.Link{Href: "/languages/" + strconv.FormatInt(updated.OriginalLanguageID.Int64, 10)})
}
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
}
q := sq.New(db)
aff, err := q.DeleteFilm(c, id)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if aff == 0 {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.Status(204)
})
}