diff --git a/Cargo.lock b/Cargo.lock index 726ebeeed2..56dcc9d209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -11755,6 +11834,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" diff --git a/Cargo.toml b/Cargo.toml index 1529ce8910..f6952a2d5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue b/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue index a6756ce443..8329b64b33 100644 --- a/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue +++ b/apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue @@ -5,14 +5,8 @@

{{ trace.project_name }}

-

- {{ trace.version_number }} -

+

@@ -33,9 +27,11 @@ diff --git a/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue b/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue index 8d42abbc1e..1d4fd8cb8a 100644 --- a/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue +++ b/apps/frontend/src/components/ui/moderation/GlobalDetailTracesList.vue @@ -65,7 +65,7 @@

Path - {{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }} +

@@ -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 { diff --git a/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue b/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue new file mode 100644 index 0000000000..6289a9cd56 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/IssueDetailPath.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue index f91bd67552..3e595510ca 100644 --- a/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue +++ b/apps/frontend/src/components/ui/moderation/ModerationTechRevCard.vue @@ -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' @@ -601,6 +601,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, @@ -999,6 +1022,8 @@ async function updateGlobalDetailStatus( const expandedClasses = reactive>(new Set()) const autoExpandedFileIds = reactive>(new Set()) const showCopyFeedback = reactive>(new Map()) +const copyingCelDetails = reactive>(new Set()) +const copiedCelDetails = reactive>(new Set()) const highlightedSourceCache = reactive>(new Map()) const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2 @@ -1025,7 +1050,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) @@ -1770,27 +1795,12 @@ function copyId() { class="border-b border-solid border-surface-1 px-4 py-3" >
-
- -
+
- {{ - truncateMiddle(classItem.filePath) - }} +
+ + +
diff --git a/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json b/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json deleted file mode 100644 index a80d2649ca..0000000000 --- a/apps/labrinth/.sqlx/query-080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json b/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json new file mode 100644 index 0000000000..20e84c3874 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json b/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json new file mode 100644 index 0000000000..3263182843 --- /dev/null +++ b/apps/labrinth/.sqlx/query-0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json b/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json deleted file mode 100644 index 4bc87e73a5..0000000000 --- a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json b/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json deleted file mode 100644 index c8a82d60ef..0000000000 --- a/apps/labrinth/.sqlx/query-0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-159d482c1859b0d0d42a15fb2b49e3a9a17755694448e00bf5e5faccb3ace76d.json b/apps/labrinth/.sqlx/query-159d482c1859b0d0d42a15fb2b49e3a9a17755694448e00bf5e5faccb3ace76d.json new file mode 100644 index 0000000000..f2b063d771 --- /dev/null +++ b/apps/labrinth/.sqlx/query-159d482c1859b0d0d42a15fb2b49e3a9a17755694448e00bf5e5faccb3ace76d.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json b/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json new file mode 100644 index 0000000000..f643ed3d22 --- /dev/null +++ b/apps/labrinth/.sqlx/query-1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json b/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json deleted file mode 100644 index 4db30b1153..0000000000 --- a/apps/labrinth/.sqlx/query-1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json deleted file mode 100644 index 921f7f92d9..0000000000 --- a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d.json b/apps/labrinth/.sqlx/query-1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d.json new file mode 100644 index 0000000000..099bf29e82 --- /dev/null +++ b/apps/labrinth/.sqlx/query-1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d.json @@ -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>\",\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>\"\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>", + "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>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + null + ] + }, + "hash": "1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d" +} diff --git a/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json b/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json new file mode 100644 index 0000000000..f918bf11f0 --- /dev/null +++ b/apps/labrinth/.sqlx/query-20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM delphi_rules WHERE delete_on_next_revision", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61" +} diff --git a/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json b/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json deleted file mode 100644 index 38d43c6043..0000000000 --- a/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $7::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n )\n SELECT id\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ORDER BY\n CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "TextArray", - "Text", - "Text", - "Bool", - "Text", - "Bool" - ] - }, - "nullable": [ - false - ] - }, - "hash": "22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9" -} diff --git a/apps/labrinth/.sqlx/query-262f8286192e30763553811a7cfe6eccc43bf85a44c5ba72020887af8ed98f78.json b/apps/labrinth/.sqlx/query-262f8286192e30763553811a7cfe6eccc43bf85a44c5ba72020887af8ed98f78.json new file mode 100644 index 0000000000..d1516e0e95 --- /dev/null +++ b/apps/labrinth/.sqlx/query-262f8286192e30763553811a7cfe6eccc43bf85a44c5ba72020887af8ed98f78.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $7::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND NOT didws.hidden\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n )\n SELECT id\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ORDER BY\n CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Text", + "Bool", + "Text", + "Bool" + ] + }, + "nullable": [ + false + ] + }, + "hash": "262f8286192e30763553811a7cfe6eccc43bf85a44c5ba72020887af8ed98f78" +} diff --git a/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json b/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json new file mode 100644 index 0000000000..5d0c482906 --- /dev/null +++ b/apps/labrinth/.sqlx/query-285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb.json b/apps/labrinth/.sqlx/query-2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb.json new file mode 100644 index 0000000000..c1aa96d89b --- /dev/null +++ b/apps/labrinth/.sqlx/query-2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb.json @@ -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>\",\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>\"\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>", + "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>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + null + ] + }, + "hash": "2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb" +} diff --git a/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json b/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json deleted file mode 100644 index 815e7795ec..0000000000 --- a/apps/labrinth/.sqlx/query-38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-3f1aaeed6e394294fa32da7e7f83765fb64bd9499a796470d544466886e1a808.json b/apps/labrinth/.sqlx/query-3f1aaeed6e394294fa32da7e7f83765fb64bd9499a796470d544466886e1a808.json new file mode 100644 index 0000000000..bf96272e15 --- /dev/null +++ b/apps/labrinth/.sqlx/query-3f1aaeed6e394294fa32da7e7f83765fb64bd9499a796470d544466886e1a808.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json b/apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json similarity index 51% rename from apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json rename to apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json index 3c99ff3fed..c04d8b8fea 100644 --- a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json +++ b/apps/labrinth/.sqlx/query-42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json b/apps/labrinth/.sqlx/query-436b1dad031f143ba987d992514c7fb02b2a67253c543ce146cf2cf190010a06.json similarity index 76% rename from apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json rename to apps/labrinth/.sqlx/query-436b1dad031f143ba987d992514c7fb02b2a67253c543ce146cf2cf190010a06.json index c27569000b..88a5139d85 100644 --- a/apps/labrinth/.sqlx/query-c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3.json +++ b/apps/labrinth/.sqlx/query-436b1dad031f143ba987d992514c7fb02b2a67253c543ce146cf2cf190010a06.json @@ -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\"\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\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ", "describe": { "columns": [ { @@ -18,5 +18,5 @@ null ] }, - "hash": "c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3" + "hash": "436b1dad031f143ba987d992514c7fb02b2a67253c543ce146cf2cf190010a06" } diff --git a/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.json b/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.json deleted file mode 100644 index fa0165cc60..0000000000 --- a/apps/labrinth/.sqlx/query-48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3.json +++ /dev/null @@ -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\"\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", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3" -} diff --git a/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json b/apps/labrinth/.sqlx/query-4a30e36261229605104f319105e88771f211fc82c2e8dc67fdd36a38d45b9f16.json similarity index 65% rename from apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json rename to apps/labrinth/.sqlx/query-4a30e36261229605104f319105e88771f211fc82c2e8dc67fdd36a38d45b9f16.json index c55b8c6c7c..39d8ee3347 100644 --- a/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json +++ b/apps/labrinth/.sqlx/query-4a30e36261229605104f319105e88771f211fc82c2e8dc67fdd36a38d45b9f16.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json b/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json new file mode 100644 index 0000000000..fcafa64eb2 --- /dev/null +++ b/apps/labrinth/.sqlx/query-4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9.json b/apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json similarity index 53% rename from apps/labrinth/.sqlx/query-cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9.json rename to apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json index 39215aaedc..ec24566ebe 100644 --- a/apps/labrinth/.sqlx/query-cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9.json +++ b/apps/labrinth/.sqlx/query-50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-54fd60c11e2a7691b0efcc384917137bc64408856543ca32091bab0ecc91d819.json b/apps/labrinth/.sqlx/query-54fd60c11e2a7691b0efcc384917137bc64408856543ca32091bab0ecc91d819.json new file mode 100644 index 0000000000..856b5a1e73 --- /dev/null +++ b/apps/labrinth/.sqlx/query-54fd60c11e2a7691b0efcc384917137bc64408856543ca32091bab0ecc91d819.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json b/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json deleted file mode 100644 index 450ec6ad79..0000000000 --- a/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH filtered_projects AS (\n SELECT\n m.id,\n m.queued,\n m.published\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $6::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published\n FROM filtered_projects\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $4\n LIMIT $3\n ),\n page_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n page_external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n CASE\n WHEN m.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(ppt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count\n FROM page_ids page\n INNER JOIN mods m ON m.id = page.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id\n LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "total_count!", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "id?", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "slug?", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "name?", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "summary?", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "status?", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "requested_status?", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "queued?", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "published?", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated?", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "organization_id?", - "type_info": "Int8" - }, - { - "ordinal": 12, - "name": "organization_name?", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "organization_icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "owner_id?", - "type_info": "Int8" - }, - { - "ordinal": 15, - "name": "owner_name?", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "owner_icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "project_types?: Vec", - "type_info": "TextArray" - }, - { - "ordinal": 18, - "name": "external_dependencies_count?", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "TextArray", - "Int8", - "Int8", - "Text", - "Bool" - ] - }, - "nullable": [ - null, - false, - true, - false, - false, - true, - false, - true, - true, - false, - false, - true, - false, - true, - false, - false, - true, - null, - null - ] - }, - "hash": "5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b" -} diff --git a/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json b/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json deleted file mode 100644 index 55ba902620..0000000000 --- a/apps/labrinth/.sqlx/query-5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json b/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json new file mode 100644 index 0000000000..e2a4cfb50f --- /dev/null +++ b/apps/labrinth/.sqlx/query-62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c" +} diff --git a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json b/apps/labrinth/.sqlx/query-63c94bea3d26a55b5eab89f2f6fac0eef45ebaa301f2ae14de4d5f3dcbd65cbb.json similarity index 93% rename from apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json rename to apps/labrinth/.sqlx/query-63c94bea3d26a55b5eab89f2f6fac0eef45ebaa301f2ae14de4d5f3dcbd65cbb.json index b9c6d3543c..f715919c0e 100644 --- a/apps/labrinth/.sqlx/query-63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73.json +++ b/apps/labrinth/.sqlx/query-63c94bea3d26a55b5eab89f2f6fac0eef45ebaa301f2ae14de4d5f3dcbd65cbb.json @@ -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>\",\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>\",\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" } diff --git a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json b/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json deleted file mode 100644 index b4c2e5a56e..0000000000 --- a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json b/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json new file mode 100644 index 0000000000..4187b628b7 --- /dev/null +++ b/apps/labrinth/.sqlx/query-6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json b/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json deleted file mode 100644 index fc7d2ac98d..0000000000 --- a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json b/apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json similarity index 70% rename from apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json rename to apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json index 20a2667bb3..34f1afab32 100644 --- a/apps/labrinth/.sqlx/query-ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91.json +++ b/apps/labrinth/.sqlx/query-6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-73e3646a21e4acb1299f0b183b92beda18c30564fc90e5213e5237f505504558.json b/apps/labrinth/.sqlx/query-73e3646a21e4acb1299f0b183b92beda18c30564fc90e5213e5237f505504558.json new file mode 100644 index 0000000000..f0a4d22896 --- /dev/null +++ b/apps/labrinth/.sqlx/query-73e3646a21e4acb1299f0b183b92beda18c30564fc90e5213e5237f505504558.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-8107e60111543c351cfa8aaf0d864e270b7b9f3f8041c5feffa43449f4d5251a.json b/apps/labrinth/.sqlx/query-8107e60111543c351cfa8aaf0d864e270b7b9f3f8041c5feffa43449f4d5251a.json new file mode 100644 index 0000000000..31a3becc83 --- /dev/null +++ b/apps/labrinth/.sqlx/query-8107e60111543c351cfa8aaf0d864e270b7b9f3f8041c5feffa43449f4d5251a.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json b/apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json similarity index 50% rename from apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json rename to apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json index 5d509fdf07..24b8b0d507 100644 --- a/apps/labrinth/.sqlx/query-66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2.json +++ b/apps/labrinth/.sqlx/query-820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-86141f67ab4efad36497a839e62e989332725543e1d3b11415502dc8521b724d.json b/apps/labrinth/.sqlx/query-86141f67ab4efad36497a839e62e989332725543e1d3b11415502dc8521b724d.json new file mode 100644 index 0000000000..3d337eed1e --- /dev/null +++ b/apps/labrinth/.sqlx/query-86141f67ab4efad36497a839e62e989332725543e1d3b11415502dc8521b724d.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9.json b/apps/labrinth/.sqlx/query-868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9.json new file mode 100644 index 0000000000..13ac058e9e --- /dev/null +++ b/apps/labrinth/.sqlx/query-868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9.json @@ -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>\",\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>\"\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>", + "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>", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + true, + true, + null + ] + }, + "hash": "868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9" +} diff --git a/apps/labrinth/.sqlx/query-89f8fcde28ddb5bc2ccf3beef08f03642603c79db754d0530a9c6f2f7d9c32b7.json b/apps/labrinth/.sqlx/query-89f8fcde28ddb5bc2ccf3beef08f03642603c79db754d0530a9c6f2f7d9c32b7.json new file mode 100644 index 0000000000..5b26565b43 --- /dev/null +++ b/apps/labrinth/.sqlx/query-89f8fcde28ddb5bc2ccf3beef08f03642603c79db754d0530a9c6f2f7d9c32b7.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json b/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json deleted file mode 100644 index 444f57bf3b..0000000000 --- a/apps/labrinth/.sqlx/query-8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json b/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json deleted file mode 100644 index 83806baae2..0000000000 --- a/apps/labrinth/.sqlx/query-9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json b/apps/labrinth/.sqlx/query-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json deleted file mode 100644 index c8aad7deda..0000000000 --- a/apps/labrinth/.sqlx/query-9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-9caa3ddeea78b2e0e2afc57bcf53e70c2fc4456864b9f375debcc266bd658d1d.json b/apps/labrinth/.sqlx/query-9caa3ddeea78b2e0e2afc57bcf53e70c2fc4456864b9f375debcc266bd658d1d.json new file mode 100644 index 0000000000..fb177cc69f --- /dev/null +++ b/apps/labrinth/.sqlx/query-9caa3ddeea78b2e0e2afc57bcf53e70c2fc4456864b9f375debcc266bd658d1d.json @@ -0,0 +1,138 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $9::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND NOT didws.hidden\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n ),\n filtered_projects AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND\n project_types[1] = $4\n )\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM filtered_projects\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $7\n LIMIT $6\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n m.team_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n page_ids.project_types,\n page_ids.external_dependencies_count\n FROM page_ids\n INNER JOIN mods m ON m.id = page_ids.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "total_count!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "slug?", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "name?", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "summary?", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "status?", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "requested_status?", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "queued?", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "published?", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated?", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "organization_id?", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "organization_name?", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "organization_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "owner_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "owner_name?", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "owner_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "project_types?: Vec", + "type_info": "TextArray" + }, + { + "ordinal": 18, + "name": "external_dependencies_count?", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Text", + "Bool", + "Int8", + "Int8", + "Text", + "Bool" + ] + }, + "nullable": [ + null, + false, + true, + false, + false, + true, + false, + true, + true, + false, + false, + true, + false, + true, + false, + false, + true, + null, + null + ] + }, + "hash": "9caa3ddeea78b2e0e2afc57bcf53e70c2fc4456864b9f375debcc266bd658d1d" +} diff --git a/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json b/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json new file mode 100644 index 0000000000..d43d795d65 --- /dev/null +++ b/apps/labrinth/.sqlx/query-a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json b/apps/labrinth/.sqlx/query-a7c4204b80587b0b73127125c695d3c779b1a052c82797b771e8fc9e38169475.json similarity index 96% rename from apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json rename to apps/labrinth/.sqlx/query-a7c4204b80587b0b73127125c695d3c779b1a052c82797b771e8fc9e38169475.json index cf686ee65a..51aed00156 100644 --- a/apps/labrinth/.sqlx/query-055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a.json +++ b/apps/labrinth/.sqlx/query-a7c4204b80587b0b73127125c695d3c779b1a052c82797b771e8fc9e38169475.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json b/apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json similarity index 52% rename from apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json rename to apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json index 2731d395ff..b8322723a3 100644 --- a/apps/labrinth/.sqlx/query-329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d.json +++ b/apps/labrinth/.sqlx/query-ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json b/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json new file mode 100644 index 0000000000..155fd19fbd --- /dev/null +++ b/apps/labrinth/.sqlx/query-abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-b0004df8d3b1c5f9193a21f981452ce4ae56f608ec698f6f1ebb4c470bb2f0f5.json b/apps/labrinth/.sqlx/query-b0004df8d3b1c5f9193a21f981452ce4ae56f608ec698f6f1ebb4c470bb2f0f5.json new file mode 100644 index 0000000000..b7d3810c3e --- /dev/null +++ b/apps/labrinth/.sqlx/query-b0004df8d3b1c5f9193a21f981452ce4ae56f608ec698f6f1ebb4c470bb2f0f5.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json b/apps/labrinth/.sqlx/query-b4f5fbf3bdc7802c8e282861f1a51991b5b114e1b793210f1d31e0144c2138b9.json similarity index 77% rename from apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json rename to apps/labrinth/.sqlx/query-b4f5fbf3bdc7802c8e282861f1a51991b5b114e1b793210f1d31e0144c2138b9.json index 99e2d4391d..e77b9af062 100644 --- a/apps/labrinth/.sqlx/query-5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41.json +++ b/apps/labrinth/.sqlx/query-b4f5fbf3bdc7802c8e282861f1a51991b5b114e1b793210f1d31e0144c2138b9.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json b/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json new file mode 100644 index 0000000000..c26c415abd --- /dev/null +++ b/apps/labrinth/.sqlx/query-b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json deleted file mode 100644 index 89bd8147dc..0000000000 --- a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-bb94018c84f3809b9ad89f35ff08501addc8f203a0a152346aaebd32db52be30.json b/apps/labrinth/.sqlx/query-bb94018c84f3809b9ad89f35ff08501addc8f203a0a152346aaebd32db52be30.json deleted file mode 100644 index 247c99cd89..0000000000 --- a/apps/labrinth/.sqlx/query-bb94018c84f3809b9ad89f35ff08501addc8f203a0a152346aaebd32db52be30.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json b/apps/labrinth/.sqlx/query-c2d2e0feb73843db73c73e8137ff0090ceb2292526e7e9ad5d54c51e8d732638.json similarity index 76% rename from apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json rename to apps/labrinth/.sqlx/query-c2d2e0feb73843db73c73e8137ff0090ceb2292526e7e9ad5d54c51e8d732638.json index a0657d3e6d..58b6b2db78 100644 --- a/apps/labrinth/.sqlx/query-52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53.json +++ b/apps/labrinth/.sqlx/query-c2d2e0feb73843db73c73e8137ff0090ceb2292526e7e9ad5d54c51e8d732638.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json b/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json deleted file mode 100644 index f245ea1c8a..0000000000 --- a/apps/labrinth/.sqlx/query-c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DROP SCHEMA public CASCADE;", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7" -} diff --git a/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json b/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json deleted file mode 100644 index 91c2728ad9..0000000000 --- a/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $9::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n ),\n filtered_projects AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND\n project_types[1] = $4\n )\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM filtered_projects\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $7\n LIMIT $6\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n m.team_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n page_ids.project_types,\n page_ids.external_dependencies_count\n FROM page_ids\n INNER JOIN mods m ON m.id = page_ids.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "total_count!", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "id?", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "slug?", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "name?", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "summary?", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "status?", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "requested_status?", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "queued?", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "published?", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated?", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "organization_id?", - "type_info": "Int8" - }, - { - "ordinal": 12, - "name": "organization_name?", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "organization_icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "owner_id?", - "type_info": "Int8" - }, - { - "ordinal": 15, - "name": "owner_name?", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "owner_icon_url?", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "project_types?: Vec", - "type_info": "TextArray" - }, - { - "ordinal": 18, - "name": "external_dependencies_count?", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "TextArray", - "Text", - "Text", - "Bool", - "Int8", - "Int8", - "Text", - "Bool" - ] - }, - "nullable": [ - null, - false, - true, - false, - false, - true, - false, - true, - true, - false, - false, - true, - false, - true, - false, - false, - true, - null, - null - ] - }, - "hash": "ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41" -} diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json deleted file mode 100644 index 469c30168a..0000000000 --- a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-cdc0598c4642df1bf2136aa895c9a1fcf8991fe53044d79e37f357dd530e3530.json b/apps/labrinth/.sqlx/query-cdc0598c4642df1bf2136aa895c9a1fcf8991fe53044d79e37f357dd530e3530.json new file mode 100644 index 0000000000..a17f724a63 --- /dev/null +++ b/apps/labrinth/.sqlx/query-cdc0598c4642df1bf2136aa895c9a1fcf8991fe53044d79e37f357dd530e3530.json @@ -0,0 +1,135 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH filtered_projects AS (\n SELECT\n m.id,\n m.queued,\n m.published\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $6::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n AND NOT didws.hidden\n )\n )\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published\n FROM filtered_projects\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $4\n LIMIT $3\n ),\n page_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n page_external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n CASE\n WHEN m.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(ppt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count\n FROM page_ids page\n INNER JOIN mods m ON m.id = page.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id\n LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "total_count!", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "slug?", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "name?", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "summary?", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "status?", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "requested_status?", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "queued?", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "published?", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated?", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "organization_id?", + "type_info": "Int8" + }, + { + "ordinal": 12, + "name": "organization_name?", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "organization_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "owner_id?", + "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "owner_name?", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "owner_icon_url?", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "project_types?: Vec", + "type_info": "TextArray" + }, + { + "ordinal": 18, + "name": "external_dependencies_count?", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Int8", + "Int8", + "Text", + "Bool" + ] + }, + "nullable": [ + null, + false, + true, + false, + false, + true, + false, + true, + true, + false, + false, + true, + false, + true, + false, + false, + true, + null, + null + ] + }, + "hash": "cdc0598c4642df1bf2136aa895c9a1fcf8991fe53044d79e37f357dd530e3530" +} diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json deleted file mode 100644 index 52e020ebf2..0000000000 --- a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json b/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json new file mode 100644 index 0000000000..1760686240 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-d973a1edd200b4cd68ec30556a1e12240885f2372265597efa6e0361831d1318.json b/apps/labrinth/.sqlx/query-d973a1edd200b4cd68ec30556a1e12240885f2372265597efa6e0361831d1318.json new file mode 100644 index 0000000000..5b5d207a52 --- /dev/null +++ b/apps/labrinth/.sqlx/query-d973a1edd200b4cd68ec30556a1e12240885f2372265597efa6e0361831d1318.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a.json b/apps/labrinth/.sqlx/query-e38dc9448a56b2dc0e99661775f488c8f1ad2f0fca12655a3cc257a8cb5a784d.json similarity index 71% rename from apps/labrinth/.sqlx/query-4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a.json rename to apps/labrinth/.sqlx/query-e38dc9448a56b2dc0e99661775f488c8f1ad2f0fca12655a3cc257a8cb5a784d.json index 21c5f042e6..95d91ae7e1 100644 --- a/apps/labrinth/.sqlx/query-4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a.json +++ b/apps/labrinth/.sqlx/query-e38dc9448a56b2dc0e99661775f488c8f1ad2f0fca12655a3cc257a8cb5a784d.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json b/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json deleted file mode 100644 index cfb283ab10..0000000000 --- a/apps/labrinth/.sqlx/query-e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json b/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json new file mode 100644 index 0000000000..82f38da2f6 --- /dev/null +++ b/apps/labrinth/.sqlx/query-e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b" +} diff --git a/apps/labrinth/.sqlx/query-e8b6e423aaf62046afd19fb1e177a3ef1f18f4814df8924926c47d4b0c7145c7.json b/apps/labrinth/.sqlx/query-e8b6e423aaf62046afd19fb1e177a3ef1f18f4814df8924926c47d4b0c7145c7.json deleted file mode 100644 index 1e3e8c4d7f..0000000000 --- a/apps/labrinth/.sqlx/query-e8b6e423aaf62046afd19fb1e177a3ef1f18f4814df8924926c47d4b0c7145c7.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4.json b/apps/labrinth/.sqlx/query-ebbd8dce6a062c18f329381c1d17ece6727b6633053e1fc8b00e48cd539073f7.json similarity index 79% rename from apps/labrinth/.sqlx/query-87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4.json rename to apps/labrinth/.sqlx/query-ebbd8dce6a062c18f329381c1d17ece6727b6633053e1fc8b00e48cd539073f7.json index eabfd4299d..4e5742b00a 100644 --- a/apps/labrinth/.sqlx/query-87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4.json +++ b/apps/labrinth/.sqlx/query-ebbd8dce6a062c18f329381c1d17ece6727b6633053e1fc8b00e48cd539073f7.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-f0333b3cc5ecd51adf07542b85107c8befb4cdaa728a167bd2586898875836c1.json b/apps/labrinth/.sqlx/query-f0333b3cc5ecd51adf07542b85107c8befb4cdaa728a167bd2586898875836c1.json new file mode 100644 index 0000000000..129d31765b --- /dev/null +++ b/apps/labrinth/.sqlx/query-f0333b3cc5ecd51adf07542b85107c8befb4cdaa728a167bd2586898875836c1.json @@ -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\"\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", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "f0333b3cc5ecd51adf07542b85107c8befb4cdaa728a167bd2586898875836c1" +} diff --git a/apps/labrinth/.sqlx/query-f803c8d7ea8457d298beb4acc43667f7e638d7e487c09c86857d316fca40813b.json b/apps/labrinth/.sqlx/query-f803c8d7ea8457d298beb4acc43667f7e638d7e487c09c86857d316fca40813b.json new file mode 100644 index 0000000000..94fb46b58b --- /dev/null +++ b/apps/labrinth/.sqlx/query-f803c8d7ea8457d298beb4acc43667f7e638d7e487c09c86857d316fca40813b.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-f94722c5b0092d3fcdb340da0402fb5a46f2ab5121a42b00ee8c0498b2871d58.json b/apps/labrinth/.sqlx/query-f94722c5b0092d3fcdb340da0402fb5a46f2ab5121a42b00ee8c0498b2871d58.json new file mode 100644 index 0000000000..f8cf749c75 --- /dev/null +++ b/apps/labrinth/.sqlx/query-f94722c5b0092d3fcdb340da0402fb5a46f2ab5121a42b00ee8c0498b2871d58.json @@ -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" +} diff --git a/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json b/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json deleted file mode 100644 index 006563f450..0000000000 --- a/apps/labrinth/.sqlx/query-fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json b/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json deleted file mode 100644 index 16a500d5ac..0000000000 --- a/apps/labrinth/.sqlx/query-fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json b/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json deleted file mode 100644 index d3e3520bcc..0000000000 --- a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json +++ /dev/null @@ -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" -} diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 1081074d9a..068331ff85 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -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 } diff --git a/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql b/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql new file mode 100644 index 0000000000..59d4ffbe82 --- /dev/null +++ b/apps/labrinth/migrations/20260720105358_delphi_trace_rules.sql @@ -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'; diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index 5f92ece21d..bd953e7e2d 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -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); diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index 1a5d9e55fb..ce0fa3da9d 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -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?; diff --git a/apps/labrinth/src/routes/internal/delphi/rescan.rs b/apps/labrinth/src/routes/internal/delphi/rescan.rs index c7d7fa297b..bfe40edfc6 100644 --- a/apps/labrinth/src/routes/internal/delphi/rescan.rs +++ b/apps/labrinth/src/routes/internal/delphi/rescan.rs @@ -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 ) "#, ) diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs new file mode 100644 index 0000000000..cde08d1581 --- /dev/null +++ b/apps/labrinth/src/routes/internal/delphi/tech_review_queue.rs @@ -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::>(); + 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::>(), + ) + .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::>(); + 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::>(), + ) + .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::>(); + 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::>(), + ) + .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::>(), + 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::>(); + 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::>(), + ) + .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::>(), + reason, + txn, + ) + .await +} diff --git a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs b/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs deleted file mode 100644 index 23a45d218a..0000000000 --- a/apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs +++ /dev/null @@ -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, - report_id: Option, - last_tech_review_message_type: Option, -} - -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::>(); - if project_ids.is_empty() { - return Ok(()); - } - - let project_ids_raw = project_ids.iter().map(|id| id.0).collect::>(); - 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::>(); - - 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::>(); - - 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 { - [ - 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() -} diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index 872593aaf1..7d05db1af2 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -115,6 +115,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, diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index e66f0e0f84..7897c8cd84 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -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 diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 2cb592533b..47ed4a6a29 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -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" 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" 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::>() @@ -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::>(); - 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::>(), &mut txn, ) .await?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs index 6b69125466..585a178a71 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs @@ -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 "#, diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs new file mode 100644 index 0000000000..10a7c0d6fa --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs @@ -0,0 +1,707 @@ +use std::collections::{BTreeMap, HashMap}; + +use actix_web::{HttpRequest, delete, get, post, put, web}; +use chrono::{DateTime, Utc}; +use eyre::eyre; +use serde::{Deserialize, Serialize}; +use xredis::RedisPool; + +use super::rules_scan::{ + RuleArtifact, RuleInput, RuleScan, RuleScope, RuleTrace, +}; +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, +}; + +const MAX_RULE_NAME_LENGTH: usize = 256; +const MAX_RULE_EXPRESSION_LENGTH: usize = 65_536; +const MAX_RULE_TEST_TRACES: usize = 10; + +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, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, + pub affected_details_count: i64, + pub affected_details: Vec, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct DelphiRuleAffectedDetail { + pub detail_id: DelphiReportIssueDetailsId, + pub issue_id: DelphiReportIssueId, + pub project_id: Option, + pub project_name: Option, + pub project_icon_url: Option, + pub version_id: Option, + pub version_name: Option, + pub version_number: Option, + pub issue_type: String, + pub key: String, + pub jar: Option, + pub file_path: String, + pub original_severity: DelphiSeverity, + pub severity: Option, + pub hidden: bool, +} + +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct WriteDelphiRule { + pub name: String, + pub rule: String, + #[serde(default)] + pub priority: i32, +} + +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct TestDelphiRule { + pub rule: String, + pub traces: Vec, +} + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestDelphiRuleTrace { + pub key: String, + pub issue_type: String, + pub severity: DelphiSeverity, + pub jar: Option, + pub file_path: String, + pub data: HashMap, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct TestDelphiRuleResponse { + pub effects: Vec>, +} + +#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(deny_unknown_fields)] +pub struct DelphiRuleEffect { + #[serde(default)] + pub severity: Option, + #[serde(default)] + pub hidden: bool, +} + +struct ValidatedRule { + name: String, + rule: String, + priority: i32, +} + +impl WriteDelphiRule { + fn validate(self) -> Result { + let name = self.name.trim().to_string(); + if name.is_empty() { + return Err(ApiError::Request(eyre!("rule name cannot be empty"))); + } + if name.chars().count() > MAX_RULE_NAME_LENGTH { + return Err(ApiError::Request(eyre!( + "rule name cannot exceed {MAX_RULE_NAME_LENGTH} characters" + ))); + } + + let rule = self.rule.trim().to_string(); + if rule.is_empty() { + return Err(ApiError::Request(eyre!( + "rule expression cannot be empty" + ))); + } + if rule.len() > MAX_RULE_EXPRESSION_LENGTH { + return Err(ApiError::Request(eyre!( + "rule expression cannot exceed {MAX_RULE_EXPRESSION_LENGTH} bytes" + ))); + } + + cel::Program::compile(&rule).map_err(|error| { + ApiError::Request(eyre!("invalid cel expression: {error}")) + })?; + + Ok(ValidatedRule { + name, + 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, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + let request = body.into_inner(); + let rule = request.rule.trim(); + if rule.is_empty() { + return Err(ApiError::Request(eyre!( + "rule expression cannot be empty" + ))); + } + if rule.len() > MAX_RULE_EXPRESSION_LENGTH { + return Err(ApiError::Request(eyre!( + "rule expression cannot exceed {MAX_RULE_EXPRESSION_LENGTH} bytes" + ))); + } + if request.traces.len() > MAX_RULE_TEST_TRACES { + return Err(ApiError::Request(eyre!( + "cannot test more than {MAX_RULE_TEST_TRACES} traces at once" + ))); + } + + let program = cel::Program::compile(rule).map_err(|error| { + ApiError::Request(eyre!("invalid cel expression: {error}")) + })?; + let mut effects = Vec::with_capacity(request.traces.len()); + + for (index, trace) in request.traces.iter().enumerate() { + let input = test_rule_input(trace); + let effect = super::rules_scan::evaluate_rule(&program, input) + .map_err(|error| { + ApiError::Request(eyre!( + "failed to evaluate test trace {index}: {error}" + )) + })?; + effects.push(effect); + } + + Ok(web::Json(TestDelphiRuleResponse { effects })) +} + +fn test_rule_input(trace: &TestDelphiRuleTrace) -> RuleInput { + RuleInput { + schema_version: 1, + trace: RuleTrace { + key: trace.key.clone(), + issue_type: trace.issue_type.clone(), + 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([ + ("sha1".to_string(), "0123456789abcdef".to_string()), + ("sha512".to_string(), "fedcba9876543210".to_string()), + ]), + }, + scope: RuleScope { + project_id: Some("example-project".to_string()), + version_id: Some("example-version".to_string()), + file_id: Some("example-file".to_string()), + }, + } +} + +/// 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)) +)] +#[get("/rules")] +pub async fn get_rules( + req: HttpRequest, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result>, 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::::new(); + for rule in rules { + if response + .last() + .is_none_or(|existing| existing.id != rule.id) + { + 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: 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)) +)] +#[get("/rules/{id}/effects")] +pub async fn get_rule_affected_details( + req: HttpRequest, + pool: web::Data, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiRuleId,)>, +) -> Result>, 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, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result, ApiError> { + let user = check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await?; + let rule = body.into_inner().validate()?; + 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, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiRuleId,)>, + body: web::Json, +) -> Result, 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()?; + 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, + redis: web::Data, + session_queue: web::Data, + 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(()) +} diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs new file mode 100644 index 0000000000..de618826c2 --- /dev/null +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs @@ -0,0 +1,829 @@ +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::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(Serialize, utoipa::ToSchema)] +pub struct RuleInput { + pub schema_version: u32, + pub trace: RuleTrace, + pub scan: RuleScan, + pub artifact: RuleArtifact, + pub scope: RuleScope, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct RuleTrace { + pub key: String, + pub issue_type: String, + pub severity: DelphiSeverity, + pub jar: Option, + pub file_path: String, + pub data: HashMap, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct RuleScan { + pub delphi_version: i32, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct RuleArtifact { + pub size: Option, + pub hashes: BTreeMap, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct RuleScope { + pub project_id: Option, + pub version_id: Option, + pub file_id: Option, +} + +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, +} + +/// 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, + redis: web::Data, + session_queue: web::Data, +) -> Result, ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + let mut schemas = Vec::new(); + ::schemas(&mut schemas); + as ToSchema>::schemas(&mut schemas); + + Ok(web::Json(DelphiRuleSchemaResponse { + input: schema_to_value(::schema())?, + output: schema_to_value( + as PartialSchema>::schema(), + )?, + components: schemas + .into_iter() + .map(|(name, schema)| Ok((name, schema_to_value(schema)?))) + .collect::>()?, + })) +} + +/// 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, + ro_pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path<(DelphiReportIssueDetailsId,)>, +) -> Result, 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>", + 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>" + 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(to_public_id), + version_id: detail.version_id.map(to_public_id), + file_id: detail.file_id.map(to_public_id), + }, + })) +} + +fn schema_to_value( + schema: T, +) -> Result { + 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, + redis: web::Data, + session_queue: web::Data, +) -> Result { + 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) => { + send_event( + &sender, + "complete", + &RuleScanEvent { + phase: "complete", + revision: summary.revision, + scanned: summary.scanned, + total: summary.total, + effects: summary.effects, + }, + ); + } + Err(error) => { + tracing::error!(error = ?error, "delphi rule scan failed"); + send_event( + &sender, + "failed", + &RuleScanErrorEvent { + message: &error.to_string(), + }, + ); + } + } + }); + + 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, +) -> Result { + 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>", + 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>" + 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; + send_progress(sender, "scanning", revision, 0, total, 0); + + 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(to_public_id), + version_id: detail.version_id.map(to_public_id), + file_id: detail.file_id.map(to_public_id), + }, + }; + + 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 { + send_progress( + sender, + "scanning", + revision, + scanned, + total, + effects.len(), + ); + tokio::task::yield_now().await; + } + } + drop(details); + + send_progress(sender, "publishing", revision, total, total, effects.len()); + + 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::>(), + &mut transaction, + ) + .await?; + tech_review_queue::remove_projects( + &affected_projects + .iter() + .filter(|project| !project.new_needs_review) + .map(|project| project.project_id) + .collect::>(), + 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>", + 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>" + 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::>(), + ) + .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(to_public_id), + version_id: detail.version_id.map(to_public_id), + file_id: detail.file_id.map(to_public_id), + }, + }; + + 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> { + 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")? + .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() +} + +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::>(); + let rule_ids = effects + .iter() + .map(|effect| effect.rule_id.0) + .collect::>(); + let severities = effects + .iter() + .map(|effect| effect.effect.severity) + .collect::>(); + let hidden = effects + .iter() + .map(|effect| effect.effect.hidden) + .collect::>(); + + 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], + &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> { + 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"), + } +} + +fn to_public_id(id: i64) -> String { + to_base62(id as u64) +} + +fn send_progress( + sender: &mpsc::UnboundedSender, + phase: &'static str, + revision: i64, + scanned: usize, + total: usize, + effects: usize, +) { + send_event( + sender, + "progress", + &RuleScanEvent { + phase, + revision, + scanned, + total, + effects, + }, + ); +} + +fn send_event( + sender: &mpsc::UnboundedSender, + event: &str, + data: &impl Serialize, +) { + let Ok(data) = serde_json::to_string(data) else { + return; + }; + let _ = + sender.send(Bytes::from(format!("event: {event}\ndata: {data}\n\n"))); +} diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 8c7074a4ba..bef02ec180 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -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?; diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index efda1648ad..ddfa45b529 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -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?; diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index c09489bec3..71de3d6b70 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -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?; diff --git a/packages/api-client/src/modules/labrinth/tech-review/internal.ts b/packages/api-client/src/modules/labrinth/tech-review/internal.ts index 0012474334..2ad32b78ff 100644 --- a/packages/api-client/src/modules/labrinth/tech-review/internal.ts +++ b/packages/api-client/src/modules/labrinth/tech-review/internal.ts @@ -6,6 +6,114 @@ export class LabrinthTechReviewInternalModule extends AbstractModule { return 'labrinth_tech_review_internal' } + public async getRules(): Promise { + return this.client.request( + '/moderation/tech-review/rules', + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getRuleSchema(): Promise { + return this.client.request( + '/moderation/tech-review/rules/schema', + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getDetailRuleInput( + detailId: string, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/details/${detailId}/input`, + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async getRuleAffectedDetails( + id: number, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/${id}/effects`, + { + api: 'labrinth', + version: 'internal', + method: 'GET', + }, + ) + } + + public async testRule( + request: Labrinth.TechReview.Internal.TestDelphiRuleRequest, + ): Promise { + return this.client.request( + '/moderation/tech-review/rules/test', + { + api: 'labrinth', + version: 'internal', + method: 'POST', + body: request, + }, + ) + } + + public async createRule( + rule: Labrinth.TechReview.Internal.WriteDelphiRule, + ): Promise { + return this.client.request( + '/moderation/tech-review/rules', + { + api: 'labrinth', + version: 'internal', + method: 'POST', + body: rule, + }, + ) + } + + public async updateRule( + id: number, + rule: Labrinth.TechReview.Internal.WriteDelphiRule, + ): Promise { + return this.client.request( + `/moderation/tech-review/rules/${id}`, + { + api: 'labrinth', + version: 'internal', + method: 'PUT', + body: rule, + }, + ) + } + + public async deleteRule(id: number): Promise { + return this.client.request(`/moderation/tech-review/rules/${id}`, { + api: 'labrinth', + version: 'internal', + method: 'DELETE', + }) + } + + public async scanRules(signal?: AbortSignal): Promise> { + return this.client.stream('/moderation/tech-review/rules/scan', { + api: 'labrinth', + version: 'internal', + method: 'POST', + signal, + }) + } + /** * Search for projects awaiting technical review. * diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index c3c7155c43..e6169e2061 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -2250,6 +2250,113 @@ 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 TestDelphiRuleTrace = { + key: string + issue_type: string + severity: DelphiSeverity + jar: string | null + file_path: string + data: Record + } + + export type TestDelphiRuleRequest = { + rule: string + traces: TestDelphiRuleTrace[] + } + + export type DelphiRuleEffect = { + severity: DelphiSeverity | null + hidden: boolean + } + + export type DelphiRuleSchema = Record + + export type DelphiRuleSchemaResponse = { + input: DelphiRuleSchema + output: DelphiRuleSchema + components: Record + } + + export type RuleInput = { + schema_version: number + trace: { + key: string + issue_type: string + severity: DelphiSeverity + jar: string | null + file_path: string + data: Record + } + scan: { + delphi_version: number + } + artifact: { + size: number | null + hashes: Record + } + scope: { + project_id: string | null + version_id: string | null + file_id: string | null + } + } + + export type TestDelphiRuleResponse = { + effects: Array + } + + 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 diff --git a/packages/delphi-cel-experiment/Cargo.toml b/packages/delphi-cel-experiment/Cargo.toml new file mode 100644 index 0000000000..34ea4460b0 --- /dev/null +++ b/packages/delphi-cel-experiment/Cargo.toml @@ -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 } diff --git a/packages/delphi-cel-experiment/src/main.rs b/packages/delphi-cel-experiment/src/main.rs new file mode 100644 index 0000000000..92fe06bd33 --- /dev/null +++ b/packages/delphi-cel-experiment/src/main.rs @@ -0,0 +1,173 @@ +use std::{collections::BTreeMap, error::Error, io}; + +use cel::{Context, Program}; +use serde::{Deserialize, Serialize}; + +const EFFECT_RULE: &str = r#" + input.trace.issue_type == "OBFUSCATED_NAMES" + && input.trace.severity == "high" + && "confidence" in input.trace.data + && input.trace.data.confidence >= 0.9 + ? { + "severity": "low", + "hidden": false + } + : null +"#; + +#[derive(Debug, Serialize)] +struct RuleInput { + schema_version: u32, + trace: TraceInput, + scan: ScanInput, + artifact: ArtifactInput, + scope: ScopeInput, +} + +#[derive(Debug, Serialize)] +struct TraceInput { + key: String, + issue_type: String, + severity: DelphiSeverity, + jar: Option, + file_path: String, + data: BTreeMap, +} + +#[derive(Debug, Serialize)] +struct ScanInput { + delphi_version: i32, +} + +#[derive(Debug, Serialize)] +struct ArtifactInput { + size: u32, + hashes: BTreeMap, +} + +#[derive(Debug, Serialize)] +struct ScopeInput { + project_id: String, + version_id: String, + file_id: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +enum DelphiSeverity { + Low, + Medium, + High, + Severe, +} + +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +struct RuleEffect { + #[serde(default)] + severity: Option, + #[serde(default)] + hidden: bool, +} + +fn main() -> Result<(), Box> { + let input = example_input(); + println!( + "CEL input context:\n{}", + serde_json::to_string_pretty(&input)? + ); + + let effect = evaluate_rule(EFFECT_RULE, &input)?; + println!("\nCEL map decoded as RuleEffect:\n{effect:#?}"); + + Ok(()) +} + +fn example_input() -> RuleInput { + RuleInput { + schema_version: 1, + trace: TraceInput { + key: "known-safe:obfuscated-bootstrap".to_string(), + issue_type: "OBFUSCATED_NAMES".to_string(), + severity: DelphiSeverity::High, + jar: Some("META-INF/jars/embedded.jar".to_string()), + file_path: "com/example/Bootstrap.class".to_string(), + data: BTreeMap::from([ + ("confidence".to_string(), serde_json::json!(0.97)), + ("symbol_count".to_string(), serde_json::json!(42)), + ]), + }, + scan: ScanInput { delphi_version: 17 }, + artifact: ArtifactInput { + size: 412_892, + hashes: BTreeMap::from([ + ("sha1".to_string(), "0123456789abcdef".to_string()), + ("sha512".to_string(), "fedcba9876543210".to_string()), + ]), + }, + scope: ScopeInput { + project_id: "AANobbMI".to_string(), + version_id: "IIJJKKLL".to_string(), + file_id: "XXYYZZ00".to_string(), + }, + } +} + +fn evaluate_rule( + expression: &str, + input: &RuleInput, +) -> Result, Box> { + let mut context = Context::default(); + context.add_variable("input", input)?; + + let value = Program::compile(expression)?.execute(&context)?; + let json = value + .json() + .map_err(|error| invalid_data(error.to_string()))?; + match json { + serde_json::Value::Null => Ok(None), + value => Ok(Some(serde_json::from_value(value)?)), + } +} + +fn invalid_data(message: impl Into) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decodes_a_map_as_an_effect() { + let effect = evaluate_rule(EFFECT_RULE, &example_input()) + .expect("rule should evaluate"); + + assert_eq!( + effect, + Some(RuleEffect { + severity: Some(DelphiSeverity::Low), + hidden: false, + }) + ); + } + + #[test] + fn returns_none_when_a_rule_does_not_match() { + let mut input = example_input(); + input.trace.issue_type = "NETWORK_ACCESS".to_string(); + + let effect = + evaluate_rule(EFFECT_RULE, &input).expect("rule should evaluate"); + + assert_eq!(effect, None); + } + + #[test] + fn rejects_unknown_map_fields() { + let error = evaluate_rule(r#"{"unknown": true}"#, &example_input()) + .expect_err("unknown fields should be rejected"); + + assert!(error.to_string().contains("unknown field")); + } +}