Skip to content

Commit 52b1644

Browse files
authored
feat(patch): Add support for * in path with arrayPrimaryKeys; add documentation patch (datahub-project#16906)
1 parent 7eca465 commit 52b1644

55 files changed

Lines changed: 3096 additions & 365 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/builder/AbstractMultiFieldPatchBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44

55
import com.fasterxml.jackson.databind.JsonNode;
66
import com.fasterxml.jackson.databind.node.ArrayNode;
7+
import com.google.common.annotations.VisibleForTesting;
78
import com.linkedin.common.urn.Urn;
89
import com.linkedin.data.ByteString;
910
import com.linkedin.events.metadata.ChangeType;
1011
import com.linkedin.mxe.GenericAspect;
1112
import com.linkedin.mxe.MetadataChangeProposal;
13+
import jakarta.json.Json;
14+
import jakarta.json.JsonArrayBuilder;
15+
import jakarta.json.JsonObjectBuilder;
16+
import jakarta.json.JsonPatch;
17+
import java.io.StringReader;
1218
import java.nio.charset.StandardCharsets;
1319
import java.util.ArrayList;
1420
import java.util.List;
@@ -95,12 +101,32 @@ protected GenericAspect buildPatch() {
95101
.set(VALUE_KEY, triple.right)));
96102

97103
GenericAspect genericAspect = new GenericAspect();
98-
genericAspect.setContentType("application/json");
104+
genericAspect.setContentType("application/json-patch+json");
99105
genericAspect.setValue(ByteString.copyString(patches.toString(), StandardCharsets.UTF_8));
100106

101107
return genericAspect;
102108
}
103109

110+
@VisibleForTesting
111+
public JsonPatch getJsonPatch() {
112+
JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
113+
List<ImmutableTriple<String, String, JsonNode>> triples = getPathValues();
114+
triples.forEach(
115+
triple -> {
116+
JsonObjectBuilder opBuilder =
117+
Json.createObjectBuilder().add(OP_KEY, triple.left).add(PATH_KEY, triple.middle);
118+
if (triple.right != null) {
119+
opBuilder.add(
120+
VALUE_KEY,
121+
Json.createReader(new StringReader(triple.right.toString())).readValue());
122+
} else {
123+
opBuilder.addNull(VALUE_KEY);
124+
}
125+
arrayBuilder.add(opBuilder);
126+
});
127+
return Json.createPatch(arrayBuilder.build());
128+
}
129+
104130
/**
105131
* Constructs a list of Op, Path, Value triples to create as patches. Not idempotent and should
106132
* not be called more than once
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.linkedin.metadata.aspect.patch.builder;
2+
3+
import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
4+
import static com.linkedin.metadata.Constants.DOCUMENTATION_ASPECT_NAME;
5+
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
7+
import com.linkedin.common.urn.Urn;
8+
import com.linkedin.metadata.aspect.patch.PatchOperationType;
9+
import javax.annotation.Nonnull;
10+
import org.apache.commons.lang3.tuple.ImmutableTriple;
11+
12+
public class DocumentationPatchBuilder
13+
extends AbstractMultiFieldPatchBuilder<DocumentationPatchBuilder> {
14+
15+
private static final String BASE_PATH = "/documentations/";
16+
private static final String DOCUMENTATION_KEY = "documentation";
17+
private static final String ATTRIBUTION_KEY = "attribution";
18+
private static final String SOURCE_KEY = "source";
19+
20+
public DocumentationPatchBuilder addDocumentation(@Nonnull String documentation) {
21+
ObjectNode value = instance.objectNode();
22+
value.put(DOCUMENTATION_KEY, documentation);
23+
pathValues.add(ImmutableTriple.of(PatchOperationType.ADD.getValue(), BASE_PATH, value));
24+
return this;
25+
}
26+
27+
/** Removes all documentation entries. */
28+
public DocumentationPatchBuilder removeDocumentation() {
29+
pathValues.add(
30+
ImmutableTriple.of(PatchOperationType.REMOVE.getValue(), "/documentations", null));
31+
return this;
32+
}
33+
34+
/** Removes the entry for a specific source. */
35+
public DocumentationPatchBuilder removeDocumentation(@Nonnull Urn attributionSource) {
36+
String path = BASE_PATH + encodeValue(attributionSource.toString());
37+
pathValues.add(ImmutableTriple.of(PatchOperationType.REMOVE.getValue(), path, null));
38+
return this;
39+
}
40+
41+
@Override
42+
protected String getAspectName() {
43+
return DOCUMENTATION_ASPECT_NAME;
44+
}
45+
46+
@Override
47+
protected String getEntityType() {
48+
if (this.targetEntityUrn == null) {
49+
throw new IllegalStateException(
50+
"Target Entity Urn must be set to determine entity type before building Patch.");
51+
}
52+
return this.targetEntityUrn.getEntityType();
53+
}
54+
}

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/builder/GlobalTagsPatchBuilder.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.fasterxml.jackson.databind.node.ObjectNode;
77
import com.linkedin.common.urn.TagUrn;
8+
import com.linkedin.common.urn.Urn;
89
import com.linkedin.metadata.aspect.patch.PatchOperationType;
910
import javax.annotation.Nonnull;
1011
import javax.annotation.Nullable;
@@ -33,7 +34,7 @@ public GlobalTagsPatchBuilder addTag(@Nonnull TagUrn urn, @Nullable String conte
3334

3435
pathValues.add(
3536
ImmutableTriple.of(
36-
PatchOperationType.ADD.getValue(), BASE_PATH + encodeValueUrn(urn), value));
37+
PatchOperationType.ADD.getValue(), BASE_PATH + encodeValueUrn(urn) + "/", value));
3738
return this;
3839
}
3940

