[object Object]

← back to Cli Printing Press

feat(cli): substitute EndpointTemplateVars in client.do() + cache key

8cb57db37db3806997af5520ed79f6d4bc37daf3 · 2026-04-29 21:48:47 -0500 · Cathryn Lavery

Route every request through buildURL(c.BaseURL, path, c.Config.TemplateVars)
when the spec declares EndpointTemplateVars, replacing the static
c.BaseURL+path concat. Both BaseURL ("https://{shop}") and path
("/admin/api/{api_version}/graphql.json") may carry placeholders, and a
single helper at do()'s entry covers GET/POST/PUT/PATCH/DELETE plus the
proxy-envelope branch.

Mix the resolved template-var values into cacheKey so two tenants on the
same logical path land in different cache entries — without this, a warm
SHOPIFY_SHOP=A cache would feed responses to a SHOPIFY_SHOP=B caller.
Flipping a value back to unset also misses the warm cache and surfaces the
TemplateVarError from buildURL instead of silently returning stale data.

Specs without EndpointTemplateVars keep the original concat; client.go
output is byte-identical for Linear, REST, and every existing fixture.

Files touched

Diff

commit 8cb57db37db3806997af5520ed79f6d4bc37daf3
Author: Cathryn Lavery <50469282+cathrynlavery@users.noreply.github.com>
Date:   Wed Apr 29 21:48:47 2026 -0500

    feat(cli): substitute EndpointTemplateVars in client.do() + cache key
    
    Route every request through buildURL(c.BaseURL, path, c.Config.TemplateVars)
    when the spec declares EndpointTemplateVars, replacing the static
    c.BaseURL+path concat. Both BaseURL ("https://{shop}") and path
    ("/admin/api/{api_version}/graphql.json") may carry placeholders, and a
    single helper at do()'s entry covers GET/POST/PUT/PATCH/DELETE plus the
    proxy-envelope branch.
    
    Mix the resolved template-var values into cacheKey so two tenants on the
    same logical path land in different cache entries — without this, a warm
    SHOPIFY_SHOP=A cache would feed responses to a SHOPIFY_SHOP=B caller.
    Flipping a value back to unset also misses the warm cache and surfaces the
    TemplateVarError from buildURL instead of silently returning stale data.
    
    Specs without EndpointTemplateVars keep the original concat; client.go
    output is byte-identical for Linear, REST, and every existing fixture.
---
 internal/generator/templates/client.go.tmpl | 40 +++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 65441183..f2042fe0 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -185,6 +185,21 @@ func (c *Client) cacheKey(path string, params map[string]string) string {
 	for k, v := range params {
 		key += k + "=" + v
 	}
+{{- if .EndpointTemplateVars}}
+	// Include resolved template-var values in the cache identity so two
+	// tenants (different SHOPIFY_SHOP) never collide on the same path, and
+	// flipping a value back to unset misses the warm cache and surfaces
+	// the actionable error from buildURL instead of returning stale data.
+	if c.Config != nil {
+		for _, name := range []string{
+		{{- range .EndpointTemplateVars}}
+			"{{.}}",
+		{{- end}}
+		} {
+			key += "|" + name + "=" + c.Config.TemplateVars[name]
+		}
+	}
+{{- end}}
 	h := sha256.Sum256([]byte(key))
 	return hex.EncodeToString(h[:8])
 }
@@ -243,11 +258,36 @@ func (c *Client) PatchWithHeaders(path string, body any, headers map[string]stri
 // do executes an HTTP request. headerOverrides, when non-nil, override global
 // RequiredHeaders for this specific request (used for per-endpoint API versioning).
 func (c *Client) do(method, path string, params map[string]string, body any, headerOverrides map[string]string) (json.RawMessage, int, error) {
+{{- if .EndpointTemplateVars}}
+	// EndpointTemplateVars are declared in the spec — resolve {placeholder}
+	// markers in BaseURL (and, for non-proxy clients, the request path)
+	// against env-backed Config.TemplateVars. Done once at the top of every
+	// request so a missing env var surfaces an actionable error instead of
+	// quietly sending a literal "{shop}" to the API.
+	var endpointVars map[string]string
+	if c.Config != nil {
+		endpointVars = c.Config.TemplateVars
+	}
+{{- end}}
 {{- if eq .ClientPattern "proxy-envelope"}}
 	// Proxy-envelope: all requests are POST'd to BaseURL with a JSON envelope
+{{- if .EndpointTemplateVars}}
+	targetURL, urlErr := buildURL(c.BaseURL, "", endpointVars)
+	if urlErr != nil {
+		return nil, 0, urlErr
+	}
+{{- else}}
 	targetURL := c.BaseURL
+{{- end}}
+{{- else}}
+{{- if .EndpointTemplateVars}}
+	targetURL, urlErr := buildURL(c.BaseURL, path, endpointVars)
+	if urlErr != nil {
+		return nil, 0, urlErr
+	}
 {{- else}}
 	targetURL := c.BaseURL + path
+{{- end}}
 {{- end}}
 
 	var bodyBytes []byte

← 289e5988 feat(cli): emit buildURL helper + Config.TemplateVars for te  ·  back to Cli Printing Press  ·  fix(cli): keep template-var env names out of verifier auth d a0e1bb4a →