project version list api

This commit is contained in:
Jake Potrebic 2020-08-06 18:59:23 -07:00
parent 34ad0d6974
commit 33819f7d1b
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
7 changed files with 84 additions and 33 deletions

View File

@ -8,6 +8,8 @@ import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.mappers.RoleMapper;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.spi.JdbiPlugin;
import org.jdbi.v3.core.statement.SqlLogger;
import org.jdbi.v3.core.statement.StatementContext;
import org.jdbi.v3.postgres.PostgresPlugin;
import org.jdbi.v3.postgres.PostgresTypes;
import org.jdbi.v3.sqlobject.SqlObjectPlugin;
@ -21,6 +23,7 @@ import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import javax.sql.DataSource;
import java.util.List;
import java.util.logging.Logger;
@Configuration
public class JDBIConfig {
@ -37,8 +40,15 @@ public class JDBIConfig {
@Bean
public Jdbi jdbi(DataSource dataSource, List<JdbiPlugin> jdbiPlugins) {
SqlLogger myLogger = new SqlLogger() {
@Override
public void logAfterExecution(StatementContext context) {
Logger.getLogger("sql").info("sql: " + context.getRenderedSql());
}
};
TransactionAwareDataSourceProxy dataSourceProxy = new TransactionAwareDataSourceProxy(dataSource);
Jdbi jdbi = Jdbi.create(dataSourceProxy);
// jdbi.setSqlLogger(myLogger); // TODO for debugging sql statements
jdbiPlugins.forEach(jdbi::installPlugin);
PostgresTypes config = jdbi.getConfig(PostgresTypes.class);
jdbi.registerRowMapper(new RoleMapper());

View File

@ -65,7 +65,7 @@ public interface VersionsApi {
@ApiResponse(code = 200, message = "Ok", response = Version.class),
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
@GetMapping(value = "/projects/{pluginId}/versions/{name}",
@GetMapping(value = "/projects/{pluginId}/versions/{name:.*}",
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Version> showVersion(@ApiParam(value = "The plugin id of the project to return the version for", required = true) @PathVariable("pluginId") String pluginId
, @ApiParam(value = "The name of the version to return", required = true) @PathVariable("name") String name

View File

@ -1,9 +1,13 @@
package me.minidigger.hangar.controller.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.api.ApiVersionsDao;
import me.minidigger.hangar.model.generated.DeployVersionInfo;
import me.minidigger.hangar.model.generated.PaginatedVersionResult;
import me.minidigger.hangar.model.generated.Pagination;
import me.minidigger.hangar.model.generated.Version;
import me.minidigger.hangar.model.generated.VersionStatsDay;
import me.minidigger.hangar.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -12,18 +16,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import me.minidigger.hangar.model.generated.DeployVersionInfo;
import me.minidigger.hangar.model.generated.PaginatedVersionResult;
import me.minidigger.hangar.model.generated.Version;
import me.minidigger.hangar.model.generated.VersionStatsDay;
@Controller
public class VersionsApiController implements VersionsApi {
@ -59,23 +58,21 @@ public class VersionsApiController implements VersionsApi {
@Override
public ResponseEntity<PaginatedVersionResult> listVersions(String pluginId, List<String> tags, Long limit, Long offset) {
apiVersionsDao.get().listVersions(pluginId, tags, false, limit, offset, null); // TODO finish api
try {
return new ResponseEntity<>(objectMapper.readValue("{\n \"result\" : [ {\n \"visibility\" : \"public\",\n \"stats\" : {\n \"downloads\" : 0\n },\n \"author\" : \"author\",\n \"file_info\" : {\n \"size_bytes\" : 6,\n \"md_5_hash\" : \"md_5_hash\",\n \"name\" : \"name\"\n },\n \"name\" : \"name\",\n \"created_at\" : \"2000-01-23T04:56:07.000+00:00\",\n \"description\" : \"description\",\n \"dependencies\" : [ {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n }, {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n } ],\n \"review_state\" : \"unreviewed\",\n \"tags\" : [ {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n }, {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n } ]\n }, {\n \"visibility\" : \"public\",\n \"stats\" : {\n \"downloads\" : 0\n },\n \"author\" : \"author\",\n \"file_info\" : {\n \"size_bytes\" : 6,\n \"md_5_hash\" : \"md_5_hash\",\n \"name\" : \"name\"\n },\n \"name\" : \"name\",\n \"created_at\" : \"2000-01-23T04:56:07.000+00:00\",\n \"description\" : \"description\",\n \"dependencies\" : [ {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n }, {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n } ],\n \"review_state\" : \"unreviewed\",\n \"tags\" : [ {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n }, {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n } ]\n } ],\n \"pagination\" : {\n \"offset\" : 6,\n \"limit\" : 0,\n \"count\" : 1\n }\n}", PaginatedVersionResult.class), HttpStatus.OK); // TODO Implement me
} catch (IOException e) {
log.error("Couldn't serialize response for content type application/json", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
// TODO handling users
List<Version> versions = apiVersionsDao.get().listVersions(pluginId, null, tags, false, limit, offset, null);
int count = apiVersionsDao.get().listVersions(pluginId, null,tags, false, limit, offset, null).size();
return new ResponseEntity<>(new PaginatedVersionResult().result(versions).pagination(new Pagination().limit(limit).offset(offset).count((long) count)), HttpStatus.OK);
}
@Override
public ResponseEntity<Version> showVersion(String pluginId, String name) {
try {
return new ResponseEntity<>(objectMapper.readValue("{\n \"visibility\" : \"public\",\n \"stats\" : {\n \"downloads\" : 0\n },\n \"author\" : \"author\",\n \"file_info\" : {\n \"size_bytes\" : 6,\n \"md_5_hash\" : \"md_5_hash\",\n \"name\" : \"name\"\n },\n \"name\" : \"name\",\n \"created_at\" : \"2000-01-23T04:56:07.000+00:00\",\n \"description\" : \"description\",\n \"dependencies\" : [ {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n }, {\n \"plugin_id\" : \"plugin_id\",\n \"version\" : \"version\"\n } ],\n \"review_state\" : \"unreviewed\",\n \"tags\" : [ {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n }, {\n \"data\" : \"data\",\n \"color\" : {\n \"background\" : \"background\",\n \"foreground\" : \"foreground\"\n },\n \"name\" : \"name\"\n } ]\n}", Version.class), HttpStatus.OK); // TODO Implement me
} catch (IOException e) {
log.error("Couldn't serialize response for content type application/json", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
// TODO handling users
Version version = apiVersionsDao.get().listVersions(pluginId, name, null, false, 1L, 0, null).stream().findFirst().orElse(null);
if (version == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
} else {
return new ResponseEntity<>(version, HttpStatus.OK);
}
}

View File

@ -1,7 +1,9 @@
package me.minidigger.hangar.db.dao.api;
import me.minidigger.hangar.db.dao.api.mappers.VersionMapper;
import me.minidigger.hangar.model.generated.Version;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.customizer.BindList.EmptyHandling;
import org.jdbi.v3.sqlobject.customizer.Define;
@ -16,20 +18,21 @@ import java.util.List;
public interface ApiVersionsDao {
@UseStringTemplateEngine
@RegisterRowMapper(VersionMapper.class)
@SqlQuery("SELECT pv.created_at," +
"pv.version_string," +
"pv.dependencies," +
"pv.visibility," +
"pv.description," +
"coalesce((SELECT sum(pvd.downloads) FROM project_versions_downloads pvd WHERE p.id = pvd.project_id AND pv.id = pvd.version_id), 0) stat_downloads," +
"coalesce((SELECT sum(pvd.downloads) FROM project_versions_downloads pvd WHERE p.id = pvd.project_id AND pv.id = pvd.version_id), 0) downloads," +
"pv.file_size fi_size_bytes," +
"pv.hash fi_md5_hash," +
"pv.file_name fi_name," +
"u.name author," +
"pv.review_state," +
"array_append(array_agg(pvt.name ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), 'Channel') AS tag_names," +
"array_append(array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.name) AS tag_datas," +
"array_append(array_agg(pvt.color ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.color + 9) AS tag_colors " +
"array_append(array_agg(pvt.name ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), 'Channel') AS tag_name," +
"array_append(array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.name) AS tag_data," +
"array_append(array_agg(pvt.color ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.color + 9) AS tag_color " +
"FROM projects p" +
" JOIN project_versions pv ON p.id = pv.project_id" +
" LEFT JOIN users u ON pv.author_id = u.id" +
@ -37,14 +40,15 @@ public interface ApiVersionsDao {
" LEFT JOIN project_channels pc ON pv.channel_id = pc.id " +
"WHERE <if(!canSeeHidden)>(pv.visibility = 0 " +
"<if(userId)>OR (:userId IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
"p.plugin_id = :pluginId AND " +
"p.plugin_id = :pluginId <if(versionName)> AND " +
" pv.version_string = '<versionName>' <endif> <if(tags)> AND " +
"(" +
" pvt.name || ':' || pvt.data IN (<tags>) OR " +
" pvt.name IN (<tags>) OR " +
" 'Channel:' || pc.name IN (<tags>) OR " +
" 'Channel' IN (<tags>)" +
" ) " +
" )<endif> " +
"GROUP BY p.id, pv.id, u.id, pc.id " +
"ORDER BY pv.created_at DESC LIMIT :limit OFFSET :offset")
List<Version> listVersions(String pluginId, @BindList(value = "tags", onEmpty = EmptyHandling.NULL_STRING) List<String> tags, @Define boolean canSeeHidden, Long limit, long offset, @Define Long userId);
List<Version> listVersions(String pluginId, @Define String versionName, @BindList(value = "tags", onEmpty = EmptyHandling.NULL_VALUE) List<String> tags, @Define boolean canSeeHidden, Long limit, long offset, @Define Long userId);
}

View File

@ -0,0 +1,36 @@
package me.minidigger.hangar.db.dao.api.mappers;
import me.minidigger.hangar.model.Visibility;
import me.minidigger.hangar.model.generated.Dependency;
import me.minidigger.hangar.model.generated.FileInfo;
import me.minidigger.hangar.model.generated.ReviewState;
import me.minidigger.hangar.model.generated.Version;
import me.minidigger.hangar.model.generated.VersionStatsAll;
import org.jdbi.v3.core.mapper.ColumnMapper;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Optional;
public class VersionMapper implements RowMapper<Version> {
@Override
public Version map(ResultSet rs, StatementContext ctx) throws SQLException {
Optional<ColumnMapper<String[]>> mapper = ctx.findColumnMapperFor(String[].class);
if (mapper.isEmpty()) throw new UnsupportedOperationException("couldn't find a mapper for String[]");
return new Version()
.createdAt(rs.getObject("created_at", OffsetDateTime.class))
.name(rs.getString("version_string"))
.dependencies(Dependency.from(Arrays.asList(mapper.get().map(rs, "dependencies", ctx))))
.visibility(Visibility.fromId(rs.getLong("visibility")))
.description(rs.getString("description"))
.stats(new VersionStatsAll().downloads(rs.getLong("downloads")))
.fileInfo(new FileInfo().name(rs.getString("fi_name")).md5Hash(rs.getString("fi_md5_hash")).sizeBytes(rs.getLong("fi_size_bytes")))
.author(rs.getString("author"))
.reviewState(ReviewState.values()[rs.getInt("review_state")]);
}
}

View File

@ -32,7 +32,6 @@ public class Version {
private List<Dependency> dependencies = new ArrayList<>();
@JsonProperty("visibility")
@EnumByOrdinal
private Visibility visibility = null;
@JsonProperty("description")
@ -50,7 +49,6 @@ public class Version {
private String author = null;
@JsonProperty("review_state")
@EnumByOrdinal
private ReviewState reviewState = null;
@JsonProperty("tags")
@ -140,10 +138,12 @@ public class Version {
@ApiModelProperty(required = true, value = "")
@NotNull
@EnumByOrdinal
public Visibility getVisibility() {
return visibility;
}
@EnumByOrdinal
public void setVisibility(Visibility visibility) {
this.visibility = visibility;
}
@ -245,10 +245,12 @@ public class Version {
@ApiModelProperty(required = true, value = "")
@NotNull
@EnumByOrdinal
public ReviewState getReviewState() {
return reviewState;
}
@EnumByOrdinal
public void setReviewState(ReviewState reviewState) {
this.reviewState = reviewState;
}
@ -271,10 +273,12 @@ public class Version {
@ApiModelProperty(required = true, value = "")
@NotNull
@Valid
@Nested("tag")
public List<Tag> getTags() {
return tags;
}
@Nested("tag")
public void setTags(List<Tag> tags) {
this.tags = tags;
}

View File

@ -97,7 +97,7 @@
</#if>
</td>
<td style="vertical-align: middle; text-align: right; padding-right: 15px;">
<a href="/${utils.urlEncode(entry.namespace)}/versions/entry.versionString/reviews"><i class="fas fa-2x fa-fw fa-info"></i></a>
<a href="/${utils.urlEncode(entry.namespace.toString())}/versions/entry.versionString/reviews"><i class="fas fa-2x fa-fw fa-info"></i></a>
</td>
</tr>
</#list>
@ -138,7 +138,7 @@
</tr>
</#if>
<#list versions?sort_by("versionCreatedAt") as entry>
<tr data-version="${utils.urlEncode(entry.namespace)}/versions/entry.versionString">
<tr data-version="${utils.urlEncode(entry.namespace.toString())}/versions/entry.versionString">
<td>
<#import "*/utils/userAvatar.ftlh" as userAvatar>
<@userAvatar.userAvatar userName=entry.getNamespace().getOwner() avatarUrl=utils.avatarUrl(entry.getNamespace().getOwner()) clazz="user-avatar-xs"></@userAvatar.userAvatar>
@ -161,7 +161,7 @@
</#if>
<br>
<td style="vertical-align: middle; text-align: right">
<a class="btn btn-success" href="/${utils.urlEncode(entry.namespace)}/versions/entry.versionString/reviews">Start review</a>
<a class="btn btn-success" href="/${utils.urlEncode(entry.namespace.toString())}/versions/entry.versionString/reviews">Start review</a>
</td>
</tr>
</#list>