From eb90c0a68e75bb19754c16e97c593521251b3915 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Mon, 25 Nov 2024 22:42:36 +0000
Subject: [PATCH] Queries mostly debugged

---
 api/boxes.yaml                        |  10 +-
 database/migrations/01_initial.up.sql |  18 +--
 database/query.sql                    |   8 ++
 server/boxes.go                       | 134 ++++++++++++++++++++
 server/contents.go                    | 161 ++++++++++++++++++++++++
 server/endpoints.go                   | 175 --------------------------
 server/locations.go                   | 127 +++++++++++++++++++
 server/server.go                      |  14 ++-
 8 files changed, 458 insertions(+), 189 deletions(-)
 create mode 100644 server/boxes.go
 create mode 100644 server/contents.go
 create mode 100644 server/locations.go

diff --git a/api/boxes.yaml b/api/boxes.yaml
index 09eb77a..1f95a39 100644
--- a/api/boxes.yaml
+++ b/api/boxes.yaml
@@ -196,13 +196,13 @@ paths:
         '500':
           $ref: '#/components/responses/Error'
 
-  /content/{bid}:
+  /content/{cid}:
     get:
       summary: Content
       description: Get a content.
       operationId: GetContent
       parameters:
-        - $ref: '#/components/parameters/BID'
+        - $ref: '#/components/parameters/CID'
       responses:
         '200':
           $ref: '#/components/responses/Content'
@@ -215,7 +215,7 @@ paths:
       description: Create a content.
       operationId: CreateContent
       parameters:
-        - $ref: '#/components/parameters/BID'
+        - $ref: '#/components/parameters/CID'
       requestBody:
         description: New content.
         required: true
@@ -237,7 +237,7 @@ paths:
       description: Update a content.
       operationId: UpdateContent
       parameters:
-        - $ref: '#/components/parameters/BID'
+        - $ref: '#/components/parameters/CID'
       requestBody:
         description: Updated content.
         required: true
@@ -261,7 +261,7 @@ paths:
       description: Delete content.
       operationId: DeleteContent
       parameters:
-        - $ref: '#/components/parameters/BID'
+        - $ref: '#/components/parameters/CID'
       responses:
         '200':
           $ref: '#/components/responses/Content'
