From 499213e4214a469302addfee4319fb6f450b82d4 Mon Sep 17 00:00:00 2001 From: Jakub Senko Date: Fri, 19 Jan 2024 18:11:41 +0100 Subject: [PATCH] fix: address code review comments Resolved: - remove format restrictions on group and artifact ID - more consistent naming - support specifying branches on artifact (version) creation - support branch "rebase" - add tests - small bug fixes --- .../registry/rest/v2/GroupsResourceImpl.java | 6 +- .../registry/rest/v3/GroupsResourceImpl.java | 302 ++-- .../http/RegistryExceptionMapperService.java | 4 +- .../registry/storage/RegistryStorage.java | 22 +- .../ReadOnlyRegistryStorageDecorator.java | 10 +- .../RegistryStorageDecoratorBase.java | 9 +- .../RegistryStorageDecoratorReadOnlyBase.java | 4 +- ...{BranchDto.java => ArtifactBranchDto.java} | 4 +- ...ranchAlreadyContainsVersionException.java} | 6 +- ...a => ArtifactBranchNotFoundException.java} | 4 +- .../AbstractReadOnlyRegistryStorage.java | 9 +- .../impl/gitops/GitOpsRegistryStorage.java | 4 +- .../kafkasql/KafkaSqlRegistryStorage.java | 23 +- .../impl/kafkasql/KafkaSqlSubmitter.java | 26 +- .../impl/kafkasql/sql/KafkaSqlSink.java | 15 +- .../impl/kafkasql/values/ActionType.java | 3 +- .../kafkasql/values/ArtifactBranchValue.java | 9 +- .../impl/sql/AbstractSqlRegistryStorage.java | 177 +- .../storage/impl/sql/CommonSqlStatements.java | 79 +- .../impl/sql/SQLServerSqlStatements.java | 18 +- .../storage/impl/sql/SqlStatements.java | 16 +- ...pper.java => ArtifactBranchDtoMapper.java} | 14 +- ...r.java => ArtifactBranchEntityMapper.java} | 14 +- .../importing/AbstractDataImporter.java | 6 +- .../storage/importing/SqlDataImporter.java | 10 +- .../registry/v3/openapi.json | 1430 +++++++++-------- .../apicurio/registry/storage/impl/sql/h2.ddl | 8 +- .../registry/storage/impl/sql/mssql.ddl | 8 +- .../registry/storage/impl/sql/postgresql.ddl | 8 +- .../noprofile/rest/v3/GroupsResourceTest.java | 718 +++++++-- .../storage/AbstractRegistryStorageTest.java | 18 +- .../readonly/ReadOnlyRegistryStorageTest.java | 3 +- .../apicurio/registry/model/ArtifactId.java | 13 +- .../io/apicurio/registry/model/BranchId.java | 4 +- .../java/io/apicurio/registry/model/GA.java | 1 - .../java/io/apicurio/registry/model/GAV.java | 3 +- .../io/apicurio/registry/model/GroupId.java | 18 +- .../io/apicurio/registry/model/VersionId.java | 2 +- .../src/main/resources/META-INF/openapi.json | 1430 +++++++++-------- .../registry/model/ModelTypesTest.java | 85 + .../model/VersionExpressionParserTest.java | 39 + .../GenerateCanonicalHashImportIT.java | 6 +- ...hEntity.java => ArtifactBranchEntity.java} | 12 +- .../registry/utils/impexp/EntityReader.java | 8 +- .../registry/utils/impexp/EntityType.java | 2 +- .../registry/utils/impexp/EntityWriter.java | 12 +- 46 files changed, 2780 insertions(+), 1842 deletions(-) rename app/src/main/java/io/apicurio/registry/storage/dto/{BranchDto.java => ArtifactBranchDto.java} (86%) rename app/src/main/java/io/apicurio/registry/storage/error/{BranchVersionAlreadyExistsException.java => ArtifactBranchAlreadyContainsVersionException.java} (64%) rename app/src/main/java/io/apicurio/registry/storage/error/{BranchNotFoundException.java => ArtifactBranchNotFoundException.java} (79%) rename app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/{ArtifactVersionBranchDtoMapper.java => ArtifactBranchDtoMapper.java} (50%) rename app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/{ArtifactVersionBranchEntityMapper.java => ArtifactBranchEntityMapper.java} (50%) create mode 100644 common/src/test/java/io/apicurio/registry/model/ModelTypesTest.java create mode 100644 common/src/test/java/io/apicurio/registry/model/VersionExpressionParserTest.java rename utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/{ArtifactVersionBranchEntity.java => ArtifactBranchEntity.java} (81%) diff --git a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java index 2df919d121..5bb5f1d8e5 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v2/GroupsResourceImpl.java @@ -590,7 +590,7 @@ public Comment addArtifactVersionComment(String groupId, String artifactId, Stri * @see io.apicurio.registry.rest.v2.GroupsResource#deleteArtifactVersionComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String) */ @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) public void deleteArtifactVersionComment(String groupId, String artifactId, String version, String commentId) { requireParameter("groupId", groupId); @@ -620,7 +620,7 @@ public List getArtifactVersionComments(String groupId, String artifactI * @see io.apicurio.registry.rest.v2.GroupsResource#updateArtifactVersionComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, io.apicurio.registry.rest.v2.beans.NewComment) */ @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) public void updateArtifactVersionComment(String groupId, String artifactId, String version, String commentId, NewComment data) { requireParameter("groupId", groupId); @@ -705,7 +705,7 @@ public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactT * @see io.apicurio.registry.rest.v2.GroupsResource#createArtifact(String, String, String, String, IfExists, Boolean, String, String, String, String, String, String, ArtifactContent) */ @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", "from_url" /*KEY_FROM_URL*/, "11", "artifact_sha" /*KEY_SHA*/}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", KEY_FROM_URL, "11", KEY_SHA}) @Authorized(style = AuthorizedStyle.GroupOnly, level = AuthorizedLevel.Write) public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactType, String xRegistryArtifactId, String xRegistryVersion, IfExists ifExists, Boolean canonical, diff --git a/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java b/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java index 1602e4dd52..75db26a2b2 100644 --- a/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java +++ b/app/src/main/java/io/apicurio/registry/rest/v3/GroupsResourceImpl.java @@ -54,7 +54,6 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.*; -import java.util.Map.Entry; import java.util.function.Supplier; import static io.apicurio.common.apps.logging.audit.AuditingConstants.*; @@ -63,7 +62,6 @@ /** * Implements the {@link GroupsResource} JAX-RS interface. - * */ @ApplicationScoped @Interceptors({ResponseErrorLivenessCheck.class, ResponseTimeoutReadinessCheck.class}) @@ -118,29 +116,27 @@ public Response getLatestArtifact(String groupId, String artifactId, HandleRefer return builder.build(); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#updateArtifact(String, String, String, String, String, String, String, InputStream) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, "4", KEY_NAME_ENCODED, "5", KEY_DESCRIPTION, "6", KEY_DESCRIPTION_ENCODED}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, + "4", KEY_NAME_ENCODED, "5", KEY_DESCRIPTION, "6", KEY_DESCRIPTION_ENCODED, "7", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public ArtifactMetaData updateArtifact(String groupId, String artifactId, String xRegistryVersion, - String xRegistryName, String xRegistryNameEncoded, String xRegistryDescription, - String xRegistryDescriptionEncoded, InputStream data) { - return this.updateArtifactWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryNameEncoded, xRegistryDescription, xRegistryDescriptionEncoded, data, Collections.emptyList()); + public ArtifactMetaData updateArtifact(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryNameEncoded, String xRegistryDescription, String xRegistryDescriptionEncoded, List xRegistryArtifactBranches, + InputStream data) { + return this.updateArtifactWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryNameEncoded, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryArtifactBranches, data, Collections.emptyList()); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#updateArtifact(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, io.apicurio.registry.rest.v3.beans.ArtifactContent) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, "4", KEY_NAME_ENCODED, "5", KEY_DESCRIPTION, "6", KEY_DESCRIPTION_ENCODED}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, + "4", KEY_NAME_ENCODED, "5", KEY_DESCRIPTION, "6", KEY_DESCRIPTION_ENCODED, "7", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public ArtifactMetaData updateArtifact(String groupId, String artifactId, String xRegistryVersion, - String xRegistryName, String xRegistryNameEncoded, String xRegistryDescription, - String xRegistryDescriptionEncoded, ArtifactContent data) { + public ArtifactMetaData updateArtifact(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryNameEncoded, String xRegistryDescription, String xRegistryDescriptionEncoded, List xRegistryArtifactBranches, + ArtifactContent data) { requireParameter("content", data.getContent()); - return this.updateArtifactWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryNameEncoded, xRegistryDescription, xRegistryDescriptionEncoded, IoUtil.toStream(data.getContent()), data.getReferences()); + return this.updateArtifactWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryNameEncoded, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryArtifactBranches, IoUtil.toStream(data.getContent()), data.getReferences()); } /** @@ -148,10 +144,10 @@ public ArtifactMetaData updateArtifact(String groupId, String artifactId, String */ @Override public List getArtifactVersionReferences(String groupId, String artifactId, - String versionExpression, ReferenceType refType) { + String versionExpression, ReferenceType refType) { var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); if (refType == null || refType == ReferenceType.OUTBOUND) { return storage.getArtifactVersion(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()) @@ -167,7 +163,10 @@ public List getArtifactVersionReferences(String groupId, Stri } } - private ArtifactMetaData updateArtifactWithRefs(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, String xRegistryNameEncoded, String xRegistryDescription, String xRegistryDescriptionEncoded, InputStream data, List references) { + private ArtifactMetaData updateArtifactWithRefs(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryNameEncoded, String xRegistryDescription, String xRegistryDescriptionEncoded, + List artifactBranches, + InputStream data, List references) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); @@ -182,7 +181,7 @@ private ArtifactMetaData updateArtifactWithRefs(String groupId, String artifactI if (content.bytes().length == 0) { throw new BadRequestException(EMPTY_CONTENT_ERROR_MESSAGE); } - return updateArtifactInternal(groupId, artifactId, xRegistryVersion, artifactName, artifactDescription, content, getContentType(), references); + return updateArtifactInternal(groupId, artifactId, xRegistryVersion, artifactName, artifactDescription, artifactBranches, content, getContentType(), references); } /** @@ -482,8 +481,8 @@ public void testUpdateArtifact(String groupId, String artifactId, InputStream da } String artifactType = lookupArtifactType(groupId, artifactId); - rulesService.applyRules(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, artifactType, content, - RuleApplicationType.UPDATE, Collections.emptyList(), Collections.emptyMap()); //TODO:references not supported for testing update + rulesService.applyRules(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, artifactType, content, + RuleApplicationType.UPDATE, Collections.emptyList(), Collections.emptyMap()); //TODO:references not supported for testing update } /** @@ -497,7 +496,7 @@ public Response getArtifactVersion(String groupId, String artifactId, String ver requireParameter("versionExpression", versionExpression); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); if (references == null) { references = HandleReferencesType.PRESERVE; @@ -534,7 +533,7 @@ public void deleteArtifactVersion(String groupId, String artifactId, String vers requireParameter("version", version); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), version, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); storage.deleteArtifactVersion(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()); } @@ -550,7 +549,7 @@ public VersionMetaData getArtifactVersionMetaData(String groupId, String artifac requireParameter("version", version); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), version, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); ArtifactVersionMetaDataDto dto = storage.getArtifactVersionMetaData(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()); return V3ApiUtil.dtoToVersionMetaData(gav.getRawGroupIdWithDefaultString(), gav.getRawArtifactId(), dto.getType(), dto); @@ -568,7 +567,7 @@ public void updateArtifactVersionMetaData(String groupId, String artifactId, Str requireParameter("versionExpression", versionExpression); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); if (data.getProperties() != null) { data.getProperties().forEach((k, v) -> requireParameter("property value", v)); @@ -593,7 +592,7 @@ public void deleteArtifactVersionMetaData(String groupId, String artifactId, Str requireParameter("version", version); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), version, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); storage.deleteArtifactVersionMetaData(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()); } @@ -610,7 +609,7 @@ public Comment addArtifactVersionComment(String groupId, String artifactId, Stri requireParameter("versionExpression", versionExpression); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); CommentDto newComment = storage.createArtifactVersionComment(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId(), data.getValue()); @@ -621,7 +620,7 @@ public Comment addArtifactVersionComment(String groupId, String artifactId, Stri * @see io.apicurio.registry.rest.v3.GroupsResource#deleteArtifactVersionComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String) */ @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) public void deleteArtifactVersionComment(String groupId, String artifactId, String versionExpression, String commentId) { requireParameter("groupId", groupId); @@ -630,7 +629,7 @@ public void deleteArtifactVersionComment(String groupId, String artifactId, Stri requireParameter("commentId", commentId); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); storage.deleteArtifactVersionComment(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId(), commentId); } @@ -646,7 +645,7 @@ public List getArtifactVersionComments(String groupId, String artifactI requireParameter("version", version); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), version, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); return storage.getArtifactVersionComments(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()) .stream() @@ -658,7 +657,7 @@ public List getArtifactVersionComments(String groupId, String artifactI * @see io.apicurio.registry.rest.v3.GroupsResource#updateArtifactVersionComment(java.lang.String, java.lang.String, java.lang.String, java.lang.String, io.apicurio.registry.rest.v3.beans.NewComment) */ @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", "comment_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) public void updateArtifactVersionComment(String groupId, String artifactId, String versionExpression, String commentId, NewComment data) { requireParameter("groupId", groupId); @@ -668,7 +667,7 @@ public void updateArtifactVersionComment(String groupId, String artifactId, Stri requireParameter("value", data.getValue()); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); storage.updateArtifactVersionComment(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId(), commentId, data.getValue()); @@ -686,7 +685,7 @@ public void updateArtifactVersionState(String groupId, String artifactId, String requireParameter("versionExpression", versionExpression); var gav = VersionExpressionParser.parse(new GA(groupId, artifactId), versionExpression, - (ga, branchId) -> storage.getArtifactBranchLeaf(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); + (ga, branchId) -> storage.getArtifactBranchTip(ga, branchId, ArtifactRetrievalBehavior.DEFAULT)); storage.updateArtifactState(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId(), data.getState()); } @@ -730,31 +729,32 @@ public void deleteArtifactsInGroup(String groupId) { storage.deleteArtifacts(new GroupId(groupId).getRawGroupIdWithNull()); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#createArtifact(String, String, String, String, IfExists, Boolean, String, String, String, String, String, String, InputStream) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", KEY_FROM_URL, "11", KEY_SHA}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, + "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, + "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", KEY_FROM_URL, "11", KEY_SHA, + "12", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupOnly, level = AuthorizedLevel.Write) - public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactType, String xRegistryArtifactId, - String xRegistryVersion, IfExists ifExists, Boolean canonical, - String xRegistryDescription, String xRegistryDescriptionEncoded, - String xRegistryName, String xRegistryNameEncoded, - String xRegistryContentHash, String xRegistryHashAlgorithm, InputStream data) { - return this.createArtifactWithRefs(groupId, xRegistryArtifactType, xRegistryArtifactId, xRegistryVersion, ifExists, canonical, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryName, xRegistryNameEncoded, xRegistryContentHash, xRegistryHashAlgorithm, data, Collections.emptyList()); + public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactType, String xRegistryArtifactId, String xRegistryVersion, + IfExists ifExists, Boolean canonical, String xRegistryDescription, String xRegistryDescriptionEncoded, + String xRegistryName, String xRegistryNameEncoded, String xRegistryContentHash, String xRegistryHashAlgorithm, + List xRegistryArtifactBranches, InputStream data) { + return this.createArtifactWithRefs(groupId, xRegistryArtifactType, xRegistryArtifactId, xRegistryVersion, ifExists, canonical, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryName, + xRegistryNameEncoded, xRegistryContentHash, xRegistryHashAlgorithm, xRegistryArtifactBranches, data, Collections.emptyList()); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#createArtifact(String, String, String, String, IfExists, Boolean, String, String, String, String, String, String, ArtifactContent) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", "from_url" /*KEY_FROM_URL*/, "11", "artifact_sha" /*KEY_SHA*/}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_TYPE, "2", KEY_ARTIFACT_ID, "3", KEY_VERSION, + "4", KEY_IF_EXISTS, "5", KEY_CANONICAL, "6", KEY_DESCRIPTION, "7", KEY_DESCRIPTION_ENCODED, + "8", KEY_NAME, "9", KEY_NAME_ENCODED, "10", KEY_FROM_URL, "11", KEY_SHA, + "12", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupOnly, level = AuthorizedLevel.Write) - public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactType, String xRegistryArtifactId, - String xRegistryVersion, IfExists ifExists, Boolean canonical, - String xRegistryDescription, String xRegistryDescriptionEncoded, - String xRegistryName, String xRegistryNameEncoded, - String xRegistryContentHash, String xRegistryHashAlgorithm, ArtifactContent data) { + public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactType, String xRegistryArtifactId, String xRegistryVersion, + IfExists ifExists, Boolean canonical, String xRegistryDescription, String xRegistryDescriptionEncoded, + String xRegistryName, String xRegistryNameEncoded, String xRegistryContentHash, String xRegistryHashAlgorithm, + List xRegistryArtifactBranches, ArtifactContent data) { requireParameter("content", data.getContent()); Client client = null; @@ -768,7 +768,9 @@ public ArtifactMetaData createArtifact(String groupId, String xRegistryArtifactT content = IoUtil.toStream(data.getContent()); } - return this.createArtifactWithRefs(groupId, xRegistryArtifactType, xRegistryArtifactId, xRegistryVersion, ifExists, canonical, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryName, xRegistryNameEncoded, xRegistryContentHash, xRegistryHashAlgorithm, content, data.getReferences()); + return this.createArtifactWithRefs(groupId, xRegistryArtifactType, xRegistryArtifactId, xRegistryVersion, ifExists, + canonical, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryName, + xRegistryNameEncoded, xRegistryContentHash, xRegistryHashAlgorithm, xRegistryArtifactBranches, content, data.getReferences()); } catch (KeyManagementException kme) { throw new RuntimeException(kme); } catch (NoSuchAlgorithmException nsae) { @@ -853,6 +855,7 @@ private ArtifactMetaData createArtifactWithRefs(String groupId, String xRegistry String xRegistryDescription, String xRegistryDescriptionEncoded, String xRegistryName, String xRegistryNameEncoded, String xRegistryContentHash, String xRegistryHashAlgorithm, + List artifactBranches, InputStream data, List references) { requireParameter("groupId", groupId); @@ -925,6 +928,11 @@ private ArtifactMetaData createArtifactWithRefs(String groupId, String xRegistry EditableArtifactMetaDataDto metaData = getEditableMetaData(artifactName, artifactDescription); ArtifactMetaDataDto amd = storage.createArtifactWithMetadata(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, xRegistryVersion, artifactType, content, metaData, referencesAsDtos); + + for (String rawBranchId : normalizeMultiValuedHeader(artifactBranches)) { + storage.createOrUpdateArtifactBranch(new GAV(groupId, artifactId, amd.getVersion()), new BranchId(rawBranchId)); + } + return V3ApiUtil.dtoToMetaData(new GroupId(groupId).getRawGroupIdWithNull(), finalArtifactId, artifactType, amd); } catch (ArtifactAlreadyExistsException ex) { return handleIfExists(groupId, xRegistryArtifactId, xRegistryVersion, ifExists, artifactName, artifactDescription, content, ct, fcanonical, references); @@ -949,91 +957,149 @@ public VersionSearchResults listArtifactVersions(String groupId, String artifact return V3ApiUtil.dtoToSearchResults(resultsDto); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#createArtifactVersion(String, String, String, String, String, String, String, InputStream) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, "4", KEY_DESCRIPTION, "5", KEY_DESCRIPTION_ENCODED, "6", KEY_NAME_ENCODED}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, + "4", KEY_DESCRIPTION, "5", KEY_DESCRIPTION_ENCODED, "6", KEY_NAME_ENCODED, "7", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public VersionMetaData createArtifactVersion(String groupId, String artifactId, - String xRegistryVersion, String xRegistryName, - String xRegistryDescription, String xRegistryDescriptionEncoded, - String xRegistryNameEncoded, InputStream data) { - return this.createArtifactVersionWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryNameEncoded, data, Collections.emptyList()); + public VersionMetaData createArtifactVersion(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryDescription, String xRegistryDescriptionEncoded, String xRegistryNameEncoded, List xRegistryArtifactBranches, + InputStream data) { + return this.createArtifactVersionWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryNameEncoded, xRegistryArtifactBranches, data, Collections.emptyList()); } - /** - * @see io.apicurio.registry.rest.v3.GroupsResource#createArtifactVersion(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, io.apicurio.registry.rest.v3.beans.ArtifactContent) - */ + @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, "4", KEY_DESCRIPTION, "5", KEY_DESCRIPTION_ENCODED, "6", KEY_NAME_ENCODED}) + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", KEY_VERSION, "3", KEY_NAME, + "4", KEY_DESCRIPTION, "5", KEY_DESCRIPTION_ENCODED, "6", KEY_NAME_ENCODED, "7", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public VersionMetaData createArtifactVersion(String groupId, String artifactId, String xRegistryVersion, - String xRegistryName, String xRegistryDescription, String xRegistryDescriptionEncoded, - String xRegistryNameEncoded, ArtifactContent data) { + public VersionMetaData createArtifactVersion(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryDescription, String xRegistryDescriptionEncoded, String xRegistryNameEncoded, List xRegistryArtifactBranches, + ArtifactContent data) { requireParameter("content", data.getContent()); - return this.createArtifactVersionWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryNameEncoded, IoUtil.toStream(data.getContent()), data.getReferences()); + return this.createArtifactVersionWithRefs(groupId, artifactId, xRegistryVersion, xRegistryName, xRegistryDescription, xRegistryDescriptionEncoded, xRegistryNameEncoded, xRegistryArtifactBranches, IoUtil.toStream(data.getContent()), data.getReferences()); } @Override @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID}) @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Read) - public Branches listArtifactBranches(String groupId, String artifactId) { + public List listArtifactBranches(String groupId, String artifactId) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); - var res = new Branches(); - for (Entry> branch : storage.getArtifactBranches(new GA(groupId, artifactId)).entrySet()) { - res.setAdditionalProperty(branch.getKey().getRawBranchId(), branch.getValue() - .stream() - .map(gav -> new Gav(gav.getRawGroupIdWithDefaultString(), gav.getRawArtifactId(), gav.getRawVersionId())) - .collect(toList()) - ); - } - return res; + + return storage.getArtifactBranches(new GA(groupId, artifactId)) + .entrySet() + .stream() + .map(e -> { + return ArtifactBranch.builder() + .groupId(groupId) + .artifactId(artifactId) + .branchId(e.getKey().getRawBranchId()) + .versions( + e.getValue() + .stream() + .map(GAV::getRawVersionId) + .collect(toList()) + ) + .build(); + }) + .collect(toList()); } @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id"}) // TODO: Requires new version of apicurio-common-app-components. + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Read) - public List getArtifactBranch(String groupId, String artifactId, String branchId) { + public ArtifactBranch getArtifactBranch(String groupId, String artifactId, String rawBranchId) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); - requireParameter("branchId", branchId); - return storage.getArtifactBranch(new GA(groupId, artifactId), new BranchId(branchId), ArtifactRetrievalBehavior.DEFAULT) - .stream() - .map(gav -> new Gav(gav.getRawGroupIdWithDefaultString(), gav.getRawArtifactId(), gav.getRawVersionId())) - .collect(toList()); + requireParameter("branchId", rawBranchId); + + return ArtifactBranch.builder() + .groupId(groupId) + .artifactId(artifactId) + .branchId(rawBranchId) + .versions( + storage.getArtifactBranch(new GA(groupId, artifactId), new BranchId(rawBranchId), ArtifactRetrievalBehavior.DEFAULT) + .stream() + .map(GAV::getRawVersionId) + .collect(toList()) + ) + .build(); + + } + + + @Override + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id", "3", KEY_VERSION}) // TODO + @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) + public ArtifactBranch createOrUpdateArtifactBranch(String groupId, String artifactId, String rawBranchId, @NotNull String version) { + requireParameter("groupId", groupId); + requireParameter("artifactId", artifactId); + requireParameter("branchId", rawBranchId); + requireParameter("version", version); + + var gav = new GAV(groupId, artifactId, version); + var branchId = new BranchId(rawBranchId); + + storage.createOrUpdateArtifactBranch(gav, branchId); + + return ArtifactBranch.builder() + .groupId(gav.getRawGroupIdWithDefaultString()) + .artifactId(gav.getRawArtifactId()) + .branchId(branchId.getRawBranchId()) + .versions( + storage.getArtifactBranch(gav, branchId, ArtifactRetrievalBehavior.DEFAULT) + .stream() + .map(GAV::getRawVersionId) + .collect(toList()) + ) + .build(); } @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id", "3", KEY_VERSION}) // TODO: Requires new version of apicurio-common-app-components. + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id", "3", "branch"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public List createOrUpdateArtifactBranch(String groupId, String artifactId, String branchId, @NotNull String data) { + public ArtifactBranch createOrReplaceArtifactBranch(String groupId, String artifactId, String rawBranchId, @NotNull ArtifactBranch branch) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); - requireParameter("branchId", branchId); - requireParameter("version", data); - var gav = new GAV(groupId, artifactId, data); - var branch = new BranchId(branchId); - storage.createOrUpdateArtifactBranch(gav, branch); - return storage.getArtifactBranch(gav, branch, ArtifactRetrievalBehavior.DEFAULT) + requireParameter("branchId", rawBranchId); + requireParameter("branch", branch); + + var ga = new GA(groupId, artifactId); + var branchId = new BranchId(rawBranchId); + var versions = branch.getVersions() .stream() - .map(gav2 -> new Gav(gav2.getRawGroupIdWithDefaultString(), gav2.getRawArtifactId(), gav2.getRawVersionId())) + .map(VersionId::new) .collect(toList()); + + storage.createOrReplaceArtifactBranch(ga, branchId, versions); + + return ArtifactBranch.builder() + .groupId(ga.getRawGroupIdWithDefaultString()) + .artifactId(ga.getRawArtifactId()) + .branchId(branchId.getRawBranchId()) + .versions( + storage.getArtifactBranch(ga, branchId, ArtifactRetrievalBehavior.DEFAULT) + .stream() + .map(GAV::getRawVersionId) + .collect(toList()) + ) + .build(); } @Override - @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id"}) // TODO: Requires new version of apicurio-common-app-components. + @Audited(extractParameters = {"0", KEY_GROUP_ID, "1", KEY_ARTIFACT_ID, "2", "branch_id"}) // TODO @Authorized(style = AuthorizedStyle.GroupAndArtifact, level = AuthorizedLevel.Write) - public void deleteArtifactBranch(String groupId, String artifactId, String branchId) { + public void deleteArtifactBranch(String groupId, String artifactId, String rawBranchId) { requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); - requireParameter("branchId", branchId); - storage.deleteArtifactBranch(new GA(groupId, artifactId), new BranchId(branchId)); + requireParameter("branchId", rawBranchId); + + storage.deleteArtifactBranch(new GA(groupId, artifactId), new BranchId(rawBranchId)); } @@ -1052,7 +1118,9 @@ public void deleteArtifactBranch(String groupId, String artifactId, String branc * @param data * @param references */ - private VersionMetaData createArtifactVersionWithRefs(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, String xRegistryDescription, String xRegistryDescriptionEncoded, String xRegistryNameEncoded, InputStream data, List references) { + private VersionMetaData createArtifactVersionWithRefs(String groupId, String artifactId, String xRegistryVersion, String xRegistryName, + String xRegistryDescription, String xRegistryDescriptionEncoded, String xRegistryNameEncoded, List artifactBranches, + InputStream data, List references) { // TODO do something with the user-provided version info requireParameter("groupId", groupId); requireParameter("artifactId", artifactId); @@ -1082,6 +1150,11 @@ private VersionMetaData createArtifactVersionWithRefs(String groupId, String art rulesService.applyRules(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, artifactType, content, RuleApplicationType.UPDATE, references, resolvedReferences); EditableArtifactMetaDataDto metaData = getEditableMetaData(artifactName, artifactDescription); ArtifactMetaDataDto amd = storage.updateArtifactWithMetadata(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, xRegistryVersion, artifactType, content, metaData, referencesAsDtos); + + for (String rawBranchId : normalizeMultiValuedHeader(artifactBranches)) { + storage.createOrUpdateArtifactBranch(new GAV(groupId, artifactId, amd.getVersion()), new BranchId(rawBranchId)); + } + return V3ApiUtil.dtoToVersionMetaData(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, artifactType, amd); } @@ -1149,7 +1222,7 @@ private ArtifactMetaData handleIfExists(String groupId, String artifactId, Strin switch (ifExists) { case UPDATE: - return updateArtifactInternal(groupId, artifactId, version, artifactName, artifactDescription, content, contentType, references); + return updateArtifactInternal(groupId, artifactId, version, artifactName, artifactDescription, List.of(), content, contentType, references); case RETURN: return artifactMetaData; case RETURN_OR_UPDATE: @@ -1169,12 +1242,14 @@ private ArtifactMetaData handleIfExistsReturnOrUpdate(String groupId, String art } catch (ArtifactNotFoundException nfe) { // This is OK - we'll update the artifact if there is no matching content already there. } - return updateArtifactInternal(groupId, artifactId, version, artifactName, artifactDescription, content, contentType, references); + return updateArtifactInternal(groupId, artifactId, version, artifactName, artifactDescription, List.of(), content, contentType, references); } + private ArtifactMetaData updateArtifactInternal(String groupId, String artifactId, String version, - String name, String description, - ContentHandle content, String contentType, List references) { + String name, String description, + List artifactBranches, + ContentHandle content, String contentType, List references) { if (ContentTypeUtil.isApplicationYaml(contentType)) { content = ContentTypeUtil.yamlToJson(content); @@ -1191,9 +1266,15 @@ private ArtifactMetaData updateArtifactInternal(String groupId, String artifactI RuleApplicationType.UPDATE, references, resolvedReferences); EditableArtifactMetaDataDto metaData = getEditableMetaData(name, description); ArtifactMetaDataDto dto = storage.updateArtifactWithMetadata(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, version, artifactType, content, metaData, referencesAsDtos); + + for (String rawBranchId : normalizeMultiValuedHeader(artifactBranches)) { + storage.createOrUpdateArtifactBranch(new GAV(groupId, artifactId, dto.getVersion()), new BranchId(rawBranchId)); + } + return V3ApiUtil.dtoToMetaData(new GroupId(groupId).getRawGroupIdWithNull(), artifactId, artifactType, dto); } + private EditableArtifactMetaDataDto getEditableMetaData(String name, String description) { if (name != null || description != null) { return new EditableArtifactMetaDataDto(name, description, null, null); @@ -1210,4 +1291,9 @@ private List toReferenceDtos(List refer .map(V3ApiUtil::referenceToDto) .collect(toList()); } + + + private List normalizeMultiValuedHeader(List value) { + return value.stream().flatMap(v -> Arrays.stream(v.split(",")).map(String::strip)).collect(toList()); + } } diff --git a/app/src/main/java/io/apicurio/registry/services/http/RegistryExceptionMapperService.java b/app/src/main/java/io/apicurio/registry/services/http/RegistryExceptionMapperService.java index 98d23df7de..c93fd069a3 100644 --- a/app/src/main/java/io/apicurio/registry/services/http/RegistryExceptionMapperService.java +++ b/app/src/main/java/io/apicurio/registry/services/http/RegistryExceptionMapperService.java @@ -64,8 +64,8 @@ public class RegistryExceptionMapperService { map.put(ArtifactAlreadyExistsException.class, HTTP_CONFLICT); map.put(ArtifactNotFoundException.class, HTTP_NOT_FOUND); map.put(BadRequestException.class, HTTP_BAD_REQUEST); - map.put(BranchNotFoundException.class, HTTP_NOT_FOUND); - map.put(BranchVersionAlreadyExistsException.class, HTTP_CONFLICT); + map.put(ArtifactBranchNotFoundException.class, HTTP_NOT_FOUND); + map.put(ArtifactBranchAlreadyContainsVersionException.class, HTTP_CONFLICT); map.put(ConfigPropertyNotFoundException.class, HTTP_NOT_FOUND); map.put(ConflictException.class, HTTP_CONFLICT); map.put(ContentNotFoundException.class, HTTP_NOT_FOUND); diff --git a/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java index 9b99b8dd42..59c3814493 100644 --- a/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java @@ -3,6 +3,7 @@ import io.apicurio.common.apps.config.DynamicConfigPropertyDto; import io.apicurio.common.apps.config.DynamicConfigStorage; import io.apicurio.registry.content.ContentHandle; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.dto.*; import io.apicurio.registry.storage.error.*; import io.apicurio.registry.storage.impexp.EntityInputStream; @@ -870,7 +871,7 @@ CommentDto createArtifactVersionCommentRaw(String groupId, String artifactId, St void importArtifactRule(ArtifactRuleEntity entity); - void importArtifactBranch(ArtifactVersionBranchEntity entity); + void importArtifactBranch(ArtifactBranchEntity entity); boolean isContentExists(String contentHash) throws RegistryStorageException; @@ -904,31 +905,40 @@ ArtifactMetaDataDto createArtifactWithMetadata(String groupId, String artifactId /** - * @return map from a branch to a sorted list of GAVs, leaf (latest) version first. + * @return map from an artifact branch to a sorted list of GAVs, branch tip (latest) version first. */ Map> getArtifactBranches(GA ga); /** - * @return sorted list of GAVs, leaf (latest) version first. + * @return sorted list of GAVs, branch tip (latest) version first. */ List getArtifactBranch(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior); /** - * Add a version to the artifact branch. The branch is created if it does not exist. The version becomes a new leaf (latest). + * Add a version to the artifact branch. The branch is created if it does not exist. The version becomes a new branch tip (latest). + * Not supported for the "latest" branch. */ void createOrUpdateArtifactBranch(GAV gav, BranchId branchId); /** - * @return GAV identifier of the leaf (latest) version in the artifact branch. + * Replace the content of the artifact branch with a new sequence of versions. + * Not supported for the "latest" branch. */ - GAV getArtifactBranchLeaf(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior); + void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions); + + + /** + * @return GAV identifier of the branch tip (latest) version in the artifact branch. + */ + GAV getArtifactBranchTip(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior); /** * Delete artifact branch. + * Not supported for the "latest" branch. */ void deleteArtifactBranch(GA ga, BranchId branchId); diff --git a/app/src/main/java/io/apicurio/registry/storage/decorator/ReadOnlyRegistryStorageDecorator.java b/app/src/main/java/io/apicurio/registry/storage/decorator/ReadOnlyRegistryStorageDecorator.java index d38432d8fe..af6224be22 100644 --- a/app/src/main/java/io/apicurio/registry/storage/decorator/ReadOnlyRegistryStorageDecorator.java +++ b/app/src/main/java/io/apicurio/registry/storage/decorator/ReadOnlyRegistryStorageDecorator.java @@ -4,6 +4,7 @@ import io.apicurio.common.apps.config.DynamicConfigPropertyDto; import io.apicurio.common.apps.config.Info; import io.apicurio.registry.content.ContentHandle; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.RegistryStorage; import io.apicurio.registry.storage.dto.*; import io.apicurio.registry.storage.error.*; @@ -423,7 +424,7 @@ public void importArtifactRule(ArtifactRuleEntity entity) { @Override - public void importArtifactBranch(ArtifactVersionBranchEntity entity) { + public void importArtifactBranch(ArtifactBranchEntity entity) { checkReadOnly(); delegate.importArtifactBranch(entity); } @@ -487,6 +488,13 @@ public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { } + @Override + public void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions) { + checkReadOnly(); + delegate.createOrReplaceArtifactBranch(ga, branchId, versions); + } + + @Override public void deleteArtifactBranch(GA ga, BranchId branchId) { checkReadOnly(); diff --git a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorBase.java b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorBase.java index 2d6ba638bd..b527c84758 100644 --- a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorBase.java +++ b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorBase.java @@ -2,6 +2,7 @@ import io.apicurio.common.apps.config.DynamicConfigPropertyDto; import io.apicurio.registry.content.ContentHandle; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.dto.*; import io.apicurio.registry.storage.error.*; import io.apicurio.registry.storage.impexp.EntityInputStream; @@ -339,7 +340,7 @@ public void importArtifactRule(ArtifactRuleEntity entity) { @Override - public void importArtifactBranch(ArtifactVersionBranchEntity entity) { + public void importArtifactBranch(ArtifactBranchEntity entity) { delegate.importArtifactBranch(entity); } @@ -395,6 +396,12 @@ public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { } + @Override + public void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions) { + delegate.createOrReplaceArtifactBranch(ga, branchId, versions); + } + + @Override public void deleteArtifactBranch(GA ga, BranchId branchId) { delegate.deleteArtifactBranch(ga, branchId); diff --git a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java index 82a251c960..7667d73f38 100644 --- a/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java +++ b/app/src/main/java/io/apicurio/registry/storage/decorator/RegistryStorageDecoratorReadOnlyBase.java @@ -390,8 +390,8 @@ public List getArtifactVersions(String groupId, String artifactId, Artif @Override - public GAV getArtifactBranchLeaf(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { - return delegate.getArtifactBranchLeaf(ga, branchId, behavior); + public GAV getArtifactBranchTip(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { + return delegate.getArtifactBranchTip(ga, branchId, behavior); } diff --git a/app/src/main/java/io/apicurio/registry/storage/dto/BranchDto.java b/app/src/main/java/io/apicurio/registry/storage/dto/ArtifactBranchDto.java similarity index 86% rename from app/src/main/java/io/apicurio/registry/storage/dto/BranchDto.java rename to app/src/main/java/io/apicurio/registry/storage/dto/ArtifactBranchDto.java index 9a6ecfda6b..f90938c83d 100644 --- a/app/src/main/java/io/apicurio/registry/storage/dto/BranchDto.java +++ b/app/src/main/java/io/apicurio/registry/storage/dto/ArtifactBranchDto.java @@ -10,13 +10,13 @@ @Setter @EqualsAndHashCode @ToString -public class BranchDto { +public class ArtifactBranchDto { private String groupId; private String artifactId; - private String branch; + private String branchId; private int branchOrder; diff --git a/app/src/main/java/io/apicurio/registry/storage/error/BranchVersionAlreadyExistsException.java b/app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchAlreadyContainsVersionException.java similarity index 64% rename from app/src/main/java/io/apicurio/registry/storage/error/BranchVersionAlreadyExistsException.java rename to app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchAlreadyContainsVersionException.java index 480d45a652..bd56215669 100644 --- a/app/src/main/java/io/apicurio/registry/storage/error/BranchVersionAlreadyExistsException.java +++ b/app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchAlreadyContainsVersionException.java @@ -4,7 +4,7 @@ import io.apicurio.registry.model.GAV; import lombok.Getter; -public class BranchVersionAlreadyExistsException extends AlreadyExistsException { +public class ArtifactBranchAlreadyContainsVersionException extends AlreadyExistsException { private static final long serialVersionUID = -2869727219770505486L; @@ -15,7 +15,7 @@ public class BranchVersionAlreadyExistsException extends AlreadyExistsException private final BranchId branchId; - public BranchVersionAlreadyExistsException(GAV gav, BranchId branchId) { + public ArtifactBranchAlreadyContainsVersionException(GAV gav, BranchId branchId) { super(message(gav, branchId)); this.gav = gav; this.branchId = branchId; @@ -23,6 +23,6 @@ public BranchVersionAlreadyExistsException(GAV gav, BranchId branchId) { private static String message(GAV gav, BranchId branchId) { - return "Branch '" + branchId + "' already contains version '" + gav + "'."; + return "Artifact branch '" + branchId + "' already contains version '" + gav + "'."; } } diff --git a/app/src/main/java/io/apicurio/registry/storage/error/BranchNotFoundException.java b/app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchNotFoundException.java similarity index 79% rename from app/src/main/java/io/apicurio/registry/storage/error/BranchNotFoundException.java rename to app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchNotFoundException.java index b5499e1d1f..82efebec05 100644 --- a/app/src/main/java/io/apicurio/registry/storage/error/BranchNotFoundException.java +++ b/app/src/main/java/io/apicurio/registry/storage/error/ArtifactBranchNotFoundException.java @@ -5,7 +5,7 @@ import lombok.Getter; -public class BranchNotFoundException extends NotFoundException { +public class ArtifactBranchNotFoundException extends NotFoundException { private static final long serialVersionUID = -5382272137668348037L; @@ -16,7 +16,7 @@ public class BranchNotFoundException extends NotFoundException { private final BranchId branchId; - public BranchNotFoundException(GA ga, BranchId branchId) { + public ArtifactBranchNotFoundException(GA ga, BranchId branchId) { super(message(ga, branchId)); this.ga = ga; this.branchId = branchId; diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/AbstractReadOnlyRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/AbstractReadOnlyRegistryStorage.java index 6886330774..bf124116de 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/AbstractReadOnlyRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/AbstractReadOnlyRegistryStorage.java @@ -3,6 +3,7 @@ import io.apicurio.common.apps.config.DynamicConfigPropertyDto; import io.apicurio.registry.content.ContentHandle; import io.apicurio.registry.exception.UnreachableCodeException; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.RegistryStorage; import io.apicurio.registry.storage.dto.*; import io.apicurio.registry.storage.error.RegistryStorageException; @@ -341,7 +342,7 @@ public void importArtifactRule(ArtifactRuleEntity entity) { @Override - public void importArtifactBranch(ArtifactVersionBranchEntity entity) { + public void importArtifactBranch(ArtifactBranchEntity entity) { readOnlyViolation(); } @@ -393,6 +394,12 @@ public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { } + @Override + public void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions) { + readOnlyViolation(); + } + + @Override public void deleteArtifactBranch(GA ga, BranchId branchId) { readOnlyViolation(); diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java index 6b384d5cd7..207c2796e9 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/gitops/GitOpsRegistryStorage.java @@ -502,8 +502,8 @@ public Map> getArtifactBranches(GA ga) { @Override - public GAV getArtifactBranchLeaf(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { - return proxy(storage -> storage.getArtifactBranchLeaf(ga, branchId, behavior)); + public GAV getArtifactBranchTip(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { + return proxy(storage -> storage.getArtifactBranchTip(ga, branchId, behavior)); } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlRegistryStorage.java index 10638036cc..a78e3279f3 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlRegistryStorage.java @@ -6,6 +6,10 @@ import io.apicurio.registry.metrics.StorageMetricsApply; import io.apicurio.registry.metrics.health.liveness.PersistenceExceptionLivenessApply; import io.apicurio.registry.metrics.health.readiness.PersistenceTimeoutReadinessApply; +import io.apicurio.registry.model.BranchId; +import io.apicurio.registry.model.GA; +import io.apicurio.registry.model.GAV; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.ArtifactStateExt; import io.apicurio.registry.storage.StorageEvent; import io.apicurio.registry.storage.StorageEventType; @@ -24,9 +28,6 @@ import io.apicurio.registry.storage.impl.sql.SqlUtil; import io.apicurio.registry.storage.importing.DataImporter; import io.apicurio.registry.storage.importing.SqlDataImporter; -import io.apicurio.registry.model.BranchId; -import io.apicurio.registry.model.GA; -import io.apicurio.registry.model.GAV; import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.RuleType; import io.apicurio.registry.utils.ConcurrentUtil; @@ -56,7 +57,6 @@ * An implementation of a registry artifactStore that extends the basic SQL artifactStore but federates 'write' operations * to other nodes in a cluster using a Kafka topic. As a result, all reads are performed locally but all * writes are published to a topic for consumption by all nodes. - * */ @ApplicationScoped @PersistenceExceptionLivenessApply @@ -813,8 +813,8 @@ public void importGroup(GroupEntity entity) { @Override - public void importArtifactBranch(ArtifactVersionBranchEntity entity) { - submitter.submitBranchImport(entity); + public void importArtifactBranch(ArtifactBranchEntity entity) { + submitter.submitArtifactBranchImport(entity); } @@ -900,14 +900,21 @@ public ArtifactMetaDataDto updateArtifact(String groupId, String artifactId, Str @Override public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { - var uuid = ConcurrentUtil.get(submitter.submitBranch(ActionType.CREATE_OR_UPDATE, gav, branchId)); + var uuid = ConcurrentUtil.get(submitter.submitArtifactBranch(ActionType.CREATE_OR_UPDATE, gav, branchId)); + coordinator.waitForResponse(uuid); + } + + + @Override + public void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions) { + var uuid = ConcurrentUtil.get(submitter.submitArtifactBranchCreateOrReplace(ga, branchId, versions)); coordinator.waitForResponse(uuid); } @Override public void deleteArtifactBranch(GA ga, BranchId branchId) { - var uuid = ConcurrentUtil.get(submitter.submitBranch(ActionType.DELETE, ga, branchId)); + var uuid = ConcurrentUtil.get(submitter.submitArtifactBranch(ActionType.DELETE, ga, branchId)); coordinator.waitForResponse(uuid); } } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlSubmitter.java b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlSubmitter.java index 26a7b2caca..3e5345c983 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlSubmitter.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/KafkaSqlSubmitter.java @@ -5,6 +5,7 @@ import io.apicurio.registry.model.BranchId; import io.apicurio.registry.model.GA; import io.apicurio.registry.model.GAV; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.dto.DownloadContextDto; import io.apicurio.registry.storage.dto.EditableArtifactMetaDataDto; import io.apicurio.registry.storage.dto.GroupMetaDataDto; @@ -13,7 +14,7 @@ import io.apicurio.registry.storage.impl.kafkasql.values.*; import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.RuleType; -import io.apicurio.registry.utils.impexp.ArtifactVersionBranchEntity; +import io.apicurio.registry.utils.impexp.ArtifactBranchEntity; import io.apicurio.registry.utils.kafka.ProducerActions; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; @@ -24,9 +25,12 @@ import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import static java.util.stream.Collectors.toList; + @ApplicationScoped @Logged public class KafkaSqlSubmitter { @@ -259,19 +263,25 @@ public CompletableFuture submitConfigProperty(String propertyName, ActionT /* ****************************************************************************************** * Artifact Branches * ****************************************************************************************** */ - public CompletableFuture submitBranch(ActionType action, GAV gav, BranchId branchId) { + public CompletableFuture submitArtifactBranch(ActionType action, GAV gav, BranchId branchId) { var key = ArtifactBranchKey.create(gav.getRawGroupId(), gav.getRawArtifactId(), branchId.getRawBranchId()); - var value = ArtifactBranchValue.create(action, gav.getRawVersionId(), null); + var value = ArtifactBranchValue.create(action, gav.getRawVersionId(), null, null); + return send(key, value); + } + public CompletableFuture submitArtifactBranch(ActionType action, GA ga, BranchId branchId) { + var key = ArtifactBranchKey.create(ga.getRawGroupId(), ga.getRawArtifactId(), branchId.getRawBranchId()); + var value = ArtifactBranchValue.create(action, null, null, null); return send(key, value); } - public CompletableFuture submitBranch(ActionType action, GA ga, BranchId branchId) { + public CompletableFuture submitArtifactBranchCreateOrReplace(GA ga, BranchId branchId, List versions) { var key = ArtifactBranchKey.create(ga.getRawGroupId(), ga.getRawArtifactId(), branchId.getRawBranchId()); - var value = ArtifactBranchValue.create(action, null, null); + var value = ArtifactBranchValue.create(ActionType.CREATE_OR_REPLACE, null, null, + versions.stream().map(VersionId::getRawVersionId).collect(toList())); return send(key, value); } - public CompletableFuture submitBranchImport(ArtifactVersionBranchEntity entity) { - var key = ArtifactBranchKey.create(entity.groupId, entity.artifactId, entity.branch); - var value = ArtifactBranchValue.create(ActionType.IMPORT, entity.version, entity.branchOrder); + public CompletableFuture submitArtifactBranchImport(ArtifactBranchEntity entity) { + var key = ArtifactBranchKey.create(entity.groupId, entity.artifactId, entity.branchId); + var value = ArtifactBranchValue.create(ActionType.IMPORT, entity.version, entity.branchOrder, null); return send(key, value); } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/sql/KafkaSqlSink.java b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/sql/KafkaSqlSink.java index 03ee6da723..25a55569a7 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/sql/KafkaSqlSink.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/sql/KafkaSqlSink.java @@ -7,6 +7,7 @@ import io.apicurio.registry.model.BranchId; import io.apicurio.registry.model.GA; import io.apicurio.registry.model.GAV; +import io.apicurio.registry.model.VersionId; import io.apicurio.registry.storage.dto.ArtifactOwnerDto; import io.apicurio.registry.storage.dto.GroupMetaDataDto; import io.apicurio.registry.storage.error.ArtifactAlreadyExistsException; @@ -33,6 +34,8 @@ import java.util.UUID; import java.util.function.Supplier; +import static java.util.stream.Collectors.toList; + @ApplicationScoped @Logged public class KafkaSqlSink { @@ -139,7 +142,7 @@ private Object doProcessMessage(ConsumerRecord record) case Comment: return processComment((CommentKey) key, (CommentValue) value); case ArtifactBranch: - return processBranch((ArtifactBranchKey) key, (ArtifactBranchValue) value); + return processArtifactBranch((ArtifactBranchKey) key, (ArtifactBranchValue) value); case Bootstrap: throw new UnreachableCodeException(); } @@ -572,19 +575,23 @@ private Object processComment(CommentKey key, CommentValue value) { } - private Object processBranch(ArtifactBranchKey key, ArtifactBranchValue value) { + private Object processArtifactBranch(ArtifactBranchKey key, ArtifactBranchValue value) { switch (value.getAction()) { case CREATE_OR_UPDATE: sqlStore.createOrUpdateArtifactBranch(new GAV(key.getGroupId(), key.getArtifactId(), value.getVersion()), new BranchId(key.getBranchId())); return null; + case CREATE_OR_REPLACE: + sqlStore.createOrReplaceArtifactBranch(new GA(key.getGroupId(), key.getArtifactId()), new BranchId(key.getBranchId()), + value.getVersions().stream().map(VersionId::new).collect(toList())); + return null; case DELETE: sqlStore.deleteArtifactBranch(new GA(key.getGroupId(), key.getArtifactId()), new BranchId(key.getBranchId())); return null; case IMPORT: - sqlStore.importArtifactBranch(ArtifactVersionBranchEntity.builder() + sqlStore.importArtifactBranch(ArtifactBranchEntity.builder() .groupId(key.getGroupId()) .artifactId(key.getArtifactId()) - .branch(key.getBranchId()) + .branchId(key.getBranchId()) .version(value.getVersion()) .branchOrder(value.getBranchOrder()) .build()); diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ActionType.java b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ActionType.java index ecdab7f58a..dc0c16c53d 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ActionType.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ActionType.java @@ -21,7 +21,8 @@ public enum ActionType { * Deletes ALL user data. Does not delete global data, such as log configuration. */ DELETE_ALL_USER_DATA(7), - CREATE_OR_UPDATE(8); + CREATE_OR_UPDATE(8), + CREATE_OR_REPLACE(9); private final byte ord; diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ArtifactBranchValue.java b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ArtifactBranchValue.java index 72e69e2e13..c0e9787396 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ArtifactBranchValue.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/kafkasql/values/ArtifactBranchValue.java @@ -7,6 +7,8 @@ import lombok.ToString; import lombok.experimental.SuperBuilder; +import java.util.List; + @RegisterForReflection @SuperBuilder @NoArgsConstructor @@ -14,15 +16,18 @@ @ToString public class ArtifactBranchValue extends AbstractMessageValue { - private String version; // nullable - Not used when deleting + private Integer branchOrder; // nullable - Used for imports - public static ArtifactBranchValue create(ActionType action, String version, Integer branchOrder) { + private List versions; // nullable - Used for ActionType.CREATE_OR_REPLACE + + public static ArtifactBranchValue create(ActionType action, String version, Integer branchOrder, List versions) { return ArtifactBranchValue.builder() .action(action) .version(version) .branchOrder(branchOrder) + .versions(versions) .build(); } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java index 730782be8e..4a5e1744d6 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/AbstractSqlRegistryStorage.java @@ -7,6 +7,7 @@ import io.apicurio.common.apps.core.System; import io.apicurio.registry.content.ContentHandle; import io.apicurio.registry.exception.UnreachableCodeException; +import io.apicurio.registry.model.*; import io.apicurio.registry.storage.*; import io.apicurio.registry.storage.dto.*; import io.apicurio.registry.storage.error.*; @@ -17,10 +18,6 @@ import io.apicurio.registry.storage.impl.sql.mappers.*; import io.apicurio.registry.storage.importing.DataImporter; import io.apicurio.registry.storage.importing.SqlDataImporter; -import io.apicurio.registry.model.BranchId; -import io.apicurio.registry.model.GA; -import io.apicurio.registry.model.GAV; -import io.apicurio.registry.model.GroupId; import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.RuleType; import io.apicurio.registry.util.DtoUtil; @@ -54,7 +51,6 @@ /** * A SQL implementation of the {@link RegistryStorage} interface. This impl does not * use any ORM technology - it simply uses native SQL for all operations. - * */ public abstract class AbstractSqlRegistryStorage implements RegistryStorage { @@ -464,7 +460,7 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(boolean firstVersion .bind(11, contentId) .execute(); - createOrUpdateArtifactBranch(new GAV(groupId, artifactId, finalVersion1), BranchId.LATEST); + createOrUpdateArtifactBranchRaw(new GAV(groupId, artifactId, finalVersion1), BranchId.LATEST); return null; }); @@ -498,7 +494,7 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(boolean firstVersion } var gav = getGAVByGlobalId(globalId); - createOrUpdateArtifactBranch(gav, BranchId.LATEST); + createOrUpdateArtifactBranchRaw(gav, BranchId.LATEST); return null; }); @@ -761,7 +757,7 @@ public List deleteArtifact(String groupId, String artifactId) .bind(1, artifactId) .execute(); - deleteAllBranchesInArtifact(new GA(groupId, artifactId)); + deleteAllArtifactBranchesInArtifact(new GA(groupId, artifactId)); // Delete versions handle.createUpdate(sqlStatements.deleteVersions()) @@ -810,7 +806,7 @@ public void deleteArtifacts(String groupId) throws RegistryStorageException { .bind(0, normalizeGroupId(groupId)) .execute(); - deleteAllBranchesInGroup(new GroupId(groupId)); + deleteAllArtifactBranchesInGroup(new GroupId(groupId)); // Delete versions handle.createUpdate(sqlStatements.deleteVersionsByGroupId()) @@ -855,7 +851,7 @@ public StoredArtifactDto getArtifact(String groupId, String artifactId, Artifact return handles.withHandle(handle -> { var ga = new GA(groupId, artifactId); try { - var gav = getArtifactBranchLeaf(ga, BranchId.LATEST, behavior); + var gav = getArtifactBranchTip(ga, BranchId.LATEST, behavior); return handle.createQuery(sqlStatements.selectArtifactVersionContent()) .bind(0, gav.getRawGroupId()) @@ -1012,16 +1008,16 @@ public ArtifactSearchResultsDto searchArtifacts(Set filters, Order selectTemplate.append("SELECT {{selectColumns}} ") .append("FROM artifacts a ") .append("JOIN versions v ON v.groupId = a.groupId AND v.artifactId = a.artifactId ") - .append("JOIN artifact_version_branches avb ON avb.groupId = v.groupId AND avb.artifactId = v.artifactId AND avb.branch = '") + .append("JOIN artifact_branches ab ON ab.groupId = v.groupId AND ab.artifactId = v.artifactId AND ab.branchId = '") .append(BranchId.LATEST.getRawBranchId()) - .append("' AND avb.version = v.version "); + .append("' AND ab.version = v.version "); if (hasContentFilter(filters)) { selectTemplate.append("JOIN content c ON v.contentId = c.contentId "); } // Formulate the WHERE clause for both queries - where.append("WHERE (avb.branchOrder = (SELECT MAX(branchOrder) FROM artifact_version_branches avb2 WHERE avb2.groupId = avb.groupId AND avb2.artifactId = avb.artifactId AND avb2.branch = avb.branch))"); + where.append("WHERE (ab.branchOrder = (SELECT MAX(branchOrder) FROM artifact_branches ab2 WHERE ab2.groupId = ab.groupId AND ab2.artifactId = ab.artifactId AND ab2.branchId = ab.branchId))"); for (SearchFilter filter : filters) { where.append(" AND ("); switch (filter.getType()) { @@ -1206,7 +1202,7 @@ public ArtifactMetaDataDto getArtifactMetaData(String groupId, String artifactId return handles.withHandle(handle -> { var ga = new GA(groupId, artifactId); try { - var gav = getArtifactBranchLeaf(ga, BranchId.LATEST, behavior); + var gav = getArtifactBranchTip(ga, BranchId.LATEST, behavior); return handle.createQuery(sqlStatements.selectArtifactMetaData()) .bind(0, gav.getRawGroupId()) @@ -1478,7 +1474,7 @@ public List getArtifactVersions(String groupId, String artifactId, Artif .stream() .map(GAV::getRawVersionId) .collect(toList()); - } catch (BranchNotFoundException ex) { + } catch (ArtifactBranchNotFoundException ex) { throw new ArtifactNotFoundException(groupId, artifactId); } } @@ -1604,7 +1600,7 @@ public void deleteArtifactVersion(String groupId, String artifactId, String vers .execute(); // Delete version in branches - handle.createUpdate(sqlStatements.deleteVersionInBranches()) + handle.createUpdate(sqlStatements.deleteVersionInArtifactBranches()) .bind(0, normalizeGroupId(groupId)) .bind(1, artifactId) .bind(2, version) @@ -2245,9 +2241,9 @@ public void exportData(Function handler) throws RegistryStorageExc // Export all artifact branches ///////////////////////////////// handles.withHandle(handle -> { - Stream stream = handle.createQuery(sqlStatements.exportArtifactBranches()) + Stream stream = handle.createQuery(sqlStatements.exportArtifactBranches()) .setFetchSize(50) - .map(ArtifactVersionBranchEntityMapper.instance) + .map(ArtifactBranchEntityMapper.instance) .stream(); // Process and then close the stream. try (stream) { @@ -2510,7 +2506,7 @@ public void deleteAllUserData() { handle.createUpdate(sqlStatements.deleteAllComments()) .execute(); - handle.createUpdate(sqlStatements.deleteAllBranches()) + handle.createUpdate(sqlStatements.deleteAllArtifactBranches()) .execute(); handle.createUpdate(sqlStatements.deleteAllVersions()) @@ -3241,15 +3237,15 @@ public Map> getArtifactBranches(GA ga) { return handle.createQuery(sqlStatements.selectArtifactBranches()) .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) - .map(ArtifactVersionBranchDtoMapper.instance) + .map(ArtifactBranchDtoMapper.instance) .list(); }); - var data2 = new HashMap>(); - for (BranchDto dto : data1) { - data2.compute(new BranchId(dto.getBranch()), (_ignored, v) -> { + var data2 = new HashMap>(); + for (ArtifactBranchDto dto : data1) { + data2.compute(new BranchId(dto.getBranchId()), (_ignored, v) -> { if (v == null) { - var initial = new ArrayList(); + var initial = new ArrayList(); initial.add(dto); return initial; } else { @@ -3260,10 +3256,10 @@ public Map> getArtifactBranches(GA ga) { } var data3 = new HashMap>(); - for (Entry> entry : data2.entrySet()) { + for (Entry> entry : data2.entrySet()) { data3.put(entry.getKey(), entry.getValue().stream() - .sorted(Comparator.comparingInt(BranchDto::getBranchOrder).reversed()) // Highest first - .map(BranchDto::toGAV) + .sorted(Comparator.comparingInt(ArtifactBranchDto::getBranchOrder).reversed()) // Highest first + .map(ArtifactBranchDto::toGAV) .collect(toList())); } @@ -3294,44 +3290,35 @@ public List getArtifactBranch(GA ga, BranchId branchId, ArtifactRetrievalBe .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) .bind(2, branchId.getRawBranchId()) - .map(ArtifactVersionBranchDtoMapper.instance) + .map(ArtifactBranchDtoMapper.instance) .list() .stream() - .map(BranchDto::toGAV) + .map(ArtifactBranchDto::toGAV) .collect(toList()); }); if (res.isEmpty()) { - throw new BranchNotFoundException(ga, branchId); + throw new ArtifactBranchNotFoundException(ga, branchId); } return res; } - /** - * IMPORTANT: Private methods can't be @Transactional. Callers MUST have started a transaction. - */ - private boolean doesArtifactBranchContainVersion(GAV gav, BranchId branchId) { - return handles.withHandleNoException(handle -> { - return handle.createQuery(sqlStatements.selectDoesArtifactBranchContainVersion()) - .bind(0, gav.getRawGroupId()) - .bind(1, gav.getRawArtifactId()) - .bind(2, branchId.getRawBranchId()) - .bind(3, gav.getRawVersionId()) - .mapTo(Long.class) - .findOne() - .isPresent(); - }); - } - - @Override @Transactional public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { - if (doesArtifactBranchContainVersion(gav, branchId)) { - throw new BranchVersionAlreadyExistsException(gav, branchId); + if (BranchId.LATEST.equals(branchId)) { + throw new NotAllowedException("Artifact branch 'latest' cannot be updated."); } + createOrUpdateArtifactBranchRaw(gav, branchId); + } + + + /** + * IMPORTANT: Private methods can't be @Transactional. Callers MUST have started a transaction. + */ + private void createOrUpdateArtifactBranchRaw(GAV gav, BranchId branchId) { handles.withHandleNoException(handle -> { try { handle.createUpdate(sqlStatements.insertArtifactBranch()) @@ -3355,29 +3342,68 @@ public void createOrUpdateArtifactBranch(GAV gav, BranchId branchId) { @Override @Transactional - public GAV getArtifactBranchLeaf(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { + public void createOrReplaceArtifactBranch(GA ga, BranchId branchId, List versions) { + if (BranchId.LATEST.equals(branchId)) { + throw new NotAllowedException("Artifact branch 'latest' cannot be replaced."); + } + + handles.withHandleNoException(handle -> { + + // Check that the versions actually exist before deleting + + for (VersionId versionId : versions) { + if (!isArtifactVersionExists(ga.getRawGroupIdWithNull(), ga.getRawArtifactId(), versionId.getRawVersionId())) { + throw new VersionNotFoundException(ga.getRawGroupIdWithNull(), ga.getRawArtifactId(), versionId.getRawVersionId()); + } + } + + deleteArtifactBranchRaw(ga, branchId); + + var reversed = new ArrayDeque<>(versions).descendingIterator(); + while (reversed.hasNext()) { + createOrUpdateArtifactBranch(new GAV(ga, reversed.next()), branchId); + } + + // Clean versions only *after* we successfully insert + + var gavs = handle.createQuery(sqlStatements.selectVersionsWithoutArtifactBranch()) + .bind(0, ga.getRawGroupId()) + .bind(1, ga.getRawArtifactId()) + .map(GAVMapper.instance) + .list(); + + for (GAV gav : gavs) { + deleteArtifactVersion(gav.getRawGroupIdWithNull(), gav.getRawArtifactId(), gav.getRawVersionId()); + } + }); + } + + + @Override + @Transactional + public GAV getArtifactBranchTip(GA ga, BranchId branchId, ArtifactRetrievalBehavior behavior) { return handles.withHandleNoException(handle -> { switch (behavior) { case DEFAULT: - return handle.createQuery(sqlStatements.selectArtifactBranchLeaf()) + return handle.createQuery(sqlStatements.selectArtifactBranchTip()) .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) .bind(2, branchId.getRawBranchId()) .map(GAVMapper.instance) .findOne() .orElseThrow(() -> new VersionNotFoundException(ga.getRawGroupIdWithDefaultString(), ga.getRawArtifactId(), - "")); + "")); case SKIP_DISABLED_LATEST: - return handle.createQuery(sqlStatements.selectArtifactBranchLeafNotDisabled()) + return handle.createQuery(sqlStatements.selectArtifactBranchTipNotDisabled()) .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) .bind(2, branchId.getRawBranchId()) .map(GAVMapper.instance) .findOne() .orElseThrow(() -> new VersionNotFoundException(ga.getRawGroupIdWithDefaultString(), ga.getRawArtifactId(), - "")); + "")); } throw new UnreachableCodeException(); }); @@ -3401,9 +3427,9 @@ private GAV getGAVByGlobalId(long globalId) { /** * IMPORTANT: Private methods can't be @Transactional. Callers MUST have started a transaction. */ - private void deleteAllBranchesInArtifact(GA ga) { + private void deleteAllArtifactBranchesInArtifact(GA ga) { handles.withHandleNoException(handle -> { - handle.createUpdate(sqlStatements.deleteAllBranchesInArtifact()) + handle.createUpdate(sqlStatements.deleteAllArtifactBranchesInArtifact()) .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) .execute(); @@ -3414,21 +3440,21 @@ private void deleteAllBranchesInArtifact(GA ga) { /** * IMPORTANT: Private methods can't be @Transactional. Callers MUST have started a transaction. */ - private void deleteAllBranchesInGroup(GroupId groupId) { + private void deleteAllArtifactBranchesInGroup(GroupId groupId) { handles.withHandleNoException(handle -> { - handle.createUpdate(sqlStatements.deleteAllBranchesInGroup()) + handle.createUpdate(sqlStatements.deleteAllArtifactBranchesInGroup()) .bind(0, groupId.getRawGroupId()) .execute(); }); } - @Override - @Transactional - public void deleteArtifactBranch(GA ga, BranchId branchId) { - if (BranchId.LATEST.equals(branchId)) { - throw new NotAllowedException("Artifact version branch 'latest' cannot be deleted."); - } + /** + * Delete an artifact branch without version cleanup. + *

+ * IMPORTANT: Private methods can't be @Transactional. Callers MUST have started a transaction. + */ + private void deleteArtifactBranchRaw(GA ga, BranchId branchId) { handles.withHandleNoException(handle -> { @@ -3439,10 +3465,24 @@ public void deleteArtifactBranch(GA ga, BranchId branchId) { .execute(); if (affected == 0) { - throw new BranchNotFoundException(ga, branchId); + throw new ArtifactBranchNotFoundException(ga, branchId); } + }); + } + + + @Override + @Transactional + public void deleteArtifactBranch(GA ga, BranchId branchId) { + if (BranchId.LATEST.equals(branchId)) { + throw new NotAllowedException("Artifact branch 'latest' cannot be deleted."); + } - var gavs = handle.createQuery(sqlStatements.selectVersionsWithoutBranch()) + handles.withHandleNoException(handle -> { + + deleteArtifactBranchRaw(ga, branchId); + + var gavs = handle.createQuery(sqlStatements.selectVersionsWithoutArtifactBranch()) .bind(0, ga.getRawGroupId()) .bind(1, ga.getRawArtifactId()) .map(GAVMapper.instance) @@ -3457,12 +3497,9 @@ public void deleteArtifactBranch(GA ga, BranchId branchId) { @Override @Transactional - public void importArtifactBranch(ArtifactVersionBranchEntity entity) { + public void importArtifactBranch(ArtifactBranchEntity entity) { var gav = entity.toGAV(); var branchId = entity.toBranchId(); - if (doesArtifactBranchContainVersion(gav, entity.toBranchId())) { - throw new BranchVersionAlreadyExistsException(gav, branchId); - } handles.withHandleNoException(handle -> { try { handle.createUpdate(sqlStatements.importArtifactBranch()) diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java index 8b1f9eca1d..ce4ad594ad 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/CommonSqlStatements.java @@ -720,7 +720,7 @@ public String exportGroups() { @Override public String exportArtifactBranches() { - return "SELECT * FROM artifact_version_branches avb"; + return "SELECT * FROM artifact_branches ab"; } @@ -1017,106 +1017,99 @@ public String selectGAVByGlobalId() { @Override public String selectArtifactBranches() { - return "SELECT avb.groupId, avb.artifactId, avb.branch, avb.branchOrder, avb.version FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ?"; + return "SELECT ab.groupId, ab.artifactId, ab.branchId, ab.branchOrder, ab.version FROM artifact_branches ab " + + "WHERE ab.groupId = ? AND ab.artifactId = ?"; } @Override public String selectArtifactBranchOrdered() { - return "SELECT avb.groupId, avb.artifactId, avb.branch, avb.branchOrder, avb.version FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? " + - "ORDER BY avb.branchOrder DESC"; + return "SELECT ab.groupId, ab.artifactId, ab.branchId, ab.branchOrder, ab.version FROM artifact_branches ab " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? " + + "ORDER BY ab.branchOrder DESC"; } @Override public String selectArtifactBranchOrderedNotDisabled() { - return "SELECT avb.groupId, avb.artifactId, avb.branch, avb.branchOrder, avb.version FROM artifact_version_branches avb " + - "JOIN versions v ON avb.groupId = v.groupId AND avb.artifactId = v.artifactId AND avb.version = v.version " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? AND v.state != 'DISABLED' " + - "ORDER BY avb.branchOrder DESC"; - } - - - @Override - public String selectDoesArtifactBranchContainVersion() { - return "SELECT 1 FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? AND avb.version = ? "; + return "SELECT ab.groupId, ab.artifactId, ab.branchId, ab.branchOrder, ab.version FROM artifact_branches ab " + + "JOIN versions v ON ab.groupId = v.groupId AND ab.artifactId = v.artifactId AND ab.version = v.version " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? AND v.state != 'DISABLED' " + + "ORDER BY ab.branchOrder DESC"; } @Override public String insertArtifactBranch() { // Note: Duplicated value of branchOrder is prevented by primary key - return "INSERT INTO artifact_version_branches (groupId, artifactId, branch, branchOrder, version) " + - "SELECT ?, ?, ?, COALESCE(MAX(avb.branchOrder), 0) + 1, ? FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ?"; + return "INSERT INTO artifact_branches (groupId, artifactId, branchId, branchOrder, version) " + + "SELECT ?, ?, ?, COALESCE(MAX(ab.branchOrder), 0) + 1, ? FROM artifact_branches ab " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ?"; } @Override - public String selectArtifactBranchLeaf() { - return "SELECT avb.groupId, avb.artifactId, avb.version FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? " + - "ORDER BY avb.branchOrder DESC LIMIT 1"; + public String selectArtifactBranchTip() { + return "SELECT ab.groupId, ab.artifactId, ab.version FROM artifact_branches ab " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? " + + "ORDER BY ab.branchOrder DESC LIMIT 1"; } @Override - public String selectArtifactBranchLeafNotDisabled() { - return "SELECT avb.groupId, avb.artifactId, avb.version FROM artifact_version_branches avb " + - "JOIN versions v ON avb.groupId = v.groupId AND avb.artifactId = v.artifactId AND avb.version = v.version " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? AND v.state != 'DISABLED' " + - "ORDER BY avb.branchOrder DESC LIMIT 1"; + public String selectArtifactBranchTipNotDisabled() { + return "SELECT ab.groupId, ab.artifactId, ab.version FROM artifact_branches ab " + + "JOIN versions v ON ab.groupId = v.groupId AND ab.artifactId = v.artifactId AND ab.version = v.version " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? AND v.state != 'DISABLED' " + + "ORDER BY ab.branchOrder DESC LIMIT 1"; } @Override public String deleteArtifactBranch() { - return "DELETE FROM artifact_version_branches " + - "WHERE groupId = ? AND artifactId = ? AND branch = ?"; + return "DELETE FROM artifact_branches " + + "WHERE groupId = ? AND artifactId = ? AND branchId = ?"; } @Override - public String deleteAllBranchesInArtifact() { - return "DELETE FROM artifact_version_branches " + + public String deleteAllArtifactBranchesInArtifact() { + return "DELETE FROM artifact_branches " + "WHERE groupId = ? AND artifactId = ?"; } @Override - public String deleteAllBranchesInGroup() { - return "DELETE FROM artifact_version_branches " + + public String deleteAllArtifactBranchesInGroup() { + return "DELETE FROM artifact_branches " + "WHERE groupId = ?"; } @Override - public String deleteAllBranches() { - return "DELETE FROM artifact_version_branches"; + public String deleteAllArtifactBranches() { + return "DELETE FROM artifact_branches"; } @Override - public String deleteVersionInBranches() { - return "DELETE FROM artifact_version_branches " + + public String deleteVersionInArtifactBranches() { + return "DELETE FROM artifact_branches " + "WHERE groupId = ? AND artifactId = ? AND version = ?"; } @Override - public String selectVersionsWithoutBranch() { + public String selectVersionsWithoutArtifactBranch() { return "SELECT DISTINCT v.groupId, v.artifactId, v.version FROM versions v " + - "LEFT JOIN artifact_version_branches avb ON v.groupId = avb.groupId AND v.artifactId = avb.artifactId AND v.version = avb.version " + - "WHERE v.groupId = ? AND v.artifactId = ? AND avb.branch IS NULL"; + "LEFT JOIN artifact_branches ab ON v.groupId = ab.groupId AND v.artifactId = ab.artifactId AND v.version = ab.version " + + "WHERE v.groupId = ? AND v.artifactId = ? AND ab.branchId IS NULL"; } @Override public String importArtifactBranch() { - return "INSERT INTO artifact_version_branches (groupId, artifactId, branch, branchOrder, version) " + + return "INSERT INTO artifact_branches (groupId, artifactId, branchId, branchOrder, version) " + "VALUES(?, ?, ?, ?, ?)"; } } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java index 931d35015f..dae0fad3a5 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SQLServerSqlStatements.java @@ -143,18 +143,18 @@ public String selectGroups() { @Override - public String selectArtifactBranchLeaf() { - return "SELECT avb.groupId, avb.artifactId, avb.version FROM artifact_version_branches avb " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? " + - "ORDER BY avb.branchOrder DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"; + public String selectArtifactBranchTip() { + return "SELECT ab.groupId, ab.artifactId, ab.version FROM artifact_branches ab " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? " + + "ORDER BY ab.branchOrder DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"; } @Override - public String selectArtifactBranchLeafNotDisabled() { - return "SELECT avb.groupId, avb.artifactId, avb.version FROM artifact_version_branches avb " + - "JOIN versions v ON avb.groupId = v.groupId AND avb.artifactId = v.artifactId AND avb.version = v.version " + - "WHERE avb.groupId = ? AND avb.artifactId = ? AND avb.branch = ? AND v.state != 'DISABLED' " + - "ORDER BY avb.branchOrder DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"; + public String selectArtifactBranchTipNotDisabled() { + return "SELECT ab.groupId, ab.artifactId, ab.version FROM artifact_branches ab " + + "JOIN versions v ON ab.groupId = v.groupId AND ab.artifactId = v.artifactId AND ab.version = v.version " + + "WHERE ab.groupId = ? AND ab.artifactId = ? AND ab.branchId = ? AND v.state != 'DISABLED' " + + "ORDER BY ab.branchOrder DESC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"; } } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java index fa0150f7b1..023a392cbd 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/SqlStatements.java @@ -592,23 +592,21 @@ public interface SqlStatements { String selectArtifactBranchOrderedNotDisabled(); - String selectDoesArtifactBranchContainVersion(); - String insertArtifactBranch(); - String selectArtifactBranchLeaf(); + String selectArtifactBranchTip(); - String selectArtifactBranchLeafNotDisabled(); + String selectArtifactBranchTipNotDisabled(); String deleteArtifactBranch(); - String deleteVersionInBranches(); + String deleteVersionInArtifactBranches(); - String deleteAllBranchesInArtifact(); + String deleteAllArtifactBranchesInArtifact(); - String deleteAllBranchesInGroup(); + String deleteAllArtifactBranchesInGroup(); - String deleteAllBranches(); + String deleteAllArtifactBranches(); - String selectVersionsWithoutBranch(); + String selectVersionsWithoutArtifactBranch(); } diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchDtoMapper.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchDtoMapper.java similarity index 50% rename from app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchDtoMapper.java rename to app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchDtoMapper.java index 47c1e1bdca..ca0d7ff2fb 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchDtoMapper.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchDtoMapper.java @@ -1,26 +1,26 @@ package io.apicurio.registry.storage.impl.sql.mappers; -import io.apicurio.registry.storage.dto.BranchDto; +import io.apicurio.registry.storage.dto.ArtifactBranchDto; import io.apicurio.registry.storage.impl.sql.jdb.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; -public class ArtifactVersionBranchDtoMapper implements RowMapper { +public class ArtifactBranchDtoMapper implements RowMapper { - public static final ArtifactVersionBranchDtoMapper instance = new ArtifactVersionBranchDtoMapper(); + public static final ArtifactBranchDtoMapper instance = new ArtifactBranchDtoMapper(); - private ArtifactVersionBranchDtoMapper() { + private ArtifactBranchDtoMapper() { } @Override - public BranchDto map(ResultSet rs) throws SQLException { - return BranchDto.builder() + public ArtifactBranchDto map(ResultSet rs) throws SQLException { + return ArtifactBranchDto.builder() .groupId(rs.getString("groupId")) .artifactId(rs.getString("artifactId")) - .branch(rs.getString("branch")) + .branchId(rs.getString("branchId")) .branchOrder(rs.getInt("branchOrder")) .version(rs.getString("version")) .build(); diff --git a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchEntityMapper.java b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchEntityMapper.java similarity index 50% rename from app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchEntityMapper.java rename to app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchEntityMapper.java index 3108bda87e..f46da07f7c 100644 --- a/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactVersionBranchEntityMapper.java +++ b/app/src/main/java/io/apicurio/registry/storage/impl/sql/mappers/ArtifactBranchEntityMapper.java @@ -2,26 +2,26 @@ import io.apicurio.registry.storage.impl.sql.SqlUtil; import io.apicurio.registry.storage.impl.sql.jdb.RowMapper; -import io.apicurio.registry.utils.impexp.ArtifactVersionBranchEntity; +import io.apicurio.registry.utils.impexp.ArtifactBranchEntity; import java.sql.ResultSet; import java.sql.SQLException; -public class ArtifactVersionBranchEntityMapper implements RowMapper { +public class ArtifactBranchEntityMapper implements RowMapper { - public static final ArtifactVersionBranchEntityMapper instance = new ArtifactVersionBranchEntityMapper(); + public static final ArtifactBranchEntityMapper instance = new ArtifactBranchEntityMapper(); - private ArtifactVersionBranchEntityMapper() { + private ArtifactBranchEntityMapper() { } @Override - public ArtifactVersionBranchEntity map(ResultSet rs) throws SQLException { - return ArtifactVersionBranchEntity.builder() + public ArtifactBranchEntity map(ResultSet rs) throws SQLException { + return ArtifactBranchEntity.builder() .groupId(SqlUtil.denormalizeGroupId(rs.getString("groupId"))) .artifactId(rs.getString("artifactId")) - .branch(rs.getString("branch")) + .branchId(rs.getString("branchId")) .branchOrder(rs.getInt("branchOrder")) .version(rs.getString("version")) .build(); diff --git a/app/src/main/java/io/apicurio/registry/storage/importing/AbstractDataImporter.java b/app/src/main/java/io/apicurio/registry/storage/importing/AbstractDataImporter.java index 1178163e5b..44bd7e76f7 100644 --- a/app/src/main/java/io/apicurio/registry/storage/importing/AbstractDataImporter.java +++ b/app/src/main/java/io/apicurio/registry/storage/importing/AbstractDataImporter.java @@ -36,8 +36,8 @@ public void importEntity(Entity entity) { case Comment: importComment((CommentEntity) entity); break; - case ArtifactVersionBranch: - importArtifactBranch((ArtifactVersionBranchEntity) entity); + case ArtifactBranch: + importArtifactBranch((ArtifactBranchEntity) entity); break; case Manifest: ManifestEntity manifest = (ManifestEntity) entity; @@ -67,5 +67,5 @@ public void importEntity(Entity entity) { protected abstract void importGroup(GroupEntity entity); - protected abstract void importArtifactBranch(ArtifactVersionBranchEntity entity); + protected abstract void importArtifactBranch(ArtifactBranchEntity entity); } diff --git a/app/src/main/java/io/apicurio/registry/storage/importing/SqlDataImporter.java b/app/src/main/java/io/apicurio/registry/storage/importing/SqlDataImporter.java index f276cb7fc8..718fe961f6 100644 --- a/app/src/main/java/io/apicurio/registry/storage/importing/SqlDataImporter.java +++ b/app/src/main/java/io/apicurio/registry/storage/importing/SqlDataImporter.java @@ -40,7 +40,7 @@ public class SqlDataImporter extends AbstractDataImporter { // To keep track of which versions have been imported private final Set gavDone = new HashSet<>(); - private final Map> branchesWaitingForVersion = new HashMap<>(); + private final Map> artifactBranchesWaitingForVersion = new HashMap<>(); public SqlDataImporter(Logger logger, RegistryStorageContentUtils utils, RegistryStorage storage, boolean preserveGlobalId, boolean preserveContentId) { @@ -97,9 +97,9 @@ public void importArtifactVersion(ArtifactVersionEntity entity) { waitingForVersion.removeAll(commentsToImport); // Import branches waiting for version - branchesWaitingForVersion.computeIfAbsent(gav, _ignored -> List.of()) + artifactBranchesWaitingForVersion.computeIfAbsent(gav, _ignored -> List.of()) .forEach(this::importEntity); - branchesWaitingForVersion.remove(gav); + artifactBranchesWaitingForVersion.remove(gav); } catch (VersionAlreadyExistsException ex) { if (ex.getGlobalId() != null) { @@ -195,12 +195,12 @@ public void importComment(CommentEntity entity) { @Override - protected void importArtifactBranch(ArtifactVersionBranchEntity entity) { + protected void importArtifactBranch(ArtifactBranchEntity entity) { try { var gav = entity.toGAV(); if (!gavDone.contains(gav)) { // The version hasn't been imported yet. Need to wait for it. - branchesWaitingForVersion.computeIfAbsent(gav, _ignored -> new ArrayList<>()) + artifactBranchesWaitingForVersion.computeIfAbsent(gav, _ignored -> new ArrayList<>()) .add(entity); } else { storage.importArtifactBranch(entity); diff --git a/app/src/main/resources-unfiltered/META-INF/resources/api-specifications/registry/v3/openapi.json b/app/src/main/resources-unfiltered/META-INF/resources/api-specifications/registry/v3/openapi.json index 3ffe086d52..c58769040f 100644 --- a/app/src/main/resources-unfiltered/META-INF/resources/api-specifications/registry/v3/openapi.json +++ b/app/src/main/resources-unfiltered/META-INF/resources/api-specifications/registry/v3/openapi.json @@ -647,7 +647,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -682,6 +682,9 @@ }, "description": "The artifact version's metadata." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -711,6 +714,9 @@ "204": { "description": "The artifact version's metadata was successfully updated." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -730,6 +736,9 @@ "204": { "description": "The artifact version's user-editable metadata was successfully deleted." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -744,7 +753,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -762,9 +771,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -791,6 +800,9 @@ "200": { "$ref": "#/components/responses/ArtifactContent" }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -810,6 +822,9 @@ "204": { "description": "The artifact version was successfully deleted." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -827,7 +842,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -845,9 +860,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -891,7 +906,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -909,9 +924,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -1003,7 +1018,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1107,7 +1122,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1446,6 +1461,9 @@ }, "description": "List of all the artifact references for this artifact." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -1460,7 +1478,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1478,9 +1496,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -1639,225 +1657,6 @@ "description": "This operation retrieves the list of limitations on used resources, that are applied on the current instance of Registry." } }, - "/groups/{groupId}/artifacts/{artifactId}": { - "summary": "Manage a single artifact.", - "get": { - "tags": [ - "Artifacts" - ], - "parameters": [ - { - "name": "references", - "description": "Allows the user to specify how references in the content should be treated.", - "schema": { - "$ref": "#/components/schemas/HandleReferencesType" - }, - "in": "query", - "required": false - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getLatestArtifact", - "summary": "Get latest artifact", - "description": "Returns the latest version of the artifact in its raw form. The `Content-Type` of the\nresponse depends on the artifact type. In most cases, this is `application/json`, but \nfor some types it may be different (for example, `PROTOBUF`).\nIf the latest version of the artifact is marked as `DISABLED`, the next available non-disabled version will be used.\n\nThis operation may fail for one of the following reasons:\n\n* No artifact with this `artifactId` exists or all versions are `DISABLED` (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "put": { - "requestBody": { - "description": "The new content of the artifact being updated. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/FileContent" - }, - "examples": { - "OpenAPI Example": { - "value": { - "openapi": "3.0.2", - "info": { - "title": "Empty API", - "version": "1.0.7", - "description": "An example API design using OpenAPI." - }, - "paths": { - "/widgets": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "description": "All widgets" - } - }, - "summary": "Get widgets" - } - } - }, - "components": { - "schemas": { - "Widget": { - "title": "Root Type for Widget", - "description": "A sample data type.", - "type": "object", - "properties": { - "property-1": { - "type": "string" - }, - "property-2": { - "type": "boolean" - } - }, - "example": { - "property-1": "value1", - "property-2": true - } - } - } - } - } - } - } - }, - "application/create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - }, - "application/vnd.create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - } - }, - "required": true - }, - "tags": [ - "Artifacts" - ], - "parameters": [ - { - "name": "X-Registry-Version", - "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. If not provided, the server will assign a version number\nautomatically.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "header" - }, - { - "name": "X-Registry-Name", - "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Name-Encoded", - "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Description", - "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Description-Encoded", - "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactDescription" - }, - "in": "header" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ArtifactMetaData" - } - } - }, - "description": "When successful, returns the updated artifact metadata." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "updateArtifact", - "summary": "Update artifact", - "description": "Updates an artifact by uploading new content. The body of the request can\nbe the raw content of the artifact or a JSON object containing both the raw content and\na set of references to other artifacts.. This is typically in JSON format for *most*\nof the supported types, but may be in another format for a few (for example, `PROTOBUF`).\nThe type of the content should be compatible with the artifact's type (it would be\nan error to update an `AVRO` artifact with new `OPENAPI` content, for example).\n\nThe update could fail for a number of reasons including:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n\nWhen successful, this creates a new version of the artifact, making it the most recent\n(and therefore official) version of the artifact." - }, - "delete": { - "tags": [ - "Artifacts" - ], - "responses": { - "204": { - "description": "Returned when the artifact was successfully deleted." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "deleteArtifact", - "summary": "Delete artifact", - "description": "Deletes an artifact completely, resulting in all versions of the artifact also being\ndeleted. This may fail for one of the following reasons:\n\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)" - }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" - }, - "in": "path", - "required": true - }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, "/groups/{groupId}/artifacts": { "summary": "Manage the collection of artifacts within a single group in the registry.", "get": { @@ -2090,6 +1889,17 @@ "type": "string" }, "in": "header" + }, + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" } ], "responses": { @@ -2136,7 +1946,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2183,7 +1993,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2201,237 +2011,11 @@ } ] }, - "/groups/{groupId}/artifacts/{artifactId}/versions": { - "summary": "Manage all the versions of an artifact in the registry.", + "/groups/{groupId}/artifacts/{artifactId}/owner": { + "summary": "Manage the ownership of a single artifact.", "get": { "tags": [ - "Versions" - ], - "parameters": [ - { - "name": "offset", - "description": "The number of versions to skip before starting to collect the result set. Defaults to 0.", - "schema": { - "type": "integer" - }, - "in": "query", - "required": false - }, - { - "name": "limit", - "description": "The number of versions to return. Defaults to 20.", - "schema": { - "type": "integer" - }, - "in": "query", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionSearchResults" - }, - "examples": { - "All Versions": { - "value": [ - 5, - 6, - 10, - 103 - ] - } - } - } - }, - "description": "List of all artifact versions." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "listArtifactVersions", - "summary": "List artifact versions", - "description": "Returns a list of all versions of the artifact. The result set is paged.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "post": { - "requestBody": { - "description": "The content of the artifact version being created or the content and a set of references to other artifacts. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/FileContent" - }, - "examples": { - "OpenAPI Example": { - "value": { - "openapi": "3.0.2", - "info": { - "title": "Empty API", - "version": "1.0.7", - "description": "An example API design using OpenAPI." - }, - "paths": { - "/widgets": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "description": "All widgets" - } - }, - "summary": "Get widgets" - } - } - }, - "components": { - "schemas": { - "Widget": { - "title": "Root Type for Widget", - "description": "A sample data type.", - "type": "object", - "properties": { - "property-1": { - "type": "string" - }, - "property-2": { - "type": "boolean" - } - }, - "example": { - "property-1": "value1", - "property-2": true - } - } - } - } - } - } - } - }, - "application/create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - }, - "application/vnd.create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - } - }, - "required": true - }, - "tags": [ - "Versions" - ], - "parameters": [ - { - "name": "X-Registry-Version", - "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. It must be unique within the artifact. If this is not\nprovided, the server will generate a new, unique version number for this new updated content.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "header" - }, - { - "name": "X-Registry-Name", - "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Description", - "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Description-Encoded", - "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Name-Encoded", - "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactName" - }, - "in": "header" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionMetaData" - } - } - }, - "description": "The artifact version was successfully created." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/RuleViolationConflict" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "createArtifactVersion", - "summary": "Create artifact version", - "description": "Creates a new version of the artifact by uploading new content. The configured rules for\nthe artifact are applied, and if they all pass, the new content is added as the most recent \nversion of the artifact. If any of the rules fail, an error is returned.\n\nThe body of the request can be the raw content of the new artifact version, or the raw content \nand a set of references pointing to other artifacts, and the type\nof that content should match the artifact's type (for example if the artifact type is `AVRO`\nthen the content of the request should be an Apache Avro document).\n\nThis operation can fail for the following reasons:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" - }, - "in": "path", - "required": true - }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, - "/groups/{groupId}/artifacts/{artifactId}/owner": { - "summary": "Manage the ownership of a single artifact.", - "get": { - "tags": [ - "Metadata" + "Metadata" ], "responses": { "200": { @@ -2487,7 +2071,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2555,7 +2139,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2833,7 +2417,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2871,6 +2455,9 @@ }, "description": "List of all the comments for this artifact." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2907,6 +2494,9 @@ }, "description": "The comment was successfully created." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2921,7 +2511,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2939,9 +2529,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -2959,49 +2549,659 @@ } } }, - "required": true - }, - "tags": [ - "Versions" - ], - "responses": { - "204": { - "description": "The value of the comment was successfully changed." + "required": true + }, + "tags": [ + "Versions" + ], + "responses": { + "204": { + "description": "The value of the comment was successfully changed." + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "updateArtifactVersionComment", + "summary": "Update a comment", + "description": "Updates the value of a single comment in an artifact version. Only the owner of the\ncomment can modify it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "delete": { + "tags": [ + "Versions" + ], + "responses": { + "204": { + "description": "The comment was successfully deleted." + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "deleteArtifactVersionComment", + "summary": "Delete a single comment", + "description": "Deletes a single comment in an artifact version. Only the owner of the\ncomment can delete it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + }, + { + "name": "versionExpression", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + }, + { + "name": "commentId", + "description": "The unique identifier of a single comment.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + } + ] + }, + "/ids/contentIds/{contentId}/": { + "summary": "Access artifact content utilizing the unique content identifier for that content.", + "get": { + "tags": [ + "Artifacts" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getContentById", + "summary": "Get artifact content by ID", + "description": "Gets the content for an artifact version in the registry using the unique content\nidentifier for that content. This content ID may be shared by multiple artifact\nversions in the case where the artifact versions are identical.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "contentId", + "description": "Global identifier for a single artifact content.", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + } + ] + }, + "/ids/contentHashes/{contentHash}/": { + "summary": "Access artifact content utilizing the SHA-256 hash of the content.", + "get": { + "tags": [ + "Artifacts" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getContentByHash", + "summary": "Get artifact content by SHA-256 hash", + "description": "Gets the content for an artifact version in the registry using the \nSHA-256 hash of the content. This content hash may be shared by multiple artifact\nversions in the case where the artifact versions have identical content.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentHash` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "contentHash", + "description": "SHA-256 content hash for a single artifact content.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}/branches": { + "summary": "Manage branches of an artifact.", + "get": { + "tags": [ + "Branches" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArtifactBranch" + } + } + } + }, + "description": "List of all artifact versions." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "listArtifactBranches", + "summary": "List artifact branches", + "description": "Returns a list of all branches in the artifact. Each branch is a list of version identifiers,\nordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}": { + "summary": "Manage a single artifact.", + "get": { + "tags": [ + "Artifacts" + ], + "parameters": [ + { + "name": "references", + "description": "Allows the user to specify how references in the content should be treated.", + "schema": { + "$ref": "#/components/schemas/HandleReferencesType" + }, + "in": "query", + "required": false + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getLatestArtifact", + "summary": "Get latest artifact", + "description": "Returns the latest version of the artifact in its raw form. The `Content-Type` of the\nresponse depends on the artifact type. In most cases, this is `application/json`, but \nfor some types it may be different (for example, `PROTOBUF`).\nIf the latest version of the artifact is marked as `DISABLED`, the next available non-disabled version will be used.\n\nThis operation may fail for one of the following reasons:\n\n* No artifact with this `artifactId` exists or all versions are `DISABLED` (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "put": { + "requestBody": { + "description": "The new content of the artifact being updated. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/FileContent" + }, + "examples": { + "OpenAPI Example": { + "value": { + "openapi": "3.0.2", + "info": { + "title": "Empty API", + "version": "1.0.7", + "description": "An example API design using OpenAPI." + }, + "paths": { + "/widgets": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "All widgets" + } + }, + "summary": "Get widgets" + } + } + }, + "components": { + "schemas": { + "Widget": { + "title": "Root Type for Widget", + "description": "A sample data type.", + "type": "object", + "properties": { + "property-1": { + "type": "string" + }, + "property-2": { + "type": "boolean" + } + }, + "example": { + "property-1": "value1", + "property-2": true + } + } + } + } + } + } + } + }, + "application/create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + }, + "application/vnd.create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + } + }, + "required": true + }, + "tags": [ + "Artifacts" + ], + "parameters": [ + { + "name": "X-Registry-Version", + "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. If not provided, the server will assign a version number\nautomatically.", + "schema": { + "$ref": "#/components/schemas/Version" + }, + "in": "header" + }, + { + "name": "X-Registry-Name", + "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Name-Encoded", + "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Description", + "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Description-Encoded", + "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactMetaData" + } + } + }, + "description": "When successful, returns the updated artifact metadata." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "updateArtifact", + "summary": "Update artifact", + "description": "Updates an artifact by uploading new content. The body of the request can\nbe the raw content of the artifact or a JSON object containing both the raw content and\na set of references to other artifacts.. This is typically in JSON format for *most*\nof the supported types, but may be in another format for a few (for example, `PROTOBUF`).\nThe type of the content should be compatible with the artifact's type (it would be\nan error to update an `AVRO` artifact with new `OPENAPI` content, for example).\n\nThe update could fail for a number of reasons including:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n\nWhen successful, this creates a new version of the artifact, making it the most recent\n(and therefore official) version of the artifact." + }, + "delete": { + "tags": [ + "Artifacts" + ], + "responses": { + "204": { + "description": "Returned when the artifact was successfully deleted." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "deleteArtifact", + "summary": "Delete artifact", + "description": "Deletes an artifact completely, resulting in all versions of the artifact also being\ndeleted. This may fail for one of the following reasons:\n\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}/versions": { + "summary": "Manage all the versions of an artifact in the registry.", + "get": { + "tags": [ + "Versions" + ], + "parameters": [ + { + "name": "offset", + "description": "The number of versions to skip before starting to collect the result set. Defaults to 0.", + "schema": { + "type": "integer" + }, + "in": "query", + "required": false + }, + { + "name": "limit", + "description": "The number of versions to return. Defaults to 20.", + "schema": { + "type": "integer" + }, + "in": "query", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionSearchResults" + }, + "examples": { + "All Versions": { + "value": [ + 5, + 6, + 10, + 103 + ] + } + } + } + }, + "description": "List of all artifact versions." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "listArtifactVersions", + "summary": "List artifact versions", + "description": "Returns a list of all versions of the artifact. The result set is paged.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "post": { + "requestBody": { + "description": "The content of the artifact version being created or the content and a set of references to other artifacts. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/FileContent" + }, + "examples": { + "OpenAPI Example": { + "value": { + "openapi": "3.0.2", + "info": { + "title": "Empty API", + "version": "1.0.7", + "description": "An example API design using OpenAPI." + }, + "paths": { + "/widgets": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "All widgets" + } + }, + "summary": "Get widgets" + } + } + }, + "components": { + "schemas": { + "Widget": { + "title": "Root Type for Widget", + "description": "A sample data type.", + "type": "object", + "properties": { + "property-1": { + "type": "string" + }, + "property-2": { + "type": "boolean" + } + }, + "example": { + "property-1": "value1", + "property-2": true + } + } + } + } + } + } + } + }, + "application/create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + }, + "application/vnd.create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + } + }, + "required": true + }, + "tags": [ + "Versions" + ], + "parameters": [ + { + "name": "X-Registry-Version", + "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. It must be unique within the artifact. If this is not\nprovided, the server will generate a new, unique version number for this new updated content.", + "schema": { + "$ref": "#/components/schemas/Version" + }, + "in": "header" + }, + { + "name": "X-Registry-Name", + "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Description", + "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Description-Encoded", + "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactDescription" + }, + "in": "header" }, - "404": { - "$ref": "#/components/responses/NotFound" + { + "name": "X-Registry-Name-Encoded", + "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactName" + }, + "in": "header" }, - "500": { - "$ref": "#/components/responses/ServerError" + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" } - }, - "operationId": "updateArtifactVersionComment", - "summary": "Update a comment", - "description": "Updates the value of a single comment in an artifact version. Only the owner of the\ncomment can modify it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "delete": { - "tags": [ - "Versions" ], "responses": { - "204": { - "description": "The comment was successfully deleted." + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionMetaData" + } + } + }, + "description": "The artifact version was successfully created." }, "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/RuleViolationConflict" + }, "500": { "$ref": "#/components/responses/ServerError" } }, - "operationId": "deleteArtifactVersionComment", - "summary": "Delete a single comment", - "description": "Deletes a single comment in an artifact version. Only the owner of the\ncomment can delete it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "createArtifactVersion", + "summary": "Create artifact version", + "description": "Creates a new version of the artifact by uploading new content. The configured rules for\nthe artifact are applied, and if they all pass, the new content is added as the most recent \nversion of the artifact. If any of the rules fail, an error is returned.\n\nThe body of the request can be the raw content of the new artifact version, or the raw content \nand a set of references pointing to other artifacts, and the type\nof that content should match the artifact's type (for example if the artifact type is `AVRO`\nthen the content of the request should be an Apache Avro document).\n\nThis operation can fail for the following reasons:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -3016,96 +3216,11 @@ }, "in": "path", "required": true - }, - { - "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "path", - "required": true - }, - { - "name": "commentId", - "description": "The unique identifier of a single comment.", - "schema": { - "type": "string" - }, - "in": "path", - "required": true - } - ] - }, - "/ids/contentIds/{contentId}/": { - "summary": "Access artifact content utilizing the unique content identifier for that content.", - "get": { - "tags": [ - "Artifacts" - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getContentById", - "summary": "Get artifact content by ID", - "description": "Gets the content for an artifact version in the registry using the unique content\nidentifier for that content. This content ID may be shared by multiple artifact\nversions in the case where the artifact versions are identical.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "contentId", - "description": "Global identifier for a single artifact content.", - "schema": { - "format": "int64", - "type": "integer" - }, - "in": "path", - "required": true - } - ] - }, - "/ids/contentHashes/{contentHash}/": { - "summary": "Access artifact content utilizing the SHA-256 hash of the content.", - "get": { - "tags": [ - "Artifacts" - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getContentByHash", - "summary": "Get artifact content by SHA-256 hash", - "description": "Gets the content for an artifact version in the registry using the \nSHA-256 hash of the content. This content hash may be shared by multiple artifact\nversions in the case where the artifact versions have identical content.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentHash` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "contentHash", - "description": "SHA-256 content hash for a single artifact content.", - "schema": { - "type": "string" - }, - "in": "path", - "required": true } ] }, - "/groups/{groupId}/artifacts/{artifactId}/branches": { - "summary": "Manage branches of an artifact.", + "/groups/{groupId}/artifacts/{artifactId}/branches/{branchId}": { + "summary": "Manage a single artifact branch.", "get": { "tags": [ "Branches" @@ -3115,11 +3230,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Branches" + "$ref": "#/components/schemas/ArtifactBranch" } } }, - "description": "List of all artifact versions." + "description": "List of versions in an artifact branch." }, "404": { "$ref": "#/components/responses/NotFound" @@ -3128,34 +3243,21 @@ "$ref": "#/components/responses/ServerError" } }, - "operationId": "listArtifactBranches", - "summary": "List artifact branches", - "description": "Returns a list of all branches in the artifact. Each branch is a list of version identifiers,\nordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "getArtifactBranch", + "summary": "List versions in an artifact branch", + "description": "Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" + "put": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactBranch" + } + } }, - "in": "path", "required": true }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, - "/groups/{groupId}/artifacts/{artifactId}/branches/{branchId}": { - "summary": "Manage a single artifact branch.", - "get": { "tags": [ "Branches" ], @@ -3164,10 +3266,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Gav" - } + "$ref": "#/components/schemas/ArtifactBranch" } } }, @@ -3176,13 +3275,16 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/Conflict" + }, "500": { "$ref": "#/components/responses/ServerError" } }, - "operationId": "getArtifactBranch", - "summary": "List versions in an artifact branch", - "description": "Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "createOrReplaceArtifactBranch", + "summary": "Replace the sequence of versions contained in an artifact branch. Branch is created if it does not exist.", + "description": "Replace the sequence of versions contained in an artifact branch. Branch is created if it does not exist. This operation is equivalent to deleting the artifact branch and adding each version in order to a new branch with the same name. This operation can be used to remove one or more versions from the branch. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Request contains duplicate versions. Artifact branches are append-only, cycles and history rewrites, except by this operation, are not supported. (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "post": { "requestBody": { @@ -3203,10 +3305,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Gav" - } + "$ref": "#/components/schemas/ArtifactBranch" } } }, @@ -3224,7 +3323,7 @@ }, "operationId": "createOrUpdateArtifactBranch", "summary": "Add a new version to an artifact branch. Branch is created if it does not exist.", - "description": "Add a new version to an artifact branch. Branch is created if it does not exist. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Branch already contains given version. Branches are append-only, cycles and history rewrites are not supported. (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" + "description": "Add a new version to an artifact branch. Branch is created if it does not exist. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Branch already contains given version. Artifact branches are append-only, cycles and history rewrites, except by replacing the entire branch using createOrReplaceArtifactBranch operation, are not supported. (HTTP error `409`)\n* A server error occurred \n" }, "delete": { "tags": [ @@ -3232,7 +3331,7 @@ ], "responses": { "204": { - "description": "Branch was successfully deleted." + "description": "Artifact branch was successfully deleted." }, "404": { "$ref": "#/components/responses/NotFound" @@ -3245,13 +3344,13 @@ } }, "operationId": "deleteArtifactBranch", - "summary": "Delete artifact branch", + "summary": "Delete artifact branch.", "description": "Deletes a single branch in the artifact. Any artifact versions that are not referenced by a branch are deleted as well, however, this does not happen until deletion of the \"latest\" branch is supported.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Deletion of the \"latest\" branch is not supported (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -3269,7 +3368,7 @@ }, { "name": "branchId", - "description": "Branch ID. Must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "Artifact branch ID. Must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { "$ref": "#/components/schemas/BranchId" }, @@ -3488,21 +3587,6 @@ "context": "/info/externalDocs[url]" } }, - "GroupId": { - "description": "An ID of a single artifact group.", - "type": "string", - "example": "\"my-group\"" - }, - "ArtifactId": { - "description": "The ID of a single artifact.", - "type": "string", - "example": "\"example-artifact\"" - }, - "Version": { - "description": "A single version of an artifact. Can be provided by the client when creating a new version,\nor it can be server-generated. The value can be any string unique to the artifact, but it is\nrecommended to use a simple integer or a semver value.", - "type": "string", - "example": "\"3.1.6\"" - }, "Properties": { "description": "User-defined name-value pairs. Name and value must be strings.", "type": "object", @@ -4521,39 +4605,61 @@ ], "type": "string" }, + "ArtifactId": { + "description": "The ID of a single artifact.", + "pattern": "^.{1,512}$", + "type": "string", + "example": "\"example-artifact\"" + }, + "GroupId": { + "description": "An ID of a single artifact group.", + "pattern": "^.{1,512}$", + "type": "string", + "example": "\"my-group\"" + }, + "Version": { + "description": "A single version of an artifact. Can be provided by the client when creating a new version,\nor it can be server-generated. The value can be any string unique to the artifact, but it is\nrecommended to use a simple integer or a semver value.", + "pattern": "^[a-zA-Z0-9._\\-+]{1,256}$", + "type": "string", + "example": "\"3.1.6\"" + }, "BranchId": { "description": "The ID of a single artifact branch.", + "pattern": "^[a-zA-Z0-9._\\-+]{1,256}$", "type": "string", "example": "\"latest\"" }, - "Gav": { - "title": "Root Type for GAV", - "description": "Artifact version identifier, consisting of groupId, artifactId, and version triple.", + "ArtifactBranch": { + "title": "Root Type for ArtifactBranches", + "description": "", + "required": [ + "groupId", + "artifactId", + "branchId", + "versions" + ], "type": "object", "properties": { "groupId": { - "type": "string" + "$ref": "#/components/schemas/GroupId", + "description": "" }, "artifactId": { - "type": "string" + "$ref": "#/components/schemas/ArtifactId", + "description": "" }, - "version": { - "type": "string" + "branchId": { + "$ref": "#/components/schemas/BranchId", + "description": "" + }, + "versions": { + "description": "", + "type": "array", + "items": { + "$ref": "#/components/schemas/Version" + } } }, - "example": { - "groupId": "default", - "artifactId": "user", - "version": "1" - } - }, - "Branches": { - "title": "Root Type for Branches", - "description": "", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Gav" - }, "example": { "latest": [ { @@ -4807,4 +4913,4 @@ } ] } -} +} \ No newline at end of file diff --git a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/h2.ddl b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/h2.ddl index e5317ef0d5..4de411c772 100644 --- a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/h2.ddl +++ b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/h2.ddl @@ -44,10 +44,10 @@ CREATE HASH INDEX IDX_versions_5 ON versions(createdBy); CREATE INDEX IDX_versions_6 ON versions(createdOn); CREATE HASH INDEX IDX_versions_7 ON versions(contentId); -CREATE TABLE artifact_version_branches (groupId VARCHAR(512) NOT NULL, artifactId VARCHAR(512) NOT NULL, branch VARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version VARCHAR(256) NOT NULL); -ALTER TABLE artifact_version_branches ADD PRIMARY KEY (groupId, artifactId, branch, branchOrder); -ALTER TABLE artifact_version_branches ADD CONSTRAINT FK_artifact_version_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); -CREATE INDEX IDX_artifact_version_branches_1 ON artifact_version_branches(groupId, artifactId, branch, branchOrder); +CREATE TABLE artifact_branches (groupId VARCHAR(512) NOT NULL, artifactId VARCHAR(512) NOT NULL, branchId VARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version VARCHAR(256) NOT NULL); +ALTER TABLE artifact_branches ADD PRIMARY KEY (groupId, artifactId, branchId, branchOrder); +ALTER TABLE artifact_branches ADD CONSTRAINT FK_artifact_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); +CREATE INDEX IDX_artifact_branches_1 ON artifact_branches(groupId, artifactId, branchId, branchOrder); CREATE TABLE properties (globalId BIGINT NOT NULL, pkey VARCHAR(256) NOT NULL, pvalue VARCHAR(1024)); ALTER TABLE properties ADD CONSTRAINT FK_props_1 FOREIGN KEY (globalId) REFERENCES versions(globalId); diff --git a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/mssql.ddl b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/mssql.ddl index ac269d1952..00669c410f 100644 --- a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/mssql.ddl +++ b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/mssql.ddl @@ -45,10 +45,10 @@ CREATE INDEX IDX_versions_5 ON versions(createdBy); CREATE INDEX IDX_versions_6 ON versions(createdOn); CREATE INDEX IDX_versions_7 ON versions(contentId); -CREATE TABLE artifact_version_branches (groupId NVARCHAR(512) NOT NULL, artifactId NVARCHAR(512) NOT NULL, branch NVARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version NVARCHAR(256) NOT NULL); -ALTER TABLE artifact_version_branches ADD PRIMARY KEY (groupId, artifactId, branch, branchOrder); -ALTER TABLE artifact_version_branches ADD CONSTRAINT FK_artifact_version_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); -CREATE INDEX IDX_artifact_version_branches_1 ON artifact_version_branches(groupId, artifactId, branch, branchOrder); +CREATE TABLE artifact_branches (groupId NVARCHAR(512) NOT NULL, artifactId NVARCHAR(512) NOT NULL, branchId NVARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version NVARCHAR(256) NOT NULL); +ALTER TABLE artifact_branches ADD PRIMARY KEY (groupId, artifactId, branchId, branchOrder); +ALTER TABLE artifact_branches ADD CONSTRAINT FK_artifact_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); +CREATE INDEX IDX_artifact_branches_1 ON artifact_branches(groupId, artifactId, branchId, branchOrder); CREATE TABLE properties (globalId BIGINT NOT NULL, pkey NVARCHAR(256) NOT NULL, pvalue NVARCHAR(1024)); ALTER TABLE properties ADD CONSTRAINT FK_props_1 FOREIGN KEY (globalId) REFERENCES versions(globalId); diff --git a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/postgresql.ddl b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/postgresql.ddl index 6c2774913e..be6dccc91d 100644 --- a/app/src/main/resources/io/apicurio/registry/storage/impl/sql/postgresql.ddl +++ b/app/src/main/resources/io/apicurio/registry/storage/impl/sql/postgresql.ddl @@ -45,10 +45,10 @@ CREATE INDEX IDX_versions_5 ON versions USING HASH (createdBy); CREATE INDEX IDX_versions_6 ON versions(createdOn); CREATE INDEX IDX_versions_7 ON versions USING HASH (contentId); -CREATE TABLE artifact_version_branches (groupId VARCHAR(512) NOT NULL, artifactId VARCHAR(512) NOT NULL, branch VARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version VARCHAR(256) NOT NULL); -ALTER TABLE artifact_version_branches ADD PRIMARY KEY (groupId, artifactId, branch, branchOrder); -ALTER TABLE artifact_version_branches ADD CONSTRAINT FK_artifact_version_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); -CREATE INDEX IDX_artifact_version_branches_1 ON artifact_version_branches(groupId, artifactId, branch, branchOrder); +CREATE TABLE artifact_branches (groupId VARCHAR(512) NOT NULL, artifactId VARCHAR(512) NOT NULL, branchId VARCHAR(256) NOT NULL, branchOrder INT NOT NULL, version VARCHAR(256) NOT NULL); +ALTER TABLE artifact_branches ADD PRIMARY KEY (groupId, artifactId, branchId, branchOrder); +ALTER TABLE artifact_branches ADD CONSTRAINT FK_artifact_branches_1 FOREIGN KEY (groupId, artifactId, version) REFERENCES versions(groupId, artifactId, version); +CREATE INDEX IDX_artifact_branches_1 ON artifact_branches(groupId, artifactId, branchId, branchOrder); CREATE TABLE properties (globalId BIGINT NOT NULL, pkey VARCHAR(256) NOT NULL, pvalue VARCHAR(1024)); ALTER TABLE properties ADD CONSTRAINT FK_props_1 FOREIGN KEY (globalId) REFERENCES versions(globalId); diff --git a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java index e1c3d18645..b72a93d81f 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/rest/v3/GroupsResourceTest.java @@ -1,49 +1,8 @@ package io.apicurio.registry.noprofile.rest.v3; -import static io.restassured.RestAssured.given; -import static java.net.HttpURLConnection.HTTP_NO_CONTENT; -import static java.net.HttpURLConnection.HTTP_OK; -import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.CoreMatchers.anything; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.equalToObject; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.apicurio.registry.rest.client.models.Error; -import org.apache.commons.codec.digest.DigestUtils; -import org.hamcrest.Matchers; -import org.jose4j.base64url.Base64; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; - import com.google.common.hash.Hashing; import io.apicurio.registry.AbstractResourceTestBase; +import io.apicurio.registry.model.BranchId; import io.apicurio.registry.model.GroupId; import io.apicurio.registry.rest.client.models.Error; import io.apicurio.registry.rest.v3.beans.*; @@ -76,16 +35,17 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import static io.restassured.RestAssured.given; import static java.net.HttpURLConnection.HTTP_NO_CONTENT; import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toSet; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.anything; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.*; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; @QuarkusTest public class GroupsResourceTest extends AbstractResourceTestBase { @@ -149,59 +109,58 @@ public void testDefaultGroup() throws Exception { } @Test - public void testCreateArtifactRule() throws Exception - { - String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testCreateArtifactRule", "testCreateArtifactRule/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); - - //Test Rule type null - Rule nullType = new Rule(); - nullType.setType(null); - nullType.setConfig("TestConfig"); - given() - .when() - .contentType(CT_JSON) - .pathParam("groupId", "testCreateArtifactRule") - .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") - .body(nullType) - .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") - .then() - .statusCode(400); - - //Test Rule config null - Rule nullConfig = new Rule(); - nullConfig.setType(RuleType.VALIDITY); - nullConfig.setConfig(null); - given() - .when() - .contentType(CT_JSON) - .pathParam("groupId", "testCreateArtifactRule") - .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") - .body(nullConfig) - .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") - .then() - .statusCode(400); - - //Test Rule config empty - Rule emptyConfig = new Rule(); - emptyConfig.setType(RuleType.VALIDITY); - emptyConfig.setConfig(""); - given() - .when() - .contentType(CT_JSON) - .pathParam("groupId", "testCreateArtifactRule") - .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") - .body(emptyConfig) - .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") - .then() - .statusCode(400); + public void testCreateArtifactRule() throws Exception { + String oaiArtifactContent = resourceToString("openapi-empty.json"); + createArtifact("testCreateArtifactRule", "testCreateArtifactRule/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); + + //Test Rule type null + Rule nullType = new Rule(); + nullType.setType(null); + nullType.setConfig("TestConfig"); + given() + .when() + .contentType(CT_JSON) + .pathParam("groupId", "testCreateArtifactRule") + .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") + .body(nullType) + .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") + .then() + .statusCode(400); + + //Test Rule config null + Rule nullConfig = new Rule(); + nullConfig.setType(RuleType.VALIDITY); + nullConfig.setConfig(null); + given() + .when() + .contentType(CT_JSON) + .pathParam("groupId", "testCreateArtifactRule") + .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") + .body(nullConfig) + .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") + .then() + .statusCode(400); + + //Test Rule config empty + Rule emptyConfig = new Rule(); + emptyConfig.setType(RuleType.VALIDITY); + emptyConfig.setConfig(""); + given() + .when() + .contentType(CT_JSON) + .pathParam("groupId", "testCreateArtifactRule") + .pathParam("artifactId", "testCreateArtifactRule/EmptyAPI/1") + .body(emptyConfig) + .post("/registry/v3/groups/{groupId}/artifacts/{artifactId}/rules") + .then() + .statusCode(400); } @Test public void testUpdateArtifactOwner() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactOwner", "testUpdateArtifactOwner/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactOwner", "testUpdateArtifactOwner/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); ArtifactOwner artifactOwner = new ArtifactOwner("newOwner"); @@ -219,7 +178,7 @@ public void testUpdateArtifactOwner() throws Exception { @Test public void testUpdateEmptyArtifactOwner() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateEmptyArtifactOwner", "testUpdateEmptyArtifactOwner/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateEmptyArtifactOwner", "testUpdateEmptyArtifactOwner/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); ArtifactOwner artifactOwner = new ArtifactOwner(""); @@ -659,7 +618,7 @@ public void testUpdateArtifact() throws Exception { @Test public void testUpdateArtifactState() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactState", "testUpdateArtifactState/EmptyAPI/1",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactState", "testUpdateArtifactState/EmptyAPI/1", ArtifactType.OPENAPI, oaiArtifactContent); UpdateState updateState = new UpdateState(); updateState.setState(ArtifactState.DEPRECATED); @@ -696,17 +655,17 @@ public void testUpdateArtifactState() throws Exception { .then() .statusCode(200) .header("X-Registry-Deprecated", "true"); - } + } @Test public void testUpdateArtifactVersionState() throws Exception { String oaiArtifactContent = resourceToString("openapi-empty.json"); - createArtifact("testUpdateArtifactVersionState", "testUpdateArtifactVersionState/EmptyAPI",ArtifactType.OPENAPI, oaiArtifactContent); + createArtifact("testUpdateArtifactVersionState", "testUpdateArtifactVersionState/EmptyAPI", ArtifactType.OPENAPI, oaiArtifactContent); UpdateState updateState = new UpdateState(); updateState.setState(ArtifactState.DEPRECATED); - // Update the artifact state to DEPRECATED. + // Update the artifact state to DEPRECATED. given() .when() .contentType(CT_JSON) @@ -741,7 +700,7 @@ public void testUpdateArtifactVersionState() throws Exception { .then() .statusCode(200) .header("X-Registry-Deprecated", "true"); - } + } @Test @DisabledIfEnvironmentVariable(named = CURRENT_ENV, matches = CURRENT_ENV_MAS_REGEX) @@ -2194,7 +2153,8 @@ public void testCreateAlreadyExistingArtifact() throws Exception { .body("createdOn", anything()) .body("version", equalTo("2")) .body("description", equalTo("An example API design using OpenAPI.")); - /*Integer globalId2 = */resp.extract().body().path("globalId"); + /*Integer globalId2 = */ + resp.extract().body().path("globalId"); // Try to create the same artifact ID with ReturnOrUpdate - should return v1 (matching content) resp = given() @@ -2890,8 +2850,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { .put(content, config -> { config.headers.add("X-Registry-Version", "2"); config.headers.add("X-Registry-ArtifactId", artifactId); - }) - ; + }); // Now try registering an artifact with an INVALID reference data = new ByteArrayInputStream(artifactContent.getBytes(StandardCharsets.UTF_8)); @@ -2907,7 +2866,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { var contentf_1 = new io.apicurio.registry.rest.client.models.ArtifactContent(); contentf_1.setContent(new String(dataf_1.readAllBytes(), StandardCharsets.UTF_8)); contentf_1.setReferences(referencesf_1); - var exception_1 = Assertions.assertThrows(Error.class, () -> { + var exception_1 = assertThrows(Error.class, () -> { clientV3 .groups() .byGroupId(GROUP) @@ -2916,13 +2875,11 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { .put(contentf_1, config -> { config.headers.add("X-Registry-Version", "2"); config.headers.add("X-Registry-ArtifactId", artifactId); - }) - ; + }); }); Assertions.assertEquals(409, exception_1.getErrorCode()); Assertions.assertEquals("RuleViolationException", exception_1.getName()); - // Now try registering an artifact with both a valid and invalid ref data = new ByteArrayInputStream(artifactContent.getBytes(StandardCharsets.UTF_8)); // valid ref @@ -2943,7 +2900,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { var contentf_2 = new io.apicurio.registry.rest.client.models.ArtifactContent(); contentf_2.setContent(new String(dataf_2.readAllBytes(), StandardCharsets.UTF_8)); contentf_2.setReferences(referencesf_2); - var exception_2 = Assertions.assertThrows(Error.class, () -> { + var exception_2 = assertThrows(Error.class, () -> { clientV3 .groups() .byGroupId(GROUP) @@ -2952,8 +2909,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { .put(contentf_2, config -> { config.headers.add("X-Registry-Version", "2"); config.headers.add("X-Registry-ArtifactId", artifactId); - }) - ; + }); }); Assertions.assertEquals(409, exception_2.getErrorCode()); Assertions.assertEquals("RuleViolationException", exception_2.getName()); @@ -2966,7 +2922,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { var contentf_3 = new io.apicurio.registry.rest.client.models.ArtifactContent(); contentf_3.setContent(new String(dataf_3.readAllBytes(), StandardCharsets.UTF_8)); contentf_3.setReferences(referencesf_3); - var exception_3 = Assertions.assertThrows(Error.class, () -> { + var exception_3 = assertThrows(Error.class, () -> { clientV3 .groups() .byGroupId(GROUP) @@ -2975,8 +2931,7 @@ public void testCreateArtifactIntegrityRuleViolation() throws Exception { .put(contentf_2, config -> { config.headers.add("X-Registry-Version", "2"); config.headers.add("X-Registry-ArtifactId", artifactId); - }) - ; + }); }); Assertions.assertEquals(409, exception_3.getErrorCode()); Assertions.assertEquals("RuleViolationException", exception_3.getName()); @@ -2994,47 +2949,528 @@ public void testGetArtifactVersionWithReferences() throws Exception { // Create the artifact that references the type List refs = Collections.singletonList( ArtifactReference.builder() - .name("./referenced-types.json#/components/schemas/Widget") - .groupId(GROUP) - .artifactId("testGetArtifactVersionWithReferences/ReferencedTypes") - .version("1") - .build()); + .name("./referenced-types.json#/components/schemas/Widget") + .groupId(GROUP) + .artifactId("testGetArtifactVersionWithReferences/ReferencedTypes") + .version("1") + .build()); createArtifactWithReferences(GROUP, "testGetArtifactVersionWithReferences/WithExternalRef", ArtifactType.OPENAPI, withExternalRefContent, refs); // Get the content of the artifact preserving external references given() - .when() - .pathParam("groupId", GROUP) - .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") - .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") - .then() - .statusCode(200) - .body("openapi", equalTo("3.0.2")) - .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", equalTo("./referenced-types.json#/components/schemas/Widget")); + .when() + .pathParam("groupId", GROUP) + .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") + .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") + .then() + .statusCode(200) + .body("openapi", equalTo("3.0.2")) + .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", equalTo("./referenced-types.json#/components/schemas/Widget")); // Get the content of the artifact rewriting external references given() - .when() - .pathParam("groupId", GROUP) - .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") - .queryParam("references", "REWRITE") - .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") - .then() - .statusCode(200) - .body("openapi", equalTo("3.0.2")) - .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", endsWith("/apis/registry/v3/groups/GroupsResourceTest/artifacts/testGetArtifactVersionWithReferences%2FReferencedTypes/versions/1?references=REWRITE#/components/schemas/Widget")); + .when() + .pathParam("groupId", GROUP) + .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") + .queryParam("references", "REWRITE") + .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") + .then() + .statusCode(200) + .body("openapi", equalTo("3.0.2")) + .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", endsWith("/apis/registry/v3/groups/GroupsResourceTest/artifacts/testGetArtifactVersionWithReferences%2FReferencedTypes/versions/1?references=REWRITE#/components/schemas/Widget")); // Get the content of the artifact inlining/dereferencing external references given() - .when() - .pathParam("groupId", GROUP) - .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") - .queryParam("references", "DEREFERENCE") - .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") - .then() - .statusCode(200) - .body("openapi", equalTo("3.0.2")) - .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", equalTo("#/components/schemas/Widget")); + .when() + .pathParam("groupId", GROUP) + .pathParam("artifactId", "testGetArtifactVersionWithReferences/WithExternalRef") + .queryParam("references", "DEREFERENCE") + .get("/registry/v3/groups/{groupId}/artifacts/{artifactId}") + .then() + .statusCode(200) + .body("openapi", equalTo("3.0.2")) + .body("paths.widgets.get.responses.200.content.json.schema.items.$ref", equalTo("#/components/schemas/Widget")); } + + @Test + public void testBranches() throws Exception { + var artifactContent1 = resourceToString("openapi-empty.json"); + var artifactContent2 = artifactContent1.replace("1.0.0", "1.1.0"); + var artifactContent3 = artifactContent1.replace("1.0.0", "1.2.0"); + var artifactContent4 = artifactContent1.replace("1.0.0", "1.3.0"); + var artifactId = UUID.randomUUID().toString(); + + // Create an artifact version, there should just be the latest branch + + var content = new io.apicurio.registry.rest.client.models.ArtifactContent(); + content.setContent(artifactContent1); + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.OPENAPI); + }); + + var branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("1")).build() + ), convert(branches)); + + // Create an artifact version, using the branch header feature + + content.setContent(artifactContent2); + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .post(content, config -> { + config.headers.add("X-Registry-ArtifactType", ArtifactType.OPENAPI); + config.headers.add("X-Registry-Artifact-Branches", "branch1"); + config.headers.add("X-Registry-Artifact-Branches", "branch2, branch3"); + }); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("2")).build() + ), convert(branches)); + + // Create another artifact version, using the branch header feature + + content.setContent(artifactContent3); + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .post(content, config -> { + config.headers.add("X-Registry-ArtifactType", ArtifactType.OPENAPI); + config.headers.add("X-Registry-Artifact-Branches", "branch1"); + config.headers.add("X-Registry-Artifact-Branches", "branch3"); + }); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("3", "2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("3", "2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3", "2")).build() + ), convert(branches)); + + // Test an endpoint to get a specific branch + + var branch = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch1") + .get(); + + assertEquals( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("3", "2")).build(), + convert(branch) + ); + + // Create an additional version, and add it manually to a branch + + content.setContent(artifactContent4); + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.OPENAPI); + }); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("3", "2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("3", "2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3", "2")).build() + ), convert(branches)); + + branch = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch2") + .post("4"); + + assertEquals( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("4", "2")).build(), + convert(branch) + ); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("4", "3", "2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("3", "2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("4", "2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3", "2")).build() + ), convert(branches)); + + // Try to replace a branch + + branch = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch1") + .put(convert(ArtifactBranch.builder().versions(List.of("4", "1")).build())); // We support omitting the repeated data + + assertEquals( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("4", "1")).build(), + convert(branch) + ); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("4", "3", "2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("4", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("4", "2")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3", "2")).build() + ), convert(branches)); + + // Adding existing version is allowed, but not recommended + + branch = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch2") + .post("2"); + + assertEquals( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch2").versions(List.of("2", "4", "2")).build(), + convert(branch) + ); + + // Failure mode: Adding a version to the latest branch is not allowed at the moment + // It could be used to "hide" a bad latest version, but it's probably better to use the artifact state feature. + // The latest branch is used in a lot of internal features, that currently do not expect duplicates or any updates. + + var error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId(BranchId.LATEST.getRawBranchId()) + .post("3"); + }); + + assertNotNull(error); + assertEquals(409, error.getErrorCode()); + assertEquals("NotAllowedException", error.getName()); + + // Failure mode: Latest branch cannot be replaced + + error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId(BranchId.LATEST.getRawBranchId()) + .put(convert(ArtifactBranch.builder().versions(List.of("4")).build())); + }); + + assertNotNull(error); + assertEquals(409, error.getErrorCode()); + assertEquals("NotAllowedException", error.getName()); + + // Smoke test version expressions, include them here since we've done some setup + + var version = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("branch=latest") + .meta() + .get(); + + assertNotNull(version); + assertEquals("4", version.getVersion()); + + version = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("branch=branch2") + .meta() + .get(); + + assertNotNull(version); + assertEquals("2", version.getVersion()); + + error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("branch=") + .meta() + .get(); + }); + + assertNotNull(error); + assertEquals(400, error.getErrorCode()); + assertEquals("ValidationException", error.getName()); + + // Delete a branch + + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch2") + .delete(); + + error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId("branch2") + .get(); + }); + + assertNotNull(error); + assertEquals(404, error.getErrorCode()); + assertEquals("ArtifactBranchNotFoundException", error.getName()); + + // Failure mode: Version expression for a deleted branch + + error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("branch=branch2") + .meta() + .get(); + }); + + assertNotNull(error); + assertEquals(404, error.getErrorCode()); + assertEquals("VersionNotFoundException", error.getName()); + + // Failure mode: Latest branch cannot be deleted + + error = assertThrows(Error.class, () -> { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .byBranchId(BranchId.LATEST.getRawBranchId()) + .delete(); + }); + + assertNotNull(error); + assertEquals(409, error.getErrorCode()); + assertEquals("NotAllowedException", error.getName()); + + // Delete a version, make sure it is removed from branches + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("4", "3", "2", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("4", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3", "2")).build() + ), convert(branches)); + + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("2") + .delete(); + + await().atMost(3, SECONDS).until(() -> { + try { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .versions() + .byVersionExpression("2") + .meta() + .get(); + return false; + } catch (Exception ignored) { + return true; + } + }); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("4", "3", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch1").versions(List.of("4", "1")).build(), + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId("branch3").versions(List.of("3")).build() + ), convert(branches)); + + // Delete the entire artifact and recreate it. Make sure branches have been cleaned up. + + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .delete(); + + await().atMost(3, SECONDS).until(() -> { + try { + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .meta() + .get(); + return false; + } catch (Exception ignored) { + return true; + } + }); + + content = new io.apicurio.registry.rest.client.models.ArtifactContent(); + content.setContent(artifactContent1); + clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .post(content, config -> { + config.headers.add("X-Registry-ArtifactId", artifactId); + config.headers.add("X-Registry-ArtifactType", ArtifactType.OPENAPI); + }); + + branches = clientV3 + .groups() + .byGroupId(GROUP) + .artifacts() + .byArtifactId(artifactId) + .branches() + .get(); + + assertEquals(Set.of( + ArtifactBranch.builder().groupId(GROUP).artifactId(artifactId).branchId(BranchId.LATEST.getRawBranchId()).versions(List.of("1")).build() + ), convert(branches)); + } + + + private static ArtifactBranch convert(io.apicurio.registry.rest.client.models.ArtifactBranch origin) { + assertNotNull(origin); + return ArtifactBranch.builder() + .groupId(origin.getGroupId()) + .artifactId(origin.getArtifactId()) + .branchId(origin.getBranchId()) + .versions(origin.getVersions()) + .build(); + } + + + private static Set convert(List origin) { + return origin + .stream() + .map(GroupsResourceTest::convert) + .collect(toSet()); + } + + + private static io.apicurio.registry.rest.client.models.ArtifactBranch convert(ArtifactBranch origin) { + assertNotNull(origin); + var res = new io.apicurio.registry.rest.client.models.ArtifactBranch(); + res.setGroupId(origin.getGroupId()); + res.setArtifactId(origin.getArtifactId()); + res.setBranchId(origin.getBranchId()); + res.setVersions(origin.getVersions()); + return res; + } } diff --git a/app/src/test/java/io/apicurio/registry/noprofile/storage/AbstractRegistryStorageTest.java b/app/src/test/java/io/apicurio/registry/noprofile/storage/AbstractRegistryStorageTest.java index 9355e4e772..b978d4dc6f 100644 --- a/app/src/test/java/io/apicurio/registry/noprofile/storage/AbstractRegistryStorageTest.java +++ b/app/src/test/java/io/apicurio/registry/noprofile/storage/AbstractRegistryStorageTest.java @@ -1073,7 +1073,7 @@ public void testComments() { @Test - public void testBranches() { + public void testArtifactBranches() { var ga = new GA(GROUP_ID, "foo"); @@ -1091,7 +1091,7 @@ public void testBranches() { var latestBranch = storage().getArtifactBranch(ga, BranchId.LATEST, DEFAULT); Assertions.assertEquals(List.of(new GAV(ga, dtoV1.getVersion())), latestBranch); - var gavV1 = storage().getArtifactBranchLeaf(ga, BranchId.LATEST, DEFAULT); + var gavV1 = storage().getArtifactBranchTip(ga, BranchId.LATEST, DEFAULT); Assertions.assertNotNull(gavV1); Assertions.assertEquals(gavV1.getRawGroupIdWithDefaultString(), dtoV1.getGroupId()); Assertions.assertEquals(gavV1.getRawArtifactId(), dtoV1.getId()); @@ -1118,13 +1118,13 @@ public void testBranches() { var otherBranch = storage().getArtifactBranch(ga, otherBranchId, DEFAULT); Assertions.assertEquals(List.of(new GAV(ga, dtoV1.getVersion())), otherBranch); - var gavV2 = storage().getArtifactBranchLeaf(ga, BranchId.LATEST, DEFAULT); + var gavV2 = storage().getArtifactBranchTip(ga, BranchId.LATEST, DEFAULT); Assertions.assertNotNull(gavV2); Assertions.assertEquals(gavV2.getRawGroupIdWithDefaultString(), dtoV2.getGroupId()); Assertions.assertEquals(gavV2.getRawArtifactId(), dtoV2.getId()); Assertions.assertEquals(gavV2.getRawVersionId(), dtoV2.getVersion()); - gavV1 = storage().getArtifactBranchLeaf(ga, otherBranchId, DEFAULT); + gavV1 = storage().getArtifactBranchTip(ga, otherBranchId, DEFAULT); Assertions.assertNotNull(gavV1); Assertions.assertEquals(gavV1.getRawGroupIdWithDefaultString(), dtoV1.getGroupId()); Assertions.assertEquals(gavV1.getRawArtifactId(), dtoV1.getId()); @@ -1139,15 +1139,15 @@ public void testBranches() { ), branches); Assertions.assertEquals(storage().getArtifactBranch(ga, BranchId.LATEST, DEFAULT), storage().getArtifactBranch(ga, otherBranchId, DEFAULT)); - Assertions.assertEquals(storage().getArtifactBranchLeaf(ga, BranchId.LATEST, DEFAULT), storage().getArtifactBranchLeaf(ga, otherBranchId, DEFAULT)); + Assertions.assertEquals(storage().getArtifactBranchTip(ga, BranchId.LATEST, DEFAULT), storage().getArtifactBranchTip(ga, otherBranchId, DEFAULT)); storage().updateArtifactState(gavV2.getRawGroupIdWithDefaultString(), gavV2.getRawArtifactId(), gavV2.getRawVersionId(), ArtifactState.DISABLED); Assertions.assertEquals(List.of(gavV1), storage().getArtifactBranch(ga, BranchId.LATEST, SKIP_DISABLED_LATEST)); - Assertions.assertEquals(gavV1, storage().getArtifactBranchLeaf(ga, BranchId.LATEST, ArtifactRetrievalBehavior.SKIP_DISABLED_LATEST)); + Assertions.assertEquals(gavV1, storage().getArtifactBranchTip(ga, BranchId.LATEST, ArtifactRetrievalBehavior.SKIP_DISABLED_LATEST)); storage().updateArtifactState(gavV2.getRawGroupIdWithDefaultString(), gavV2.getRawArtifactId(), gavV2.getRawVersionId(), ArtifactState.ENABLED); Assertions.assertEquals(List.of(gavV2, gavV1), storage().getArtifactBranch(ga, BranchId.LATEST, SKIP_DISABLED_LATEST)); - Assertions.assertEquals(gavV2, storage().getArtifactBranchLeaf(ga, BranchId.LATEST, ArtifactRetrievalBehavior.SKIP_DISABLED_LATEST)); + Assertions.assertEquals(gavV2, storage().getArtifactBranchTip(ga, BranchId.LATEST, ArtifactRetrievalBehavior.SKIP_DISABLED_LATEST)); storage().deleteArtifactVersion(gavV1.getRawGroupIdWithDefaultString(), gavV1.getRawArtifactId(), gavV1.getRawVersionId()); @@ -1156,8 +1156,8 @@ public void testBranches() { storage().deleteArtifactBranch(ga, otherBranchId); - Assertions.assertThrows(BranchNotFoundException.class, () -> storage().getArtifactBranch(ga, otherBranchId, DEFAULT)); - Assertions.assertThrows(VersionNotFoundException.class, () -> storage().getArtifactBranchLeaf(ga, otherBranchId, DEFAULT)); + Assertions.assertThrows(ArtifactBranchNotFoundException.class, () -> storage().getArtifactBranch(ga, otherBranchId, DEFAULT)); + Assertions.assertThrows(VersionNotFoundException.class, () -> storage().getArtifactBranchTip(ga, otherBranchId, DEFAULT)); Assertions.assertThrows(NotAllowedException.class, () -> storage().deleteArtifactBranch(ga, BranchId.LATEST)); } diff --git a/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java b/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java index 45594f4af8..28fbb0a107 100644 --- a/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java +++ b/app/src/test/java/io/apicurio/registry/storage/impl/readonly/ReadOnlyRegistryStorageTest.java @@ -50,6 +50,7 @@ public class ReadOnlyRegistryStorageTest { entry("createDownload1", new State(true, s -> s.createDownload(null))), entry("createGlobalRule2", new State(true, s -> s.createGlobalRule(null, null))), entry("createGroup1", new State(true, s -> s.createGroup(null))), + entry("createOrReplaceArtifactBranch3", new State(true, s -> s.createOrReplaceArtifactBranch(null, null, null))), entry("createOrUpdateArtifactBranch2", new State(true, s -> s.createOrUpdateArtifactBranch(null, null))), entry("createRoleMapping3", new State(true, s -> s.createRoleMapping(null, null, null))), entry("deleteAllExpiredDownloads0", new State(true, RegistryStorage::deleteAllExpiredDownloads)), @@ -74,7 +75,7 @@ public class ReadOnlyRegistryStorageTest { entry("getArtifactByContentId1", new State(false, s -> s.getArtifactByContentId(0))), entry("getArtifactBranch3", new State(false, s -> s.getArtifactBranch(null, null, null))), entry("getArtifactBranches1", new State(false, s -> s.getArtifactBranches(null))), - entry("getArtifactBranchLeaf3", new State(false, s -> s.getArtifactBranchLeaf(null, null, null))), + entry("getArtifactBranchTip3", new State(false, s -> s.getArtifactBranchTip(null, null, null))), entry("getArtifactContentIds2", new State(false, s -> s.getArtifactContentIds(null, null))), entry("getArtifactIds1", new State(false, s -> s.getArtifactIds(null))), entry("getArtifactMetaData1", new State(false, s -> s.getArtifactMetaData(0))), diff --git a/common/src/main/java/io/apicurio/registry/model/ArtifactId.java b/common/src/main/java/io/apicurio/registry/model/ArtifactId.java index 18afc98ac9..00bbd4acd8 100644 --- a/common/src/main/java/io/apicurio/registry/model/ArtifactId.java +++ b/common/src/main/java/io/apicurio/registry/model/ArtifactId.java @@ -8,16 +8,7 @@ @Getter @EqualsAndHashCode -public class ArtifactId { - - // TODO: Restrict artifact ID format? - // /** - // * Pattern requirements: - // * - Must not contain reserved characters ":=,<>" (see VersionExpressionParser) - // * - Must accept valid Kafka topic name - // * - Must fit in the database column - // */ - // private static final Pattern VALID_PATTERN = Pattern.compile("[A-Za-z0-9._-]{1,512}"); // TODO: UPGRADE INCOMPATIBILITY +public final class ArtifactId { private static final Pattern VALID_PATTERN = Pattern.compile(".{1,512}"); @@ -27,7 +18,7 @@ public class ArtifactId { public ArtifactId(String rawArtifactId) { if (!isValid(rawArtifactId)) { throw new ValidationException("Artifact ID '" + rawArtifactId + "' is invalid. " + - "It must consist of alphanumeric characters or '._-', and have length 1..512 (inclusive)."); + "It must have length 1..512 (inclusive)."); } this.rawArtifactId = rawArtifactId; } diff --git a/common/src/main/java/io/apicurio/registry/model/BranchId.java b/common/src/main/java/io/apicurio/registry/model/BranchId.java index 64b3398eca..ab1534e538 100644 --- a/common/src/main/java/io/apicurio/registry/model/BranchId.java +++ b/common/src/main/java/io/apicurio/registry/model/BranchId.java @@ -9,7 +9,7 @@ @Getter @EqualsAndHashCode -public class BranchId { +public final class BranchId { /** * Pattern requirements: @@ -17,7 +17,7 @@ public class BranchId { * - Must accept semver string * - Must fit in the database column */ - private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z0-9._\\-+]{1,256}"); // TODO: UPGRADE INCOMPATIBILITY + private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z0-9._\\-+]{1,256}"); public static final BranchId LATEST = new BranchId("latest"); diff --git a/common/src/main/java/io/apicurio/registry/model/GA.java b/common/src/main/java/io/apicurio/registry/model/GA.java index 22953bbcc8..23460b2f2c 100644 --- a/common/src/main/java/io/apicurio/registry/model/GA.java +++ b/common/src/main/java/io/apicurio/registry/model/GA.java @@ -8,7 +8,6 @@ @EqualsAndHashCode(callSuper = true) public class GA extends GroupId { - private final ArtifactId artifactId; diff --git a/common/src/main/java/io/apicurio/registry/model/GAV.java b/common/src/main/java/io/apicurio/registry/model/GAV.java index 73537da907..a3c9cf516f 100644 --- a/common/src/main/java/io/apicurio/registry/model/GAV.java +++ b/common/src/main/java/io/apicurio/registry/model/GAV.java @@ -5,8 +5,7 @@ @Getter @EqualsAndHashCode(callSuper = true) -public class GAV extends GA { - +public final class GAV extends GA { private final VersionId versionId; diff --git a/common/src/main/java/io/apicurio/registry/model/GroupId.java b/common/src/main/java/io/apicurio/registry/model/GroupId.java index a9533f5726..639cbe09c8 100644 --- a/common/src/main/java/io/apicurio/registry/model/GroupId.java +++ b/common/src/main/java/io/apicurio/registry/model/GroupId.java @@ -10,27 +10,23 @@ @EqualsAndHashCode public class GroupId { - private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z0-9._-]{1,512}"); // TODO: UPGRADE INCOMPATIBILITY + private static final Pattern VALID_PATTERN = Pattern.compile(".{1,512}"); private static final String DEFAULT_STRING = "default"; - public static final GroupId DEFAULT = new GroupId(); - - private final String rawGroupId; + private static final String DEFAULT_RAW_GROUP_ID = "__$GROUPID$__"; // TODO: Consider using "default" as a default group ID. + public static final GroupId DEFAULT = new GroupId(DEFAULT_RAW_GROUP_ID); - private GroupId() { - // Skips validation - this.rawGroupId = "__$GROUPID$__"; // TODO: Consider using "default" as a default group ID. - } + private final String rawGroupId; public GroupId(String rawGroupId) { if (!isValid(rawGroupId)) { throw new ValidationException("Group ID '" + rawGroupId + "' is invalid. " + - "It must consist of alphanumeric characters or '._-', and have length 1..512 (inclusive)."); + "It must have length 1..512 (inclusive)."); } - this.rawGroupId = rawGroupId == null || DEFAULT_STRING.equalsIgnoreCase(rawGroupId) ? DEFAULT.getRawGroupId() : rawGroupId; + this.rawGroupId = rawGroupId == null || DEFAULT_STRING.equalsIgnoreCase(rawGroupId) ? DEFAULT_RAW_GROUP_ID : rawGroupId; } @@ -56,6 +52,6 @@ public String toString() { public static boolean isValid(String rawGroupId) { - return rawGroupId == null || DEFAULT.getRawGroupId().equals(rawGroupId) || VALID_PATTERN.matcher(rawGroupId).matches(); + return rawGroupId == null || VALID_PATTERN.matcher(rawGroupId).matches(); } } diff --git a/common/src/main/java/io/apicurio/registry/model/VersionId.java b/common/src/main/java/io/apicurio/registry/model/VersionId.java index 9c1f43e3db..e1ae0dd5c8 100644 --- a/common/src/main/java/io/apicurio/registry/model/VersionId.java +++ b/common/src/main/java/io/apicurio/registry/model/VersionId.java @@ -24,7 +24,7 @@ public class VersionId { public VersionId(String rawVersionId) { if (!isValid(rawVersionId)) { throw new ValidationException("Version ID '" + rawVersionId + "' is invalid. " + - "It must consist of alphanumeric characters or '._-', and have length 1..256 (inclusive)."); + "It must consist of alphanumeric characters or '._-+', and have length 1..256 (inclusive)."); } this.rawVersionId = rawVersionId; } diff --git a/common/src/main/resources/META-INF/openapi.json b/common/src/main/resources/META-INF/openapi.json index 3ffe086d52..c58769040f 100644 --- a/common/src/main/resources/META-INF/openapi.json +++ b/common/src/main/resources/META-INF/openapi.json @@ -647,7 +647,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -682,6 +682,9 @@ }, "description": "The artifact version's metadata." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -711,6 +714,9 @@ "204": { "description": "The artifact version's metadata was successfully updated." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -730,6 +736,9 @@ "204": { "description": "The artifact version's user-editable metadata was successfully deleted." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -744,7 +753,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -762,9 +771,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -791,6 +800,9 @@ "200": { "$ref": "#/components/responses/ArtifactContent" }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -810,6 +822,9 @@ "204": { "description": "The artifact version was successfully deleted." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -827,7 +842,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -845,9 +860,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -891,7 +906,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -909,9 +924,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -1003,7 +1018,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1107,7 +1122,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1446,6 +1461,9 @@ }, "description": "List of all the artifact references for this artifact." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -1460,7 +1478,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -1478,9 +1496,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -1639,225 +1657,6 @@ "description": "This operation retrieves the list of limitations on used resources, that are applied on the current instance of Registry." } }, - "/groups/{groupId}/artifacts/{artifactId}": { - "summary": "Manage a single artifact.", - "get": { - "tags": [ - "Artifacts" - ], - "parameters": [ - { - "name": "references", - "description": "Allows the user to specify how references in the content should be treated.", - "schema": { - "$ref": "#/components/schemas/HandleReferencesType" - }, - "in": "query", - "required": false - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getLatestArtifact", - "summary": "Get latest artifact", - "description": "Returns the latest version of the artifact in its raw form. The `Content-Type` of the\nresponse depends on the artifact type. In most cases, this is `application/json`, but \nfor some types it may be different (for example, `PROTOBUF`).\nIf the latest version of the artifact is marked as `DISABLED`, the next available non-disabled version will be used.\n\nThis operation may fail for one of the following reasons:\n\n* No artifact with this `artifactId` exists or all versions are `DISABLED` (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "put": { - "requestBody": { - "description": "The new content of the artifact being updated. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/FileContent" - }, - "examples": { - "OpenAPI Example": { - "value": { - "openapi": "3.0.2", - "info": { - "title": "Empty API", - "version": "1.0.7", - "description": "An example API design using OpenAPI." - }, - "paths": { - "/widgets": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "description": "All widgets" - } - }, - "summary": "Get widgets" - } - } - }, - "components": { - "schemas": { - "Widget": { - "title": "Root Type for Widget", - "description": "A sample data type.", - "type": "object", - "properties": { - "property-1": { - "type": "string" - }, - "property-2": { - "type": "boolean" - } - }, - "example": { - "property-1": "value1", - "property-2": true - } - } - } - } - } - } - } - }, - "application/create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - }, - "application/vnd.create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - } - }, - "required": true - }, - "tags": [ - "Artifacts" - ], - "parameters": [ - { - "name": "X-Registry-Version", - "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. If not provided, the server will assign a version number\nautomatically.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "header" - }, - { - "name": "X-Registry-Name", - "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Name-Encoded", - "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Description", - "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Description-Encoded", - "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactDescription" - }, - "in": "header" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ArtifactMetaData" - } - } - }, - "description": "When successful, returns the updated artifact metadata." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "updateArtifact", - "summary": "Update artifact", - "description": "Updates an artifact by uploading new content. The body of the request can\nbe the raw content of the artifact or a JSON object containing both the raw content and\na set of references to other artifacts.. This is typically in JSON format for *most*\nof the supported types, but may be in another format for a few (for example, `PROTOBUF`).\nThe type of the content should be compatible with the artifact's type (it would be\nan error to update an `AVRO` artifact with new `OPENAPI` content, for example).\n\nThe update could fail for a number of reasons including:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n\nWhen successful, this creates a new version of the artifact, making it the most recent\n(and therefore official) version of the artifact." - }, - "delete": { - "tags": [ - "Artifacts" - ], - "responses": { - "204": { - "description": "Returned when the artifact was successfully deleted." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "deleteArtifact", - "summary": "Delete artifact", - "description": "Deletes an artifact completely, resulting in all versions of the artifact also being\ndeleted. This may fail for one of the following reasons:\n\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)" - }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" - }, - "in": "path", - "required": true - }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, "/groups/{groupId}/artifacts": { "summary": "Manage the collection of artifacts within a single group in the registry.", "get": { @@ -2090,6 +1889,17 @@ "type": "string" }, "in": "header" + }, + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" } ], "responses": { @@ -2136,7 +1946,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2183,7 +1993,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2201,237 +2011,11 @@ } ] }, - "/groups/{groupId}/artifacts/{artifactId}/versions": { - "summary": "Manage all the versions of an artifact in the registry.", + "/groups/{groupId}/artifacts/{artifactId}/owner": { + "summary": "Manage the ownership of a single artifact.", "get": { "tags": [ - "Versions" - ], - "parameters": [ - { - "name": "offset", - "description": "The number of versions to skip before starting to collect the result set. Defaults to 0.", - "schema": { - "type": "integer" - }, - "in": "query", - "required": false - }, - { - "name": "limit", - "description": "The number of versions to return. Defaults to 20.", - "schema": { - "type": "integer" - }, - "in": "query", - "required": false - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionSearchResults" - }, - "examples": { - "All Versions": { - "value": [ - 5, - 6, - 10, - 103 - ] - } - } - } - }, - "description": "List of all artifact versions." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "listArtifactVersions", - "summary": "List artifact versions", - "description": "Returns a list of all versions of the artifact. The result set is paged.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "post": { - "requestBody": { - "description": "The content of the artifact version being created or the content and a set of references to other artifacts. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/FileContent" - }, - "examples": { - "OpenAPI Example": { - "value": { - "openapi": "3.0.2", - "info": { - "title": "Empty API", - "version": "1.0.7", - "description": "An example API design using OpenAPI." - }, - "paths": { - "/widgets": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "description": "All widgets" - } - }, - "summary": "Get widgets" - } - } - }, - "components": { - "schemas": { - "Widget": { - "title": "Root Type for Widget", - "description": "A sample data type.", - "type": "object", - "properties": { - "property-1": { - "type": "string" - }, - "property-2": { - "type": "boolean" - } - }, - "example": { - "property-1": "value1", - "property-2": true - } - } - } - } - } - } - } - }, - "application/create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - }, - "application/vnd.create.extended+json": { - "schema": { - "$ref": "#/components/schemas/ArtifactContent" - } - } - }, - "required": true - }, - "tags": [ - "Versions" - ], - "parameters": [ - { - "name": "X-Registry-Version", - "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. It must be unique within the artifact. If this is not\nprovided, the server will generate a new, unique version number for this new updated content.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "header" - }, - { - "name": "X-Registry-Name", - "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactName" - }, - "in": "header" - }, - { - "name": "X-Registry-Description", - "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/ArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Description-Encoded", - "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactDescription" - }, - "in": "header" - }, - { - "name": "X-Registry-Name-Encoded", - "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", - "schema": { - "$ref": "#/components/schemas/EncodedArtifactName" - }, - "in": "header" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionMetaData" - } - } - }, - "description": "The artifact version was successfully created." - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/RuleViolationConflict" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "createArtifactVersion", - "summary": "Create artifact version", - "description": "Creates a new version of the artifact by uploading new content. The configured rules for\nthe artifact are applied, and if they all pass, the new content is added as the most recent \nversion of the artifact. If any of the rules fail, an error is returned.\n\nThe body of the request can be the raw content of the new artifact version, or the raw content \nand a set of references pointing to other artifacts, and the type\nof that content should match the artifact's type (for example if the artifact type is `AVRO`\nthen the content of the request should be an Apache Avro document).\n\nThis operation can fail for the following reasons:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" - }, - "in": "path", - "required": true - }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, - "/groups/{groupId}/artifacts/{artifactId}/owner": { - "summary": "Manage the ownership of a single artifact.", - "get": { - "tags": [ - "Metadata" + "Metadata" ], "responses": { "200": { @@ -2487,7 +2071,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2555,7 +2139,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2833,7 +2417,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2871,6 +2455,9 @@ }, "description": "List of all the comments for this artifact." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2907,6 +2494,9 @@ }, "description": "The comment was successfully created." }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "404": { "$ref": "#/components/responses/NotFound" }, @@ -2921,7 +2511,7 @@ "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -2939,9 +2529,9 @@ }, { "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { - "$ref": "#/components/schemas/Version" + "type": "string" }, "in": "path", "required": true @@ -2959,49 +2549,659 @@ } } }, - "required": true - }, - "tags": [ - "Versions" - ], - "responses": { - "204": { - "description": "The value of the comment was successfully changed." + "required": true + }, + "tags": [ + "Versions" + ], + "responses": { + "204": { + "description": "The value of the comment was successfully changed." + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "updateArtifactVersionComment", + "summary": "Update a comment", + "description": "Updates the value of a single comment in an artifact version. Only the owner of the\ncomment can modify it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "delete": { + "tags": [ + "Versions" + ], + "responses": { + "204": { + "description": "The comment was successfully deleted." + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "deleteArtifactVersionComment", + "summary": "Delete a single comment", + "description": "Deletes a single comment in an artifact version. Only the owner of the\ncomment can delete it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + }, + { + "name": "versionExpression", + "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and artifact branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + }, + { + "name": "commentId", + "description": "The unique identifier of a single comment.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + } + ] + }, + "/ids/contentIds/{contentId}/": { + "summary": "Access artifact content utilizing the unique content identifier for that content.", + "get": { + "tags": [ + "Artifacts" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getContentById", + "summary": "Get artifact content by ID", + "description": "Gets the content for an artifact version in the registry using the unique content\nidentifier for that content. This content ID may be shared by multiple artifact\nversions in the case where the artifact versions are identical.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "contentId", + "description": "Global identifier for a single artifact content.", + "schema": { + "format": "int64", + "type": "integer" + }, + "in": "path", + "required": true + } + ] + }, + "/ids/contentHashes/{contentHash}/": { + "summary": "Access artifact content utilizing the SHA-256 hash of the content.", + "get": { + "tags": [ + "Artifacts" + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getContentByHash", + "summary": "Get artifact content by SHA-256 hash", + "description": "Gets the content for an artifact version in the registry using the \nSHA-256 hash of the content. This content hash may be shared by multiple artifact\nversions in the case where the artifact versions have identical content.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentHash` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "contentHash", + "description": "SHA-256 content hash for a single artifact content.", + "schema": { + "type": "string" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}/branches": { + "summary": "Manage branches of an artifact.", + "get": { + "tags": [ + "Branches" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArtifactBranch" + } + } + } + }, + "description": "List of all artifact versions." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "listArtifactBranches", + "summary": "List artifact branches", + "description": "Returns a list of all branches in the artifact. Each branch is a list of version identifiers,\nordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}": { + "summary": "Manage a single artifact.", + "get": { + "tags": [ + "Artifacts" + ], + "parameters": [ + { + "name": "references", + "description": "Allows the user to specify how references in the content should be treated.", + "schema": { + "$ref": "#/components/schemas/HandleReferencesType" + }, + "in": "query", + "required": false + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/ArtifactContent" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "getLatestArtifact", + "summary": "Get latest artifact", + "description": "Returns the latest version of the artifact in its raw form. The `Content-Type` of the\nresponse depends on the artifact type. In most cases, this is `application/json`, but \nfor some types it may be different (for example, `PROTOBUF`).\nIf the latest version of the artifact is marked as `DISABLED`, the next available non-disabled version will be used.\n\nThis operation may fail for one of the following reasons:\n\n* No artifact with this `artifactId` exists or all versions are `DISABLED` (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "put": { + "requestBody": { + "description": "The new content of the artifact being updated. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/FileContent" + }, + "examples": { + "OpenAPI Example": { + "value": { + "openapi": "3.0.2", + "info": { + "title": "Empty API", + "version": "1.0.7", + "description": "An example API design using OpenAPI." + }, + "paths": { + "/widgets": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "All widgets" + } + }, + "summary": "Get widgets" + } + } + }, + "components": { + "schemas": { + "Widget": { + "title": "Root Type for Widget", + "description": "A sample data type.", + "type": "object", + "properties": { + "property-1": { + "type": "string" + }, + "property-2": { + "type": "boolean" + } + }, + "example": { + "property-1": "value1", + "property-2": true + } + } + } + } + } + } + } + }, + "application/create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + }, + "application/vnd.create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + } + }, + "required": true + }, + "tags": [ + "Artifacts" + ], + "parameters": [ + { + "name": "X-Registry-Version", + "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. If not provided, the server will assign a version number\nautomatically.", + "schema": { + "$ref": "#/components/schemas/Version" + }, + "in": "header" + }, + { + "name": "X-Registry-Name", + "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Name-Encoded", + "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Description", + "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Description-Encoded", + "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactMetaData" + } + } + }, + "description": "When successful, returns the updated artifact metadata." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "updateArtifact", + "summary": "Update artifact", + "description": "Updates an artifact by uploading new content. The body of the request can\nbe the raw content of the artifact or a JSON object containing both the raw content and\na set of references to other artifacts.. This is typically in JSON format for *most*\nof the supported types, but may be in another format for a few (for example, `PROTOBUF`).\nThe type of the content should be compatible with the artifact's type (it would be\nan error to update an `AVRO` artifact with new `OPENAPI` content, for example).\n\nThe update could fail for a number of reasons including:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n\nWhen successful, this creates a new version of the artifact, making it the most recent\n(and therefore official) version of the artifact." + }, + "delete": { + "tags": [ + "Artifacts" + ], + "responses": { + "204": { + "description": "Returned when the artifact was successfully deleted." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "deleteArtifact", + "summary": "Delete artifact", + "description": "Deletes an artifact completely, resulting in all versions of the artifact also being\ndeleted. This may fail for one of the following reasons:\n\n* No artifact with the `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)" + }, + "parameters": [ + { + "name": "groupId", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/GroupId" + }, + "in": "path", + "required": true + }, + { + "name": "artifactId", + "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", + "schema": { + "$ref": "#/components/schemas/ArtifactId" + }, + "in": "path", + "required": true + } + ] + }, + "/groups/{groupId}/artifacts/{artifactId}/versions": { + "summary": "Manage all the versions of an artifact in the registry.", + "get": { + "tags": [ + "Versions" + ], + "parameters": [ + { + "name": "offset", + "description": "The number of versions to skip before starting to collect the result set. Defaults to 0.", + "schema": { + "type": "integer" + }, + "in": "query", + "required": false + }, + { + "name": "limit", + "description": "The number of versions to return. Defaults to 20.", + "schema": { + "type": "integer" + }, + "in": "query", + "required": false + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionSearchResults" + }, + "examples": { + "All Versions": { + "value": [ + 5, + 6, + 10, + 103 + ] + } + } + } + }, + "description": "List of all artifact versions." + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/ServerError" + } + }, + "operationId": "listArtifactVersions", + "summary": "List artifact versions", + "description": "Returns a list of all versions of the artifact. The result set is paged.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + }, + "post": { + "requestBody": { + "description": "The content of the artifact version being created or the content and a set of references to other artifacts. This is often, but not always, JSON data\nrepresenting one of the supported artifact types:\n\n* Avro (`AVRO`)\n* Protobuf (`PROTOBUF`)\n* JSON Schema (`JSON`)\n* Kafka Connect (`KCONNECT`)\n* OpenAPI (`OPENAPI`)\n* AsyncAPI (`ASYNCAPI`)\n* GraphQL (`GRAPHQL`)\n* Web Services Description Language (`WSDL`)\n* XML Schema (`XSD`)\n", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/FileContent" + }, + "examples": { + "OpenAPI Example": { + "value": { + "openapi": "3.0.2", + "info": { + "title": "Empty API", + "version": "1.0.7", + "description": "An example API design using OpenAPI." + }, + "paths": { + "/widgets": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "All widgets" + } + }, + "summary": "Get widgets" + } + } + }, + "components": { + "schemas": { + "Widget": { + "title": "Root Type for Widget", + "description": "A sample data type.", + "type": "object", + "properties": { + "property-1": { + "type": "string" + }, + "property-2": { + "type": "boolean" + } + }, + "example": { + "property-1": "value1", + "property-2": true + } + } + } + } + } + } + } + }, + "application/create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + }, + "application/vnd.create.extended+json": { + "schema": { + "$ref": "#/components/schemas/ArtifactContent" + } + } + }, + "required": true + }, + "tags": [ + "Versions" + ], + "parameters": [ + { + "name": "X-Registry-Version", + "description": "Specifies the version number of this new version of the artifact content. This would typically\nbe a simple integer or a SemVer value. It must be unique within the artifact. If this is not\nprovided, the server will generate a new, unique version number for this new updated content.", + "schema": { + "$ref": "#/components/schemas/Version" + }, + "in": "header" + }, + { + "name": "X-Registry-Name", + "description": "Specifies the artifact name of this new version of the artifact content. Name must be ASCII-only string. If this is not\nprovided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactName" + }, + "in": "header" + }, + { + "name": "X-Registry-Description", + "description": "Specifies the artifact description of this new version of the artifact content. Description must be ASCII-only string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/ArtifactDescription" + }, + "in": "header" + }, + { + "name": "X-Registry-Description-Encoded", + "description": "Specifies the artifact description of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the description from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactDescription" + }, + "in": "header" }, - "404": { - "$ref": "#/components/responses/NotFound" + { + "name": "X-Registry-Name-Encoded", + "description": "Specifies the artifact name of this new version of the artifact content. Value of this must be Base64 encoded string. If this is not provided, the server will extract the name from the artifact content.", + "schema": { + "$ref": "#/components/schemas/EncodedArtifactName" + }, + "in": "header" }, - "500": { - "$ref": "#/components/responses/ServerError" + { + "name": "X-Registry-Artifact-Branches", + "description": "An optional, comma-separated list of artifact branch IDs, that specify branches into which the newly created version will be added. The list may contain a single branch ID, but may not be empty. A new version is always automatically inserted into the \"latest\" branch. If any of the branches do not already exist, they will be created.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "in": "header" } - }, - "operationId": "updateArtifactVersionComment", - "summary": "Update a comment", - "description": "Updates the value of a single comment in an artifact version. Only the owner of the\ncomment can modify it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "delete": { - "tags": [ - "Versions" ], "responses": { - "204": { - "description": "The comment was successfully deleted." + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionMetaData" + } + } + }, + "description": "The artifact version was successfully created." }, "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/RuleViolationConflict" + }, "500": { "$ref": "#/components/responses/ServerError" } }, - "operationId": "deleteArtifactVersionComment", - "summary": "Delete a single comment", - "description": "Deletes a single comment in an artifact version. Only the owner of the\ncomment can delete it. The `artifactId`, unique `version` number, and `commentId` \nmust be provided.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No version with this `version` exists (HTTP error `404`)\n* No comment with this `commentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "createArtifactVersion", + "summary": "Create artifact version", + "description": "Creates a new version of the artifact by uploading new content. The configured rules for\nthe artifact are applied, and if they all pass, the new content is added as the most recent \nversion of the artifact. If any of the rules fail, an error is returned.\n\nThe body of the request can be the raw content of the new artifact version, or the raw content \nand a set of references pointing to other artifacts, and the type\nof that content should match the artifact's type (for example if the artifact type is `AVRO`\nthen the content of the request should be an Apache Avro document).\n\nThis operation can fail for the following reasons:\n\n* Provided content (request body) was empty (HTTP error `400`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* The new content violates one of the rules configured for the artifact (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -3016,96 +3216,11 @@ }, "in": "path", "required": true - }, - { - "name": "versionExpression", - "description": "An expression resolvable to a specific version ID within the given group and artifact. The following rules apply:\n\n - If the expression is in the form \"branch={branchId}\", and branch {branchId} exists: The expression is resolved to a version that the branch points to.\n - Otherwise: The expression is resolved to a version with the same ID, which must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", - "schema": { - "$ref": "#/components/schemas/Version" - }, - "in": "path", - "required": true - }, - { - "name": "commentId", - "description": "The unique identifier of a single comment.", - "schema": { - "type": "string" - }, - "in": "path", - "required": true - } - ] - }, - "/ids/contentIds/{contentId}/": { - "summary": "Access artifact content utilizing the unique content identifier for that content.", - "get": { - "tags": [ - "Artifacts" - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getContentById", - "summary": "Get artifact content by ID", - "description": "Gets the content for an artifact version in the registry using the unique content\nidentifier for that content. This content ID may be shared by multiple artifact\nversions in the case where the artifact versions are identical.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "contentId", - "description": "Global identifier for a single artifact content.", - "schema": { - "format": "int64", - "type": "integer" - }, - "in": "path", - "required": true - } - ] - }, - "/ids/contentHashes/{contentHash}/": { - "summary": "Access artifact content utilizing the SHA-256 hash of the content.", - "get": { - "tags": [ - "Artifacts" - ], - "responses": { - "200": { - "$ref": "#/components/responses/ArtifactContent" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/ServerError" - } - }, - "operationId": "getContentByHash", - "summary": "Get artifact content by SHA-256 hash", - "description": "Gets the content for an artifact version in the registry using the \nSHA-256 hash of the content. This content hash may be shared by multiple artifact\nversions in the case where the artifact versions have identical content.\n\nThis operation may fail for one of the following reasons:\n\n* No content with this `contentHash` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" - }, - "parameters": [ - { - "name": "contentHash", - "description": "SHA-256 content hash for a single artifact content.", - "schema": { - "type": "string" - }, - "in": "path", - "required": true } ] }, - "/groups/{groupId}/artifacts/{artifactId}/branches": { - "summary": "Manage branches of an artifact.", + "/groups/{groupId}/artifacts/{artifactId}/branches/{branchId}": { + "summary": "Manage a single artifact branch.", "get": { "tags": [ "Branches" @@ -3115,11 +3230,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Branches" + "$ref": "#/components/schemas/ArtifactBranch" } } }, - "description": "List of all artifact versions." + "description": "List of versions in an artifact branch." }, "404": { "$ref": "#/components/responses/NotFound" @@ -3128,34 +3243,21 @@ "$ref": "#/components/responses/ServerError" } }, - "operationId": "listArtifactBranches", - "summary": "List artifact branches", - "description": "Returns a list of all branches in the artifact. Each branch is a list of version identifiers,\nordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "getArtifactBranch", + "summary": "List versions in an artifact branch", + "description": "Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" }, - "parameters": [ - { - "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/GroupId" + "put": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactBranch" + } + } }, - "in": "path", "required": true }, - { - "name": "artifactId", - "description": "The artifact ID. Can be a string (client-provided) or UUID (server-generated), representing the unique artifact identifier. Must follow the \".{1,512}\" pattern.", - "schema": { - "$ref": "#/components/schemas/ArtifactId" - }, - "in": "path", - "required": true - } - ] - }, - "/groups/{groupId}/artifacts/{artifactId}/branches/{branchId}": { - "summary": "Manage a single artifact branch.", - "get": { "tags": [ "Branches" ], @@ -3164,10 +3266,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Gav" - } + "$ref": "#/components/schemas/ArtifactBranch" } } }, @@ -3176,13 +3275,16 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/Conflict" + }, "500": { "$ref": "#/components/responses/ServerError" } }, - "operationId": "getArtifactBranch", - "summary": "List versions in an artifact branch", - "description": "Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* A server error occurred (HTTP error `500`)\n" + "operationId": "createOrReplaceArtifactBranch", + "summary": "Replace the sequence of versions contained in an artifact branch. Branch is created if it does not exist.", + "description": "Replace the sequence of versions contained in an artifact branch. Branch is created if it does not exist. This operation is equivalent to deleting the artifact branch and adding each version in order to a new branch with the same name. This operation can be used to remove one or more versions from the branch. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Request contains duplicate versions. Artifact branches are append-only, cycles and history rewrites, except by this operation, are not supported. (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "post": { "requestBody": { @@ -3203,10 +3305,7 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Gav" - } + "$ref": "#/components/schemas/ArtifactBranch" } } }, @@ -3224,7 +3323,7 @@ }, "operationId": "createOrUpdateArtifactBranch", "summary": "Add a new version to an artifact branch. Branch is created if it does not exist.", - "description": "Add a new version to an artifact branch. Branch is created if it does not exist. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\n\nThis operation can fail for the following reasons:\n\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Branch already contains given version. Branches are append-only, cycles and history rewrites are not supported. (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" + "description": "Add a new version to an artifact branch. Branch is created if it does not exist. Returns a list of version identifiers in the artifact branch, ordered from the latest (tip of the branch) to the oldest.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Version does not exist (HTTP error `409`)\n* Branch already contains given version. Artifact branches are append-only, cycles and history rewrites, except by replacing the entire branch using createOrReplaceArtifactBranch operation, are not supported. (HTTP error `409`)\n* A server error occurred \n" }, "delete": { "tags": [ @@ -3232,7 +3331,7 @@ ], "responses": { "204": { - "description": "Branch was successfully deleted." + "description": "Artifact branch was successfully deleted." }, "404": { "$ref": "#/components/responses/NotFound" @@ -3245,13 +3344,13 @@ } }, "operationId": "deleteArtifactBranch", - "summary": "Delete artifact branch", + "summary": "Delete artifact branch.", "description": "Deletes a single branch in the artifact. Any artifact versions that are not referenced by a branch are deleted as well, however, this does not happen until deletion of the \"latest\" branch is supported.\nThis operation can fail for the following reasons:\n* No group with this `groupId` exists (HTTP error `404`)\n* No artifact with this `artifactId` exists (HTTP error `404`)\n* No branch with this `branchId` exists (HTTP error `404`)\n* Deletion of the \"latest\" branch is not supported (HTTP error `409`)\n* A server error occurred (HTTP error `500`)\n" }, "parameters": [ { "name": "groupId", - "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \"[a-zA-Z0-9._-]{1,512}\" pattern.", + "description": "The artifact group ID. Must be a string provided by the client, representing the name of the grouping of artifacts. Must follow the \".{1,512}\" pattern.", "schema": { "$ref": "#/components/schemas/GroupId" }, @@ -3269,7 +3368,7 @@ }, { "name": "branchId", - "description": "Branch ID. Must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", + "description": "Artifact branch ID. Must follow the \"[a-zA-Z0-9._\\\\-+]{1,256}\" pattern.", "schema": { "$ref": "#/components/schemas/BranchId" }, @@ -3488,21 +3587,6 @@ "context": "/info/externalDocs[url]" } }, - "GroupId": { - "description": "An ID of a single artifact group.", - "type": "string", - "example": "\"my-group\"" - }, - "ArtifactId": { - "description": "The ID of a single artifact.", - "type": "string", - "example": "\"example-artifact\"" - }, - "Version": { - "description": "A single version of an artifact. Can be provided by the client when creating a new version,\nor it can be server-generated. The value can be any string unique to the artifact, but it is\nrecommended to use a simple integer or a semver value.", - "type": "string", - "example": "\"3.1.6\"" - }, "Properties": { "description": "User-defined name-value pairs. Name and value must be strings.", "type": "object", @@ -4521,39 +4605,61 @@ ], "type": "string" }, + "ArtifactId": { + "description": "The ID of a single artifact.", + "pattern": "^.{1,512}$", + "type": "string", + "example": "\"example-artifact\"" + }, + "GroupId": { + "description": "An ID of a single artifact group.", + "pattern": "^.{1,512}$", + "type": "string", + "example": "\"my-group\"" + }, + "Version": { + "description": "A single version of an artifact. Can be provided by the client when creating a new version,\nor it can be server-generated. The value can be any string unique to the artifact, but it is\nrecommended to use a simple integer or a semver value.", + "pattern": "^[a-zA-Z0-9._\\-+]{1,256}$", + "type": "string", + "example": "\"3.1.6\"" + }, "BranchId": { "description": "The ID of a single artifact branch.", + "pattern": "^[a-zA-Z0-9._\\-+]{1,256}$", "type": "string", "example": "\"latest\"" }, - "Gav": { - "title": "Root Type for GAV", - "description": "Artifact version identifier, consisting of groupId, artifactId, and version triple.", + "ArtifactBranch": { + "title": "Root Type for ArtifactBranches", + "description": "", + "required": [ + "groupId", + "artifactId", + "branchId", + "versions" + ], "type": "object", "properties": { "groupId": { - "type": "string" + "$ref": "#/components/schemas/GroupId", + "description": "" }, "artifactId": { - "type": "string" + "$ref": "#/components/schemas/ArtifactId", + "description": "" }, - "version": { - "type": "string" + "branchId": { + "$ref": "#/components/schemas/BranchId", + "description": "" + }, + "versions": { + "description": "", + "type": "array", + "items": { + "$ref": "#/components/schemas/Version" + } } }, - "example": { - "groupId": "default", - "artifactId": "user", - "version": "1" - } - }, - "Branches": { - "title": "Root Type for Branches", - "description": "", - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Gav" - }, "example": { "latest": [ { @@ -4807,4 +4913,4 @@ } ] } -} +} \ No newline at end of file diff --git a/common/src/test/java/io/apicurio/registry/model/ModelTypesTest.java b/common/src/test/java/io/apicurio/registry/model/ModelTypesTest.java new file mode 100644 index 0000000000..ffe1515152 --- /dev/null +++ b/common/src/test/java/io/apicurio/registry/model/ModelTypesTest.java @@ -0,0 +1,85 @@ +package io.apicurio.registry.model; + + +import jakarta.validation.ValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ModelTypesTest { + + + @Test + void testGroupId() { + assertTrue(GroupId.DEFAULT.isDefaultGroup()); + + assertEquals(GroupId.DEFAULT, new GroupId(null)); + assertEquals(GroupId.DEFAULT, new GroupId("default")); + assertEquals(null, GroupId.DEFAULT.getRawGroupIdWithNull()); + assertEquals("default", GroupId.DEFAULT.getRawGroupIdWithDefaultString()); + + assertThrows(ValidationException.class, () -> new GroupId("")); + assertThrows(ValidationException.class, () -> new GroupId("x".repeat(513))); + + new GroupId("x"); + new GroupId("._@# $%"); + new GroupId("x".repeat(512)); + } + + + @Test + void testArtifactId() { + assertThrows(ValidationException.class, () -> new ArtifactId(null)); + assertThrows(ValidationException.class, () -> new ArtifactId("")); + assertThrows(ValidationException.class, () -> new ArtifactId("x".repeat(513))); + + new ArtifactId("x"); + new ArtifactId("._@# $%"); + new ArtifactId("x".repeat(512)); + } + + + @Test + void testVersionId() { + assertThrows(ValidationException.class, () -> new VersionId(null)); + assertThrows(ValidationException.class, () -> new VersionId("")); + assertThrows(ValidationException.class, () -> new VersionId(" ")); + assertThrows(ValidationException.class, () -> new VersionId("=")); + assertThrows(ValidationException.class, () -> new VersionId("x".repeat(257))); + + new VersionId("x"); + new VersionId("._-+"); + new VersionId("x".repeat(256)); + } + + + @Test + void testBranchId() { + assertEquals(BranchId.LATEST, new BranchId("latest")); + + assertThrows(ValidationException.class, () -> new BranchId(null)); + assertThrows(ValidationException.class, () -> new BranchId("")); + assertThrows(ValidationException.class, () -> new BranchId(" ")); + assertThrows(ValidationException.class, () -> new BranchId("=")); + assertThrows(ValidationException.class, () -> new BranchId("x".repeat(257))); + + new BranchId("x"); + new BranchId("._-+"); + new BranchId("x".repeat(256)); + } + + + @Test + void testGAandGAV() { + var ga1 = new GA(null, "artifact1"); + var ga2 = new GA("default", "artifact1"); + assertEquals(ga1, ga2); + + var gav1 = new GAV(null, "artifact1", "version1"); + var gav2 = new GAV(ga2, new VersionId("version1")); + assertEquals(gav1, gav2); + + assertNotEquals(ga1, gav1); + assertNotEquals(gav1, ga1); + } +} diff --git a/common/src/test/java/io/apicurio/registry/model/VersionExpressionParserTest.java b/common/src/test/java/io/apicurio/registry/model/VersionExpressionParserTest.java new file mode 100644 index 0000000000..37b28424a5 --- /dev/null +++ b/common/src/test/java/io/apicurio/registry/model/VersionExpressionParserTest.java @@ -0,0 +1,39 @@ +package io.apicurio.registry.model; + + +import jakarta.validation.ValidationException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class VersionExpressionParserTest { + + + @Test + void testVersionExpressionParser() { + var ga1 = new GA(null, "artifact1"); + + assertEquals(new GAV(ga1, new VersionId("version1")), VersionExpressionParser.parse(ga1, "branch=latest", this::getArtifactBranchTip)); + assertEquals(new GAV(ga1, new VersionId("version2")), VersionExpressionParser.parse(ga1, "branch=1.0.x", this::getArtifactBranchTip)); + + assertEquals(new GAV(ga1, new VersionId("version3")), VersionExpressionParser.parse(ga1, "version3", this::getArtifactBranchTip)); + + assertThrows(ValidationException.class, () -> VersionExpressionParser.parse(ga1, "branch =1.0.x", this::getArtifactBranchTip)); + assertThrows(ValidationException.class, () -> VersionExpressionParser.parse(ga1, "branch 1.0.x", this::getArtifactBranchTip)); + assertThrows(ValidationException.class, () -> VersionExpressionParser.parse(ga1, "ranch=1.0.x", this::getArtifactBranchTip)); + assertThrows(ValidationException.class, () -> VersionExpressionParser.parse(ga1, "branch=1.0.@", this::getArtifactBranchTip)); + assertThrows(ValidationException.class, () -> VersionExpressionParser.parse(ga1, "branch=", this::getArtifactBranchTip)); + } + + + private GAV getArtifactBranchTip(GA ga, BranchId branchId) { + if (BranchId.LATEST.equals(branchId)) { + return new GAV(ga, new VersionId("version1")); + } + if (new BranchId("1.0.x").equals(branchId)) { + return new GAV(ga, new VersionId("version2")); + } + return null; + } +} diff --git a/integration-tests/src/test/java/io/apicurio/tests/migration/GenerateCanonicalHashImportIT.java b/integration-tests/src/test/java/io/apicurio/tests/migration/GenerateCanonicalHashImportIT.java index 1f7d282c3d..3518137a1c 100644 --- a/integration-tests/src/test/java/io/apicurio/tests/migration/GenerateCanonicalHashImportIT.java +++ b/integration-tests/src/test/java/io/apicurio/tests/migration/GenerateCanonicalHashImportIT.java @@ -6,7 +6,7 @@ import io.apicurio.registry.types.ArtifactState; import io.apicurio.registry.types.ArtifactType; import io.apicurio.registry.utils.IoUtil; -import io.apicurio.registry.utils.impexp.ArtifactVersionBranchEntity; +import io.apicurio.registry.utils.impexp.ArtifactBranchEntity; import io.apicurio.registry.utils.impexp.ArtifactVersionEntity; import io.apicurio.registry.utils.impexp.ContentEntity; import io.apicurio.registry.utils.impexp.EntityWriter; @@ -130,9 +130,9 @@ public InputStream generateExportedZip(Map artifacts) { writer.writeEntity(versionEntity); writer.writeEntity( - ArtifactVersionBranchEntity.builder() + ArtifactBranchEntity.builder() .artifactId(artifactId) - .branch(BranchId.LATEST.getRawBranchId()) + .branchId(BranchId.LATEST.getRawBranchId()) .branchOrder(1) .version("1") .build() diff --git a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactVersionBranchEntity.java b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactBranchEntity.java similarity index 81% rename from utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactVersionBranchEntity.java rename to utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactBranchEntity.java index e795c53a22..77575b0756 100644 --- a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactVersionBranchEntity.java +++ b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/ArtifactBranchEntity.java @@ -15,12 +15,16 @@ @AllArgsConstructor(access = PRIVATE) @ToString @RegisterForReflection -public class ArtifactVersionBranchEntity extends Entity { +public class ArtifactBranchEntity extends Entity { public String groupId; + public String artifactId; + public String version; - public String branch; + + public String branchId; + public int branchOrder; @@ -30,12 +34,12 @@ public GAV toGAV() { public BranchId toBranchId() { - return new BranchId(branch); + return new BranchId(branchId); } @Override public EntityType getEntityType() { - return EntityType.ArtifactVersionBranch; + return EntityType.ArtifactBranch; } } diff --git a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityReader.java b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityReader.java index f21b66098c..4d1314627a 100644 --- a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityReader.java +++ b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityReader.java @@ -48,8 +48,8 @@ public Entity readEntity() throws IOException { return readGroup(entry); case Comment: return readComment(entry); - case ArtifactVersionBranch: - return readArtifactVersionBranch(entry); + case ArtifactBranch: + return readArtifactBranch(entry); case Manifest: return readManifest(entry); } @@ -96,8 +96,8 @@ private CommentEntity readComment(ZipEntry entry) throws IOException { return this.readEntry(entry, CommentEntity.class); } - private ArtifactVersionBranchEntity readArtifactVersionBranch(ZipEntry entry) throws IOException { - return this.readEntry(entry, ArtifactVersionBranchEntity.class); + private ArtifactBranchEntity readArtifactBranch(ZipEntry entry) throws IOException { + return this.readEntry(entry, ArtifactBranchEntity.class); } private GlobalRuleEntity readGlobalRule(ZipEntry entry) throws IOException { diff --git a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityType.java b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityType.java index 96ed6a68f6..079103fb15 100644 --- a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityType.java +++ b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityType.java @@ -5,6 +5,6 @@ @RegisterForReflection public enum EntityType { - Manifest, GlobalRule, Content, Group, ArtifactVersion, ArtifactRule, Comment, ArtifactVersionBranch + Manifest, GlobalRule, Content, Group, ArtifactVersion, ArtifactRule, Comment, ArtifactBranch } diff --git a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityWriter.java b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityWriter.java index 4def458b23..28bd87f60f 100644 --- a/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityWriter.java +++ b/utils/importexport/src/main/java/io/apicurio/registry/utils/impexp/EntityWriter.java @@ -52,8 +52,8 @@ public void writeEntity(Entity entity) throws IOException { case Comment: writeEntity((CommentEntity) entity); break; - case ArtifactVersionBranch: - writeEntity((ArtifactVersionBranchEntity) entity); + case ArtifactBranch: + writeEntity((ArtifactBranchEntity) entity); break; case Manifest: writeEntity((ManifestEntity) entity); @@ -106,9 +106,9 @@ private void writeEntity(CommentEntity entity) throws IOException { write(mdEntry, entity, CommentEntity.class); } - private void writeEntity(ArtifactVersionBranchEntity entity) throws IOException { - ZipEntry mdEntry = createZipEntry(EntityType.ArtifactVersionBranch, entity.groupId, entity.artifactId, entity.branch + '-' + entity.branchOrder, "json"); - write(mdEntry, entity, ArtifactVersionBranchEntity.class); + private void writeEntity(ArtifactBranchEntity entity) throws IOException { + ZipEntry mdEntry = createZipEntry(EntityType.ArtifactBranch, entity.groupId, entity.artifactId, entity.branchId + '-' + entity.branchOrder, "json"); + write(mdEntry, entity, ArtifactBranchEntity.class); } private ZipEntry createZipEntry(EntityType type, String fileName, String fileExt) { @@ -122,7 +122,7 @@ private ZipEntry createZipEntry(EntityType type, String groupId, String artifact path = String.format("groups/%s/artifacts/%s/rules/%s.%s.%s", groupOrDefault(groupId), artifactId, fileName, type.name(), fileExt); break; case ArtifactVersion: - case ArtifactVersionBranch: + case ArtifactBranch: path = String.format("groups/%s/artifacts/%s/versions/%s.%s.%s", groupOrDefault(groupId), artifactId, fileName, type.name(), fileExt); break; case Content: