Subject: [PATCH links] Fixing bug where tag issues would fail due to stupid use of regexp. Instead we now parse the valid JSON and strip out null entries.

From: Peter Sanchez <[email-masked]>
Date: Sat, 14 Jun 2025 13:37:09 -0600

I don't really remember what lead us to use the regexp pattern to strip
out null entries initially but it was a dumb decision.

Changelog-fixed: No longer using regexp to parse null entries from json
  (no clue wtf we were thinking there)
---
 models/base_url.go   | 13 +++++--------
 models/link_short.go | 24 +++++++++---------------
 models/listing.go    | 34 ++++++++++++----------------------
 models/org_link.go   | 24 ++++++++----------------
 models/utils.go      | 23 +++++++++++++++++++++++
 5 files changed, 57 insertions(+), 61 deletions(-)

diff --git a/models/base_url.go b/models/base_url.go
index affaa04..18c6560 100644
--- a/models/base_url.go
+++ b/models/base_url.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
-	"regexp"
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
@@ -90,14 +89,12 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL,
 				&url.LastParseAttempt, &url.CreatedOn, &tags); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &url.Tags)
-				if err != nil {
-					return err
-				}
+
+			url.Tags, err = BuildSliceFromJSONString[Tag](tags)
+			if err != nil {
+				return err
 			}
+
 			err = url.ToLocalTZ(tz)
 			if err != nil {
 				return err
diff --git a/models/link_short.go b/models/link_short.go
index cda1a20..31a38b0 100644
--- a/models/link_short.go
+++ b/models/link_short.go
@@ -5,7 +5,6 @@ import (
 	"database/sql"
 	"encoding/json"
 	"fmt"
-	"regexp"
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
@@ -48,14 +47,11 @@ func GetLinkShorts(ctx context.Context, opts *database.FilterOptions) ([]*LinkSh
 				&ls.OrgID, &ls.UserID, &ls.CreatedOn, &ls.UpdatedOn, &ls.LookupName, &tags); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &ls.Tags)
-				if err != nil {
-					return err
-				}
+			ls.Tags, err = BuildSliceFromJSONString[Tag](tags)
+			if err != nil {
+				return err
 			}
+
 			err = ls.ToLocalTZ(tz)
 			if err != nil {
 				return err
@@ -263,14 +259,12 @@ func ExportLinkShorts(ctx context.Context, opts *database.FilterOptions) ([]*Exp
 			if err = rows.Scan(&ls.ID, &ls.Title, &ls.URL, &ls.ShortCode, &ls.CreatedOn, &tags); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &ls.Tags)
-				if err != nil {
-					return err
-				}
+			ls.Tags, err = BuildSliceFromJSONString[ExportTag](tags)
+			err = json.Unmarshal([]byte(tags), &ls.Tags)
+			if err != nil {
+				return err
 			}
+
 			linkShorts = append(linkShorts, &ls)
 		}
 		return nil
diff --git a/models/listing.go b/models/listing.go
index fa3c778..7e58ce8 100644
--- a/models/listing.go
+++ b/models/listing.go
@@ -3,9 +3,7 @@ package models
 import (
 	"context"
 	"database/sql"
-	"encoding/json"
 	"fmt"
-	"regexp"
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
@@ -43,7 +41,6 @@ func GetListings(ctx context.Context, opts *database.FilterOptions) ([]*Listing,
 		}
 		defer rows.Close()
 
-		re := regexp.MustCompile(`(,\s)?null,?`)
 		for rows.Next() {
 			var ls Listing
 			var tags string
@@ -51,13 +48,12 @@ func GetListings(ctx context.Context, opts *database.FilterOptions) ([]*Listing,
 				&ls.UserID, &ls.IsDefault, &ls.IsActive, &ls.CreatedOn, &ls.UpdatedOn, &ls.LookupName, &tags); err != nil {
 				return err
 			}
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &ls.Tags)
-				if err != nil {
-					return err
-				}
+
+			ls.Tags, err = BuildSliceFromJSONString[Tag](tags)
+			if err != nil {
+				return err
 			}
+
 			err = ls.ToLocalTZ(tz)
 			if err != nil {
 				return err
@@ -249,22 +245,16 @@ func ExportListings(ctx context.Context, opts *database.FilterOptions) ([]*Expor
 			if err = rows.Scan(&ls.ID, &ls.Title, &ls.Slug, &ls.CreatedOn, &tags, &links); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			links = re.ReplaceAllString(links, "")
-			if links != "[]" {
-				err = json.Unmarshal([]byte(links), &ls.Links)
-				if err != nil {
-					return err
-				}
+			ls.Links, err = BuildSliceFromJSONString[ExportLink](links)
+			if err != nil {
+				return err
 			}
 
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &ls.Tags)
-				if err != nil {
-					return err
-				}
+			ls.Tags, err = BuildSliceFromJSONString[ExportTag](tags)
+			if err != nil {
+				return err
 			}
+
 			listings = append(listings, &ls)
 		}
 		return nil
diff --git a/models/org_link.go b/models/org_link.go
index acf6da1..d46657c 100644
--- a/models/org_link.go
+++ b/models/org_link.go
@@ -3,10 +3,8 @@ package models
 import (
 	"context"
 	"database/sql"
-	"encoding/json"
 	"fmt"
 	"net/url"
-	"regexp"
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
@@ -69,14 +67,11 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
 				&o.BaseURLData, &o.BaseURLCounter, &o.BaseURLHash); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &o.Tags)
-				if err != nil {
-					return err
-				}
+			o.Tags, err = BuildSliceFromJSONString[Tag](tags)
+			if err != nil {
+				return err
 			}
+
 			err = o.ToLocalTZ(tz)
 			if err != nil {
 				return err
@@ -349,14 +344,11 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor
 				&o.Unread, &o.Starred, &o.Hash, &o.CreatedOn, &tags); err != nil {
 				return err
 			}
-			re := regexp.MustCompile(`(,\s)?null,?`)
-			tags = re.ReplaceAllString(tags, "")
-			if tags != "[]" {
-				err = json.Unmarshal([]byte(tags), &o.Tags)
-				if err != nil {
-					return err
-				}
+			o.Tags, err = BuildSliceFromJSONString[ExportTag](tags)
+			if err != nil {
+				return err
 			}
+
 			links = append(links, &o)
 		}
 		return nil
diff --git a/models/utils.go b/models/utils.go
index 0255066..f8d787e 100644
--- a/models/utils.go
+++ b/models/utils.go
@@ -3,6 +3,8 @@ package models
 import (
 	"context"
 	"database/sql"
+	"encoding/json"
+	"fmt"
 	mrand "math/rand"
 	"strings"
 	"time"
@@ -96,3 +98,24 @@ func ShowLinkCounter(obj any) bool {
 		return false
 	}
 }
+
+// SliceNilRemover will remove any nil values from a given slice
+func SliceNilRemover[T any](in []*T) []T {
+	out := make([]T, 0, len(in))
+	for _, item := range in {
+		if item != nil {
+			out = append(out, *item)
+		}
+	}
+	return out
+}
+
+// BuildSliceFromString will build slice from a json_agg PostgreSQL value.
+func BuildSliceFromJSONString[T any](jsonInput string) ([]T, error) {
+	var dval []*T
+	err := json.Unmarshal([]byte(jsonInput), &dval)
+	if err != nil {
+		return nil, fmt.Errorf("invalid JSON array: %w", err)
+	}
+	return SliceNilRemover(dval), nil
+}
-- 
2.47.2

---

From: "Peter Sanchez" <[email-masked]>
Date: Sat, 14 Jun 2025 13:42:33 -0600

Applied.

To [email-masked]:~netlandish/links
   dbf83fe..9ed99a4  master -> master