Compare commits

...
Author SHA1 Message Date
aecsocket cc77fd1b87 Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-30 05:15:25 +01:00
aecsocket b92fc10e54 cleanup 2026-07-27 17:47:17 +01:00
aecsocket 849943ed4e clean up db stuff 2026-07-27 10:41:26 +01:00
aecsocket 18c95e2f61 prepr 2026-07-27 10:19:54 +01:00
aecsocket 7c7efd5efe Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-27 10:05:50 +01:00
aecsocket 4c5e1074fd fix 2026-07-26 15:36:41 +01:00
aecsocket 36d3aab0de fmt 2026-07-26 15:26:02 +01:00
aecsocket 88c0915066 Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-26 15:20:26 +01:00
aecsocket f0dff8ebd5 clean up __dummy stuff 2026-07-26 15:17:50 +01:00
aecsocket 8d82c228cf rules affect tech review detail statuses 2026-07-24 13:49:20 +01:00
aecsocket 4b07ed720e remove old ui 2026-07-23 09:12:37 +01:00
aecsocket ea42c644f3 show in/out schema 2026-07-22 20:14:57 +01:00
aecsocket 28154689f4 run rule scans 2026-07-22 19:34:18 +01:00
aecsocket 3c77cc1502 use Ace editor 2026-07-22 18:21:33 +01:00
aecsocket b6b19e543c add demo delphi rules ui 2026-07-22 17:43:03 +01:00
aecsocket 023c789a31 wip: delphi trace transform rules 2026-07-19 09:46:12 +01:00
101 changed files with 5153 additions and 1368 deletions
Generated
+88 -3
View File
@@ -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"
+2
View File
@@ -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"
},
+23 -2
View File
@@ -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>
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM delphi_rules WHERE delete_on_next_revision",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
File diff suppressed because one or more lines are too long
@@ -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"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
File diff suppressed because one or more lines are too long
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DROP SCHEMA public CASCADE;",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7"
}
File diff suppressed because one or more lines are too long
@@ -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"
}
File diff suppressed because one or more lines are too long
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+1
View File
@@ -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';
+1
View File
@@ -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()
}
+9
View File
@@ -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"),
}
}
+3 -2
View File
@@ -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?;
+2 -2
View File
@@ -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?;
+2 -2
View File
@@ -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
+10
View File
@@ -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