@@ -44,6 +45,16 @@ public GlobalTagsPatchBuilder removeTag(@Nonnull TagUrn urn) {
4445
return this;
4546
}
4647

48+
/** Removes only the entry for this tag URN attributed to a specific source. */
49+
public GlobalTagsPatchBuilder removeTag(@Nonnull TagUrn urn, @Nonnull Urn attributionSource) {
50+
pathValues.add(
51+
ImmutableTriple.of(
52+
PatchOperationType.REMOVE.getValue(),
53+
BASE_PATH + encodeValueUrn(urn) + "/" + encodeValue(attributionSource.toString()),
54+
null));
55+
return this;
56+
}
57+
4758
@Override
4859
protected String getAspectName() {
4960
return GLOBAL_TAGS_ASPECT_NAME;

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/builder/GlossaryTermsPatchBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.fasterxml.jackson.databind.node.ObjectNode;
77
import com.linkedin.common.urn.GlossaryTermUrn;
8+
import com.linkedin.common.urn.Urn;
89
import com.linkedin.metadata.aspect.patch.PatchOperationType;
910
import javax.annotation.Nonnull;
1011
import javax.annotation.Nullable;
@@ -34,7 +35,7 @@ public GlossaryTermsPatchBuilder addTerm(@Nonnull GlossaryTermUrn urn, @Nullable
3435

3536
pathValues.add(
3637
ImmutableTriple.of(
37-
PatchOperationType.ADD.getValue(), BASE_PATH + encodeValueUrn(urn), value));
38+
PatchOperationType.ADD.getValue(), BASE_PATH + encodeValueUrn(urn) + "/", value));
3839
return this;
3940
}
4041

@@ -45,6 +46,17 @@ public GlossaryTermsPatchBuilder removeTerm(@Nonnull GlossaryTermUrn urn) {
4546
return this;
4647
}
4748

