gastownhall/beads · error · storage.ErrValidation

%w: invalid issue type %s

Error message

%w: invalid issue type %s

What it means

ValidateScalarUpdates returns this when a 'type' update value is the right Go type but is not a recognized issue type: types.IssueType.IsValidWithCustom(customTypes) is false for both built-in and configured custom types. It wraps storage.ErrValidation and names the rejected type in the message.

Source

Thrown at internal/storage/issueops/aggregate.go:126

// ValidateScalarUpdates checks typed scalar values before they reach SQL.
func ValidateScalarUpdates(ctx context.Context, tx DBTX, updates map[string]interface{}) error {
	if rawType, ok := updates["issue_type"]; ok {
		var issueType types.IssueType
		switch value := rawType.(type) {
		case types.IssueType:
			issueType = value
		case string:
			issueType = types.IssueType(value)
		default:
			return fmt.Errorf("%w: invalid issue type %v", storage.ErrValidation, rawType)
		}
		customTypes, err := ResolveCustomTypesInTx(ctx, tx)
		if err != nil {
			return fmt.Errorf("resolve custom issue types: %w", err)
		}
		if !issueType.IsValidWithCustom(customTypes) {
			return fmt.Errorf("%w: invalid issue type %s", storage.ErrValidation, issueType)
		}
	}
	for _, field := range []string{"assignee", "owner"} {
		if raw, ok := updates[field]; ok {
			if value, ok := raw.(string); ok {
				if err := types.CheckFieldLen(field, value); err != nil {
					return err
				}
			}
		}
	}
	return nil
}

// AuthorizeAssigneeTransferWithPools is the assignee-transfer fence itself,
// with the pool aliases supplied rather than read. It is the single source of
// the predicate: the DBTX path below and the unit-of-work backend, which
// reaches config through a use case rather than a transaction, both evaluate

View on GitHub (pinned to 71377f2769)

Solutions

  1. Use one of the exported types.IssueType constants instead of raw strings
  2. Register the custom type in the database (or correct the spelling/case) before issuing updates
  3. Pre-check with issueType.IsValidWithCustom(customTypes) in the caller and surface a friendly message

Example fix

// before
updates["type"] = "Featur" // not valid
// after
t := types.IssueType(strings.ToLower(strings.TrimSpace(input)))
if !t.IsValidWithCustom(customTypes) { return fmt.Errorf("unknown issue type %q", t) }
updates["type"] = t
Defensive patterns

Strategy: validation

Validate before calling

ct, err := issueops.ResolveCustomTypesInTx(ctx, tx)
if err != nil { return err }
t := types.IssueType(candidate)
if !t.IsValidWithCustom(ct) {
  return fmt.Errorf("unknown issue type %q; valid: %v", t, append(types.AllBuiltin(), ct...))
}

Type guard

func knownIssueType(t types.IssueType, custom []types.IssueType) bool {
  return t.IsValidWithCustom(custom)
}

Try / catch

if err := issueops.ValidateScalarUpdates(ctx, tx, updates); errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "invalid issue type") {
  // suggest closest valid type to the user from builtin + custom list
}

Prevention

When it happens

Trigger: updateIssueInTx supplies updates["type"] as a string or types.IssueType whose value is not in the valid builtin set nor in the custom types resolved from the DB.

Common situations: Typo'd type names ('featur' instead of 'feature'); case mismatches ('BUG'); referencing a custom type that was deleted or not yet registered in this database; copying type names between projects with different custom types.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/c8fd0fb175d286ea. Report an issue: GitHub.