mirror of
https://github.com/modrinth/code.git
synced 2026-08-01 05:36:39 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc77fd1b87 | ||
|
|
b92fc10e54 | ||
|
|
849943ed4e | ||
|
|
18c95e2f61 | ||
|
|
7c7efd5efe | ||
|
|
4c5e1074fd | ||
|
|
36d3aab0de | ||
|
|
88c0915066 | ||
|
|
f0dff8ebd5 | ||
|
|
8d82c228cf | ||
|
|
4b07ed720e | ||
|
|
ea42c644f3 | ||
|
|
28154689f4 | ||
|
|
3c77cc1502 | ||
|
|
b6b19e543c | ||
|
|
023c789a31 |
Generated
+88
-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,22 @@ 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",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "censor"
|
||||
version = "0.3.0"
|
||||
@@ -2855,6 +2909,15 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
|
||||
|
||||
[[package]]
|
||||
name = "delphi-cel-experiment"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cel",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -3520,7 +3583,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",
|
||||
]
|
||||
@@ -5463,6 +5526,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
"cel",
|
||||
"censor",
|
||||
"chrono",
|
||||
"chumsky",
|
||||
@@ -6202,6 +6266,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"
|
||||
@@ -7224,6 +7297,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"
|
||||
@@ -11756,6 +11835,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"
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"packages/app-lib",
|
||||
"packages/ariadne",
|
||||
"packages/daedalus",
|
||||
"packages/delphi-cel-experiment",
|
||||
"packages/labrinth-derive",
|
||||
"packages/modrinth-content-management",
|
||||
"packages/modrinth-log",
|
||||
@@ -56,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"] }
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
|
||||
@@ -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">
|
||||
@@ -33,9 +27,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, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
|
||||
}>()
|
||||
@@ -46,12 +42,4 @@ const localTraceLink = computed(
|
||||
props.trace.detail_id,
|
||||
)}`,
|
||||
)
|
||||
|
||||
function decodeTracePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -65,7 +65,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>
|
||||
@@ -133,6 +133,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()
|
||||
@@ -165,14 +166,6 @@ 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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -49,6 +48,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'
|
||||
|
||||
@@ -602,6 +602,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,
|
||||
@@ -1000,6 +1023,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
|
||||
|
||||
@@ -1026,7 +1051,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)
|
||||
@@ -1771,27 +1796,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"
|
||||
@@ -1865,9 +1875,12 @@ function copyId() {
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<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"
|
||||
@@ -1942,6 +1955,23 @@ function copyId() {
|
||||
</div>
|
||||
|
||||
<div class="detail-verdict-action-groups">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
type="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>
|
||||
</ButtonStyled>
|
||||
<div
|
||||
class="detail-verdict-buttons"
|
||||
role="group"
|
||||
|
||||
@@ -2957,6 +2957,9 @@
|
||||
"moderation.moderate": {
|
||||
"message": "Moderate"
|
||||
},
|
||||
"moderation.page.delphi-rules": {
|
||||
"message": "Delphi rules"
|
||||
},
|
||||
"moderation.page.external-projects": {
|
||||
"message": "External projects"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,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 { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||
|
||||
definePageMeta({
|
||||
@@ -51,6 +58,10 @@ const messages = defineMessages({
|
||||
id: 'moderation.page.global-detail-traces',
|
||||
defaultMessage: 'Global traces',
|
||||
},
|
||||
delphiRulesTitle: {
|
||||
id: 'moderation.page.delphi-rules',
|
||||
defaultMessage: 'Delphi rules',
|
||||
},
|
||||
})
|
||||
|
||||
const moderationLinks = [
|
||||
@@ -71,6 +82,11 @@ const moderationLinks = [
|
||||
href: '/moderation/global-traces',
|
||||
icon: HashIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.delphiRulesTitle),
|
||||
href: '/moderation/technical-review/rules',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const mobileNavOptions = [
|
||||
@@ -79,12 +95,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)
|
||||
@@ -110,6 +129,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')
|
||||
}
|
||||
|
||||
@@ -43,7 +43,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>
|
||||
@@ -103,6 +103,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()
|
||||
@@ -137,14 +138,6 @@ 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 'severe':
|
||||
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
ListFilterIcon,
|
||||
LoaderCircleIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
@@ -607,6 +609,13 @@ watch(totalPages, (pages) => {
|
||||
<div
|
||||
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
|
||||
>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/moderation/technical-review/rules">
|
||||
<SettingsIcon class="size-5" />
|
||||
Rules
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
|
||||
<Combobox
|
||||
v-model="currentResponseFilter"
|
||||
class="!w-full flex-grow sm:!w-[120px] sm:flex-grow-0"
|
||||
|
||||
@@ -0,0 +1,993 @@
|
||||
<template>
|
||||
<NewModal ref="ruleModal" :header="modalTitle" :on-hide="handleRuleModalHide">
|
||||
<form class="flex w-[48rem] max-w-full flex-col gap-3" @submit.prevent="saveRule">
|
||||
<label class="font-semibold text-contrast" for="rule-name">Name</label>
|
||||
<StyledInput
|
||||
id="rule-name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
placeholder="Known-safe obfuscated bootstrap"
|
||||
/>
|
||||
|
||||
<label class="font-semibold text-contrast" for="rule-priority">Priority</label>
|
||||
<StyledInput
|
||||
id="rule-priority"
|
||||
v-model="form.priority"
|
||||
type="number"
|
||||
:min="-2147483648"
|
||||
:max="2147483647"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Higher-priority rules run first. Rules with the same priority run in creation order.
|
||||
</p>
|
||||
|
||||
<label class="font-semibold text-contrast" for="rule-expression">CEL expression</label>
|
||||
<div
|
||||
class="relative overflow-hidden rounded-[20px] border border-solid border-surface-4 shadow-sm"
|
||||
>
|
||||
<component
|
||||
:is="editorComponent"
|
||||
v-if="editorComponent"
|
||||
id="rule-expression"
|
||||
:value="form.rule"
|
||||
lang="javascript"
|
||||
theme="modrinth"
|
||||
:print-margin="false"
|
||||
:options="RULE_EDITOR_OPTIONS"
|
||||
:style="{ height: '16rem', fontSize: '0.875rem' }"
|
||||
class="ace-modrinth rounded-[20px]"
|
||||
@init="onRuleEditorInit"
|
||||
@update:value="handleRuleInput"
|
||||
/>
|
||||
<div v-else class="flex h-64 items-center justify-center bg-bg-raised">
|
||||
<LoaderCircleIcon class="size-8 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Return <code>null</code> when the rule does not match, or a map containing
|
||||
<code>severity</code> and/or <code>hidden</code> when it does.
|
||||
</p>
|
||||
|
||||
<details class="rounded-xl border border-divider bg-bg-raised p-3">
|
||||
<summary class="cursor-pointer font-semibold text-contrast">
|
||||
Input and output schema
|
||||
</summary>
|
||||
<div v-if="isLoadingRuleSchema" class="mt-3 flex items-center gap-2 text-secondary">
|
||||
<LoaderCircleIcon class="size-4 animate-spin" />
|
||||
Loading schema…
|
||||
</div>
|
||||
<p v-else-if="ruleSchemaError" class="m-0 mt-3 text-sm text-red">
|
||||
{{ ruleSchemaError }}
|
||||
</p>
|
||||
<div v-else class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 mb-2 text-xs font-semibold uppercase tracking-wide text-secondary">
|
||||
Input (<code>input</code>)
|
||||
</p>
|
||||
<pre
|
||||
class="m-0 overflow-x-auto rounded-lg bg-surface-1 p-3 text-xs leading-relaxed text-contrast"
|
||||
><code>{{ ruleInputSchemaText }}</code></pre>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 mb-2 text-xs font-semibold uppercase tracking-wide text-secondary">
|
||||
Output
|
||||
</p>
|
||||
<pre
|
||||
class="m-0 overflow-x-auto rounded-lg bg-surface-1 p-3 text-xs leading-relaxed text-contrast"
|
||||
><code>{{ ruleOutputSchemaText }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section class="mt-2 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="m-0 text-base font-bold text-contrast">Test traces</h3>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
These results are evaluated from the current expression.
|
||||
</p>
|
||||
</div>
|
||||
<LoaderCircleIcon v-if="isTestingRule" class="size-5 animate-spin text-secondary" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ruleTestError"
|
||||
class="border-red/40 rounded-lg border bg-highlight-red p-3 text-sm text-red"
|
||||
>
|
||||
{{ ruleTestError }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="example in previewExamples"
|
||||
:key="example.original.key"
|
||||
class="grid items-stretch gap-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]"
|
||||
>
|
||||
<article class="universal-card flex min-w-0 flex-col gap-3">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary">Original</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize"
|
||||
:class="getSeverityBadgeColor(example.original.severity)"
|
||||
>
|
||||
{{ example.original.severity }}
|
||||
</span>
|
||||
<strong class="break-all text-contrast">{{ example.original.issue_type }}</strong>
|
||||
</div>
|
||||
<dl class="m-0 grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-sm">
|
||||
<dt class="text-secondary">Key</dt>
|
||||
<dd class="m-0 break-all font-mono text-contrast">{{ example.original.key }}</dd>
|
||||
<dt class="text-secondary">File</dt>
|
||||
<dd class="m-0 break-all font-mono text-contrast">
|
||||
{{ example.original.file_path }}
|
||||
</dd>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-center text-2xl font-bold text-secondary max-md:rotate-90"
|
||||
aria-hidden="true"
|
||||
>
|
||||
→
|
||||
</div>
|
||||
|
||||
<article class="universal-card flex min-w-0 flex-col gap-3">
|
||||
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary">
|
||||
New state
|
||||
</p>
|
||||
<div v-if="example.effect?.hidden" class="flex items-center gap-2 text-secondary">
|
||||
<EyeOffIcon class="size-5" />
|
||||
<strong class="text-contrast">Hidden from reports</strong>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize"
|
||||
:class="getSeverityBadgeColor(example.effectiveSeverity)"
|
||||
>
|
||||
{{ example.effectiveSeverity }}
|
||||
</span>
|
||||
<strong class="break-all text-contrast">{{ example.original.issue_type }}</strong>
|
||||
</div>
|
||||
<p class="m-0 text-sm text-secondary">{{ example.summary }}</p>
|
||||
</template>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="closeRuleModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button type="submit" :disabled="isSaving">
|
||||
{{ isSaving ? 'Saving...' : 'Save rule' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
|
||||
<ConfirmModal
|
||||
ref="deleteModal"
|
||||
:title="`Delete ${ruleToDelete?.name ?? 'rule'}?`"
|
||||
description="The rule will stop being included the next time the rules are scanned. Existing effects remain active until then."
|
||||
:markdown="false"
|
||||
proceed-label="Delete rule"
|
||||
@proceed="deleteRule"
|
||||
/>
|
||||
<ConfirmModal
|
||||
ref="scanModal"
|
||||
title="Run a full Delphi rule scan?"
|
||||
description="Every stored issue detail will be evaluated against the current rules. Existing effects remain active unless the entire scan succeeds."
|
||||
:markdown="false"
|
||||
proceed-label="Run full scan"
|
||||
@proceed="runFullScan"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<NuxtLink to="/moderation/technical-review" aria-label="Back to tech review queue">
|
||||
<ArrowLeftIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<div>
|
||||
<h1 class="m-0 text-2xl font-bold text-contrast">Delphi rules</h1>
|
||||
<p class="m-0 text-secondary">Transform or hide Delphi issue traces.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<ButtonStyled>
|
||||
<button type="button" :disabled="isScanning" @click="scanModal?.show()">
|
||||
<PlayIcon />
|
||||
{{ isScanning ? 'Scanning...' : 'Run full scan' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button type="button" :disabled="isScanning" @click="openCreateModal">
|
||||
<PlusIcon />
|
||||
Create rule
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="isScanning && scanProgress" class="universal-card flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="m-0 text-base font-bold text-contrast">Scanning Delphi rule effects</h2>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ scanProgress.scanned.toLocaleString() }} of
|
||||
{{ scanProgress.total.toLocaleString() }} details scanned ·
|
||||
{{ scanProgress.effects.toLocaleString() }} effects
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-sm font-semibold capitalize text-secondary">
|
||||
{{ scanProgress.phase }} revision {{ scanProgress.revision }}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
:progress="scanProgress.scanned"
|
||||
:max="Math.max(scanProgress.total, 1)"
|
||||
:waiting="scanProgress.total === 0 && scanProgress.phase !== 'complete'"
|
||||
full-width
|
||||
show-progress
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div v-if="isLoading" class="universal-card flex h-32 items-center justify-center">
|
||||
<LoaderCircleIcon class="size-8 animate-spin text-secondary" />
|
||||
</div>
|
||||
<div v-else-if="loadFailed" class="universal-card flex flex-col items-center gap-3 py-8">
|
||||
<p class="m-0 text-secondary">Failed to load Delphi rules.</p>
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="loadRules">Try again</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="rules.length === 0"
|
||||
type="no-search-result"
|
||||
heading="No Delphi rules"
|
||||
description="Create a rule to transform matching issue traces."
|
||||
/>
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<article
|
||||
v-for="rule in rules"
|
||||
:key="rule.id"
|
||||
class="universal-card relative flex flex-col gap-3 overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">{{ rule.name }}</h2>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Priority {{ rule.priority }} · Revision {{ rule.revision }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button type="button" :disabled="isScanning" @click="openEditModal(rule)">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button type="button" :disabled="isScanning" @click="openDeleteModal(rule)">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<pre
|
||||
class="m-0 overflow-x-auto rounded-lg bg-bg-raised p-3 text-sm"
|
||||
><code>{{ rule.rule }}</code></pre>
|
||||
|
||||
<section class="flex flex-col gap-2">
|
||||
<h3 class="m-0 text-sm font-semibold text-contrast">
|
||||
Affected details ({{ rule.affected_details_count.toLocaleString() }})
|
||||
</h3>
|
||||
<p v-if="rule.affected_details_count === 0" class="m-0 text-sm text-secondary">
|
||||
No details are affected in the current revision.
|
||||
</p>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="detail in getVisibleRuleDetails(rule)"
|
||||
:key="detail.detail_id"
|
||||
class="flex min-w-0 items-center justify-between gap-3 rounded-lg border border-divider bg-bg-raised px-3 py-2"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="mb-1 flex min-w-0 items-center gap-1.5 text-sm">
|
||||
<NuxtLink
|
||||
v-if="detail.project_id"
|
||||
:to="getProjectLink(detail)"
|
||||
class="flex min-w-0 items-center gap-1.5 font-semibold text-contrast hover:underline"
|
||||
>
|
||||
<Avatar
|
||||
:src="detail.project_icon_url"
|
||||
:alt="detail.project_name ?? ''"
|
||||
size="xs"
|
||||
no-shadow
|
||||
/>
|
||||
<span class="truncate">{{ detail.project_name ?? detail.project_id }}</span>
|
||||
</NuxtLink>
|
||||
<span v-else class="text-secondary">Unattached trace</span>
|
||||
<template v-if="detail.project_id && detail.version_id">
|
||||
<span class="shrink-0 text-secondary" aria-hidden="true">·</span>
|
||||
<NuxtLink
|
||||
:to="getVersionLink(detail)"
|
||||
class="truncate text-secondary hover:underline"
|
||||
>
|
||||
{{ detail.version_name ?? detail.version_number ?? detail.version_id }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span
|
||||
v-if="!detail.hidden"
|
||||
class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize"
|
||||
:class="getSeverityBadgeColor(detail.severity ?? detail.original_severity)"
|
||||
>
|
||||
{{ detail.severity ?? detail.original_severity }}
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1 text-xs font-semibold text-secondary">
|
||||
<EyeOffIcon class="size-4" />
|
||||
Hidden
|
||||
</span>
|
||||
<strong class="truncate text-sm text-contrast">{{ detail.issue_type }}</strong>
|
||||
</div>
|
||||
<p
|
||||
class="m-0 mt-0.5 flex min-w-0 items-center gap-1 font-mono text-xs text-secondary"
|
||||
>
|
||||
<IssueDetailPath
|
||||
:segments="[detail.jar, detail.file_path]"
|
||||
hide-base-mrpack
|
||||
truncate
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<NuxtLink v-if="detail.project_id" :to="getAffectedDetailLink(detail)">
|
||||
<ExternalIcon />
|
||||
View
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
disabled
|
||||
title="This trace is not attached to a project"
|
||||
>
|
||||
<ExternalIcon />
|
||||
View
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="rule.affected_details_count > 3"
|
||||
class="relative z-20 mt-1 flex justify-center"
|
||||
>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="loadingAffectedRuleIds.has(rule.id)"
|
||||
@click="toggleAffectedDetails(rule)"
|
||||
>
|
||||
<LoaderCircleIcon
|
||||
v-if="loadingAffectedRuleIds.has(rule.id)"
|
||||
class="animate-spin"
|
||||
/>
|
||||
{{ expandedAffectedDetails.has(rule.id) ? 'Show less' : 'Show more' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
v-if="rule.affected_details_count > 3 && !expandedAffectedDetails.has(rule.id)"
|
||||
class="pointer-events-none absolute inset-0 z-10 bg-gradient-to-b from-transparent to-surface-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Labrinth, SseParser } from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeOffIcon,
|
||||
LoaderCircleIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
ProgressBar,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import type { Ace } from 'ace-builds'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
|
||||
|
||||
const DEFAULT_RULE = `input.trace.issue_type == "OBFUSCATED_NAMES"
|
||||
? {"severity": "low", "hidden": false}
|
||||
: null`
|
||||
const RULE_EDITOR_OPTIONS: Partial<Ace.EditorOptions> = {
|
||||
useWorker: false,
|
||||
tabSize: 2,
|
||||
useSoftTabs: true,
|
||||
}
|
||||
|
||||
const TEST_INPUTS: Labrinth.TechReview.Internal.RuleInput[] = [
|
||||
{
|
||||
schema_version: 1,
|
||||
trace: {
|
||||
key: 'known-safe:obfuscated-bootstrap',
|
||||
issue_type: 'OBFUSCATED_NAMES',
|
||||
severity: 'high',
|
||||
jar: 'META-INF/jars/embedded.jar',
|
||||
file_path: 'com/example/Bootstrap.class',
|
||||
data: {
|
||||
confidence: 0.97,
|
||||
symbol_count: 42,
|
||||
},
|
||||
},
|
||||
scan: {
|
||||
delphi_version: 17,
|
||||
},
|
||||
artifact: {
|
||||
size: 412_892,
|
||||
hashes: {
|
||||
sha1: '0123456789abcdef',
|
||||
sha512: 'fedcba9876543210',
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
project_id: 'example-project',
|
||||
version_id: 'example-version',
|
||||
file_id: 'example-file',
|
||||
},
|
||||
},
|
||||
{
|
||||
schema_version: 1,
|
||||
trace: {
|
||||
key: 'network/known-telemetry-host',
|
||||
issue_type: 'SUSPICIOUS_NETWORK_ACCESS',
|
||||
severity: 'medium',
|
||||
jar: null,
|
||||
file_path: 'com/example/Telemetry.class',
|
||||
data: {
|
||||
host: 'telemetry.example.com',
|
||||
},
|
||||
},
|
||||
scan: {
|
||||
delphi_version: 18,
|
||||
},
|
||||
artifact: {
|
||||
size: 98_304,
|
||||
hashes: {
|
||||
sha1: 'abcdef0123456789',
|
||||
sha512: '0123456789abcdef',
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
project_id: 'telemetry-project',
|
||||
version_id: 'telemetry-version',
|
||||
file_id: 'telemetry-file',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
useHead({ title: 'Delphi rules - Modrinth' })
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const ruleModal = useTemplateRef<InstanceType<typeof NewModal>>('ruleModal')
|
||||
const deleteModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('deleteModal')
|
||||
const scanModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('scanModal')
|
||||
const editorComponent = shallowRef<Component | null>(null)
|
||||
const ruleEditorInstance = shallowRef<Ace.Editor | null>(null)
|
||||
|
||||
const rules = ref<Labrinth.TechReview.Internal.DelphiRule[]>([])
|
||||
const isLoading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const isScanning = ref(false)
|
||||
const isTestingRule = ref(false)
|
||||
const isLoadingRuleSchema = ref(false)
|
||||
const isRuleModalOpen = ref(false)
|
||||
const loadFailed = ref(false)
|
||||
const ruleSchemaError = ref<string | null>(null)
|
||||
const ruleSchema = ref<Labrinth.TechReview.Internal.DelphiRuleSchemaResponse | null>(null)
|
||||
const editingRuleId = ref<number | null>(null)
|
||||
const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null)
|
||||
const ruleTestEffects = ref<Array<Labrinth.TechReview.Internal.DelphiRuleEffect | null>>([])
|
||||
const ruleTestError = ref<string | null>(null)
|
||||
const scanProgress = ref<Labrinth.TechReview.Internal.DelphiRuleScanEvent | null>(null)
|
||||
const expandedAffectedDetails = reactive(
|
||||
new Map<number, Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]>(),
|
||||
)
|
||||
const loadingAffectedRuleIds = reactive(new Set<number>())
|
||||
const form = reactive({
|
||||
name: '',
|
||||
priority: 0 as number | undefined,
|
||||
rule: DEFAULT_RULE,
|
||||
})
|
||||
let ruleTestRequestId = 0
|
||||
let scanAbortController: AbortController | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
const [{ VAceEditor }] = await Promise.all([
|
||||
import('vue3-ace-editor'),
|
||||
import('@modrinth/ui/src/utils/ace-theme'),
|
||||
])
|
||||
editorComponent.value = VAceEditor
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => (editingRuleId.value === null ? 'Create rule' : 'Edit rule'))
|
||||
const ruleInputSchemaText = computed(() =>
|
||||
ruleSchema.value ? formatRuleSchema(ruleSchema.value.input, ruleSchema.value.components) : '',
|
||||
)
|
||||
const ruleOutputSchemaText = computed(() =>
|
||||
ruleSchema.value ? formatRuleSchema(ruleSchema.value.output, ruleSchema.value.components) : '',
|
||||
)
|
||||
const previewExamples = computed(() =>
|
||||
TEST_INPUTS.map(({ trace: original }, index) => {
|
||||
const effect = ruleTestEffects.value[index] ?? null
|
||||
const effectiveSeverity = effect?.severity ?? original.severity
|
||||
let summary: string
|
||||
|
||||
if (isTestingRule.value) {
|
||||
summary = 'Evaluating the current expression...'
|
||||
} else if (ruleTestError.value) {
|
||||
summary = 'Preview unavailable.'
|
||||
} else if (!effect) {
|
||||
summary = 'Rule does not match; no change.'
|
||||
} else if (effect.severity && effect.severity !== original.severity) {
|
||||
summary = `Severity changed from ${original.severity} to ${effect.severity}.`
|
||||
} else {
|
||||
summary = 'Rule matched; no visible change.'
|
||||
}
|
||||
|
||||
return {
|
||||
original,
|
||||
effect,
|
||||
effectiveSeverity,
|
||||
summary,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
|
||||
switch (severity) {
|
||||
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 'low':
|
||||
default:
|
||||
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||
}
|
||||
}
|
||||
|
||||
function isSchema(value: unknown): value is Labrinth.TechReview.Internal.DelphiRuleSchema {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function formatRuleSchema(
|
||||
schema: Labrinth.TechReview.Internal.DelphiRuleSchema,
|
||||
components: Record<string, Labrinth.TechReview.Internal.DelphiRuleSchema>,
|
||||
depth = 0,
|
||||
visitedReferences = new Set<string>(),
|
||||
): string {
|
||||
if (typeof schema.$ref === 'string') {
|
||||
const name = decodeURIComponent(schema.$ref.split('/').at(-1) ?? '')
|
||||
if (visitedReferences.has(name)) return name || 'unknown'
|
||||
|
||||
const referencedSchema = components[name]
|
||||
if (!referencedSchema) return name || 'unknown'
|
||||
|
||||
const visited = new Set(visitedReferences)
|
||||
visited.add(name)
|
||||
return formatRuleSchema(referencedSchema, components, depth, visited)
|
||||
}
|
||||
|
||||
const resolved = schema
|
||||
const alternatives = [resolved.oneOf, resolved.anyOf].find(Array.isArray)
|
||||
if (alternatives) {
|
||||
return alternatives
|
||||
.filter(isSchema)
|
||||
.map((alternative) => formatRuleSchema(alternative, components, depth, visitedReferences))
|
||||
.join(' | ')
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved.enum)) {
|
||||
return resolved.enum.map((value) => JSON.stringify(value)).join(' | ')
|
||||
}
|
||||
|
||||
const declaredTypes = Array.isArray(resolved.type)
|
||||
? resolved.type.filter((type): type is string => typeof type === 'string')
|
||||
: typeof resolved.type === 'string'
|
||||
? [resolved.type]
|
||||
: []
|
||||
const nullable = resolved.nullable === true || declaredTypes.includes('null')
|
||||
const type = declaredTypes.find((value) => value !== 'null')
|
||||
let formatted: string
|
||||
|
||||
if (type === 'object' || isSchema(resolved.properties) || resolved.additionalProperties) {
|
||||
const properties = isSchema(resolved.properties) ? resolved.properties : {}
|
||||
const required = new Set(
|
||||
Array.isArray(resolved.required)
|
||||
? resolved.required.filter((name): name is string => typeof name === 'string')
|
||||
: [],
|
||||
)
|
||||
const indentation = ' '.repeat(depth)
|
||||
const childIndentation = ' '.repeat(depth + 1)
|
||||
const lines = Object.entries(properties)
|
||||
.filter((entry): entry is [string, Labrinth.TechReview.Internal.DelphiRuleSchema] =>
|
||||
isSchema(entry[1]),
|
||||
)
|
||||
.map(
|
||||
([name, property]) =>
|
||||
`${childIndentation}${JSON.stringify(name)}${required.has(name) ? '' : '?'}: ${formatRuleSchema(property, components, depth + 1, visitedReferences)}`,
|
||||
)
|
||||
|
||||
if (isSchema(resolved.additionalProperties)) {
|
||||
lines.push(
|
||||
`${childIndentation}[key: string]: ${formatRuleSchema(resolved.additionalProperties, components, depth + 1, visitedReferences)}`,
|
||||
)
|
||||
} else if (resolved.additionalProperties === true) {
|
||||
lines.push(`${childIndentation}[key: string]: unknown`)
|
||||
}
|
||||
|
||||
formatted = lines.length === 0 ? '{}' : `{\n${lines.join(',\n')}\n${indentation}}`
|
||||
} else if (type === 'array') {
|
||||
formatted = isSchema(resolved.items)
|
||||
? `Array<${formatRuleSchema(resolved.items, components, depth, visitedReferences)}>`
|
||||
: 'unknown[]'
|
||||
} else if (type === 'integer' || type === 'number') {
|
||||
formatted = 'number'
|
||||
} else if (type === 'boolean' || type === 'string' || type === 'null') {
|
||||
formatted = type
|
||||
} else {
|
||||
formatted = 'unknown'
|
||||
}
|
||||
|
||||
return nullable && formatted !== 'null' ? `${formatted} | null` : formatted
|
||||
}
|
||||
|
||||
function onRuleEditorInit(editor: Ace.Editor) {
|
||||
ruleEditorInstance.value = editor
|
||||
editor.session.setUseWrapMode(true)
|
||||
}
|
||||
|
||||
function handleRuleInput(rule: string) {
|
||||
form.rule = rule
|
||||
queueRuleTest()
|
||||
}
|
||||
|
||||
async function testRule() {
|
||||
if (!isRuleModalOpen.value) return
|
||||
|
||||
const requestId = ++ruleTestRequestId
|
||||
const rule = form.rule.trim()
|
||||
ruleTestEffects.value = []
|
||||
ruleTestError.value = null
|
||||
|
||||
if (!rule) {
|
||||
isTestingRule.value = false
|
||||
ruleTestError.value = 'Enter a CEL expression to test it.'
|
||||
return
|
||||
}
|
||||
|
||||
isTestingRule.value = true
|
||||
try {
|
||||
const response = await client.labrinth.tech_review_internal.testRule({
|
||||
rule,
|
||||
inputs: TEST_INPUTS,
|
||||
})
|
||||
if (requestId !== ruleTestRequestId) return
|
||||
|
||||
ruleTestEffects.value = response.effects
|
||||
} catch (error) {
|
||||
if (requestId !== ruleTestRequestId) return
|
||||
|
||||
ruleTestError.value = error instanceof Error ? error.message : 'The rule could not be tested.'
|
||||
} finally {
|
||||
if (requestId === ruleTestRequestId) {
|
||||
isTestingRule.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testRuleDebounced = useDebounceFn(testRule, 350)
|
||||
|
||||
function queueRuleTest() {
|
||||
if (!isRuleModalOpen.value) return
|
||||
|
||||
ruleTestRequestId += 1
|
||||
ruleTestEffects.value = []
|
||||
ruleTestError.value = null
|
||||
isTestingRule.value = true
|
||||
void testRuleDebounced()
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
isLoading.value = true
|
||||
loadFailed.value = false
|
||||
try {
|
||||
rules.value = await client.labrinth.tech_review_internal.getRules()
|
||||
expandedAffectedDetails.clear()
|
||||
} catch (error) {
|
||||
console.error('Failed to load Delphi rules', error)
|
||||
loadFailed.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRuleSchema() {
|
||||
if (ruleSchema.value || isLoadingRuleSchema.value) return
|
||||
|
||||
isLoadingRuleSchema.value = true
|
||||
ruleSchemaError.value = null
|
||||
try {
|
||||
ruleSchema.value = await client.labrinth.tech_review_internal.getRuleSchema()
|
||||
} catch (error) {
|
||||
console.error('Failed to load Delphi rule schema', error)
|
||||
ruleSchemaError.value = 'The rule input and output schema could not be loaded.'
|
||||
} finally {
|
||||
isLoadingRuleSchema.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getVisibleRuleDetails(
|
||||
rule: Labrinth.TechReview.Internal.DelphiRule,
|
||||
): Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[] {
|
||||
return expandedAffectedDetails.get(rule.id) ?? rule.affected_details
|
||||
}
|
||||
|
||||
function getAffectedDetailLink(
|
||||
detail: Labrinth.TechReview.Internal.DelphiRuleAffectedDetail,
|
||||
): string {
|
||||
return `/moderation/technical-review/${detail.project_id}?detail=${encodeURIComponent(detail.detail_id)}`
|
||||
}
|
||||
|
||||
function getProjectLink(detail: Labrinth.TechReview.Internal.DelphiRuleAffectedDetail): string {
|
||||
return `/project/${detail.project_id}`
|
||||
}
|
||||
|
||||
function getVersionLink(detail: Labrinth.TechReview.Internal.DelphiRuleAffectedDetail): string {
|
||||
return `/project/${detail.project_id}/version/${detail.version_id}`
|
||||
}
|
||||
|
||||
async function toggleAffectedDetails(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
||||
if (expandedAffectedDetails.has(rule.id)) {
|
||||
expandedAffectedDetails.delete(rule.id)
|
||||
return
|
||||
}
|
||||
if (loadingAffectedRuleIds.has(rule.id)) return
|
||||
|
||||
loadingAffectedRuleIds.add(rule.id)
|
||||
try {
|
||||
const details = await client.labrinth.tech_review_internal.getRuleAffectedDetails(rule.id)
|
||||
expandedAffectedDetails.set(rule.id, details)
|
||||
} catch (error) {
|
||||
console.error('Failed to load details affected by Delphi rule', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to load affected details',
|
||||
text: 'The complete list of affected details could not be loaded.',
|
||||
})
|
||||
} finally {
|
||||
loadingAffectedRuleIds.delete(rule.id)
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
if (isScanning.value) return
|
||||
editingRuleId.value = null
|
||||
form.name = ''
|
||||
form.priority = 0
|
||||
form.rule = DEFAULT_RULE
|
||||
isRuleModalOpen.value = true
|
||||
ruleModal.value?.show()
|
||||
nextTick(() => ruleEditorInstance.value?.resize(true))
|
||||
void loadRuleSchema()
|
||||
void testRule()
|
||||
}
|
||||
|
||||
function openEditModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
||||
if (isScanning.value) return
|
||||
editingRuleId.value = rule.id
|
||||
form.name = rule.name
|
||||
form.priority = rule.priority
|
||||
form.rule = rule.rule
|
||||
isRuleModalOpen.value = true
|
||||
ruleModal.value?.show()
|
||||
nextTick(() => ruleEditorInstance.value?.resize(true))
|
||||
void loadRuleSchema()
|
||||
void testRule()
|
||||
}
|
||||
|
||||
function closeRuleModal() {
|
||||
ruleModal.value?.hide()
|
||||
}
|
||||
|
||||
function handleRuleModalHide() {
|
||||
isRuleModalOpen.value = false
|
||||
ruleEditorInstance.value = null
|
||||
ruleTestRequestId += 1
|
||||
isTestingRule.value = false
|
||||
}
|
||||
|
||||
async function saveRule() {
|
||||
if (isSaving.value || isScanning.value) return
|
||||
|
||||
const priority = form.priority
|
||||
if (
|
||||
!form.name.trim() ||
|
||||
!form.rule.trim() ||
|
||||
typeof priority !== 'number' ||
|
||||
!Number.isInteger(priority) ||
|
||||
priority < -2147483648 ||
|
||||
priority > 2147483647
|
||||
) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Invalid rule',
|
||||
text: 'Enter a name, an integer priority, and a CEL expression.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
const payload = {
|
||||
name: form.name,
|
||||
priority,
|
||||
rule: form.rule,
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingRuleId.value === null) {
|
||||
await client.labrinth.tech_review_internal.createRule(payload)
|
||||
} else {
|
||||
await client.labrinth.tech_review_internal.updateRule(editingRuleId.value, payload)
|
||||
}
|
||||
closeRuleModal()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Rule saved',
|
||||
text: 'The rule will take effect after the next manual scan.',
|
||||
})
|
||||
await loadRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to save Delphi rule', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to save rule',
|
||||
text: 'Check the CEL expression and try again.',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
||||
if (isScanning.value) return
|
||||
ruleToDelete.value = rule
|
||||
deleteModal.value?.show()
|
||||
}
|
||||
|
||||
async function deleteRule() {
|
||||
if (isScanning.value) return
|
||||
const rule = ruleToDelete.value
|
||||
if (!rule) return
|
||||
|
||||
try {
|
||||
await client.labrinth.tech_review_internal.deleteRule(rule.id)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Rule deleted',
|
||||
text: `${rule.name} will be removed by the next manual scan.`,
|
||||
})
|
||||
await loadRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete Delphi rule', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to delete rule',
|
||||
text: 'The Delphi rule could not be deleted.',
|
||||
})
|
||||
} finally {
|
||||
ruleToDelete.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runFullScan() {
|
||||
if (isScanning.value) return
|
||||
|
||||
isScanning.value = true
|
||||
scanProgress.value = null
|
||||
scanAbortController = new AbortController()
|
||||
let completed = false
|
||||
|
||||
try {
|
||||
const stream = await client.labrinth.tech_review_internal.scanRules(scanAbortController.signal)
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const parser = new SseParser()
|
||||
|
||||
const processItems = (items: ReturnType<SseParser['feed']>) => {
|
||||
for (const item of items) {
|
||||
if (item.kind !== 'event') continue
|
||||
|
||||
if (item.event === 'failed') {
|
||||
const error = JSON.parse(
|
||||
item.data,
|
||||
) as Labrinth.TechReview.Internal.DelphiRuleScanErrorEvent
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
if (item.event === 'progress' || item.event === 'complete') {
|
||||
scanProgress.value = JSON.parse(
|
||||
item.data,
|
||||
) as Labrinth.TechReview.Internal.DelphiRuleScanEvent
|
||||
completed ||= item.event === 'complete'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
processItems(parser.feed(decoder.decode(value, { stream: true })))
|
||||
}
|
||||
|
||||
const finalChunk = decoder.decode()
|
||||
if (finalChunk) processItems(parser.feed(finalChunk))
|
||||
processItems(parser.end())
|
||||
|
||||
if (!completed || !scanProgress.value) {
|
||||
throw new Error('The scan stream ended before the new revision was published.')
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Rule scan complete',
|
||||
text: `${scanProgress.value.scanned.toLocaleString()} details were scanned for revision ${scanProgress.value.revision}.`,
|
||||
})
|
||||
await loadRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to scan Delphi rules', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Rule scan failed',
|
||||
text: error instanceof Error ? error.message : 'The previous rule revision remains active.',
|
||||
})
|
||||
} finally {
|
||||
isScanning.value = false
|
||||
scanAbortController = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRules)
|
||||
onUnmounted(() => scanAbortController?.abort())
|
||||
</script>
|
||||
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
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES\n (1000, 'userland', 'userland@modrinth.com', 'developer'),\n (1001, 'useful', 'useful@modrinth.com', 'developer'),\n (1002, 'Useless', 'useless@modrinth.com', 'developer')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021"
|
||||
}
|
||||
Generated
+188
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"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\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\t\tpreview.hidden AS \"hidden?\"\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\teffect.hidden,\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": "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"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "affected_details_count!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "detail_id?: DelphiReportIssueDetailsId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "issue_id?: DelphiReportIssueId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "project_id?: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "project_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "project_icon_url?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "version_id?: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "version_name?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "version_number?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "issue_type?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "key?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "jar?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "file_path?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "original_severity?: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "effect_severity?: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "hidden?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "159d482c1859b0d0d42a15fb2b49e3a9a17755694448e00bf5e5faccb3ace76d"
|
||||
}
|
||||
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
-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
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
|
||||
}
|
||||
Generated
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"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 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(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 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 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": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "delphi_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "size?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "file_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "version_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "project_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d"
|
||||
}
|
||||
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
-28
File diff suppressed because one or more lines are too long
Generated
+28
File diff suppressed because one or more lines are too long
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"
|
||||
}
|
||||
Generated
+104
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"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 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(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 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 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": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "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": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES (2100, 'prefix_under_score', 'prefix_under_score@modrinth.com', 'developer')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec"
|
||||
}
|
||||
Generated
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id AS \"id!: DelphiRuleId\", 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": "rule",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3f1aaeed6e394294fa32da7e7f83765fb64bd9499a796470d544466886e1a808"
|
||||
}
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
|
||||
"query": "\n SELECT COUNT(*) AS \"count!\"\n FROM delphi_report_issue_details\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
@@ -16,5 +16,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
|
||||
"hash": "42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e"
|
||||
}
|
||||
+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 NOT didws.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": "436b1dad031f143ba987d992514c7fb02b2a67253c543ce146cf2cf190010a06"
|
||||
}
|
||||
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"
|
||||
}
|
||||
+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 NOT didws.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": "4a30e36261229605104f319105e88771f211fc82c2e8dc67fdd36a38d45b9f16"
|
||||
}
|
||||
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"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE users SET email_verified = TRUE WHERE id = $1",
|
||||
"query": "DELETE FROM delphi_rule_effects WHERE revision <> $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9"
|
||||
"hash": "50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705"
|
||||
}
|
||||
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 NOT didws.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 NOT detail.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": "54fd60c11e2a7691b0efcc384917137bc64408856543ca32091bab0ecc91d819"
|
||||
}
|
||||
Generated
-135
File diff suppressed because one or more lines are too long
Generated
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES ($1, $2, $3, 'developer')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81"
|
||||
}
|
||||
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"
|
||||
}
|
||||
+2
-2
@@ -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 NOT didws.hidden\n ORDER BY didws.issue_id, didws.id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -117,5 +117,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73"
|
||||
"hash": "63c94bea3d26a55b5eab89f2f6fac0eef45ebaa301f2ae14de4d5f3dcbd65cbb"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
|
||||
}
|
||||
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
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Numeric",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
|
||||
}
|
||||
+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
+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 )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\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": "73e3646a21e4acb1299f0b183b92beda18c30564fc90e5213e5237f505504558"
|
||||
}
|
||||
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 NOT didws.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 NOT didws.hidden\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8107e60111543c351cfa8aaf0d864e270b7b9f3f8041c5feffa43449f4d5251a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE SCHEMA public;",
|
||||
"query": "LOCK TABLE delphi_rules IN SHARE MODE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2"
|
||||
"hash": "820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2"
|
||||
}
|
||||
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 NOT detail.hidden\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "86141f67ab4efad36497a839e62e989332725543e1d3b11415502dc8521b724d"
|
||||
}
|
||||
Generated
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"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 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(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 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 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": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "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": "hashes!: Json<BTreeMap<String, String>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9"
|
||||
}
|
||||
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 NOT detail.hidden\n ) AS old_needs_review,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND NOT COALESCE(new_effect.hidden, FALSE)\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": "89f8fcde28ddb5bc2ccf3beef08f03642603c79db754d0530a9c6f2f7d9c32b7"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE moderation_locks SET locked_at = NOW() - ($1::bigint * INTERVAL '1 minute') - INTERVAL '1 second' WHERE project_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7"
|
||||
}
|
||||
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"
|
||||
}
|
||||
Generated
+138
File diff suppressed because one or more lines are too long
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
@@ -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 NOT didws.hidden\n ORDER BY didws.id\n LIMIT $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -144,5 +144,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a"
|
||||
"hash": "a7c4204b80587b0b73127125c695d3c779b1a052c82797b771e8fc9e38169475"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE mods SET status = 'processing' WHERE id = $1",
|
||||
"query": "UPDATE delphi_rule_revisions SET revision = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d"
|
||||
"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"
|
||||
}
|
||||
+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 NOT didws.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": "b4f5fbf3bdc7802c8e282861f1a51991b5b114e1b793210f1d31e0144c2138b9"
|
||||
}
|
||||
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"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
|
||||
}
|
||||
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 SELECT\n p.project_id AS \"project_id!: DBProjectId\",\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 = p.project_id\n AND didws.status = 'pending'\n AND dri.issue_type != $3\n ) AS \"has_pending_detail!\",\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 = p.project_id\n AND didws.status = 'unsafe'\n AND dri.issue_type != $3\n ) AS \"has_unsafe_detail!\",\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 = p.project_id\n AND didws.status = 'pending'\n AND dri.issue_type = $3\n ) 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 project_ids 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": "bb94018c84f3809b9ad89f35ff08501addc8f203a0a152346aaebd32db52be30"
|
||||
}
|
||||
+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 NOT didws.hidden\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53"
|
||||
"hash": "c2d2e0feb73843db73c73e8137ff0090ceb2292526e7e9ad5d54c51e8d732638"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DROP SCHEMA public CASCADE;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7"
|
||||
}
|
||||
Generated
-138
File diff suppressed because one or more lines are too long
Generated
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
|
||||
}
|
||||
Generated
+135
File diff suppressed because one or more lines are too long
Generated
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
|
||||
}
|
||||
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
+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"
|
||||
}
|
||||
+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 )\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 NOT didws.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 )\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": "4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a"
|
||||
"hash": "e38dc9448a56b2dc0e99661775f488c8f1ad2f0fca12655a3cc257a8cb5a784d"
|
||||
}
|
||||
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
-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 )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\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": "e8b6e423aaf62046afd19fb1e177a3ef1f18f4814df8924926c47d4b0c7145c7"
|
||||
}
|
||||
+2
-2
@@ -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 )\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 NOT didws.hidden\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": [
|
||||
{
|
||||
@@ -143,5 +143,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4"
|
||||
"hash": "ebbd8dce6a062c18f329381c1d17ece6727b6633053e1fc8b00e48cd539073f7"
|
||||
}
|
||||
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 NOT didws.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 NOT didws.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 NOT visible_detail.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": "f0333b3cc5ecd51adf07542b85107c8befb4cdaa728a167bd2586898875836c1"
|
||||
}
|
||||
Generated
+130
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"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\t\teffect.hidden\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": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "effect_severity: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "hidden",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f803c8d7ea8457d298beb4acc43667f7e638d7e487c09c86857d316fca40813b"
|
||||
}
|
||||
Generated
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO delphi_rule_effects (\n revision,\n detail_id,\n rule_id,\n severity,\n hidden\n )\n SELECT $1, effect.*\n FROM UNNEST(\n $2::BIGINT[],\n $3::BIGINT[],\n $4::delphi_severity[],\n $5::BOOLEAN[]\n ) AS effect(detail_id, rule_id, severity, hidden)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8Array",
|
||||
"Int8Array",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "delphi_severity[]",
|
||||
"kind": {
|
||||
"Array": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"BoolArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f94722c5b0092d3fcdb340da0402fb5a46f2ab5121a42b00ee8c0498b2871d58"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_advisory_unlock(1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pg_advisory_unlock",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429"
|
||||
}
|
||||
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"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
|
||||
}
|
||||
@@ -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,102 @@
|
||||
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,
|
||||
hidden BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
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,
|
||||
COALESCE(dre.hidden, FALSE) AS hidden,
|
||||
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 NOT didws.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';
|
||||
@@ -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);
|
||||
|
||||
@@ -30,7 +30,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(
|
||||
@@ -208,6 +208,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
|
||||
@@ -227,7 +228,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,
|
||||
@@ -243,12 +244,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 NOT didws.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 NOT didws.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 NOT detail.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,411 +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 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
|
||||
)
|
||||
SELECT
|
||||
p.project_id AS "project_id!: DBProjectId",
|
||||
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 = p.project_id
|
||||
AND didws.status = 'pending'
|
||||
AND dri.issue_type != $3
|
||||
) AS "has_pending_detail!",
|
||||
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 = p.project_id
|
||||
AND didws.status = 'unsafe'
|
||||
AND dri.issue_type != $3
|
||||
) AS "has_unsafe_detail!",
|
||||
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 = p.project_id
|
||||
AND didws.status = 'pending'
|
||||
AND dri.issue_type = $3
|
||||
) 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 project_ids 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?;
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -118,6 +118,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,
|
||||
|
||||
@@ -295,6 +295,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 NOT didws.hidden
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -542,6 +543,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 NOT didws.hidden
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -770,6 +772,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 NOT didws.hidden
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -898,6 +901,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 NOT didws.hidden
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
|
||||
@@ -33,10 +33,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,
|
||||
@@ -44,10 +41,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)
|
||||
@@ -250,7 +251,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 NOT didws.hidden
|
||||
)
|
||||
) AS "data!: sqlx::types::Json<FileIssue>"
|
||||
FROM delphi_report_issues dri
|
||||
@@ -305,6 +308,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 NOT didws.hidden
|
||||
), 'low'::delphi_severity),
|
||||
-- TODO: replace with `json_array` in Postgres 16
|
||||
'issues', (
|
||||
SELECT coalesce(json_agg(
|
||||
@@ -327,15 +339,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 NOT didws.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 NOT visible_detail.hidden
|
||||
)
|
||||
)
|
||||
) AS "data!: sqlx::types::Json<FileReport>"
|
||||
FROM delphi_reports dr
|
||||
@@ -530,7 +549,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 NOT didws.hidden
|
||||
ORDER BY didws.issue_id, didws.id
|
||||
"#,
|
||||
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
|
||||
@@ -628,14 +649,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,
|
||||
@@ -644,12 +665,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(),
|
||||
@@ -728,13 +760,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 NOT didws.hidden
|
||||
LEFT JOIN threads_messages tm_last
|
||||
ON tm_last.thread_id = t.id
|
||||
AND tm_last.id = (
|
||||
@@ -778,8 +812,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 NOT detail.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')))
|
||||
@@ -789,8 +840,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
|
||||
@@ -1035,8 +1086,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 NOT didws.hidden
|
||||
"#,
|
||||
project_id as _,
|
||||
)
|
||||
@@ -1055,24 +1105,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")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
let record = sqlx::query!(
|
||||
r#"
|
||||
@@ -1257,10 +1301,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
|
||||
@@ -1320,10 +1360,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,
|
||||
)
|
||||
@@ -1338,9 +1375,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?;
|
||||
@@ -1448,9 +1484,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?;
|
||||
|
||||
@@ -188,9 +188,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 NOT didws.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 || '%'
|
||||
@@ -248,7 +248,7 @@ pub async fn search_global_issue_details(
|
||||
AND didv.detail_key = didws.key
|
||||
WHERE
|
||||
didws.key = ANY($1::text[])
|
||||
AND dri.issue_type != '__dummy'
|
||||
AND NOT didws.hidden
|
||||
)
|
||||
SELECT
|
||||
detail_key AS "detail_key!",
|
||||
@@ -367,9 +367,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 NOT didws.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
|
||||
"#,
|
||||
@@ -412,7 +412,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 NOT didws.hidden
|
||||
ORDER BY didws.id
|
||||
LIMIT $3
|
||||
"#,
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
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::{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 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: Option<DelphiSeverity>,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
#[serde(default)]
|
||||
pub severity: Option<DelphiSeverity>,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
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::Validation(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!("invalid cel expression: {error}"))
|
||||
})?;
|
||||
|
||||
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?;
|
||||
|
||||
let mut request = body.into_inner();
|
||||
request.rule = request.rule.trim().to_string();
|
||||
request.validate().map_err(|error| {
|
||||
ApiError::Validation(validation_errors_to_string(error, None))
|
||||
})?;
|
||||
|
||||
let rule = request.rule;
|
||||
let program =
|
||||
tokio::task::spawn_blocking(move || cel::Program::compile(&rule))
|
||||
.await
|
||||
.wrap_internal_err("failed to join cel compilation task")?
|
||||
.map_err(|error| {
|
||||
ApiError::Request(eyre!("invalid cel expression: {error}"))
|
||||
})?;
|
||||
let mut effects = Vec::with_capacity(request.inputs.len());
|
||||
|
||||
for (index, input) in request.inputs.iter().enumerate() {
|
||||
let effect = super::rules_scan::evaluate_rule(&program, input)
|
||||
.map_err(|error| {
|
||||
ApiError::Request(eyre!(
|
||||
"failed to evaluate test input {index}: {error}"
|
||||
))
|
||||
})?;
|
||||
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?;
|
||||
|
||||
let rules = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
delphi_rule.id AS "id!: DelphiRuleId",
|
||||
delphi_rule.name,
|
||||
delphi_rule.rule,
|
||||
delphi_rule.priority,
|
||||
delphi_rule.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",
|
||||
preview.hidden AS "hidden?"
|
||||
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,
|
||||
effect.hidden,
|
||||
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(hidden),
|
||||
) = (
|
||||
rule.detail_id,
|
||||
rule.issue_id,
|
||||
rule.issue_type,
|
||||
rule.key,
|
||||
rule.file_path,
|
||||
rule.original_severity,
|
||||
rule.hidden,
|
||||
) {
|
||||
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: rule.effect_severity,
|
||||
hidden,
|
||||
})
|
||||
} 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,
|
||||
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?;
|
||||
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",
|
||||
effect.hidden
|
||||
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,
|
||||
hidden: detail.hidden,
|
||||
})
|
||||
.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?;
|
||||
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,
|
||||
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?;
|
||||
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")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
Ok(web::Json(DelphiRule {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
rule: rule.rule,
|
||||
priority: rule.priority,
|
||||
revision: rule.revision,
|
||||
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?;
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use bytes::Bytes;
|
||||
use eyre::{Context as _, Result, eyre};
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Json;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use utoipa::{PartialSchema, ToSchema};
|
||||
use xredis::RedisPool;
|
||||
|
||||
use super::rules::DelphiRuleEffect;
|
||||
use crate::routes::internal::delphi::tech_review_queue::{
|
||||
self, TechReviewRemovalReason,
|
||||
};
|
||||
use crate::{
|
||||
auth::check_is_moderator_from_headers,
|
||||
database::{
|
||||
PgPool, PgTransaction, ReadOnlyPgPool,
|
||||
models::{
|
||||
DBProjectId, DelphiReportIssueDetailsId, DelphiRuleId,
|
||||
delphi_report_item::DelphiSeverity,
|
||||
},
|
||||
},
|
||||
models::pats::Scopes,
|
||||
queue::session::AuthQueue,
|
||||
routes::ApiError,
|
||||
};
|
||||
|
||||
const RULE_SCAN_LOCK_ID: i64 = 0x6465_6c70_6869_7275;
|
||||
const PROGRESS_INTERVAL: usize = 50;
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(get_rule_schema)
|
||||
.service(get_detail_rule_input)
|
||||
.service(scan_rules);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RuleScanEvent<'a> {
|
||||
phase: &'a str,
|
||||
revision: i64,
|
||||
scanned: usize,
|
||||
total: usize,
|
||||
effects: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RuleScanErrorEvent<'a> {
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct RuleInput {
|
||||
pub schema_version: u32,
|
||||
pub trace: RuleTrace,
|
||||
pub scan: RuleScan,
|
||||
pub artifact: RuleArtifact,
|
||||
pub scope: RuleScope,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct RuleTrace {
|
||||
pub key: String,
|
||||
pub issue_type: String,
|
||||
pub severity: DelphiSeverity,
|
||||
pub jar: Option<String>,
|
||||
pub file_path: String,
|
||||
pub data: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct RuleScan {
|
||||
pub delphi_version: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct RuleArtifact {
|
||||
pub size: Option<i32>,
|
||||
pub hashes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct RuleScope {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub file_id: Option<String>,
|
||||
}
|
||||
|
||||
struct CompiledRule {
|
||||
id: DelphiRuleId,
|
||||
program: cel::Program,
|
||||
}
|
||||
|
||||
struct MaterializedEffect {
|
||||
detail_id: i64,
|
||||
rule_id: DelphiRuleId,
|
||||
effect: DelphiRuleEffect,
|
||||
}
|
||||
|
||||
struct ScanSummary {
|
||||
revision: i64,
|
||||
scanned: usize,
|
||||
total: usize,
|
||||
effects: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct DelphiRuleSchemaResponse {
|
||||
pub input: serde_json::Value,
|
||||
pub output: serde_json::Value,
|
||||
pub components: BTreeMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Get the schemas for the CEL input and output values.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = DelphiRuleSchemaResponse))
|
||||
)]
|
||||
#[get("/rules/schema")]
|
||||
pub async fn get_rule_schema(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<web::Json<DelphiRuleSchemaResponse>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut schemas = Vec::new();
|
||||
<RuleInput as ToSchema>::schemas(&mut schemas);
|
||||
<Option<DelphiRuleEffect> as ToSchema>::schemas(&mut schemas);
|
||||
|
||||
Ok(web::Json(DelphiRuleSchemaResponse {
|
||||
input: schema_to_value(<RuleInput as PartialSchema>::schema())?,
|
||||
output: schema_to_value(
|
||||
<Option<DelphiRuleEffect> as PartialSchema>::schema(),
|
||||
)?,
|
||||
components: schemas
|
||||
.into_iter()
|
||||
.map(|(name, schema)| Ok((name, schema_to_value(schema)?)))
|
||||
.collect::<Result<_, ApiError>>()?,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the exact CEL input for a Delphi issue detail.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses(
|
||||
(status = OK, body = RuleInput),
|
||||
(status = NOT_FOUND, description = "Detail not found")
|
||||
)
|
||||
)]
|
||||
#[get("/rules/details/{detail_id}/input")]
|
||||
pub async fn get_detail_rule_input(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
ro_pool: web::Data<ReadOnlyPgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
path: web::Path<(DelphiReportIssueDetailsId,)>,
|
||||
) -> Result<web::Json<RuleInput>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (detail_id,) = path.into_inner();
|
||||
let detail = crate::util::error::Context::wrap_internal_err(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
detail.key,
|
||||
issue.issue_type,
|
||||
detail.severity AS "severity: DelphiSeverity",
|
||||
detail.jar,
|
||||
detail.file_path,
|
||||
detail.data AS "data: Json<HashMap<String, serde_json::Value>>",
|
||||
report.delphi_version,
|
||||
file.size AS "size?",
|
||||
file.id AS "file_id?",
|
||||
version.id AS "version_id?",
|
||||
version.mod_id AS "project_id?",
|
||||
COALESCE(file_hashes.hashes, '{}'::jsonb)
|
||||
AS "hashes!: Json<BTreeMap<String, String>>"
|
||||
FROM delphi_report_issue_details detail
|
||||
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 LATERAL (
|
||||
SELECT
|
||||
jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes
|
||||
FROM hashes
|
||||
WHERE hashes.file_id = file.id
|
||||
) file_hashes ON TRUE
|
||||
WHERE detail.id = $1
|
||||
"#,
|
||||
detail_id as DelphiReportIssueDetailsId,
|
||||
)
|
||||
.fetch_optional(&***ro_pool)
|
||||
.await,
|
||||
"failed to fetch delphi rule input",
|
||||
)?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
Ok(web::Json(RuleInput {
|
||||
schema_version: 1,
|
||||
trace: RuleTrace {
|
||||
key: detail.key,
|
||||
issue_type: detail.issue_type,
|
||||
severity: detail.severity,
|
||||
jar: detail.jar,
|
||||
file_path: detail.file_path,
|
||||
data: detail.data.0,
|
||||
},
|
||||
scan: RuleScan {
|
||||
delphi_version: detail.delphi_version,
|
||||
},
|
||||
artifact: RuleArtifact {
|
||||
size: detail.size,
|
||||
hashes: detail.hashes.0,
|
||||
},
|
||||
scope: RuleScope {
|
||||
project_id: detail.project_id.map(|id| to_base62(id as u64)),
|
||||
version_id: detail.version_id.map(|id| to_base62(id as u64)),
|
||||
file_id: detail.file_id.map(|id| to_base62(id as u64)),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn schema_to_value<T: Serialize>(
|
||||
schema: T,
|
||||
) -> Result<serde_json::Value, ApiError> {
|
||||
serde_json::to_value(schema).map_err(|error| {
|
||||
ApiError::Internal(
|
||||
eyre!(error).wrap_err("failed to serialize Delphi rule schema"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Re-evaluate every Delphi issue detail and atomically publish a new rule revision.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK), (status = CONFLICT))
|
||||
)]
|
||||
#[post("/rules/scan")]
|
||||
pub async fn scan_rules(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = crate::util::error::Context::wrap_internal_err(
|
||||
pool.begin().await,
|
||||
"failed to begin delphi rule scan",
|
||||
)?;
|
||||
|
||||
sqlx::query!("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
ApiError::Internal(
|
||||
eyre!(error)
|
||||
.wrap_err("failed to set delphi rule scan isolation"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let acquired = sqlx::query_scalar!(
|
||||
"SELECT pg_try_advisory_xact_lock($1)",
|
||||
RULE_SCAN_LOCK_ID,
|
||||
)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
ApiError::Internal(
|
||||
eyre!(error).wrap_err("failed to acquire delphi rule scan lock"),
|
||||
)
|
||||
})?
|
||||
.unwrap_or(false);
|
||||
|
||||
if !acquired {
|
||||
return Err(ApiError::Conflict(
|
||||
"a delphi rule scan is already running".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (sender, receiver) = mpsc::unbounded_channel();
|
||||
actix_web::rt::spawn(async move {
|
||||
match run_scan(transaction, &sender).await {
|
||||
Ok(summary) => {
|
||||
let event = RuleScanEvent {
|
||||
phase: "complete",
|
||||
revision: summary.revision,
|
||||
scanned: summary.scanned,
|
||||
total: summary.total,
|
||||
effects: summary.effects,
|
||||
};
|
||||
if let Ok(data) = serde_json::to_string(&event) {
|
||||
let _ = sender.send(Bytes::from(format!(
|
||||
"event: complete\ndata: {data}\n\n"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(error = ?error, "delphi rule scan failed");
|
||||
let message = error.to_string();
|
||||
let event = RuleScanErrorEvent { message: &message };
|
||||
if let Ok(data) = serde_json::to_string(&event) {
|
||||
let _ = sender.send(Bytes::from(format!(
|
||||
"event: failed\ndata: {data}\n\n"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stream =
|
||||
UnboundedReceiverStream::new(receiver).map(Ok::<_, std::io::Error>);
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.insert_header(("Cache-Control", "no-cache"))
|
||||
.insert_header(("X-Accel-Buffering", "no"))
|
||||
.streaming(stream))
|
||||
}
|
||||
|
||||
async fn run_scan(
|
||||
mut transaction: PgTransaction<'static>,
|
||||
sender: &mpsc::UnboundedSender<Bytes>,
|
||||
) -> Result<ScanSummary> {
|
||||
sqlx::query!("LOCK TABLE delphi_rules IN SHARE MODE")
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to lock delphi rules")?;
|
||||
sqlx::query!("LOCK TABLE delphi_report_issue_details IN SHARE MODE")
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to lock delphi issue details")?;
|
||||
|
||||
let current_revision = sqlx::query_scalar!(
|
||||
"SELECT revision FROM delphi_rule_revisions LIMIT 1 FOR UPDATE",
|
||||
)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to fetch the current delphi rule revision")?;
|
||||
let revision = current_revision
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| eyre!("delphi rule revision overflowed"))?;
|
||||
|
||||
let rules = fetch_compiled_rules(&mut transaction).await?;
|
||||
|
||||
let total = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) AS "count!"
|
||||
FROM delphi_report_issue_details
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to count delphi issue details")? as usize;
|
||||
|
||||
let mut details = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
detail.id,
|
||||
detail.key,
|
||||
issue.issue_type,
|
||||
detail.severity AS "severity: DelphiSeverity",
|
||||
detail.jar,
|
||||
detail.file_path,
|
||||
detail.data AS "data: Json<HashMap<String, serde_json::Value>>",
|
||||
report.delphi_version,
|
||||
file.size AS "size?",
|
||||
file.id AS "file_id?",
|
||||
version.id AS "version_id?",
|
||||
version.mod_id AS "project_id?",
|
||||
COALESCE(file_hashes.hashes, '{}'::jsonb)
|
||||
AS "hashes!: Json<BTreeMap<String, String>>"
|
||||
FROM delphi_report_issue_details detail
|
||||
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 (
|
||||
SELECT
|
||||
file_id,
|
||||
jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes
|
||||
FROM hashes
|
||||
GROUP BY file_id
|
||||
) file_hashes ON file_hashes.file_id = file.id
|
||||
ORDER BY detail.id
|
||||
"#,
|
||||
)
|
||||
.fetch(&mut transaction);
|
||||
|
||||
let mut effects = Vec::new();
|
||||
let mut scanned = 0;
|
||||
let event = RuleScanEvent {
|
||||
phase: "scanning",
|
||||
revision,
|
||||
scanned: 0,
|
||||
total,
|
||||
effects: 0,
|
||||
};
|
||||
if let Ok(data) = serde_json::to_string(&event) {
|
||||
let _ = sender
|
||||
.send(Bytes::from(format!("event: progress\ndata: {data}\n\n")));
|
||||
}
|
||||
|
||||
while let Some(detail) = details
|
||||
.try_next()
|
||||
.await
|
||||
.wrap_err("failed to fetch a delphi issue detail")?
|
||||
{
|
||||
let detail_id = detail.id;
|
||||
let input = RuleInput {
|
||||
schema_version: 1,
|
||||
trace: RuleTrace {
|
||||
key: detail.key,
|
||||
issue_type: detail.issue_type,
|
||||
severity: detail.severity,
|
||||
jar: detail.jar,
|
||||
file_path: detail.file_path,
|
||||
data: detail.data.0,
|
||||
},
|
||||
scan: RuleScan {
|
||||
delphi_version: detail.delphi_version,
|
||||
},
|
||||
artifact: RuleArtifact {
|
||||
size: detail.size,
|
||||
hashes: detail.hashes.0,
|
||||
},
|
||||
scope: RuleScope {
|
||||
project_id: detail.project_id.map(|id| to_base62(id as u64)),
|
||||
version_id: detail.version_id.map(|id| to_base62(id as u64)),
|
||||
file_id: detail.file_id.map(|id| to_base62(id as u64)),
|
||||
},
|
||||
};
|
||||
|
||||
for rule in &rules {
|
||||
let effect = evaluate_rule(&rule.program, &input)
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"failed to evaluate delphi rule {} for detail {detail_id}",
|
||||
rule.id.0
|
||||
)
|
||||
})?;
|
||||
if let Some(effect) = effect {
|
||||
effects.push(MaterializedEffect {
|
||||
detail_id,
|
||||
rule_id: rule.id,
|
||||
effect,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
scanned += 1;
|
||||
if scanned % PROGRESS_INTERVAL == 0 || scanned == total {
|
||||
let event = RuleScanEvent {
|
||||
phase: "scanning",
|
||||
revision,
|
||||
scanned,
|
||||
total,
|
||||
effects: effects.len(),
|
||||
};
|
||||
if let Ok(data) = serde_json::to_string(&event) {
|
||||
let _ = sender.send(Bytes::from(format!(
|
||||
"event: progress\ndata: {data}\n\n"
|
||||
)));
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
drop(details);
|
||||
|
||||
let event = RuleScanEvent {
|
||||
phase: "publishing",
|
||||
revision,
|
||||
scanned: total,
|
||||
total,
|
||||
effects: effects.len(),
|
||||
};
|
||||
if let Ok(data) = serde_json::to_string(&event) {
|
||||
let _ = sender
|
||||
.send(Bytes::from(format!("event: progress\ndata: {data}\n\n")));
|
||||
}
|
||||
|
||||
insert_materialized_effects(revision, &effects, &mut transaction).await?;
|
||||
|
||||
let affected_projects = sqlx::query!(
|
||||
r#"
|
||||
WITH project_membership AS (
|
||||
SELECT
|
||||
detail.project_id,
|
||||
BOOL_OR(
|
||||
detail.status IN ('pending', 'unsafe')
|
||||
AND NOT detail.hidden
|
||||
) AS old_needs_review,
|
||||
BOOL_OR(
|
||||
detail.status IN ('pending', 'unsafe')
|
||||
AND NOT COALESCE(new_effect.hidden, FALSE)
|
||||
) AS new_needs_review
|
||||
FROM delphi_issue_details_with_statuses detail
|
||||
LEFT JOIN delphi_rule_effects new_effect
|
||||
ON new_effect.revision = $1
|
||||
AND new_effect.detail_id = detail.id
|
||||
GROUP BY detail.project_id
|
||||
)
|
||||
SELECT
|
||||
project_id AS "project_id!: DBProjectId",
|
||||
new_needs_review AS "new_needs_review!"
|
||||
FROM project_membership
|
||||
WHERE old_needs_review IS DISTINCT FROM new_needs_review
|
||||
"#,
|
||||
revision,
|
||||
)
|
||||
.fetch_all(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to fetch projects affected by delphi rule changes")?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE delphi_rules SET revision = $1 WHERE NOT delete_on_next_revision",
|
||||
revision,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to update delphi rule revisions")?;
|
||||
sqlx::query!("UPDATE delphi_rule_revisions SET revision = $1", revision)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to publish the delphi rule revision")?;
|
||||
|
||||
tech_review_queue::add_projects(
|
||||
&affected_projects
|
||||
.iter()
|
||||
.filter(|project| project.new_needs_review)
|
||||
.map(|project| project.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
tech_review_queue::remove_projects(
|
||||
&affected_projects
|
||||
.iter()
|
||||
.filter(|project| !project.new_needs_review)
|
||||
.map(|project| project.project_id)
|
||||
.collect::<Vec<_>>(),
|
||||
TechReviewRemovalReason::RulesChanged,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM delphi_rule_effects WHERE revision <> $1",
|
||||
revision,
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to delete old delphi rule effects")?;
|
||||
sqlx::query!("DELETE FROM delphi_rules WHERE delete_on_next_revision")
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
.wrap_err("failed to delete retired delphi rules")?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_err("failed to commit the delphi rule scan")?;
|
||||
|
||||
Ok(ScanSummary {
|
||||
revision,
|
||||
scanned: total,
|
||||
total,
|
||||
effects: effects.len(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn materialize_current_rule_effects(
|
||||
detail_ids: &[DelphiReportIssueDetailsId],
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<()> {
|
||||
if detail_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let revision = sqlx::query_scalar!(
|
||||
"SELECT revision FROM delphi_rule_revisions LIMIT 1",
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await
|
||||
.wrap_err("failed to fetch the current delphi rule revision")?;
|
||||
let rules = fetch_compiled_rules(transaction).await?;
|
||||
|
||||
if rules.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let details = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
detail.id,
|
||||
detail.key,
|
||||
issue.issue_type,
|
||||
detail.severity AS "severity: DelphiSeverity",
|
||||
detail.jar,
|
||||
detail.file_path,
|
||||
detail.data AS "data: Json<HashMap<String, serde_json::Value>>",
|
||||
report.delphi_version,
|
||||
file.size AS "size?",
|
||||
file.id AS "file_id?",
|
||||
version.id AS "version_id?",
|
||||
version.mod_id AS "project_id?",
|
||||
COALESCE(file_hashes.hashes, '{}'::jsonb)
|
||||
AS "hashes!: Json<BTreeMap<String, String>>"
|
||||
FROM delphi_report_issue_details detail
|
||||
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 LATERAL (
|
||||
SELECT
|
||||
jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes
|
||||
FROM hashes
|
||||
WHERE hashes.file_id = file.id
|
||||
) file_hashes ON TRUE
|
||||
WHERE detail.id = ANY($1::bigint[])
|
||||
ORDER BY detail.id
|
||||
"#,
|
||||
&detail_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await
|
||||
.wrap_err("failed to fetch new delphi issue details")?;
|
||||
|
||||
let mut effects = Vec::new();
|
||||
for detail in details {
|
||||
let input = RuleInput {
|
||||
schema_version: 1,
|
||||
trace: RuleTrace {
|
||||
key: detail.key,
|
||||
issue_type: detail.issue_type,
|
||||
severity: detail.severity,
|
||||
jar: detail.jar,
|
||||
file_path: detail.file_path,
|
||||
data: detail.data.0,
|
||||
},
|
||||
scan: RuleScan {
|
||||
delphi_version: detail.delphi_version,
|
||||
},
|
||||
artifact: RuleArtifact {
|
||||
size: detail.size,
|
||||
hashes: detail.hashes.0,
|
||||
},
|
||||
scope: RuleScope {
|
||||
project_id: detail.project_id.map(|id| to_base62(id as u64)),
|
||||
version_id: detail.version_id.map(|id| to_base62(id as u64)),
|
||||
file_id: detail.file_id.map(|id| to_base62(id as u64)),
|
||||
},
|
||||
};
|
||||
|
||||
for rule in &rules {
|
||||
let effect =
|
||||
evaluate_rule(&rule.program, &input).wrap_err_with(|| {
|
||||
format!(
|
||||
"failed to evaluate delphi rule {} for detail {}",
|
||||
rule.id.0, detail.id
|
||||
)
|
||||
})?;
|
||||
if let Some(effect) = effect {
|
||||
effects.push(MaterializedEffect {
|
||||
detail_id: detail.id,
|
||||
rule_id: rule.id,
|
||||
effect,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insert_materialized_effects(revision, &effects, transaction).await
|
||||
}
|
||||
|
||||
async fn fetch_compiled_rules(
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<Vec<CompiledRule>> {
|
||||
let rules = sqlx::query!(
|
||||
r#"
|
||||
SELECT id AS "id!: DelphiRuleId", rule
|
||||
FROM delphi_rules
|
||||
WHERE NOT delete_on_next_revision
|
||||
ORDER BY priority DESC, id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await
|
||||
.wrap_err("failed to fetch delphi rules")?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
rules
|
||||
.into_iter()
|
||||
.map(|rule| {
|
||||
let program =
|
||||
cel::Program::compile(&rule.rule).map_err(|error| {
|
||||
eyre!(
|
||||
"failed to compile delphi rule {}: {error}",
|
||||
rule.id.0
|
||||
)
|
||||
})?;
|
||||
Ok(CompiledRule {
|
||||
id: rule.id,
|
||||
program,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to join cel compilation task")?
|
||||
}
|
||||
|
||||
async fn insert_materialized_effects(
|
||||
revision: i64,
|
||||
effects: &[MaterializedEffect],
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<()> {
|
||||
if effects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let detail_ids = effects
|
||||
.iter()
|
||||
.map(|effect| effect.detail_id)
|
||||
.collect::<Vec<_>>();
|
||||
let rule_ids = effects
|
||||
.iter()
|
||||
.map(|effect| effect.rule_id.0)
|
||||
.collect::<Vec<_>>();
|
||||
let severities = effects
|
||||
.iter()
|
||||
.map(|effect| effect.effect.severity)
|
||||
.collect::<Vec<_>>();
|
||||
let hidden = effects
|
||||
.iter()
|
||||
.map(|effect| effect.effect.hidden)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO delphi_rule_effects (
|
||||
revision,
|
||||
detail_id,
|
||||
rule_id,
|
||||
severity,
|
||||
hidden
|
||||
)
|
||||
SELECT $1, effect.*
|
||||
FROM UNNEST(
|
||||
$2::BIGINT[],
|
||||
$3::BIGINT[],
|
||||
$4::delphi_severity[],
|
||||
$5::BOOLEAN[]
|
||||
) AS effect(detail_id, rule_id, severity, hidden)
|
||||
"#,
|
||||
revision,
|
||||
&detail_ids,
|
||||
&rule_ids,
|
||||
&severities as &[Option<DelphiSeverity>],
|
||||
&hidden,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.wrap_err("failed to insert delphi rule effects")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn evaluate_rule(
|
||||
program: &cel::Program,
|
||||
input: impl Serialize,
|
||||
) -> Result<Option<DelphiRuleEffect>> {
|
||||
let mut context = cel::Context::default();
|
||||
context
|
||||
.add_variable("input", input)
|
||||
.wrap_err("failed to build cel input")?;
|
||||
|
||||
let value = program
|
||||
.execute(&context)
|
||||
.wrap_err("failed to execute cel expression")?;
|
||||
let value = value.json().map_err(|error| {
|
||||
eyre!("failed to convert cel result to json: {error}")
|
||||
})?;
|
||||
|
||||
match value {
|
||||
serde_json::Value::Null => Ok(None),
|
||||
value => serde_json::from_value(value)
|
||||
.map(Some)
|
||||
.wrap_err("cel expression returned an invalid rule effect"),
|
||||
}
|
||||
}
|
||||
@@ -2781,8 +2781,9 @@ pub async fn project_delete_internal(
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
delphi::tech_review_sync::sync_deleted_project_tech_review_exit(
|
||||
project.inner.id,
|
||||
delphi::tech_review_queue::remove_projects(
|
||||
&[project.inner.id],
|
||||
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
|
||||
&mut transaction,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -934,9 +934,9 @@ pub async fn delete_file(
|
||||
database::models::version_item::cleanup_unused_attribution_files_and_groups(&mut transaction)
|
||||
.await?;
|
||||
|
||||
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?;
|
||||
|
||||
@@ -1231,9 +1231,9 @@ pub async fn version_delete(
|
||||
)
|
||||
.await?;
|
||||
|
||||
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?;
|
||||
|
||||
@@ -6,6 +6,114 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
|
||||
return 'labrinth_tech_review_internal'
|
||||
}
|
||||
|
||||
public async getRules(): Promise<Labrinth.TechReview.Internal.DelphiRule[]> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.DelphiRule[]>(
|
||||
'/moderation/tech-review/rules',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getRuleSchema(): Promise<Labrinth.TechReview.Internal.DelphiRuleSchemaResponse> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.DelphiRuleSchemaResponse>(
|
||||
'/moderation/tech-review/rules/schema',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getDetailRuleInput(
|
||||
detailId: string,
|
||||
): Promise<Labrinth.TechReview.Internal.RuleInput> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.RuleInput>(
|
||||
`/moderation/tech-review/rules/details/${detailId}/input`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getRuleAffectedDetails(
|
||||
id: number,
|
||||
): Promise<Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]>(
|
||||
`/moderation/tech-review/rules/${id}/effects`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async testRule(
|
||||
request: Labrinth.TechReview.Internal.TestDelphiRuleRequest,
|
||||
): Promise<Labrinth.TechReview.Internal.TestDelphiRuleResponse> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.TestDelphiRuleResponse>(
|
||||
'/moderation/tech-review/rules/test',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async createRule(
|
||||
rule: Labrinth.TechReview.Internal.WriteDelphiRule,
|
||||
): Promise<Labrinth.TechReview.Internal.DelphiRule> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.DelphiRule>(
|
||||
'/moderation/tech-review/rules',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: rule,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async updateRule(
|
||||
id: number,
|
||||
rule: Labrinth.TechReview.Internal.WriteDelphiRule,
|
||||
): Promise<Labrinth.TechReview.Internal.DelphiRule> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.DelphiRule>(
|
||||
`/moderation/tech-review/rules/${id}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PUT',
|
||||
body: rule,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async deleteRule(id: number): Promise<void> {
|
||||
return this.client.request<void>(`/moderation/tech-review/rules/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
public async scanRules(signal?: AbortSignal): Promise<ReadableStream<Uint8Array>> {
|
||||
return this.client.stream('/moderation/tech-review/rules/scan', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for projects awaiting technical review.
|
||||
*
|
||||
|
||||
@@ -2264,6 +2264,104 @@ export namespace Labrinth {
|
||||
|
||||
export namespace TechReview {
|
||||
export namespace Internal {
|
||||
export type DelphiRule = {
|
||||
id: number
|
||||
name: string
|
||||
rule: string
|
||||
priority: number
|
||||
revision: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: number | null
|
||||
updated_by: number | null
|
||||
affected_details_count: number
|
||||
affected_details: DelphiRuleAffectedDetail[]
|
||||
}
|
||||
|
||||
export type DelphiRuleAffectedDetail = {
|
||||
detail_id: string
|
||||
issue_id: string
|
||||
project_id: string | null
|
||||
project_name: string | null
|
||||
project_icon_url: string | null
|
||||
version_id: string | null
|
||||
version_name: string | null
|
||||
version_number: string | null
|
||||
issue_type: string
|
||||
key: string
|
||||
jar: string | null
|
||||
file_path: string
|
||||
original_severity: DelphiSeverity
|
||||
severity: DelphiSeverity | null
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export type WriteDelphiRule = {
|
||||
name: string
|
||||
rule: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
export type TestDelphiRuleRequest = {
|
||||
rule: string
|
||||
inputs: RuleInput[]
|
||||
}
|
||||
|
||||
export type DelphiRuleEffect = {
|
||||
severity: DelphiSeverity | null
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export type DelphiRuleSchema = Record<string, unknown>
|
||||
|
||||
export type DelphiRuleSchemaResponse = {
|
||||
input: DelphiRuleSchema
|
||||
output: DelphiRuleSchema
|
||||
components: Record<string, DelphiRuleSchema>
|
||||
}
|
||||
|
||||
export type RuleInput = {
|
||||
schema_version: number
|
||||
trace: {
|
||||
key: string
|
||||
issue_type: string
|
||||
severity: DelphiSeverity
|
||||
jar: string | null
|
||||
file_path: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
scan: {
|
||||
delphi_version: number
|
||||
}
|
||||
artifact: {
|
||||
size: number | null
|
||||
hashes: Record<string, string>
|
||||
}
|
||||
scope: {
|
||||
project_id: string | null
|
||||
version_id: string | null
|
||||
file_id: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type TestDelphiRuleResponse = {
|
||||
effects: Array<DelphiRuleEffect | null>
|
||||
}
|
||||
|
||||
export type DelphiRuleScanPhase = 'scanning' | 'publishing' | 'complete'
|
||||
|
||||
export type DelphiRuleScanEvent = {
|
||||
phase: DelphiRuleScanPhase
|
||||
revision: number
|
||||
scanned: number
|
||||
total: number
|
||||
effects: number
|
||||
}
|
||||
|
||||
export type DelphiRuleScanErrorEvent = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export type SearchProjectsRequest = {
|
||||
limit?: number
|
||||
page?: number
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "delphi-cel-experiment"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
cel = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user