Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions opengin/core-api/cmd/server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import (
// Server implements the COREService
type Server struct {
pb.UnimplementedCOREServiceServer
mongoRepo *mongorepository.MongoRepository
neo4jRepo *neo4jrepository.Neo4jRepository
postgresRepo *postgres.PostgresRepository
attributeProcessor *engine.EntityAttributeProcessor
mongoRepo *mongorepository.MongoRepository
neo4jRepo *neo4jrepository.Neo4jRepository
postgresRepo *postgres.PostgresRepository
attributeProcessor *engine.EntityAttributeProcessor
}

// CreateEntity handles entity creation with relationships, metadata and attributes
Expand Down Expand Up @@ -354,29 +354,7 @@ func (s *Server) ReadEntities(ctx context.Context, req *pb.ReadEntityRequest) (*
// Convert filtered entities to pb.Entity format
var entities []*pb.Entity
for _, entity := range filteredEntities {
pbEntity := &pb.Entity{
Id: entity["id"].(string),
Kind: &pb.Kind{
Major: entity["kind"].(string),
Minor: entity["minorKind"].(string),
},
Created: entity["created"].(string),
Name: &pb.TimeBasedValue{ // How to represent time based value name?
StartTime: entity["created"].(string),
Value: &anypb.Any{
TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
Value: []byte(entity["name"].(string)),
},
},
}

// Add terminated if present
if terminated, ok := entity["terminated"].(string); ok && terminated != "" {
pbEntity.Terminated = terminated
pbEntity.Name.EndTime = terminated
}

entities = append(entities, pbEntity)
entities = append(entities, entityFromGraphFilterResult(entity))
}

return &pb.EntityList{
Expand Down
44 changes: 43 additions & 1 deletion opengin/core-api/cmd/server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,52 @@ package main

import (
"fmt"
pb "lk/datafoundation/core-api/lk/datafoundation/core-api"
"log"

"lk/datafoundation/core-api/commons"
pb "lk/datafoundation/core-api/lk/datafoundation/core-api"
)

// entityFromGraphFilterResult maps a Neo4j filter row to a protobuf Entity.
// Name is packed as google.protobuf.StringValue (same framing as ReadEntity).
func entityFromGraphFilterResult(entity map[string]interface{}) *pb.Entity {
created := ""
if v, ok := entity["created"].(string); ok {
created = v
}
terminated := ""
if v, ok := entity["terminated"].(string); ok {
terminated = v
}
name := ""
if v, ok := entity["name"].(string); ok {
name = v
}
major := ""
if v, ok := entity["kind"].(string); ok {
major = v
}
minor := ""
if v, ok := entity["minorKind"].(string); ok {
minor = v
}
id := ""
if v, ok := entity["id"].(string); ok {
id = v
}

return &pb.Entity{
Id: id,
Kind: &pb.Kind{
Major: major,
Minor: minor,
},
Created: created,
Terminated: terminated,
Name: commons.CreateTimeBasedValue(created, terminated, name),
}
}

func debugMetadata(req *pb.Entity) {

log.Printf("Reading Entity metadata: %s", req.Id)
Expand Down
72 changes: 72 additions & 0 deletions opengin/core-api/cmd/server/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2025 Lanka Data Foundation
// SPDX-License-Identifier: Apache-2.0

package main

import (
"bytes"
"testing"

"lk/datafoundation/core-api/commons"

"google.golang.org/protobuf/types/known/wrapperspb"
)

func TestEntityFromGraphFilterResultPacksFramedStringValue(t *testing.T) {
const entityName = "Ministry of Health"
entity := entityFromGraphFilterResult(map[string]interface{}{
"id": "minister-health-001",
"kind": "Organization",
"minorKind": "Minister",
"created": "2024-01-01T00:00:00Z",
"terminated": "",
"name": entityName,
})

if entity.Name == nil || entity.Name.Value == nil {
t.Fatal("expected name Any to be set")
}

wantType := "type.googleapis.com/google.protobuf.StringValue"
if entity.Name.Value.TypeUrl != wantType {
t.Fatalf("typeUrl = %q, want %q", entity.Name.Value.TypeUrl, wantType)
}

// Raw UTF-8 (the old ReadEntities bug) must not be stored in Any.Value.
if bytes.Equal(entity.Name.Value.Value, []byte(entityName)) {
t.Fatal("name Any.Value is raw UTF-8; expected protobuf StringValue framing")
}

var unpacked wrapperspb.StringValue
if err := entity.Name.Value.UnmarshalTo(&unpacked); err != nil {
t.Fatalf("UnmarshalTo StringValue: %v", err)
}
if unpacked.Value != entityName {
t.Fatalf("unpacked name = %q, want %q", unpacked.Value, entityName)
}

if got := commons.ExtractStringFromAny(entity.Name.Value); got != entityName {
t.Fatalf("ExtractStringFromAny = %q, want %q", got, entityName)
}

if entity.Name.StartTime != "2024-01-01T00:00:00Z" {
t.Fatalf("StartTime = %q", entity.Name.StartTime)
}
}

func TestEntityFromGraphFilterResultSetsTerminatedOnName(t *testing.T) {
entity := entityFromGraphFilterResult(map[string]interface{}{
"id": "ended-entity",
"kind": "Person",
"minorKind": "Employee",
"created": "2024-01-01T00:00:00Z",
"terminated": "2024-12-31T00:00:00Z",
"name": "Ended",
})
if entity.Terminated != "2024-12-31T00:00:00Z" {
t.Fatalf("Terminated = %q", entity.Terminated)
}
if entity.Name.EndTime != "2024-12-31T00:00:00Z" {
t.Fatalf("Name.EndTime = %q", entity.Name.EndTime)
}
}
46 changes: 46 additions & 0 deletions opengin/core-api/commons/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 Lanka Data Foundation
// SPDX-License-Identifier: Apache-2.0

package commons

import (
"bytes"
"testing"

"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)

func TestConvertStringToAnyIncludesProtobufFraming(t *testing.T) {
const name = "Ministry of Health"
packed := ConvertStringToAny(name)

if bytes.Equal(packed.Value, []byte(name)) {
t.Fatal("ConvertStringToAny stored raw UTF-8; expected StringValue framing")
}
// google.protobuf.StringValue is field 1, length-delimited: 0x0A <len> <utf8>
if len(packed.Value) < 2 || packed.Value[0] != 0x0A || packed.Value[1] != byte(len(name)) {
t.Fatalf("missing StringValue framing, got %x", packed.Value)
}

var sv wrapperspb.StringValue
if err := packed.UnmarshalTo(&sv); err != nil {
t.Fatalf("UnmarshalTo: %v", err)
}
if sv.Value != name {
t.Fatalf("unpacked = %q, want %q", sv.Value, name)
}

// Contrast: labeling raw UTF-8 as StringValue (the old search bug) does not decode.
broken := &anypb.Any{
TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
Value: []byte(name),
}
var brokenSV wrapperspb.StringValue
if err := broken.UnmarshalTo(&brokenSV); err == nil && brokenSV.Value == name {
t.Fatal("raw UTF-8 Any unexpectedly unmarshaled to the full name")
}
if ExtractStringFromAny(broken) == name {
t.Fatal("ExtractStringFromAny should not treat unframed UTF-8 as a StringValue")
}
}
19 changes: 10 additions & 9 deletions opengin/read-api/read_api_service.bal
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,22 @@ COREServiceClient ep = check new (coreServiceUrl, grpcConfig);
function extractValueAsString('any:Any anyValue) returns string {
string typeUrl = anyValue.typeUrl;

// Default to using the value's string representation
string valueStr = anyValue.toString();
// Unpack protobuf StringValue (used for entity names) to the plain string.
// any:toString() would otherwise return a JSON blob of {typeUrl, hex value}.
if typeUrl.endsWith("StringValue") || typeUrl.endsWith("string") {
string|error unpacked = pbAny:unpack(anyValue, string);
if unpacked is string {
return unpacked;
}
}

// Check if it's a known primitive type and handle accordingly
if (typeUrl.endsWith("string")) {
return anyValue.value.toString();
} else if (typeUrl.endsWith("int32") || typeUrl.endsWith("int64") ||
if (typeUrl.endsWith("int32") || typeUrl.endsWith("int64") ||
typeUrl.endsWith("float") || typeUrl.endsWith("double")) {
return anyValue.value.toString();
} else if (typeUrl.endsWith("bool")) {
return anyValue.value.toString();
} else {
// For complex types, attempt to return as JSON string if possible
return valueStr;
}
return anyValue.toString();
}

// Helper function to convert decimal values to float for protobuf compatibility
Expand Down
8 changes: 8 additions & 0 deletions opengin/read-api/tests/read_api_service_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ function unwrapAny(pbAny:Any anyValue) returns string|error {
return pbAny:unpack(anyValue, string);
}

@test:Config {}
function testExtractValueAsStringReturnsPlainName() returns error? {
pbAny:Any packed = check pbAny:pack("Ministry of Health");
string name = extractValueAsString(packed);
test:assertEquals(name, "Ministry of Health");
test:assertFalse(name.startsWith("{"), "name must not be a JSON-serialized Any");
}

// Helper function to convert decimal values to float for protobuf compatibility
// Note that this is a temporary solution to convert decimal values to float for protobuf compatibility.
// It is not a permanent solution and should be removed when the protobuf library is updated to support decimal values.
Expand Down
13 changes: 2 additions & 11 deletions opengin/tests/e2e/basic_core_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,17 +708,8 @@ def read_minister(self):
f"Expected created date {self.START_DATE}, got {response_data['created']}"
)

print("im here befire you bitchhhhh....")

# The name value is a protobuf Any that needs to be decoded
print("i am here..............")
name_obj = json.loads(response_data["name"])
print(name_obj)
hex_value = name_obj["value"]
print(hex_value)
decoded_name = bytes.fromhex(hex_value).decode('utf-8')
assert decoded_name == "Minister of Finance and Economy", (
f"Expected name 'Minister of Finance and Economy', got {decoded_name}"
assert response_data["name"] == "Minister of Finance and Economy", (
f"Expected name 'Minister of Finance and Economy', got {response_data['name']}"
)

def update_attributes_stage_1(self):
Expand Down
Loading