Skip to content

Commit 32ba9ec

Browse files
authored
fix: resolve correct aspect name per entity type in UpdateDescription (#86)
* fix: resolve correct aspect name per entity type in UpdateDescription (#85) UpdateDescription hardcoded editableDatasetProperties, causing writes to fail for non-dataset entities. Now resolves the correct aspect name and field name from a mapping table based on the entity type in the URN. * refactor: improve descriptionAspect type and test assertions - Rename editablePropertiesAspect to descriptionAspect (handles non-editable aspects too) - Use errors.New for ErrUnsupportedEntityType sentinel - Add UnmarshalJSON for symmetric serialization - Strengthen test assertions to verify preserved field values, not just existence - Document intentionally excluded entity types
1 parent 575023b commit 32ba9ec

File tree

2 files changed

+318
-33
lines changed

2 files changed

+318
-33
lines changed

pkg/client/write.go

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import (
88
"time"
99
)
1010

11+
// ErrUnsupportedEntityType is returned when an entity type does not support description updates.
12+
// Entity types intentionally excluded (no editable description aspect in DataHub):
13+
// tag, corpuser, corpGroup, mlModel, mlModelGroup, notebook.
14+
var ErrUnsupportedEntityType = errors.New("unsupported entity type for description update")
15+
1116
// entityTypeFromURN derives the DataHub entity type string from a parsed URN.
1217
// Maps URN entity types to the REST API entity type names.
1318
func entityTypeFromURN(urn string) (string, error) {
@@ -18,6 +23,39 @@ func entityTypeFromURN(urn string) (string, error) {
1823
return parsed.EntityType, nil
1924
}
2025

26+
// descriptionAspectInfo holds the aspect name and field name for updating
27+
// an entity's description. The field is "description" for most entity types,
28+
// but "definition" for glossary entities.
29+
type descriptionAspectInfo struct {
30+
AspectName string
31+
FieldName string
32+
}
33+
34+
// descriptionAspectMap maps DataHub entity types to their description aspect.
35+
// glossaryTerm and glossaryNode use "definition" instead of "description".
36+
// dataProduct and domain use non-editable property aspects.
37+
var descriptionAspectMap = map[string]descriptionAspectInfo{
38+
"dataset": {AspectName: "editableDatasetProperties", FieldName: "description"},
39+
"dashboard": {AspectName: "editableDashboardProperties", FieldName: "description"},
40+
"chart": {AspectName: "editableChartProperties", FieldName: "description"},
41+
"dataFlow": {AspectName: "editableDataFlowProperties", FieldName: "description"},
42+
"dataJob": {AspectName: "editableDataJobProperties", FieldName: "description"},
43+
"container": {AspectName: "editableContainerProperties", FieldName: "description"},
44+
"dataProduct": {AspectName: "dataProductProperties", FieldName: "description"},
45+
"domain": {AspectName: "domainProperties", FieldName: "description"},
46+
"glossaryTerm": {AspectName: "glossaryTermInfo", FieldName: "definition"},
47+
"glossaryNode": {AspectName: "glossaryNodeInfo", FieldName: "definition"},
48+
}
49+
50+
// lookupDescriptionAspect returns the aspect info for updating the description of the given entity type.
51+
func lookupDescriptionAspect(entityType string) (descriptionAspectInfo, error) {
52+
info, ok := descriptionAspectMap[entityType]
53+
if !ok {
54+
return descriptionAspectInfo{}, fmt.Errorf("%w: %s", ErrUnsupportedEntityType, entityType)
55+
}
56+
return info, nil
57+
}
58+
2159
// editableSchemaAspect is the REST API representation of editableSchemaMetadata.
2260
type editableSchemaAspect struct {
2361
EditableSchemaFieldInfo []editableFieldInfo `json:"editableSchemaFieldInfo"`
@@ -32,49 +70,77 @@ type editableFieldInfo struct {
3270
GlossaryTerms json.RawMessage `json:"glossaryTerms,omitempty"`
3371
}
3472

35-
// editablePropertiesAspect represents the editableDatasetProperties aspect.
36-
type editablePropertiesAspect struct {
37-
Description string `json:"description"`
38-
Created *auditStampRaw `json:"created,omitempty"`
39-
LastModified *auditStampRaw `json:"lastModified,omitempty"`
73+
// descriptionAspect represents a generic properties aspect for description updates.
74+
// Uses a map to preserve all existing fields during read-modify-write regardless of
75+
// which aspect is being updated — different aspects have different schemas.
76+
type descriptionAspect struct {
77+
fields map[string]json.RawMessage
78+
}
79+
80+
// MarshalJSON serializes the aspect as a flat JSON object.
81+
func (a *descriptionAspect) MarshalJSON() ([]byte, error) {
82+
return json.Marshal(a.fields)
83+
}
84+
85+
// UnmarshalJSON deserializes a flat JSON object into the aspect's field map.
86+
func (a *descriptionAspect) UnmarshalJSON(data []byte) error {
87+
return json.Unmarshal(data, &a.fields)
88+
}
89+
90+
// setDescription sets the description value in the aspect under the given field name.
91+
func (a *descriptionAspect) setDescription(fieldName, value string) error {
92+
encoded, err := json.Marshal(value)
93+
if err != nil {
94+
return fmt.Errorf("encoding description: %w", err)
95+
}
96+
a.fields[fieldName] = encoded
97+
return nil
4098
}
4199

42100
// UpdateDescription sets the editable description for any entity using read-modify-write.
101+
// Resolves the correct aspect name and field name based on the entity type in the URN.
43102
func (c *Client) UpdateDescription(ctx context.Context, urn, description string) error {
44103
entityType, err := entityTypeFromURN(urn)
45104
if err != nil {
46105
return fmt.Errorf("UpdateDescription: %w", err)
47106
}
48107

49-
props, err := c.readEditableProperties(ctx, urn)
108+
aspectInfo, err := lookupDescriptionAspect(entityType)
50109
if err != nil {
51110
return fmt.Errorf("UpdateDescription: %w", err)
52111
}
53112

54-
props.Description = description
113+
props, err := c.readEditableProperties(ctx, urn, aspectInfo.AspectName)
114+
if err != nil {
115+
return fmt.Errorf("UpdateDescription: %w", err)
116+
}
117+
118+
if err := props.setDescription(aspectInfo.FieldName, description); err != nil {
119+
return fmt.Errorf("UpdateDescription: %w", err)
120+
}
55121

56122
return c.postIngestProposal(ctx, ingestProposal{
57123
EntityType: entityType,
58124
EntityURN: urn,
59-
AspectName: "editableDatasetProperties",
125+
AspectName: aspectInfo.AspectName,
60126
Aspect: props,
61127
})
62128
}
63129

64-
// readEditableProperties reads the current editableDatasetProperties aspect.
130+
// readEditableProperties reads the current properties aspect for an entity.
65131
// Returns an empty aspect if none exists (not an error).
66-
func (c *Client) readEditableProperties(ctx context.Context, urn string) (*editablePropertiesAspect, error) {
67-
raw, err := c.getAspect(ctx, urn, "editableDatasetProperties")
132+
func (c *Client) readEditableProperties(ctx context.Context, urn, aspectName string) (*descriptionAspect, error) {
133+
raw, err := c.getAspect(ctx, urn, aspectName)
68134
if err != nil {
69135
if errors.Is(err, ErrNotFound) {
70-
return &editablePropertiesAspect{}, nil
136+
return &descriptionAspect{fields: map[string]json.RawMessage{}}, nil
71137
}
72-
return nil, fmt.Errorf("reading editableDatasetProperties: %w", err)
138+
return nil, fmt.Errorf("reading %s: %w", aspectName, err)
73139
}
74140

75-
var props editablePropertiesAspect
141+
var props descriptionAspect
76142
if err := json.Unmarshal(raw, &props); err != nil {
77-
return nil, fmt.Errorf("parsing editableDatasetProperties: %w", err)
143+
return nil, fmt.Errorf("parsing %s: %w", aspectName, err)
78144
}
79145
return &props, nil
80146
}

0 commit comments

Comments
 (0)