49+
/** Removes only the entry for this term URN attributed to a specific source. */
50+
public GlossaryTermsPatchBuilder removeTerm(
51+
@Nonnull GlossaryTermUrn urn, @Nonnull Urn attributionSource) {
52+
pathValues.add(
53+
ImmutableTriple.of(
54+
PatchOperationType.REMOVE.getValue(),
55+
BASE_PATH + encodeValueUrn(urn) + "/" + encodeValue(attributionSource.toString()),
56+
null));
57+
return this;
58+
}
59+
4860
@Override
4961
protected String getAspectName() {
5062
return GLOSSARY_TERMS_ASPECT_NAME;

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/builder/OwnershipPatchBuilder.java

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,50 +8,51 @@
88
import com.linkedin.common.urn.Urn;
99
import com.linkedin.metadata.aspect.patch.PatchOperationType;
1010
import javax.annotation.Nonnull;
11+
import javax.annotation.Nullable;
1112
import org.apache.commons.lang3.tuple.ImmutableTriple;
1213

1314
public class OwnershipPatchBuilder extends AbstractMultiFieldPatchBuilder<OwnershipPatchBuilder> {
1415

1516
private static final String BASE_PATH = "/owners/";
1617
private static final String OWNER_KEY = "owner";
1718
private static final String TYPE_KEY = "type";
19+
private static final String TYPE_URN_KEY = "typeUrn";
1820

1921
public OwnershipPatchBuilder addOwner(@Nonnull Urn owner, @Nonnull OwnershipType type) {
22+
return addOwner(owner, type, null);
23+
}
24+
25+
public OwnershipPatchBuilder addOwner(
26+
@Nonnull Urn owner, @Nonnull OwnershipType type, @Nullable Urn typeUrn) {
2027
ObjectNode value = instance.objectNode();
2128
value.put(OWNER_KEY, owner.toString());
2229
value.put(TYPE_KEY, type.toString());
23-
30+
if (typeUrn != null) {
31+
value.put(TYPE_URN_KEY, typeUrn.toString());
32+
}
2433
pathValues.add(
2534
ImmutableTriple.of(
2635
PatchOperationType.ADD.getValue(),
27-
BASE_PATH + encodeValueUrn(owner) + "/" + encodeValue(type.toString()),
36+
BASE_PATH
37+
+ encodeValueUrn(owner)
38+
+ "/"
39+
+ encodeValue(type.toString())
40+
+ "/"
41+
+ encodeValue(typeUrn != null ? typeUrn.toString() : "")
42+
+ "/",
2843
value));
29-
3044
return this;
3145
}
3246

33-
/**
34-
* Remove all ownership types for an owner
35-
*
36-
* @param owner
37-
* @return
38-
*/
47+
/** Removes all ownership entries for this owner. */
3948
public OwnershipPatchBuilder removeOwner(@Nonnull Urn owner) {
4049
pathValues.add(
4150
ImmutableTriple.of(
4251
PatchOperationType.REMOVE.getValue(), BASE_PATH + encodeValueUrn(owner), null));
43-
4452
return this;
4553
}
4654

47-
/**
48-
* Removes a specific ownership type for a particular owner, a single owner may have multiple
49-
* ownership types
50-
*
51-
* @param owner
52-
* @param type
53-
* @return
54-
*/
55+
/** Removes all entries for a given owner with a specific ownership-type enum. */
5556
public OwnershipPatchBuilder removeOwnershipType(
5657
@Nonnull Urn owner, @Nonnull OwnershipType type) {
5758
pathValues.add(
@@ -62,6 +63,43 @@ public OwnershipPatchBuilder removeOwnershipType(
6263
return this;
6364
}
6465

66+
/** Removes all entries for a given owner with a specific ownership type and typeUrn. */
67+
public OwnershipPatchBuilder removeOwner(
68+
@Nonnull Urn owner, @Nonnull OwnershipType type, @Nonnull Urn typeUrn) {
69+
pathValues.add(
70+
ImmutableTriple.of(
71+
PatchOperationType.REMOVE.getValue(),
72+
BASE_PATH
73+
+ encodeValueUrn(owner)
74+
+ "/"
75+
+ encodeValue(type.toString())
76+
+ "/"
77+
+ encodeValue(typeUrn.toString()),
78+
null));
79+
return this;
80+
}
81+
82+
public OwnershipPatchBuilder removeOwner(
83+
@Nonnull Urn owner,
84+
@Nonnull OwnershipType type,
85+
@Nonnull Urn typeUrn,
86+
@Nonnull Urn attributionSource) {
87+
pathValues.add(
88+
ImmutableTriple.of(
89+
PatchOperationType.REMOVE.getValue(),
90+
BASE_PATH
91+
+ encodeValueUrn(owner)
92+
+ "/"
93+
+ encodeValue(type.toString())
94+
+ "/"
95+
+ encodeValue(typeUrn.toString())
96+
+ "/"
97+
+ encodeValue(attributionSource.toString()),
98+
null));
99+
100+
return this;
101+
}
102+
65103
@Override
66104
protected String getAspectName() {
67105
return OWNERSHIP_ASPECT_NAME;

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/builder/StructuredPropertiesPatchBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public StructuredPropertiesPatchBuilder setNumberProperty(
4747
pathValues.add(
4848
ImmutableTriple.of(
4949
PatchOperationType.ADD.getValue(),
50-
BASE_PATH + "/" + encodeValueUrn(propertyUrn),
50+
BASE_PATH + "/" + encodeValueUrn(propertyUrn) + "/",
5151
newProperty));
5252
return this;
5353
}
@@ -69,7 +69,7 @@ public StructuredPropertiesPatchBuilder setNumberProperty(
6969
pathValues.add(
7070
ImmutableTriple.of(
7171
PatchOperationType.ADD.getValue(),
72-
BASE_PATH + "/" + encodeValueUrn(propertyUrn),
72+
BASE_PATH + "/" + encodeValueUrn(propertyUrn) + "/",
7373
newProperty));
7474
return this;
7575
}
@@ -88,7 +88,7 @@ public StructuredPropertiesPatchBuilder setStringProperty(
8888
pathValues.add(
8989
ImmutableTriple.of(
9090
PatchOperationType.ADD.getValue(),
91-
BASE_PATH + "/" + encodeValueUrn(propertyUrn),
91+
BASE_PATH + "/" + encodeValueUrn(propertyUrn) + "/",
9292
newProperty));
9393
return this;
9494
}
@@ -110,7 +110,7 @@ public StructuredPropertiesPatchBuilder setStringProperty(
110110
pathValues.add(
111111
ImmutableTriple.of(
112112
PatchOperationType.ADD.getValue(),
113-
BASE_PATH + "/" + encodeValueUrn(propertyUrn),
113+
BASE_PATH + "/" + encodeValueUrn(propertyUrn) + "/",
114114
newProperty));
115115
return this;
116116
}

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/ArrayMergingTemplate.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ default JsonNode transformedMapToArray(
103103
return transformedNode;
104104
}
105105
ObjectNode rebasedNode = transformedNode.deepCopy();
106+
// fieldNode is null when the entire array field was removed by the patch.
107+
// Return an empty array rather than NPE.
108+
if (fieldNode == null || fieldNode.isNull()) {
109+
return rebasedNode.set(arrayFieldName, instance.arrayNode());
110+
}
106111
ObjectNode mapNode = (ObjectNode) fieldNode;
107112
ArrayNode arrayNode;
108113

@@ -118,6 +123,11 @@ default JsonNode transformedMapToArray(
118123

119124
default ArrayNode mergeToArray(JsonNode mapNode, List<String> keyFields) {
120125
if (keyFields.isEmpty()) {
126+
// When a plain add sets an array at an entity-key level (e.g. /tags/<urn> with
127+
// value [{...}]), the leaf ArrayNode's elements must be expanded into the result.
128+
if (mapNode instanceof ArrayNode) {
129+
return instance.arrayNode().addAll((ArrayNode) mapNode);
130+
}
121131
return instance.arrayNode().add(mapNode);
122132
} else {
123133
ArrayNode mergingArray = instance.arrayNode();

entity-registry/src/main/java/com/linkedin/metadata/aspect/patch/template/AspectTemplateEngine.java

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
import jakarta.json.JsonPatch;
99
import java.util.HashMap;
1010
import java.util.Map;
11-
import java.util.Set;
12-
import java.util.stream.Collectors;
13-
import java.util.stream.Stream;
1411
import javax.annotation.Nonnull;
1512
import javax.annotation.Nullable;
1613

@@ -20,32 +17,6 @@
2017
*/
2118
public class AspectTemplateEngine {
2219

23-
public static final Set<String> SUPPORTED_TEMPLATES =
24-
Stream.of(
25-
DATASET_PROPERTIES_ASPECT_NAME,
26-
EDITABLE_SCHEMA_METADATA_ASPECT_NAME,
27-
GLOBAL_TAGS_ASPECT_NAME,
28-
GLOSSARY_TERMS_ASPECT_NAME,
29-
OWNERSHIP_ASPECT_NAME,
30-
UPSTREAM_LINEAGE_ASPECT_NAME,
31-
DATA_FLOW_INFO_ASPECT_NAME,
32-
DATA_JOB_INFO_ASPECT_NAME,
33-
DATA_PRODUCT_PROPERTIES_ASPECT_NAME,
34-
DATA_JOB_INPUT_OUTPUT_ASPECT_NAME,
35-
CHART_INFO_ASPECT_NAME,
36-
DASHBOARD_INFO_ASPECT_NAME,
37-
STRUCTURED_PROPERTIES_ASPECT_NAME,
38-
STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME,
39-
FORM_INFO_ASPECT_NAME,
40-
UPSTREAM_LINEAGE_ASPECT_NAME,
41-
VERSION_PROPERTIES_ASPECT_NAME,
42-
SIBLINGS_ASPECT_NAME,
43-
DOMAINS_ASPECT_NAME,
44-
EDITABLE_DATASET_PROPERTIES_ASPECT_NAME,
45-
CONTAINER_EDITABLE_PROPERTIES_ASPECT_NAME,
46-
ML_MODEL_GROUP_EDITABLE_PROPERTIES_ASPECT_NAME)
47-
.collect(Collectors.toSet());
48-
4920
private final Map<String, Template<? extends RecordTemplate>> _aspectTemplateMap;
5021

5122
public AspectTemplateEngine() {

0 commit comments

Comments
 (0)