Fixes #4977 Add APIs for adding custom fields to an existing entity (#4978)

This commit is contained in:
Suresh Srinivas 2022-05-16 15:26:20 -07:00 committed by GitHub
parent 06b062c1f4
commit 0ba22c1d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 501 additions and 177 deletions

View File

@ -1,18 +1,12 @@
-- CREATE TABLE IF NOT EXISTS type_entity (
-- Table to be used for generic entities that are limited in number. Examples of generic entities are
-- Attribute entity, domain entities etc.
--
-- This reduces need for defining a table per entity.
--
CREATE TABLE IF NOT EXISTS generic_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL, id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
-- Fully qualified name formed by entityType + "." + entityName name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL,
fullyQualifiedName VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.fullyQualifiedName') NOT NULL, category VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.category') NOT NULL,
json JSON NOT NULL, json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL, updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL, updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE (fullyQualifiedName) UNIQUE (name)
); );
ALTER TABLE webhook_entity ALTER TABLE webhook_entity

View File

@ -1,18 +1,12 @@
-- CREATE TABLE IF NOT EXISTS type_entity (
-- Table to be used for generic entities that are limited in number. Examples of generic entities are
-- Attribute entity, domain entities etc.
--
-- This reduces need for defining a table per entity.
--
CREATE TABLE IF NOT EXISTS generic_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL, id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
-- Fully qualified name formed by entityType + "." + entityName name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
fullyQualifiedName VARCHAR(256) GENERATED ALWAYS AS (json ->> 'fullyQualifiedName') STORED NOT NULL, category VARCHAR(256) GENERATED ALWAYS AS (json ->> 'category') STORED NOT NULL,
json JSONB NOT NULL, json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL, updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL, updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE (fullyQualifiedName) UNIQUE (name)
); );
ALTER TABLE webhook_entity ALTER TABLE webhook_entity

View File

@ -486,6 +486,9 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire.version}</version> <version>${maven-surefire.version}</version>
<configuration>
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions> <executions>
<execution> <execution>
<id>default-test</id> <id>default-test</id>

View File

