From d6ca0d5a825e38d915d8547159fe977ec8648235 Mon Sep 17 00:00:00 2001 From: yinebebt Date: Wed, 8 Apr 2026 02:56:41 +0300 Subject: [PATCH] feat: remove environment partitioning from link storage Remove the environment system (DefaultEnvironment, X-Environment header, ?environment= query param) from the library. Links are no longer partitioned by environment - if separation is needed, run separate instances. BREAKING CHANGE: Store.List signature changed from List(ctx, linkType, environment, cursor, count) to List(ctx, linkType, cursor, count). Config.DefaultEnvironment and Link.Environment fields removed. Closes #7 --- cmd/deeplink/main.go | 2 +- config.go | 7 ------- memory_store.go | 4 ++-- model.go | 2 -- redis_store.go | 4 ++-- server.go | 41 ++++++++++++----------------------------- store.go | 4 ++-- 7 files changed, 19 insertions(+), 45 deletions(-) diff --git a/cmd/deeplink/main.go b/cmd/deeplink/main.go index df296a5..244bcac 100644 --- a/cmd/deeplink/main.go +++ b/cmd/deeplink/main.go @@ -238,7 +238,7 @@ func handleDashboard(service *deeplink.Service, tmpl *template.Template, logger for _, linkType := range service.Types() { var cursor uint64 for { - links, next, err := cfg.Store.List(r.Context(), linkType, cfg.DefaultEnvironment, cursor, 100) + links, next, err := cfg.Store.List(r.Context(), linkType, cursor, 100) if err != nil { logger.Error("dashboard: failed to list links", "error", err, "type", linkType) break diff --git a/config.go b/config.go index 1a7404c..2798239 100644 --- a/config.go +++ b/config.go @@ -17,10 +17,6 @@ type Config struct { // IDLength is the length of generated short IDs. Default: 17. IDLength int - // DefaultEnvironment is used when no environment is specified - // in the request. Default: "dev". - DefaultEnvironment string - // Store is the persistence backend for links. Required. Store Store @@ -62,9 +58,6 @@ func (c *Config) defaults() { if c.IDLength == 0 { c.IDLength = 17 } - if c.DefaultEnvironment == "" { - c.DefaultEnvironment = "dev" - } if c.BaseURL != "" && !strings.HasSuffix(c.BaseURL, "/") { c.BaseURL += "/" } diff --git a/memory_store.go b/memory_store.go index a7d6d36..c74e3c3 100644 --- a/memory_store.go +++ b/memory_store.go @@ -60,7 +60,7 @@ func (s *MemoryStore) Clicks(_ context.Context, id string) (int64, error) { return s.clicks[id], nil } -func (s *MemoryStore) List(_ context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error) { +func (s *MemoryStore) List(_ context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -70,7 +70,7 @@ func (s *MemoryStore) List(_ context.Context, linkType, environment string, curs ids := make([]string, 0, len(s.payload)) for id, payload := range s.payload { - if payload.Type == linkType && payload.Environment == environment { + if payload.Type == linkType { ids = append(ids, id) } } diff --git a/model.go b/model.go index 99255f3..ea3b226 100644 --- a/model.go +++ b/model.go @@ -19,8 +19,6 @@ type Link struct { // ImageURL for the OG preview image. ImageURL string `json:"image_url,omitempty"` - // Environment groups links (e.g. "dev", "staging", "production"). - Environment string `json:"environment,omitempty"` // CreatedAt is an RFC 3339 timestamp. Set by the service. CreatedAt string `json:"created_at,omitempty"` diff --git a/redis_store.go b/redis_store.go index 9f47641..97d36b6 100644 --- a/redis_store.go +++ b/redis_store.go @@ -68,7 +68,7 @@ func (s *RedisStore) Clicks(ctx context.Context, id string) (int64, error) { return n, err } -func (s *RedisStore) List(ctx context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error) { +func (s *RedisStore) List(ctx context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error) { scanCount := max(count, 100) keys, nextCursor, err := s.client.Scan(ctx, cursor, s.scanPattern(), scanCount).Result() @@ -118,7 +118,7 @@ func (s *RedisStore) List(ctx context.Context, linkType, environment string, cur continue } - if p.Type == linkType && p.Environment == environment { + if p.Type == linkType { clicks, _ := clickCmds[key].Int64() links = append(links, LinkInfo{ ShortLink: s.stripPrefix(key), diff --git a/server.go b/server.go index 864446c..31ce460 100644 --- a/server.go +++ b/server.go @@ -15,7 +15,7 @@ import ( // // POST /shorten create a short link // GET /{shortID} preview page (or 302 if no template) -// GET /links/{type} list links by type and environment +// GET /links/{type} list links by type // GET /links/{type}/{shortID} single link with click count // GET /health health check // @@ -45,7 +45,6 @@ func (s *Service) Handler() http.Handler { func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) { start := time.Now() - env := s.envFromRequest(r) r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB var payload Link @@ -54,8 +53,6 @@ func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) { return } - payload.Environment = env - processor := s.registry.Get(strings.ToLower(payload.Type)) if processor == nil { s.respondError(w, ErrInvalidType) @@ -69,18 +66,17 @@ func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) { shortURL, err := s.shortenURL(r.Context(), &payload) if err != nil { - s.config.Logger.Error("failed to shorten URL", "error", err, "type", payload.Type, "env", env) + s.config.Logger.Error("failed to shorten URL", "error", err, "type", payload.Type) s.respondError(w, NewError(err, http.StatusInternalServerError, "failed to shorten URL")) return } respondJSON(w, http.StatusCreated, map[string]string{"short_url": shortURL}) - s.config.Logger.Info("link generated", "shortID", strings.TrimPrefix(shortURL, s.config.BaseURL), "type", payload.Type, "env", env, "duration", time.Since(start)) + s.config.Logger.Info("link generated", "shortID", strings.TrimPrefix(shortURL, s.config.BaseURL), "type", payload.Type, "duration", time.Since(start)) } func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) { start := time.Now() - env := s.envFromRequest(r) shortID := r.PathValue("shortID") if s.skipPath(shortID) { @@ -90,7 +86,7 @@ func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) { payload, err := s.expandURL(r.Context(), shortID) if err != nil { - s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID, "env", env) + s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID) http.NotFound(w, r) return } @@ -113,7 +109,7 @@ func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "public, max-age=3600") w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) - s.config.Logger.Info("preview rendered", "shortID", shortID, "type", payload.Type, "env", env, "duration", time.Since(start)) + s.config.Logger.Info("preview rendered", "shortID", shortID, "type", payload.Type, "duration", time.Since(start)) } // handleStaticPreview renders a preview page without auto-redirect. @@ -121,14 +117,13 @@ func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) { // flow where the user should tap to continue. func (s *Service) handleStaticPreview(w http.ResponseWriter, r *http.Request) { start := time.Now() - env := s.envFromRequest(r) w.Header().Set("Content-Type", "text/html") shortID := r.PathValue("shortID") payload, err := s.expandURL(r.Context(), shortID) if err != nil { - s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID, "env", env) + s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID) http.NotFound(w, r) return } @@ -149,7 +144,7 @@ func (s *Service) handleStaticPreview(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) - s.config.Logger.Info("preview rendered (no redirect)", "shortID", shortID, "type", payload.Type, "env", env, "duration", time.Since(start)) + s.config.Logger.Info("preview rendered (no redirect)", "shortID", shortID, "type", payload.Type, "duration", time.Since(start)) } func (s *Service) handleRedirect(w http.ResponseWriter, r *http.Request) { @@ -177,15 +172,14 @@ func (s *Service) handleRedirect(w http.ResponseWriter, r *http.Request) { func (s *Service) handleLinkList(w http.ResponseWriter, r *http.Request) { start := time.Now() - env := s.envFromRequest(r) linkType := r.PathValue("type") var allLinks []LinkInfo var cursor uint64 for { - links, next, err := s.config.Store.List(r.Context(), linkType, env, cursor, 100) + links, next, err := s.config.Store.List(r.Context(), linkType, cursor, 100) if err != nil { - s.config.Logger.Error("failed to list links", "error", err, "type", linkType, "env", env) + s.config.Logger.Error("failed to list links", "error", err, "type", linkType) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -209,18 +203,17 @@ func (s *Service) handleLinkList(w http.ResponseWriter, r *http.Request) { s.config.Logger.Error("failed to encode response", "error", err) } - s.config.Logger.Info("links listed", "type", linkType, "count", len(allLinks), "env", env, "duration", time.Since(start)) + s.config.Logger.Info("links listed", "type", linkType, "count", len(allLinks), "duration", time.Since(start)) } func (s *Service) handleLinkDetail(w http.ResponseWriter, r *http.Request) { start := time.Now() - env := s.envFromRequest(r) linkType := r.PathValue("type") shortID := r.PathValue("shortID") payload, err := s.config.Store.Get(r.Context(), shortID) if err != nil { - s.config.Logger.Warn("link not found", "shortId", shortID, "env", env) + s.config.Logger.Warn("link not found", "shortId", shortID) http.Error(w, "link not found", http.StatusNotFound) return } @@ -237,7 +230,7 @@ func (s *Service) handleLinkDetail(w http.ResponseWriter, r *http.Request) { s.config.Logger.Error("failed to encode response", "error", err) } - s.config.Logger.Info("link detail fetched", "shortId", shortID, "env", env, "duration", time.Since(start)) + s.config.Logger.Info("link detail fetched", "shortId", shortID, "duration", time.Since(start)) } func (s *Service) handleWellKnown(w http.ResponseWriter, r *http.Request) { @@ -274,16 +267,6 @@ func (s *Service) buildPreviewData(payload *Link) any { return payload } -func (s *Service) envFromRequest(r *http.Request) string { - if env := r.Header.Get("X-Environment"); env != "" { - return env - } - if env := r.URL.Query().Get("environment"); env != "" { - return env - } - return s.config.DefaultEnvironment -} - func (s *Service) withCORS(h http.HandlerFunc) http.HandlerFunc { allowed := make(map[string]bool, len(s.config.AllowedOrigins)) for _, o := range s.config.AllowedOrigins { diff --git a/store.go b/store.go index 3d76ced..3a22f2d 100644 --- a/store.go +++ b/store.go @@ -12,10 +12,10 @@ type Store interface { IncrClick(ctx context.Context, id string) (int64, error) // Clicks returns the current click count for id. Clicks(ctx context.Context, id string) (int64, error) - // List returns links matching linkType and environment. + // List returns links matching linkType. // Pass cursor 0 to start. The returned cursor is 0 when there are // no more results. Cursor values are opaque and store-specific; // do not assume sequential offsets. // Returned [LinkInfo.ShortLink] values contain only the short ID (no base URL). - List(ctx context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error) + List(ctx context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error) }