diff --git a/database/migrations/01_initial.up.sql b/database/migrations/01_initial.up.sql
index 38b032b..d60b655 100644
--- a/database/migrations/01_initial.up.sql
+++ b/database/migrations/01_initial.up.sql
@@ -1,19 +1,21 @@
-CREATE TABLE IF NOT EXISTS contents (
+CREATE TABLE IF NOT EXISTS locations (
   id           TEXT    PRIMARY KEY,
-  box          TEXT    NOT NULL,
-  description  TEXT    NOT NULL,
-  tags         TEXT
+  description  TEXT    NOT NULL
 ) WITHOUT ROWID;
-CREATE INDEX IF NOT EXISTS contents_box_idx ON contents(box);
 
 CREATE TABLE IF NOT EXISTS boxes (
   id           TEXT    PRIMARY KEY,
   location     TEXT    NOT NULL,
-  description  TEXT    NOT NULL
+  description  TEXT    NOT NULL,
+  FOREIGN KEY(location) REFERENCES locations(id)
 ) WITHOUT ROWID;
 CREATE INDEX IF NOT EXISTS boxes_location_idx ON boxes(location);
 
-CREATE TABLE IF NOT EXISTS locations (
+CREATE TABLE IF NOT EXISTS contents (
   id           TEXT    PRIMARY KEY,
-  description  TEXT    NOT NULL
+  box          TEXT    NOT NULL,
+  description  TEXT    NOT NULL,
+  tags         TEXT,
+  FOREIGN KEY(box) REFERENCES boxes(id)
 ) WITHOUT ROWID;
+CREATE INDEX IF NOT EXISTS contents_box_idx ON contents(box);
diff --git a/database/query.sql b/database/query.sql
index 8219b76..4e8da7c 100644
--- a/database/query.sql
+++ b/database/query.sql
@@ -50,6 +50,10 @@ RETURNING *;
 -- name: GetBoxes :many
 SELECT * FROM boxes;
 
+-- name: GetBoxesWithFilter :many
+SELECT * FROM boxes
+WHERE description LIKE ?;
+
 -- name: GetContent :one
 SELECT * FROM contents
 WHERE id = ? LIMIT 1;
@@ -74,4 +78,8 @@ RETURNING *;
 -- name: GetContents :many
 SELECT * FROM contents;
 
+-- name: GetContentsWithFilter :many
+SELECT * FROM contents
+WHERE description LIKE ?;
+
 -- vim:et
diff --git a/server/boxes.go b/server/boxes.go
new file mode 100644
index 0000000..b526eb6
--- /dev/null
+++ b/server/boxes.go
@@ -0,0 +1,134 @@
+// Package server implements the server.
+package server
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"git.lyda.ie/kevin/boxes/api"
+	"git.lyda.ie/kevin/boxes/database/store"
+)
+
+// GetBox TODO.
+func (ep *Endpoints) GetBox(ctx context.Context, req api.GetBoxRequestObject) (api.GetBoxResponseObject, error) {
+	box, err := ep.db.GetBox(ctx, req.Bid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetBox404JSONResponse{
+			Code:    404,
+			Message: "Not found",
+		}, nil
+	}
+	return api.GetBox200JSONResponse{
+		BoxJSONResponse: api.BoxJSONResponse{
+			Id:          box.ID,
+			Location:    box.Location,
+			Description: box.Description,
+		}}, nil
+}
+
+// CreateBox TODO.
+func (ep *Endpoints) CreateBox(ctx context.Context, req api.CreateBoxRequestObject) (api.CreateBoxResponseObject, error) {
+	if req.Bid != req.Body.Id {
+		log.Printf("error: %+v != %+v", req.Bid, req.Body)
+		return api.CreateBox406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newBox := store.CreateBoxParams{
+		ID:          req.Bid,
+		Location:    req.Body.Location,
+		Description: req.Body.Description,
+	}
+	result, err := ep.db.CreateBox(ctx, newBox)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.CreateBox507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.CreateBox200JSONResponse{
+		BoxJSONResponse: api.BoxJSONResponse{
+			Id:          result.ID,
+			Location:    result.Location,
+			Description: result.Description,
+		}}, nil
+}
+
+// UpdateBox TODO.
+func (ep *Endpoints) UpdateBox(ctx context.Context, req api.UpdateBoxRequestObject) (api.UpdateBoxResponseObject, error) {
+	if req.Bid != req.Body.Id {
+		return api.UpdateBox406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newBox := store.UpdateBoxParams{
+		ID:          req.Bid,
+		Location:    req.Body.Location,
+		Description: req.Body.Description,
+	}
+	result, err := ep.db.UpdateBox(ctx, newBox)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.UpdateBox507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.UpdateBox200JSONResponse{
+		BoxJSONResponse: api.BoxJSONResponse{
+			Id:          result.ID,
+			Location:    result.Location,
+			Description: result.Description,
+		}}, nil
+}
+
+// DeleteBox TODO.
+func (ep *Endpoints) DeleteBox(ctx context.Context, req api.DeleteBoxRequestObject) (api.DeleteBoxResponseObject, error) {
+	result, err := ep.db.DeleteBox(ctx, req.Bid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.DeleteBox404JSONResponse{
+			Code:    404,
+			Message: "Failed to delete",
+		}, nil
+	}
+	return api.DeleteBox200JSONResponse{
+		BoxJSONResponse: api.BoxJSONResponse{
+			Id:          result.ID,
+			Location:    result.Location,
+			Description: result.Description,
+		}}, nil
+}
+
+// GetBoxes returns a list of boxes.
+func (ep *Endpoints) GetBoxes(ctx context.Context, req api.GetBoxesRequestObject) (api.GetBoxesResponseObject, error) {
+	var boxes []store.Box
+	var err error
+	if req.Params.Filter == nil {
+		boxes, err = ep.db.GetBoxes(ctx)
+	} else {
+		boxes, err = ep.db.GetBoxesWithFilter(ctx, fmt.Sprintf("%%%s%%", *req.Params.Filter))
+	}
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetBoxes500JSONResponse{
+			Code:    500,
+			Message: "Failed to fetch",
+		}, nil
+	}
+	result := make(api.Boxes, len(boxes))
+	for i := range boxes {
+		result[i].Id = boxes[i].ID
+		result[i].Location = boxes[i].Location
+		result[i].Description = boxes[i].Description
+	}
+
+	return api.GetBoxes200JSONResponse{
+		BoxesJSONResponse: result,
+	}, nil
+}
diff --git a/server/contents.go b/server/contents.go
new file mode 100644
index 0000000..f7cb4a2
--- /dev/null
+++ b/server/contents.go
@@ -0,0 +1,161 @@
+// Package server implements the server.
+package server
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"log"
+	"strings"
+
+	"git.lyda.ie/kevin/boxes/api"
+	"git.lyda.ie/kevin/boxes/database/store"
+)
+
+func tags2db(tags *[]string) sql.NullString {
+	if tags == nil {
+		return sql.NullString{}
+	}
+	return sql.NullString{
+		String: fmt.Sprintf(",%s,", strings.Join(*tags, ",")),
+		Valid:  true,
+	}
+}
+
+func db2tags(tags sql.NullString) *[]string {
+	if tags.Valid {
+		t := strings.Split(strings.Trim(tags.String, ","), ",")
+		return &t
+	}
+	return nil
+}
+
+// GetContent TODO.
+func (ep *Endpoints) GetContent(ctx context.Context, req api.GetContentRequestObject) (api.GetContentResponseObject, error) {
+	content, err := ep.db.GetContent(ctx, req.Cid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetContent404JSONResponse{
+			Code:    404,
+			Message: "Not found",
+		}, nil
+	}
+	return api.GetContent200JSONResponse{
+		ContentJSONResponse: api.ContentJSONResponse{
+			Id:          content.ID,
+			Box:         content.Box,
+			Description: content.Description,
+			Tags:        db2tags(content.Tags),
+		}}, nil
+}
+
+// CreateContent TODO.
+func (ep *Endpoints) CreateContent(ctx context.Context, req api.CreateContentRequestObject) (api.CreateContentResponseObject, error) {
+	if req.Cid != req.Body.Id {
+		log.Printf("error: %+v != %+v", req.Cid, req.Body)
+		return api.CreateContent406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newContent := store.CreateContentParams{
+		ID:          req.Cid,
+		Box:         req.Body.Box,
+		Description: req.Body.Description,
+		Tags:        tags2db(req.Body.Tags),
+	}
+	result, err := ep.db.CreateContent(ctx, newContent)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.CreateContent507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.CreateContent200JSONResponse{
+		ContentJSONResponse: api.ContentJSONResponse{
+			Id:          result.ID,
+			Box:         result.Box,
+			Description: result.Description,
+			Tags:        db2tags(result.Tags),
+		}}, nil
+}
+
+// UpdateContent TODO.
+func (ep *Endpoints) UpdateContent(ctx context.Context, req api.UpdateContentRequestObject) (api.UpdateContentResponseObject, error) {
+	if req.Cid != req.Body.Id {
+		return api.UpdateContent406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newContent := store.UpdateContentParams{
+		ID:          req.Cid,
+		Box:         req.Body.Box,
+		Description: req.Body.Description,
+		Tags:        tags2db(req.Body.Tags),
+	}
+	result, err := ep.db.UpdateContent(ctx, newContent)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.UpdateContent507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.UpdateContent200JSONResponse{
+		ContentJSONResponse: api.ContentJSONResponse{
+			Id:          result.ID,
+			Box:         result.Box,
+			Description: result.Description,
+			Tags:        db2tags(result.Tags),
+		}}, nil
+}
+
+// DeleteContent TODO.
+func (ep *Endpoints) DeleteContent(ctx context.Context, req api.DeleteContentRequestObject) (api.DeleteContentResponseObject, error) {
+	result, err := ep.db.DeleteContent(ctx, req.Cid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.DeleteContent404JSONResponse{
+			Code:    404,
+			Message: "Failed to delete",
+		}, nil
+	}
+	return api.DeleteContent200JSONResponse{
+		ContentJSONResponse: api.ContentJSONResponse{
+			Id:          result.ID,
+			Box:         result.Box,
+			Description: result.Description,
+			Tags:        db2tags(result.Tags),
+		}}, nil
+}
+
+// GetContents returns a list of contents.
+func (ep *Endpoints) GetContents(ctx context.Context, req api.GetContentsRequestObject) (api.GetContentsResponseObject, error) {
+	var contents []store.Content
+	var err error
+	if req.Params.Filter == nil {
+		contents, err = ep.db.GetContents(ctx)
+	} else {
+		contents, err = ep.db.GetContentsWithFilter(ctx, fmt.Sprintf("%%%s%%", *req.Params.Filter))
+	}
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetContents500JSONResponse{
+			Code:    500,
+			Message: "Failed to fetch",
+		}, nil
+	}
+	result := make(api.Contents, len(contents))
+	for i := range contents {
+		result[i].Id = contents[i].ID
+		result[i].Box = contents[i].Box
+		result[i].Description = contents[i].Description
+		result[i].Tags = db2tags(contents[i].Tags)
+	}
+
+	return api.GetContents200JSONResponse{
+		ContentsJSONResponse: result,
+	}, nil
+}
diff --git a/server/endpoints.go b/server/endpoints.go
index 8317497..a2fd3d2 100644
--- a/server/endpoints.go
+++ b/server/endpoints.go
@@ -2,11 +2,8 @@
 package server
 
 import (
-	"context"
 	"database/sql"
-	"log"
 
-	"git.lyda.ie/kevin/boxes/api"
 	"git.lyda.ie/kevin/boxes/database/store"
 )
 
@@ -21,175 +18,3 @@ func NewEndpoints(db *sql.DB) *Endpoints {
 		db: *store.New(db),
 	}
 }
-
-// GetLocation TODO.
-func (ep *Endpoints) GetLocation(ctx context.Context, req api.GetLocationRequestObject) (api.GetLocationResponseObject, error) {
-	loc, err := ep.db.GetLocation(ctx, req.Lid)
-	if err != nil {
-		log.Printf("error: %+v", err)
-		return api.GetLocation404JSONResponse{
-			Code:    404,
-			Message: "Not found",
-		}, nil
-	}
-	return api.GetLocation200JSONResponse{
-		LocationJSONResponse: api.LocationJSONResponse{
-			Id:          loc.ID,
-			Description: loc.Description,
-		}}, nil
-}
-
-// CreateLocation TODO.
-func (ep *Endpoints) CreateLocation(ctx context.Context, req api.CreateLocationRequestObject) (api.CreateLocationResponseObject, error) {
-	if req.Lid != req.Body.Id {
-		log.Printf("error: %+v != %+v", req.Lid, req.Body)
-		return api.CreateLocation406JSONResponse{
-			Code:    406,
-			Message: "Path id not equal to sent id",
-		}, nil
-	}
-	newLoc := store.CreateLocationParams{
-		ID:          req.Lid,
-		Description: req.Body.Description,
-	}
-	result, err := ep.db.CreateLocation(ctx, newLoc)
-	if err != nil {
-		log.Printf("error: %+v", err)
-		return api.CreateLocation507JSONResponse{
-			Code:    507,
-			Message: "Failed to store",
-		}, nil
-	}
-	return api.CreateLocation200JSONResponse{
-		LocationJSONResponse: api.LocationJSONResponse{
-			Id:          result.ID,
-			Description: result.Description,
-		}}, nil
-}
-
-// UpdateLocation TODO.
-func (ep *Endpoints) UpdateLocation(ctx context.Context, req api.UpdateLocationRequestObject) (api.UpdateLocationResponseObject, error) {
-	if req.Lid != req.Body.Id {
-		return api.UpdateLocation406JSONResponse{
-			Code:    406,
-			Message: "Path id not equal to sent id",
-		}, nil
-	}
-	newLoc := store.UpdateLocationParams{
-		ID:          req.Lid,
-		Description: req.Body.Description,
-	}
-	result, err := ep.db.UpdateLocation(ctx, newLoc)
-	if err != nil {
-		log.Printf("error: %+v", err)
-		return api.UpdateLocation507JSONResponse{
-			Code:    507,
-			Message: "Failed to store",
-		}, nil
-	}
-	return api.UpdateLocation200JSONResponse{
-		LocationJSONResponse: api.LocationJSONResponse{
-			Id:          result.ID,
-			Description: result.Description,
-		}}, nil
-}
-
-// DeleteLocation TODO.
-func (ep *Endpoints) DeleteLocation(ctx context.Context, req api.DeleteLocationRequestObject) (api.DeleteLocationResponseObject, error) {
-	result, err := ep.db.DeleteLocation(ctx, req.Lid)
-	if err != nil {
-		log.Printf("error: %+v", err)
-		return api.DeleteLocation404JSONResponse{
-			Code:    404,
-			Message: "Failed to delete",
-		}, nil
-	}
-	return api.DeleteLocation200JSONResponse{
-		LocationJSONResponse: api.LocationJSONResponse{
-			Id:          result.ID,
-			Description: result.Description,
-		}}, nil
-}
-
-// GetLocations returns a list of locations.
-func (ep *Endpoints) GetLocations(ctx context.Context, req api.GetLocationsRequestObject) (api.GetLocationsResponseObject, error) {
-	var locs []store.Location
-	var err error
-	if req.Params.Filter == nil {
-		locs, err = ep.db.GetLocations(ctx)
-	} else {
-		locs, err = ep.db.GetLocationsWithFilter(ctx, *req.Params.Filter)
-	}
-	if err != nil {
-		log.Printf("error: %+v", err)
-		return api.GetLocations500JSONResponse{
-			Code:    500,
-			Message: "Failed to fetch",
-		}, nil
-	}
-	result := make(api.Locations, len(locs))
-	for i := range locs {
-		result[i].Id = locs[i].ID
-		result[i].Description = locs[i].Description
-	}
-
-	return api.GetLocations200JSONResponse{
-		LocationsJSONResponse: result,
-	}, nil
-}
-
-// GetBox TODO.
-func (ep *Endpoints) GetBox(ctx context.Context, req api.GetBoxRequestObject) (api.GetBoxResponseObject, error) {
-	return api.GetBox200JSONResponse{}, nil
-}
-
-// CreateBox TODO.
-func (ep *Endpoints) CreateBox(ctx context.Context, req api.CreateBoxRequestObject) (api.CreateBoxResponseObject, error) {
-	return api.CreateBox200JSONResponse{}, nil
-}
-
-// UpdateBox TODO.
-func (ep *Endpoints) UpdateBox(ctx context.Context, req api.UpdateBoxRequestObject) (api.UpdateBoxResponseObject, error) {
-	return api.UpdateBox200JSONResponse{}, nil
-}
-
-// DeleteBox TODO.
-func (ep *Endpoints) DeleteBox(ctx context.Context, req api.DeleteBoxRequestObject) (api.DeleteBoxResponseObject, error) {
-	return api.DeleteBox200JSONResponse{}, nil
-}
-
-// GetBoxes returns a list of locations.
-func (ep *Endpoints) GetBoxes(ctx context.Context, req api.GetBoxesRequestObject) (api.GetBoxesResponseObject, error) {
-	boxes := []api.Box{}
-	return api.GetBoxes200JSONResponse{
-		BoxesJSONResponse: boxes,
-	}, nil
-}
-
-// GetContent TODO.
-func (ep *Endpoints) GetContent(ctx context.Context, req api.GetContentRequestObject) (api.GetContentResponseObject, error) {
-	return api.GetContent200JSONResponse{}, nil
-}
-
-// CreateContent TODO.
-func (ep *Endpoints) CreateContent(ctx context.Context, req api.CreateContentRequestObject) (api.CreateContentResponseObject, error) {
-	return api.CreateContent200JSONResponse{}, nil
-}
-
-// UpdateContent TODO.
-func (ep *Endpoints) UpdateContent(ctx context.Context, req api.UpdateContentRequestObject) (api.UpdateContentResponseObject, error) {
-	return api.UpdateContent200JSONResponse{}, nil
-}
-
-// DeleteContent TODO.
-func (ep *Endpoints) DeleteContent(ctx context.Context, req api.DeleteContentRequestObject) (api.DeleteContentResponseObject, error) {
-	return api.DeleteContent200JSONResponse{}, nil
-}
-
-// GetContents returns a list of locations.
-func (ep *Endpoints) GetContents(ctx context.Context, req api.GetContentsRequestObject) (api.GetContentsResponseObject, error) {
-	content := []api.Content{}
-	return api.GetContents200JSONResponse{
-		ContentsJSONResponse: content,
-	}, nil
-}
diff --git a/server/locations.go b/server/locations.go
new file mode 100644
index 0000000..b2ef0f2
--- /dev/null
+++ b/server/locations.go
@@ -0,0 +1,127 @@
+// Package server implements the server.
+package server
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"git.lyda.ie/kevin/boxes/api"
+	"git.lyda.ie/kevin/boxes/database/store"
+)
+
+// GetLocation TODO.
+func (ep *Endpoints) GetLocation(ctx context.Context, req api.GetLocationRequestObject) (api.GetLocationResponseObject, error) {
+	loc, err := ep.db.GetLocation(ctx, req.Lid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetLocation404JSONResponse{
+			Code:    404,
+			Message: "Not found",
+		}, nil
+	}
+	return api.GetLocation200JSONResponse{
+		LocationJSONResponse: api.LocationJSONResponse{
+			Id:          loc.ID,
+			Description: loc.Description,
+		}}, nil
+}
+
+// CreateLocation TODO.
+func (ep *Endpoints) CreateLocation(ctx context.Context, req api.CreateLocationRequestObject) (api.CreateLocationResponseObject, error) {
+	if req.Lid != req.Body.Id {
+		log.Printf("error: %+v != %+v", req.Lid, req.Body)
+		return api.CreateLocation406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newLoc := store.CreateLocationParams{
+		ID:          req.Lid,
+		Description: req.Body.Description,
+	}
+	result, err := ep.db.CreateLocation(ctx, newLoc)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.CreateLocation507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.CreateLocation200JSONResponse{
+		LocationJSONResponse: api.LocationJSONResponse{
+			Id:          result.ID,
+			Description: result.Description,
+		}}, nil
+}
+
+// UpdateLocation TODO.
+func (ep *Endpoints) UpdateLocation(ctx context.Context, req api.UpdateLocationRequestObject) (api.UpdateLocationResponseObject, error) {
+	if req.Lid != req.Body.Id {
+		return api.UpdateLocation406JSONResponse{
+			Code:    406,
+			Message: "Path id not equal to sent id",
+		}, nil
+	}
+	newLoc := store.UpdateLocationParams{
+		ID:          req.Lid,
+		Description: req.Body.Description,
+	}
+	result, err := ep.db.UpdateLocation(ctx, newLoc)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.UpdateLocation507JSONResponse{
+			Code:    507,
+			Message: "Failed to store",
+		}, nil
+	}
+	return api.UpdateLocation200JSONResponse{
+		LocationJSONResponse: api.LocationJSONResponse{
+			Id:          result.ID,
+			Description: result.Description,
+		}}, nil
+}
+
+// DeleteLocation TODO.
+func (ep *Endpoints) DeleteLocation(ctx context.Context, req api.DeleteLocationRequestObject) (api.DeleteLocationResponseObject, error) {
+	result, err := ep.db.DeleteLocation(ctx, req.Lid)
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.DeleteLocation404JSONResponse{
+			Code:    404,
+			Message: "Failed to delete",
+		}, nil
+	}
+	return api.DeleteLocation200JSONResponse{
+		LocationJSONResponse: api.LocationJSONResponse{
+			Id:          result.ID,
+			Description: result.Description,
+		}}, nil
+}
+
+// GetLocations returns a list of locations.
+func (ep *Endpoints) GetLocations(ctx context.Context, req api.GetLocationsRequestObject) (api.GetLocationsResponseObject, error) {
+	var locs []store.Location
+	var err error
+	if req.Params.Filter == nil {
+		locs, err = ep.db.GetLocations(ctx)
+	} else {
+		locs, err = ep.db.GetLocationsWithFilter(ctx, fmt.Sprintf("%%%s%%", *req.Params.Filter))
+	}
+	if err != nil {
+		log.Printf("error: %+v", err)
+		return api.GetLocations500JSONResponse{
+			Code:    500,
+			Message: "Failed to fetch",
+		}, nil
+	}
+	result := make(api.Locations, len(locs))
+	for i := range locs {
+		result[i].Id = locs[i].ID
+		result[i].Description = locs[i].Description
+	}
+
+	return api.GetLocations200JSONResponse{
+		LocationsJSONResponse: result,
+	}, nil
+}
diff --git a/server/server.go b/server/server.go
index afaa717..1995215 100644
--- a/server/server.go
+++ b/server/server.go
@@ -2,12 +2,12 @@
 package server
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
 
 	// The sqlite db and migration driver.
 	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
-	_ "modernc.org/sqlite"
 
 	"git.lyda.ie/kevin/boxes/api"
 	"git.lyda.ie/kevin/boxes/database"
@@ -16,6 +16,7 @@ import (
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
+	"modernc.org/sqlite"
 )
 
 // Run starts the metrics and API servers.
@@ -31,6 +32,17 @@ func Run(cmd *cobra.Command, _ []string) {
 	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{}))
 
 	// Connect and configure the database.
+	// See https://theitsolutions.io/blog/modernc.org-sqlite-with-go
+	const pragmas = `
+	PRAGMA foreign_keys = ON;
+`
+	ctx := context.TODO()
+	sqlite.RegisterConnectionHook(func(conn sqlite.ExecQuerierContext, _ string) error {
+		_, err = conn.ExecContext(ctx, pragmas, nil)
+
+		return err
+	})
+
 	migrationFS, err := iofs.New(database.MigrationsFS, "migrations")
 	cobra.CheckErr(err)
 	migration, err := migrate.NewWithSourceInstance("iofs", migrationFS, "sqlite://"+dbfile)
-- 
GitLab