mirror of
https://github.com/modrinth/code.git
synced 2026-08-29 02:54:51 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f245cacd | ||
|
|
641ad9942a | ||
|
|
58f89d9940 | ||
|
|
30477f3316 | ||
|
|
a7196deb98 | ||
|
|
9f28a9ba5f | ||
|
|
b5b7e9bc42 | ||
|
|
0af562ef6f | ||
|
|
e9200c7483 | ||
|
|
fe9ae94a5d | ||
|
|
871a161327 | ||
|
|
797ec2ec3c | ||
|
|
237963bb35 | ||
|
|
ba667921d1 | ||
|
|
8c2611805b | ||
|
|
c204e00ebc | ||
|
|
5cf9d423ad | ||
|
|
ac00e2bd5d | ||
|
|
43818e4f90 | ||
|
|
7ef2492d03 | ||
|
|
37d371e46c | ||
|
|
26e1d645b8 | ||
|
|
04d00c0ba0 | ||
|
|
7f59feeb98 | ||
|
|
4c8abfb78d | ||
|
|
5ead1499cd | ||
|
|
ddb45f6aac | ||
|
|
cc77fd1b87 | ||
|
|
b92fc10e54 | ||
|
|
849943ed4e | ||
|
|
18c95e2f61 | ||
|
|
7c7efd5efe | ||
|
|
4c5e1074fd | ||
|
|
36d3aab0de | ||
|
|
88c0915066 | ||
|
|
f0dff8ebd5 | ||
|
|
8d82c228cf | ||
|
|
4b07ed720e | ||
|
|
ea42c644f3 | ||
|
|
28154689f4 | ||
|
|
3c77cc1502 | ||
|
|
b6b19e543c | ||
|
|
023c789a31 |
Generated
+80
-3
@@ -437,6 +437,23 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "antlr4rust"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093d520274bfff7278d776f7ea12981a0a0a6f96db90964658e0f38fc6e9a6a6"
|
||||
dependencies = [
|
||||
"better_any",
|
||||
"bit-set 0.8.0",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"murmur3",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"typed-arena",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
@@ -1421,10 +1438,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"pastey",
|
||||
"pastey 0.1.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "better_any"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -1451,7 +1474,16 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1460,6 +1492,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.3"
|
||||
@@ -1889,6 +1927,23 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cel"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814"
|
||||
dependencies = [
|
||||
"antlr4rust",
|
||||
"base64 0.22.1",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"pastey 0.2.3",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "censor"
|
||||
version = "0.3.0"
|
||||
@@ -3530,7 +3585,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-set 0.5.3",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
@@ -5483,6 +5538,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
"cel",
|
||||
"censor",
|
||||
"chrono",
|
||||
"chumsky",
|
||||
@@ -6170,6 +6226,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8"
|
||||
|
||||
[[package]]
|
||||
name = "murmur3"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mutually_exclusive_features"
|
||||
version = "0.1.0"
|
||||
@@ -7192,6 +7257,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "path-util"
|
||||
version = "0.0.0"
|
||||
@@ -11863,6 +11934,12 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.0"
|
||||
|
||||
@@ -57,6 +57,7 @@ bitflags = "2.9.4"
|
||||
bon = "3.9.3"
|
||||
bytemuck = "1.24.0"
|
||||
bytes = "1.10.1"
|
||||
cel = { version = "0.14.0", default-features = false, features = ["json", "regex"] }
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
|
||||
@@ -1602,9 +1602,8 @@ async function downloadUpdate(versionToDownload) {
|
||||
.then(() => {
|
||||
downloading.value = false
|
||||
finishedDownloading.value = true
|
||||
unlistenUpdateDownload?.().then(() => {
|
||||
unlistenUpdateDownload = null
|
||||
})
|
||||
unlistenUpdateDownload?.()
|
||||
unlistenUpdateDownload = null
|
||||
console.log('Finished downloading!')
|
||||
markAppUpdateActionable(versionToDownload.version, 'downloaded')
|
||||
scheduleDelayedUpdatePopup()
|
||||
|
||||
@@ -482,8 +482,7 @@ onUnmounted(() => {
|
||||
:aria-valuemin="1"
|
||||
:aria-valuemax="maxVisibleItems"
|
||||
:aria-valuenow="visibleItemCount"
|
||||
class="group/resize relative inset-x-0 bottom-0 flex h-6 -mt-3 w-1/3 mx-auto cursor-ns-resize touch-none select-none items-center justify-center opacity-0 transition-opacity duration-200 hover:opacity-100 focus-visible:opacity-100"
|
||||
:class="{ 'opacity-100': resizing }"
|
||||
class="group/resize relative inset-x-0 bottom-0 flex h-6 -mt-3 w-1/3 mx-auto cursor-ns-resize touch-none select-none items-center justify-center"
|
||||
@pointerdown="onResizePointerDown"
|
||||
@pointermove="onResizePointerMove"
|
||||
@pointerup="endResize"
|
||||
@@ -496,11 +495,11 @@ onUnmounted(() => {
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span
|
||||
class="h-px w-6 transition-colors"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-surface-3 group-hover/resize:bg-secondary'"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-secondary'"
|
||||
/>
|
||||
<span
|
||||
class="h-px w-6 transition-colors"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-surface-3 group-hover/resize:bg-secondary'"
|
||||
:class="showOverdrag ? 'bg-red' : 'bg-secondary'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,8 @@
|
||||
<p class="m-0 break-words font-semibold text-contrast">
|
||||
{{ trace.project_name }}
|
||||
</p>
|
||||
<p class="m-0 mt-1 flex flex-wrap items-center gap-1 text-sm text-secondary">
|
||||
<span class="break-all">{{ trace.version_number }}</span>
|
||||
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span class="break-all">{{ decodeTracePath(trace.file_name) }}</span>
|
||||
<template v-if="trace.jar">
|
||||
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span class="break-all">{{ decodeTracePath(trace.jar) }}</span>
|
||||
</template>
|
||||
<p class="m-0 mt-1 text-sm text-secondary">
|
||||
<IssueDetailPath :segments="[trace.version_number, trace.file_name, trace.jar]" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -31,9 +25,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ChevronRightIcon, ExternalIcon } from '@modrinth/assets'
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonLink } from '@modrinth/ui'
|
||||
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
|
||||
}>()
|
||||
@@ -44,12 +40,4 @@ const localTraceLink = computed(
|
||||
props.trace.detail_id,
|
||||
)}`,
|
||||
)
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</p>
|
||||
<p class="m-0 break-all text-secondary">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }}
|
||||
<IssueDetailPath :segments="[getLatestLocalTrace(trace)?.file_path]" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,6 +131,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -163,24 +164,19 @@ function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDeta
|
||||
return trace.local_traces.at(-1)
|
||||
}
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function getSeverityBadgeColor(
|
||||
severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined,
|
||||
): string {
|
||||
switch (severity) {
|
||||
case 'malware':
|
||||
case 'severe':
|
||||
return 'border-red/60 bg-highlight-red text-red'
|
||||
case 'high':
|
||||
return 'border-orange/60 bg-highlight-orange text-orange'
|
||||
case 'medium':
|
||||
return 'border-green/60 bg-highlight-green text-green'
|
||||
case 'hidden':
|
||||
return 'border-divider bg-surface-2 text-secondary'
|
||||
case 'low':
|
||||
default:
|
||||
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<span class="inline-flex min-w-0 flex-wrap items-center gap-1">
|
||||
<template v-for="(segment, index) in decodedSegments" :key="`${segment}-${index}`">
|
||||
<ChevronRightIcon v-if="index > 0" class="size-4 shrink-0" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="isTruncated(segment) ? segment : undefined"
|
||||
class="break-all"
|
||||
:class="{
|
||||
'font-semibold text-contrast': emphasizeLast && index === decodedSegments.length - 1,
|
||||
'text-secondary': emphasizeLast && index < decodedSegments.length - 1,
|
||||
}"
|
||||
>
|
||||
{{ formatSegment(segment) }}
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
segments: readonly (string | null | undefined)[]
|
||||
truncate?: boolean
|
||||
maxLength?: number
|
||||
emphasizeLast?: boolean
|
||||
decode?: boolean
|
||||
hideBaseMrpack?: boolean
|
||||
}>(),
|
||||
{
|
||||
truncate: false,
|
||||
maxLength: 120,
|
||||
emphasizeLast: false,
|
||||
decode: true,
|
||||
hideBaseMrpack: false,
|
||||
},
|
||||
)
|
||||
|
||||
const decodedSegments = computed(() => {
|
||||
const segments = props.segments
|
||||
.flatMap((segment) => segment?.split('#') ?? [])
|
||||
.filter((segment) => segment.length > 0)
|
||||
.map((segment) => (props.decode ? decodePath(segment) : segment))
|
||||
|
||||
if (props.hideBaseMrpack && segments[0]?.toLowerCase().endsWith('.mrpack')) {
|
||||
return segments.slice(1)
|
||||
}
|
||||
|
||||
return segments
|
||||
})
|
||||
|
||||
function decodePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function isTruncated(segment: string): boolean {
|
||||
return props.truncate && segment.length > props.maxLength
|
||||
}
|
||||
|
||||
function formatSegment(segment: string): string {
|
||||
if (!isTruncated(segment)) return segment
|
||||
|
||||
const separator = '...'
|
||||
const charsToShow = props.maxLength - separator.length
|
||||
const frontChars = Math.ceil(charsToShow / 3)
|
||||
const backChars = Math.floor((charsToShow * 2) / 3)
|
||||
return segment.slice(0, frontChars) + separator + segment.slice(-backChars)
|
||||
}
|
||||
</script>
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
CheckCheckIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClipboardCopyIcon,
|
||||
CodeIcon,
|
||||
CopyIcon,
|
||||
@@ -48,6 +47,7 @@ import {
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
|
||||
import ThreadView from '~/components/ui/thread/ThreadView.vue'
|
||||
|
||||
@@ -227,7 +227,10 @@ async function updateGlobalIssueDetail(
|
||||
])
|
||||
}
|
||||
|
||||
const severityOrder = { severe: 3, high: 2, medium: 1, low: 0 } as Record<string, number>
|
||||
const severityOrder = { malware: 4, severe: 3, high: 2, medium: 1, low: 0, hidden: -1 } as Record<
|
||||
string,
|
||||
number
|
||||
>
|
||||
|
||||
type DetailDecision = 'safe' | 'malware' | 'pending'
|
||||
type DetailDecisionScope = 'local' | 'global'
|
||||
@@ -491,12 +494,15 @@ function handleTabClick(index: number) {
|
||||
|
||||
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
|
||||
switch (severity) {
|
||||
case 'malware':
|
||||
case 'severe':
|
||||
return 'border-red/60 border bg-highlight-red text-red'
|
||||
case 'high':
|
||||
return 'border-orange/60 border bg-highlight-orange text-orange'
|
||||
case 'medium':
|
||||
return 'border-green/60 border bg-highlight-green text-green'
|
||||
case 'hidden':
|
||||
return 'border-divider border bg-surface-2 text-secondary'
|
||||
case 'low':
|
||||
default:
|
||||
return 'border-blue/60 border bg-highlight-blue text-blue'
|
||||
@@ -515,12 +521,15 @@ function truncateMiddle(str: string, maxLength: number = 120): string {
|
||||
|
||||
const severityColor = computed(() => {
|
||||
switch (highestSeverity.value) {
|
||||
case 'malware':
|
||||
case 'severe':
|
||||
return 'text-red bg-highlight-red border-solid border-[1px] border-red'
|
||||
case 'high':
|
||||
return 'text-orange bg-highlight-orange border-solid border-[1px] border-orange'
|
||||
case 'medium':
|
||||
return 'text-green bg-highlight-green border-solid border-[1px] border-green'
|
||||
case 'hidden':
|
||||
return 'text-secondary bg-surface-2 border-solid border-[1px] border-divider'
|
||||
case 'low':
|
||||
default:
|
||||
return 'text-blue bg-highlight-blue border-solid border-[1px] border-blue'
|
||||
@@ -604,6 +613,29 @@ async function copyToClipboard(code: string, detailId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDetailCelInput(detailId: string) {
|
||||
if (copyingCelDetails.has(detailId)) return
|
||||
|
||||
copyingCelDetails.add(detailId)
|
||||
try {
|
||||
const input = await client.labrinth.tech_review_internal.getDetailRuleInput(detailId)
|
||||
await navigator.clipboard.writeText(JSON.stringify(input, null, 2))
|
||||
copiedCelDetails.add(detailId)
|
||||
setTimeout(() => {
|
||||
copiedCelDetails.delete(detailId)
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy CEL input:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to copy CEL input',
|
||||
text: 'An error occurred while loading the trace rule input.',
|
||||
})
|
||||
} finally {
|
||||
copyingCelDetails.delete(detailId)
|
||||
}
|
||||
}
|
||||
|
||||
function getDetailDecision(
|
||||
detailId: string,
|
||||
backendStatus: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
|
||||
@@ -1010,6 +1042,8 @@ async function updateGlobalDetailStatus(
|
||||
const expandedClasses = reactive<Set<string>>(new Set())
|
||||
const autoExpandedFileIds = reactive<Set<string>>(new Set())
|
||||
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
|
||||
const copyingCelDetails = reactive<Set<string>>(new Set())
|
||||
const copiedCelDetails = reactive<Set<string>>(new Set())
|
||||
const highlightedSourceCache = reactive<Map<string, { source: string; lines: string[] }>>(new Map())
|
||||
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2
|
||||
|
||||
@@ -1036,7 +1070,7 @@ interface JarGroup {
|
||||
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
|
||||
if (!jar) return []
|
||||
const segments = jar
|
||||
.split(/[/#]/)
|
||||
.split('#')
|
||||
.map((s) => decodeURIComponent(s.trim()))
|
||||
.filter((s) => s.length > 0)
|
||||
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
|
||||
@@ -1820,27 +1854,12 @@ function copyId() {
|
||||
class="border-b border-solid border-surface-1 px-4 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<template
|
||||
v-for="(segment, index) in jarGroup.segments"
|
||||
:key="`${jarGroup.key}-${index}`"
|
||||
>
|
||||
<span
|
||||
class="font-mono text-sm"
|
||||
:class="
|
||||
index === jarGroup.segments.length - 1
|
||||
? 'font-semibold text-contrast'
|
||||
: 'text-secondary'
|
||||
"
|
||||
>
|
||||
{{ segment }}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="index < jarGroup.segments.length - 1"
|
||||
class="size-4 text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<IssueDetailPath
|
||||
:segments="jarGroup.segments"
|
||||
:decode="false"
|
||||
class="font-mono text-sm"
|
||||
emphasize-last
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="getJarRemainingUnmarkedCount(jarGroup) > 0"
|
||||
@@ -1914,9 +1933,12 @@ function copyId() {
|
||||
<ChevronDownIcon class="h-5 w-5 text-contrast" />
|
||||
</IconButton>
|
||||
|
||||
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
|
||||
truncateMiddle(classItem.filePath)
|
||||
}}</span>
|
||||
<IssueDetailPath
|
||||
:segments="[classItem.jar, classItem.filePath]"
|
||||
class="font-mono font-semibold"
|
||||
hide-base-mrpack
|
||||
truncate
|
||||
/>
|
||||
|
||||
<div
|
||||
class="rounded-full border-solid px-2.5 py-1"
|
||||
@@ -1991,6 +2013,20 @@ function copyId() {
|
||||
</div>
|
||||
|
||||
<div class="detail-verdict-action-groups">
|
||||
<Button
|
||||
:disabled="copyingCelDetails.has(flag.detail.id)"
|
||||
@click="copyDetailCelInput(flag.detail.id)"
|
||||
>
|
||||
<LoaderCircleIcon
|
||||
v-if="copyingCelDetails.has(flag.detail.id)"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ClipboardCopyIcon v-else aria-hidden="true" />
|
||||
<span aria-live="polite">
|
||||
{{ copiedCelDetails.has(flag.detail.id) ? 'Copied!' : 'Copy CEL' }}
|
||||
</span>
|
||||
</Button>
|
||||
<div
|
||||
class="detail-verdict-buttons"
|
||||
role="group"
|
||||
|
||||
@@ -47,7 +47,7 @@ function createLockStatuses(
|
||||
) as Record<DisclosureType, DisclosureLockStatus>
|
||||
|
||||
for (const disclosure of disclosures) {
|
||||
lockStatuses[disclosure.type] = disclosure.lock_status
|
||||
lockStatuses[disclosure.type] = disclosure.lock_status ?? 'unlocked'
|
||||
}
|
||||
|
||||
return lockStatuses
|
||||
|
||||
@@ -2957,6 +2957,9 @@
|
||||
"moderation.moderate": {
|
||||
"message": "Moderate"
|
||||
},
|
||||
"moderation.page.delphi-rules": {
|
||||
"message": "Delphi rules"
|
||||
},
|
||||
"moderation.page.external-projects": {
|
||||
"message": "External projects"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import {
|
||||
FolderIcon,
|
||||
GlobeIcon,
|
||||
HashIcon,
|
||||
ReportIcon,
|
||||
SettingsIcon,
|
||||
ShieldCheckIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { getMarginTarget } from '@modrinth/moderation'
|
||||
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||
|
||||
@@ -58,6 +65,10 @@ const messages = defineMessages({
|
||||
id: 'moderation.page.global-detail-traces',
|
||||
defaultMessage: 'Global traces',
|
||||
},
|
||||
delphiRulesTitle: {
|
||||
id: 'moderation.page.delphi-rules',
|
||||
defaultMessage: 'Delphi rules',
|
||||
},
|
||||
})
|
||||
|
||||
const moderationLinks = [
|
||||
@@ -78,6 +89,11 @@ const moderationLinks = [
|
||||
href: '/moderation/global-traces',
|
||||
icon: HashIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.delphiRulesTitle),
|
||||
href: '/moderation/technical-review/rules',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const mobileNavOptions = [
|
||||
@@ -86,12 +102,15 @@ const mobileNavOptions = [
|
||||
formatMessage(messages.reportsTitle),
|
||||
formatMessage(messages.externalFilesTitle),
|
||||
formatMessage(messages.globalDetailTracesTitle),
|
||||
formatMessage(messages.delphiRulesTitle),
|
||||
]
|
||||
|
||||
const selectedChip = computed({
|
||||
get() {
|
||||
const path = route.path
|
||||
if (path.startsWith('/moderation/technical-review')) {
|
||||
if (path.startsWith('/moderation/technical-review/rules')) {
|
||||
return formatMessage(messages.delphiRulesTitle)
|
||||
} else if (path.startsWith('/moderation/technical-review')) {
|
||||
return formatMessage(messages.technicalReviewTitle)
|
||||
} else if (path.startsWith('/moderation/reports')) {
|
||||
return formatMessage(messages.reportsTitle)
|
||||
@@ -117,6 +136,8 @@ function navigateToPage(selectedOption: string) {
|
||||
router.push('/moderation/external-projects')
|
||||
} else if (selectedOption === formatMessage(messages.globalDetailTracesTitle)) {
|
||||
router.push('/moderation/global-traces')
|
||||
} else if (selectedOption === formatMessage(messages.delphiRulesTitle)) {
|
||||
router.push('/moderation/technical-review/rules')
|
||||
} else {
|
||||
router.push('/moderation')
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</p>
|
||||
<p class="m-0 break-all text-secondary">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ decodeTracePath(latestLocalTrace.file_path) }}
|
||||
<IssueDetailPath :segments="[latestLocalTrace.file_path]" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -134,22 +135,17 @@ const pageEnd = computed(() =>
|
||||
)
|
||||
const latestLocalTrace = computed(() => trace.value?.local_traces.at(-1))
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
|
||||
switch (severity) {
|
||||
case 'malware':
|
||||
case 'severe':
|
||||
return 'border-red/60 bg-highlight-red text-red'
|
||||
case 'high':
|
||||
return 'border-orange/60 bg-highlight-orange text-orange'
|
||||
case 'medium':
|
||||
return 'border-green/60 bg-highlight-green text-green'
|
||||
case 'hidden':
|
||||
return 'border-divider bg-surface-2 text-secondary'
|
||||
case 'low':
|
||||
default:
|
||||
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
ListFilterIcon,
|
||||
LoaderCircleIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonLink,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
@@ -652,6 +654,11 @@ onUnmounted(() => {
|
||||
<div
|
||||
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
|
||||
>
|
||||
<ButtonLink to="/moderation/technical-review/rules">
|
||||
<SettingsIcon class="size-5" />
|
||||
Rules
|
||||
</ButtonLink>
|
||||
|
||||
<Combobox
|
||||
v-model="currentResponseFilter"
|
||||
class="!w-full flex-grow sm:!w-[120px] sm:flex-grow-0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+192
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tSELECT\n\t\t\tdelphi_rule.id AS \"id!: DelphiRuleId\",\n\t\t\tdelphi_rule.name,\n\t\t\tdelphi_rule.rule,\n\t\t\tdelphi_rule.priority,\n\t\t\tdelphi_rule.revision,\n\t\t\t(\n\t\t\t\tSELECT revision FROM delphi_rule_revisions LIMIT 1\n\t\t\t) AS \"current_revision!\",\n\t\t\tdelphi_rule.created_at,\n\t\t\tdelphi_rule.updated_at,\n\t\t\tdelphi_rule.created_by,\n\t\t\tdelphi_rule.updated_by,\n\t\t\tCOALESCE(preview.affected_details_count, 0)\n\t\t\t\tAS \"affected_details_count!\",\n\t\t\tpreview.detail_id AS \"detail_id?: DelphiReportIssueDetailsId\",\n\t\t\tpreview.issue_id AS \"issue_id?: DelphiReportIssueId\",\n\t\t\tpreview.project_id AS \"project_id?: DBProjectId\",\n\t\t\tpreview.project_name AS \"project_name?\",\n\t\t\tpreview.project_icon_url AS \"project_icon_url?\",\n\t\t\tpreview.version_id AS \"version_id?: DBVersionId\",\n\t\t\tpreview.version_name AS \"version_name?\",\n\t\t\tpreview.version_number AS \"version_number?\",\n\t\t\tpreview.issue_type AS \"issue_type?\",\n\t\t\tpreview.key AS \"key?\",\n\t\t\tpreview.jar AS \"jar?\",\n\t\t\tpreview.file_path AS \"file_path?\",\n\t\t\tpreview.original_severity AS \"original_severity?: DelphiSeverity\",\n\t\t\tpreview.severity AS \"effect_severity?: DelphiSeverity\"\n\t\tFROM delphi_rules delphi_rule\n\t\tLEFT JOIN LATERAL (\n\t\t\tSELECT\n\t\t\t\teffect.detail_id,\n\t\t\t\tdetail.issue_id,\n\t\t\t\tversion.mod_id AS project_id,\n\t\t\t\tproject.name AS project_name,\n\t\t\t\tproject.icon_url AS project_icon_url,\n\t\t\t\tversion.id AS version_id,\n\t\t\t\tversion.name AS version_name,\n\t\t\t\tversion.version_number,\n\t\t\t\tissue.issue_type,\n\t\t\t\tdetail.key,\n\t\t\t\tdetail.jar,\n\t\t\t\tdetail.file_path,\n\t\t\t\tdetail.severity AS original_severity,\n\t\t\t\teffect.severity,\n\t\t\t\tCOUNT(*) OVER () AS affected_details_count\n\t\t\tFROM delphi_rule_effects effect\n\t\t\tINNER JOIN delphi_rule_revisions published\n\t\t\t\tON published.revision = effect.revision\n\t\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\t\tON detail.id = effect.detail_id\n\t\t\tINNER JOIN delphi_report_issues issue\n\t\t\t\tON issue.id = detail.issue_id\n\t\t\tINNER JOIN delphi_reports report\n\t\t\t\tON report.id = issue.report_id\n\t\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\t\tWHERE effect.rule_id = delphi_rule.id\n\t\t\tORDER BY effect.detail_id DESC\n\t\t\tLIMIT 3\n\t\t) preview ON TRUE\n\t\tWHERE NOT delphi_rule.delete_on_next_revision\n\t\tORDER BY\n\t\t\tdelphi_rule.priority DESC,\n\t\t\tdelphi_rule.id,\n\t\t\tpreview.detail_id DESC\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!: DelphiRuleId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "rule",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "priority",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "revision",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "current_revision!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "affected_details_count!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "detail_id?: DelphiReportIssueDetailsId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "issue_id?: DelphiReportIssueId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "project_id?: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "project_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "project_icon_url?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "version_id?: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "version_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "version_number?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "issue_type?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "key?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "jar?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "file_path?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "original_severity?: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "effect_severity?: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "020f0da009002bd07c9711d617ea83f82b979397364ac9f61abb9bc8833a0d51"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH project_membership AS (\n SELECT\n detail.project_id,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ) AS old_needs_review,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND COALESCE(\n new_effect.severity,\n detail.original_severity\n ) != 'hidden'\n ) AS new_needs_review\n FROM delphi_issue_details_with_statuses detail\n LEFT JOIN delphi_rule_effects new_effect\n ON new_effect.revision = $1\n AND new_effect.detail_id = detail.id\n GROUP BY detail.project_id\n )\n SELECT\n project_id AS \"project_id!: DBProjectId\",\n new_needs_review AS \"new_needs_review!\"\n FROM project_membership\n WHERE old_needs_review IS DISTINCT FROM new_needs_review\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "new_needs_review!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9"
|
||||
}
|
||||
Generated
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = $1\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = $1\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "thread_id: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "last_tech_review_message_type",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT revision FROM delphi_rule_revisions LIMIT 1 FOR UPDATE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "revision",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH inserted AS (\n INSERT INTO delphi_tech_review_queue (project_id)\n SELECT unnest($1::bigint[])\n ON CONFLICT (project_id) DO NOTHING\n RETURNING project_id\n )\n SELECT\n inserted.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = inserted.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM inserted\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5"
|
||||
}
|
||||
Generated
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0d01a3991e7551a8b7936bf8f4cc1760d2e89af99dd71849eda35d6c6820aa43"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = ANY($1::bigint[])\n ORDER BY didws.issue_id, didws.id\n ",
|
||||
"query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = ANY($1::bigint[])\n AND didws.severity != 'hidden'\n ORDER BY didws.issue_id, didws.id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -41,10 +41,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -117,5 +119,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73"
|
||||
"hash": "15d8434ef325e21959ef8ab0541f78f647125717e2d157e0769e80aeae2cdc90"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE delphi_rules SET revision = $1 WHERE NOT delete_on_next_revision",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848"
|
||||
}
|
||||
Generated
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id AS \"id!: DelphiRuleId\", name, rule\n FROM delphi_rules\n WHERE NOT delete_on_next_revision\n ORDER BY priority DESC, id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!: DelphiRuleId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "rule",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "182be4110f45b6be6a9174200a07a887cebbaeab0a8c1da8b008e0c5e94fbc4e"
|
||||
}
|
||||
Generated
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND didws.status = 'pending'\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM delphi_rules WHERE delete_on_next_revision",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "20da3e21ce6115bd80746be3f6e7273771aed45eea03e46c23ef74a0a59ecfe3"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = $1\n RETURNING project_id AS \"project_id: DBProjectId\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e"
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND dri.issue_type != '__dummy'\n ORDER BY didws.id\n LIMIT $3\n ",
|
||||
"query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND didws.severity != 'hidden'\n ORDER BY didws.id\n LIMIT $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -76,10 +76,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -144,5 +146,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a"
|
||||
"hash": "36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575"
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ",
|
||||
"query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND didws.severity != 'hidden'\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -76,10 +76,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -144,5 +146,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005"
|
||||
"hash": "3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*) AS \"count!\"\n FROM delphi_report_issue_details\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e"
|
||||
}
|
||||
Generated
+120
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "severity: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "jar",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "data: Json<HashMap<String, serde_json::Value>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "delphi_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "size?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "file_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "version_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "project_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "project_types!: Vec<String>",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "loaders!: Vec<String>",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "448ba47493dc3b83b33ce83316c2ad488032235981bc118532ad1094701eae2d"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n\t\t\t\t\t\tdri.report_id = dr.id\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ) AS \"data!: sqlx::types::Json<FileReport>\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "data!: sqlx::types::Json<FileReport>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3"
|
||||
}
|
||||
Generated
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT project_id, type, metadata\n FROM project_disclosures\n WHERE project_id = ANY($1)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4ccad9e07ae61ba87b1784b6ee9a136ddbe802cd3a4c649e4982d9f28ae097ed"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT requested.project_id AS \"project_id!: DBProjectId\"\n FROM unnest($1::bigint[]) AS requested(project_id)\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.project_id = requested.project_id\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM delphi_rule_effects WHERE revision <> $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ",
|
||||
"query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53"
|
||||
"hash": "563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
|
||||
"query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d"
|
||||
"hash": "5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323"
|
||||
}
|
||||
Generated
-60
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH project_ids AS (\n SELECT unnest($1::bigint[]) AS project_id\n ),\n detail_states AS (\n SELECT\n p.project_id,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_pending_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'unsafe'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_unsafe_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type = $3\n ),\n FALSE\n ) AS has_dummy\n FROM project_ids p\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = p.project_id\n LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n GROUP BY p.project_id\n )\n SELECT\n p.project_id AS \"project_id!: DBProjectId\",\n p.has_pending_detail AS \"has_pending_detail!\",\n p.has_unsafe_detail AS \"has_unsafe_detail!\",\n p.has_dummy AS \"has_dummy!\",\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = p.project_id\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = p.project_id\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\",\n (\n SELECT dr.id\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n WHERE v.mod_id = p.project_id\n ORDER BY dr.created DESC, dr.id DESC\n LIMIT 1\n ) AS \"report_id: DelphiReportId\"\n FROM detail_states p\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "has_pending_detail!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "has_unsafe_detail!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "has_dummy!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "thread_id: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_tech_review_message_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "report_id: DelphiReportId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (\n cardinality($7::text[]) = 0\n OR EXISTS (\n SELECT 1\n FROM versions issue_version\n INNER JOIN files issue_file\n ON issue_file.version_id = issue_version.id\n INNER JOIN delphi_reports issue_report\n ON issue_report.file_id = issue_file.id\n INNER JOIN delphi_report_issues issue\n ON issue.report_id = issue_report.id\n INNER JOIN delphi_issue_details_with_statuses detail\n ON detail.issue_id = issue.id\n WHERE\n issue_version.mod_id = m.id\n AND issue.issue_type = ANY($7::text[])\n AND detail.severity != 'hidden'\n )\n )\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_try_advisory_xact_lock($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pg_try_advisory_xact_lock",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f"
|
||||
}
|
||||
Generated
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN (\n SELECT\n loader_version.version_id,\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n GROUP BY loader_version.version_id\n ) version_metadata\n ON version_metadata.version_id = version.id\n LEFT JOIN (\n SELECT\n file_id,\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n GROUP BY file_id\n ) file_hashes ON file_hashes.file_id = file.id\n ORDER BY detail.id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "severity: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "jar",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "file_path",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "data: Json<HashMap<String, serde_json::Value>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "delphi_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "size?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "file_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "version_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "project_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "project_types!: Vec<String>",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "loaders!: Vec<String>",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "6cbc61717a4ac5dd6d1b07137643526bf321a861f42b59c9b21c6f50196d3158"
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.key = ANY($1::text[])\n ",
|
||||
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.id = ANY($1::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,12 +11,12 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91"
|
||||
"hash": "6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "LOCK TABLE delphi_rules IN SHARE MODE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'severity', COALESCE((\n SELECT MAX(didws.severity)\n FROM delphi_report_issues severity_issue\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = severity_issue.id\n WHERE\n severity_issue.report_id = dr.id\n AND didws.severity != 'hidden'\n ), 'low'::delphi_severity),\n -- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n dri.report_id = dr.id\n AND EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses visible_detail\n WHERE\n visible_detail.issue_id = dri.id\n AND visible_detail.severity != 'hidden'\n )\n )\n ) AS \"data!: sqlx::types::Json<FileReport>\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "data!: sqlx::types::Json<FileReport>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f"
|
||||
}
|
||||
Generated
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id\n\t\t\tFROM project_disclosures\n\t\t\tWHERE type = $1 AND project_id = ANY($2)\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "87d4778e52ac3e9fd885f9c77595705f8efc09ac21e0cf9a15c4cce019aaeb05"
|
||||
}
|
||||
Generated
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO delphi_rule_effects (\n revision,\n detail_id,\n rule_id,\n severity\n )\n SELECT $1, effect.*\n FROM UNNEST(\n $2::BIGINT[],\n $3::BIGINT[],\n $4::delphi_severity[]\n ) AS effect(detail_id, rule_id, severity)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8Array",
|
||||
"Int8Array",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "delphi_severity[]",
|
||||
"kind": {
|
||||
"Array": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675"
|
||||
}
|
||||
Generated
+3
-1
@@ -26,10 +26,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-1
@@ -22,10 +22,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM delphi_report_issue_details drid\n WHERE issue_id IN (\n SELECT dri.id\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n WHERE m.id = $1 AND dri.issue_type = '__dummy'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT m.id\n FROM mods m\n WHERE\n EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.project_id = m.id\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n AND NOT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json<FileIssue>\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ",
|
||||
"query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n )\n ) AS \"data!: sqlx::types::Json<FileIssue>\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3"
|
||||
"hash": "9d044826831bd15b1e0772e8a5db312189b48fcafaaf1b8c4c9f483c97b694ed"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tDELETE FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = $2\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9eea063cb153da407548f957cef7da1a33a059b0a1d00b51e27b4a5399492b13"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.key = ANY($1::text[])\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT m.id\n FROM mods m\n INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id\n WHERE\n EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n AND NOT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n AND didws.severity != 'hidden'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE delphi_rule_revisions SET revision = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT revision FROM delphi_rule_revisions LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "revision",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537"
|
||||
}
|
||||
Generated
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tINSERT INTO delphi_rules (\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t)\n\t\tVALUES (\n\t\t\t$1,\n\t\t\t$2,\n\t\t\t$3,\n\t\t\t(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1),\n\t\t\t$4,\n\t\t\t$4\n\t\t)\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!: DelphiRuleId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "rule",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "priority",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "revision",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b0004df8d3b1c5f9193a21f981452ce4ae56f608ec698f6f1ebb4c470bb2f0f5"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tdelete_on_next_revision = TRUE,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $2\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING id\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Generated
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "all_found!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "upserted_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73"
|
||||
}
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH random_id_point AS (\n SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point\n )\n SELECT id FROM mods\n WHERE status = ANY($1)\n AND EXISTS (\n SELECT 1 FROM versions v\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = lv.loader_id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.mod_id = mods.id AND pt.name = $3\n -- prevents decorrelation, so this stops at the first match instead\n -- of scanning all versions before the outer sort/limit applies\n OFFSET 0\n )\n ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)\n LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c97315540d36355668a1fdd33175b946cd026c718bf4c7d16b23953f6ec5b840"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE\n detail.project_id = ANY($1::bigint[])\n AND detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4"
|
||||
}
|
||||
Generated
-19
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator, lock_status)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tset_by_moderator = $5,\n\t\t\t\tlock_status = $6\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Int8",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d4b427672138b0c75f123a23ce104f69a6d12ff29e21a15f8fdd73386634a3ec"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH removed AS (\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = ANY($1::bigint[])\n RETURNING project_id\n )\n SELECT\n removed.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = removed.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM removed\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b"
|
||||
}
|
||||
Generated
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "all_found!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "upserted_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e"
|
||||
}
|
||||
Generated
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tname = $2,\n\t\t\trule = $3,\n\t\t\tpriority = $4,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $5\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!: DelphiRuleId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "rule",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "priority",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "revision",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d973a1edd200b4cd68ec30556a1e12240885f2372265597efa6e0361831d1318"
|
||||
}
|
||||
Generated
-58
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, set_by_moderator, lock_status\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "disclosure_type!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_by",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "set_by_moderator",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "lock_status",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc13b4d8b84681dc4f1ea5f4f735a5176583d2781dbb9aa428f8f920844f2a1e"
|
||||
}
|
||||
Generated
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n\t\tSELECT\n\t\t\teffect.detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n\t\t\tdetail.issue_id AS \"issue_id!: DelphiReportIssueId\",\n\t\t\tversion.mod_id AS \"project_id?: DBProjectId\",\n\t\t\tproject.name AS \"project_name?\",\n\t\t\tproject.icon_url AS \"project_icon_url?\",\n\t\t\tversion.id AS \"version_id?: DBVersionId\",\n\t\t\tversion.name AS \"version_name?\",\n\t\t\tversion.version_number AS \"version_number?\",\n\t\t\tissue.issue_type,\n\t\t\tdetail.key,\n\t\t\tdetail.jar,\n\t\t\tdetail.file_path,\n\t\t\tdetail.severity AS \"original_severity!: DelphiSeverity\",\n\t\t\teffect.severity AS \"effect_severity!: DelphiSeverity\"\n\t\tFROM delphi_rule_effects effect\n\t\tINNER JOIN delphi_rule_revisions published\n\t\t\tON published.revision = effect.revision\n\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\tON detail.id = effect.detail_id\n\t\tINNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n\t\tINNER JOIN delphi_reports report ON report.id = issue.report_id\n\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\tWHERE effect.rule_id = $1\n\t\tORDER BY effect.detail_id DESC\n\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "detail_id!: DelphiReportIssueDetailsId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "issue_id!: DelphiReportIssueId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "project_id?: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "project_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "project_icon_url?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "version_id?: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "version_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "version_number?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "jar",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "file_path",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "original_severity!: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "effect_severity!: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e07b7f513dd5d2a0e06cc1880746cd8d7d4c27896cbfcb3e26085f99e3552c98"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.id = ANY($1::bigint[])\n AND dri.issue_type != '__dummy'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b"
|
||||
}
|
||||
Generated
+126
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = ANY($1::bigint[])\n ORDER BY detail.id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "severity: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "jar",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "file_path",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "data: Json<HashMap<String, serde_json::Value>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "delphi_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "size?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "file_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "version_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "project_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "project_types!: Vec<String>",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "loaders!: Vec<String>",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "e8ea2268b7d3e4ec1741bfc0312306aad5e2d79f1bb4bf144d48933f7fa1ee83"
|
||||
}
|
||||
Generated
+3
-1
@@ -19,10 +19,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -41,5 +41,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41"
|
||||
"hash": "f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH dummy_issue AS (\n INSERT INTO delphi_report_issues (report_id, issue_type)\n VALUES ($1, $2)\n ON CONFLICT (report_id, issue_type)\n DO UPDATE SET issue_type = EXCLUDED.issue_type\n RETURNING id\n )\n INSERT INTO delphi_report_issue_details (\n issue_id,\n key,\n jar,\n file_path,\n decompiled_source,\n data,\n severity\n )\n SELECT\n id,\n '',\n NULL,\n '',\n NULL,\n '{}'::jsonb,\n 'low'::delphi_severity\n FROM dummy_issue\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dummy_issue.id\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -43,5 +43,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5"
|
||||
"hash": "ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d"
|
||||
}
|
||||
@@ -33,6 +33,7 @@ aws-sdk-s3 = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
cel = { workspace = true }
|
||||
censor = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
chumsky = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
-- Development fixtures for the Delphi trace-rule editor and scanner.
|
||||
-- This file is idempotent and only replaces rows using the fixture namespace.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DELETE FROM delphi_rule_effects
|
||||
WHERE rule_id IN (
|
||||
SELECT id
|
||||
FROM delphi_rules
|
||||
WHERE name LIKE '[DEV trace-rule fixture]%'
|
||||
);
|
||||
|
||||
DELETE FROM delphi_reports
|
||||
WHERE artifact_url = 'https://example.invalid/fixtures/delphi-trace-rules.json';
|
||||
|
||||
DELETE FROM delphi_rules
|
||||
WHERE name LIKE '[DEV trace-rule fixture]%';
|
||||
|
||||
WITH fixture_report AS (
|
||||
INSERT INTO delphi_reports (
|
||||
file_id,
|
||||
delphi_version,
|
||||
artifact_url,
|
||||
severity
|
||||
)
|
||||
VALUES (
|
||||
NULL,
|
||||
18,
|
||||
'https://example.invalid/fixtures/delphi-trace-rules.json',
|
||||
'malware'
|
||||
)
|
||||
RETURNING id
|
||||
), fixture_issues AS (
|
||||
INSERT INTO delphi_report_issues (report_id, issue_type)
|
||||
SELECT fixture_report.id, fixture_issue.issue_type
|
||||
FROM fixture_report
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('OBFUSCATED_NAMES'),
|
||||
('SUSPICIOUS_NETWORK_ACCESS'),
|
||||
('RUNTIME_EXEC_USAGE'),
|
||||
('BUNDLED_LIBRARY'),
|
||||
('HARDCODED_URL'),
|
||||
('CRYPTO_MINING_SIGNATURE'),
|
||||
('CLASSLOADER_USAGE'),
|
||||
('NATIVE_LIBRARY_LOAD')
|
||||
) AS fixture_issue(issue_type)
|
||||
RETURNING id, issue_type
|
||||
)
|
||||
INSERT INTO delphi_report_issue_details (
|
||||
issue_id,
|
||||
key,
|
||||
jar,
|
||||
file_path,
|
||||
decompiled_source,
|
||||
data,
|
||||
severity
|
||||
)
|
||||
SELECT
|
||||
fixture_issues.id,
|
||||
fixture_detail.key,
|
||||
fixture_detail.jar,
|
||||
fixture_detail.file_path,
|
||||
fixture_detail.decompiled_source,
|
||||
fixture_detail.data,
|
||||
fixture_detail.severity::delphi_severity
|
||||
FROM fixture_issues
|
||||
INNER JOIN (
|
||||
VALUES
|
||||
(
|
||||
'OBFUSCATED_NAMES',
|
||||
'dev-trace-rules/obfuscation/known-bootstrap',
|
||||
'META-INF/jars/bootstrap.jar',
|
||||
'com/example/bootstrap/Bootstrap.class',
|
||||
'class Bootstrap { /* generated fixture */ }',
|
||||
'{"confidence": 0.99, "symbol_count": 58}'::jsonb,
|
||||
'high'
|
||||
),
|
||||
(
|
||||
'OBFUSCATED_NAMES',
|
||||
'dev-trace-rules/obfuscation/unknown-library',
|
||||
NULL,
|
||||
'com/example/internal/A.class',
|
||||
'class A { /* generated fixture */ }',
|
||||
'{"confidence": 0.71, "symbol_count": 240}'::jsonb,
|
||||
'high'
|
||||
),
|
||||
(
|
||||
'SUSPICIOUS_NETWORK_ACCESS',
|
||||
'dev-trace-rules/network/malware-host',
|
||||
NULL,
|
||||
'com/example/network/Backdoor.class',
|
||||
'class Backdoor { /* generated fixture */ }',
|
||||
'{"host": "evil.example", "port": 4444, "protocol": "tcp"}'::jsonb,
|
||||
'medium'
|
||||
),
|
||||
(
|
||||
'SUSPICIOUS_NETWORK_ACCESS',
|
||||
'dev-trace-rules/network/telemetry-host',
|
||||
NULL,
|
||||
'com/example/telemetry/TelemetryClient.class',
|
||||
'class TelemetryClient { /* generated fixture */ }',
|
||||
'{"host": "telemetry.example.com", "port": 443, "protocol": "https"}'::jsonb,
|
||||
'medium'
|
||||
),
|
||||
(
|
||||
'SUSPICIOUS_NETWORK_ACCESS',
|
||||
'dev-trace-rules/network/unmatched-host',
|
||||
NULL,
|
||||
'com/example/update/UpdateChecker.class',
|
||||
'class UpdateChecker { /* generated fixture */ }',
|
||||
'{"host": "api.github.com", "port": 443, "protocol": "https"}'::jsonb,
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'RUNTIME_EXEC_USAGE',
|
||||
'dev-trace-rules/runtime/powershell',
|
||||
NULL,
|
||||
'com/example/platform/WindowsHelper.class',
|
||||
'class WindowsHelper { /* generated fixture */ }',
|
||||
'{"command": "powershell.exe", "arguments": ["-EncodedCommand", "fixture"]}'::jsonb,
|
||||
'high'
|
||||
),
|
||||
(
|
||||
'RUNTIME_EXEC_USAGE',
|
||||
'dev-trace-rules/runtime/desktop-opener',
|
||||
NULL,
|
||||
'com/example/platform/DesktopBrowser.class',
|
||||
'class DesktopBrowser { /* generated fixture */ }',
|
||||
'{"command": "xdg-open", "arguments": ["https://example.com/docs"]}'::jsonb,
|
||||
'medium'
|
||||
),
|
||||
(
|
||||
'RUNTIME_EXEC_USAGE',
|
||||
'dev-trace-rules/runtime/unmatched-java',
|
||||
NULL,
|
||||
'com/example/launcher/JavaLauncher.class',
|
||||
'class JavaLauncher { /* generated fixture */ }',
|
||||
'{"command": "java", "arguments": ["-version"]}'::jsonb,
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'BUNDLED_LIBRARY',
|
||||
'dev-trace-rules/library/example-helper',
|
||||
'META-INF/jars/example-helper.jar',
|
||||
'com/example/helper/Helper.class',
|
||||
NULL,
|
||||
'{"library": "example-helper", "version": "1.0.0"}'::jsonb,
|
||||
'high'
|
||||
),
|
||||
(
|
||||
'BUNDLED_LIBRARY',
|
||||
'dev-trace-rules/library/kotlin-runtime',
|
||||
'META-INF/jars/kotlin-stdlib.jar',
|
||||
'kotlin/collections/CollectionsKt.class',
|
||||
NULL,
|
||||
'{"library": "kotlin-stdlib", "version": "2.1.0"}'::jsonb,
|
||||
'medium'
|
||||
),
|
||||
(
|
||||
'HARDCODED_URL',
|
||||
'dev-trace-rules/url/known-documentation',
|
||||
NULL,
|
||||
'com/example/help/DocumentationLink.class',
|
||||
'class DocumentationLink { /* generated fixture */ }',
|
||||
'{"url": "https://docs.example.com/help"}'::jsonb,
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'HARDCODED_URL',
|
||||
'dev-trace-rules/url/unmatched-webhook',
|
||||
NULL,
|
||||
'com/example/integration/WebhookClient.class',
|
||||
'class WebhookClient { /* generated fixture */ }',
|
||||
'{"url": "https://hooks.example.net/incoming"}'::jsonb,
|
||||
'medium'
|
||||
),
|
||||
(
|
||||
'CRYPTO_MINING_SIGNATURE',
|
||||
'dev-trace-rules/crypto/randomx-miner',
|
||||
NULL,
|
||||
'com/example/worker/NativeWorker.class',
|
||||
'class NativeWorker { /* generated fixture */ }',
|
||||
'{"algorithm": "randomx", "pool": "pool.example"}'::jsonb,
|
||||
'severe'
|
||||
),
|
||||
(
|
||||
'CLASSLOADER_USAGE',
|
||||
'dev-trace-rules/classloader/plugin-loader',
|
||||
'META-INF/jars/plugin-api.jar',
|
||||
'com/example/plugin/PluginLoader.class',
|
||||
'class PluginLoader { /* generated fixture */ }',
|
||||
'{"purpose": "plugin-discovery", "parent_first": true}'::jsonb,
|
||||
'high'
|
||||
),
|
||||
(
|
||||
'NATIVE_LIBRARY_LOAD',
|
||||
'dev-trace-rules/native/unmatched-jna',
|
||||
'META-INF/jars/jna.jar',
|
||||
'com/sun/jna/Native.class',
|
||||
NULL,
|
||||
'{"library": "jnidispatch", "platform": "linux-x86_64"}'::jsonb,
|
||||
'high'
|
||||
)
|
||||
) AS fixture_detail(
|
||||
issue_type,
|
||||
key,
|
||||
jar,
|
||||
file_path,
|
||||
decompiled_source,
|
||||
data,
|
||||
severity
|
||||
) USING (issue_type);
|
||||
|
||||
INSERT INTO delphi_rules (name, rule, priority, revision)
|
||||
VALUES
|
||||
(
|
||||
'[DEV trace-rule fixture] Escalate known malware host',
|
||||
'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "evil.example" ? "malware" : null',
|
||||
1000,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Escalate crypto-mining signatures',
|
||||
'trace.issue_type == "CRYPTO_MINING_SIGNATURE" && trace.data.algorithm == "randomx" ? {"severity": "malware"} : null',
|
||||
900,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Escalate encoded PowerShell',
|
||||
'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "powershell.exe" ? {"severity": "severe"} : null',
|
||||
800,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Hide known-safe obfuscation',
|
||||
'trace.issue_type == "OBFUSCATED_NAMES" && trace.data.confidence >= 0.95 ? "hidden" : null',
|
||||
700,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Downgrade known telemetry',
|
||||
'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "telemetry.example.com" ? {"severity": "low"} : null',
|
||||
600,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Downgrade desktop URL openers',
|
||||
'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "xdg-open" ? {"severity": "low"} : null',
|
||||
500,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Downgrade bundled libraries',
|
||||
'trace.issue_type == "BUNDLED_LIBRARY" ? "low" : null',
|
||||
400,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Hide documentation URLs',
|
||||
'trace.issue_type == "HARDCODED_URL" && trace.data.url == "https://docs.example.com/help" ? {"severity": "hidden"} : null',
|
||||
300,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Downgrade plugin classloaders',
|
||||
'trace.issue_type == "CLASSLOADER_USAGE" && trace.data.purpose == "plugin-discovery" ? {"severity": "low"} : null',
|
||||
200,
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
|
||||
),
|
||||
(
|
||||
'[DEV trace-rule fixture] Pending native-library exception',
|
||||
'trace.issue_type == "NATIVE_LIBRARY_LOAD" && trace.data.library == "jnidispatch" ? {"severity": "low"} : null',
|
||||
100,
|
||||
(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1)
|
||||
);
|
||||
|
||||
INSERT INTO delphi_rule_effects (revision, detail_id, rule_id, severity)
|
||||
SELECT
|
||||
(SELECT revision FROM delphi_rule_revisions LIMIT 1),
|
||||
detail.id,
|
||||
rule.id,
|
||||
fixture_effect.severity::delphi_severity
|
||||
FROM (
|
||||
VALUES
|
||||
(
|
||||
'dev-trace-rules/network/malware-host',
|
||||
'[DEV trace-rule fixture] Escalate known malware host',
|
||||
'malware'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/crypto/randomx-miner',
|
||||
'[DEV trace-rule fixture] Escalate crypto-mining signatures',
|
||||
'malware'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/runtime/powershell',
|
||||
'[DEV trace-rule fixture] Escalate encoded PowerShell',
|
||||
'severe'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/obfuscation/known-bootstrap',
|
||||
'[DEV trace-rule fixture] Hide known-safe obfuscation',
|
||||
'hidden'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/network/telemetry-host',
|
||||
'[DEV trace-rule fixture] Downgrade known telemetry',
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/runtime/desktop-opener',
|
||||
'[DEV trace-rule fixture] Downgrade desktop URL openers',
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/library/example-helper',
|
||||
'[DEV trace-rule fixture] Downgrade bundled libraries',
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/library/kotlin-runtime',
|
||||
'[DEV trace-rule fixture] Downgrade bundled libraries',
|
||||
'low'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/url/known-documentation',
|
||||
'[DEV trace-rule fixture] Hide documentation URLs',
|
||||
'hidden'
|
||||
),
|
||||
(
|
||||
'dev-trace-rules/classloader/plugin-loader',
|
||||
'[DEV trace-rule fixture] Downgrade plugin classloaders',
|
||||
'low'
|
||||
)
|
||||
) AS fixture_effect(detail_key, rule_name, severity)
|
||||
INNER JOIN delphi_report_issue_details detail
|
||||
ON detail.key = fixture_effect.detail_key
|
||||
INNER JOIN delphi_rules rule
|
||||
ON rule.name = fixture_effect.rule_name;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,103 @@
|
||||
ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'hidden' BEFORE 'low';
|
||||
ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'malware' AFTER 'severe';
|
||||
|
||||
CREATE TABLE delphi_rules (
|
||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT REFERENCES users(id)
|
||||
ON DELETE SET NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by BIGINT REFERENCES users(id)
|
||||
ON DELETE SET NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
rule VARCHAR(65536) NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
revision BIGINT NOT NULL DEFAULT 0,
|
||||
delete_on_next_revision BOOL NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE delphi_rule_revisions (
|
||||
revision BIGINT PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO delphi_rule_revisions (revision)
|
||||
VALUES (1);
|
||||
|
||||
CREATE TABLE delphi_rule_effects (
|
||||
revision BIGINT NOT NULL,
|
||||
detail_id BIGINT NOT NULL REFERENCES delphi_report_issue_details(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
rule_id BIGINT NOT NULL REFERENCES delphi_rules(id)
|
||||
ON UPDATE CASCADE,
|
||||
severity delphi_severity NOT NULL,
|
||||
PRIMARY KEY (revision, detail_id)
|
||||
);
|
||||
|
||||
CREATE INDEX delphi_rule_effects_rule_id ON delphi_rule_effects(rule_id);
|
||||
|
||||
DROP VIEW delphi_issue_details_with_statuses;
|
||||
|
||||
CREATE VIEW delphi_issue_details_with_statuses AS
|
||||
SELECT
|
||||
drid.id,
|
||||
drid.issue_id,
|
||||
drid.key,
|
||||
drid.jar,
|
||||
drid.file_path,
|
||||
drid.decompiled_source,
|
||||
drid.data,
|
||||
drid.severity AS original_severity,
|
||||
COALESCE(dre.severity, drid.severity) AS severity,
|
||||
m.id AS project_id,
|
||||
didv.verdict AS local_status,
|
||||
dgdv.verdict AS global_status,
|
||||
COALESCE(dgdv.verdict, didv.verdict, 'pending') AS status
|
||||
FROM delphi_report_issue_details drid
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id
|
||||
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
|
||||
INNER JOIN files f ON f.id = dr.file_id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN mods m ON m.id = v.mod_id
|
||||
LEFT JOIN delphi_global_detail_verdicts dgdv
|
||||
ON drid.key = dgdv.detail_key
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON m.id = didv.project_id
|
||||
AND drid.key = didv.detail_key
|
||||
LEFT JOIN (
|
||||
SELECT revision
|
||||
FROM delphi_rule_revisions
|
||||
LIMIT 1
|
||||
) drr ON TRUE
|
||||
LEFT JOIN delphi_rule_effects dre
|
||||
ON dre.revision = drr.revision
|
||||
AND dre.detail_id = drid.id;
|
||||
|
||||
CREATE TABLE delphi_tech_review_queue (
|
||||
project_id BIGINT PRIMARY KEY REFERENCES mods(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO delphi_tech_review_queue (project_id)
|
||||
SELECT DISTINCT didws.project_id
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
(
|
||||
dri.issue_type = '__dummy'
|
||||
AND didws.status = 'pending'
|
||||
)
|
||||
OR (
|
||||
dri.issue_type != '__dummy'
|
||||
AND didws.status IN ('pending', 'unsafe')
|
||||
AND didws.severity::text != 'hidden'
|
||||
);
|
||||
|
||||
DELETE FROM delphi_report_issue_details detail
|
||||
USING delphi_report_issues issue
|
||||
WHERE
|
||||
detail.issue_id = issue.id
|
||||
AND issue.issue_type = '__dummy';
|
||||
|
||||
DELETE FROM delphi_report_issues
|
||||
WHERE issue_type = '__dummy';
|
||||
@@ -55,7 +55,7 @@ impl DBDelphiReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// A severity level reported by Delphi.
|
||||
/// A severity level for a Delphi report or transformed issue detail.
|
||||
#[derive(
|
||||
Deserialize,
|
||||
Serialize,
|
||||
@@ -76,6 +76,8 @@ impl DBDelphiReport {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "delphi_severity", rename_all = "snake_case")]
|
||||
pub enum DelphiSeverity {
|
||||
#[serde(alias = "HIDDEN")]
|
||||
Hidden,
|
||||
#[serde(alias = "LOW")]
|
||||
Low,
|
||||
#[serde(alias = "MEDIUM")]
|
||||
|
||||
@@ -292,4 +292,5 @@ id_type!(StatusId as i32);
|
||||
id_type!(DelphiReportId as i64);
|
||||
id_type!(DelphiReportIssueId as i64);
|
||||
id_type!(DelphiReportIssueDetailsId as i64);
|
||||
id_type!(DelphiRuleId as i64);
|
||||
id_type!(SharedInstanceId as i64);
|
||||
|
||||
@@ -111,8 +111,10 @@ impl DisclosureLockStatus {
|
||||
pub struct ProjectDisclosureData {
|
||||
#[serde(flatten)]
|
||||
pub disclosure: ProjectDisclosure,
|
||||
pub set_by_moderator: bool,
|
||||
pub lock_status: DisclosureLockStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub set_by_moderator: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lock_status: Option<DisclosureLockStatus>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<UserId>,
|
||||
@@ -124,16 +126,18 @@ impl ProjectDisclosureData {
|
||||
pub fn from_db(
|
||||
value: DBProjectDisclosure,
|
||||
viewer_is_moderator: bool,
|
||||
viewer_is_member: bool,
|
||||
) -> Self {
|
||||
let updated_by = (!value.set_by_moderator || viewer_is_moderator)
|
||||
.then_some(value.updated_by.into());
|
||||
|
||||
Self {
|
||||
disclosure: value.disclosure,
|
||||
set_by_moderator: value.set_by_moderator,
|
||||
lock_status: value.lock_status,
|
||||
set_by_moderator: (viewer_is_member || viewer_is_moderator)
|
||||
.then_some(value.set_by_moderator),
|
||||
lock_status: (viewer_is_member || viewer_is_moderator)
|
||||
.then_some(value.lock_status),
|
||||
updated_at: value.updated_at,
|
||||
updated_by,
|
||||
updated_by: ((!value.set_by_moderator && viewer_is_member)
|
||||
|| viewer_is_moderator)
|
||||
.then_some(value.updated_by.into()),
|
||||
deleted_at: value.deleted_at,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod rescan;
|
||||
pub mod tech_review_sync;
|
||||
pub mod tech_review_queue;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(
|
||||
@@ -209,6 +209,7 @@ async fn ingest_report_deserialized(
|
||||
"Delphi found issues in file",
|
||||
);
|
||||
|
||||
let mut inserted_detail_ids = Vec::new();
|
||||
for (issue_type, issue_details) in report.issues {
|
||||
let issue_id = DBDelphiReportIssue {
|
||||
id: DelphiReportIssueId(0), // This will be set by the database
|
||||
@@ -228,7 +229,7 @@ async fn ingest_report_deserialized(
|
||||
let decompiled_source =
|
||||
report.decompiled_sources.get(&issue_detail.file);
|
||||
|
||||
ReportIssueDetail {
|
||||
let detail_id = ReportIssueDetail {
|
||||
id: DelphiReportIssueDetailsId(0), // This will be set by the database
|
||||
issue_id,
|
||||
key: issue_detail.key.0,
|
||||
@@ -244,12 +245,19 @@ async fn ingest_report_deserialized(
|
||||
.insert(&mut transaction)
|
||||
.await
|
||||
.wrap_internal_err("failed to insert Delphi issue detail")?;
|
||||
inserted_detail_ids.push(detail_id);
|
||||
}
|
||||
}
|
||||
|
||||
tech_review_sync::sync_project_tech_review_state(
|
||||
crate::routes::internal::moderation::tech_review::rules_scan::materialize_current_rule_effects(
|
||||
&inserted_detail_ids,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("failed to apply delphi rules to new issue details")?;
|
||||
|
||||
tech_review_queue::add_projects_with_review_details(
|
||||
&[DBProjectId::from(report.project_id)],
|
||||
tech_review_sync::TechReviewExitReason::Resolved,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -97,26 +97,23 @@ async fn fetch_unreviewed_tech_review_project_ids(
|
||||
r#"
|
||||
SELECT DISTINCT m.id
|
||||
FROM mods m
|
||||
INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id
|
||||
WHERE
|
||||
EXISTS(
|
||||
SELECT 1
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
didws.project_id = m.id
|
||||
AND didws.status = 'pending'
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
AND NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
didws.project_id = m.id
|
||||
AND didws.status IN ('safe', 'unsafe')
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Maintains explicit project membership in the technical review queue.
|
||||
//!
|
||||
//! Queue membership is represented by a row in `delphi_tech_review_queue`.
|
||||
//! Enter and exit thread messages are emitted only when an insert or delete
|
||||
//! actually changes that membership, in the same transaction.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
PgTransaction,
|
||||
models::{DBProjectId, DBThreadId, thread_item::ThreadMessageBuilder},
|
||||
},
|
||||
models::threads::MessageBody,
|
||||
routes::ApiError,
|
||||
util::error::Context,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TechReviewRemovalReason {
|
||||
RulesChanged,
|
||||
FileDeleted,
|
||||
}
|
||||
|
||||
pub async fn add_projects(
|
||||
project_ids: &[DBProjectId],
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
|
||||
if project_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
WITH inserted AS (
|
||||
INSERT INTO delphi_tech_review_queue (project_id)
|
||||
SELECT unnest($1::bigint[])
|
||||
ON CONFLICT (project_id) DO NOTHING
|
||||
RETURNING project_id
|
||||
)
|
||||
SELECT
|
||||
inserted.project_id AS "project_id!: DBProjectId",
|
||||
(
|
||||
SELECT thread.id
|
||||
FROM threads thread
|
||||
WHERE thread.mod_id = inserted.project_id
|
||||
ORDER BY thread.id
|
||||
LIMIT 1
|
||||
) AS "thread_id!: DBThreadId"
|
||||
FROM inserted
|
||||
"#,
|
||||
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add projects to technical review queue")?;
|
||||
|
||||
for row in rows {
|
||||
ThreadMessageBuilder {
|
||||
author_id: None,
|
||||
body: MessageBody::TechReviewEntered,
|
||||
thread_id: row.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add entering technical review message")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_projects(
|
||||
project_ids: &[DBProjectId],
|
||||
reason: TechReviewRemovalReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
|
||||
if project_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
WITH removed AS (
|
||||
DELETE FROM delphi_tech_review_queue
|
||||
WHERE project_id = ANY($1::bigint[])
|
||||
RETURNING project_id
|
||||
)
|
||||
SELECT
|
||||
removed.project_id AS "project_id!: DBProjectId",
|
||||
(
|
||||
SELECT thread.id
|
||||
FROM threads thread
|
||||
WHERE thread.mod_id = removed.project_id
|
||||
ORDER BY thread.id
|
||||
LIMIT 1
|
||||
) AS "thread_id!: DBThreadId"
|
||||
FROM removed
|
||||
"#,
|
||||
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to remove projects from technical review queue",
|
||||
)?;
|
||||
|
||||
let body = match reason {
|
||||
TechReviewRemovalReason::RulesChanged => MessageBody::TechReviewExited,
|
||||
TechReviewRemovalReason::FileDeleted => {
|
||||
MessageBody::TechReviewExitFileDeleted
|
||||
}
|
||||
};
|
||||
|
||||
for row in rows {
|
||||
ThreadMessageBuilder {
|
||||
author_id: None,
|
||||
body: body.clone(),
|
||||
thread_id: row.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add exiting technical review message")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_projects_with_review_details(
|
||||
project_ids: &[DBProjectId],
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
|
||||
if project_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId"
|
||||
FROM delphi_issue_details_with_statuses detail
|
||||
WHERE
|
||||
detail.project_id = ANY($1::bigint[])
|
||||
AND detail.status IN ('pending', 'unsafe')
|
||||
AND detail.severity != 'hidden'
|
||||
"#,
|
||||
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to find projects requiring technical review")?;
|
||||
|
||||
add_projects(
|
||||
&rows
|
||||
.into_iter()
|
||||
.map(|row| row.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_projects_without_details(
|
||||
project_ids: &[DBProjectId],
|
||||
reason: TechReviewRemovalReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
|
||||
if project_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT requested.project_id AS "project_id!: DBProjectId"
|
||||
FROM unnest($1::bigint[]) AS requested(project_id)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM delphi_issue_details_with_statuses detail
|
||||
WHERE detail.project_id = requested.project_id
|
||||
)
|
||||
"#,
|
||||
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to find projects without technical review details",
|
||||
)?;
|
||||
|
||||
remove_projects(
|
||||
&rows
|
||||
.into_iter()
|
||||
.map(|row| row.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
reason,
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
//! Synchronizes moderation thread messages and dummy queue blockers with the
|
||||
//! current computed tech review state of affected projects.
|
||||
//!
|
||||
//! When a project has a Delphi report submitted for it, or when a moderator
|
||||
//! updates one of its issue details' rows (like flagging a detail as *globally*
|
||||
//! safe or unsafe), we will need to recheck if the project still belongs in the
|
||||
//! tech review queue or if it needs to exit now.
|
||||
//!
|
||||
//! Side-note: "entering the queue" or "exiting the queue" right now just means
|
||||
//! adding a new message to the project's moderation thread which indicates if
|
||||
//! it entered/exited. In the future this should be replaced with a more proper
|
||||
//! audit log table, or "is project currently in tech review" table.
|
||||
//!
|
||||
//! A project is considered to need tech review when it has at least one
|
||||
//! non-dummy issue detail whose effective status is pending or unsafe, or when
|
||||
//! it already has a dummy pending detail blocking the final review submission.
|
||||
//! Effective status is just the local detail's verdict (from
|
||||
//! `delphi_issue_detail_verdicts`), or if it's null then the global verdict for
|
||||
//! the same `drid.key` (from `delphi_global_detail_verdicts`).
|
||||
//!
|
||||
//! Some examples of how this behavior manifests: let's assume you have projects
|
||||
//! _A_ and _B_ currently in tech review. They each have one (unresolved) issue
|
||||
//! detail with key _K_.
|
||||
//! - If you mark _K_ on _A_ as locally safe/unsafe, then _A_ is fully resolved,
|
||||
//! but we still have the `__dummy` detail, which means it's still in the
|
||||
//! queue until the moderator submits the actual report. _B_ is entirely
|
||||
//! unaffected.
|
||||
//! - If you mark _K_ on _A_ as globally safe, then _A_ and _B_ both get fully
|
||||
//! resolved, but both still have the `__dummy` detail, so they also still
|
||||
//! need the final report to be submitted by the moderator.
|
||||
//!
|
||||
//! In practice, this means that some projects may have e.g. "100/100 traces
|
||||
//! are safe" reported, but they will just be waiting for final moderator
|
||||
//! approval.
|
||||
//!
|
||||
//! The logic for checking whether a project is now in tech review or not, and
|
||||
//! sending the appropriate message, is complex! That's why this module exists:
|
||||
//! to act as a single chokepoint which (correctly) syncs all the state, instead
|
||||
//! of having each mutation run its own ad-hoc update logic.
|
||||
|
||||
use crate::util::error::ApiContext as _;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
database::{
|
||||
PgTransaction,
|
||||
models::{
|
||||
DBProjectId, DBThreadId, DelphiReportId,
|
||||
delphi_report_item::DelphiVerdict,
|
||||
thread_item::ThreadMessageBuilder,
|
||||
},
|
||||
},
|
||||
models::threads::MessageBody,
|
||||
routes::ApiError,
|
||||
util::error::Context,
|
||||
};
|
||||
|
||||
const DUMMY_ISSUE_TYPE: &str = "__dummy";
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TechReviewExitReason {
|
||||
Resolved,
|
||||
FileDeleted,
|
||||
}
|
||||
|
||||
struct ProjectTechReviewState {
|
||||
has_pending_detail: bool,
|
||||
has_unsafe_detail: bool,
|
||||
has_dummy: bool,
|
||||
thread_id: Option<DBThreadId>,
|
||||
report_id: Option<DelphiReportId>,
|
||||
last_tech_review_message_type: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn sync_project_tech_review_state(
|
||||
project_ids: &[DBProjectId],
|
||||
exit_reason: TechReviewExitReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
|
||||
if project_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let project_ids_raw = project_ids.iter().map(|id| id.0).collect::<Vec<_>>();
|
||||
let tech_review_message_types = tech_review_message_types();
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
WITH project_ids AS (
|
||||
SELECT unnest($1::bigint[]) AS project_id
|
||||
),
|
||||
detail_states AS (
|
||||
SELECT
|
||||
p.project_id,
|
||||
COALESCE(
|
||||
BOOL_OR(
|
||||
didws.status = 'pending'
|
||||
AND dri.issue_type != $3
|
||||
),
|
||||
FALSE
|
||||
) AS has_pending_detail,
|
||||
COALESCE(
|
||||
BOOL_OR(
|
||||
didws.status = 'unsafe'
|
||||
AND dri.issue_type != $3
|
||||
),
|
||||
FALSE
|
||||
) AS has_unsafe_detail,
|
||||
COALESCE(
|
||||
BOOL_OR(
|
||||
didws.status = 'pending'
|
||||
AND dri.issue_type = $3
|
||||
),
|
||||
FALSE
|
||||
) AS has_dummy
|
||||
FROM project_ids p
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.project_id = p.project_id
|
||||
LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
GROUP BY p.project_id
|
||||
)
|
||||
SELECT
|
||||
p.project_id AS "project_id!: DBProjectId",
|
||||
p.has_pending_detail AS "has_pending_detail!",
|
||||
p.has_unsafe_detail AS "has_unsafe_detail!",
|
||||
p.has_dummy AS "has_dummy!",
|
||||
(
|
||||
SELECT t.id
|
||||
FROM threads t
|
||||
WHERE t.mod_id = p.project_id
|
||||
ORDER BY t.id
|
||||
LIMIT 1
|
||||
) AS "thread_id: DBThreadId",
|
||||
(
|
||||
SELECT tm.body->>'type'
|
||||
FROM threads t
|
||||
INNER JOIN threads_messages tm ON tm.thread_id = t.id
|
||||
WHERE
|
||||
t.mod_id = p.project_id
|
||||
AND tm.body->>'type' = ANY($2::text[])
|
||||
ORDER BY tm.created DESC, tm.id DESC
|
||||
LIMIT 1
|
||||
) AS "last_tech_review_message_type",
|
||||
(
|
||||
SELECT dr.id
|
||||
FROM versions v
|
||||
INNER JOIN files f ON f.version_id = v.id
|
||||
INNER JOIN delphi_reports dr ON dr.file_id = f.id
|
||||
WHERE v.mod_id = p.project_id
|
||||
ORDER BY dr.created DESC, dr.id DESC
|
||||
LIMIT 1
|
||||
) AS "report_id: DelphiReportId"
|
||||
FROM detail_states p
|
||||
"#,
|
||||
&project_ids_raw,
|
||||
&tech_review_message_types,
|
||||
DUMMY_ISSUE_TYPE,
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch project tech review state")?;
|
||||
|
||||
for row in rows {
|
||||
let state = ProjectTechReviewState {
|
||||
has_pending_detail: row.has_pending_detail,
|
||||
has_unsafe_detail: row.has_unsafe_detail,
|
||||
has_dummy: row.has_dummy,
|
||||
thread_id: row.thread_id,
|
||||
report_id: row.report_id,
|
||||
last_tech_review_message_type: row.last_tech_review_message_type,
|
||||
};
|
||||
|
||||
sync_one_project_tech_review_state(state, exit_reason, txn)
|
||||
.await
|
||||
.wrap_api_err("executing `sync_one_project_tech_review_state`")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_detail_key_tech_review_state(
|
||||
detail_keys: &[String],
|
||||
exit_reason: TechReviewExitReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let detail_keys = detail_keys.iter().cloned().unique().collect::<Vec<_>>();
|
||||
|
||||
if detail_keys.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId"
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.key = ANY($1::text[])
|
||||
"#,
|
||||
&detail_keys,
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch projects affected by detail keys")?;
|
||||
|
||||
let project_ids = rows
|
||||
.into_iter()
|
||||
.map(|row| row.project_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sync_project_tech_review_state(&project_ids, exit_reason, txn).await
|
||||
}
|
||||
|
||||
pub async fn sync_deleted_project_tech_review_exit(
|
||||
project_id: DBProjectId,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let tech_review_message_types = tech_review_message_types();
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
(
|
||||
SELECT t.id
|
||||
FROM threads t
|
||||
WHERE t.mod_id = $1
|
||||
ORDER BY t.id
|
||||
LIMIT 1
|
||||
) AS "thread_id: DBThreadId",
|
||||
(
|
||||
SELECT tm.body->>'type'
|
||||
FROM threads t
|
||||
INNER JOIN threads_messages tm ON tm.thread_id = t.id
|
||||
WHERE
|
||||
t.mod_id = $1
|
||||
AND tm.body->>'type' = ANY($2::text[])
|
||||
ORDER BY tm.created DESC, tm.id DESC
|
||||
LIMIT 1
|
||||
) AS "last_tech_review_message_type"
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
&tech_review_message_types,
|
||||
)
|
||||
.fetch_one(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch deleted project tech review state")?;
|
||||
|
||||
if let Some(thread_id) = row.thread_id
|
||||
&& should_send_exit(row.last_tech_review_message_type.as_deref())
|
||||
{
|
||||
insert_exit_message(thread_id, TechReviewExitReason::FileDeleted, txn)
|
||||
.await
|
||||
.wrap_api_err("executing `insert_exit_message`")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_one_project_tech_review_state(
|
||||
state: ProjectTechReviewState,
|
||||
exit_reason: TechReviewExitReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let needs_tech_review =
|
||||
state.has_pending_detail || state.has_unsafe_detail || state.has_dummy;
|
||||
|
||||
if needs_tech_review {
|
||||
if (state.has_pending_detail || state.has_unsafe_detail)
|
||||
&& !state.has_dummy
|
||||
&& let Some(report_id) = state.report_id
|
||||
{
|
||||
// TODO: Currently, the queue query determines whether a project is
|
||||
// in tech review by checking whether it has any pending issue
|
||||
// details. If all visible issue details are marked safe or unsafe
|
||||
// before the final report is submitted, the project would otherwise
|
||||
// leave the tech review queue without a final tech review verdict
|
||||
// message.
|
||||
//
|
||||
// This should be replaced with explicit tech review state, such as
|
||||
// an append-only project tech review event table where the latest
|
||||
// enter/exit event is the current state. Until then, this dummy
|
||||
// issue detail acts as the pending queue blocker.
|
||||
ensure_dummy_issue_detail(report_id, txn)
|
||||
.await
|
||||
.wrap_api_err("validating dummy issue detail")?;
|
||||
}
|
||||
|
||||
if let Some(thread_id) = state.thread_id
|
||||
&& state.last_tech_review_message_type.as_deref()
|
||||
!= Some(MessageBody::TechReviewEntered.as_ref())
|
||||
{
|
||||
ThreadMessageBuilder {
|
||||
author_id: None,
|
||||
body: MessageBody::TechReviewEntered,
|
||||
thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add entering tech review message")?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if matches!(exit_reason, TechReviewExitReason::Resolved)
|
||||
&& state.last_tech_review_message_type.as_deref()
|
||||
== Some(MessageBody::TechReviewEntered.as_ref())
|
||||
{
|
||||
if let Some(report_id) = state.report_id {
|
||||
ensure_dummy_issue_detail(report_id, txn)
|
||||
.await
|
||||
.wrap_api_err("validating dummy issue detail")?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(thread_id) = state.thread_id
|
||||
&& should_send_exit(state.last_tech_review_message_type.as_deref())
|
||||
{
|
||||
insert_exit_message(thread_id, exit_reason, txn)
|
||||
.await
|
||||
.wrap_api_err("executing `insert_exit_message`")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_send_exit(last_tech_review_message_type: Option<&str>) -> bool {
|
||||
matches!(last_tech_review_message_type, Some(message_type) if !matches!(
|
||||
message_type,
|
||||
message_type if message_type == MessageBody::TechReviewExited.as_ref()
|
||||
|| message_type == MessageBody::TechReviewExitFileDeleted.as_ref()
|
||||
|| message_type == tech_review_completed_message_type()
|
||||
))
|
||||
}
|
||||
|
||||
async fn insert_exit_message(
|
||||
thread_id: DBThreadId,
|
||||
exit_reason: TechReviewExitReason,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
let body = match exit_reason {
|
||||
TechReviewExitReason::Resolved => MessageBody::TechReviewExited,
|
||||
TechReviewExitReason::FileDeleted => {
|
||||
MessageBody::TechReviewExitFileDeleted
|
||||
}
|
||||
};
|
||||
|
||||
ThreadMessageBuilder {
|
||||
author_id: None,
|
||||
body,
|
||||
thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to add exiting tech review message")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_dummy_issue_detail(
|
||||
report_id: DelphiReportId,
|
||||
txn: &mut PgTransaction<'_>,
|
||||
) -> Result<(), ApiError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
WITH dummy_issue AS (
|
||||
INSERT INTO delphi_report_issues (report_id, issue_type)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (report_id, issue_type)
|
||||
DO UPDATE SET issue_type = EXCLUDED.issue_type
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO delphi_report_issue_details (
|
||||
issue_id,
|
||||
key,
|
||||
jar,
|
||||
file_path,
|
||||
decompiled_source,
|
||||
data,
|
||||
severity
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
'',
|
||||
NULL,
|
||||
'',
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
'low'::delphi_severity
|
||||
FROM dummy_issue
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM delphi_report_issue_details drid
|
||||
WHERE drid.issue_id = dummy_issue.id
|
||||
)
|
||||
"#,
|
||||
report_id as DelphiReportId,
|
||||
DUMMY_ISSUE_TYPE,
|
||||
)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to ensure dummy Delphi report issue detail")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tech_review_message_types() -> Vec<String> {
|
||||
[
|
||||
MessageBody::TechReviewEntered.as_ref(),
|
||||
MessageBody::TechReviewExited.as_ref(),
|
||||
MessageBody::TechReviewExitFileDeleted.as_ref(),
|
||||
tech_review_completed_message_type(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|message_type| message_type.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tech_review_completed_message_type() -> &'static str {
|
||||
MessageBody::TechReview {
|
||||
verdict: DelphiVerdict::Safe,
|
||||
}
|
||||
.as_ref()
|
||||
}
|
||||
@@ -121,6 +121,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
moderation::tech_review::search_projects,
|
||||
moderation::tech_review::global::search_global_issue_details,
|
||||
moderation::tech_review::global::get_global_issue_detail,
|
||||
moderation::tech_review::rules::get_rules,
|
||||
moderation::tech_review::rules::test_rule,
|
||||
moderation::tech_review::rules::get_rule_affected_details,
|
||||
moderation::tech_review::rules::create_rule,
|
||||
moderation::tech_review::rules::update_rule,
|
||||
moderation::tech_review::rules::delete_rule,
|
||||
moderation::tech_review::rules_scan::get_rule_schema,
|
||||
moderation::tech_review::rules_scan::get_detail_rule_input,
|
||||
moderation::tech_review::rules_scan::scan_rules,
|
||||
moderation::tech_review::get_project_report,
|
||||
moderation::tech_review::submit_report,
|
||||
moderation::tech_review::update_issue_details,
|
||||
|
||||
@@ -297,6 +297,7 @@ pub async fn get_projects_internal(
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.project_id = m.id
|
||||
AND didws.status = 'pending'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -546,6 +547,7 @@ pub async fn get_projects_internal(
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.project_id = m.id
|
||||
AND didws.status = 'pending'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -777,6 +779,7 @@ pub async fn get_project_ids(
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.project_id = m.id
|
||||
AND didws.status = 'pending'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -905,6 +908,7 @@ pub async fn get_project_ids(
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.project_id = mods.id
|
||||
AND didws.status = 'pending'
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
|
||||
@@ -34,10 +34,7 @@ use crate::{
|
||||
queue::session::AuthQueue,
|
||||
routes::{
|
||||
ApiError,
|
||||
internal::{
|
||||
delphi::tech_review_sync::{self, TechReviewExitReason},
|
||||
moderation::Ownership,
|
||||
},
|
||||
internal::{delphi::tech_review_queue, moderation::Ownership},
|
||||
},
|
||||
search::SearchState,
|
||||
util::error::Context,
|
||||
@@ -45,10 +42,14 @@ use crate::{
|
||||
use eyre::eyre;
|
||||
|
||||
pub mod global;
|
||||
pub mod rules;
|
||||
pub mod rules_scan;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(search_projects)
|
||||
.configure(global::config)
|
||||
.configure(rules::config)
|
||||
.configure(rules_scan::config)
|
||||
.service(get_project_report)
|
||||
.service(get_report)
|
||||
.service(get_issue)
|
||||
@@ -252,7 +253,9 @@ pub async fn get_issue(
|
||||
)
|
||||
), '[]'::jsonb)
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.issue_id = dri.id
|
||||
WHERE
|
||||
didws.issue_id = dri.id
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
) AS "data!: sqlx::types::Json<FileIssue>"
|
||||
FROM delphi_report_issues dri
|
||||
@@ -308,6 +311,15 @@ pub async fn get_report(
|
||||
'file_size', f.size,
|
||||
'flag_reason', 'delphi',
|
||||
'download_url', f.url,
|
||||
'severity', COALESCE((
|
||||
SELECT MAX(didws.severity)
|
||||
FROM delphi_report_issues severity_issue
|
||||
INNER JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.issue_id = severity_issue.id
|
||||
WHERE
|
||||
severity_issue.report_id = dr.id
|
||||
AND didws.severity != 'hidden'
|
||||
), 'low'::delphi_severity),
|
||||
-- TODO: replace with `json_array` in Postgres 16
|
||||
'issues', (
|
||||
SELECT coalesce(json_agg(
|
||||
@@ -330,15 +342,22 @@ pub async fn get_report(
|
||||
)
|
||||
), '[]'::jsonb)
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.issue_id = dri.id
|
||||
WHERE
|
||||
didws.issue_id = dri.id
|
||||
AND didws.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
), '[]'::json)
|
||||
FROM delphi_report_issues dri
|
||||
WHERE
|
||||
dri.report_id = dr.id
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
dri.report_id = dr.id
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM delphi_issue_details_with_statuses visible_detail
|
||||
WHERE
|
||||
visible_detail.issue_id = dri.id
|
||||
AND visible_detail.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
) AS "data!: sqlx::types::Json<FileReport>"
|
||||
FROM delphi_reports dr
|
||||
@@ -533,7 +552,9 @@ async fn fetch_project_reports(
|
||||
didws.global_status AS "global_status?: DelphiStatus",
|
||||
didws.status AS "status!: DelphiStatus"
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.issue_id = ANY($1::bigint[])
|
||||
WHERE
|
||||
didws.issue_id = ANY($1::bigint[])
|
||||
AND didws.severity != 'hidden'
|
||||
ORDER BY didws.issue_id, didws.id
|
||||
"#,
|
||||
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
|
||||
@@ -631,14 +652,14 @@ async fn fetch_project_reports(
|
||||
let mut file_issues = Vec::new();
|
||||
|
||||
for issue_row in report_issues {
|
||||
if issue_row.issue_type == "__dummy" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let issue_details = details_by_issue
|
||||
.get(&issue_row.id)
|
||||
.unwrap_or(&empty_details);
|
||||
|
||||
if issue_details.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
file_issues.push(FileIssue {
|
||||
id: issue_row.id,
|
||||
report_id: issue_row.report_id,
|
||||
@@ -647,12 +668,23 @@ async fn fetch_project_reports(
|
||||
});
|
||||
}
|
||||
|
||||
if file_issues.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let severity = file_issues
|
||||
.iter()
|
||||
.flat_map(|issue| issue.details.iter())
|
||||
.map(|detail| detail.severity)
|
||||
.max()
|
||||
.unwrap_or(report_row.severity);
|
||||
|
||||
file_reports.push(FileReport {
|
||||
report_id: report_row.report_id,
|
||||
file_id: FileId::from(file_row.file_id),
|
||||
created: report_row.created,
|
||||
flag_reason: FlagReason::Delphi,
|
||||
severity: report_row.severity,
|
||||
severity,
|
||||
file_name: file_row.filename.clone(),
|
||||
file_size: file_row.size,
|
||||
download_url: file_row.url.clone(),
|
||||
@@ -732,13 +764,15 @@ pub async fn search_projects(
|
||||
m.id AS "project_id: DBProjectId",
|
||||
MIN(t.id) AS "thread_id!: DBThreadId"
|
||||
FROM mods m
|
||||
INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id
|
||||
INNER JOIN threads t ON t.mod_id = m.id
|
||||
INNER JOIN versions v ON v.mod_id = m.id
|
||||
INNER JOIN files f ON f.version_id = v.id
|
||||
INNER JOIN delphi_reports dr ON dr.file_id = f.id
|
||||
INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id
|
||||
INNER JOIN delphi_issue_details_with_statuses didws
|
||||
LEFT JOIN versions v ON v.mod_id = m.id
|
||||
LEFT JOIN files f ON f.version_id = v.id
|
||||
LEFT JOIN delphi_reports dr ON dr.file_id = f.id
|
||||
LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.issue_id = dri.id
|
||||
AND didws.severity != 'hidden'
|
||||
LEFT JOIN threads_messages tm_last
|
||||
ON tm_last.thread_id = t.id
|
||||
AND tm_last.id = (
|
||||
@@ -782,8 +816,25 @@ pub async fn search_projects(
|
||||
)
|
||||
AND m.status NOT IN ('draft', 'rejected', 'withheld')
|
||||
AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))
|
||||
AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))
|
||||
AND didws.status = 'pending'
|
||||
AND (
|
||||
cardinality($7::text[]) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM versions issue_version
|
||||
INNER JOIN files issue_file
|
||||
ON issue_file.version_id = issue_version.id
|
||||
INNER JOIN delphi_reports issue_report
|
||||
ON issue_report.file_id = issue_file.id
|
||||
INNER JOIN delphi_report_issues issue
|
||||
ON issue.report_id = issue_report.id
|
||||
INNER JOIN delphi_issue_details_with_statuses detail
|
||||
ON detail.issue_id = issue.id
|
||||
WHERE
|
||||
issue_version.mod_id = m.id
|
||||
AND issue.issue_type = ANY($7::text[])
|
||||
AND detail.severity != 'hidden'
|
||||
)
|
||||
)
|
||||
AND (
|
||||
$5::text IS NULL
|
||||
OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))
|
||||
@@ -793,8 +844,8 @@ pub async fn search_projects(
|
||||
ORDER BY
|
||||
CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,
|
||||
CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,
|
||||
CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,
|
||||
CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,
|
||||
CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC,
|
||||
CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC,
|
||||
-- tie-breaker: oldest reports
|
||||
MIN(dr.created) ASC
|
||||
LIMIT $1 OFFSET $2
|
||||
@@ -1044,8 +1095,7 @@ pub async fn submit_report(
|
||||
WHERE
|
||||
m.id = $1
|
||||
AND didws.status = 'pending'
|
||||
-- see delphi.rs todo comment
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND didws.severity != 'hidden'
|
||||
"#,
|
||||
project_id as _,
|
||||
)
|
||||
@@ -1063,24 +1113,18 @@ pub async fn submit_report(
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM delphi_report_issue_details drid
|
||||
WHERE issue_id IN (
|
||||
SELECT dri.id
|
||||
FROM mods m
|
||||
INNER JOIN versions v ON v.mod_id = m.id
|
||||
INNER JOIN files f ON f.version_id = v.id
|
||||
INNER JOIN delphi_reports dr ON dr.file_id = f.id
|
||||
INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id
|
||||
WHERE m.id = $1 AND dri.issue_type = '__dummy'
|
||||
)
|
||||
",
|
||||
project_id as _,
|
||||
sqlx::query_scalar!(
|
||||
r#"
|
||||
DELETE FROM delphi_tech_review_queue
|
||||
WHERE project_id = $1
|
||||
RETURNING project_id AS "project_id: DBProjectId"
|
||||
"#,
|
||||
project_id as DBProjectId,
|
||||
)
|
||||
.execute(&mut txn)
|
||||
.fetch_optional(&mut txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to delete dummy issue")?;
|
||||
.wrap_internal_err("failed to remove project from technical review queue")?
|
||||
.wrap_not_found_err("project not found in technical review queue")?;
|
||||
|
||||
let record = sqlx::query!(
|
||||
r#"
|
||||
@@ -1269,10 +1313,6 @@ pub async fn update_issue_details(
|
||||
i.verdict
|
||||
FROM incoming i
|
||||
INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
-- see delphi.rs todo comment
|
||||
dri.issue_type != '__dummy'
|
||||
),
|
||||
validated AS (
|
||||
SELECT
|
||||
@@ -1336,10 +1376,7 @@ pub async fn update_issue_details(
|
||||
r#"
|
||||
SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId"
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
WHERE
|
||||
didws.id = ANY($1::bigint[])
|
||||
AND dri.issue_type != '__dummy'
|
||||
WHERE didws.id = ANY($1::bigint[])
|
||||
"#,
|
||||
&detail_ids,
|
||||
)
|
||||
@@ -1354,9 +1391,8 @@ pub async fn update_issue_details(
|
||||
.map(|row| row.project_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tech_review_sync::sync_project_tech_review_state(
|
||||
tech_review_queue::add_projects_with_review_details(
|
||||
&affected_project_ids,
|
||||
TechReviewExitReason::Resolved,
|
||||
&mut txn,
|
||||
)
|
||||
.await
|
||||
@@ -1381,7 +1417,7 @@ pub async fn update_issue_details(
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[post("/global-issue-detail")]
|
||||
#[post("/global-traces")]
|
||||
pub async fn update_global_issue_details(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
@@ -1474,9 +1510,25 @@ pub async fn update_global_issue_details(
|
||||
.await
|
||||
.wrap_internal_err("failed to update global issue details")?;
|
||||
|
||||
tech_review_sync::sync_detail_key_tech_review_state(
|
||||
let affected_projects = sqlx::query!(
|
||||
r#"
|
||||
SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId"
|
||||
FROM delphi_issue_details_with_statuses detail
|
||||
WHERE detail.key = ANY($1::text[])
|
||||
"#,
|
||||
&detail_keys,
|
||||
TechReviewExitReason::Resolved,
|
||||
)
|
||||
.fetch_all(&mut txn)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"failed to fetch projects affected by global detail updates",
|
||||
)?;
|
||||
|
||||
tech_review_queue::add_projects_with_review_details(
|
||||
&affected_projects
|
||||
.into_iter()
|
||||
.map(|row| row.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
&mut txn,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -133,7 +133,7 @@ pub struct GlobalIssueDetailTrace {
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = SearchGlobalIssueDetailsResponse))
|
||||
)]
|
||||
#[post("/global-issue-detail/search")]
|
||||
#[post("/global-traces/search")]
|
||||
pub async fn search_global_issue_details(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
@@ -199,9 +199,9 @@ pub async fn search_global_issue_details(
|
||||
FROM delphi_global_detail_verdicts dgdv
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.key = dgdv.detail_key
|
||||
AND didws.severity != 'hidden'
|
||||
LEFT JOIN delphi_report_issues dri
|
||||
ON dri.id = didws.issue_id
|
||||
AND dri.issue_type != '__dummy'
|
||||
WHERE (
|
||||
$1::text IS NULL
|
||||
OR dgdv.detail_key ILIKE '%' || $1 || '%'
|
||||
@@ -269,6 +269,7 @@ pub async fn search_global_issue_details(
|
||||
AND didv.detail_key = didws.key
|
||||
WHERE
|
||||
didws.key = ANY($1::text[])
|
||||
AND didws.severity != 'hidden'
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND (
|
||||
$3::text IS NULL
|
||||
@@ -356,7 +357,7 @@ pub async fn search_global_issue_details(
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = GetGlobalIssueDetailResponse))
|
||||
)]
|
||||
#[post("/global-issue-detail/local-traces")]
|
||||
#[post("/global-traces/local-traces")]
|
||||
pub async fn get_global_issue_detail(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
@@ -395,9 +396,9 @@ pub async fn get_global_issue_detail(
|
||||
FROM delphi_global_detail_verdicts dgdv
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.key = dgdv.detail_key
|
||||
AND didws.severity != 'hidden'
|
||||
LEFT JOIN delphi_report_issues dri
|
||||
ON dri.id = didws.issue_id
|
||||
AND dri.issue_type != '__dummy'
|
||||
WHERE dgdv.detail_key = $1
|
||||
GROUP BY dgdv.detail_key, dgdv.verdict
|
||||
"#,
|
||||
@@ -440,7 +441,7 @@ pub async fn get_global_issue_detail(
|
||||
WHERE
|
||||
didws.key = $1
|
||||
AND ($2::bigint IS NULL OR didws.id > $2)
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND didws.severity != 'hidden'
|
||||
ORDER BY didws.id
|
||||
LIMIT $3
|
||||
"#,
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
use actix_web::{HttpRequest, delete, get, post, put, web};
|
||||
use chrono::{DateTime, Utc};
|
||||
use eyre::eyre;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::rules_scan::RuleInput;
|
||||
use crate::{
|
||||
auth::check_is_moderator_from_headers,
|
||||
database::{
|
||||
PgPool, ReadOnlyPgPool,
|
||||
models::{
|
||||
DBProjectId, DBVersionId, DelphiReportIssueDetailsId,
|
||||
DelphiReportIssueId, DelphiRuleId,
|
||||
delphi_report_item::DelphiSeverity,
|
||||
},
|
||||
},
|
||||
models::{
|
||||
ids::{ProjectId, VersionId},
|
||||
pats::Scopes,
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
routes::ApiError,
|
||||
util::{cel, error::Context, validate::validation_errors_to_string},
|
||||
};
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(get_rules)
|
||||
.service(test_rule)
|
||||
.service(get_rule_affected_details)
|
||||
.service(create_rule)
|
||||
.service(update_rule)
|
||||
.service(delete_rule);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct DelphiRule {
|
||||
pub id: DelphiRuleId,
|
||||
pub name: String,
|
||||
pub rule: String,
|
||||
pub priority: i32,
|
||||
pub revision: i64,
|
||||
pub current_revision: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<i64>,
|
||||
pub updated_by: Option<i64>,
|
||||
pub affected_details_count: i64,
|
||||
pub affected_details: Vec<DelphiRuleAffectedDetail>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct DelphiRuleAffectedDetail {
|
||||
pub detail_id: DelphiReportIssueDetailsId,
|
||||
pub issue_id: DelphiReportIssueId,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub project_name: Option<String>,
|
||||
pub project_icon_url: Option<String>,
|
||||
pub version_id: Option<VersionId>,
|
||||
pub version_name: Option<String>,
|
||||
pub version_number: Option<String>,
|
||||
pub issue_type: String,
|
||||
pub key: String,
|
||||
pub jar: Option<String>,
|
||||
pub file_path: String,
|
||||
pub original_severity: DelphiSeverity,
|
||||
pub severity: DelphiSeverity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, utoipa::ToSchema)]
|
||||
pub struct WriteDelphiRule {
|
||||
#[validate(length(min = 1, max = 256))]
|
||||
pub name: String,
|
||||
#[validate(length(min = 1, max = 65536))]
|
||||
pub rule: String,
|
||||
#[serde(default)]
|
||||
pub priority: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate, utoipa::ToSchema)]
|
||||
pub struct TestDelphiRule {
|
||||
#[validate(length(min = 1, max = 65536))]
|
||||
pub rule: String,
|
||||
#[validate(length(max = 10))]
|
||||
pub inputs: Vec<RuleInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct TestDelphiRuleResponse {
|
||||
pub effects: Vec<Option<DelphiRuleEffect>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DelphiRuleEffect {
|
||||
pub severity: DelphiSeverity,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum DelphiRuleOutput {
|
||||
Severity(DelphiSeverity),
|
||||
Effect(DelphiRuleEffect),
|
||||
}
|
||||
|
||||
struct ValidatedRule {
|
||||
name: String,
|
||||
rule: String,
|
||||
priority: i32,
|
||||
}
|
||||
|
||||
impl WriteDelphiRule {
|
||||
async fn validate(mut self) -> Result<ValidatedRule, ApiError> {
|
||||
self.name = self.name.trim().to_string();
|
||||
self.rule = self.rule.trim().to_string();
|
||||
Validate::validate(&self).map_err(|error| {
|
||||
ApiError::Request(eyre!(validation_errors_to_string(error, None)))
|
||||
})?;
|
||||
|
||||
let expression = self.rule.clone();
|
||||
tokio::task::spawn_blocking(move || cel::Program::compile(&expression))
|
||||
.await
|
||||
.wrap_internal_err("failed to join cel compilation task")?
|
||||
.map_err(|error| {
|
||||
ApiError::Request(
|
||||
eyre!(error).wrap_err("invalid cel expression"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(ValidatedRule {
|
||||
name: self.name,
|
||||
rule: self.rule,
|
||||
priority: self.priority,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a CEL rule against caller-provided issue traces without saving it.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
request_body = TestDelphiRule,
|
||||
responses((status = OK, body = TestDelphiRuleResponse))
|
||||
)]
|
||||
#[post("/rules/test")]
|
||||
pub async fn test_rule(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<TestDelphiRule>,
|
||||
) -> Result<web::Json<TestDelphiRuleResponse>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let mut request = body.into_inner();
|
||||
request.rule = request.rule.trim().to_string();
|
||||
request.validate().map_err(|error| {
|
||||
ApiError::Request(eyre!(validation_errors_to_string(error, None)))
|
||||
})?;
|
||||
|
||||
let rule = request.rule;
|
||||
let expression = rule.clone();
|
||||
let program =
|
||||
tokio::task::spawn_blocking(move || cel::Program::compile(&expression))
|
||||
.await
|
||||
.wrap_internal_err("failed to join cel compilation task")?
|
||||
.map_err(|error| {
|
||||
ApiError::Request(
|
||||
eyre!(error).wrap_err("invalid cel expression"),
|
||||
)
|
||||
})?;
|
||||
let mut effects = Vec::with_capacity(request.inputs.len());
|
||||
|
||||
for (index, input) in request.inputs.iter().enumerate() {
|
||||
let evaluation =
|
||||
super::rules_scan::evaluate_rule(&program, &rule, input);
|
||||
let effect = evaluation.wrap_request_err(format!(
|
||||
"failed to evaluate test input {index}"
|
||||
))?;
|
||||
effects.push(effect);
|
||||
}
|
||||
|
||||
Ok(web::Json(TestDelphiRuleResponse { effects }))
|
||||
}
|
||||
|
||||
/// List all Delphi rules that are not pending deletion.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = Vec<DelphiRule>))
|
||||
)]
|
||||
#[get("/rules")]
|
||||
pub async fn get_rules(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<Vec<DelphiRule>>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let rules = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
delphi_rule.id AS "id!: DelphiRuleId",
|
||||
delphi_rule.name,
|
||||
delphi_rule.rule,
|
||||
delphi_rule.priority,
|
||||
delphi_rule.revision,
|
||||
(
|
||||
SELECT revision FROM delphi_rule_revisions LIMIT 1
|
||||
) AS "current_revision!",
|
||||
delphi_rule.created_at,
|
||||
delphi_rule.updated_at,
|
||||
delphi_rule.created_by,
|
||||
delphi_rule.updated_by,
|
||||
COALESCE(preview.affected_details_count, 0)
|
||||
AS "affected_details_count!",
|
||||
preview.detail_id AS "detail_id?: DelphiReportIssueDetailsId",
|
||||
preview.issue_id AS "issue_id?: DelphiReportIssueId",
|
||||
preview.project_id AS "project_id?: DBProjectId",
|
||||
preview.project_name AS "project_name?",
|
||||
preview.project_icon_url AS "project_icon_url?",
|
||||
preview.version_id AS "version_id?: DBVersionId",
|
||||
preview.version_name AS "version_name?",
|
||||
preview.version_number AS "version_number?",
|
||||
preview.issue_type AS "issue_type?",
|
||||
preview.key AS "key?",
|
||||
preview.jar AS "jar?",
|
||||
preview.file_path AS "file_path?",
|
||||
preview.original_severity AS "original_severity?: DelphiSeverity",
|
||||
preview.severity AS "effect_severity?: DelphiSeverity"
|
||||
FROM delphi_rules delphi_rule
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
effect.detail_id,
|
||||
detail.issue_id,
|
||||
version.mod_id AS project_id,
|
||||
project.name AS project_name,
|
||||
project.icon_url AS project_icon_url,
|
||||
version.id AS version_id,
|
||||
version.name AS version_name,
|
||||
version.version_number,
|
||||
issue.issue_type,
|
||||
detail.key,
|
||||
detail.jar,
|
||||
detail.file_path,
|
||||
detail.severity AS original_severity,
|
||||
effect.severity,
|
||||
COUNT(*) OVER () AS affected_details_count
|
||||
FROM delphi_rule_effects effect
|
||||
INNER JOIN delphi_rule_revisions published
|
||||
ON published.revision = effect.revision
|
||||
INNER JOIN delphi_report_issue_details detail
|
||||
ON detail.id = effect.detail_id
|
||||
INNER JOIN delphi_report_issues issue
|
||||
ON issue.id = detail.issue_id
|
||||
INNER JOIN delphi_reports report
|
||||
ON report.id = issue.report_id
|
||||
LEFT JOIN files file ON file.id = report.file_id
|
||||
LEFT JOIN versions version ON version.id = file.version_id
|
||||
LEFT JOIN mods project ON project.id = version.mod_id
|
||||
WHERE effect.rule_id = delphi_rule.id
|
||||
ORDER BY effect.detail_id DESC
|
||||
LIMIT 3
|
||||
) preview ON TRUE
|
||||
WHERE NOT delphi_rule.delete_on_next_revision
|
||||
ORDER BY
|
||||
delphi_rule.priority DESC,
|
||||
delphi_rule.id,
|
||||
preview.detail_id DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&***ro_pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch delphi rules")?;
|
||||
|
||||
let mut response = Vec::<DelphiRule>::new();
|
||||
for rule in rules {
|
||||
let affected_detail = if let (
|
||||
Some(detail_id),
|
||||
Some(issue_id),
|
||||
Some(issue_type),
|
||||
Some(key),
|
||||
Some(file_path),
|
||||
Some(original_severity),
|
||||
Some(severity),
|
||||
) = (
|
||||
rule.detail_id,
|
||||
rule.issue_id,
|
||||
rule.issue_type,
|
||||
rule.key,
|
||||
rule.file_path,
|
||||
rule.original_severity,
|
||||
rule.effect_severity,
|
||||
) {
|
||||
Some(DelphiRuleAffectedDetail {
|
||||
detail_id,
|
||||
issue_id,
|
||||
project_id: rule.project_id.map(ProjectId::from),
|
||||
project_name: rule.project_name,
|
||||
project_icon_url: rule.project_icon_url,
|
||||
version_id: rule.version_id.map(VersionId::from),
|
||||
version_name: rule.version_name,
|
||||
version_number: rule.version_number,
|
||||
issue_type,
|
||||
key,
|
||||
jar: rule.jar,
|
||||
file_path,
|
||||
original_severity,
|
||||
severity,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(existing) = response.last_mut()
|
||||
&& existing.id == rule.id
|
||||
{
|
||||
if let Some(affected_detail) = affected_detail {
|
||||
existing.affected_details.push(affected_detail);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
response.push(DelphiRule {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
rule: rule.rule,
|
||||
priority: rule.priority,
|
||||
revision: rule.revision,
|
||||
current_revision: rule.current_revision,
|
||||
created_at: rule.created_at,
|
||||
updated_at: rule.updated_at,
|
||||
created_by: rule.created_by,
|
||||
updated_by: rule.updated_by,
|
||||
affected_details_count: rule.affected_details_count,
|
||||
affected_details: affected_detail.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(web::Json(response))
|
||||
}
|
||||
|
||||
/// List all details affected by a Delphi rule in the published revision.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = Vec<DelphiRuleAffectedDetail>))
|
||||
)]
|
||||
#[get("/rules/{id}/effects")]
|
||||
pub async fn get_rule_affected_details(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
path: web::Path<(DelphiRuleId,)>,
|
||||
) -> Result<web::Json<Vec<DelphiRuleAffectedDetail>>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
let (rule_id,) = path.into_inner();
|
||||
|
||||
let details = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
effect.detail_id AS "detail_id!: DelphiReportIssueDetailsId",
|
||||
detail.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
version.mod_id AS "project_id?: DBProjectId",
|
||||
project.name AS "project_name?",
|
||||
project.icon_url AS "project_icon_url?",
|
||||
version.id AS "version_id?: DBVersionId",
|
||||
version.name AS "version_name?",
|
||||
version.version_number AS "version_number?",
|
||||
issue.issue_type,
|
||||
detail.key,
|
||||
detail.jar,
|
||||
detail.file_path,
|
||||
detail.severity AS "original_severity!: DelphiSeverity",
|
||||
effect.severity AS "effect_severity!: DelphiSeverity"
|
||||
FROM delphi_rule_effects effect
|
||||
INNER JOIN delphi_rule_revisions published
|
||||
ON published.revision = effect.revision
|
||||
INNER JOIN delphi_report_issue_details detail
|
||||
ON detail.id = effect.detail_id
|
||||
INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id
|
||||
INNER JOIN delphi_reports report ON report.id = issue.report_id
|
||||
LEFT JOIN files file ON file.id = report.file_id
|
||||
LEFT JOIN versions version ON version.id = file.version_id
|
||||
LEFT JOIN mods project ON project.id = version.mod_id
|
||||
WHERE effect.rule_id = $1
|
||||
ORDER BY effect.detail_id DESC
|
||||
"#,
|
||||
rule_id as DelphiRuleId,
|
||||
)
|
||||
.fetch_all(&***ro_pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch details affected by delphi rule")?;
|
||||
|
||||
Ok(web::Json(
|
||||
details
|
||||
.into_iter()
|
||||
.map(|detail| DelphiRuleAffectedDetail {
|
||||
detail_id: detail.detail_id,
|
||||
issue_id: detail.issue_id,
|
||||
project_id: detail.project_id.map(ProjectId::from),
|
||||
project_name: detail.project_name,
|
||||
project_icon_url: detail.project_icon_url,
|
||||
version_id: detail.version_id.map(VersionId::from),
|
||||
version_name: detail.version_name,
|
||||
version_number: detail.version_number,
|
||||
issue_type: detail.issue_type,
|
||||
key: detail.key,
|
||||
jar: detail.jar,
|
||||
file_path: detail.file_path,
|
||||
original_severity: detail.original_severity,
|
||||
severity: detail.effect_severity,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a Delphi rule. It will be applied by the next manual rule scan.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
request_body = WriteDelphiRule,
|
||||
responses((status = OK, body = DelphiRule))
|
||||
)]
|
||||
#[post("/rules")]
|
||||
pub async fn create_rule(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<WriteDelphiRule>,
|
||||
) -> Result<web::Json<DelphiRule>, ApiError> {
|
||||
let user = check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
let rule = body.into_inner().validate().await?;
|
||||
let user_id = user.id.0 as i64;
|
||||
|
||||
let rule = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO delphi_rules (
|
||||
name,
|
||||
rule,
|
||||
priority,
|
||||
revision,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1),
|
||||
$4,
|
||||
$4
|
||||
)
|
||||
RETURNING
|
||||
id AS "id!: DelphiRuleId",
|
||||
name,
|
||||
rule,
|
||||
priority,
|
||||
revision,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
"#,
|
||||
rule.name,
|
||||
rule.rule,
|
||||
rule.priority,
|
||||
user_id,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to create delphi rule")?;
|
||||
|
||||
Ok(web::Json(DelphiRule {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
rule: rule.rule,
|
||||
priority: rule.priority,
|
||||
revision: rule.revision,
|
||||
current_revision: rule.revision - 1,
|
||||
created_at: rule.created_at,
|
||||
updated_at: rule.updated_at,
|
||||
created_by: rule.created_by,
|
||||
updated_by: rule.updated_by,
|
||||
affected_details_count: 0,
|
||||
affected_details: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Update a Delphi rule. Its materialized effects remain unchanged until the next scan.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
request_body = WriteDelphiRule,
|
||||
responses((status = OK, body = DelphiRule), (status = NOT_FOUND))
|
||||
)]
|
||||
#[put("/rules/{id}")]
|
||||
pub async fn update_rule(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
path: web::Path<(DelphiRuleId,)>,
|
||||
body: web::Json<WriteDelphiRule>,
|
||||
) -> Result<web::Json<DelphiRule>, ApiError> {
|
||||
let user = check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
let (id,) = path.into_inner();
|
||||
let rule = body.into_inner().validate().await?;
|
||||
let user_id = user.id.0 as i64;
|
||||
|
||||
let rule = sqlx::query!(
|
||||
r#"
|
||||
UPDATE delphi_rules
|
||||
SET
|
||||
name = $2,
|
||||
rule = $3,
|
||||
priority = $4,
|
||||
revision = (
|
||||
SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1
|
||||
),
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
updated_by = $5
|
||||
WHERE id = $1 AND NOT delete_on_next_revision
|
||||
RETURNING
|
||||
id AS "id!: DelphiRuleId",
|
||||
name,
|
||||
rule,
|
||||
priority,
|
||||
revision,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
"#,
|
||||
id as DelphiRuleId,
|
||||
rule.name,
|
||||
rule.rule,
|
||||
rule.priority,
|
||||
user_id,
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to update delphi rule")?
|
||||
.wrap_not_found_err("delphi rule not found")?;
|
||||
|
||||
Ok(web::Json(DelphiRule {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
rule: rule.rule,
|
||||
priority: rule.priority,
|
||||
revision: rule.revision,
|
||||
current_revision: rule.revision - 1,
|
||||
created_at: rule.created_at,
|
||||
updated_at: rule.updated_at,
|
||||
created_by: rule.created_by,
|
||||
updated_by: rule.updated_by,
|
||||
affected_details_count: 0,
|
||||
affected_details: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Mark a Delphi rule for deletion when the next rule scan is published.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK), (status = NOT_FOUND))
|
||||
)]
|
||||
#[delete("/rules/{id}")]
|
||||
pub async fn delete_rule(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
path: web::Path<(DelphiRuleId,)>,
|
||||
) -> Result<(), ApiError> {
|
||||
let user = check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
let (id,) = path.into_inner();
|
||||
|
||||
let deleted = sqlx::query!(
|
||||
r#"
|
||||
UPDATE delphi_rules
|
||||
SET
|
||||
delete_on_next_revision = TRUE,
|
||||
revision = (
|
||||
SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1
|
||||
),
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
updated_by = $2
|
||||
WHERE id = $1 AND NOT delete_on_next_revision
|
||||
RETURNING id
|
||||
"#,
|
||||
id as DelphiRuleId,
|
||||
user.id.0 as i64,
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to mark delphi rule for deletion")?;
|
||||
|
||||
if deleted.is_none() {
|
||||
return Err(ApiError::NotFound(eyre!("delphi rule not found")));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -235,6 +235,23 @@ impl ApiError {
|
||||
}
|
||||
|
||||
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
|
||||
let report = match self {
|
||||
Self::Internal(report)
|
||||
| Self::Request(report)
|
||||
| Self::Auth(report)
|
||||
| Self::NotFound(report)
|
||||
| Self::Conflict(report)
|
||||
| Self::FailedDependency(report)
|
||||
| Self::PreconditionRequired(report)
|
||||
| Self::PreconditionFailed(report)
|
||||
| Self::RateLimit(report) => report,
|
||||
};
|
||||
let details = report
|
||||
.chain()
|
||||
.skip(1)
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
Self::Internal(..) => "internal_error",
|
||||
@@ -247,8 +264,8 @@ impl ApiError {
|
||||
Self::PreconditionFailed(..) => "precondition_failed",
|
||||
Self::RateLimit(..) => "ratelimit_error",
|
||||
},
|
||||
description: format!("{self:#}"),
|
||||
details: None,
|
||||
description: report.to_string(),
|
||||
details: (!details.is_empty()).then(|| serde_json::json!(details)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,3 +289,25 @@ impl actix_web::ResponseError for ApiError {
|
||||
HttpResponse::build(self.status_code()).json(self.as_api_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ApiError;
|
||||
|
||||
#[test]
|
||||
fn api_error_serializes_source_chain_as_details() {
|
||||
let error = ApiError::Request(
|
||||
eyre::eyre!("root cause")
|
||||
.wrap_err("intermediate context")
|
||||
.wrap_err("request failed"),
|
||||
);
|
||||
|
||||
let response = error.as_api_error();
|
||||
|
||||
assert_eq!(response.description, "request failed");
|
||||
assert_eq!(
|
||||
response.details,
|
||||
Some(serde_json::json!(["intermediate context", "root cause"])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,10 @@ pub async fn random_projects_get(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let count = v3::projects::RandomProjects { count: count.count };
|
||||
let count = v3::projects::RandomProjects {
|
||||
count: count.count,
|
||||
project_type: None,
|
||||
};
|
||||
|
||||
let response = v3::projects::random_projects_get(
|
||||
web::Query(count),
|
||||
|
||||
@@ -73,14 +73,15 @@ pub async fn get_project_disclosures(
|
||||
let viewer_is_moderator =
|
||||
user_option.as_ref().is_some_and(|user| user.role.is_mod());
|
||||
|
||||
let include_deleted = viewer_is_moderator
|
||||
// Moderators can see regardless of membership, short circuit to avoid extra db call
|
||||
let viewer_is_member = viewer_is_moderator
|
||||
|| is_team_member_project(&project.inner, &user_option, &pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to check project team membership")?;
|
||||
|
||||
let disclosures = db_models::DBProjectDisclosure::get_many_for_project(
|
||||
project.inner.id,
|
||||
include_deleted,
|
||||
viewer_is_moderator || viewer_is_member,
|
||||
&***ro_pool,
|
||||
)
|
||||
.await
|
||||
@@ -90,7 +91,11 @@ pub async fn get_project_disclosures(
|
||||
disclosures: disclosures
|
||||
.into_iter()
|
||||
.map(|disclosure| {
|
||||
ProjectDisclosureData::from_db(disclosure, viewer_is_moderator)
|
||||
ProjectDisclosureData::from_db(
|
||||
disclosure,
|
||||
viewer_is_moderator,
|
||||
viewer_is_member,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
|
||||
@@ -44,6 +44,7 @@ use chrono::Utc;
|
||||
use eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use rand::seq::SliceRandom;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
@@ -104,11 +105,15 @@ pub async fn clear_project_cache_and_queue_search(
|
||||
pub struct RandomProjects {
|
||||
#[validate(range(min = 1, max = 100))]
|
||||
pub count: u32,
|
||||
pub project_type: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "projects",
|
||||
params(("count" = u32, Query)),
|
||||
params(
|
||||
("count" = u32, Query),
|
||||
("project_type" = Option<String>, Query),
|
||||
),
|
||||
responses((status = OK))
|
||||
)]
|
||||
#[get("/projects_random")]
|
||||
@@ -120,37 +125,84 @@ pub async fn random_projects_get_route(
|
||||
random_projects_get(count, pool, redis).await
|
||||
}
|
||||
|
||||
// Filtered candidates are sparser and unevenly spaced, so the nearest-point pick
|
||||
// tends to repeat; oversample a neighborhood and shuffle it down to counter that.
|
||||
const RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR: u32 = 20;
|
||||
|
||||
pub async fn random_projects_get(
|
||||
web::Query(count): web::Query<RandomProjects>,
|
||||
web::Query(params): web::Query<RandomProjects>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
count
|
||||
params
|
||||
.validate()
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_request_err("validating request")?;
|
||||
|
||||
let project_ids = sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
let statuses = crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut project_ids = if let Some(project_type) = ¶ms.project_type {
|
||||
let fetch_limit = params.count * RANDOM_PROJECT_TYPE_OVERSAMPLE_FACTOR;
|
||||
|
||||
sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM versions v
|
||||
INNER JOIN loaders_versions lv ON v.id = lv.version_id
|
||||
INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = lv.loader_id
|
||||
INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
|
||||
WHERE v.mod_id = mods.id AND pt.name = $3
|
||||
-- prevents decorrelation, so this stops at the first match instead
|
||||
-- of scanning all versions before the outer sort/limit applies
|
||||
OFFSET 0
|
||||
)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&statuses,
|
||||
fetch_limit as i32,
|
||||
project_type,
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&*crate::models::projects::ProjectStatus::iterator()
|
||||
.filter(|x| x.is_searchable())
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
count.count as i32,
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await.wrap_internal_err("querying random project IDs")?;
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("querying random project IDs")?
|
||||
} else {
|
||||
sqlx::query!(
|
||||
// IDs are randomly generated (see the `generate_ids` macro), so fetching a
|
||||
// number of mods nearest to a random point in the ID space is equivalent to
|
||||
// random sampling
|
||||
"WITH random_id_point AS (
|
||||
SELECT POINT(RANDOM() * ((SELECT MAX(id) FROM mods) - (SELECT MIN(id) FROM mods) + 1) + (SELECT MIN(id) FROM mods), 0) AS point
|
||||
)
|
||||
SELECT id FROM mods
|
||||
WHERE status = ANY($1)
|
||||
ORDER BY POINT(id, 0) <-> (SELECT point FROM random_id_point)
|
||||
LIMIT $2",
|
||||
&statuses,
|
||||
params.count as i32,
|
||||
)
|
||||
.fetch(&**pool)
|
||||
.map_ok(|m| db_ids::DBProjectId(m.id))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.wrap_internal_err("querying random project IDs")?
|
||||
};
|
||||
|
||||
if params.project_type.is_some() {
|
||||
project_ids.shuffle(&mut rand::thread_rng());
|
||||
project_ids.truncate(params.count as usize);
|
||||
}
|
||||
|
||||
let projects_data =
|
||||
db_models::DBProject::get_many_ids(&project_ids, &**pool, &redis)
|
||||
@@ -3001,6 +3053,13 @@ pub async fn project_delete_internal(
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
delphi::tech_review_queue::remove_projects(
|
||||
&[project.inner.id],
|
||||
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("removing project from technical review queue")?;
|
||||
|
||||
// rejected & withheld projects are transferred to ghost so moderation data is preserved
|
||||
if matches!(
|
||||
@@ -3164,15 +3223,6 @@ pub async fn project_delete_internal(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
delphi::tech_review_sync::sync_deleted_project_tech_review_exit(
|
||||
project.inner.id,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.wrap_api_err(
|
||||
"executing `tech_review_sync::sync_deleted_project_tech_review_exit`",
|
||||
)?;
|
||||
|
||||
let context = ImageContext::Project {
|
||||
project_id: Some(project.inner.id.into()),
|
||||
};
|
||||
|
||||
@@ -970,9 +970,9 @@ pub async fn delete_file(
|
||||
database::models::version_item::cleanup_unused_attribution_files_and_groups(&mut transaction)
|
||||
.await.wrap_internal_err("deleting version item from database")?;
|
||||
|
||||
delphi::tech_review_sync::sync_project_tech_review_state(
|
||||
delphi::tech_review_queue::remove_projects_without_details(
|
||||
&[row.project_id],
|
||||
delphi::tech_review_sync::TechReviewExitReason::FileDeleted,
|
||||
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1317,9 +1317,9 @@ pub async fn version_delete(
|
||||
.await
|
||||
.wrap_internal_err("deleting version from database")?;
|
||||
|
||||
delphi::tech_review_sync::sync_project_tech_review_state(
|
||||
delphi::tech_review_queue::remove_projects_without_details(
|
||||
&[version.inner.project_id],
|
||||
delphi::tech_review_sync::TechReviewExitReason::FileDeleted,
|
||||
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user