more progress on cockroach

Signed-off-by: MiniDigger <admin@benndorf.dev>
This commit is contained in:
MiniDigger 2021-11-20 19:11:12 +01:00 committed by MiniDigger | Martin
parent 4d04a854bb
commit b4aff65770
25 changed files with 131 additions and 69 deletions

View File

@ -13,7 +13,7 @@ services:
volumes:
- db_data:/var/lib/postgresql/data
cockroach:
image: cockroachdb/cockroach
image: cockroachdb/cockroach:latest-v21.2
ports:
- "26257:26257"
- "26000:8080"

View File

@ -3,7 +3,7 @@
<v-card>
<v-card-title v-text="$t('organization.new.title')" />
<v-card-subtitle>{{ $t('organization.new.text') }}</v-card-subtitle>
<template v-if="currentUser.headerData.organizationCount < 1">
<template v-if="currentUser.headerData.organizationCount < validations.maxOrgCount">
<v-card-text>
<v-form v-model="validForm">
<v-text-field

View File

@ -3,15 +3,20 @@ package io.papermc.hangar.db.customtypes;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.postgresql.util.PGobject;
import java.lang.reflect.Type;
import java.util.Map;
public class JSONB extends PGobject {
private static final String TYPE_STRING = "jsonb";
private transient JsonNode json;
private transient Map<String, String> map;
public JSONB(String value) {
setType(TYPE_STRING);
@ -19,6 +24,16 @@ public class JSONB extends PGobject {
parseJson();
}
public JSONB(Object value) {
setType(TYPE_STRING);
try {
this.value = new ObjectMapper().writeValueAsString(value);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
parseJson();
}
@JsonCreator
public JSONB(JsonNode json) {
setType(TYPE_STRING);
@ -35,6 +50,10 @@ public class JSONB extends PGobject {
return json;
}
public Map<String, String> getMap() {
return map;
}
@Override
public void setValue(String value) {
this.value = value;
@ -44,6 +63,8 @@ public class JSONB extends PGobject {
private void parseJson() {
try {
this.json = new ObjectMapper().readTree(value);
this.map = new ObjectMapper().readValue(value, new TypeReference<Map<String, String>>() {
});
} catch (JsonProcessingException | ClassCastException e) {
e.printStackTrace();
}

View File

@ -50,7 +50,7 @@ public interface UsersDAO {
" u.name," +
" u.tagline," +
" u.join_date," +
" array(SELECT role_id FROM user_global_roles WHERE u.id = user_id) roles," +
" array(SELECT role_id FROM user_global_roles WHERE u.id = user_id) AS roles," +
" (SELECT count(*) FROM project_members_all pma WHERE pma.user_id = u.id) AS project_count," +
" u.read_prompts," +
" u.locked," +

View File

@ -35,7 +35,7 @@ public interface HangarStatsDAO {
" sq.project_id," +
" <if(includeVersionId)>sq.version_id,<endif>" +
" count(DISTINCT sq.<if(withUserId)>user_id<else>address<endif>) FILTER (WHERE sq.processed \\<@ ARRAY[1])" +
" FROM (SELECT date_trunc('DAY', d.created_at) AS day," +
" FROM (SELECT date_trunc('DAY', d.created_at)::date AS day," +
" d.project_id," +
" <if(includeVersionId)>d.version_id,<endif>" +
" <if(withUserId)>user_id<else>address<endif>," +

View File

@ -36,9 +36,9 @@ public interface HealthDAO {
" p.visibility" +
" FROM projects p " +
" JOIN home_projects hp ON p.id = hp.id" +
" WHERE hp.last_updated < (now() - make_interval(secs := <age>))" +
" WHERE hp.last_updated < (now() - INTERVAL <age>)" +
" ORDER BY p.created_at DESC")
List<UnhealthyProject> getStaleProjects(@Define("age") long staleAgeSeconds);
List<UnhealthyProject> getStaleProjects(@Define("age") String staleAgeSeconds);
@SqlQuery(" SELECT p.owner_name \"owner\"," +
" p.slug," +

View File

@ -26,7 +26,7 @@ public interface JobsDAO {
long countAwaitingJobs();
@SqlQuery("UPDATE jobs SET state = 'started', last_updated = now() WHERE id = (" +
" SELECT id FROM jobs WHERE state = 'not_started' AND (retry_at IS NULL OR retry_at < now()) ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1" +
" SELECT id FROM jobs WHERE state = 'not_started' AND (retry_at IS NULL OR retry_at < now()) ORDER BY id /*FOR UPDATE SKIP LOCKED*/ LIMIT 1" +
") RETURNING *")
JobTable fetchJob();

View File

@ -13,7 +13,7 @@ import org.springframework.stereotype.Repository;
public interface ApiKeyDAO {
@Timestamped
@SqlUpdate("INSERT INTO api_keys (created_at, name, owner_id, token_identifier, token, raw_key_permissions) VALUES (:now, :name, :ownerId, :tokenIdentifier, crypt(:token, gen_salt('bf')), :permissions::bit(64))")
@SqlUpdate("INSERT INTO api_keys (created_at, name, owner_id, token_identifier, token, raw_key_permissions) VALUES (:now, :name, :ownerId, :tokenIdentifier, :token, :permissions::bit(64))")
void insert(@BindBean ApiKeyTable apiKeyTable);
@SqlUpdate("DELETE FROM api_keys WHERE name = :keyName AND owner_id = :userId")
@ -22,8 +22,8 @@ public interface ApiKeyDAO {
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE owner_id = :userId AND lower(name) = lower(:name)")
ApiKeyTable getByUserAndName(long userId, String name);
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE token_identifier = :identifier AND token = crypt(:token, token)")
ApiKeyTable findApiKey(String identifier, String token);
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE token_identifier = :identifier AND token = :hashedToken")
ApiKeyTable findApiKey(String identifier, String hashedToken);
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE owner_id = :userId AND token_identifier = :identifier")
ApiKeyTable findApiKey(long userId, String identifier);

View File

@ -107,11 +107,11 @@ public interface ProjectsApiDAO {
" JOIN projects p ON hp.id = p.id" +
" WHERE true <filters>" + // Not sure how else to get here a single Where
" <if(!seeHidden)> AND (hp.visibility = 0 <if(requesterId)>OR (:requesterId = ANY(hp.project_members) AND hp.visibility != 4)<endif>) <endif> " +
" <if(orderBy)>ORDER BY :orderBy<endif> " +
" ORDER BY <orderBy> " +
" <offsetLimit>")
@RegisterColumnMapper(PromotedVersionMapper.class)
@DefineNamedBindings
List<Project> getProjects(@Define boolean seeHidden, Long requesterId, String orderBy,
List<Project> getProjects(@Define boolean seeHidden, Long requesterId, @Define String orderBy,
@BindPagination RequestPagination pagination);
// This query can be shorter because it doesnt need all those column values as above does, just a single column for the amount of rows to be counted

View File

@ -18,27 +18,27 @@ public interface UsersApiDAO {
@RegisterConstructorMapper(ProjectCompact.class)
@UseStringTemplateEngine
@SqlQuery("SELECT p.created_at," +
" p.name," +
" p.owner_name \"owner\"," +
" p.slug," +
" p.views," +
" p.downloads," +
" p.recent_views," +
" p.recent_downloads," +
" p.stars," +
" p.watchers," +
" p.category," +
" p.visibility" +
@SqlQuery("SELECT hp.created_at," +
" hp.name," +
" hp.owner_name \"owner\"," +
" hp.slug," +
" hp.views," +
" hp.downloads," +
" hp.recent_views," +
" hp.recent_downloads," +
" hp.stars," +
" hp.watchers," +
" hp.category," +
" hp.visibility" +
" FROM users u " +
" JOIN project_stars ps ON u.id = ps.user_id" +
" JOIN home_projects p ON ps.project_id = p.id" +
" JOIN home_projects hp ON ps.project_id = hp.id" +
" WHERE " +
" <if(!canSeeHidden)> (p.visibility = 0 OR p.visibility = 1" +
" <if(userId)>OR (<userId> = ANY(p.project_members) AND p.visibility != 4)<endif>) AND<endif>" +
" u.name = :user" +
" ORDER BY :sortOrder LIMIT :limit OFFSET :offset")
List<ProjectCompact> getUserStarred(String user, @Define boolean canSeeHidden, @Define Long userId, String sortOrder, long limit, long offset);
" ORDER BY <sortOrder> LIMIT :limit OFFSET :offset")
List<ProjectCompact> getUserStarred(String user, @Define boolean canSeeHidden, @Define Long userId, @Define String sortOrder, long limit, long offset);
@UseStringTemplateEngine
@SqlQuery("SELECT count(*)" +
@ -53,27 +53,27 @@ public interface UsersApiDAO {
@RegisterConstructorMapper(ProjectCompact.class)
@UseStringTemplateEngine
@SqlQuery("SELECT p.created_at," +
" p.name," +
" p.owner_name \"owner\"," +
" p.slug," +
" p.views," +
" p.downloads," +
" p.recent_views," +
" p.recent_downloads," +
" p.stars," +
" p.watchers," +
" p.category," +
" p.visibility" +
@SqlQuery("SELECT hp.created_at," +
" hp.name," +
" hp.owner_name \"owner\"," +
" hp.slug," +
" hp.views," +
" hp.downloads," +
" hp.recent_views," +
" hp.recent_downloads," +
" hp.stars," +
" hp.watchers," +
" hp.category," +
" hp.visibility" +
" FROM users u " +
" JOIN project_watchers pw ON u.id = pw.user_id" +
" JOIN home_projects p ON pw.project_id = p.id" +
" JOIN home_projects hp ON pw.project_id = hp.id" +
" WHERE " +
" <if(!canSeeHidden)> (p.visibility = 0 OR p.visibility = 1" +
" <if(userId)>OR (<userId> = ANY(p.project_members) AND p.visibility != 4)<endif>) AND<endif>" +
" u.name = :user" +
" ORDER BY :sortOrder LIMIT :limit OFFSET :offset")
List<ProjectCompact> getUserWatching(String user, @Define boolean canSeeHidden, @Define Long userId, String sortOrder, long limit, long offset);
" ORDER BY <sortOrder> LIMIT :limit OFFSET :offset")
List<ProjectCompact> getUserWatching(String user, @Define boolean canSeeHidden, @Define Long userId, @Define String sortOrder, long limit, long offset);
@UseStringTemplateEngine
@SqlQuery("SELECT count(*)" +
@ -92,7 +92,7 @@ public interface UsersApiDAO {
" u.tagline," +
" u.join_date," +
" u.locked," +
" array(SELECT r.name FROM roles r JOIN user_global_roles ugr ON r.id = ugr.role_id WHERE u.id = ugr.user_id ORDER BY r.permission::BIGINT DESC) roles," +
" array(SELECT r.name FROM roles r JOIN user_global_roles ugr ON r.id = ugr.role_id WHERE u.id = ugr.user_id ORDER BY r.permission::BIGINT DESC) AS roles," +
" (SELECT count(*) FROM project_members_all pma WHERE pma.user_id = u.id) AS project_count" +
" FROM users u" +
" WHERE u.id IN " +
@ -111,7 +111,7 @@ public interface UsersApiDAO {
" u.tagline," +
" u.join_date," +
" u.locked," +
" array_agg(r.name ORDER BY r.permission::BIGINT DESC) roles," +
" array_agg(r.name ORDER BY r.permission::BIGINT DESC) AS roles," +
" (SELECT count(*) FROM project_members_all pma WHERE pma.user_id = u.id) AS project_count" +
" FROM users u" +
" JOIN user_global_roles ugr ON u.id = ugr.user_id" +

View File

@ -6,6 +6,7 @@ import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
import java.time.OffsetDateTime;
import java.util.Map;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.customtypes.JobState;
import io.papermc.hangar.model.internal.job.JobType;
@ -17,9 +18,9 @@ public class JobTable extends Table {
private final String lastErrorDescriptor;
private final JobState state;
private final JobType jobType;
private final Map<String, String> jobProperties;
private final JSONB jobProperties;
public JobTable(OffsetDateTime lastUpdated, OffsetDateTime retryAt, String lastError, String lastErrorDescriptor, JobState state, @EnumByName JobType jobType, Map<String, String> jobProperties) {
public JobTable(OffsetDateTime lastUpdated, OffsetDateTime retryAt, String lastError, String lastErrorDescriptor, JobState state, @EnumByName JobType jobType, JSONB jobProperties) {
this.lastUpdated = lastUpdated;
this.retryAt = retryAt;
this.lastError = lastError;
@ -30,7 +31,7 @@ public class JobTable extends Table {
}
@JdbiConstructor
public JobTable(OffsetDateTime createdAt, long id, OffsetDateTime lastUpdated, OffsetDateTime retryAt, String lastError, String lastErrorDescriptor, JobState state, @EnumByName JobType jobType, Map<String, String> jobProperties) {
public JobTable(OffsetDateTime createdAt, long id, OffsetDateTime lastUpdated, OffsetDateTime retryAt, String lastError, String lastErrorDescriptor, JobState state, @EnumByName JobType jobType, JSONB jobProperties) {
super(createdAt, id);
this.lastUpdated = lastUpdated;
this.retryAt = retryAt;
@ -65,7 +66,7 @@ public class JobTable extends Table {
return jobType;
}
public Map<String, String> getJobProperties() {
public JSONB getJobProperties() {
return jobProperties;
}

View File

@ -61,6 +61,7 @@ public class ApiKeyTable extends Table implements Named {
", ownerId=" + ownerId +
", tokenIdentifier='" + tokenIdentifier + '\'' +
", token='" + token + '\'' +
", token='" + token + '\'' +
", permissions=" + permissions +
"} " + super.toString();
}

View File

@ -44,7 +44,7 @@ public class DeleteDiscourseTopicJob extends Job {
public static DeleteDiscourseTopicJob loadFromTable(JobTable table) {
DeleteDiscourseTopicJob job = new DeleteDiscourseTopicJob();
job.fromTable(table);
job.setJobProperties(table.getJobProperties());
job.setJobProperties(table.getJobProperties().getMap());
job.loadFromProperties();
return job;
}

View File

@ -1,5 +1,6 @@
package io.papermc.hangar.model.internal.job;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.customtypes.JobState;
import io.papermc.hangar.model.Model;
import io.papermc.hangar.model.db.JobTable;
@ -104,12 +105,12 @@ public abstract class Job extends Model {
this.lastErrorDescriptor = table.getLastErrorDescriptor();
this.state = table.getState();
this.jobType = table.getJobType();
this.jobProperties = table.getJobProperties();
this.jobProperties = table.getJobProperties().getMap();
}
public JobTable toTable() {
saveIntoProperties();
return new JobTable(createdAt, -1, lastUpdated, retryAt, lastError, lastErrorDescriptor, state, jobType, jobProperties);
return new JobTable(createdAt, -1, lastUpdated, retryAt, lastError, lastErrorDescriptor, state, jobType, new JSONB(jobProperties));
}
@Override

View File

@ -4,6 +4,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.model.db.JobTable;
public class PostDiscourseReplyJob extends Job {
@ -75,7 +76,7 @@ public class PostDiscourseReplyJob extends Job {
public static PostDiscourseReplyJob loadFromTable(JobTable table) {
PostDiscourseReplyJob job = new PostDiscourseReplyJob();
job.fromTable(table);
job.setJobProperties(table.getJobProperties());
job.setJobProperties(table.getJobProperties().getMap());
job.loadFromProperties();
return job;
}

View File

@ -44,7 +44,7 @@ public class UpdateDiscourseProjectTopicJob extends Job {
public static UpdateDiscourseProjectTopicJob loadFromTable(JobTable table) {
UpdateDiscourseProjectTopicJob job = new UpdateDiscourseProjectTopicJob();
job.fromTable(table);
job.setJobProperties(table.getJobProperties());
job.setJobProperties(table.getJobProperties().getMap());
job.loadFromProperties();
return job;
}

View File

@ -44,7 +44,7 @@ public class UpdateDiscourseVersionPostJob extends Job {
public static UpdateDiscourseVersionPostJob loadFromTable(JobTable table) {
UpdateDiscourseVersionPostJob job = new UpdateDiscourseVersionPostJob();
job.fromTable(table);
job.setJobProperties(table.getJobProperties());
job.setJobProperties(table.getJobProperties().getMap());
job.loadFromProperties();
return job;
}

View File

@ -12,11 +12,14 @@ import io.papermc.hangar.model.identified.UserIdentified;
import io.papermc.hangar.model.internal.api.requests.CreateAPIKeyForm;
import io.papermc.hangar.model.internal.logs.LogAction;
import io.papermc.hangar.model.internal.logs.contexts.UserContext;
import io.papermc.hangar.util.CryptoUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@ -54,7 +57,8 @@ public class APIKeyService extends HangarComponent {
String tokenIdentifier = UUID.randomUUID().toString();
String token = UUID.randomUUID().toString();
apiKeyDAO.insert(new ApiKeyTable(apiKeyForm.getName(), userIdentified.getUserId(), tokenIdentifier, token, keyPermission));
String hashedToken = CryptoUtils.hmacSha256(config.security.getTokenSecret(), token.getBytes(StandardCharsets.UTF_8));
apiKeyDAO.insert(new ApiKeyTable(apiKeyForm.getName(), userIdentified.getUserId(), tokenIdentifier, hashedToken, keyPermission));
actionLogger.user(LogAction.USER_APIKEY_CREATED.create(UserContext.of(userIdentified.getUserId()), "Key Name: " + apiKeyForm.getName() + "<br>" + apiKeyForm.getPermissions().stream().map(NamedPermission::getFrontendName).collect(Collectors.joining(",<br>")), ""));
return tokenIdentifier + "." + token;
}

View File

@ -1,6 +1,7 @@
package io.papermc.hangar.service.api;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.config.hangar.HangarSecurityConfig;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
import io.papermc.hangar.db.dao.internal.table.auth.ApiKeyDAO;
import io.papermc.hangar.exceptions.HangarApiException;
@ -9,9 +10,12 @@ import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.auth.ApiKeyTable;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.TokenService;
import io.papermc.hangar.util.CryptoUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
@Service
@ -39,7 +43,8 @@ public class APIAuthenticationService extends HangarComponent {
}
String identifier = apiKey.split("\\.")[0];
String token = apiKey.split("\\.")[1];
ApiKeyTable apiKeyTable = apiKeyDAO.findApiKey(identifier, token);
String hashedToken = CryptoUtils.hmacSha256(config.security.getTokenSecret(), token.getBytes(StandardCharsets.UTF_8));
ApiKeyTable apiKeyTable = apiKeyDAO.findApiKey(identifier, hashedToken);
if (apiKeyTable == null) {
throw new HangarApiException("No valid API Key found");
}

View File

@ -30,7 +30,7 @@ public class HealthService extends HangarComponent {
}
public List<UnhealthyProject> getStaleProjects() {
return healthDAO.getStaleProjects(config.projects.getStaleAge().toSeconds());
return healthDAO.getStaleProjects("'" + config.projects.getStaleAge().toSeconds() + " SECONDS'");
}
public List<UnhealthyProject> getNonPublicProjects() {

View File

@ -39,6 +39,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.nio.file.Files;
@ -108,6 +109,9 @@ public class ProjectService extends HangarComponent {
public HangarProject getHangarProject(String author, String slug) {
Pair<Long, Project> project = hangarProjectsDAO.getProject(author, slug, getHangarUserId());
if (project == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
ProjectOwner projectOwner = getProjectOwner(author);
var members = hangarProjectsDAO.getProjectMembers(project.getLeft(), getHangarUserId(), permissionService.getProjectPermissions(getHangarUserId(), project.getLeft()).has(Permission.EditProjectSettings));
String lastVisibilityChangeComment = "";
@ -217,7 +221,8 @@ public class ProjectService extends HangarComponent {
}
public void refreshHomeProjects() {
hangarProjectsDAO.refreshHomeProjects();
// hangarProjectsDAO.refreshHomeProjects();
// TODO fix refreshHomeProjects: ERROR: cannot refresh view in an explicit transaction
}
public List<UserTable> getProjectWatchers(long projectId) {

View File

@ -15,16 +15,21 @@ public class CryptoUtils {
private static final char[] hexArray = "0123456789abcdef".toCharArray();
public static byte[] hmac(String algo, byte[] secret, byte[] data) throws NoSuchAlgorithmException, InvalidKeyException {
public static byte[] hmac(String algo, byte[] secret, byte[] data) {
Preconditions.checkArgument(secret.length != 0, "empty secret");
Preconditions.checkArgument(data.length != 0, "nothing to hash!");
try {
Mac hmac = Mac.getInstance(algo);
SecretKeySpec keySpec = new SecretKeySpec(secret, algo);
hmac.init(keySpec);
return hmac.doFinal(data);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static String hmacSha256(String secret, byte[] data) throws InvalidKeyException, NoSuchAlgorithmException {
public static String hmacSha256(String secret, byte[] data) {
return bytesToHex(hmac("HmacSHA256", secret.getBytes(StandardCharsets.UTF_8), data));
}

View File

@ -30,6 +30,19 @@ spring:
WRITE_DATES_AS_TIMESTAMPS: false
date-format: com.fasterxml.jackson.databind.util.StdDateFormat
###############
# Email Stuff #
###############
# mail:
# host: smtp.gmail.com
# username: username
# password: password
# properties:
# mail.transport.protocol: smtp
# mail.smtp.port: 587
# mail.smtp.auth: true
# mail.smtp.starttls.enable: true
#############
# Fake User #
#############
@ -164,6 +177,7 @@ hangar:
logging:
level:
root: INFO
org.springframework: DEBUG
org.springframework: INFO
org.springframework.context.support.PostProcessorRegistrationDelegate: WARN
io.papermc.hangar.service.internal.JobService: DEBUG
io.papermc.hangar.config.WebConfig.LoggingInterceptor: DEBUG

View File

@ -152,7 +152,7 @@ CREATE TABLE project_pages
);
CREATE INDEX page_slug_idx
ON project_pages (slug);
ON project_pages (lower(slug));
CREATE INDEX page_parent_id_idx
ON project_pages (parent_id);
@ -974,13 +974,15 @@ SELECT p.id,
p.description,
p.name,
p.created_at,
max(lv.created_at) AS last_updated--,--
max(lv.created_at) AS last_updated,
-- to_jsonb(ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', tags.tag_name,
-- 'tag_version', tags.tag_version, 'tag_color',
-- tags.tag_color) AS jsonb_build_object
-- FROM tags
-- WHERE tags.project_id = p.id
-- LIMIT 5)) AS promoted_versions,
--TODO fix homepage view
ARRAY(SELECT 1 WHERE FALSE) AS promoted_versions,
-- ((setweight((to_tsvector('english'::regconfig, p.name::text) ||
-- to_tsvector('english'::regconfig, regexp_replace(p.name::text, '([a-z])([A-Z]+)'::text,
-- '\1_\2'::text, 'g'::text))), 'A'::"char") ||
@ -993,6 +995,8 @@ SELECT p.id,
-- '\1_\2'::text,
-- 'g'::text)),
-- 'D'::"char") AS search_words
-- TODO fix homepage view
'DUM' AS search_words
FROM projects p
LEFT JOIN project_versions lv ON p.id = lv.project_id
JOIN project_members_all pm ON p.id = pm.id

View File

@ -6,5 +6,5 @@ SELECT
(SELECT count(*) FROM project_flags WHERE created_at::date <= day AND (created_at::date >= day OR resolved_at IS NULL)) flagsOpened,
(SELECT count(*) FROM project_flags WHERE resolved_at::date = day) flagsClosed,
day::date
FROM (SELECT generate_series(:startDate, :endDate, INTERVAL '1 DAY') AS day) dates
FROM (SELECT generate_series(:startDate::timestamp, :endDate::timestamp, INTERVAL '1 DAY') AS day) dates
ORDER BY day