Skip to content
Closed
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
65 changes: 65 additions & 0 deletions pkg/clients/create_data_element_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package clients

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)

// CreateDataElementRequest is the request body for creating a Data Element asset.
type CreateDataElementRequest struct {
Name string `json:"name"`
DomainId string `json:"domainId"`
TypeId string `json:"typeId"`
DisplayName string `json:"displayName,omitempty"`
StatusId string `json:"statusId,omitempty"`
}

// CreateDataElementResponse is the response from creating a Data Element asset.
type CreateDataElementResponse struct {
Id string `json:"id"`
ResourceType string `json:"resourceType"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
CreatedOn int64 `json:"createdOn"`
CreatedBy string `json:"createdBy"`
}

// CreateDataElement creates a new Data Element asset via the Collibra REST API.
func CreateDataElement(ctx context.Context, client *http.Client, reqBody CreateDataElementRequest) (*CreateDataElementResponse, error) {
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/rest/2.0/assets", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()

Check failure on line 49 in pkg/clients/create_data_element_client.go

View workflow job for this annotation

GitHub Actions / build

Error return value of `resp.Body.Close` is not checked (errcheck)

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}

if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
}

var result CreateDataElementResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &result, nil
}
64 changes: 64 additions & 0 deletions pkg/tools/create_data_element/tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package create_data_element

import (
"context"
"fmt"
"net/http"

"github.com/collibra/chip/pkg/chip"
"github.com/collibra/chip/pkg/clients"
)

// DataElementAssetTypeId is the Collibra asset type UUID for Data Element.
const DataElementAssetTypeId = "00000000-0000-0000-0000-000000031302"

// Input defines the parameters for creating a Data Element asset.
type Input struct {
Name string `json:"name" jsonschema:"Name of the Data Element asset to create. Must be unique within the domain."`
DomainId string `json:"domain_id" jsonschema:"UUID of the domain to create the Data Element in."`
DisplayName string `json:"display_name,omitempty" jsonschema:"Optional. Display name for the Data Element asset."`
StatusId string `json:"status_id,omitempty" jsonschema:"Optional. UUID of the status to assign to the Data Element."`
}

// Output defines the result of creating a Data Element asset.
type Output struct {
Id string `json:"id" jsonschema:"UUID of the created Data Element asset."`
ResourceType string `json:"resource_type" jsonschema:"Resource type of the created asset."`
}

// NewTool creates a new create_data_element tool instance.
func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] {
return &chip.Tool[Input, Output]{
Name: "create_data_element",
Description: "Create a new Data Element asset in a specified Collibra domain.",
Handler: handler(collibraClient),
Permissions: []string{"dgc.ai-copilot"},
}
}

func handler(collibraClient *http.Client) chip.ToolHandlerFunc[Input, Output] {
return func(ctx context.Context, input Input) (Output, error) {
if input.Name == "" {
return Output{}, fmt.Errorf("name is required")
}
if input.DomainId == "" {
return Output{}, fmt.Errorf("domain_id is required")
}

resp, err := clients.CreateDataElement(ctx, collibraClient, clients.CreateDataElementRequest{
Name: input.Name,
DomainId: input.DomainId,
TypeId: DataElementAssetTypeId,
DisplayName: input.DisplayName,
StatusId: input.StatusId,
})
if err != nil {
return Output{}, err
}

return Output{
Id: resp.Id,
ResourceType: resp.ResourceType,
}, nil
}
}
200 changes: 200 additions & 0 deletions pkg/tools/create_data_element/tool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package create_data_element_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/collibra/chip/pkg/clients"
"github.com/collibra/chip/pkg/tools/create_data_element"
"github.com/collibra/chip/pkg/tools/testutil"
)

func TestCreateDataElement_Success(t *testing.T) {
handler := http.NewServeMux()
handler.Handle("/rest/2.0/assets", testutil.JsonHandlerInOut(
func(_ *http.Request, req clients.CreateDataElementRequest) (int, clients.CreateDataElementResponse) {
if req.Name != "test_element" {
t.Errorf("expected name %q, got %q", "test_element", req.Name)
}
if req.DomainId != "d1234567-abcd-1234-abcd-1234567890ab" {
t.Errorf("expected domainId %q, got %q", "d1234567-abcd-1234-abcd-1234567890ab", req.DomainId)
}
if req.TypeId != create_data_element.DataElementAssetTypeId {
t.Errorf("expected typeId %q, got %q", create_data_element.DataElementAssetTypeId, req.TypeId)
}
return http.StatusCreated, clients.CreateDataElementResponse{
Id: "a1234567-abcd-1234-abcd-1234567890ab",
ResourceType: "Asset",
Name: req.Name,
DisplayName: req.Name,
CreatedOn: 1700000000000,
CreatedBy: "user-uuid",
}
},
))

server := httptest.NewServer(handler)
defer server.Close()

client := testutil.NewClient(server)
output, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if output.Id != "a1234567-abcd-1234-abcd-1234567890ab" {
t.Errorf("got id %q, want %q", output.Id, "a1234567-abcd-1234-abcd-1234567890ab")
}
if output.ResourceType != "Asset" {
t.Errorf("got resource_type %q, want %q", output.ResourceType, "Asset")
}
}

func TestCreateDataElement_WithOptionalFields(t *testing.T) {
handler := http.NewServeMux()
handler.Handle("/rest/2.0/assets", testutil.JsonHandlerInOut(
func(_ *http.Request, req clients.CreateDataElementRequest) (int, clients.CreateDataElementResponse) {
if req.DisplayName != "My Display Name" {
t.Errorf("expected displayName %q, got %q", "My Display Name", req.DisplayName)
}
if req.StatusId != "s1234567-abcd-1234-abcd-1234567890ab" {
t.Errorf("expected statusId %q, got %q", "s1234567-abcd-1234-abcd-1234567890ab", req.StatusId)
}
return http.StatusCreated, clients.CreateDataElementResponse{
Id: "b1234567-abcd-1234-abcd-1234567890ab",
ResourceType: "Asset",
Name: req.Name,
DisplayName: req.DisplayName,
CreatedOn: 1700000000000,
CreatedBy: "user-uuid",
}
},
))

server := httptest.NewServer(handler)
defer server.Close()

client := testutil.NewClient(server)
output, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
DisplayName: "My Display Name",
StatusId: "s1234567-abcd-1234-abcd-1234567890ab",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if output.Id != "b1234567-abcd-1234-abcd-1234567890ab" {
t.Errorf("got id %q, want %q", output.Id, "b1234567-abcd-1234-abcd-1234567890ab")
}
if output.ResourceType != "Asset" {
t.Errorf("got resource_type %q, want %q", output.ResourceType, "Asset")
}
}

func TestCreateDataElement_MissingName(t *testing.T) {
client := &http.Client{}
_, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
})
if err == nil {
t.Fatal("expected error for missing name, got nil")
}
if err.Error() != "name is required" {
t.Errorf("got error %q, want %q", err.Error(), "name is required")
}
}

func TestCreateDataElement_MissingDomainId(t *testing.T) {
client := &http.Client{}
_, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
})
if err == nil {
t.Fatal("expected error for missing domain_id, got nil")
}
if err.Error() != "domain_id is required" {
t.Errorf("got error %q, want %q", err.Error(), "domain_id is required")
}
}

func TestCreateDataElement_DuplicateNameConflict(t *testing.T) {
handler := http.NewServeMux()
handler.Handle("/rest/2.0/assets", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"errorCode":"DUPLICATE","message":"Asset with name 'test_element' already exists in domain"}`))
}))

