mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
[CLI] Prisma migration warning improvements (#682)
This commit is contained in:
parent
1a7a04b87c
commit
40041ff42b
@ -6,7 +6,7 @@ waspMigrate/.wasp/out/db/migrations/no-date-foo/migration.sql
|
|||||||
waspMigrate/.wasp/out/db/package.json
|
waspMigrate/.wasp/out/db/package.json
|
||||||
waspMigrate/.wasp/out/db/schema.prisma
|
waspMigrate/.wasp/out/db/schema.prisma
|
||||||
waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum
|
waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum
|
||||||
waspMigrate/.wasp/out/db/schema.prisma.wasp-migrate-checksum
|
waspMigrate/.wasp/out/db/schema.prisma.wasp-last-db-concurrence-checksum
|
||||||
waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
|
waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
|
||||||
waspMigrate/.wasp/out/server/.npmrc
|
waspMigrate/.wasp/out/server/.npmrc
|
||||||
waspMigrate/.wasp/out/server/README.md
|
waspMigrate/.wasp/out/server/README.md
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Wasp.Generator.DbGenerator
|
module Wasp.Generator.DbGenerator
|
||||||
( genDb,
|
( genDb,
|
||||||
warnIfDbSchemaChangedSinceLastMigration,
|
warnIfDbNeedsMigration,
|
||||||
genPrismaClient,
|
genPrismaClient,
|
||||||
postWriteDbGeneratorActions,
|
postWriteDbGeneratorActions,
|
||||||
)
|
)
|
||||||
@ -23,8 +23,8 @@ import Wasp.Generator.Common (ProjectRootDir)
|
|||||||
import Wasp.Generator.DbGenerator.Common
|
import Wasp.Generator.DbGenerator.Common
|
||||||
( dbMigrationsDirInDbRootDir,
|
( dbMigrationsDirInDbRootDir,
|
||||||
dbRootDirInProjectRootDir,
|
dbRootDirInProjectRootDir,
|
||||||
|
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
|
||||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||||
dbSchemaChecksumOnLastMigrateFileProjectRootDir,
|
|
||||||
dbSchemaFileInDbTemplatesDir,
|
dbSchemaFileInDbTemplatesDir,
|
||||||
dbSchemaFileInProjectRootDir,
|
dbSchemaFileInProjectRootDir,
|
||||||
dbTemplatesDirInTemplatesDir,
|
dbTemplatesDirInTemplatesDir,
|
||||||
@ -83,42 +83,70 @@ genMigrationsDir spec =
|
|||||||
-- | This function operates on generated code, and thus assumes the file drafts were written to disk
|
-- | This function operates on generated code, and thus assumes the file drafts were written to disk
|
||||||
postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError])
|
postWriteDbGeneratorActions :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO ([GeneratorWarning], [GeneratorError])
|
||||||
postWriteDbGeneratorActions spec dstDir = do
|
postWriteDbGeneratorActions spec dstDir = do
|
||||||
dbGeneratorWarnings <- maybeToList <$> warnIfDbSchemaChangedSinceLastMigration spec dstDir
|
dbGeneratorWarnings <- maybeToList <$> warnIfDbNeedsMigration spec dstDir
|
||||||
dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir
|
dbGeneratorErrors <- maybeToList <$> genPrismaClient spec dstDir
|
||||||
return (dbGeneratorWarnings, dbGeneratorErrors)
|
return (dbGeneratorWarnings, dbGeneratorErrors)
|
||||||
|
|
||||||
-- | Checks if user needs to run `wasp db migrate-dev` due to changes they did in schema.prisma, and if so, returns a warning.
|
-- | Checks if user needs to run `wasp db migrate-dev` due to changes in schema.prisma, and if so, returns a warning.
|
||||||
-- When doing this, it looks at schema.prisma in the generated project.
|
-- When doing this, it looks at schema.prisma in the generated project.
|
||||||
--
|
--
|
||||||
-- This function makes following assumptions:
|
-- This function makes following assumptions:
|
||||||
-- - schema.prisma will exist in the generated project even if no Entities were defined.
|
-- - schema.prisma will exist in the generated project even if no Entities were defined.
|
||||||
-- Due to how Prisma itself works, this assumption is currently fulfilled.
|
-- Due to how Prisma itself works, this assumption is currently fulfilled.
|
||||||
-- - schema.prisma.wasp-checksum contains the checksum of the schema.prisma as it was during the last `wasp db migrate-dev`.
|
-- - schema.prisma.wasp-last-db-concurrence-checksum contains the checksum of the schema.prisma as it was when we last ensured it matched the DB.
|
||||||
--
|
--
|
||||||
-- Given that, there are two cases in which we wish to warn the user to run `wasp db migrate-dev`:
|
-- Given that, there are two cases in which we wish to warn the user to run `wasp db migrate-dev`:
|
||||||
-- (1) If schema.prisma.wasp-checksum exists, but is not equal to checksum(schema.prisma), we know they made changes to schema.prisma and should migrate.
|
-- (1) If schema.prisma.wasp-last-db-concurrence-checksum exists, but is not equal to checksum(schema.prisma), we know there were changes to schema.prisma and they should migrate.
|
||||||
-- (2) If schema.prisma.wasp-checksum does not exist, but the user has entities defined in schema.prisma (and thus, AppSpec).
|
-- (2) If schema.prisma.wasp-last-db-concurrence-checksum does not exist, but the user has entities defined in schema.prisma (and thus, AppSpec).
|
||||||
-- This could imply they have never migrated locally, or that they have but are simply missing their generated project dir.
|
-- This could imply they have never migrated locally, or that they have but are simply missing their generated project dir.
|
||||||
-- Common scenarios for the second warning include:
|
-- Common scenarios for the second warning include:
|
||||||
-- - After a fresh checkout, or after `wasp clean`; possible false positives in these cases, but for safety, it's still preferable to warn.
|
-- - After a fresh checkout, or after `wasp clean`.
|
||||||
-- - When they previously had no entities and just added their first.
|
-- - When they previously had no entities and just added their first.
|
||||||
warnIfDbSchemaChangedSinceLastMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
|
-- In either of those scenarios, validate against DB itself to avoid redundant warnings.
|
||||||
warnIfDbSchemaChangedSinceLastMigration spec projectRootDir = do
|
--
|
||||||
|
-- NOTE: As one final optimization, if they do not have a schema.prisma.wasp-last-db-concurrence-checksum but the schema is
|
||||||
|
-- in sync with the databse, we generate that file to avoid future checks.
|
||||||
|
--
|
||||||
|
-- NOTE: Because we currently only allow devs to migrate-dev, we only compare the schema to the DB since
|
||||||
|
-- there are no likely scenarios where schema == db but schema != migrations dir. In the future, as we add more DB commands,
|
||||||
|
-- we may wish to also compare the migrations dir to the DB as well.
|
||||||
|
warnIfDbNeedsMigration :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
|
||||||
|
warnIfDbNeedsMigration spec projectRootDir = do
|
||||||
dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp
|
dbSchemaChecksumFileExists <- doesFileExist dbSchemaChecksumFp
|
||||||
|
|
||||||
if dbSchemaChecksumFileExists
|
if dbSchemaChecksumFileExists
|
||||||
then do
|
then warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp
|
||||||
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp
|
else
|
||||||
dbChecksumFileContents <- readFile dbSchemaChecksumFp
|
if entitiesExist
|
||||||
return $ warnIf (dbSchemaFileChecksum /= dbChecksumFileContents) "Your Prisma schema has changed, you should run `wasp db migrate-dev`."
|
then warnIfSchemaDiffersFromDb projectRootDir
|
||||||
else return $ warnIf entitiesExist "Please run `wasp db migrate-dev` to ensure the local project is fully initialized."
|
else return Nothing
|
||||||
where
|
where
|
||||||
dbSchemaFp = SP.fromAbsFile $ projectRootDir </> dbSchemaFileInProjectRootDir
|
dbSchemaFp = SP.fromAbsFile $ projectRootDir </> dbSchemaFileInProjectRootDir
|
||||||
dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir </> dbSchemaChecksumOnLastMigrateFileProjectRootDir
|
dbSchemaChecksumFp = SP.fromAbsFile $ projectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir
|
||||||
entitiesExist = not . null $ getEntities spec
|
entitiesExist = not . null $ getEntities spec
|
||||||
|
|
||||||
warnIf :: Bool -> String -> Maybe GeneratorWarning
|
warnIfSchemaDiffersFromChecksum :: FilePath -> FilePath -> IO (Maybe GeneratorWarning)
|
||||||
warnIf b msg = if b then Just $ GeneratorNeedsMigrationWarning msg else Nothing
|
warnIfSchemaDiffersFromChecksum dbSchemaFp dbSchemaChecksumFp = do
|
||||||
|
dbSchemaFileChecksum <- hexToString <$> checksumFromFilePath dbSchemaFp
|
||||||
|
dbChecksumFileContents <- readFile dbSchemaChecksumFp
|
||||||
|
if dbSchemaFileChecksum /= dbChecksumFileContents
|
||||||
|
then return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema has changed, please run `wasp db migrate-dev` when ready."
|
||||||
|
else return Nothing
|
||||||
|
|
||||||
|
warnIfSchemaDiffersFromDb :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorWarning)
|
||||||
|
warnIfSchemaDiffersFromDb projectRootDir = do
|
||||||
|
-- NOTE: If we wanted to, we could also check that the migrations dir == db,
|
||||||
|
-- but a schema check should handle all most likely cases.
|
||||||
|
schemaMatchesDb <- DbOps.doesSchemaMatchDb projectRootDir
|
||||||
|
case schemaMatchesDb of
|
||||||
|
Just True -> do
|
||||||
|
-- NOTE: Since we know schema == db, writing this file prevents future redundant Prisma checks.
|
||||||
|
DbOps.writeDbSchemaChecksumToFile projectRootDir (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
|
||||||
|
return Nothing
|
||||||
|
Just False -> return . Just $ GeneratorNeedsMigrationWarning "Your Prisma schema does not match your database, please run `wasp db migrate-dev`."
|
||||||
|
-- NOTE: If there was an error, it could mean we could not connect to the SQLite db, since it does not exist.
|
||||||
|
-- Or it could mean their DATABASE_URL is wrong, or database is down, or any other number of causes.
|
||||||
|
-- In any case, migrating will either solve it (in the SQLite case), or allow Prisma to give them enough info to troubleshoot.
|
||||||
|
Nothing -> return . Just $ GeneratorNeedsMigrationWarning "Wasp was unable to verify your database is up to date. Running `wasp db migrate-dev` may fix this and will provide more info."
|
||||||
|
|
||||||
genPrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
|
genPrismaClient :: AppSpec -> Path' Abs (Dir ProjectRootDir) -> IO (Maybe GeneratorError)
|
||||||
genPrismaClient spec projectRootDir = do
|
genPrismaClient spec projectRootDir = do
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
module Wasp.Generator.DbGenerator.Common
|
module Wasp.Generator.DbGenerator.Common
|
||||||
( dbMigrationsDirInDbRootDir,
|
( dbMigrationsDirInDbRootDir,
|
||||||
dbRootDirInProjectRootDir,
|
dbRootDirInProjectRootDir,
|
||||||
dbSchemaChecksumOnLastMigrateFileProjectRootDir,
|
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
|
||||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||||
dbSchemaFileInDbTemplatesDir,
|
dbSchemaFileInDbTemplatesDir,
|
||||||
dbSchemaFileInProjectRootDir,
|
dbSchemaFileInProjectRootDir,
|
||||||
@ -19,10 +19,10 @@ data DbRootDir
|
|||||||
|
|
||||||
data DbTemplatesDir
|
data DbTemplatesDir
|
||||||
|
|
||||||
-- | This file represents the checksum of schema.prisma
|
-- | This file represents the checksum of schema.prisma at the point
|
||||||
-- at the point at which `prisma db migrate-dev` was last run. It is used
|
-- at which we last interacted with the DB to ensure they matched.
|
||||||
-- to help warn the user of instances when they may need to migrate.
|
-- It is used to help warn the user of instances when they may need to migrate.
|
||||||
data DbSchemaChecksumOnLastMigrateFile
|
data DbSchemaChecksumOnLastDbConcurrenceFile
|
||||||
|
|
||||||
-- | This file represents the checksum of schema.prisma
|
-- | This file represents the checksum of schema.prisma
|
||||||
-- at the point at which `prisma generate` was last run. It is used
|
-- at the point at which `prisma generate` was last run. It is used
|
||||||
@ -49,11 +49,11 @@ dbSchemaFileInProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaFileInDbRoo
|
|||||||
dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir)
|
dbMigrationsDirInDbRootDir :: Path' (Rel DbRootDir) (Dir DbMigrationsDir)
|
||||||
dbMigrationsDirInDbRootDir = [reldir|migrations|]
|
dbMigrationsDirInDbRootDir = [reldir|migrations|]
|
||||||
|
|
||||||
dbSchemaChecksumOnLastMigrateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastMigrateFile)
|
dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile)
|
||||||
dbSchemaChecksumOnLastMigrateFileInDbRootDir = [relfile|schema.prisma.wasp-migrate-checksum|]
|
dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir = [relfile|schema.prisma.wasp-last-db-concurrence-checksum|]
|
||||||
|
|
||||||
dbSchemaChecksumOnLastMigrateFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastMigrateFile)
|
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir :: Path' (Rel ProjectRootDir) (File DbSchemaChecksumOnLastDbConcurrenceFile)
|
||||||
dbSchemaChecksumOnLastMigrateFileProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaChecksumOnLastMigrateFileInDbRootDir
|
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir = dbRootDirInProjectRootDir </> dbSchemaChecksumOnLastDbConcurrenceFileInDbRootDir
|
||||||
|
|
||||||
dbSchemaChecksumOnLastGenerateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastGenerateFile)
|
dbSchemaChecksumOnLastGenerateFileInDbRootDir :: Path' (Rel DbRootDir) (File DbSchemaChecksumOnLastGenerateFile)
|
||||||
dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-generate-checksum|]
|
dbSchemaChecksumOnLastGenerateFileInDbRootDir = [relfile|schema.prisma.wasp-generate-checksum|]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
module Wasp.Generator.DbGenerator.Jobs
|
module Wasp.Generator.DbGenerator.Jobs
|
||||||
( migrateDev,
|
( migrateDev,
|
||||||
|
migrateDiff,
|
||||||
generatePrismaClient,
|
generatePrismaClient,
|
||||||
runStudio,
|
runStudio,
|
||||||
)
|
)
|
||||||
@ -49,6 +50,25 @@ migrateDev projectDir maybeMigrationName = do
|
|||||||
|
|
||||||
runNodeCommandAsJob serverDir "script" scriptArgs J.Db
|
runNodeCommandAsJob serverDir "script" scriptArgs J.Db
|
||||||
|
|
||||||
|
-- | Diffs the Prisma schema file against the db.
|
||||||
|
-- Because of the --exit-code flag, it changes the exit code behavior
|
||||||
|
-- to signal if the diff is empty or not (Empty: 0, Error: 1, Not empty: 2)
|
||||||
|
migrateDiff :: Path' Abs (Dir ProjectRootDir) -> J.Job
|
||||||
|
migrateDiff projectDir = do
|
||||||
|
let serverDir = projectDir </> serverRootDirInProjectRootDir
|
||||||
|
let schemaFileFp = SP.toFilePath $ projectDir </> dbSchemaFileInProjectRootDir
|
||||||
|
let prismaMigrateDiffCmdArgs =
|
||||||
|
[ "migrate",
|
||||||
|
"diff",
|
||||||
|
"--from-schema-datamodel",
|
||||||
|
schemaFileFp,
|
||||||
|
"--to-schema-datasource",
|
||||||
|
schemaFileFp,
|
||||||
|
"--exit-code"
|
||||||
|
]
|
||||||
|
|
||||||
|
runNodeCommandAsJob serverDir (absPrismaExecutableFp projectDir) prismaMigrateDiffCmdArgs J.Db
|
||||||
|
|
||||||
-- | Runs `prisma studio` - Prisma's db inspector.
|
-- | Runs `prisma studio` - Prisma's db inspector.
|
||||||
runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job
|
runStudio :: Path' Abs (Dir ProjectRootDir) -> J.Job
|
||||||
runStudio projectDir = do
|
runStudio projectDir = do
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
module Wasp.Generator.DbGenerator.Operations
|
module Wasp.Generator.DbGenerator.Operations
|
||||||
( migrateDevAndCopyToSource,
|
( migrateDevAndCopyToSource,
|
||||||
generatePrismaClient,
|
generatePrismaClient,
|
||||||
|
doesSchemaMatchDb,
|
||||||
|
writeDbSchemaChecksumToFile,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -18,8 +20,8 @@ import Wasp.Generator.Common (ProjectRootDir)
|
|||||||
import Wasp.Generator.DbGenerator.Common
|
import Wasp.Generator.DbGenerator.Common
|
||||||
( dbMigrationsDirInDbRootDir,
|
( dbMigrationsDirInDbRootDir,
|
||||||
dbRootDirInProjectRootDir,
|
dbRootDirInProjectRootDir,
|
||||||
|
dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir,
|
||||||
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
dbSchemaChecksumOnLastGenerateFileProjectRootDir,
|
||||||
dbSchemaChecksumOnLastMigrateFileProjectRootDir,
|
|
||||||
dbSchemaFileInProjectRootDir,
|
dbSchemaFileInProjectRootDir,
|
||||||
)
|
)
|
||||||
import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs
|
import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs
|
||||||
@ -57,7 +59,7 @@ finalizeMigration genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs = do
|
|||||||
-- NOTE: We are updating a managed CopyDirFileDraft outside the normal generation process, so we must invalidate the checksum entry for it.
|
-- NOTE: We are updating a managed CopyDirFileDraft outside the normal generation process, so we must invalidate the checksum entry for it.
|
||||||
Generator.WriteFileDrafts.removeFromChecksumFile genProjectRootDirAbs [Right $ SP.castDir dbMigrationsDirInProjectRootDir]
|
Generator.WriteFileDrafts.removeFromChecksumFile genProjectRootDirAbs [Right $ SP.castDir dbMigrationsDirInProjectRootDir]
|
||||||
res <- copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs
|
res <- copyMigrationsBackToSource genProjectRootDirAbs dbMigrationsDirInWaspProjectDirAbs
|
||||||
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastMigrateFileProjectRootDir)
|
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastDbConcurrenceFileProjectRootDir)
|
||||||
return res
|
return res
|
||||||
where
|
where
|
||||||
dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir
|
dbMigrationsDirInProjectRootDir = dbRootDirInProjectRootDir SP.</> dbMigrationsDirInDbRootDir
|
||||||
@ -96,3 +98,21 @@ generatePrismaClient genProjectRootDirAbs = do
|
|||||||
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir)
|
writeDbSchemaChecksumToFile genProjectRootDirAbs (SP.castFile dbSchemaChecksumOnLastGenerateFileProjectRootDir)
|
||||||
return $ Right ()
|
return $ Right ()
|
||||||
ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code
|
ExitFailure code -> return $ Left $ "Prisma client generation failed with exit code: " ++ show code
|
||||||
|
|
||||||
|
-- | Checks `prisma migrate diff` exit code to determine if schema.prisma is
|
||||||
|
-- different than the DB. Returns Nothing on error as we do not know the current state.
|
||||||
|
-- Returns Just True if schema.prisma is the same as DB, Just False if it is different, and
|
||||||
|
-- Nothing if the check itself failed (exe: if a connection to the DB could not be established).
|
||||||
|
-- NOTE: Here we only compare the schema to the DB, and not the migrations dir.
|
||||||
|
doesSchemaMatchDb :: Path' Abs (Dir ProjectRootDir) -> IO (Maybe Bool)
|
||||||
|
doesSchemaMatchDb genProjectRootDirAbs = do
|
||||||
|
chan <- newChan
|
||||||
|
(_, dbExitCode) <-
|
||||||
|
concurrently
|
||||||
|
(readJobMessagesAndPrintThemPrefixed chan)
|
||||||
|
(DbJobs.migrateDiff genProjectRootDirAbs chan)
|
||||||
|
-- Schema in sync: 0, Error: 1, Schema differs: 2
|
||||||
|
case dbExitCode of
|
||||||
|
ExitSuccess -> return $ Just True
|
||||||
|
ExitFailure 2 -> return $ Just False
|
||||||
|
ExitFailure _ -> return Nothing
|
||||||
|
Loading…
Reference in New Issue
Block a user