@ -40,7 +40,6 @@ import org.slf4j.LoggerFactory;
public class CatalogGenericExceptionMapper implements ExceptionMapper<Throwable> { public class CatalogGenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override @Override
public Response toResponse(Throwable ex) { public Response toResponse(Throwable ex) {
ex.printStackTrace();
LOG.debug(ex.getMessage()); LOG.debug(ex.getMessage());
if (ex instanceof ProcessingException || ex instanceof IllegalArgumentException) { if (ex instanceof ProcessingException || ex instanceof IllegalArgumentException) {
final Response response = BadRequestException.of().getResponse(); final Response response = BadRequestException.of().getResponse();

View File

@ -176,7 +176,7 @@ public interface CollectionDAO {
WebhookDAO webhookDAO(); WebhookDAO webhookDAO();
@CreateSqlObject @CreateSqlObject
GenericEntityDAO genericEntityDAO(); TypeEntityDAO typeEntityDAO();
interface DashboardDAO extends EntityDAO<Dashboard> { interface DashboardDAO extends EntityDAO<Dashboard> {
@Override @Override
@ -339,16 +339,23 @@ public interface CollectionDAO {
interface EntityRelationshipDAO { interface EntityRelationshipDAO {
default int insert(UUID fromId, UUID toId, String fromEntity, String toEntity, int relation) { default int insert(UUID fromId, UUID toId, String fromEntity, String toEntity, int relation) {
return insert(fromId.toString(), toId.toString(), fromEntity, toEntity, relation); return insert(fromId, toId, fromEntity, toEntity, relation, null);
}
default int insert(UUID fromId, UUID toId, String fromEntity, String toEntity, int relation, String json) {
return insert(fromId.toString(), toId.toString(), fromEntity, toEntity, relation, json);
} }
@ConnectionAwareSqlUpdate( @ConnectionAwareSqlUpdate(
value = value =
"INSERT IGNORE INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation) VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation)", "INSERT IGNORE INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation) "
+ "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation)",
connectionType = MYSQL) connectionType = MYSQL)
@ConnectionAwareSqlUpdate( @ConnectionAwareSqlUpdate(
value = value =
"INSERT INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation) VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation) ON CONFLICT (fromId, toId, relation) DO NOTHING", "INSERT INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation) "
+ "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation) "
+ "ON CONFLICT (fromId, toId, relation) DO NOTHING",
connectionType = POSTGRES) connectionType = POSTGRES)
int insert( int insert(
@Bind("fromId") String fromId, @Bind("fromId") String fromId,
@ -357,6 +364,25 @@ public interface CollectionDAO {
@Bind("toEntity") String toEntity, @Bind("toEntity") String toEntity,
@Bind("relation") int relation); @Bind("relation") int relation);
@ConnectionAwareSqlUpdate(
value =
"INSERT IGNORE INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation, json) "
+ "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation, :json)",
connectionType = MYSQL)
@ConnectionAwareSqlUpdate(
value =
"INSERT INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation, json) VALUES "
+ "(:fromId, :toId, :fromEntity, :toEntity, :relation, (:json :: jsonb)) "
+ "ON CONFLICT (fromId, toId, relation) DO NOTHING",
connectionType = POSTGRES)
int insert(
@Bind("fromId") String fromId,
@Bind("toId") String toId,
@Bind("fromEntity") String fromEntity,
@Bind("toEntity") String toEntity,
@Bind("relation") int relation,
@Bind("json") String json);
// //
// Find to operations // Find to operations
// //
@ -368,17 +394,6 @@ public interface CollectionDAO {
List<EntityReference> findTo( List<EntityReference> findTo(
@Bind("fromId") String fromId, @Bind("fromEntity") String fromEntity, @Bind("relation") int relation); @Bind("fromId") String fromId, @Bind("fromEntity") String fromEntity, @Bind("relation") int relation);
@SqlQuery(
"SELECT toId, toEntity FROM entity_relationship "
+ "WHERE fromId = :fromId AND fromEntity = :fromEntity AND relation = :relation AND toEntity = :toEntity "
+ "ORDER BY toId")
@RegisterRowMapper(ToEntityReferenceMapper.class)
List<EntityReference> findToReference(
@Bind("fromId") String fromId,
@Bind("fromEntity") String fromEntity,
@Bind("relation") int relation,
@Bind("toEntity") String toEntity);
@SqlQuery( @SqlQuery(
"SELECT toId FROM entity_relationship " "SELECT toId FROM entity_relationship "
+ "WHERE fromId = :fromId AND fromEntity = :fromEntity AND relation = :relation AND toEntity = :toEntity " + "WHERE fromId = :fromId AND fromEntity = :fromEntity AND relation = :relation AND toEntity = :toEntity "
@ -421,17 +436,6 @@ public interface CollectionDAO {
List<EntityReference> findFrom( List<EntityReference> findFrom(
@Bind("toId") String toId, @Bind("toEntity") String toEntity, @Bind("relation") int relation); @Bind("toId") String toId, @Bind("toEntity") String toEntity, @Bind("relation") int relation);
@SqlQuery(
"SELECT fromId, fromEntity FROM entity_relationship "
+ "WHERE toId = :toId AND toEntity = :toEntity AND relation = :relation AND fromEntity = :fromEntity "
+ "ORDER BY fromId")
@RegisterRowMapper(FromEntityReferenceMapper.class)
List<EntityReference> findFromEntity(
@Bind("toId") String toId,
@Bind("toEntity") String toEntity,
@Bind("relation") int relation,
@Bind("fromEntity") String fromEntity);
// //
// Delete Operations // Delete Operations
// //
@ -709,18 +713,22 @@ public interface CollectionDAO {
interface FieldRelationshipDAO { interface FieldRelationshipDAO {
@ConnectionAwareSqlUpdate( @ConnectionAwareSqlUpdate(
value = value =
"INSERT IGNORE INTO field_relationship(fromFQN, toFQN, fromType, toType, relation) VALUES (:fromFQN, :toFQN, :fromType, :toType, :relation)", "INSERT IGNORE INTO field_relationship(fromFQN, toFQN, fromType, toType, relation, json) "
+ "VALUES (:fromFQN, :toFQN, :fromType, :toType, :relation, :json)",
connectionType = MYSQL) connectionType = MYSQL)
@ConnectionAwareSqlUpdate( @ConnectionAwareSqlUpdate(
value = value =
"INSERT INTO field_relationship(fromFQN, toFQN, fromType, toType, relation) VALUES (:fromFQN, :toFQN, :fromType, :toType, :relation) ON CONFLICT (fromFQN, toFQN, relation) DO NOTHING", "INSERT INTO field_relationship(fromFQN, toFQN, fromType, toType, relation, json) "
+ "VALUES (:fromFQN, :toFQN, :fromType, :toType, :relation, (:json :: jsonb)) "
+ "ON CONFLICT (fromFQN, toFQN, relation) DO NOTHING",
connectionType = POSTGRES) connectionType = POSTGRES)
void insert( int insert(
@Bind("fromFQN") String fromFQN, @Bind("fromFQN") String fromFQN,
@Bind("toFQN") String toFQN, @Bind("toFQN") String toFQN,
@Bind("fromType") String fromType, @Bind("fromType") String fromType,
@Bind("toType") String toType, @Bind("toType") String toType,
@Bind("relation") int relation); @Bind("relation") int relation,
@Bind("json") String json);
@ConnectionAwareSqlUpdate( @ConnectionAwareSqlUpdate(
value = value =
@ -794,6 +802,16 @@ public interface CollectionDAO {
@SqlUpdate("DELETE from field_relationship <cond>") @SqlUpdate("DELETE from field_relationship <cond>")
void deleteAllByPrefixInternal(@Define("cond") String cond); void deleteAllByPrefixInternal(@Define("cond") String cond);
@SqlUpdate(
"DELETE from field_relationship WHERE fromFQN = :fromFQN AND toFQN = :toFQN AND fromType = :fromType "
+ "AND toType = :toType AND relation = :relation")
void delete(
@Bind("fromFQN") String fromFQN,
@Bind("toFQN") String toFQN,
@Bind("fromType") String fromType,
@Bind("toType") String toType,
@Bind("relation") int relation);
class ToFieldMapper implements RowMapper<List<String>> { class ToFieldMapper implements RowMapper<List<String>> {
@Override @Override
public List<String> map(ResultSet rs, StatementContext ctx) throws SQLException { public List<String> map(ResultSet rs, StatementContext ctx) throws SQLException {
@ -1560,10 +1578,10 @@ public interface CollectionDAO {
List<String> listWithoutEntityFilter(@Bind("eventType") String eventType, @Bind("timestamp") long timestamp); List<String> listWithoutEntityFilter(@Bind("eventType") String eventType, @Bind("timestamp") long timestamp);
} }
interface GenericEntityDAO extends EntityDAO<Type> { interface TypeEntityDAO extends EntityDAO<Type> {
@Override @Override
default String getTableName() { default String getTableName() {
return "generic_entity"; return "type_entity";
} }
@Override @Override
@ -1573,7 +1591,7 @@ public interface CollectionDAO {
@Override @Override
default String getNameColumn() { default String getNameColumn() {
return "fullyQualifiedName"; return "name";
} }
@Override @Override

View File

@ -115,7 +115,7 @@ import org.openmetadata.catalog.util.ResultList;
public abstract class EntityRepository<T extends EntityInterface> { public abstract class EntityRepository<T extends EntityInterface> {
private final String collectionPath; private final String collectionPath;
private final Class<T> entityClass; private final Class<T> entityClass;
private final String entityType; protected final String entityType;
public final EntityDAO<T> dao; public final EntityDAO<T> dao;
protected final CollectionDAO daoCollection; protected final CollectionDAO daoCollection;
protected final List<String> allowedFields; protected final List<String> allowedFields;
@ -129,7 +129,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
private final Fields patchFields; private final Fields patchFields;
/** Fields that can be updated during PUT operation */ /** Fields that can be updated during PUT operation */
private final Fields putFields; protected final Fields putFields;
EntityRepository( EntityRepository(
String collectionPath, String collectionPath,
@ -737,8 +737,24 @@ public abstract class EntityRepository<T extends EntityInterface> {
return addRelationship(fromId, toId, fromEntity, toEntity, relationship, false); return addRelationship(fromId, toId, fromEntity, toEntity, relationship, false);
} }
public int addRelationship(
UUID fromId, UUID toId, String fromEntity, String toEntity, Relationship relationship, String json) {
return addRelationship(fromId, toId, fromEntity, toEntity, relationship, json, false);
}
public int addRelationship( public int addRelationship(
UUID fromId, UUID toId, String fromEntity, String toEntity, Relationship relationship, boolean bidirectional) { UUID fromId, UUID toId, String fromEntity, String toEntity, Relationship relationship, boolean bidirectional) {
return addRelationship(fromId, toId, fromEntity, toEntity, relationship, null, bidirectional);
}
public int addRelationship(
UUID fromId,
UUID toId,
String fromEntity,
String toEntity,
Relationship relationship,
String json,
boolean bidirectional) {
UUID from = fromId; UUID from = fromId;
UUID to = toId; UUID to = toId;
if (bidirectional && fromId.compareTo(toId) > 0) { if (bidirectional && fromId.compareTo(toId) > 0) {
@ -747,7 +763,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
from = toId; from = toId;
to = fromId; to = fromId;
} }
return daoCollection.relationshipDAO().insert(from, to, fromEntity, toEntity, relationship.ordinal()); return daoCollection.relationshipDAO().insert(fromId, toId, fromEntity, toEntity, relationship.ordinal(), json);
} }
public List<String> findBoth(UUID entity1, String entityType1, Relationship relationship, String entity2) { public List<String> findBoth(UUID entity1, String entityType1, Relationship relationship, String entity2) {

View File

@ -13,6 +13,11 @@
package org.openmetadata.catalog.jdbi3; package org.openmetadata.catalog.jdbi3;
import static org.openmetadata.catalog.type.Relationship.ADDRESSED_TO;
import static org.openmetadata.catalog.type.Relationship.CREATED;
import static org.openmetadata.catalog.type.Relationship.IS_ABOUT;
import static org.openmetadata.catalog.type.Relationship.REPLIED_TO;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -83,13 +88,7 @@ public class FeedRepository {
dao.feedDAO().insert(JsonUtils.pojoToJson(thread)); dao.feedDAO().insert(JsonUtils.pojoToJson(thread));
// Add relationship User -- created --> Thread relationship // Add relationship User -- created --> Thread relationship
dao.relationshipDAO() dao.relationshipDAO().insert(createdByUser.getId(), thread.getId(), Entity.USER, Entity.THREAD, CREATED.ordinal());
.insert(
createdByUser.getId().toString(),
thread.getId().toString(),
Entity.USER,
Entity.THREAD,
Relationship.CREATED.ordinal());
// Add field relationship data asset Thread -- isAbout ---> entity/entityField // Add field relationship data asset Thread -- isAbout ---> entity/entityField
// relationship // relationship
@ -99,17 +98,13 @@ public class FeedRepository {
about.getFullyQualifiedFieldValue(), // to FQN about.getFullyQualifiedFieldValue(), // to FQN
Entity.THREAD, // From type Entity.THREAD, // From type
about.getFullyQualifiedFieldType(), // to Type about.getFullyQualifiedFieldType(), // to Type
Relationship.IS_ABOUT.ordinal()); IS_ABOUT.ordinal(),
null);
// Add the owner also as addressedTo as the entity he owns when addressed, the owner is actually being addressed // Add the owner also as addressedTo as the entity he owns when addressed, the owner is actually being addressed
if (entityOwner != null) { if (entityOwner != null) {
dao.relationshipDAO() dao.relationshipDAO()
.insert( .insert(thread.getId(), entityOwner.getId(), Entity.THREAD, entityOwner.getType(), ADDRESSED_TO.ordinal());
thread.getId().toString(),
entityOwner.getId().toString(),
Entity.THREAD,
entityOwner.getType(),
Relationship.ADDRESSED_TO.ordinal());
} }
// Add mentions to field relationship table // Add mentions to field relationship table
@ -151,7 +146,8 @@ public class FeedRepository {
thread.getId().toString(), thread.getId().toString(),
mention.getFullyQualifiedFieldType(), mention.getFullyQualifiedFieldType(),
Entity.THREAD, Entity.THREAD,
Relationship.MENTIONED_IN.ordinal())); Relationship.MENTIONED_IN.ordinal(),
null));
} }
@Transaction @Transaction
@ -178,13 +174,7 @@ public class FeedRepository {
} }
} }
if (!relationAlreadyExists) { if (!relationAlreadyExists) {
dao.relationshipDAO() dao.relationshipDAO().insert(fromUser.getId(), thread.getId(), Entity.USER, Entity.THREAD, REPLIED_TO.ordinal());
.insert(
fromUser.getId().toString(),
thread.getId().toString(),
Entity.USER,
Entity.THREAD,
Relationship.REPLIED_TO.ordinal());
} }
// Add mentions into field relationship table // Add mentions into field relationship table
@ -233,7 +223,7 @@ public class FeedRepository {
result = result =
dao.feedDAO() dao.feedDAO()
.listCountByEntityLink( .listCountByEntityLink(
StringUtils.EMPTY, Entity.THREAD, StringUtils.EMPTY, Relationship.IS_ABOUT.ordinal(), isResolved); StringUtils.EMPTY, Entity.THREAD, StringUtils.EMPTY, IS_ABOUT.ordinal(), isResolved);
} else { } else {
EntityLink entityLink = EntityLink.parse(link); EntityLink entityLink = EntityLink.parse(link);
EntityReference reference = EntityUtil.validateEntityLink(entityLink); EntityReference reference = EntityUtil.validateEntityLink(entityLink);
@ -253,7 +243,7 @@ public class FeedRepository {
entityLink.getFullyQualifiedFieldValue(), entityLink.getFullyQualifiedFieldValue(),
Entity.THREAD, Entity.THREAD,
entityLink.getFullyQualifiedFieldType(), entityLink.getFullyQualifiedFieldType(),
Relationship.IS_ABOUT.ordinal(), IS_ABOUT.ordinal(),
isResolved); isResolved);
} }
} }
@ -333,7 +323,7 @@ public class FeedRepository {
limit + 1, limit + 1,
time, time,
isResolved, isResolved,
Relationship.IS_ABOUT.ordinal()); IS_ABOUT.ordinal());
} else { } else {
jsons = jsons =
dao.feedDAO() dao.feedDAO()
@ -343,7 +333,7 @@ public class FeedRepository {
limit + 1, limit + 1,
time, time,
isResolved, isResolved,
Relationship.IS_ABOUT.ordinal()); IS_ABOUT.ordinal());
} }
threads = JsonUtils.readObjects(jsons, Thread.class); threads = JsonUtils.readObjects(jsons, Thread.class);
total = total =
@ -352,7 +342,7 @@ public class FeedRepository {
entityLink.getFullyQualifiedFieldValue(), entityLink.getFullyQualifiedFieldValue(),
entityLink.getFullyQualifiedFieldType(), entityLink.getFullyQualifiedFieldType(),
isResolved, isResolved,
Relationship.IS_ABOUT.ordinal()); IS_ABOUT.ordinal());
} }
} else { } else {
FilteredThreads filteredThreads; FilteredThreads filteredThreads;
@ -474,7 +464,7 @@ public class FeedRepository {
String userId, int limit, long time, boolean isResolved, PaginationType paginationType) throws IOException { String userId, int limit, long time, boolean isResolved, PaginationType paginationType) throws IOException {
List<EntityReference> teams = List<EntityReference> teams =
EntityUtil.populateEntityReferences( EntityUtil.populateEntityReferences(
dao.relationshipDAO().findFromEntity(userId, Entity.USER, Relationship.HAS.ordinal(), Entity.TEAM)); dao.relationshipDAO().findFrom(userId, Entity.USER, Relationship.HAS.ordinal(), Entity.TEAM), Entity.TEAM);
List<String> teamNames = teams.stream().map(EntityReference::getName).collect(Collectors.toList()); List<String> teamNames = teams.stream().map(EntityReference::getName).collect(Collectors.toList());
if (teamNames.isEmpty()) { if (teamNames.isEmpty()) {
teamNames = List.of(StringUtils.EMPTY); teamNames = List.of(StringUtils.EMPTY);

View File

@ -25,9 +25,6 @@ import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction; import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.catalog.Entity; import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.teams.Role; import org.openmetadata.catalog.entity.teams.Role;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.resources.teams.RoleResource; import org.openmetadata.catalog.resources.teams.RoleResource;
import org.openmetadata.catalog.type.EntityReference; import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.type.Relationship; import org.openmetadata.catalog.type.Relationship;
@ -178,18 +175,5 @@ public class RoleRepository extends EntityRepository<Role> {
.relationshipDAO() .relationshipDAO()
.deleteTo(role.getId().toString(), Entity.ROLE, Relationship.HAS.ordinal(), Entity.USER); .deleteTo(role.getId().toString(), Entity.ROLE, Relationship.HAS.ordinal(), Entity.USER);
} }
private List<User> getAllUsers() {
EntityRepository<User> userRepository = Entity.getEntityRepository(Entity.USER);
try {
// Assumptions:
// - we will not have more than Integer.MAX_VALUE users in the system.
// - we do not need to update deleted user's roles.
ListFilter filter = new ListFilter();
return userRepository.listAfter(null, Fields.EMPTY_FIELDS, filter, Integer.MAX_VALUE - 1, null).getData();
} catch (IOException e) {
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entitiesNotFound(Entity.USER));
}
}
} }
} }

View File

@ -16,34 +16,43 @@
package org.openmetadata.catalog.jdbi3; package org.openmetadata.catalog.jdbi3;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.Entity; import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.Type; import org.openmetadata.catalog.entity.Type;
import org.openmetadata.catalog.entity.type.Category;
import org.openmetadata.catalog.entity.type.CustomField;
import org.openmetadata.catalog.resources.types.TypeResource; import org.openmetadata.catalog.resources.types.TypeResource;
import org.openmetadata.catalog.type.Include;
import org.openmetadata.catalog.type.Relationship;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.EntityUtil.Fields; import org.openmetadata.catalog.util.EntityUtil.Fields;
import org.openmetadata.catalog.util.FullyQualifiedName; import org.openmetadata.catalog.util.FullyQualifiedName;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.RestUtil.PutResponse;
@Slf4j
public class TypeRepository extends EntityRepository<Type> { public class TypeRepository extends EntityRepository<Type> {
// TODO fix this private static final String UPDATE_FIELDS = "customFields";
private static final String UPDATE_FIELDS = ""; private static final String PATCH_FIELDS = "customFields";
private static final String PATCH_FIELDS = "";
public TypeRepository(CollectionDAO dao) { public TypeRepository(CollectionDAO dao) {
super( super(TypeResource.COLLECTION_PATH, Entity.TYPE, Type.class, dao.typeEntityDAO(), dao, PATCH_FIELDS, UPDATE_FIELDS);
TypeResource.COLLECTION_PATH,
Entity.TYPE,
Type.class,
dao.genericEntityDAO(),
dao,
PATCH_FIELDS,
UPDATE_FIELDS);
allowEdits = true; allowEdits = true;
} }
@Override @Override
public Type setFields(Type attribute, Fields fields) throws IOException { public Type setFields(Type type, Fields fields) throws IOException {
return attribute; type.withCustomFields(fields.contains("customFields") ? getCustomFields(type) : null);
return type;
} }
@Override @Override
@ -54,9 +63,10 @@ public class TypeRepository extends EntityRepository<Type> {
@Override @Override
public void storeEntity(Type type, boolean update) throws IOException { public void storeEntity(Type type, boolean update) throws IOException {
URI href = type.getHref(); URI href = type.getHref();
type.withHref(null); List<CustomField> customFields = type.getCustomFields();
type.withHref(null).withCustomFields(null);
store(type.getId(), type, update); store(type.getId(), type, update);
type.withHref(href); type.withHref(href).withCustomFields(customFields);
} }
@Override @Override
@ -65,19 +75,95 @@ public class TypeRepository extends EntityRepository<Type> {
} }
@Override @Override
public void setFullyQualifiedName(Type entity) { public EntityUpdater getUpdater(Type original, Type updated, Operation operation) {
entity.setFullyQualifiedName(FullyQualifiedName.build(Entity.TYPE, entity.getNameSpace(), entity.getName())); return new TypeUpdater(original, updated, operation);
} }
@Override public PutResponse<Type> addCustomField(UriInfo uriInfo, String updatedBy, String id, CustomField field)
public EntityUpdater getUpdater(Type original, Type updated, Operation operation) { throws IOException {
return new AttributeUpdater(original, updated, operation); Type type = dao.findEntityById(UUID.fromString(id), Include.NON_DELETED);
if (type.getCategory().equals(Category.Field)) {
throw new IllegalArgumentException("Field types can't be extended");
}
setFields(type, putFields);
dao.findEntityById(field.getFieldType().getId()); // Validate customField type exists
type.getCustomFields().add(field);
type.setUpdatedBy(updatedBy);
type.setUpdatedAt(System.currentTimeMillis());
return createOrUpdate(uriInfo, type);
}
private String getCustomFieldFQNPrefix(Type type) {
return FullyQualifiedName.build(type.getName(), "customFields");
}
private String getCustomFieldFQN(Type type, String fieldName) {
return FullyQualifiedName.build(type.getName(), "customFields", fieldName);
}
private List<CustomField> getCustomFields(Type type) throws IOException {
List<CustomField> customFields = new ArrayList<>();
List<List<String>> results =
daoCollection
.fieldRelationshipDAO()
.listToByPrefix(getCustomFieldFQNPrefix(type), Entity.TYPE, Entity.TYPE, Relationship.HAS.ordinal());
for (List<String> result : results) {
CustomField field = JsonUtils.readValue(result.get(2), CustomField.class);
field.setFieldType(dao.findEntityReferenceByName(result.get(1)));
customFields.add(field);
}
customFields.sort(EntityUtil.compareCustomField);
return customFields;
} }
/** Handles entity updated from PUT and POST operation. */ /** Handles entity updated from PUT and POST operation. */
public class AttributeUpdater extends EntityUpdater { public class TypeUpdater extends EntityUpdater {
public AttributeUpdater(Type original, Type updated, Operation operation) { public TypeUpdater(Type original, Type updated, Operation operation) {
super(original, updated, operation); super(original, updated, operation);
} }
@Override
public void entitySpecificUpdate() throws IOException {
updateCustomFields();
}
private void updateCustomFields() throws JsonProcessingException {
List<CustomField> updatedFields = listOrEmpty(updated.getCustomFields());
List<CustomField> origFields = listOrEmpty(original.getCustomFields());
List<CustomField> added = new ArrayList<>();
List<CustomField> deleted = new ArrayList<>();
recordListChange("charts", origFields, updatedFields, added, deleted, EntityUtil.customFieldMatch);
for (CustomField field : added) {
String customFieldFQN = getCustomFieldFQN(updated, field.getName());
String customFieldJson = JsonUtils.pojoToJson(field);
LOG.info(
"Adding customField {} with type {} to the entity {}",
field.getName(),
field.getFieldType().getName(),
updated.getName());
daoCollection
.fieldRelationshipDAO()
.insert(
customFieldFQN,
field.getFieldType().getName(),
Entity.TYPE,
Entity.TYPE,
Relationship.HAS.ordinal(),
customFieldJson);
}
for (CustomField field : deleted) {
String customFieldFQN = getCustomFieldFQN(updated, field.getName());
LOG.info(
"Deleting customField {} with type {} from the entity {}",
field.getName(),
field.getFieldType().getName(),
updated.getName());
daoCollection
.fieldRelationshipDAO()
.delete(
customFieldFQN, field.getFieldType().getName(), Entity.TYPE, Entity.TYPE, Relationship.HAS.ordinal());
}
}
} }
} }

View File

@ -16,6 +16,7 @@ package org.openmetadata.catalog.resources.types;
import static org.openmetadata.catalog.security.SecurityUtil.ADMIN; import static org.openmetadata.catalog.security.SecurityUtil.ADMIN;
import static org.openmetadata.catalog.security.SecurityUtil.BOT; import static org.openmetadata.catalog.security.SecurityUtil.BOT;
import static org.openmetadata.catalog.security.SecurityUtil.OWNER; import static org.openmetadata.catalog.security.SecurityUtil.OWNER;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@ -52,18 +53,22 @@ import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.CatalogApplicationConfig; import org.openmetadata.catalog.CatalogApplicationConfig;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.CreateType; import org.openmetadata.catalog.api.CreateType;
import org.openmetadata.catalog.entity.Type; import org.openmetadata.catalog.entity.Type;
import org.openmetadata.catalog.entity.type.Category;
import org.openmetadata.catalog.entity.type.CustomField;
import org.openmetadata.catalog.jdbi3.CollectionDAO; import org.openmetadata.catalog.jdbi3.CollectionDAO;
import org.openmetadata.catalog.jdbi3.ListFilter; import org.openmetadata.catalog.jdbi3.ListFilter;
import org.openmetadata.catalog.jdbi3.TypeRepository; import org.openmetadata.catalog.jdbi3.TypeRepository;
import org.openmetadata.catalog.resources.Collection; import org.openmetadata.catalog.resources.Collection;
import org.openmetadata.catalog.resources.EntityResource; import org.openmetadata.catalog.resources.EntityResource;
import org.openmetadata.catalog.security.Authorizer; import org.openmetadata.catalog.security.Authorizer;
import org.openmetadata.catalog.security.SecurityUtil;
import org.openmetadata.catalog.type.EntityHistory; import org.openmetadata.catalog.type.EntityHistory;
import org.openmetadata.catalog.type.Include; import org.openmetadata.catalog.type.Include;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils; import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.RestUtil.PutResponse;
import org.openmetadata.catalog.util.ResultList; import org.openmetadata.catalog.util.ResultList;
@Path("/v1/metadata/types") @Path("/v1/metadata/types")
@ -77,7 +82,8 @@ public class TypeResource extends EntityResource<Type, TypeRepository> {
@Override @Override
public Type addHref(UriInfo uriInfo, Type type) { public Type addHref(UriInfo uriInfo, Type type) {
return type; // Nothing to do listOrEmpty(type.getCustomFields()).forEach(field -> Entity.withHref(uriInfo, field.getFieldType()));
return type;
} }
@Inject @Inject
@ -88,26 +94,18 @@ public class TypeResource extends EntityResource<Type, TypeRepository> {
@SuppressWarnings("unused") // Method used for reflection @SuppressWarnings("unused") // Method used for reflection
public void initialize(CatalogApplicationConfig config) throws IOException { public void initialize(CatalogApplicationConfig config) throws IOException {
// Find tag definitions and load tag categories from the json file, if necessary // Find tag definitions and load tag categories from the json file, if necessary
List<String> jsonSchemas = EntityUtil.getJsonDataResources(".*json/schema/type/.*\\.json$");
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
for (String jsonSchema : jsonSchemas) { List<Type> types = JsonUtils.getTypes();
try { types.forEach(
List<Type> types = JsonUtils.getTypes(jsonSchema); type -> {
types.forEach( type.withId(UUID.randomUUID()).withUpdatedBy("admin").withUpdatedAt(now);
type -> { LOG.info("Loading type {} with schema {}", type.getName(), type.getSchema());
type.withId(UUID.randomUUID()).withUpdatedBy("admin").withUpdatedAt(now); try {
LOG.info("Loading from {} type {} with schema {}", jsonSchema, type.getName(), type.getSchema()); this.dao.createOrUpdate(null, type);
try { } catch (IOException e) {
this.dao.createOrUpdate(null, type); LOG.error("Error loading type {}", type.getName(), e);
} catch (IOException e) { }
LOG.error("Error loading type {} from {}", type.getName(), jsonSchema, e); });
e.printStackTrace();
}
});
} catch (Exception e) {
LOG.warn("Failed to initialize the types from jsonSchema file {}", jsonSchema, e);
}
}
} }
public static class TypeList extends ResultList<Type> { public static class TypeList extends ResultList<Type> {
@ -121,7 +119,7 @@ public class TypeResource extends EntityResource<Type, TypeRepository> {
} }
} }
public static final String FIELDS = ""; public static final String FIELDS = "customFields";
@GET @GET
@Valid @Valid
@ -141,6 +139,11 @@ public class TypeResource extends EntityResource<Type, TypeRepository> {
public ResultList<Type> list( public ResultList<Type> list(
@Context UriInfo uriInfo, @Context UriInfo uriInfo,
@Context SecurityContext securityContext, @Context SecurityContext securityContext,
@Parameter(
description = "Filter types by metadata type category.",
schema = @Schema(type = "string", example = "Field, Entity"))
@QueryParam("category")
Category category,
@Parameter(description = "Limit the number types returned. (1 to 1000000, " + "default = 10)") @Parameter(description = "Limit the number types returned. (1 to 1000000, " + "default = 10)")
@DefaultValue("10") @DefaultValue("10")
@Min(0) @Min(0)
@ -349,11 +352,35 @@ public class TypeResource extends EntityResource<Type, TypeRepository> {
return delete(uriInfo, securityContext, id, false, true, ADMIN | BOT); return delete(uriInfo, securityContext, id, false, true, ADMIN | BOT);
} }
@PUT
@Path("/{id}")
@Operation(
summary = "Add a field to an entity",
tags = "metadata",
description = "Add a field to an entity type. Fields can only be added to entity type and not field type.",
responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "404", description = "type for instance {id} is not found")
})
public Response addField(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Type Id", schema = @Schema(type = "string")) @PathParam("id") String id,
CustomField field)
throws IOException {
SecurityUtil.authorizeAdmin(authorizer, securityContext, ADMIN | BOT);
PutResponse<Type> response = dao.addCustomField(uriInfo, securityContext.getUserPrincipal().getName(), id, field);
addHref(uriInfo, response.getEntity());
return response.toResponse();
}
private Type getType(SecurityContext securityContext, CreateType create) { private Type getType(SecurityContext securityContext, CreateType create) {
return new Type() return new Type()
.withId(UUID.randomUUID()) .withId(UUID.randomUUID())
.withName(create.getName()) .withName(create.getName())
.withFullyQualifiedName(create.getName())
.withDisplayName(create.getDisplayName()) .withDisplayName(create.getDisplayName())
.withCategory(create.getCategory())
.withSchema(create.getSchema()) .withSchema(create.getSchema())
.withDescription(create.getDescription()) .withDescription(create.getDescription())
.withUpdatedBy(securityContext.getUserPrincipal().getName()) .withUpdatedBy(securityContext.getUserPrincipal().getName())

View File

@ -40,6 +40,7 @@ import org.openmetadata.catalog.EntityInterface;
import org.openmetadata.catalog.api.data.TermReference; import org.openmetadata.catalog.api.data.TermReference;
import org.openmetadata.catalog.entity.data.GlossaryTerm; import org.openmetadata.catalog.entity.data.GlossaryTerm;
import org.openmetadata.catalog.entity.data.Table; import org.openmetadata.catalog.entity.data.Table;
import org.openmetadata.catalog.entity.type.CustomField;
import org.openmetadata.catalog.exception.CatalogExceptionMessage; import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.exception.EntityNotFoundException; import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.jdbi3.CollectionDAO.EntityVersionPair; import org.openmetadata.catalog.jdbi3.CollectionDAO.EntityVersionPair;
@ -80,6 +81,7 @@ public final class EntityUtil {
Comparator.comparing(TableConstraint::getConstraintType); Comparator.comparing(TableConstraint::getConstraintType);
public static final Comparator<ChangeEvent> compareChangeEvent = Comparator.comparing(ChangeEvent::getTimestamp); public static final Comparator<ChangeEvent> compareChangeEvent = Comparator.comparing(ChangeEvent::getTimestamp);
public static final Comparator<GlossaryTerm> compareGlossaryTerm = Comparator.comparing(GlossaryTerm::getName); public static final Comparator<GlossaryTerm> compareGlossaryTerm = Comparator.comparing(GlossaryTerm::getName);
public static final Comparator<CustomField> compareCustomField = Comparator.comparing(CustomField::getName);
// //
// Matchers used for matching two items in a list // Matchers used for matching two items in a list
@ -130,6 +132,9 @@ public final class EntityUtil {
public static final BiPredicate<TermReference, TermReference> termReferenceMatch = public static final BiPredicate<TermReference, TermReference> termReferenceMatch =
(ref1, ref2) -> ref1.getName().equals(ref2.getName()) && ref1.getEndpoint().equals(ref2.getEndpoint()); (ref1, ref2) -> ref1.getName().equals(ref2.getName()) && ref1.getEndpoint().equals(ref2.getEndpoint());
public static final BiPredicate<CustomField, CustomField> customFieldMatch =
(ref1, ref2) -> ref1.getName().equals(ref2.getName());
private EntityUtil() {} private EntityUtil() {}
/** Validate Ingestion Schedule */ /** Validate Ingestion Schedule */

View File

@ -45,10 +45,14 @@ import javax.json.JsonReader;
import javax.json.JsonStructure; import javax.json.JsonStructure;
import javax.json.JsonValue; import javax.json.JsonValue;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.entity.Type; import org.openmetadata.catalog.entity.Type;
import org.openmetadata.catalog.entity.type.Category;
@Slf4j
public final class JsonUtils { public final class JsonUtils {
public static final String TYPE_ANNOTATION = "@om-type"; public static final String FIELD_TYPE_ANNOTATION = "@om-field-type";
public static final String ENTITY_TYPE_ANNOTATION = "@om-entity-type";
public static final MediaType DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_TYPE; public static final MediaType DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_TYPE;
private static final ObjectMapper OBJECT_MAPPER; private static final ObjectMapper OBJECT_MAPPER;
private static final JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V7); private static final JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V7);
@ -217,11 +221,39 @@ public final class JsonUtils {
return comment != null && comment.contains(annotation); return comment != null && comment.contains(annotation);
} }
/** Get all the fields types and entity types from OpenMetadata JSON schema definition files. */
public static List<Type> getTypes() throws IOException {
// Get Field Types
List<Type> types = new ArrayList<>();
List<String> jsonSchemas = EntityUtil.getJsonDataResources(".*json/schema/type/.*\\.json$");
for (String jsonSchema : jsonSchemas) {
try {
types.addAll(JsonUtils.getFieldTypes(jsonSchema));
} catch (Exception e) {
LOG.warn("Failed to initialize the types from jsonSchema file {}", jsonSchema, e);
}
}
// Get Entity Types
jsonSchemas = EntityUtil.getJsonDataResources(".*json/schema/entity/.*\\.json$");
for (String jsonSchema : jsonSchemas) {
try {
Type entityType = JsonUtils.getEntityType(jsonSchema);
if (entityType != null) {
types.add(entityType);
}
} catch (Exception e) {
LOG.warn("Failed to initialize the types from jsonSchema file {}", jsonSchema, e);
}
}
return types;
}
/** /**
* Get all the types from the `definitions` section of a JSON schema file that are annotated with "$comment" field set * Get all the fields types from the `definitions` section of a JSON schema file that are annotated with "$comment"
* to "@om-type". * field set to "@om-field-type".
*/ */
public static List<Type> getTypes(String jsonSchemaFile) throws IOException { public static List<Type> getFieldTypes(String jsonSchemaFile) throws IOException {
JsonNode node = JsonNode node =
OBJECT_MAPPER.readTree( OBJECT_MAPPER.readTree(
Objects.requireNonNull(JsonUtils.class.getClassLoader().getResourceAsStream(jsonSchemaFile))); Objects.requireNonNull(JsonUtils.class.getClassLoader().getResourceAsStream(jsonSchemaFile)));
@ -236,12 +268,15 @@ public final class JsonUtils {
Iterator<Entry<String, JsonNode>> definitions = node.get("definitions").fields(); Iterator<Entry<String, JsonNode>> definitions = node.get("definitions").fields();
while (definitions != null && definitions.hasNext()) { while (definitions != null && definitions.hasNext()) {
Entry<String, JsonNode> entry = definitions.next(); Entry<String, JsonNode> entry = definitions.next();
String typeName = entry.getKey();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
if (JsonUtils.hasAnnotation(value, JsonUtils.TYPE_ANNOTATION)) { if (JsonUtils.hasAnnotation(value, JsonUtils.FIELD_TYPE_ANNOTATION)) {
String description = String.valueOf(value.get("description")); String description = String.valueOf(value.get("description"));
Type type = Type type =
new Type() new Type()
.withName(entry.getKey()) .withName(typeName)
.withCategory(Category.Field)
.withFullyQualifiedName(typeName)
.withNameSpace(jsonNamespace) .withNameSpace(jsonNamespace)
.withDescription(description) .withDescription(description)
.withDisplayName(entry.getKey()) .withDisplayName(entry.getKey())
@ -251,4 +286,33 @@ public final class JsonUtils {
} }
return types; return types;
} }
/**
* Get all the fields types from the `definitions` section of a JSON schema file that are annotated with "$comment"
* field set to "@om-entity-type".
*/
public static Type getEntityType(String jsonSchemaFile) throws IOException {
JsonNode node =
OBJECT_MAPPER.readTree(
Objects.requireNonNull(JsonUtils.class.getClassLoader().getResourceAsStream(jsonSchemaFile)));
if (!JsonUtils.hasAnnotation(node, JsonUtils.ENTITY_TYPE_ANNOTATION)) {
return null;
}
String fileName = Paths.get(jsonSchemaFile).getFileName().toString();
String entityName = fileName.replace(" ", "").replace(".json", "");
String namespaceFile = Paths.get(jsonSchemaFile).getParent().getFileName().toString();
String namespace = namespaceFile.replace(" ", "").replace(".json", "");
String description = String.valueOf(node.get("description"));
return new Type()
.withName(entityName)
.withCategory(Category.Entity)
.withFullyQualifiedName(entityName)
.withNameSpace(namespace)
.withDescription(description)
.withDisplayName(entityName)
.withSchema(node.toPrettyString());
}
} }

View File

@ -1,5 +1,5 @@
{ {
"$id": "https://open-metadata.org/schema/api/data/createAttribute.json", "$id": "https://open-metadata.org/schema/api/data/createType.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "createType", "title": "createType",
"description": "Create a Type to be used for extending entities.", "description": "Create a Type to be used for extending entities.",
@ -7,7 +7,7 @@
"properties": { "properties": {
"name": { "name": {
"description": "Unique name that identifies a Type.", "description": "Unique name that identifies a Type.",
"$ref": "../type/basic.json#/definitions/entityName" "$ref": "../entity/type.json#/definitions/typeName"
}, },
"nameSpace": { "nameSpace": {
"description": "Namespace or group to which this type belongs to.", "description": "Namespace or group to which this type belongs to.",
@ -20,7 +20,10 @@
}, },
"description": { "description": {
"description": "Optional description of the type.", "description": "Optional description of the type.",
"type": "string" "$ref" : "../type/basic.json#/definitions/markdown"
},
"category" : {
"$ref" : "../entity/type.json#/definitions/category"
}, },
"schema": { "schema": {
"description": "JSON schema encoded as string. This will be used to validate the type values.", "description": "JSON schema encoded as string. This will be used to validate the type values.",

View File

@ -2,6 +2,7 @@
"$id": "https://open-metadata.org/schema/entity/data/table.json", "$id": "https://open-metadata.org/schema/entity/data/table.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Table", "title": "Table",
"$comment" : "@om-entity-type",
"description": "This schema defines the Table entity. A Table organizes data in rows and columns and is defined by a Schema. OpenMetadata does not have a separate abstraction for Schema. Both Table and Schema are captured in this entity.", "description": "This schema defines the Table entity. A Table organizes data in rows and columns and is defined by a Schema. OpenMetadata does not have a separate abstraction for Schema. Both Table and Schema are captured in this entity.",
"type": "object", "type": "object",
"javaType": "org.openmetadata.catalog.entity.data.Table", "javaType": "org.openmetadata.catalog.entity.data.Table",

View File

@ -2,10 +2,63 @@
"$id": "https://open-metadata.org/schema/entity/type.json", "$id": "https://open-metadata.org/schema/entity/type.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Type", "title": "Type",
"description": "This schema defines a type entity used for extending an entity with custom attributes.", "description": "This schema defines a type as an entity. Types includes field types and entity types. Custom types can also be defined by the users to extend the metadata system.",
"type": "object", "type": "object",
"javaType": "org.openmetadata.catalog.entity.Type", "javaType": "org.openmetadata.catalog.entity.Type",
"javaInterfaces": ["org.openmetadata.catalog.EntityInterface"], "javaInterfaces": ["org.openmetadata.catalog.EntityInterface"],
"definitions": {
"category" : {
"description" : "Metadata category to which a type belongs to.",
"type": "string",
"javaType": "org.openmetadata.catalog.entity.type.Category",
"enum": [
"field",
"entity"
],
"javaEnums": [
{
"name": "Field"
},
{
"name": "Entity"
}
]
},
"fieldName": {
"description": "Name of the entity field. Note a field name must be unique for an entity. Field name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
"type" : "string",
"pattern": "^[a-z][a-zA-Z0-9]+$"
},
"typeName": {
"description": "Name of the field or entity types. Note a field name must be unique for an entity. Field name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
"type" : "string",
"pattern": "^[a-z][a-zA-Z0-9]+$"
},
"customField" : {
"description" : "Type used for adding custom field to an entity to extend it.",
"type" : "object",
"javaType": "org.openmetadata.catalog.entity.type.CustomField",
"properties": {
"name": {
"description": "Name of the entity field. Note a field name must be unique for an entity. Field name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
"$ref" : "#/definitions/fieldName"
},
"description": {
"$ref" : "../type/basic.json#/definitions/markdown"
},
"fieldType": {
"description": "Reference to a field type. Only field types are allows and entity types are not allowed as custom fields to extend an existing entity",
"$ref": "../type/entityReference.json"
}
},
"required": [
"name",
"description",
"fieldType"
],
"additionalProperties": false
}
},
"properties": { "properties": {
"id": { "id": {
"description": "Unique identifier of the type instance.", "description": "Unique identifier of the type instance.",
@ -13,29 +66,39 @@
}, },
"name": { "name": {
"description": "Unique name that identifies the type.", "description": "Unique name that identifies the type.",
"$ref": "../type/basic.json#/definitions/entityName" "$ref": "#/definitions/typeName"
}, },
"fullyQualifiedName": { "fullyQualifiedName": {
"description": "FullyQualifiedName same as `name`.", "description": "FullyQualifiedName same as `name`.",
"$ref": "../type/basic.json#/definitions/fullyQualifiedEntityName" "$ref": "../type/basic.json#/definitions/fullyQualifiedEntityName"
}, },
"nameSpace": {
"description": "Namespace or group to which this type belongs to.",
"type": "string",
"default" : "custom"
},
"displayName": { "displayName": {
"description": "Display Name that identifies this type.", "description": "Display Name that identifies this type.",
"type": "string" "type": "string"
}, },
"description": { "description": {
"description": "Optional description of entity.", "description": "Optional description of entity.",
"type": "string" "$ref" : "../type/basic.json#/definitions/markdown"
},
"nameSpace": {
"description": "Namespace or group to which this type belongs to. For example, some of the field types commonly used can come from `basic` namespace. Some of the entities such as `table`, `database`, etc. come from `data` namespace.",
"type": "string",
"default" : "custom"
},
"category" : {
"$ref" : "#/definitions/category"
}, },
"schema": { "schema": {
"description": "JSON schema encoded as string that defines the type. This will be used to validate the type values.", "description": "JSON schema encoded as string that defines the type. This will be used to validate the type values.",
"$ref": "../type/basic.json#/definitions/jsonSchema" "$ref": "../type/basic.json#/definitions/jsonSchema"
}, },
"customFields" : {
"description": "Custom fields added to extend the entity. Only available for entity type",
"type" : "array",
"items" : {
"$ref" : "#/definitions/customField"
}
},
"version": { "version": {
"description": "Metadata version of the entity.", "description": "Metadata version of the entity.",
"$ref": "../type/entityHistory.json#/definitions/entityVersion" "$ref": "../type/entityHistory.json#/definitions/entityVersion"
@ -61,7 +124,7 @@
"name", "name",
"nameSpace", "nameSpace",
"description", "description",
"type" "schema"
], ],
"additionalProperties": false "additionalProperties": false
} }

View File

@ -5,17 +5,17 @@
"description": "This schema defines basic common types that are used by other schemas.", "description": "This schema defines basic common types that are used by other schemas.",
"definitions": { "definitions": {
"integer" : { "integer" : {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "An integer type.", "description": "An integer type.",
"type" : "integer" "type" : "integer"
}, },
"number" : { "number" : {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "A numeric type that includes integer or floating point numbers.", "description": "A numeric type that includes integer or floating point numbers.",
"type" : "integer" "type" : "integer"
}, },
"string" : { "string" : {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "A String type.", "description": "A String type.",
"type" : "string" "type" : "string"
}, },
@ -25,7 +25,7 @@
"format": "uuid" "format": "uuid"
}, },
"email": { "email": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Email address of a user or other entities.", "description": "Email address of a user or other entities.",
"type": "string", "type": "string",
"format": "email", "format": "email",
@ -34,7 +34,7 @@
"maxLength": 127 "maxLength": 127
}, },
"timestamp": { "timestamp": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Timestamp in Unix epoch time milliseconds.", "description": "Timestamp in Unix epoch time milliseconds.",
"@comment": "Note that during code generation this is converted into long", "@comment": "Note that during code generation this is converted into long",
"type": "integer", "type": "integer",
@ -46,7 +46,7 @@
"format": "uri" "format": "uri"
}, },
"timeInterval": { "timeInterval": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"type": "object", "type": "object",
"description": "Time interval in unixTimeMillis.", "description": "Time interval in unixTimeMillis.",
"javaType": "org.openmetadata.catalog.type.TimeInterval", "javaType": "org.openmetadata.catalog.type.TimeInterval",
@ -63,18 +63,18 @@
"additionalProperties": false "additionalProperties": false
}, },
"duration": { "duration": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Duration in ISO 8601 format in UTC. Example - 'P23DT23H'.", "description": "Duration in ISO 8601 format in UTC. Example - 'P23DT23H'.",
"type": "string" "type": "string"
}, },
"date": { "date": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Date in ISO 8601 format in UTC. Example - '2018-11-13'.", "description": "Date in ISO 8601 format in UTC. Example - '2018-11-13'.",
"type": "string", "type": "string",
"format": "date" "format": "date"
}, },
"dateTime": { "dateTime": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Date and time in ISO 8601 format. Example - '2018-11-13T20:20:39+00:00'.", "description": "Date and time in ISO 8601 format. Example - '2018-11-13T20:20:39+00:00'.",
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@ -97,12 +97,12 @@
"maxLength": 256 "maxLength": 256
}, },
"sqlQuery": { "sqlQuery": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "SQL query statement. Example - 'select * from orders'.", "description": "SQL query statement. Example - 'select * from orders'.",
"type": "string" "type": "string"
}, },
"markdown": { "markdown": {
"$comment" : "@om-type", "$comment" : "@om-field-type",
"description": "Text in Markdown format", "description": "Text in Markdown format",
"type": "string" "type": "string"
}, },

View File

@ -49,6 +49,7 @@ public abstract class CatalogApplicationTest {
// The system properties are provided by maven-surefire for testing with mysql and postgres // The system properties are provided by maven-surefire for testing with mysql and postgres
final String jdbcContainerClassName = System.getProperty("jdbcContainerClassName"); final String jdbcContainerClassName = System.getProperty("jdbcContainerClassName");
final String jdbcContainerImage = System.getProperty("jdbcContainerImage"); final String jdbcContainerImage = System.getProperty("jdbcContainerImage");
LOG.info("Using test container class {} and image {}", jdbcContainerClassName, jdbcContainerImage);
SQL_CONTAINER = SQL_CONTAINER =
(JdbcDatabaseContainer<?>) (JdbcDatabaseContainer<?>)

View File

@ -145,6 +145,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K> extends C
protected boolean supportsAuthorizedMetadataOperations = true; protected boolean supportsAuthorizedMetadataOperations = true;
protected boolean supportsFieldsQueryParam = true; protected boolean supportsFieldsQueryParam = true;
protected boolean supportsEmptyDescription = true; protected boolean supportsEmptyDescription = true;
protected boolean supportsNameWithDot = true;
public static final String DATA_STEWARD_ROLE_NAME = "DataSteward"; public static final String DATA_STEWARD_ROLE_NAME = "DataSteward";
public static final String DATA_CONSUMER_ROLE_NAME = "DataConsumer"; public static final String DATA_CONSUMER_ROLE_NAME = "DataConsumer";
@ -623,14 +624,14 @@ public abstract class EntityResourceTest<T extends EntityInterface, K> extends C
// Common entity tests for POST operations // Common entity tests for POST operations
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Test @Test
void post_entityCreateWithInvalidName_400() { protected void post_entityCreateWithInvalidName_400() {
// Create an entity with mandatory name field null // Create an entity with mandatory name field null
final K request = createRequest(null, "description", "displayName", null); final K request = createRequest(null, "description", "displayName", null);
assertResponse(() -> createEntity(request, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[name must not be null]"); assertResponse(() -> createEntity(request, ADMIN_AUTH_HEADERS), BAD_REQUEST, "[name must not be null]");
// Create an entity with mandatory name field empty // Create an entity with mandatory name field empty
final K request1 = createRequest("", "description", "displayName", null); final K request1 = createRequest("", "description", "displayName", null);
assertResponse(() -> createEntity(request1, ADMIN_AUTH_HEADERS), BAD_REQUEST, ENTITY_NAME_LENGTH_ERROR); assertResponseContains(() -> createEntity(request1, ADMIN_AUTH_HEADERS), BAD_REQUEST, ENTITY_NAME_LENGTH_ERROR);
// Create an entity with mandatory name field too long // Create an entity with mandatory name field too long
final K request2 = createRequest(LONG_ENTITY_NAME, "description", "displayName", null); final K request2 = createRequest(LONG_ENTITY_NAME, "description", "displayName", null);
@ -698,6 +699,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K> extends C
@Test @Test
void post_entityWithDots_200() throws HttpResponseException { void post_entityWithDots_200() throws HttpResponseException {
if (!supportsNameWithDot) {
return;
}
// Entity without "." should not have quoted fullyQualifiedName // Entity without "." should not have quoted fullyQualifiedName
String name = format("%s_foo_bar", entityType); String name = format("%s_foo_bar", entityType);
K request = createRequest(name, "", null, null); K request = createRequest(name, "", null, null);

View File

@ -14,20 +14,30 @@
package org.openmetadata.catalog.resources.metadata; package org.openmetadata.catalog.resources.metadata;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.openmetadata.catalog.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.catalog.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.catalog.util.TestUtils.assertResponse;
import static org.openmetadata.catalog.util.TestUtils.assertResponseContains;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response.Status;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException; import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import org.openmetadata.catalog.Entity; import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.CreateType; import org.openmetadata.catalog.api.CreateType;
import org.openmetadata.catalog.entity.Type; import org.openmetadata.catalog.entity.Type;
import org.openmetadata.catalog.entity.type.Category;
import org.openmetadata.catalog.entity.type.CustomField;
import org.openmetadata.catalog.resources.EntityResourceTest; import org.openmetadata.catalog.resources.EntityResourceTest;
import org.openmetadata.catalog.resources.types.TypeResource; import org.openmetadata.catalog.resources.types.TypeResource;
import org.openmetadata.catalog.resources.types.TypeResource.TypeList; import org.openmetadata.catalog.resources.types.TypeResource.TypeList;
@ -43,12 +53,63 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
super(Entity.TYPE, Type.class, TypeList.class, "metadata/types", TypeResource.FIELDS); super(Entity.TYPE, Type.class, TypeList.class, "metadata/types", TypeResource.FIELDS);
supportsEmptyDescription = false; supportsEmptyDescription = false;
supportsFieldsQueryParam = false; supportsFieldsQueryParam = false;
supportsNameWithDot = false;
} }
@BeforeAll @BeforeAll
public void setup(TestInfo test) throws IOException, URISyntaxException { public void setup(TestInfo test) throws IOException, URISyntaxException {
super.setup(test); super.setup(test);
INT_TYPE = getEntityByName("type.basic.integer", "", ADMIN_AUTH_HEADERS); INT_TYPE = getEntityByName("integer", "", ADMIN_AUTH_HEADERS);
}
@Override
@Test
public void post_entityCreateWithInvalidName_400() {
String[][] tests = {
{"Abcd", "[name must match \"^[a-z][a-zA-Z0-9]+$\"]"},
{"a bc", "[name must match \"^[a-z][a-zA-Z0-9]+$\"]"}, // Name must not have space
{"a_bc", "[name must match \"^[a-z][a-zA-Z0-9]+$\"]"}, // Name must not be underscored
{"a-bc", "[name must match \"^[a-z][a-zA-Z0-9]+$\"]"}, // Name must not be hyphened
};
CreateType create = createRequest("placeHolder", "", "", null);
for (String[] test : tests) {
LOG.info("Testing with the name {}", test[0]);
create.withName(test[0]);
assertResponseContains(() -> createEntity(create, ADMIN_AUTH_HEADERS), Status.BAD_REQUEST, test[1]);
}
}
@Test
public void put_customField_200() throws HttpResponseException {
Type tableEntity = getEntityByName("table", "customFields", ADMIN_AUTH_HEADERS);
assertTrue(listOrEmpty(tableEntity.getCustomFields()).isEmpty());
// Add a custom field with name intA with type integer
CustomField fieldA =
new CustomField().withName("intA").withDescription("intA").withFieldType(INT_TYPE.getEntityReference());
tableEntity = addCustomField(tableEntity.getId(), fieldA, Status.OK, ADMIN_AUTH_HEADERS);
assertEquals(1, tableEntity.getCustomFields().size());
assertEquals(fieldA, tableEntity.getCustomFields().get(0));
// Add a second field with name intB with type integer
CustomField fieldB =
new CustomField().withName("intB").withDescription("intB").withFieldType(INT_TYPE.getEntityReference());
tableEntity = addCustomField(tableEntity.getId(), fieldB, Status.OK, ADMIN_AUTH_HEADERS);
assertEquals(2, tableEntity.getCustomFields().size());
assertEquals(fieldA, tableEntity.getCustomFields().get(0));
assertEquals(fieldB, tableEntity.getCustomFields().get(1));
}
@Test
public void put_customFieldToFieldType_4xx() {
// Adding a custom field to a field type is not allowed (only entity type is allowed)
CustomField field =
new CustomField().withName("intA").withDescription("intA").withFieldType(INT_TYPE.getEntityReference());
assertResponse(
() -> addCustomField(INT_TYPE.getId(), field, Status.CREATED, ADMIN_AUTH_HEADERS),
Status.BAD_REQUEST,
"Field types can't be extended");
} }
@Override @Override
@ -61,12 +122,23 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
return type; return type;
} }
public Type addCustomField(UUID entityTypeId, CustomField customField, Status status, Map<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = getResource(entityTypeId);
return TestUtils.put(target, customField, Type.class, status, authHeaders);
}
@Override @Override
public CreateType createRequest(String name, String description, String displayName, EntityReference owner) { public CreateType createRequest(String name, String description, String displayName, EntityReference owner) {
if (name != null) {
name = name.replaceAll("[. _-]", "");
}
System.out.println("XXX Using the name " + name);
return new CreateType() return new CreateType()
.withName(name) .withName(name)
.withDescription(description) .withDescription(description)
.withDisplayName(displayName) .withDisplayName(displayName)
.withCategory(Category.Field)
.withSchema(INT_TYPE.getSchema()); .withSchema(INT_TYPE.getSchema());
} }