Skip to content

Conversation

@laurenleach
Copy link
Contributor

@laurenleach laurenleach commented Aug 29, 2025

Note

Adds robust VariableType JSON (de)serialization, updates models to typed variables, and reworks catalog item variable fetching to include item and set variables with choices and de-duplication.

  • ServiceNow client (pkg/servicenow/client.go):
    • Rework GetCatalogItemVariablesPlusSets to fetch item-level variables and set variables, load choices for all, map to CatalogItemVariable, and de-duplicate; increased limits and clearer errors.
  • Models (pkg/servicenow/model.go):
    • Replace VariableTypeNew and ad-hoc parsing with typed VariableType plus custom JSON UnmarshalJSON/MarshalJSON supporting multiple incoming formats; update CatalogItemVariable.Type and ItemOptionNew.Type to VariableType.
    • Simplify ConvertVariableToSchemaCustomField to use typed VariableType; ignore container start/end variables.
    • Improve boolean parsing via strconv.ParseBool; remove parseVariableType; map variables using typed v.Type.

Written by Cursor Bugbot for commit 6da1f43. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • None
  • Bug Fixes

    • More complete and deduplicated variables for catalog items, including attached variable sets, with correct choices.
    • Handles empty variable-set links without failing.
    • More reliable type and boolean parsing; container-only variables are skipped to avoid invalid fields.
  • Refactor

    • Unified variable and choice aggregation into a single flow for consistency and maintainability.
    • Standardized variable type representation with JSON (un)marshalling.
    • No changes to public method signatures.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got some doubts regarding pagination but the PR looks good to me, so I approved

func (c *Client) GetCatalogItemVariablesPlusSets(ctx context.Context, itemSysID string) ([]CatalogItemVariable, error) {
itemVars, err := c.GetCatalogItemVariables(ctx, itemSysID)
// Item-level variables from Table API
itemVarsRaw, _, err := c.GetVariablesForItem(ctx, itemSysID, PaginationVars{Limit: 500})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are cutting a page of 500 items, right?
The page token is not being stored here. Are we handling that pagination or is it a case we cannot handle?


// Find attached variable sets
// Variable sets attached to this item
links, _, err := c.GetVariableSetLinksForItem(ctx, itemSysID, PaginationVars{Limit: 200})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we could have extra pages also? Or it's ok to discard the next page token?

// Variables inside those sets
setVarsRaw := []ItemOptionNew{}
if len(setIDs) > 0 {
setVarsRaw, _, err = c.GetVariablesBySetIDs(ctx, setIDs, PaginationVars{Limit: 1000})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't wanna repeat myself but same question about pagination here

cvSet = append(cvSet, MapItemOptionNewToCatalogItemVariable(v, choicesByQ[v.SysID]))
choicesByQ := map[string][]QuestionChoice{}
if len(varIDs) > 0 {
choices, _, err := c.GetChoicesForVariables(ctx, varIDs, PaginationVars{Limit: 5000})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about the pagination here

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Refactors GetCatalogItemVariablesPlusSets to aggregate item-level and set-level variables, fetch choices in a single pass, and unify mapping to CatalogItemVariable. Updates model types to use a strong VariableType with JSON marshal/unmarshal, adjusts related structs and helpers, and simplifies type handling in conversion/mapping functions.

Changes

Cohort / File(s) Summary
ServiceNow client aggregation refactor
pkg/servicenow/client.go
Reworked GetCatalogItemVariablesPlusSets to: fetch item variables and set-linked variables, merge them, retrieve all choices once, rebuild choice mappings, and produce a deduplicated CatalogItemVariable list. Updated error messages and removed per-set choice fetching.
Model type system overhaul
pkg/servicenow/model.go
Introduced strong VariableType with JSON marshal/unmarshal; updated CatalogItemVariable and ItemOptionNew to use VariableType; revised ConvertVariableToSchemaCustomField, MapItemOptionNewToCatalogItemVariable, and boolStr parsing; explicit handling for container types; added imports.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant SN as ServiceNow API

  Note over C: GetCatalogItemVariablesPlusSets(itemID)

  C->>SN: GetVariablesForItem(itemID)
  SN-->>C: itemVars

  C->>SN: GetVariableSetLinks(itemID)
  SN-->>C: setLinks

  C->>SN: GetVariablesBySetIDs(setLinks.setIDs)
  SN-->>C: setVars

  Note over C: Merge itemVars + setVars -> allVars

  C->>SN: GetChoicesForVariables(allVars.ids)
  SN-->>C: choices

  Note over C: Build choicesByQuestion, map allVars -> CatalogItemVariable (dedupe)
  C-->>C: Return aggregated CatalogItemVariables
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A bunny hops through fields of vars,
Merging sets and item stars.
Choices gathered in one glide,
Types made strong, no need to hide.
Thump-thump—schemas neatly aligned,
Carrots crunched, bugs left behind. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “support variables within containers” accurately reflects the addition of container-type handling in the model layer, which is a real part of the changeset, but it omits the significant client-side rework to aggregate item and set variables. Because it refers to a valid aspect of the changes, it meets the criteria for a partially related title.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch lauren/container-variables

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

cvSet = append(cvSet, MapItemOptionNewToCatalogItemVariable(v, choicesByQ[v.SysID]))
choicesByQ := map[string][]QuestionChoice{}
if len(varIDs) > 0 {
choices, _, err := c.GetChoicesForVariables(ctx, varIDs, PaginationVars{Limit: 5000})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: API Pagination Token Ignored

The GetCatalogItemVariablesPlusSets function discards pagination tokens from GetVariablesForItem, GetVariableSetLinksForItem, GetVariablesBySetIDs, and GetChoicesForVariables. When any of these APIs return more results than their limits (e.g., 500, 200, 1000, 5000), only the first page is processed. This causes silent data loss and incomplete catalog item variable information.

Fix in Cursor Fix in Web

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/servicenow/model.go (1)

401-447: UI label bug: using value as DisplayName for choices.

For MultipleChoice and SelectBox branches, DisplayName is set to c.Value. That renders IDs instead of human-readable labels.

Apply:

-			allowedChoices = append(allowedChoices, &v2.TicketCustomFieldObjectValue{
-				Id:          c.Value,
-				DisplayName: c.Value,
-			})
+			allowedChoices = append(allowedChoices, &v2.TicketCustomFieldObjectValue{
+				Id:          c.Value,
+				DisplayName: c.Label,
+			})

Repeat the same change in the SelectBox/LookupSelectBox case.

🧹 Nitpick comments (3)
pkg/servicenow/client.go (3)

544-546: Doc nit: clarify that choices are fetched for all variables, not just set variables.


660-676: Stale comment: this function is used.

Comment says “Unused…”, but it’s called in GetCatalogItemVariablesPlusSets. Please update.


591-603: Optional: deterministic ordering.

If downstream expects a stable variable order, consider sorting out by name or by SN’s “order” if available in item_option_new (not currently selected in sysparm_fields).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4c7a4c and 6da1f43.

📒 Files selected for processing (2)
  • pkg/servicenow/client.go (1 hunks)
  • pkg/servicenow/model.go (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
pkg/servicenow/client.go (2)
pkg/servicenow/model.go (5)
  • CatalogItemVariable (192-213)
  • VariableSet (319-323)
  • ItemOptionNew (326-339)
  • QuestionChoice (342-347)
  • MapItemOptionNewToCatalogItemVariable (464-492)
pkg/servicenow/request.go (1)
  • PaginationVars (80-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (9)
pkg/servicenow/model.go (5)

192-213: Struct tightening to strong types looks good.

Switching CatalogItemVariable fields to concrete types and VariableType is a solid improvement.


247-311: Robust VariableType (un)marshalling — LGTM.

Covers null/empty/object/quoted/bare numbers and emits numeric JSON. Please confirm we never POST this field back to SN expecting a quoted string.


458-461: boolStr switch to strconv.ParseBool — good improvement.


325-339: Field tag check: reference qualifier key.

RefQualifier uses json:"reference_qual". SN sometimes exposes this as reference_qual vs ref_qualifier depending on table/Display Value settings. Please double-check the exact field name from item_option_new in your instance.


463-492: Type mapping uses strong VariableType — LGTM.

pkg/servicenow/client.go (4)

548-551: Pagination missing for item-level variables (may truncate at 500).

You ignore the next page token from GetVariablesForItem, so items beyond the first page are silently dropped. Please loop until next is empty. This was raised previously.

Apply:

-	itemVarsRaw, _, err := c.GetVariablesForItem(ctx, itemSysID, PaginationVars{Limit: 500})
-	if err != nil {
-		return nil, fmt.Errorf("get item-level variables: %w", err)
-	}
+	var itemVarsRaw []ItemOptionNew
+	pgItem := PaginationVars{Limit: 500}
+	for {
+		page, next, err := c.GetVariablesForItem(ctx, itemSysID, pgItem)
+		if err != nil {
+			return nil, fmt.Errorf("get item-level variables: %w", err)
+		}
+		itemVarsRaw = append(itemVarsRaw, page...)
+		if next == "" {
+			break
+		}
+		if off, err := strconv.Atoi(next); err == nil {
+			pgItem.Offset = off
+		} else {
+			break
+		}
+	}

Also add import:

 import (
   "bytes"
   "context"
   "encoding/json"
   "errors"
   "fmt"
   "io"
   "math"
   "net/http"
   "net/url"
   "strings"
+  "strconv"
 )

554-561: Pagination missing for variable-set links + lack of setID de-dup.

Same issue: only first page of io_set_item links is considered; duplicates can inflate queries. Loop and de-dup. Previously flagged.

-	links, _, err := c.GetVariableSetLinksForItem(ctx, itemSysID, PaginationVars{Limit: 200})
-	if err != nil {
-		return nil, fmt.Errorf("get variable set links: %w", err)
-	}
-	setIDs := make([]string, 0, len(links))
-	for _, l := range links {
-		setIDs = append(setIDs, l.VariableSet)
-	}
+	var links []VariableSetM2M
+	pgLinks := PaginationVars{Limit: 200}
+	for {
+		page, next, err := c.GetVariableSetLinksForItem(ctx, itemSysID, pgLinks)
+		if err != nil {
+			return nil, fmt.Errorf("get variable set links: %w", err)
+		}
+		links = append(links, page...)
+		if next == "" {
+			break
+		}
+		if off, err := strconv.Atoi(next); err == nil {
+			pgLinks.Offset = off
+		} else {
+			break
+		}
+	}
+	// de-dup set IDs
+	setIDSet := make(map[string]struct{}, len(links))
+	for _, l := range links {
+		setIDSet[l.VariableSet] = struct{}{}
+	}
+	setIDs := make([]string, 0, len(setIDSet))
+	for id := range setIDSet {
+		setIDs = append(setIDs, id)
+	}

565-570: Pagination missing for set variables (across all sets).

Only the first page from GetVariablesBySetIDs is processed. Loop over pages. Previously raised.

-	setVarsRaw := []ItemOptionNew{}
-	if len(setIDs) > 0 {
-		setVarsRaw, _, err = c.GetVariablesBySetIDs(ctx, setIDs, PaginationVars{Limit: 1000})
-		if err != nil {
-			return nil, fmt.Errorf("get variables by set ids: %w", err)
-		}
-	}
+	setVarsRaw := []ItemOptionNew{}
+	if len(setIDs) > 0 {
+		pgSet := PaginationVars{Limit: 1000}
+		for {
+			page, next, err := c.GetVariablesBySetIDs(ctx, setIDs, pgSet)
+			if err != nil {
+				return nil, fmt.Errorf("get variables by set ids: %w", err)
+			}
+			setVarsRaw = append(setVarsRaw, page...)
+			if next == "" {
+				break
+			}
+			if off, err := strconv.Atoi(next); err == nil {
+				pgSet.Offset = off
+			} else {
+				break
+			}
+		}
+	}

575-589: Choices fetch: de-dup variable IDs, chunk the IN query, and paginate choices.

  • varIDs may contain duplicates; de-dup first.
  • questionIN with thousands of IDs risks URL/query-size limits; chunk.
  • You also ignore next page tokens from GetChoicesForVariables. Previously flagged.
-	varIDs := make([]string, 0, len(allRaw))
-	for _, v := range allRaw {
-		varIDs = append(varIDs, v.SysID)
-	}
+	idSet := make(map[string]struct{}, len(allRaw))
+	for _, v := range allRaw {
+		idSet[v.SysID] = struct{}{}
+	}
+	varIDs := make([]string, 0, len(idSet))
+	for id := range idSet {
+		varIDs = append(varIDs, id)
+	}
 
 	choicesByQ := map[string][]QuestionChoice{}
 	if len(varIDs) > 0 {
-		choices, _, err := c.GetChoicesForVariables(ctx, varIDs, PaginationVars{Limit: 5000})
-		if err != nil {
-			return nil, fmt.Errorf("get choices for variables: %w", err)
-		}
-		for _, ch := range choices {
-			choicesByQ[ch.Question] = append(choicesByQ[ch.Question], ch)
-		}
+		const chunk = 200
+		for i := 0; i < len(varIDs); i += chunk {
+			end := i + chunk
+			if end > len(varIDs) {
+				end = len(varIDs)
+			}
+			pgChoices := PaginationVars{Limit: 1000}
+			for {
+				page, next, err := c.GetChoicesForVariables(ctx, varIDs[i:end], pgChoices)
+				if err != nil {
+					return nil, fmt.Errorf("get choices for variables: %w", err)
+				}
+				for _, ch := range page {
+					choicesByQ[ch.Question] = append(choicesByQ[ch.Question], ch)
+				}
+				if next == "" {
+					break
+				}
+				if off, err := strconv.Atoi(next); err == nil {
+					pgChoices.Offset = off
+				} else {
+					break
+				}
+			}
+		}
 	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants