Received: from mail.netlandish.com (mail.netlandish.com [174.136.98.166])
	by code.netlandish.com (Postfix) with ESMTP id 103D7352
	for <~netlandish/links-dev@lists.code.netlandish.com>; Sat, 07 Mar 2026 16:10:39 +0000 (UTC)
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.221.173; helo=mail-vk1-f173.google.com; envelope-from=peter@netlandish.com; receiver=<UNKNOWN> 
Authentication-Results: mail.netlandish.com;
	dkim=pass (1024-bit key; unprotected) header.d=netlandish.com header.i=@netlandish.com header.b=HDAkceZr
Received: from mail-vk1-f173.google.com (mail-vk1-f173.google.com [209.85.221.173])
	by mail.netlandish.com (Postfix) with ESMTP id C956B1D818B
	for <~netlandish/links-dev@lists.code.netlandish.com>; Sat, 07 Mar 2026 16:10:36 +0000 (UTC)
Received: by mail-vk1-f173.google.com with SMTP id 71dfb90a1353d-56a8da2faf4so8381258e0c.2
        for <~netlandish/links-dev@lists.code.netlandish.com>; Sat, 07 Mar 2026 08:10:36 -0800 (PST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=netlandish.com; s=google; t=1772899836; x=1773504636; darn=lists.code.netlandish.com;
        h=content-transfer-encoding:mime-version:message-id:date:subject:cc
         :to:from:from:to:cc:subject:date:message-id:reply-to;
        bh=wCH2QplPUZNO3BsNL2zdVr1PFj+qz+3Prbl3VNjbHr4=;
        b=HDAkceZrPdcqXUtLmoWaUSnwx8mNZUjA3u06DRKAZAWOuAgruaEMSJ7VhZ0xk774E7
         iNWlWvs1aYcSrPYzz4xRnUjxlmpkclptyKbkT31qebtUQBaXKfVKjetuvWAqpd0hjN72
         vNcOaSglKzWy4L3pUmtfkCfmfQEkUZd5W5fJA=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20230601; t=1772899836; x=1773504636;
        h=content-transfer-encoding:mime-version:message-id:date:subject:cc
         :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date
         :message-id:reply-to;
        bh=wCH2QplPUZNO3BsNL2zdVr1PFj+qz+3Prbl3VNjbHr4=;
        b=k4CLlvFujcCwyak6r39ge8+GIBpQzf4kQNwNnQYcjQ+bVshVds4SN4UL8/N2u0RNm5
         1Gc/Sig7ltEE4suPvqxiH4lpoo0lGM2guCxLzuKGp7AM2qUNP2WVZe/nJVQjVfrKIRGj
         Q3FkJd0s5AR49IudhftLL1LNPwZUUmsL829TWHOEZIowBZscHlqYU0CJKmtG7yTI73TO
         hTiZ+e49hOXcuGYTr+q34jOjP5VuZNVj6Nq+4uYjGSQ/cvz3/7yL0n6vooDq5TvyORFL
         AC4y9UxSLRC7Uk/Zc7Y2Q4Lsz4ooQSnA0WfI6tl5rngVEHFpHMdfAsPWKvAWw6qqfoHH
         dy4A==
X-Gm-Message-State: AOJu0YyKNppbw19X3tAi93ytKzaPhqFEakWfvxdhtiX7dnJi5qYBOrOi
	cz/K7gcE59uuovbyPLhwknIEiYd3+nJog5UM0BDNerZcd0J82uTZGhXucwf5WDowwMAzmoZwy7M
	iR56LChI=
X-Gm-Gg: ATEYQzwEOrAm1V3gu7cPHvB+0q8xNaDVamj4bOl5m2gfgaA7H6xGSF/Cc0GCbjPGZr2
	8pUJV/xCAqu1oI6JRuJvXr2ERodjn60ERpX9ajDsiNzmQV8vFtJMehxf/h1O0c+YFN2HE7C6s/q
	p/A6fMIWF+hRVa6QxqHwdXCxacdRM0FTvmi9LMtDV0uG/RSoJ4kbVo5TwWtd3McJYcxMyFEr8z9
	raTm6a0QKq0BipttjFFRRt8kCdPLC8UE/JG4viYYnPfny24LgJ7geGDEeNLwj+nGKsiKPJopTVu
	hX5ZA+p40IlSIO8ZtEShSZKyQ5RfFuIFeQKCreDOtIYykMMXlseQxqxl1zfEmlq37SL6dcA7oag
	3lmXb3bAWmvFe1cxGZ9zZ4LtCy8vVudUnjcJfDHP6ATtv2vbNeOmgzVg3fH2o8jHIWUub9ui9Wn
	8UTMJuxWuwwkIoLsijV7UtNtNo
X-Received: by 2002:a05:6123:2c3:b0:566:341b:4ddd with SMTP id 71dfb90a1353d-56b07bc22b0mr2323186e0c.0.1772899835612;
        Sat, 07 Mar 2026 08:10:35 -0800 (PST)
Received: from localhost ([2803:2d60:1107:87f:e175:1ea1:cce6:57bc])
        by smtp.gmail.com with ESMTPSA id a1e0cc1a2514c-94e7b38e378sm4598942241.7.2026.03.07.08.10.34
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Sat, 07 Mar 2026 08:10:35 -0800 (PST)
From: Peter Sanchez <peter@netlandish.com>
To: ~netlandish/links-dev@lists.code.netlandish.com
Cc: Peter Sanchez <peter@netlandish.com>
Subject: [PATCH links] api: getTags was missing the tag count. It's been added.
Date: Sat,  7 Mar 2026 10:10:26 -0600
Message-ID: <20260307161032.25584-1-peter@netlandish.com>
X-Mailer: git-send-email 2.52.0
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

Changelog-fixed: getTags was not including tag count. Now it is.
Changelog-updated: api version bumped to 0.11.2
---
 api/graph/schema.resolvers.go | 32 ++++++++++++-----
 models/tag.go                 | 66 +++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+), 9 deletions(-)

diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index 55e5c5a..f4990c3 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -5127,7 +5127,7 @@ func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
 	return &model.Version{
 		Major:           0,
 		Minor:           11,
-		Patch:           1,
+		Patch:           2,
 		DeprecationDate: nil,
 	}, nil
 }
@@ -5854,22 +5854,36 @@ func (r *queryResolver) GetTags(ctx context.Context, input model.GetTagsInput) (
 			fq = sq.Eq{"ll.org_id": org.ID}
 		}
 	} else {
-		// No service specified. Give tags for all services
-		fq = sq.Or{
-			sq.Eq{"ol.org_id": org.ID},
-			sq.Eq{"ll.org_id": org.ID},
-			sq.Eq{"s.org_id": org.ID},
-		}
+		fq = sq.Expr(`(
+			EXISTS (SELECT 1 FROM tag_links _tl JOIN org_links _ol ON _ol.id = _tl.org_link_id WHERE _tl.tag_id = t.id AND _ol.org_id = ?)
+			OR EXISTS (SELECT 1 FROM tag_link_shorts _ts JOIN link_shorts _s ON _s.id = _ts.link_short_id WHERE _ts.tag_id = t.id AND _s.org_id = ?)
+			OR EXISTS (SELECT 1 FROM tag_listings _tll JOIN listings _ll ON _ll.id = _tll.listing_id WHERE _tll.tag_id = t.id AND _ll.org_id = ?)
+		)`, org.ID, org.ID, org.ID)
 	}
 
 	opts := &database.FilterOptions{
 		Filter:  fq,
 		Limit:   250,
-		OrderBy: "name ASC",
+		OrderBy: "t.id ASC",
 	}
 
 	// Set pagination limit to 250 for tag pagination
 	ctx = PaginationContext(ctx, 250)
