mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
show in/out schema
This commit is contained in:
@@ -37,6 +37,37 @@
|
|||||||
<code>severity</code> and/or <code>hidden</code> when it does.
|
<code>severity</code> and/or <code>hidden</code> when it does.
|
||||||
</p>
|
</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">
|
<section class="mt-2 flex flex-col gap-3">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -233,7 +264,11 @@
|
|||||||
description="Create a rule to transform matching issue traces."
|
description="Create a rule to transform matching issue traces."
|
||||||
/>
|
/>
|
||||||
<div v-else class="flex flex-col gap-3">
|
<div v-else class="flex flex-col gap-3">
|
||||||
<article v-for="rule in rules" :key="rule.id" class="universal-card 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 class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-bold text-contrast">{{ rule.name }}</h2>
|
<h2 class="m-0 text-lg font-bold text-contrast">{{ rule.name }}</h2>
|
||||||
@@ -257,6 +292,112 @@
|
|||||||
<pre
|
<pre
|
||||||
class="m-0 overflow-x-auto rounded-lg bg-bg-raised p-3 text-sm"
|
class="m-0 overflow-x-auto rounded-lg bg-bg-raised p-3 text-sm"
|
||||||
><code>{{ rule.rule }}</code></pre>
|
><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"
|
||||||
|
>
|
||||||
|
<template v-if="detail.jar">
|
||||||
|
<span class="truncate">{{ detail.jar }}</span>
|
||||||
|
<ChevronRightIcon class="size-3.5 shrink-0" aria-hidden="true" />
|
||||||
|
</template>
|
||||||
|
<span class="truncate">{{ detail.file_path }}</span>
|
||||||
|
</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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,8 +407,10 @@
|
|||||||
import { type Labrinth, SseParser } from '@modrinth/api-client'
|
import { type Labrinth, SseParser } from '@modrinth/api-client'
|
||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
|
ExternalIcon,
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -275,6 +418,7 @@ import {
|
|||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
|
Avatar,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
@@ -335,13 +479,20 @@ const isLoading = ref(true)
|
|||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const isScanning = ref(false)
|
const isScanning = ref(false)
|
||||||
const isTestingRule = ref(false)
|
const isTestingRule = ref(false)
|
||||||
|
const isLoadingRuleSchema = ref(false)
|
||||||
const isRuleModalOpen = ref(false)
|
const isRuleModalOpen = ref(false)
|
||||||
const loadFailed = 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 editingRuleId = ref<number | null>(null)
|
||||||
const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null)
|
const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null)
|
||||||
const ruleTestEffects = ref<Array<Labrinth.TechReview.Internal.DelphiRuleEffect | null>>([])
|
const ruleTestEffects = ref<Array<Labrinth.TechReview.Internal.DelphiRuleEffect | null>>([])
|
||||||
const ruleTestError = ref<string | null>(null)
|
const ruleTestError = ref<string | null>(null)
|
||||||
const scanProgress = ref<Labrinth.TechReview.Internal.DelphiRuleScanEvent | 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({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
rule: DEFAULT_RULE,
|
rule: DEFAULT_RULE,
|
||||||
@@ -358,6 +509,12 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const modalTitle = computed(() => (editingRuleId.value === null ? 'Create rule' : 'Edit rule'))
|
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(() =>
|
const previewExamples = computed(() =>
|
||||||
TEST_TRACES.map((original, index) => {
|
TEST_TRACES.map((original, index) => {
|
||||||
const effect = ruleTestEffects.value[index] ?? null
|
const effect = ruleTestEffects.value[index] ?? null
|
||||||
@@ -399,6 +556,92 @@ function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function onRuleEditorInit(editor: Ace.Editor) {
|
||||||
ruleEditorInstance.value = editor
|
ruleEditorInstance.value = editor
|
||||||
editor.session.setUseWrapMode(true)
|
editor.session.setUseWrapMode(true)
|
||||||
@@ -460,6 +703,7 @@ async function loadRules() {
|
|||||||
loadFailed.value = false
|
loadFailed.value = false
|
||||||
try {
|
try {
|
||||||
rules.value = await client.labrinth.tech_review_internal.getRules()
|
rules.value = await client.labrinth.tech_review_internal.getRules()
|
||||||
|
expandedAffectedDetails.clear()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load Delphi rules', error)
|
console.error('Failed to load Delphi rules', error)
|
||||||
loadFailed.value = true
|
loadFailed.value = true
|
||||||
@@ -468,6 +712,64 @@ async function loadRules() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function openCreateModal() {
|
||||||
if (isScanning.value) return
|
if (isScanning.value) return
|
||||||
editingRuleId.value = null
|
editingRuleId.value = null
|
||||||
@@ -476,6 +778,7 @@ function openCreateModal() {
|
|||||||
isRuleModalOpen.value = true
|
isRuleModalOpen.value = true
|
||||||
ruleModal.value?.show()
|
ruleModal.value?.show()
|
||||||
nextTick(() => ruleEditorInstance.value?.resize(true))
|
nextTick(() => ruleEditorInstance.value?.resize(true))
|
||||||
|
void loadRuleSchema()
|
||||||
void testRule()
|
void testRule()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +790,7 @@ function openEditModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
|||||||
isRuleModalOpen.value = true
|
isRuleModalOpen.value = true
|
||||||
ruleModal.value?.show()
|
ruleModal.value?.show()
|
||||||
nextTick(() => ruleEditorInstance.value?.resize(true))
|
nextTick(() => ruleEditorInstance.value?.resize(true))
|
||||||
|
void loadRuleSchema()
|
||||||
void testRule()
|
void testRule()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,9 +117,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
moderation::tech_review::global::get_global_issue_detail,
|
moderation::tech_review::global::get_global_issue_detail,
|
||||||
moderation::tech_review::rules::get_rules,
|
moderation::tech_review::rules::get_rules,
|
||||||
moderation::tech_review::rules::test_rule,
|
moderation::tech_review::rules::test_rule,
|
||||||
|
moderation::tech_review::rules::get_rule_affected_details,
|
||||||
moderation::tech_review::rules::create_rule,
|
moderation::tech_review::rules::create_rule,
|
||||||
moderation::tech_review::rules::update_rule,
|
moderation::tech_review::rules::update_rule,
|
||||||
moderation::tech_review::rules::delete_rule,
|
moderation::tech_review::rules::delete_rule,
|
||||||
|
moderation::tech_review::rules_scan::get_rule_schema,
|
||||||
moderation::tech_review::rules_scan::scan_rules,
|
moderation::tech_review::rules_scan::scan_rules,
|
||||||
moderation::tech_review::get_project_report,
|
moderation::tech_review::get_project_report,
|
||||||
moderation::tech_review::submit_report,
|
moderation::tech_review::submit_report,
|
||||||
|
|||||||
@@ -5,13 +5,23 @@ use chrono::{DateTime, Utc};
|
|||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::rules_scan::{
|
||||||
|
RuleArtifact, RuleInput, RuleScan, RuleScope, RuleTrace,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::check_is_moderator_from_headers,
|
auth::check_is_moderator_from_headers,
|
||||||
database::{
|
database::{
|
||||||
PgPool, ReadOnlyPgPool, models::delphi_report_item::DelphiSeverity,
|
PgPool, ReadOnlyPgPool,
|
||||||
|
models::{
|
||||||
|
DBProjectId, DBVersionId, DelphiReportIssueDetailsId,
|
||||||
|
DelphiReportIssueId, delphi_report_item::DelphiSeverity,
|
||||||
|
},
|
||||||
redis::RedisPool,
|
redis::RedisPool,
|
||||||
},
|
},
|
||||||
models::pats::Scopes,
|
models::{
|
||||||
|
ids::{ProjectId, VersionId},
|
||||||
|
pats::Scopes,
|
||||||
|
},
|
||||||
queue::session::AuthQueue,
|
queue::session::AuthQueue,
|
||||||
routes::ApiError,
|
routes::ApiError,
|
||||||
util::error::Context,
|
util::error::Context,
|
||||||
@@ -24,6 +34,7 @@ const MAX_RULE_TEST_TRACES: usize = 10;
|
|||||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
cfg.service(get_rules)
|
cfg.service(get_rules)
|
||||||
.service(test_rule)
|
.service(test_rule)
|
||||||
|
.service(get_rule_affected_details)
|
||||||
.service(create_rule)
|
.service(create_rule)
|
||||||
.service(update_rule)
|
.service(update_rule)
|
||||||
.service(delete_rule);
|
.service(delete_rule);
|
||||||
@@ -39,6 +50,27 @@ pub struct DelphiRule {
|
|||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub created_by: Option<i64>,
|
pub created_by: Option<i64>,
|
||||||
pub updated_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, utoipa::ToSchema)]
|
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||||
@@ -77,33 +109,6 @@ pub struct DelphiRuleEffect {
|
|||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct TestRuleInput<'a> {
|
|
||||||
schema_version: u32,
|
|
||||||
trace: &'a TestDelphiRuleTrace,
|
|
||||||
scan: TestRuleScan,
|
|
||||||
artifact: TestRuleArtifact,
|
|
||||||
scope: TestRuleScope,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct TestRuleScan {
|
|
||||||
delphi_version: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct TestRuleArtifact {
|
|
||||||
size: u32,
|
|
||||||
hashes: BTreeMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct TestRuleScope {
|
|
||||||
project_id: String,
|
|
||||||
version_id: String,
|
|
||||||
file_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ValidatedRule {
|
struct ValidatedRule {
|
||||||
name: String,
|
name: String,
|
||||||
rule: String,
|
rule: String,
|
||||||
@@ -203,22 +208,29 @@ pub async fn test_rule(
|
|||||||
Ok(web::Json(TestDelphiRuleResponse { effects }))
|
Ok(web::Json(TestDelphiRuleResponse { effects }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_rule_input(trace: &TestDelphiRuleTrace) -> TestRuleInput<'_> {
|
fn test_rule_input(trace: &TestDelphiRuleTrace) -> RuleInput {
|
||||||
TestRuleInput {
|
RuleInput {
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
trace,
|
trace: RuleTrace {
|
||||||
scan: TestRuleScan { delphi_version: 17 },
|
key: trace.key.clone(),
|
||||||
artifact: TestRuleArtifact {
|
issue_type: trace.issue_type.clone(),
|
||||||
size: 412_892,
|
severity: trace.severity,
|
||||||
|
jar: trace.jar.clone(),
|
||||||
|
file_path: trace.file_path.clone(),
|
||||||
|
data: trace.data.clone(),
|
||||||
|
},
|
||||||
|
scan: RuleScan { delphi_version: 17 },
|
||||||
|
artifact: RuleArtifact {
|
||||||
|
size: Some(412_892),
|
||||||
hashes: BTreeMap::from([
|
hashes: BTreeMap::from([
|
||||||
("sha1".to_string(), "0123456789abcdef".to_string()),
|
("sha1".to_string(), "0123456789abcdef".to_string()),
|
||||||
("sha512".to_string(), "fedcba9876543210".to_string()),
|
("sha512".to_string(), "fedcba9876543210".to_string()),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
scope: TestRuleScope {
|
scope: RuleScope {
|
||||||
project_id: "example-project".to_string(),
|
project_id: Some("example-project".to_string()),
|
||||||
version_id: "example-version".to_string(),
|
version_id: Some("example-version".to_string()),
|
||||||
file_id: "example-file".to_string(),
|
file_id: Some("example-file".to_string()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,27 +262,81 @@ pub async fn get_rules(
|
|||||||
let rules = sqlx::query!(
|
let rules = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
delphi_rule.id,
|
||||||
name,
|
delphi_rule.name,
|
||||||
rule,
|
delphi_rule.rule,
|
||||||
revision,
|
delphi_rule.revision,
|
||||||
created_at,
|
delphi_rule.created_at,
|
||||||
updated_at,
|
delphi_rule.updated_at,
|
||||||
created_by,
|
delphi_rule.created_by,
|
||||||
updated_by
|
delphi_rule.updated_by,
|
||||||
FROM delphi_rules
|
COALESCE(preview.affected_details_count, 0)
|
||||||
WHERE NOT delete_on_next_revision
|
AS "affected_details_count!",
|
||||||
ORDER BY id
|
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.id, preview.detail_id DESC
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.fetch_all(&***ro_pool)
|
.fetch_all(&***ro_pool)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("failed to fetch delphi rules")?;
|
.wrap_internal_err("failed to fetch delphi rules")?;
|
||||||
|
|
||||||
Ok(web::Json(
|
let mut response = Vec::<DelphiRule>::new();
|
||||||
rules
|
for rule in rules {
|
||||||
.into_iter()
|
if response
|
||||||
.map(|rule| DelphiRule {
|
.last()
|
||||||
|
.is_none_or(|existing| existing.id != rule.id)
|
||||||
|
{
|
||||||
|
response.push(DelphiRule {
|
||||||
id: rule.id,
|
id: rule.id,
|
||||||
name: rule.name,
|
name: rule.name,
|
||||||
rule: rule.rule,
|
rule: rule.rule,
|
||||||
@@ -279,6 +345,137 @@ pub async fn get_rules(
|
|||||||
updated_at: rule.updated_at,
|
updated_at: rule.updated_at,
|
||||||
created_by: rule.created_by,
|
created_by: rule.created_by,
|
||||||
updated_by: rule.updated_by,
|
updated_by: rule.updated_by,
|
||||||
|
affected_details_count: rule.affected_details_count,
|
||||||
|
affected_details: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
response
|
||||||
|
.last_mut()
|
||||||
|
.expect("a delphi rule was inserted above")
|
||||||
|
.affected_details
|
||||||
|
.push(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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<(i64,)>,
|
||||||
|
) -> 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,
|
||||||
|
)
|
||||||
|
.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(),
|
.collect(),
|
||||||
))
|
))
|
||||||
@@ -354,6 +551,8 @@ pub async fn create_rule(
|
|||||||
updated_at: rule.updated_at,
|
updated_at: rule.updated_at,
|
||||||
created_by: rule.created_by,
|
created_by: rule.created_by,
|
||||||
updated_by: rule.updated_by,
|
updated_by: rule.updated_by,
|
||||||
|
affected_details_count: 0,
|
||||||
|
affected_details: Vec::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +626,8 @@ pub async fn update_rule(
|
|||||||
updated_at: rule.updated_at,
|
updated_at: rule.updated_at,
|
||||||
created_by: rule.created_by,
|
created_by: rule.created_by,
|
||||||
updated_by: rule.updated_by,
|
updated_by: rule.updated_by,
|
||||||
|
affected_details_count: 0,
|
||||||
|
affected_details: Vec::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
use actix_web::{HttpRequest, HttpResponse, post, web};
|
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||||
use ariadne::ids::base62_impl::to_base62;
|
use ariadne::ids::base62_impl::to_base62;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use eyre::{Context as _, Result, eyre};
|
use eyre::{Context as _, Result, eyre};
|
||||||
@@ -9,6 +9,7 @@ use serde::Serialize;
|
|||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
use utoipa::{PartialSchema, ToSchema};
|
||||||
|
|
||||||
use super::rules::DelphiRuleEffect;
|
use super::rules::DelphiRuleEffect;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -26,7 +27,7 @@ const RULE_SCAN_LOCK_ID: i64 = 0x6465_6c70_6869_7275;
|
|||||||
const PROGRESS_INTERVAL: usize = 50;
|
const PROGRESS_INTERVAL: usize = 50;
|
||||||
|
|
||||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
cfg.service(scan_rules);
|
cfg.service(get_rule_schema).service(scan_rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -43,41 +44,41 @@ struct RuleScanErrorEvent<'a> {
|
|||||||
message: &'a str,
|
message: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
struct RuleInput {
|
pub(super) struct RuleInput {
|
||||||
schema_version: u32,
|
pub(super) schema_version: u32,
|
||||||
trace: RuleTrace,
|
pub(super) trace: RuleTrace,
|
||||||
scan: RuleScan,
|
pub(super) scan: RuleScan,
|
||||||
artifact: RuleArtifact,
|
pub(super) artifact: RuleArtifact,
|
||||||
scope: RuleScope,
|
pub(super) scope: RuleScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
struct RuleTrace {
|
pub(super) struct RuleTrace {
|
||||||
key: String,
|
pub(super) key: String,
|
||||||
issue_type: String,
|
pub(super) issue_type: String,
|
||||||
severity: DelphiSeverity,
|
pub(super) severity: DelphiSeverity,
|
||||||
jar: Option<String>,
|
pub(super) jar: Option<String>,
|
||||||
file_path: String,
|
pub(super) file_path: String,
|
||||||
data: HashMap<String, serde_json::Value>,
|
pub(super) data: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
struct RuleScan {
|
pub(super) struct RuleScan {
|
||||||
delphi_version: i32,
|
pub(super) delphi_version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
struct RuleArtifact {
|
pub(super) struct RuleArtifact {
|
||||||
size: Option<i32>,
|
pub(super) size: Option<i32>,
|
||||||
hashes: BTreeMap<String, String>,
|
pub(super) hashes: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
struct RuleScope {
|
pub(super) struct RuleScope {
|
||||||
project_id: Option<String>,
|
pub(super) project_id: Option<String>,
|
||||||
version_id: Option<String>,
|
pub(super) version_id: Option<String>,
|
||||||
file_id: Option<String>,
|
pub(super) file_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CompiledRule {
|
struct CompiledRule {
|
||||||
@@ -98,6 +99,62 @@ struct ScanSummary {
|
|||||||
effects: 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>>()?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
/// Re-evaluate every Delphi issue detail and atomically publish a new rule revision.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
context_path = "/moderation/tech-review",
|
context_path = "/moderation/tech-review",
|
||||||
@@ -316,12 +373,13 @@ async fn run_scan(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for rule in &rules {
|
for rule in &rules {
|
||||||
let effect = evaluate_rule(&rule.program, &input).wrap_err_with(|| {
|
let effect = evaluate_rule(&rule.program, &input)
|
||||||
format!(
|
.wrap_err_with(|| {
|
||||||
"failed to evaluate delphi rule {} for detail {detail_id}",
|
format!(
|
||||||
rule.id
|
"failed to evaluate delphi rule {} for detail {detail_id}",
|
||||||
)
|
rule.id
|
||||||
})?;
|
)
|
||||||
|
})?;
|
||||||
if let Some(effect) = effect {
|
if let Some(effect) = effect {
|
||||||
effects.push(MaterializedEffect {
|
effects.push(MaterializedEffect {
|
||||||
detail_id,
|
detail_id,
|
||||||
|
|||||||
@@ -17,6 +17,30 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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(
|
public async testRule(
|
||||||
request: Labrinth.TechReview.Internal.TestDelphiRuleRequest,
|
request: Labrinth.TechReview.Internal.TestDelphiRuleRequest,
|
||||||
): Promise<Labrinth.TechReview.Internal.TestDelphiRuleResponse> {
|
): Promise<Labrinth.TechReview.Internal.TestDelphiRuleResponse> {
|
||||||
|
|||||||
@@ -2235,6 +2235,26 @@ export namespace Labrinth {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
created_by: number | null
|
created_by: number | null
|
||||||
updated_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 = {
|
export type WriteDelphiRule = {
|
||||||
@@ -2261,6 +2281,14 @@ export namespace Labrinth {
|
|||||||
hidden: boolean
|
hidden: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DelphiRuleSchema = Record<string, unknown>
|
||||||
|
|
||||||
|
export type DelphiRuleSchemaResponse = {
|
||||||
|
input: DelphiRuleSchema
|
||||||
|
output: DelphiRuleSchema
|
||||||
|
components: Record<string, DelphiRuleSchema>
|
||||||
|
}
|
||||||
|
|
||||||
export type TestDelphiRuleResponse = {
|
export type TestDelphiRuleResponse = {
|
||||||
effects: Array<DelphiRuleEffect | null>
|
effects: Array<DelphiRuleEffect | null>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user