diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index df434dc6..da318274 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -795,6 +795,139 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/space: + get: + tags: [ Space ] + summary: Get a list of spaces. + description: | + Return a list of spaces for the current collective. + + All spaces are returned, including those not owned by the + current user. + + It is possible to restrict the results by a substring match of + the name. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/q" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/SpaceList" + post: + tags: [ Space ] + summary: Create a new space + description: | + Create a new space owned by the current user. If a space with + the same name already exists, an error is thrown. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewSpace" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/space/{id}: + get: + tags: [ Space ] + summary: Get space details. + description: | + Return details about a space. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/SpaceDetail" + put: + tags: [ Space ] + summary: Change the name of a space + description: | + Changes the name of a space. The new name must not exists. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NewSpace" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + delete: + tags: [ Space ] + summary: Delete a space by its id. + description: | + Deletes a space. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/space/{id}/member/{userId}: + put: + tags: [ Space ] + summary: Add a member to this space + description: | + Adds a member to this space (identified by `id`). + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/userId" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + delete: + tags: [ Space ] + summary: Removes a member from this space. + description: | + Removes a member from this space. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/userId" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/collective: get: tags: [ Collective ] @@ -2358,6 +2491,80 @@ paths: components: schemas: + SpaceList: + description: | + A list of spaces with their member counts. + required: + - items + properties: + items: + type: array + items: + $ref: "#/components/schemas/SpaceItem" + SpaceItem: + description: | + An item in a space list. + required: + - id + - name + - owner + - created + - members + properties: + id: + type: string + format: ident + name: + type: string + owner: + $ref: "#/components/schemas/IdName" + created: + type: integer + format: date-time + members: + type: integer + format: int32 + NewSpace: + description: | + Data required to create a new space. + required: + - name + properties: + name: + type: string + SpaceDetail: + description: | + Details about a space. + required: + - id + - name + - owner + - created + - members + properties: + id: + type: string + format: ident + name: + type: string + owner: + $ref: "#/components/schemas/IdName" + created: + type: integer + format: date-time + members: + type: array + items: + $ref: "#/components/schemas/IdName" + SpaceMember: + description: | + Information to add or remove a space member. + required: + - userId + properties: + userId: + type: string + format: ident ItemFtsSearch: description: | Query description for a full-text only search. @@ -3739,6 +3946,13 @@ components: required: true schema: type: string + userId: + name: userId + in: path + description: An identifier + required: true + schema: + type: string itemId: name: itemId in: path diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.8.0__spaces.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.8.0__spaces.sql index e3741345..8a43c097 100644 --- a/modules/store/src/main/resources/db/migration/postgresql/V1.8.0__spaces.sql +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.8.0__spaces.sql @@ -19,12 +19,5 @@ CREATE TABLE "space_member" ( foreign key ("user_id") references "user_"("uid") ); -CREATE TABLE "space_item" ( - "id" varchar(254) not null primary key, - "space_id" varchar(254) not null, - "item_id" varchar(254) not null, - "created" timestamp not null, - unique ("space_id", "item_id"), - foreign key ("space_id") references "space"("id"), - foreign key ("item_id") references "item"("itemid") -); +ALTER TABLE "item" +ADD COLUMN "space_id" varchar(254) NULL; diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala index 987a77c0..3e319ffa 100644 --- a/modules/store/src/main/scala/docspell/store/records/RItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala @@ -27,7 +27,8 @@ case class RItem( dueDate: Option[Timestamp], created: Timestamp, updated: Timestamp, - notes: Option[String] + notes: Option[String], + spaceId: Option[Ident] ) {} object RItem { @@ -58,6 +59,7 @@ object RItem { None, now, now, + None, None ) @@ -80,6 +82,7 @@ object RItem { val created = Column("created") val updated = Column("updated") val notes = Column("notes") + val space = Column("space_id") val all = List( id, cid, @@ -96,7 +99,8 @@ object RItem { dueDate, created, updated, - notes + notes, + space ) } import Columns._ @@ -107,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}" + fr"${v.created},${v.updated},${v.notes},${v.spaceId}" ).update.run def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] = diff --git a/modules/store/src/main/scala/docspell/store/records/RSpace.scala b/modules/store/src/main/scala/docspell/store/records/RSpace.scala index d6efe2c6..1da2ef0d 100644 --- a/modules/store/src/main/scala/docspell/store/records/RSpace.scala +++ b/modules/store/src/main/scala/docspell/store/records/RSpace.scala @@ -1,5 +1,7 @@ package docspell.store.records +import cats.effect._ +import cats.implicits._ import docspell.common._ import docspell.store.impl.Column import docspell.store.impl.Implicits._ @@ -17,6 +19,12 @@ case class RSpace( object RSpace { + def newSpace[F[_]: Sync](name: String, account: AccountId): F[RSpace] = + for { + nId <- Ident.randomId[F] + now <- Timestamp.current[F] + } yield RSpace(nId, name, account.collective, account.user, now) + val table = fr"space" object Columns { @@ -41,4 +49,35 @@ object RSpace { sql.update.run } + def update(v: RSpace): 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))) + .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 findAll( + coll: Ident, + nameQ: Option[String], + order: Columns.type => Column + ): ConnectionIO[Vector[RSpace]] = { + 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] + } + } diff --git a/modules/store/src/main/scala/docspell/store/records/RSpaceItem.scala b/modules/store/src/main/scala/docspell/store/records/RSpaceItem.scala deleted file mode 100644 index ab7187fd..00000000 --- a/modules/store/src/main/scala/docspell/store/records/RSpaceItem.scala +++ /dev/null @@ -1,42 +0,0 @@ -package docspell.store.records - -import docspell.common._ -import docspell.store.impl.Column -import docspell.store.impl.Implicits._ - -import doobie._ -import doobie.implicits._ - -case class RSpaceItem( - id: Ident, - spaceId: Ident, - itemId: Ident, - created: Timestamp -) - -object RSpaceItem { - - val table = fr"space" - - object Columns { - - val id = Column("id") - val space = Column("space_id") - val item = Column("user_id") - val created = Column("created") - - val all = List(id, space, user, created) - } - - import Columns._ - - def insert(value: RSpaceItem): ConnectionIO[Int] = { - val sql = insertRow( - table, - all, - fr"${value.id},${value.spaceId},${value.itemId},${value.created}" - ) - sql.update.run - } - -}