server := httptest.NewServer(handler)
defer server.Close()

client := testutil.NewClient(server)
_, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
})
if err == nil {
t.Fatal("expected error for duplicate name, got nil")
}
expected := `unexpected status 400: {"errorCode":"DUPLICATE","message":"Asset with name 'test_element' already exists in domain"}`
if err.Error() != expected {
t.Errorf("got error %q, want %q", err.Error(), expected)
}
}

func TestCreateDataElement_ServerError(t *testing.T) {
handler := http.NewServeMux()
handler.Handle("/rest/2.0/assets", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`internal server error`))
}))

server := httptest.NewServer(handler)
defer server.Close()

client := testutil.NewClient(server)
_, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
})
if err == nil {
t.Fatal("expected error for server error, got nil")
}
}

func TestCreateDataElement_TypeIdIsSet(t *testing.T) {
var capturedTypeId string
handler := http.NewServeMux()
handler.Handle("/rest/2.0/assets", testutil.JsonHandlerInOut(
func(_ *http.Request, req clients.CreateDataElementRequest) (int, clients.CreateDataElementResponse) {
capturedTypeId = req.TypeId
return http.StatusCreated, clients.CreateDataElementResponse{
Id: "c1234567-abcd-1234-abcd-1234567890ab",
ResourceType: "Asset",
Name: req.Name,
}
},
))

server := httptest.NewServer(handler)
defer server.Close()

client := testutil.NewClient(server)
_, err := create_data_element.NewTool(client).Handler(t.Context(), create_data_element.Input{
Name: "test_element",
DomainId: "d1234567-abcd-1234-abcd-1234567890ab",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if capturedTypeId != create_data_element.DataElementAssetTypeId {
t.Errorf("got typeId %q, want %q", capturedTypeId, create_data_element.DataElementAssetTypeId)
}
}
2 changes: 2 additions & 0 deletions pkg/tools/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/collibra/chip/pkg/tools/add_data_classification_match"
"github.com/collibra/chip/pkg/tools/ask_dad"
"github.com/collibra/chip/pkg/tools/ask_glossary"
"github.com/collibra/chip/pkg/tools/create_data_element"
"github.com/collibra/chip/pkg/tools/find_data_classification_matches"
"github.com/collibra/chip/pkg/tools/get_asset_details"
"github.com/collibra/chip/pkg/tools/keyword_search"
Expand All @@ -31,6 +32,7 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv
toolRegister(server, toolConfig, list_data_contracts.NewTool(client))
toolRegister(server, toolConfig, push_data_contract_manifest.NewTool(client))
toolRegister(server, toolConfig, pull_data_contract_manifest.NewTool(client))
toolRegister(server, toolConfig, create_data_element.NewTool(client))
}

func toolRegister[In, Out any](server *chip.Server, toolConfig *chip.ServerToolConfig, tool *chip.Tool[In, Out]) {
Expand Down
Loading