Skip to content

feat: Support Arbitrary Endpoint Matching for Edge Ingest - BED-7451#2422

Open
zinic wants to merge 1 commit intomainfrom
BED-7451
Open

feat: Support Arbitrary Endpoint Matching for Edge Ingest - BED-7451#2422
zinic wants to merge 1 commit intomainfrom
BED-7451

Conversation

@zinic
Copy link
Contributor

@zinic zinic commented Feb 25, 2026

Description

Updates OpenGraph Ingest schema to support a match_by strategy of "property" that then allows an OG implementer to specify an arbitrary property to match against for an edge endpoint.

Motivation and Context

Resolves BED-7451

HYBRID ENVIRONMENTS PATHS

How Has This Been Tested?

Existing tests.

Types of changes

  • Chore (a change that does not modify the application functionality)
  • New feature (non-breaking change which adds functionality)

Checklist:

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for property-based node matching in relationships, enabling edges to be defined by custom properties alongside existing ID and name matching options.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This PR introduces property-based matching for graph edge endpoints. A new Property field is added to endpoint models across layers, the JSON schema expands match_by options to include "property", and ingestion logic is refactored from endpoint resolution to batch construction while adding node kind merging utilities.

Changes

Cohort / File(s) Summary
Data Model Enhancements
packages/go/ein/incoming_models.go, packages/go/ein/models.go
Added Property field to EdgeEndpoint and IngestibleEndpoint structs. Introduced IdentityProperty() method and MatchByGenericProperty constant to support property-based endpoint matching strategy.
JSON Schema Expansion
packages/go/chow/ingestvalidator/jsonschema/edge.json
Extended start/end match_by enum to include "property" option alongside "id" and "name". Added separate "property" field for property names and expanded "value" field to support multiple primitive types (number, string, boolean, array).
Ingestion Logic Refactoring
cmd/api/src/services/graphify/convertors.go, cmd/api/src/services/graphify/ingestrelationships.go
Replaced endpoint-based resolution logic with buildIngestionUpdateBatch for direct batch construction of RelationshipUpdate objects. Removed multi-endpoint resolution flow. Added helper functions: MergeNodeKinds, mergeKinds, filterOutEmptyKind, deduplicateKinds for node kind management.
Test Updates
cmd/api/src/services/graphify/ingestrelationships_integration_test.go
Replaced resolveRelationships() calls with buildIngestionUpdateBatch() in integration tests. Removed Test_ResolveAllEndpointsByName test block and associated test cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

enhancement

Suggested reviewers

  • juggernot325
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding support for arbitrary endpoint matching via a new match_by strategy called 'property' for edge ingestion, directly aligning with all modified files and the PR objectives.
Description check ✅ Passed The description provides a clear summary of changes, resolves the associated ticket (BED-7451), and addresses motivation. However, the Testing section is minimal ('Existing tests') and documentation updates remain incomplete as noted in the checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BED-7451

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.

@zinic zinic changed the title feat+chore: Support Arbitrary Endpoint Matching for Edge Ingest - BED-7451 feat: Support Arbitrary Endpoint Matching for Edge Ingest - BED-7451 Feb 25, 2026
@coderabbitai coderabbitai bot added the enhancement New feature or request label Feb 25, 2026
Copy link
Contributor

@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: 5

🧹 Nitpick comments (1)
cmd/api/src/services/graphify/ingestrelationships_integration_test.go (1)

380-380: Prefer integration assertions through IngestRelationships instead of the internal builder.

These call-site swaps now exercise buildIngestionUpdateBatch directly and bypass submission/changelog behavior in IngestRelationships. Consider keeping this helper in unit tests and using IngestRelationships for integration coverage.

Also applies to: 420-420, 450-450, 480-480, 510-510, 540-540, 570-570, 610-610, 649-649, 687-687, 725-725, 755-755, 785-785, 852-852

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/api/src/services/graphify/ingestrelationships_integration_test.go` at
line 380, Replace direct uses of the internal builder buildIngestionUpdateBatch
in the integration test with the public IngestRelationships entry point so the
test exercises submission/changelog behavior; specifically, where you currently
call buildIngestionUpdateBatch(ingestContext,
[]ein.IngestibleRelationship{ingestibleRel}, graph.EmptyKind) (and the other
similar call sites), invoke IngestRelationships with the same ingestContext and
payload (wrapping ingestibleRel as needed) and assert against the results and
side-effects produced by IngestRelationships rather than the builder, keeping
buildIngestionUpdateBatch for unit tests only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/api/src/services/graphify/convertors.go`:
- Around line 68-77: The conversion currently unconditionally calls
strings.ToUpper on entity.Start.Value and entity.End.Value when building
ein.IngestibleEndpoint, which mutates values used with match_by: property;
change the logic in convertors.go so Value is only uppercased when the
endpoint's MatchBy is not the property-match strategy: inspect
entity.Start.MatchBy / entity.End.MatchBy (or the result of
ein.IngestMatchStrategy(...)) and if it represents the property match leave
Value unchanged, otherwise use strings.ToUpper; update both constructions of
ein.IngestibleEndpoint (for Start and End) to apply this conditional
normalization.

In `@cmd/api/src/services/graphify/ingestrelationships.go`:
- Around line 194-233: buildIngestionUpdateBatch currently can panic when
rel.RelProps is nil and emits updates with empty/invalid identity properties;
before mutating rel.RelProps or building updates, validate each rel: if
rel.RelProps == nil initialize it (or add an error to errs via
errorlist.Builder) and set LastSeen, and verify rel.Source.IdentityProperty(),
rel.Source.Value, rel.Target.IdentityProperty(), and rel.Target.Value are
non-empty; if any identity property/value is empty add a descriptive error to
errs and skip producing an update for that rel (or return an error), ensuring
you use the existing errs builder so errs.Build() returns collected errors;
update code paths around rel.RelProps, rel.Source, rel.Target and the
construction of graph.RelationshipUpdate (functions/types:
buildIngestionUpdateBatch, IngestContext, MergeNodeKinds, graph.PrepareNode,
graph.PrepareRelationship) to perform these checks and filtering before
appending to updates.

In `@packages/go/chow/ingestvalidator/jsonschema/edge.json`:
- Around line 9-18: Add a JSON Schema conditional so that when the "match_by"
field equals "property" the "property" field becomes required: update the
"match_by"/"property" block in edge.json to include an "if": {"properties":
{"match_by": {"const": "property"}}}, "then": {"required": ["property"]} (and
any corresponding "else" behavior if needed); apply the same conditional guard
to the second occurrence of the same pattern (lines 33-42 equivalent) so both
schemas enforce that "property" must be present when match_by is "property".
- Around line 20-21: The JSON schema for endpoint "value" currently permits
non-string types which our ingest models (EdgeEndpoint.Value and
IngestibleEndpoint.Value) can't deserialize; update the schema entries that
define the endpoint "value" (where "type" is currently
["number","string","boolean","array"]) to restrict the type to "string" only so
validation aligns with the Go models, or alternately update EdgeEndpoint.Value
and IngestibleEndpoint.Value in the Go code to accept a typed representation if
you intend to support non-string values end-to-end; ensure the schema change is
applied to all occurrences corresponding to the endpoint value definitions
referenced by EdgeEndpoint.Value and IngestibleEndpoint.Value.

In `@packages/go/ein/models.go`:
- Around line 56-67: The IdentityProperty method on IngestibleEndpoint currently
returns an empty string when s.MatchBy is unset; change its default branch to
return the ID property instead of "" so zero-value endpoints use the default ID
behavior. In other words, in the IdentityProperty() function (handling MatchBy,
MatchByID, MatchByName, MatchByGenericProperty), ensure the fallback/else case
returns common.ObjectID.String() (same as MatchByID) rather than an empty string
so identity keys are never emitted empty.

---

Nitpick comments:
In `@cmd/api/src/services/graphify/ingestrelationships_integration_test.go`:
- Line 380: Replace direct uses of the internal builder
buildIngestionUpdateBatch in the integration test with the public
IngestRelationships entry point so the test exercises submission/changelog
behavior; specifically, where you currently call
buildIngestionUpdateBatch(ingestContext,
[]ein.IngestibleRelationship{ingestibleRel}, graph.EmptyKind) (and the other
similar call sites), invoke IngestRelationships with the same ingestContext and
payload (wrapping ingestibleRel as needed) and assert against the results and
side-effects produced by IngestRelationships rather than the builder, keeping
buildIngestionUpdateBatch for unit tests only.

ℹ️ Review info

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a13c695 and d671192.

📒 Files selected for processing (6)
  • cmd/api/src/services/graphify/convertors.go
  • cmd/api/src/services/graphify/ingestrelationships.go
  • cmd/api/src/services/graphify/ingestrelationships_integration_test.go
  • packages/go/chow/ingestvalidator/jsonschema/edge.json
  • packages/go/ein/incoming_models.go
  • packages/go/ein/models.go

