Remove unused revision repository
This commit is contained in:
@@ -26,6 +26,8 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
|||||||
`src/db/repositories/project-command-transaction.persistence.ts` for the
|
`src/db/repositories/project-command-transaction.persistence.ts` for the
|
||||||
atomic domain-write, revision and history transition boundary. Its applied
|
atomic domain-write, revision and history transition boundary. Its applied
|
||||||
forward-command variant preserves derived CircuitDeviceRow override metadata.
|
forward-command variant preserves derived CircuitDeviceRow override metadata.
|
||||||
|
- Low-level `appendProjectRevision` persistence is adapter-internal and tested
|
||||||
|
directly; do not reintroduce a standalone runtime revision repository.
|
||||||
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
||||||
repositories expose only active reads. Runtime writes belong in typed command
|
repositories expose only active reads. Runtime writes belong in typed command
|
||||||
repositories; direct integration fixtures belong under `tests/support`.
|
repositories; direct integration fixtures belong under `tests/support`.
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ aus. Für CircuitDeviceRow-Feldänderungen übernimmt eine typisierte Variante d
|
|||||||
während der Fachänderung um Override-Metadaten ergänzten Forward-Command. Die
|
während der Fachänderung um Override-Metadaten ergänzten Forward-Command. Die
|
||||||
fachlichen Validierungs-, Forward- und Inversenregeln bleiben im jeweiligen
|
fachlichen Validierungs-, Forward- und Inversenregeln bleiben im jeweiligen
|
||||||
Store.
|
Store.
|
||||||
|
Die Low-Level-Funktion `appendProjectRevision` bleibt ein internes Detail dieser
|
||||||
|
Persistenzgrenze und wird direkt mit einer realen SQLite-Transaktion getestet.
|
||||||
|
Ein eigenständiges Runtime-Revisions-Repository existiert nicht.
|
||||||
`circuit-device-row.insert` und `circuit-device-row.delete` sind atomare
|
`circuit-device-row.insert` und `circuit-device-row.delete` sind atomare
|
||||||
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
Strukturkommandos. Beim Löschen wird die vollständige Zeile im inversen
|
||||||
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
Kommando gesichert, sodass Undo dieselbe UUID und alle Fachwerte wiederherstellt.
|
||||||
|
|||||||
@@ -350,6 +350,8 @@ Implemented foundation:
|
|||||||
restore, share one tested transaction wrapper for domain mutation, revision
|
restore, share one tested transaction wrapper for domain mutation, revision
|
||||||
append and history transition; its applied-forward variant retains derived
|
append and history transition; its applied-forward variant retains derived
|
||||||
CircuitDeviceRow override metadata
|
CircuitDeviceRow override metadata
|
||||||
|
- low-level revision append persistence is tested directly; the unused
|
||||||
|
standalone runtime revision repository and store interface are removed
|
||||||
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover
|
- the legacy consumer UI and application read/write endpoints are removed after verified data cutover
|
||||||
- retained legacy rows are accessible only through explicit database upgrade tooling
|
- retained legacy rows are accessible only through explicit database upgrade tooling
|
||||||
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
|
- project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.repository.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:backup": "tsx scripts/db-backup.ts",
|
"db:backup": "tsx scripts/db-backup.ts",
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { ProjectRevisionConflictError } from "../../domain/errors/project-revision-conflict.error.js";
|
|
||||||
import type {
|
|
||||||
AppendProjectRevisionInput,
|
|
||||||
AppendedProjectRevision,
|
|
||||||
ProjectRevisionStore,
|
|
||||||
} from "../../domain/ports/project-revision.store.js";
|
|
||||||
import type { AppDatabase } from "../database-context.js";
|
|
||||||
import { projects } from "../schema/projects.js";
|
|
||||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
|
||||||
|
|
||||||
export { ProjectRevisionConflictError };
|
|
||||||
|
|
||||||
export class ProjectRevisionRepository implements ProjectRevisionStore {
|
|
||||||
constructor(private readonly database: AppDatabase) {}
|
|
||||||
|
|
||||||
getCurrentRevision(projectId: string) {
|
|
||||||
const project = this.database
|
|
||||||
.select({ currentRevision: projects.currentRevision })
|
|
||||||
.from(projects)
|
|
||||||
.where(eq(projects.id, projectId))
|
|
||||||
.get();
|
|
||||||
return project?.currentRevision ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
append(input: AppendProjectRevisionInput): AppendedProjectRevision {
|
|
||||||
return this.database.transaction((tx) =>
|
|
||||||
appendProjectRevision(tx, input)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,8 +24,3 @@ export interface AppendedProjectRevision {
|
|||||||
revisionNumber: number;
|
revisionNumber: number;
|
||||||
createdAtIso: string;
|
createdAtIso: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectRevisionStore {
|
|
||||||
getCurrentRevision(projectId: string): number | null;
|
|
||||||
append(input: AppendProjectRevisionInput): AppendedProjectRevision;
|
|
||||||
}
|
|
||||||
|
|||||||
+33
-29
@@ -5,12 +5,11 @@ import { eq } from "drizzle-orm";
|
|||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
import {
|
import {
|
||||||
createDatabaseContext,
|
createDatabaseContext,
|
||||||
|
type AppDatabase,
|
||||||
type DatabaseContext,
|
type DatabaseContext,
|
||||||
} from "../src/db/database-context.js";
|
} from "../src/db/database-context.js";
|
||||||
import {
|
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
|
||||||
ProjectRevisionConflictError,
|
import { appendProjectRevision } from "../src/db/repositories/project-revision.persistence.js";
|
||||||
ProjectRevisionRepository,
|
|
||||||
} from "../src/db/repositories/project-revision.repository.js";
|
|
||||||
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
|
||||||
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
import { projectChangeSets } from "../src/db/schema/project-change-sets.js";
|
||||||
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
import { projectRevisions } from "../src/db/schema/project-revisions.js";
|
||||||
@@ -27,10 +26,11 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendTestRevision(
|
function appendTestRevision(
|
||||||
repository: ProjectRevisionRepository,
|
database: AppDatabase,
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return repository.append({
|
return database.transaction((tx) =>
|
||||||
|
appendProjectRevision(tx, {
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
expectedRevision,
|
expectedRevision,
|
||||||
source: "user",
|
source: "user",
|
||||||
@@ -46,21 +46,30 @@ function appendTestRevision(
|
|||||||
type: "circuit.update",
|
type: "circuit.update",
|
||||||
payload: { circuitId: "circuit-1", displayName: "Alt" },
|
payload: { circuitId: "circuit-1", displayName: "Alt" },
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("project revision repository", () => {
|
function getCurrentRevision(database: AppDatabase, projectId: string) {
|
||||||
|
return (
|
||||||
|
database
|
||||||
|
.select({ currentRevision: projects.currentRevision })
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, projectId))
|
||||||
|
.get()?.currentRevision ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("project revision persistence", () => {
|
||||||
it("appends immutable revision metadata and payloads with a monotonic number", () => {
|
it("appends immutable revision metadata and payloads with a monotonic number", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 0);
|
||||||
|
const appended = appendTestRevision(context.db, 0);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 0);
|
|
||||||
const appended = appendTestRevision(repository, 0);
|
|
||||||
|
|
||||||
assert.equal(appended.projectId, "project-1");
|
assert.equal(appended.projectId, "project-1");
|
||||||
assert.equal(appended.revisionNumber, 1);
|
assert.equal(appended.revisionNumber, 1);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 1);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 1);
|
||||||
|
|
||||||
const revision = context.db
|
const revision = context.db
|
||||||
.select()
|
.select()
|
||||||
@@ -105,9 +114,9 @@ describe("project revision repository", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const next = appendTestRevision(repository, 1);
|
const next = appendTestRevision(context.db, 1);
|
||||||
assert.equal(next.revisionNumber, 2);
|
assert.equal(next.revisionNumber, 2);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 2);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 2);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
@@ -116,18 +125,17 @@ describe("project revision repository", () => {
|
|||||||
it("rejects a stale expected revision without writing history", () => {
|
it("rejects a stale expected revision without writing history", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
appendTestRevision(context.db, 0);
|
||||||
appendTestRevision(repository, 0);
|
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 0),
|
() => appendTestRevision(context.db, 0),
|
||||||
(error) =>
|
(error) =>
|
||||||
error instanceof ProjectRevisionConflictError &&
|
error instanceof ProjectRevisionConflictError &&
|
||||||
error.expectedRevision === 0 &&
|
error.expectedRevision === 0 &&
|
||||||
error.actualRevision === 1
|
error.actualRevision === 1
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 1);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 1);
|
||||||
assert.equal(context.db.select().from(projectRevisions).all().length, 1);
|
assert.equal(context.db.select().from(projectRevisions).all().length, 1);
|
||||||
assert.equal(context.db.select().from(projectChangeSets).all().length, 1);
|
assert.equal(context.db.select().from(projectChangeSets).all().length, 1);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -145,14 +153,12 @@ describe("project revision repository", () => {
|
|||||||
SELECT RAISE(ABORT, 'forced change-set failure');
|
SELECT RAISE(ABORT, 'forced change-set failure');
|
||||||
END;
|
END;
|
||||||
`);
|
`);
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 0),
|
() => appendTestRevision(context.db, 0),
|
||||||
/forced change-set failure/
|
/forced change-set failure/
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 0);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 0);
|
||||||
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
assert.equal(context.db.select().from(projectRevisions).all().length, 0);
|
||||||
assert.equal(context.db.select().from(projectChangeSets).all().length, 0);
|
assert.equal(context.db.select().from(projectChangeSets).all().length, 0);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -163,7 +169,6 @@ describe("project revision repository", () => {
|
|||||||
it("captures and retains automatic snapshots without deleting named snapshots", () => {
|
it("captures and retains automatic snapshots without deleting named snapshots", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const revisions = new ProjectRevisionRepository(context.db);
|
|
||||||
const snapshots = new ProjectSnapshotRepository(context.db);
|
const snapshots = new ProjectSnapshotRepository(context.db);
|
||||||
const named = snapshots.createNamed({
|
const named = snapshots.createNamed({
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
@@ -177,7 +182,7 @@ describe("project revision repository", () => {
|
|||||||
expectedRevision < 325;
|
expectedRevision < 325;
|
||||||
expectedRevision += 1
|
expectedRevision += 1
|
||||||
) {
|
) {
|
||||||
appendTestRevision(revisions, expectedRevision);
|
appendTestRevision(context.db, expectedRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stored = context.db
|
const stored = context.db
|
||||||
@@ -234,13 +239,12 @@ describe("project revision repository", () => {
|
|||||||
it("rolls back revision 25 when automatic snapshot persistence fails", () => {
|
it("rolls back revision 25 when automatic snapshot persistence fails", () => {
|
||||||
const context = createTestDatabase();
|
const context = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
const repository = new ProjectRevisionRepository(context.db);
|
|
||||||
for (
|
for (
|
||||||
let expectedRevision = 0;
|
let expectedRevision = 0;
|
||||||
expectedRevision < 24;
|
expectedRevision < 24;
|
||||||
expectedRevision += 1
|
expectedRevision += 1
|
||||||
) {
|
) {
|
||||||
appendTestRevision(repository, expectedRevision);
|
appendTestRevision(context.db, expectedRevision);
|
||||||
}
|
}
|
||||||
context.sqlite.exec(`
|
context.sqlite.exec(`
|
||||||
CREATE TRIGGER fail_automatic_project_snapshot
|
CREATE TRIGGER fail_automatic_project_snapshot
|
||||||
@@ -252,10 +256,10 @@ describe("project revision repository", () => {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => appendTestRevision(repository, 24),
|
() => appendTestRevision(context.db, 24),
|
||||||
/forced automatic snapshot failure/
|
/forced automatic snapshot failure/
|
||||||
);
|
);
|
||||||
assert.equal(repository.getCurrentRevision("project-1"), 24);
|
assert.equal(getCurrentRevision(context.db, "project-1"), 24);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
context.db.select().from(projectRevisions).all().length,
|
context.db.select().from(projectRevisions).all().length,
|
||||||
24
|
24
|
||||||
Reference in New Issue
Block a user