+
+	if input.Limit == nil {
+		defaultLimit := 250
+		input.Limit = &defaultLimit
+	}
+
+	getTagsFn := func(ctx context.Context, opts *database.FilterOptions) ([]*models.Tag, error) {
+		var svc *string
+		if input.Service != nil {
+			s := string(*input.Service)
+			svc = &s
+		}
+		return models.GetTagsWithCount(ctx, opts, svc, org.ID)
+	}
+
 	tags, pageInfo, err := QueryModel(
 		ctx,
 		opts,
@@ -5878,7 +5892,7 @@ func (r *queryResolver) GetTags(ctx context.Context, input model.GetTagsInput) (
 		input.Limit,
 		input.Before,
 		input.After,
-		models.GetTags,
+		getTagsFn,
 		func(tag *models.Tag) int {
 			return tag.ID
 		},
diff --git a/models/tag.go b/models/tag.go
index c9cbcfa..4eb502b 100644
--- a/models/tag.go
+++ b/models/tag.go
@@ -67,6 +67,72 @@ func TagRelationOrderString(v TagCloudOrdering) string {
 	return "LOWER(t.name) ASC"
 }
 
+// GetTagsWithCount returns tags with usage counts.
+func GetTagsWithCount(ctx context.Context, opts *database.FilterOptions, service *string, orgID int) ([]*Tag, error) {
+	if opts == nil {
+		opts = &database.FilterOptions{}
+	}
+	tz := timezone.ForContext(ctx)
+	tags := make([]*Tag, 0)
+	if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
+		q := opts.GetBuilder(nil)
+		q = q.Columns("t.id", "t.name", "t.slug", "t.created_on").
+			From("tags t")
+
+		if service != nil {
+			switch *service {
+			case DomainServiceLinks:
+				q = q.Join("tag_links tl ON tl.tag_id = t.id").
+					Join("org_links ol ON ol.id = tl.org_link_id").
+					Column("count(*) as tag_count")
+			case DomainServiceShort:
+				q = q.Join("tag_link_shorts ts ON ts.tag_id = t.id").
+					Join("link_shorts s ON ts.link_short_id = s.id").
+					Column("count(*) as tag_count")
+			case DomainServiceList:
+				q = q.Join("tag_listings tll ON tll.tag_id = t.id").
+					Join("listings ll ON tll.listing_id = ll.id").
+					Column("count(*) as tag_count")
+			}
+			q = q.GroupBy("t.id", "t.name", "t.slug", "t.created_on")
+		} else {
+			q = q.Column(`(
+				(SELECT COUNT(*) FROM tag_links _tl JOIN org_links _ol ON _ol.id = _tl.org_link_id WHERE _tl.tag_id = t.id AND _ol.org_id = ?) +
+				(SELECT COUNT(*) FROM tag_link_shorts _ts JOIN link_shorts _s ON _s.id = _ts.link_short_id WHERE _ts.tag_id = t.id AND _s.org_id = ?) +
+				(SELECT COUNT(*) FROM tag_listings _tll JOIN listings _ll ON _ll.id = _tll.listing_id WHERE _tll.tag_id = t.id AND _ll.org_id = ?)
+			) as tag_count`, orgID, orgID, orgID)
+		}
+
+		rows, err := q.
+			PlaceholderFormat(database.GetPlaceholderFormat()).
+			RunWith(tx).
+			QueryContext(ctx)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				return nil
+			}
+			return err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var t Tag
+			if err = rows.Scan(&t.ID, &t.Name, &t.Slug, &t.CreatedOn, &t.Count); err != nil {
+				return err
+			}
+			err = t.ToLocalTZ(tz)
+			if err != nil {
+				return err
+			}
+			tags = append(tags, &t)
+		}
+		return nil
+	}); err != nil {
+		return nil, err
+	}
+	return tags, nil
+}
+
 // GetTags ...
 func GetTags(ctx context.Context, opts *database.FilterOptions) ([]*Tag, error) {
 	if opts == nil {
-- 
2.52.0