Comment on lines +68 to +77
Property: entity.Start.Property,
Value: strings.ToUpper(entity.Start.Value),
MatchBy: ein.IngestMatchStrategy(entity.Start.MatchBy),
Kind: graph.StringKind(entity.Start.Kind),
},
ein.IngestibleEndpoint{
Value: strings.ToUpper(entity.End.Value),
MatchBy: ein.IngestMatchStrategy(entity.End.MatchBy),
Kind: graph.StringKind(entity.End.Kind),
Property: entity.End.Property,
Value: strings.ToUpper(entity.End.Value),
MatchBy: ein.IngestMatchStrategy(entity.End.MatchBy),
Kind: graph.StringKind(entity.End.Kind),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not uppercase values for match_by: property.

On Line 69 and Line 75, value normalization is unconditional. That mutates property-match values and can cause false misses for case-sensitive properties.

💡 Proposed fix
 func ConvertGenericEdge(entity ein.GenericEdge, converted *ConvertedData) error {
+	startMatchBy := ein.IngestMatchStrategy(entity.Start.MatchBy)
+	startValue := entity.Start.Value
+	if startMatchBy != ein.MatchByGenericProperty {
+		startValue = strings.ToUpper(startValue)
+	}
+
+	endMatchBy := ein.IngestMatchStrategy(entity.End.MatchBy)
+	endValue := entity.End.Value
+	if endMatchBy != ein.MatchByGenericProperty {
+		endValue = strings.ToUpper(endValue)
+	}
+
 	ingestibleRel := ein.NewIngestibleRelationship(
 		ein.IngestibleEndpoint{
 			Property: entity.Start.Property,
-			Value:    strings.ToUpper(entity.Start.Value),
-			MatchBy:  ein.IngestMatchStrategy(entity.Start.MatchBy),
+			Value:    startValue,
+			MatchBy:  startMatchBy,
 			Kind:     graph.StringKind(entity.Start.Kind),
 		},
 		ein.IngestibleEndpoint{
 			Property: entity.End.Property,
-			Value:    strings.ToUpper(entity.End.Value),
-			MatchBy:  ein.IngestMatchStrategy(entity.End.MatchBy),
+			Value:    endValue,
+			MatchBy:  endMatchBy,
 			Kind:     graph.StringKind(entity.End.Kind),
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/api/src/services/graphify/convertors.go` around lines 68 - 77, The
conversion currently unconditionally calls strings.ToUpper on entity.Start.Value
and entity.End.Value when building ein.IngestibleEndpoint, which mutates values
used with match_by: property; change the logic in convertors.go so Value is only
uppercased when the endpoint's MatchBy is not the property-match strategy:
inspect entity.Start.MatchBy / entity.End.MatchBy (or the result of
ein.IngestMatchStrategy(...)) and if it represents the property match leave
Value unchanged, otherwise use strings.ToUpper; update both constructions of
ein.IngestibleEndpoint (for Start and End) to apply this conditional
normalization.

Comment on lines +194 to 233
func buildIngestionUpdateBatch(batch *IngestContext, rels []ein.IngestibleRelationship, sourceKind graph.Kind) ([]graph.RelationshipUpdate, error) {
var (
filters = make([]graph.Criteria, 0, len(seen))
buildFilter = func(key endpointKey) graph.Criteria {
var criteria []graph.Criteria

criteria = append(criteria, query.Equals(query.NodeProperty(common.Name.String()), key.Name))
if key.Kind != "" {
criteria = append(criteria, query.Kind(query.Node(), graph.StringKind(key.Kind)))
}
return query.And(criteria...)
}
updates []graph.RelationshipUpdate
errs = errorlist.NewBuilder()
)

// aggregate all Name:Kind pairs in 1 DAWGs query for 1 round trip
for key := range seen {
filters = append(filters, buildFilter(key))
}

var (
resolved = map[endpointKey]string{}
ambiguous = map[endpointKey]bool{}
)

if err := batch.Nodes().Filter(query.Or(filters...)).Fetch(
func(cursor graph.Cursor[*graph.Node]) error {

for node := range cursor.Chan() {
nameVal, _ := node.Properties.Get(common.Name.String()).String()
objectID, err := node.Properties.Get(string(common.ObjectID)).String()
if err != nil || objectID == "" {
slog.Warn("Matched node missing objectid",
slog.String("name", nameVal),
slog.Any("kinds", node.Kinds))
continue
}

// edge case: resolve an empty key to match endpoints that provide no Kind filter
node.Kinds = append(node.Kinds, graph.EmptyKind)

// resolve all names found to objectids,
// record ambiguous matches (when more than one match is found, we cannot disambiguate the requested node and must skip the update)
for _, kind := range node.Kinds {
key := endpointKey{Name: strings.ToUpper(nameVal), Kind: kind.String()}
if existingID, exists := resolved[key]; exists && existingID != objectID {
ambiguous[key] = true
} else {
resolved[key] = objectID
}
}
}

return nil
},
); err != nil {
return nil, err
}

// remove ambiguous matches
for key := range ambiguous {
delete(resolved, key)
}

return resolved, nil
}
for _, rel := range rels {
rel.RelProps[common.LastSeen.String()] = batch.IngestTime

// resolveRelationships transforms a list of ingestible relationships into a
// slice of graph.RelationshipUpdate objects, suitable for ingestion into the
// graph database.
//
// The function resolves all source and target endpoints to their corresponding
// object IDs if MatchByName is set on an endpoint. Relationships with unresolved
// or ambiguous endpoints are skipped and logged with a warning.
//
// The identityKind parameter determines the identity kind used for both start
// and end nodes if provided. eg. ad.Base and az.Base are used for *hound collections, and generic ingest has no base kind.
//
// Each resolved relationship is stamped with the current UTC timestamp as the "last seen" property.
//
// Returns a slice of valid relationship updates or an error if resolution fails.
func resolveRelationships(batch *IngestContext, rels []ein.IngestibleRelationship, sourceKind graph.Kind) ([]graph.RelationshipUpdate, error) {
if cache, err := resolveAllEndpointsByName(batch.Batch, rels); err != nil {
return nil, err
} else {
var (
updates []graph.RelationshipUpdate
errs = errorlist.NewBuilder()
startIdentityProperty = rel.Source.IdentityProperty()
startKinds = MergeNodeKinds(sourceKind, rel.Source.Kind)
startProperties = graph.AsProperties(map[string]any{
startIdentityProperty: rel.Source.Value,
common.LastSeen.String(): batch.IngestTime,
})

endIdentityProperty = rel.Target.IdentityProperty()
endKinds = MergeNodeKinds(sourceKind, rel.Target.Kind)
endProperties = graph.AsProperties(map[string]any{
endIdentityProperty: rel.Target.Value,
common.LastSeen.String(): batch.IngestTime,
})
)

for _, rel := range rels {
srcID, srcOK := resolveEndpointID(rel.Source, cache)
targetID, targetOK := resolveEndpointID(rel.Target, cache)

if !srcOK || !targetOK {
slog.Warn("Skipping unresolved relationship",
slog.String("source", rel.Source.Value),
slog.String("target", rel.Target.Value),
slog.Bool("resolved_source", srcOK),
slog.Bool("resolved_target", targetOK),
slog.String("type", rel.RelType.String()))
errs.Add(
IngestUserDataError{
Msg: fmt.Sprintf("skipping invalid relationship. unable to resolve endpoints. source: %s, target: %s", rel.Source.Value, rel.Target.Value),
},
)
continue
}

rel.RelProps[common.LastSeen.String()] = batch.IngestTime

startKinds := MergeNodeKinds(sourceKind, rel.Source.Kind)
endKinds := MergeNodeKinds(sourceKind, rel.Target.Kind)

update := graph.RelationshipUpdate{
Start: graph.PrepareNode(graph.AsProperties(graph.PropertyMap{
common.ObjectID: srcID,
common.LastSeen: batch.IngestTime,
}), startKinds...),
StartIdentityProperties: []string{common.ObjectID.String()},
StartIdentityKind: sourceKind,
End: graph.PrepareNode(graph.AsProperties(graph.PropertyMap{
common.ObjectID: targetID,
common.LastSeen: batch.IngestTime,
}), endKinds...),
EndIdentityKind: sourceKind,
EndIdentityProperties: []string{common.ObjectID.String()},
Relationship: graph.PrepareRelationship(graph.AsProperties(rel.RelProps), rel.RelType),
}

updates = append(updates, update)
update := graph.RelationshipUpdate{
Start: graph.PrepareNode(startProperties, startKinds...),
StartIdentityKind: sourceKind,
StartIdentityProperties: []string{startIdentityProperty},
End: graph.PrepareNode(endProperties, endKinds...),
EndIdentityKind: sourceKind,
EndIdentityProperties: []string{endIdentityProperty},
Relationship: graph.PrepareRelationship(graph.AsProperties(rel.RelProps), rel.RelType),
}

return updates, errs.Build()
}
}

func resolveEndpointID(endpoint ein.IngestibleEndpoint, cache map[endpointKey]string) (string, bool) {
if endpoint.MatchBy == ein.MatchByName {
key := endpointKey{
Name: strings.ToUpper(endpoint.Value),
Kind: "",
}
if endpoint.Kind != nil {
key.Kind = endpoint.Kind.String()
}
id, ok := cache[key]
return id, ok
updates = append(updates, update)
}

// Fallback to raw value if matching by ID
return endpoint.Value, endpoint.Value != ""
return updates, errs.Build()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

buildIngestionUpdateBatch never rejects invalid endpoints and can panic on nil RelProps.

Line 197 initializes an error builder but no errors are added, and Line 201 can panic if rel.RelProps is nil. Also, empty identity properties/values are not filtered, so invalid updates are still emitted.

💡 Proposed fix
 func buildIngestionUpdateBatch(batch *IngestContext, rels []ein.IngestibleRelationship, sourceKind graph.Kind) ([]graph.RelationshipUpdate, error) {
 	var (
 		updates []graph.RelationshipUpdate
 		errs    = errorlist.NewBuilder()
 	)
 
 	for _, rel := range rels {
+		if rel.RelProps == nil {
+			rel.RelProps = make(map[string]any)
+		}
+
+		startIdentityProperty := rel.Source.IdentityProperty()
+		endIdentityProperty := rel.Target.IdentityProperty()
+		if rel.Source.Value == "" || rel.Target.Value == "" || startIdentityProperty == "" || endIdentityProperty == "" {
+			errs.Add(fmt.Errorf("skipping invalid relationship: invalid endpoint identity"))
+			continue
+		}
+
 		rel.RelProps[common.LastSeen.String()] = batch.IngestTime
 
 		var (
-			startIdentityProperty = rel.Source.IdentityProperty()
 			startKinds            = MergeNodeKinds(sourceKind, rel.Source.Kind)
 			startProperties       = graph.AsProperties(map[string]any{
 				startIdentityProperty:    rel.Source.Value,
 				common.LastSeen.String(): batch.IngestTime,
 			})
 
-			endIdentityProperty = rel.Target.IdentityProperty()
 			endKinds            = MergeNodeKinds(sourceKind, rel.Target.Kind)
 			endProperties       = graph.AsProperties(map[string]any{
 				endIdentityProperty:      rel.Target.Value,
 				common.LastSeen.String(): batch.IngestTime,
 			})
 		)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/api/src/services/graphify/ingestrelationships.go` around lines 194 - 233,
buildIngestionUpdateBatch currently can panic when rel.RelProps is nil and emits
updates with empty/invalid identity properties; before mutating rel.RelProps or
building updates, validate each rel: if rel.RelProps == nil initialize it (or
add an error to errs via errorlist.Builder) and set LastSeen, and verify
rel.Source.IdentityProperty(), rel.Source.Value, rel.Target.IdentityProperty(),
and rel.Target.Value are non-empty; if any identity property/value is empty add
a descriptive error to errs and skip producing an update for that rel (or return
an error), ensuring you use the existing errs builder so errs.Build() returns
collected errors; update code paths around rel.RelProps, rel.Source, rel.Target
and the construction of graph.RelationshipUpdate (functions/types:
buildIngestionUpdateBatch, IngestContext, MergeNodeKinds, graph.PrepareNode,
graph.PrepareRelationship) to perform these checks and filtering before
appending to updates.

Comment on lines 9 to +18
"match_by": {
"type": "string",
"enum": ["id", "name"],
"enum": ["id", "name", "property"],
"default": "id",
"description": "Whether to match the start node by its unique object ID or by its name property."
"description": "Matching method to use. Addressing a node by its 'objectid' or 'name' property which are indexed. All other lookups may use the 'property' match_by option and specify a property name to target."
},
"value": {
"property": {
"type": "string",
"description": "Optional. The property name to match against when using the 'property' match_by method."
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require property when match_by is "property".

Right now property is optional for the property strategy. That permits invalid references (no identity property key) to pass schema validation.

💡 Proposed schema guard
     "start": {
       "type": "object",
       "properties": {
@@
       },
+      "allOf": [
+        {
+          "if": {
+            "properties": { "match_by": { "const": "property" } },
+            "required": ["match_by"]
+          },
+          "then": { "required": ["property", "value"] }
+        }
+      ],
       "required": ["value"]
     },
@@
     "end": {
       "type": "object",
       "properties": {
@@
       },
+      "allOf": [
+        {
+          "if": {
+            "properties": { "match_by": { "const": "property" } },
+            "required": ["match_by"]
+          },
+          "then": { "required": ["property", "value"] }
+        }
+      ],
       "required": ["value"]
     },

Also applies to: 33-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/chow/ingestvalidator/jsonschema/edge.json` around lines 9 - 18,
Add a JSON Schema conditional so that when the "match_by" field equals
"property" the "property" field becomes required: update the
"match_by"/"property" block in edge.json to include an "if": {"properties":
{"match_by": {"const": "property"}}}, "then": {"required": ["property"]} (and
any corresponding "else" behavior if needed); apply the same conditional guard
to the second occurrence of the same pattern (lines 33-42 equivalent) so both
schemas enforce that "property" must be present when match_by is "property".

Comment on lines +20 to 21
"type": ["number", "string", "boolean", "array"],
"description": "The value used for matching — either an object ID or a name, depending on match_by."
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Schema accepts endpoint value types that current ingest models cannot deserialize.

Line 20/44 now permit non-string value, and Line 91 demonstrates numeric input. But EdgeEndpoint.Value and IngestibleEndpoint.Value are still string in packages/go/ein/incoming_models.go (Line 386) and packages/go/ein/models.go (Line 51). This creates validate-pass but ingest-fail payloads.

💡 Safe contract fix (until typed values are supported end-to-end)
-        "value": {
-          "type": ["number", "string", "boolean", "array"],
+        "value": {
+          "type": "string",
           "description": "The value used for matching — either an object ID or a name, depending on match_by."
         },
@@
-        "value": {
-          "type": ["number", "string", "boolean", "array"],
+        "value": {
+          "type": "string",
           "description": "The value used for matching — either an object ID or a name, depending on match_by."
         },

Also applies to: 44-45, 87-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/chow/ingestvalidator/jsonschema/edge.json` around lines 20 - 21,
The JSON schema for endpoint "value" currently permits non-string types which
our ingest models (EdgeEndpoint.Value and IngestibleEndpoint.Value) can't
deserialize; update the schema entries that define the endpoint "value" (where
"type" is currently ["number","string","boolean","array"]) to restrict the type
to "string" only so validation aligns with the Go models, or alternately update
EdgeEndpoint.Value and IngestibleEndpoint.Value in the Go code to accept a typed
representation if you intend to support non-string values end-to-end; ensure the
schema change is applied to all occurrences corresponding to the endpoint value
definitions referenced by EdgeEndpoint.Value and IngestibleEndpoint.Value.

Comment on lines +56 to 67
func (s IngestibleEndpoint) IdentityProperty() string {
switch s.MatchBy {
case MatchByID:
return common.ObjectID.String()
case MatchByName:
return common.Name.String()
case MatchByGenericProperty:
return s.Property
}

return ""
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore default ID matching in IdentityProperty().

Line 56-67 returns "" when MatchBy is unset. That breaks existing zero-value endpoints that rely on default ID behavior and can emit empty identity property keys.

💡 Proposed fix
 func (s IngestibleEndpoint) IdentityProperty() string {
 	switch s.MatchBy {
-	case MatchByID:
+	case "", MatchByID:
 		return common.ObjectID.String()
 	case MatchByName:
 		return common.Name.String()
 	case MatchByGenericProperty:
 		return s.Property
 	}
 
 	return ""
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s IngestibleEndpoint) IdentityProperty() string {
switch s.MatchBy {
case MatchByID:
return common.ObjectID.String()
case MatchByName:
return common.Name.String()
case MatchByGenericProperty:
return s.Property
}
return ""
}
func (s IngestibleEndpoint) IdentityProperty() string {
switch s.MatchBy {
case "", MatchByID:
return common.ObjectID.String()
case MatchByName:
return common.Name.String()
case MatchByGenericProperty:
return s.Property
}
return ""
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/go/ein/models.go` around lines 56 - 67, The IdentityProperty method
on IngestibleEndpoint currently returns an empty string when s.MatchBy is unset;
change its default branch to return the ID property instead of "" so zero-value
endpoints use the default ID behavior. In other words, in the IdentityProperty()
function (handling MatchBy, MatchByID, MatchByName, MatchByGenericProperty),
ensure the fallback/else case returns common.ObjectID.String() (same as
MatchByID) rather than an empty string so identity keys are never emitted empty.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant