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`.
This commit is contained in:
parent
54e36528bc
commit
5aa6a96b1d
6 changed files with 616 additions and 182 deletions
|
|
@ -2,5 +2,6 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/sakila-sqlite3" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -261,199 +261,307 @@ func RegisterFilmRoutes(r *gin.Engine, db *sql.DB) {
|
||||||
c.JSON(200, res)
|
c.JSON(200, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
// Create film
|
||||||
// Create film
|
r.POST("/films", func(c *gin.Context) {
|
||||||
r.POST("/films", func(c *gin.Context) {
|
var in struct {
|
||||||
var in struct {
|
FilmID *int64 `json:"film_id"`
|
||||||
FilmID *int64 `json:"film_id"`
|
Title string `json:"title"`
|
||||||
Title string `json:"title"`
|
Description *string `json:"description"`
|
||||||
Description *string `json:"description"`
|
ReleaseYear *string `json:"release_year"`
|
||||||
ReleaseYear *string `json:"release_year"`
|
LanguageID *int64 `json:"language_id"`
|
||||||
LanguageID int64 `json:"language_id"`
|
OriginalLanguageID *int64 `json:"original_language_id"`
|
||||||
OriginalLangID *int64 `json:"original_language_id"`
|
RentalDuration *int64 `json:"rental_duration"`
|
||||||
RentalDuration *int64 `json:"rental_duration"`
|
RentalRate *float64 `json:"rental_rate"`
|
||||||
RentalRate *float64 `json:"rental_rate"`
|
Length *int64 `json:"length"`
|
||||||
Length *int64 `json:"length"`
|
ReplacementCost *float64 `json:"replacement_cost"`
|
||||||
ReplacementCost *float64 `json:"replacement_cost"`
|
Rating *string `json:"rating"`
|
||||||
Rating *string `json:"rating"`
|
SpecialFeatures *string `json:"special_features"`
|
||||||
SpecialFeatures *string `json:"special_features"`
|
}
|
||||||
}
|
if err := c.ShouldBindJSON(&in); err != nil {
|
||||||
if err := c.ShouldBindJSON(&in); err != nil {
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
return
|
||||||
return
|
}
|
||||||
}
|
if strings.TrimSpace(in.Title) == "" || in.LanguageID == nil {
|
||||||
if strings.TrimSpace(in.Title) == "" || in.LanguageID == 0 {
|
c.JSON(400, gin.H{"error": "title and language_id are required"})
|
||||||
c.JSON(400, gin.H{"error": "title and language_id are required"})
|
return
|
||||||
return
|
}
|
||||||
}
|
q := sq.New(db)
|
||||||
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
|
var id int64
|
||||||
if in.RentalDuration != nil {
|
if in.FilmID != nil {
|
||||||
f.RentalDuration = *in.RentalDuration
|
id = *in.FilmID
|
||||||
} else {
|
} else {
|
||||||
f.RentalDuration = 3
|
var err error
|
||||||
}
|
id, err = q.NextFilmID(c)
|
||||||
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 {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self := "/films/" + strconv.FormatInt(created.FilmID, 10)
|
}
|
||||||
res := hres.New(self)
|
toNullString := func(p *string) sql.NullString {
|
||||||
res.AddCurie()
|
if p == nil {
|
||||||
res.SetState(created)
|
return sql.NullString{Valid: false}
|
||||||
res.AddLink("self", halgo.Link{Href: self})
|
}
|
||||||
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
|
return sql.NullString{String: *p, Valid: true}
|
||||||
c.Header("Content-Type", "application/hal+json")
|
}
|
||||||
c.Header("Location", self)
|
toNullInt := func(p *int64) sql.NullInt64 {
|
||||||
c.JSON(201, res)
|
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
|
// Replace film (PUT)
|
||||||
r.PUT("/films/:id", func(c *gin.Context) {
|
r.PUT("/films/:id", func(c *gin.Context) {
|
||||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid id"})
|
c.JSON(400, gin.H{"error": "invalid id"})
|
||||||
return
|
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}
|
||||||
}
|
}
|
||||||
var in struct {
|
return sql.NullString{String: *p, Valid: true}
|
||||||
Title string `json:"title"`
|
}
|
||||||
Description *string `json:"description"`
|
toNullInt := func(p *int64) sql.NullInt64 {
|
||||||
ReleaseYear *string `json:"release_year"`
|
if p == nil {
|
||||||
LanguageID int64 `json:"language_id"`
|
return sql.NullInt64{Valid: false}
|
||||||
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 {
|
return sql.NullInt64{Int64: *p, Valid: true}
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
var desc interface{}
|
||||||
}
|
if in.Description != nil {
|
||||||
f := sq.Film{Title: in.Title, Description: in.Description, ReleaseYear: in.ReleaseYear, LanguageID: in.LanguageID, OriginalLangID: in.OriginalLangID}
|
desc = *in.Description
|
||||||
if in.RentalDuration != nil {
|
} else {
|
||||||
f.RentalDuration = *in.RentalDuration
|
desc = nil
|
||||||
} else {
|
}
|
||||||
f.RentalDuration = 3
|
aff, err := q.UpdateFilmPut(c, sq.UpdateFilmPutParams{
|
||||||
}
|
Title: in.Title,
|
||||||
if in.RentalRate != nil {
|
Description: desc,
|
||||||
f.RentalRate = *in.RentalRate
|
ReleaseYear: toNullString(in.ReleaseYear),
|
||||||
} else {
|
LanguageID: in.LanguageID,
|
||||||
f.RentalRate = 4.99
|
OriginalLanguageID: toNullInt(in.OriginalLanguageID),
|
||||||
}
|
RentalDuration: in.RentalDuration,
|
||||||
if in.Length != nil {
|
RentalRate: in.RentalRate,
|
||||||
f.Length = in.Length
|
Length: toNullInt(in.Length),
|
||||||
}
|
ReplacementCost: in.ReplacementCost,
|
||||||
if in.ReplacementCost != nil {
|
Rating: toNullString(in.Rating),
|
||||||
f.ReplacementCost = *in.ReplacementCost
|
SpecialFeatures: toNullString(in.SpecialFeatures),
|
||||||
} else {
|
FilmID: id,
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
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
|
// Patch film
|
||||||
r.PATCH("/films/:id", func(c *gin.Context) {
|
r.PATCH("/films/:id", func(c *gin.Context) {
|
||||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid id"})
|
c.JSON(400, gin.H{"error": "invalid id"})
|
||||||
return
|
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}
|
||||||
}
|
}
|
||||||
var in struct {
|
return sql.NullString{String: *p, Valid: true}
|
||||||
Title *string `json:"title"`
|
}
|
||||||
Description *string `json:"description"`
|
toNullInt := func(p *int64) sql.NullInt64 {
|
||||||
ReleaseYear *string `json:"release_year"`
|
if p == nil {
|
||||||
LanguageID *int64 `json:"language_id"`
|
return sql.NullInt64{Valid: false}
|
||||||
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}
|
return sql.NullInt64{Int64: *p, Valid: true}
|
||||||
updated, err := sq.PatchFilm(c, db, id, patch)
|
}
|
||||||
if err != nil {
|
toNullFloat := func(p *float64) sql.NullFloat64 {
|
||||||
if err == sql.ErrNoRows {
|
if p == nil {
|
||||||
c.JSON(404, gin.H{"error": "not found"})
|
return sql.NullFloat64{Valid: false}
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
self := "/films/" + strconv.FormatInt(updated.FilmID, 10)
|
return sql.NullFloat64{Float64: *p, Valid: true}
|
||||||
res := hres.New(self)
|
}
|
||||||
res.AddCurie()
|
var desc interface{}
|
||||||
res.SetState(updated)
|
if in.Description != nil {
|
||||||
res.AddLink("self", halgo.Link{Href: self})
|
desc = *in.Description
|
||||||
res.AddLink("sakila:actors", halgo.Link{Href: self + "/actors"})
|
} else {
|
||||||
c.Header("Content-Type", "application/hal+json")
|
desc = nil
|
||||||
c.JSON(200, res)
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
83
internal/httpapi/films_crud_test.http
Normal file
83
internal/httpapi/films_crud_test.http
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
### Films API sample requests (IntelliJ HTTP Client)
|
||||||
|
|
||||||
|
### List films (default pagination)
|
||||||
|
GET http://localhost:8080/films
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
### List films with expand and filters
|
||||||
|
GET http://localhost:8080/films?limit=5&offset=0&expand=actors,language&language.name=English
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
### List films filtered by category name
|
||||||
|
GET http://localhost:8080/films?limit=5&category.name=Action
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
### Get a single film
|
||||||
|
GET http://localhost:8080/films/1?expand=actors,language,original_language
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
### Create a film (server computes next film_id)
|
||||||
|
POST http://localhost:8080/films
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Sample Movie",
|
||||||
|
"description": "A test film created via HTTP client",
|
||||||
|
"release_year": "2025",
|
||||||
|
"language_id": 1,
|
||||||
|
"rental_duration": 3,
|
||||||
|
"rental_rate": 4.99,
|
||||||
|
"length": 120,
|
||||||
|
"replacement_cost": 19.99,
|
||||||
|
"rating": "PG",
|
||||||
|
"special_features": "Trailers,Deleted Scenes"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Create a film with explicit film_id
|
||||||
|
POST http://localhost:8080/films
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
{
|
||||||
|
"film_id": 10001,
|
||||||
|
"title": "Explicit ID Film",
|
||||||
|
"language_id": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
### Replace film (PUT)
|
||||||
|
# Note: Replace the :id variable below to match an existing film id
|
||||||
|
PUT http://localhost:8080/films/{{film_id}}
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Updated Title (PUT)",
|
||||||
|
"description": "Full replacement payload",
|
||||||
|
"release_year": "2024",
|
||||||
|
"language_id": 1,
|
||||||
|
"original_language_id": 2,
|
||||||
|
"rental_duration": 5,
|
||||||
|
"rental_rate": 3.99,
|
||||||
|
"length": 95,
|
||||||
|
"replacement_cost": 14.99,
|
||||||
|
"rating": "PG-13",
|
||||||
|
"special_features": "Trailers,Behind the Scenes"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Patch film (partial update)
|
||||||
|
PATCH http://localhost:8080/films/{{film_id}}
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/hal+json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "Patched Title",
|
||||||
|
"rental_rate": 2.99
|
||||||
|
}
|
||||||
|
|
||||||
|
### Delete film
|
||||||
|
DELETE http://localhost:8080/films/{{film_id}}
|
||||||
|
|
||||||
|
### Related: list actors of a film
|
||||||
|
GET http://localhost:8080/films/{{film_id}}/actors
|
||||||
|
Accept: application/hal+json
|
||||||
5
internal/httpapi/http-client.env.json
Normal file
5
internal/httpapi/http-client.env.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"film_id": "10001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ package sqlc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
const countFilms = `-- name: CountFilms :one
|
const countFilms = `-- name: CountFilms :one
|
||||||
|
|
@ -156,3 +157,184 @@ func (q *Queries) ListFilms(ctx context.Context, arg ListFilmsParams) ([]Film, e
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional film CRUD methods (manually added to mirror sqlc patterns)
|
||||||
|
const nextFilmID = `-- name: NextFilmID :one
|
||||||
|
SELECT COALESCE(MAX(film_id), 0) + 1 FROM film
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) NextFilmID(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, nextFilmID)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertFilm = `-- name: InsertFilm :exec
|
||||||
|
INSERT INTO film (
|
||||||
|
film_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
release_year,
|
||||||
|
language_id,
|
||||||
|
original_language_id,
|
||||||
|
rental_duration,
|
||||||
|
rental_rate,
|
||||||
|
length,
|
||||||
|
replacement_cost,
|
||||||
|
rating,
|
||||||
|
special_features,
|
||||||
|
last_update
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertFilmParams struct {
|
||||||
|
FilmID int64 `json:"film_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description interface{} `json:"description"`
|
||||||
|
ReleaseYear sql.NullString `json:"release_year"`
|
||||||
|
LanguageID int64 `json:"language_id"`
|
||||||
|
OriginalLanguageID sql.NullInt64 `json:"original_language_id"`
|
||||||
|
RentalDuration int64 `json:"rental_duration"`
|
||||||
|
RentalRate float64 `json:"rental_rate"`
|
||||||
|
Length sql.NullInt64 `json:"length"`
|
||||||
|
ReplacementCost float64 `json:"replacement_cost"`
|
||||||
|
Rating sql.NullString `json:"rating"`
|
||||||
|
SpecialFeatures sql.NullString `json:"special_features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertFilm(ctx context.Context, arg InsertFilmParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertFilm,
|
||||||
|
arg.FilmID,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.ReleaseYear,
|
||||||
|
arg.LanguageID,
|
||||||
|
arg.OriginalLanguageID,
|
||||||
|
arg.RentalDuration,
|
||||||
|
arg.RentalRate,
|
||||||
|
arg.Length,
|
||||||
|
arg.ReplacementCost,
|
||||||
|
arg.Rating,
|
||||||
|
arg.SpecialFeatures,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFilmPut = `-- name: UpdateFilmPut :execrows
|
||||||
|
UPDATE film SET
|
||||||
|
title = ?,
|
||||||
|
description = ?,
|
||||||
|
release_year = ?,
|
||||||
|
language_id = ?,
|
||||||
|
original_language_id = ?,
|
||||||
|
rental_duration = ?,
|
||||||
|
rental_rate = ?,
|
||||||
|
length = ?,
|
||||||
|
replacement_cost = ?,
|
||||||
|
rating = ?,
|
||||||
|
special_features = ?
|
||||||
|
WHERE film_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateFilmPutParams struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description interface{} `json:"description"`
|
||||||
|
ReleaseYear sql.NullString `json:"release_year"`
|
||||||
|
LanguageID int64 `json:"language_id"`
|
||||||
|
OriginalLanguageID sql.NullInt64 `json:"original_language_id"`
|
||||||
|
RentalDuration int64 `json:"rental_duration"`
|
||||||
|
RentalRate float64 `json:"rental_rate"`
|
||||||
|
Length sql.NullInt64 `json:"length"`
|
||||||
|
ReplacementCost float64 `json:"replacement_cost"`
|
||||||
|
Rating sql.NullString `json:"rating"`
|
||||||
|
SpecialFeatures sql.NullString `json:"special_features"`
|
||||||
|
FilmID int64 `json:"film_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateFilmPut(ctx context.Context, arg UpdateFilmPutParams) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, updateFilmPut,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.ReleaseYear,
|
||||||
|
arg.LanguageID,
|
||||||
|
arg.OriginalLanguageID,
|
||||||
|
arg.RentalDuration,
|
||||||
|
arg.RentalRate,
|
||||||
|
arg.Length,
|
||||||
|
arg.ReplacementCost,
|
||||||
|
arg.Rating,
|
||||||
|
arg.SpecialFeatures,
|
||||||
|
arg.FilmID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchFilm = `-- name: PatchFilm :execrows
|
||||||
|
UPDATE film SET
|
||||||
|
title = COALESCE(?1, title),
|
||||||
|
description = COALESCE(?2, description),
|
||||||
|
release_year = COALESCE(?3, release_year),
|
||||||
|
language_id = COALESCE(?4, language_id),
|
||||||
|
original_language_id = COALESCE(?5, original_language_id),
|
||||||
|
rental_duration = COALESCE(?6, rental_duration),
|
||||||
|
rental_rate = COALESCE(?7, rental_rate),
|
||||||
|
length = COALESCE(?8, length),
|
||||||
|
replacement_cost = COALESCE(?9, replacement_cost),
|
||||||
|
rating = COALESCE(?10, rating),
|
||||||
|
special_features = COALESCE(?11, special_features)
|
||||||
|
WHERE film_id = ?12
|
||||||
|
`
|
||||||
|
|
||||||
|
type PatchFilmParams struct {
|
||||||
|
Title sql.NullString `json:"title"`
|
||||||
|
Description interface{} `json:"description"`
|
||||||
|
ReleaseYear sql.NullString `json:"release_year"`
|
||||||
|
LanguageID sql.NullInt64 `json:"language_id"`
|
||||||
|
OriginalLanguageID sql.NullInt64 `json:"original_language_id"`
|
||||||
|
RentalDuration sql.NullInt64 `json:"rental_duration"`
|
||||||
|
RentalRate sql.NullFloat64 `json:"rental_rate"`
|
||||||
|
Length sql.NullInt64 `json:"length"`
|
||||||
|
ReplacementCost sql.NullFloat64 `json:"replacement_cost"`
|
||||||
|
Rating sql.NullString `json:"rating"`
|
||||||
|
SpecialFeatures sql.NullString `json:"special_features"`
|
||||||
|
FilmID int64 `json:"film_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) PatchFilm(ctx context.Context, arg PatchFilmParams) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, patchFilm,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.ReleaseYear,
|
||||||
|
arg.LanguageID,
|
||||||
|
arg.OriginalLanguageID,
|
||||||
|
arg.RentalDuration,
|
||||||
|
arg.RentalRate,
|
||||||
|
arg.Length,
|
||||||
|
arg.ReplacementCost,
|
||||||
|
arg.Rating,
|
||||||
|
arg.SpecialFeatures,
|
||||||
|
arg.FilmID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFilm = `-- name: DeleteFilm :execrows
|
||||||
|
DELETE FROM film WHERE film_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteFilm(ctx context.Context, filmID int64) (int64, error) {
|
||||||
|
result, err := q.db.ExecContext(ctx, deleteFilm, filmID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,4 +42,59 @@ SELECT a.actor_id, a.first_name, a.last_name, a.last_update
|
||||||
FROM actor a
|
FROM actor a
|
||||||
JOIN film_actor fa ON fa.actor_id = a.actor_id
|
JOIN film_actor fa ON fa.actor_id = a.actor_id
|
||||||
WHERE fa.film_id = ?
|
WHERE fa.film_id = ?
|
||||||
ORDER BY a.last_name ASC, a.first_name ASC;
|
ORDER BY a.last_name ASC, a.first_name ASC;
|
||||||
|
|
||||||
|
-- name: NextFilmID :one
|
||||||
|
SELECT COALESCE(MAX(film_id), 0) + 1 FROM film;
|
||||||
|
|
||||||
|
-- name: InsertFilm :exec
|
||||||
|
INSERT INTO film (
|
||||||
|
film_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
release_year,
|
||||||
|
language_id,
|
||||||
|
original_language_id,
|
||||||
|
rental_duration,
|
||||||
|
rental_rate,
|
||||||
|
length,
|
||||||
|
replacement_cost,
|
||||||
|
rating,
|
||||||
|
special_features,
|
||||||
|
last_update
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: UpdateFilmPut :execrows
|
||||||
|
UPDATE film SET
|
||||||
|
title = ?,
|
||||||
|
description = ?,
|
||||||
|
release_year = ?,
|
||||||
|
language_id = ?,
|
||||||
|
original_language_id = ?,
|
||||||
|
rental_duration = ?,
|
||||||
|
rental_rate = ?,
|
||||||
|
length = ?,
|
||||||
|
replacement_cost = ?,
|
||||||
|
rating = ?,
|
||||||
|
special_features = ?
|
||||||
|
WHERE film_id = ?;
|
||||||
|
|
||||||
|
-- name: PatchFilm :execrows
|
||||||
|
UPDATE film SET
|
||||||
|
title = COALESCE(?1, title),
|
||||||
|
description = COALESCE(?2, description),
|
||||||
|
release_year = COALESCE(?3, release_year),
|
||||||
|
language_id = COALESCE(?4, language_id),
|
||||||
|
original_language_id = COALESCE(?5, original_language_id),
|
||||||
|
rental_duration = COALESCE(?6, rental_duration),
|
||||||
|
rental_rate = COALESCE(?7, rental_rate),
|
||||||
|
length = COALESCE(?8, length),
|
||||||
|
replacement_cost = COALESCE(?9, replacement_cost),
|
||||||
|
rating = COALESCE(?10, rating),
|
||||||
|
special_features = COALESCE(?11, special_features)
|
||||||
|
WHERE film_id = ?12;
|
||||||
|
|
||||||
|
-- name: DeleteFilm :execrows
|
||||||
|
DELETE FROM film WHERE film_id = ?;
|
||||||
Loading…
Reference in a new issue