Rename space -> folder

This commit is contained in:
Eike Kettner
2020-07-11 11:38:57 +02:00
parent 0365c1980a
commit 2ab0b5e222
22 changed files with 755 additions and 754 deletions

View File

@ -1,4 +1,4 @@
CREATE TABLE "space" (
CREATE TABLE "folder" (
"id" varchar(254) not null primary key,
"name" varchar(254) not null,
"cid" varchar(254) not null,
@ -9,15 +9,15 @@ CREATE TABLE "space" (
foreign key ("owner") references "user_"("uid")
);
CREATE TABLE "space_member" (
CREATE TABLE "folder_member" (
"id" varchar(254) not null primary key,
"space_id" varchar(254) not null,
"folder_id" varchar(254) not null,
"user_id" varchar(254) not null,
"created" timestamp not null,
unique ("space_id", "user_id"),
foreign key ("space_id") references "space"("id"),
unique ("folder_id", "user_id"),
foreign key ("folder_id") references "folder"("id"),
foreign key ("user_id") references "user_"("uid")
);
ALTER TABLE "item"
ADD COLUMN "space_id" varchar(254) NULL;
ADD COLUMN "folder_id" varchar(254) NULL;

View File

@ -0,0 +1,249 @@
package docspell.store.queries
import cats.data.OptionT
import cats.implicits._
import docspell.common._
import docspell.store.impl.Implicits._
import docspell.store.records._
import doobie._
import doobie.implicits._
object QFolder {
final case class FolderItem(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int
) {
def withMembers(members: List[IdRef]): FolderDetail =
FolderDetail(id, name, owner, created, member, memberCount, members)
}
final case class FolderDetail(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int,
members: List[IdRef]
)
sealed trait FolderChangeResult
object FolderChangeResult {
case object Success extends FolderChangeResult
def success: FolderChangeResult = Success
case object NotFound extends FolderChangeResult
def notFound: FolderChangeResult = NotFound
case object Forbidden extends FolderChangeResult
def forbidden: FolderChangeResult = Forbidden
case object Exists extends FolderChangeResult
def exists: FolderChangeResult = Exists
}
def delete(id: Ident, account: AccountId): ConnectionIO[FolderChangeResult] = {
def tryDelete =
for {
_ <- RItem.removeFolder(id)
_ <- RFolderMember.deleteAll(id)
_ <- RFolder.delete(id)
} yield FolderChangeResult.success
(for {
uid <- OptionT(findUserId(account))
folder <- OptionT(RFolder.findById(id))
res <- OptionT.liftF(
if (folder.owner == uid) tryDelete
else FolderChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(FolderChangeResult.notFound)
}
def changeName(
folder: Ident,
account: AccountId,
name: String
): ConnectionIO[FolderChangeResult] = {
def tryUpdate(ns: RFolder): ConnectionIO[FolderChangeResult] =
for {
n <- RFolder.update(ns)
res =
if (n == 0) FolderChangeResult.notFound
else FolderChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
folder <- OptionT(RFolder.findById(folder))
res <- OptionT.liftF(
if (folder.owner == uid) tryUpdate(folder.copy(name = name))
else FolderChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(FolderChangeResult.notFound)
}
def removeMember(
folder: Ident,
account: AccountId,
member: Ident
): ConnectionIO[FolderChangeResult] = {
def tryRemove: ConnectionIO[FolderChangeResult] =
for {
n <- RFolderMember.delete(member, folder)
res =
if (n == 0) FolderChangeResult.notFound
else FolderChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
folder <- OptionT(RFolder.findById(folder))
res <- OptionT.liftF(
if (folder.owner == uid) tryRemove
else FolderChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(FolderChangeResult.notFound)
}
def addMember(
folder: Ident,
account: AccountId,
member: Ident
): ConnectionIO[FolderChangeResult] = {
def tryAdd: ConnectionIO[FolderChangeResult] =
for {
spm <- RFolderMember.findByUserId(member, folder)
mem <- RFolderMember.newMember[ConnectionIO](folder, member)
res <-
if (spm.isDefined) FolderChangeResult.exists.pure[ConnectionIO]
else RFolderMember.insert(mem).map(_ => FolderChangeResult.Success)
} yield res
(for {
uid <- OptionT(findUserId(account))
folder <- OptionT(RFolder.findById(folder))
res <- OptionT.liftF(
if (folder.owner == uid) tryAdd
else FolderChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(FolderChangeResult.notFound)
}
def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = {
val mUserId = RFolderMember.Columns.user.prefix("m")
val mFolderId = RFolderMember.Columns.folder.prefix("m")
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sColl = RFolder.Columns.collective.prefix("s")
val sId = RFolder.Columns.id.prefix("s")
val from = RFolderMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
RFolder.table ++ fr"s ON" ++ mFolderId.is(sId)
val memberQ = selectSimple(
Seq(uId, uLogin),
from,
and(mFolderId.is(id), sColl.is(account.collective))
).query[IdRef].to[Vector]
(for {
folder <- OptionT(findAll(account, Some(id), None, None).map(_.headOption))
memb <- OptionT.liftF(memberQ)
} yield folder.withMembers(memb.toList)).value
}
def findAll(
account: AccountId,
idQ: Option[Ident],
ownerLogin: Option[Ident],
nameQ: Option[String]
): ConnectionIO[Vector[FolderItem]] = {
// with memberlogin as
// (select m.folder_id,u.login
// from folder_member m
// inner join user_ u on u.uid = m.user_id
// inner join folder s on s.id = m.folder_id
// where s.cid = 'eike'
// union all
// select s.id,u.login
// from folder s
// inner join user_ u on u.uid = s.owner
// where s.cid = 'eike')
// select s.id
// ,s.name
// ,s.owner
// ,u.login
// ,s.created
// ,(select count(*) > 0 from memberlogin where folder_id = s.id and login = 'eike') as member
// ,(select count(*) - 1 from memberlogin where folder_id = s.id) as member_count
// from folder s
// inner join user_ u on u.uid = s.owner
// where s.cid = 'eike';
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sId = RFolder.Columns.id.prefix("s")
val sOwner = RFolder.Columns.owner.prefix("s")
val sName = RFolder.Columns.name.prefix("s")
val sColl = RFolder.Columns.collective.prefix("s")
val mUser = RFolderMember.Columns.user.prefix("m")
val mFolder = RFolderMember.Columns.folder.prefix("m")
//CTE
val cte: Fragment = {
val from1 = RFolderMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
RFolder.table ++ fr"s ON" ++ sId.is(mFolder)
val from2 = RFolder.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
withCTE(
"memberlogin" ->
(selectSimple(Seq(mFolder, uLogin), from1, sColl.is(account.collective)) ++
fr"UNION ALL" ++
selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective)))
)
}
val isMember =
fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) ++
fr"AND" ++ uLogin.prefix("").is(account.user)
val memberCount =
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId)
//Query
val cols = Seq(
sId.f,
sName.f,
sOwner.f,
uLogin.f,
RFolder.Columns.created.prefix("s").f,
fr"(" ++ isMember ++ fr") as mem",
fr"(" ++ memberCount ++ fr") as cnt"
)
val from = RFolder.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
val where =
sColl.is(account.collective) :: idQ.toList
.map(id => sId.is(id)) ::: nameQ.toList.map(q =>
sName.lowerLike(s"%${q.toLowerCase}%")
) ::: ownerLogin.toList.map(login => uLogin.is(login))
(cte ++ selectSimple(commas(cols), from, and(where) ++ orderBy(sName.asc)))
.query[FolderItem]
.to[Vector]
}
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
RUser.findByAccount(account).map(_.map(_.uid))
}

View File

@ -1,249 +0,0 @@
package docspell.store.queries
import cats.data.OptionT
import cats.implicits._
import docspell.common._
import docspell.store.impl.Implicits._
import docspell.store.records._
import doobie._
import doobie.implicits._
object QSpace {
final case class SpaceItem(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int
) {
def withMembers(members: List[IdRef]): SpaceDetail =
SpaceDetail(id, name, owner, created, member, memberCount, members)
}
final case class SpaceDetail(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
member: Boolean,
memberCount: Int,
members: List[IdRef]
)
sealed trait SpaceChangeResult
object SpaceChangeResult {
case object Success extends SpaceChangeResult
def success: SpaceChangeResult = Success
case object NotFound extends SpaceChangeResult
def notFound: SpaceChangeResult = NotFound
case object Forbidden extends SpaceChangeResult
def forbidden: SpaceChangeResult = Forbidden
case object Exists extends SpaceChangeResult
def exists: SpaceChangeResult = Exists
}
def delete(id: Ident, account: AccountId): ConnectionIO[SpaceChangeResult] = {
def tryDelete =
for {
_ <- RItem.removeSpace(id)
_ <- RSpaceMember.deleteAll(id)
_ <- RSpace.delete(id)
} yield SpaceChangeResult.success
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(id))
res <- OptionT.liftF(
if (space.owner == uid) tryDelete
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def changeName(
space: Ident,
account: AccountId,
name: String
): ConnectionIO[SpaceChangeResult] = {
def tryUpdate(ns: RSpace): ConnectionIO[SpaceChangeResult] =
for {
n <- RSpace.update(ns)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryUpdate(space.copy(name = name))
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def removeMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryRemove: ConnectionIO[SpaceChangeResult] =
for {
n <- RSpaceMember.delete(member, space)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryRemove
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def addMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryAdd: ConnectionIO[SpaceChangeResult] =
for {
spm <- RSpaceMember.findByUserId(member, space)
mem <- RSpaceMember.newMember[ConnectionIO](space, member)
res <-
if (spm.isDefined) SpaceChangeResult.exists.pure[ConnectionIO]
else RSpaceMember.insert(mem).map(_ => SpaceChangeResult.Success)
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryAdd
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def findById(id: Ident, account: AccountId): ConnectionIO[Option[SpaceDetail]] = {
val mUserId = RSpaceMember.Columns.user.prefix("m")
val mSpaceId = RSpaceMember.Columns.space.prefix("m")
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sColl = RSpace.Columns.collective.prefix("s")
val sId = RSpace.Columns.id.prefix("s")
val from = RSpaceMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
RSpace.table ++ fr"s ON" ++ mSpaceId.is(sId)
val memberQ = selectSimple(
Seq(uId, uLogin),
from,
and(mSpaceId.is(id), sColl.is(account.collective))
).query[IdRef].to[Vector]
(for {
space <- OptionT(findAll(account, Some(id), None, None).map(_.headOption))
memb <- OptionT.liftF(memberQ)
} yield space.withMembers(memb.toList)).value
}
def findAll(
account: AccountId,
idQ: Option[Ident],
ownerLogin: Option[Ident],
nameQ: Option[String]
): ConnectionIO[Vector[SpaceItem]] = {
// with memberlogin as
// (select m.space_id,u.login
// from space_member m
// inner join user_ u on u.uid = m.user_id
// inner join space s on s.id = m.space_id
// where s.cid = 'eike'
// union all
// select s.id,u.login
// from space s
// inner join user_ u on u.uid = s.owner
// where s.cid = 'eike')
// select s.id
// ,s.name
// ,s.owner
// ,u.login
// ,s.created
// ,(select count(*) > 0 from memberlogin where space_id = s.id and login = 'eike') as member
// ,(select count(*) - 1 from memberlogin where space_id = s.id) as member_count
// from space s
// inner join user_ u on u.uid = s.owner
// where s.cid = 'eike';
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sId = RSpace.Columns.id.prefix("s")
val sOwner = RSpace.Columns.owner.prefix("s")
val sName = RSpace.Columns.name.prefix("s")
val sColl = RSpace.Columns.collective.prefix("s")
val mUser = RSpaceMember.Columns.user.prefix("m")
val mSpace = RSpaceMember.Columns.space.prefix("m")
//CTE
val cte: Fragment = {
val from1 = RSpaceMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
RSpace.table ++ fr"s ON" ++ sId.is(mSpace)
val from2 = RSpace.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
withCTE(
"memberlogin" ->
(selectSimple(Seq(mSpace, uLogin), from1, sColl.is(account.collective)) ++
fr"UNION ALL" ++
selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective)))
)
}
val isMember =
fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mSpace.prefix("").is(sId) ++
fr"AND" ++ uLogin.prefix("").is(account.user)
val memberCount =
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mSpace.prefix("").is(sId)
//Query
val cols = Seq(
sId.f,
sName.f,
sOwner.f,
uLogin.f,
RSpace.Columns.created.prefix("s").f,
fr"(" ++ isMember ++ fr") as mem",
fr"(" ++ memberCount ++ fr") as cnt"
)
val from = RSpace.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
val where =
sColl.is(account.collective) :: idQ.toList
.map(id => sId.is(id)) ::: nameQ.toList.map(q =>
sName.lowerLike(s"%${q.toLowerCase}%")
) ::: ownerLogin.toList.map(login => uLogin.is(login))
(cte ++ selectSimple(commas(cols), from, and(where) ++ orderBy(sName.asc)))
.query[SpaceItem]
.to[Vector]
}
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
RUser.findByAccount(account).map(_.map(_.uid))
}

View File

@ -9,7 +9,7 @@ import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RSpace(
case class RFolder(
id: Ident,
name: String,
collectiveId: Ident,
@ -17,15 +17,15 @@ case class RSpace(
created: Timestamp
)
object RSpace {
object RFolder {
def newSpace[F[_]: Sync](name: String, account: AccountId): F[RSpace] =
def newFolder[F[_]: Sync](name: String, account: AccountId): F[RFolder] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RSpace(nId, name, account.collective, account.user, now)
} yield RFolder(nId, name, account.collective, account.user, now)
val table = fr"space"
val table = fr"folder"
object Columns {
@ -40,7 +40,7 @@ object RSpace {
import Columns._
def insert(value: RSpace): ConnectionIO[Int] = {
def insert(value: RFolder): ConnectionIO[Int] = {
val sql = insertRow(
table,
all,
@ -49,37 +49,37 @@ object RSpace {
sql.update.run
}
def update(v: RSpace): ConnectionIO[Int] =
def update(v: RFolder): ConnectionIO[Int] =
updateRow(
table,
and(id.is(v.id), collective.is(v.collectiveId), owner.is(v.owner)),
name.setTo(v.name)
).update.run
def existsByName(coll: Ident, spaceName: String): ConnectionIO[Boolean] =
selectCount(id, table, and(collective.is(coll), name.is(spaceName)))
def existsByName(coll: Ident, folderName: String): ConnectionIO[Boolean] =
selectCount(id, table, and(collective.is(coll), name.is(folderName)))
.query[Int]
.unique
.map(_ > 0)
def findById(spaceId: Ident): ConnectionIO[Option[RSpace]] = {
val sql = selectSimple(all, table, id.is(spaceId))
sql.query[RSpace].option
def findById(folderId: Ident): ConnectionIO[Option[RFolder]] = {
val sql = selectSimple(all, table, id.is(folderId))
sql.query[RFolder].option
}
def findAll(
coll: Ident,
nameQ: Option[String],
order: Columns.type => Column
): ConnectionIO[Vector[RSpace]] = {
): ConnectionIO[Vector[RFolder]] = {
val q = Seq(collective.is(coll)) ++ (nameQ match {
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
case None => Seq.empty
})
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
sql.query[RSpace].to[Vector]
sql.query[RFolder].to[Vector]
}
def delete(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(spaceId)).update.run
def delete(folderId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(folderId)).update.run
}

View File

@ -0,0 +1,61 @@
package docspell.store.records
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RFolderMember(
id: Ident,
folderId: Ident,
userId: Ident,
created: Timestamp
)
object RFolderMember {
def newMember[F[_]: Sync](folder: Ident, user: Ident): F[RFolderMember] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RFolderMember(nId, folder, user, now)
val table = fr"folder_member"
object Columns {
val id = Column("id")
val folder = Column("folder_id")
val user = Column("user_id")
val created = Column("created")
val all = List(id, folder, user, created)
}
import Columns._
def insert(value: RFolderMember): ConnectionIO[Int] = {
val sql = insertRow(
table,
all,
fr"${value.id},${value.folderId},${value.userId},${value.created}"
)
sql.update.run
}
def findByUserId(userId: Ident, folderId: Ident): ConnectionIO[Option[RFolderMember]] =
selectSimple(all, table, and(folder.is(folderId), user.is(userId)))
.query[RFolderMember]
.option
def delete(userId: Ident, folderId: Ident): ConnectionIO[Int] =
deleteFrom(table, and(folder.is(folderId), user.is(userId))).update.run
def deleteAll(folderId: Ident): ConnectionIO[Int] =
deleteFrom(table, folder.is(folderId)).update.run
}

View File

@ -28,7 +28,7 @@ case class RItem(
created: Timestamp,
updated: Timestamp,
notes: Option[String],
spaceId: Option[Ident]
folderId: Option[Ident]
) {}
object RItem {
@ -82,7 +82,7 @@ object RItem {
val created = Column("created")
val updated = Column("updated")
val notes = Column("notes")
val space = Column("space_id")
val folder = Column("folder_id")
val all = List(
id,
cid,
@ -100,7 +100,7 @@ object RItem {
created,
updated,
notes,
space
folder
)
}
import Columns._
@ -111,7 +111,7 @@ object RItem {
all,
fr"${v.id},${v.cid},${v.name},${v.itemDate},${v.source},${v.direction},${v.state}," ++
fr"${v.corrOrg},${v.corrPerson},${v.concPerson},${v.concEquipment},${v.inReplyTo},${v.dueDate}," ++
fr"${v.created},${v.updated},${v.notes},${v.spaceId}"
fr"${v.created},${v.updated},${v.notes},${v.folderId}"
).update.run
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
@ -300,8 +300,8 @@ object RItem {
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option
def removeSpace(spaceId: Ident): ConnectionIO[Int] = {
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
val empty: Option[Ident] = None
updateRow(table, space.is(spaceId), space.setTo(empty)).update.run
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
}
}

View File

@ -1,61 +0,0 @@
package docspell.store.records
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RSpaceMember(
id: Ident,
spaceId: Ident,
userId: Ident,
created: Timestamp
)
object RSpaceMember {
def newMember[F[_]: Sync](space: Ident, user: Ident): F[RSpaceMember] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RSpaceMember(nId, space, user, now)
val table = fr"space_member"
object Columns {
val id = Column("id")
val space = Column("space_id")
val user = Column("user_id")
val created = Column("created")
val all = List(id, space, user, created)
}
import Columns._
def insert(value: RSpaceMember): ConnectionIO[Int] = {
val sql = insertRow(
table,
all,
fr"${value.id},${value.spaceId},${value.userId},${value.created}"
)
sql.update.run
}
def findByUserId(userId: Ident, spaceId: Ident): ConnectionIO[Option[RSpaceMember]] =
selectSimple(all, table, and(space.is(spaceId), user.is(userId)))
.query[RSpaceMember]
.option
def delete(userId: Ident, spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, and(space.is(spaceId), user.is(userId))).update.run
def deleteAll(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, space.is(spaceId)).update.run
}