This commit is contained in:
@@ -35,7 +35,8 @@ import {
|
||||
migrateModerationCaseAssessments,
|
||||
migrateModerationCaseRelationships,
|
||||
migrateModerationCaseFollowups,
|
||||
migrateModerationCaseReferenceIndexes
|
||||
migrateModerationCaseReferenceIndexes,
|
||||
migrateModerationCaseStates
|
||||
} from '../moderation/schema.js';
|
||||
import type { Migration } from './migration-runner.js';
|
||||
|
||||
@@ -203,6 +204,12 @@ export const migrations: readonly Migration[] = [
|
||||
version: 35,
|
||||
name: 'moderation case foreign key reference indexes',
|
||||
run: migrateModerationCaseReferenceIndexes
|
||||
},
|
||||
{
|
||||
version: 36,
|
||||
name: 'moderation case lifecycle states',
|
||||
mode: 'foreign-keys-off',
|
||||
run: migrateModerationCaseStates
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
canRevokeModerationRestriction,
|
||||
isModerationCaseOpen,
|
||||
moderationCaseOutcomes,
|
||||
moderationCaseStatuses,
|
||||
moderationCaseStatusLabels,
|
||||
type ModerationCaseStatus,
|
||||
type ModerationCasePriority,
|
||||
@@ -438,13 +439,9 @@ function updateCase(actor: AuthUser, input: Record<string, unknown>, now: string
|
||||
const status = input.status === undefined ? row.status : input.status;
|
||||
if (
|
||||
input.status !== undefined &&
|
||||
status !== 'open' &&
|
||||
status !== 'in_review' &&
|
||||
status !== 'escalated'
|
||||
!moderationCaseStatuses.includes(status as ModerationCaseStatus)
|
||||
)
|
||||
throw new AuthError('Choose an active case status, or close the case with an outcome.');
|
||||
if (input.status !== undefined && !isModerationCaseOpen(row.status))
|
||||
throw new AuthError('Use reopen to reactivate a closed case.', 409);
|
||||
throw new AuthError('Choose Open, In progress, or Closed.');
|
||||
const reason = textField(input.reason, 'Status change reason', 4000, status === row.status);
|
||||
const changes: Record<string, unknown> = {};
|
||||
if (title !== row.title) changes.title = { from: row.title, to: title };
|
||||
@@ -585,6 +582,7 @@ export function mutateModeration(actorId: string, input: Record<string, unknown>
|
||||
notifyCase(actor.id, row.id, 'evidence', now);
|
||||
return { id: row.id };
|
||||
}
|
||||
// Older clients may still record decisions, but every closure now stores only Closed.
|
||||
if (input.op === 'resolve-case') {
|
||||
const row = caseRow(input.id);
|
||||
if (row.revision !== input.revision || !isModerationCaseOpen(row.status))
|
||||
@@ -620,7 +618,7 @@ export function mutateModeration(actorId: string, input: Record<string, unknown>
|
||||
.prepare(
|
||||
'UPDATE moderation_cases SET status=?,updated_at=?,revision=revision+1 WHERE id=?'
|
||||
)
|
||||
.run(outcome, now, row.id);
|
||||
.run('closed', now, row.id);
|
||||
moderationEvent(
|
||||
actor,
|
||||
`case.${outcome}`,
|
||||
|
||||
@@ -114,7 +114,7 @@ export function moderationOverview(actorId: string, query: ModerationQuery): Mod
|
||||
if (query.view === 'cases') {
|
||||
if (query.caseStatus === 'active' || query.caseStatus === 'closed') {
|
||||
where.push(
|
||||
`p.status ${query.caseStatus === 'closed' ? 'NOT ' : ''}IN ('open','in_review','escalated')`
|
||||
`p.status ${query.caseStatus === 'closed' ? 'NOT ' : ''}IN ('open','in_review')`
|
||||
);
|
||||
} else if (query.caseStatus) {
|
||||
where.push('p.status=?');
|
||||
@@ -220,7 +220,7 @@ export function moderationOverview(actorId: string, query: ModerationQuery): Mod
|
||||
result.cases = rows<Omit<ModerationCase, 'evidence'>>(
|
||||
'moderation_cases',
|
||||
'p.id,p.game,p.guid,p.title,p.status,p.priority,p.assignee_id AS assigneeId,(SELECT username FROM auth_users WHERE id=p.assignee_id) AS assignee,p.actor,p.created_at AS createdAt,p.updated_at AS updatedAt,p.revision,p.review_at AS reviewAt,p.review_reason AS reviewReason,p.review_sanction_id AS reviewSanctionId',
|
||||
"CASE WHEN p.review_at<=? THEN 0 ELSE 1 END,CASE WHEN p.review_at<=? THEN p.review_at END,CASE WHEN p.status IN ('open','in_review','escalated') THEN 0 ELSE 1 END,CASE p.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,p.updated_at DESC,p.id",
|
||||
"CASE WHEN p.review_at<=? THEN 0 ELSE 1 END,CASE WHEN p.review_at<=? THEN p.review_at END,CASE WHEN p.status IN ('open','in_review') THEN 0 ELSE 1 END,CASE p.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,p.updated_at DESC,p.id",
|
||||
[now, now]
|
||||
).map((c) => ({
|
||||
...c,
|
||||
|
||||
@@ -235,3 +235,42 @@ export function migrateModerationCaseReferenceIndexes(handle: Database.Database)
|
||||
ON moderation_case_notifications(case_id);
|
||||
`);
|
||||
}
|
||||
|
||||
/** Normalize lifecycle state while retaining every prior decision and child record. */
|
||||
export function migrateModerationCaseStates(handle: Database.Database): void {
|
||||
if (handle.pragma('foreign_keys', { simple: true }))
|
||||
throw new Error('The case status migration requires foreign keys to be disabled');
|
||||
const objects = handle
|
||||
.prepare(
|
||||
`SELECT sql FROM sqlite_master
|
||||
WHERE tbl_name='moderation_cases' AND type IN ('index','trigger') AND sql IS NOT NULL`
|
||||
)
|
||||
.all() as { sql: string }[];
|
||||
// Capture old statuses even for imported cases without a corresponding decision event.
|
||||
handle.exec(`INSERT INTO moderation_events(game,guid,actor,action,subject_id,detail,created_at)
|
||||
SELECT game,guid,'Maxis','case.status_migrated',id,
|
||||
json_object('from',status,'to',CASE WHEN status='escalated' THEN 'in_review' ELSE 'closed' END),
|
||||
strftime('%Y-%m-%dT%H:%M:%fZ','now') FROM moderation_cases
|
||||
WHERE status NOT IN ('open','in_review');
|
||||
CREATE TABLE moderation_cases_state (
|
||||
id TEXT PRIMARY KEY, game TEXT NOT NULL, guid TEXT NOT NULL, title TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK(status IN ('open','in_review','closed')),
|
||||
actor_id TEXT NOT NULL, actor TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,
|
||||
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('low','normal','high','urgent')),
|
||||
assignee_id TEXT REFERENCES auth_users(id) ON DELETE SET NULL,
|
||||
review_at TEXT, review_reason TEXT NOT NULL DEFAULT '',
|
||||
review_sanction_id TEXT REFERENCES moderation_sanctions(id) ON DELETE SET NULL
|
||||
);
|
||||
INSERT INTO moderation_cases_state
|
||||
SELECT id,game,guid,title,
|
||||
CASE WHEN status IN ('open','in_review') THEN status
|
||||
WHEN status='escalated' THEN 'in_review' ELSE 'closed' END,
|
||||
actor_id,actor,created_at,updated_at,
|
||||
revision+CASE WHEN status IN ('open','in_review') THEN 0 ELSE 1 END,
|
||||
priority,assignee_id,review_at,review_reason,review_sanction_id FROM moderation_cases;
|
||||
DROP TABLE moderation_cases;
|
||||
ALTER TABLE moderation_cases_state RENAME TO moderation_cases;
|
||||
`);
|
||||
for (const object of objects) handle.exec(object.sql);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user