add integration with posts creating

This commit is contained in:
2022-01-22 13:30:49 +06:00
parent d1fa0242fa
commit 5e61c2a770
22 changed files with 548 additions and 43 deletions
client
build.gradle
src
commonMain
kotlin
dev
inmo
postssystem
client
features/content
binary
common
src
commonMain
kotlin
dev
server
src
jvmMain
kotlin
dev
inmo
postssystem
features
common
src
commonMain
kotlin
dev
inmo
postssystem
features
content
gradle.properties
server
build.gradle
src
main
java
dev
inmo
postssystem
server
services/posts
client
src
commonMain
common
server

@ -23,6 +23,9 @@ kotlin {
api project(":postssystem.features.content.client") api project(":postssystem.features.content.client")
api project(":postssystem.features.content.text.client") api project(":postssystem.features.content.text.client")
api project(":postssystem.features.content.binary.client") api project(":postssystem.features.content.binary.client")
api project(":postssystem.services.posts.client")
api "dev.inmo:micro_utils.fsm.common:$microutils_version" api "dev.inmo:micro_utils.fsm.common:$microutils_version"
api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version" api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version"
api "dev.inmo:micro_utils.crypto:$microutils_version" api "dev.inmo:micro_utils.crypto:$microutils_version"

@ -25,11 +25,12 @@ import dev.inmo.postssystem.client.settings.auth.AuthSettings
import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings import dev.inmo.postssystem.client.settings.auth.DefaultAuthSettings
import dev.inmo.postssystem.features.common.common.SerializersModuleConfigurator import dev.inmo.postssystem.features.common.common.SerializersModuleConfigurator
import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier import dev.inmo.postssystem.features.common.common.singleWithRandomQualifier
import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.common.OtherContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator import dev.inmo.postssystem.features.content.text.common.TextContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.status.client.StatusFeatureClient import dev.inmo.postssystem.features.status.client.StatusFeatureClient
import dev.inmo.postssystem.services.posts.client.ClientPostsService
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -43,6 +44,7 @@ import org.koin.core.context.startKoin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.core.qualifier.* import org.koin.core.qualifier.*
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.dsl.binds
import org.koin.dsl.module import org.koin.dsl.module
val UIScopeQualifier = StringQualifier("CoroutineScopeUI") val UIScopeQualifier = StringQualifier("CoroutineScopeUI")
@ -69,7 +71,6 @@ fun baseKoin(
module { module {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) } singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) }
@ -133,5 +134,9 @@ fun getAuthorizedFeaturesDIModule(
single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) } single<ReadFilesStorage> { ClientReadFilesStorage(get(serverUrlQualifier), get(), get()) }
single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) } single<ReadUsersStorage> { UsersStorageKtorClient(get(serverUrlQualifier), get()) }
single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) } single<RolesStorage<Role>> { ClientRolesStorage(get(serverUrlQualifier), get(), Role.serializer()) }
single<PostsService> { ClientPostsService(get(serverUrlQualifier), get()) } binds arrayOf(
ReadPostsService::class,
WritePostsService::class
)
} }
} }

@ -1,11 +0,0 @@
package dev.inmo.postssystem.features.content.binary.common
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentSerializersModuleConfigurator
import kotlinx.serialization.modules.PolymorphicModuleBuilder
object BinaryContentSerializerModuleConfigurator : ContentSerializersModuleConfigurator.Element {
override fun PolymorphicModuleBuilder<Content>.invoke() {
subclass(DefaultBinaryContent::class, DefaultBinaryContent.serializer())
}
}

@ -1,14 +0,0 @@
package dev.inmo.postssystem.features.content.binary.common
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.features.common.common.SimpleInputProvider
import dev.inmo.postssystem.features.content.common.BinaryContent
import kotlinx.serialization.Serializable
@Serializable
data class DefaultBinaryContent(
override val filename: FileName,
override val mimeType: MimeType,
override val inputProvider: SimpleInputProvider
) : BinaryContent

@ -2,7 +2,6 @@ package dev.inmo.postssystem.features.content.binary.server
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import dev.inmo.micro_utils.repos.UpdatedValuePair import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.postssystem.features.content.binary.common.DefaultBinaryContent
import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
import dev.inmo.postssystem.features.files.common.* import dev.inmo.postssystem.features.files.common.*
@ -12,7 +11,7 @@ import kotlinx.coroutines.flow.map
class BinaryServerContentStorage( class BinaryServerContentStorage(
private val filesStorage: FilesStorage private val filesStorage: FilesStorage
) : ServerContentStorage<DefaultBinaryContent> { ) : ServerContentStorage<BinaryContent> {
private val FileId.asContentId private val FileId.asContentId
get() = ContentId(string) get() = ContentId(string)
private val ContentId.asFileId private val ContentId.asFileId
@ -20,13 +19,13 @@ class BinaryServerContentStorage(
private val FullFileInfoStorageWrapper.asRegisteredContent private val FullFileInfoStorageWrapper.asRegisteredContent
get() = RegisteredContent( get() = RegisteredContent(
id.asContentId, id.asContentId,
DefaultBinaryContent( BinaryContent(
fileInfo.name, fileInfo.name,
fileInfo.mimeType, fileInfo.mimeType,
fileInfo.inputProvider fileInfo.inputProvider
) )
) )
private val DefaultBinaryContent.asFullFileInfo private val BinaryContent.asFullFileInfo
get() = FullFileInfo( get() = FullFileInfo(
filename, filename,
mimeType, mimeType,
@ -36,7 +35,7 @@ class BinaryServerContentStorage(
override val newObjectsFlow: Flow<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent } override val newObjectsFlow: Flow<RegisteredContent> = filesStorage.newObjectsFlow.map { it.asRegisteredContent }
override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent } override val updatedObjectsFlow: Flow<RegisteredContent> = filesStorage.updatedObjectsFlow.map { it.asRegisteredContent }
override suspend fun create(values: List<DefaultBinaryContent>): List<RegisteredContent> { override suspend fun create(values: List<BinaryContent>): List<RegisteredContent> {
return filesStorage.create( return filesStorage.create(
values.map { it.asFullFileInfo } values.map { it.asFullFileInfo }
).map { it.asRegisteredContent } ).map { it.asRegisteredContent }
@ -46,14 +45,14 @@ class BinaryServerContentStorage(
filesStorage.deleteById(ids.map { it.asFileId }) filesStorage.deleteById(ids.map { it.asFileId })
} }
override suspend fun update(id: ContentId, value: DefaultBinaryContent): RegisteredContent? { override suspend fun update(id: ContentId, value: BinaryContent): RegisteredContent? {
return filesStorage.update( return filesStorage.update(
id.asFileId, id.asFileId,
value.asFullFileInfo value.asFullFileInfo
) ?.asRegisteredContent ) ?.asRegisteredContent
} }
override suspend fun update(values: List<UpdatedValuePair<ContentId, DefaultBinaryContent>>): List<RegisteredContent> { override suspend fun update(values: List<UpdatedValuePair<ContentId, BinaryContent>>): List<RegisteredContent> {
return filesStorage.update( return filesStorage.update(
values.map { (id, content) -> values.map { (id, content) ->
id.asFileId to content.asFullFileInfo id.asFileId to content.asFullFileInfo

@ -3,6 +3,7 @@ package dev.inmo.postssystem.features.content.common
import dev.inmo.micro_utils.common.FileName import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.mime_types.MimeType import dev.inmo.micro_utils.mime_types.MimeType
import dev.inmo.postssystem.features.common.common.SimpleInputProvider import dev.inmo.postssystem.features.common.common.SimpleInputProvider
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
@ -26,11 +27,13 @@ interface SimpleContent : Content
/** /**
* This type represents some binary data which can be sent with multipart and deserialized from it * This type represents some binary data which can be sent with multipart and deserialized from it
*/ */
interface BinaryContent : Content { data class BinaryContent(
val filename: FileName val filename: FileName,
val mimeType: MimeType val mimeType: MimeType,
val inputProvider: SimpleInputProvider val inputProvider: SimpleInputProvider
} ) : Content
val ContentSerializer = PolymorphicSerializer(Content::class)
/** /**
* Content which is already registered in database. Using its [id] you can retrieve all known * Content which is already registered in database. Using its [id] you can retrieve all known

@ -13,13 +13,13 @@ kotlin_version=1.6.10
kotlin_serialisation_core_version=1.3.2 kotlin_serialisation_core_version=1.3.2
koin_version=3.1.2 koin_version=3.1.2
microutils_version=0.9.1 microutils_version=0.9.4
ktor_version=1.6.7 ktor_version=1.6.7
logback_version=1.2.10 logback_version=1.2.10
uuid_version=0.3.1 uuid_version=0.3.1
klock_version=2.4.10 klock_version=2.4.12
tgbotapi_version=0.38.1 tgbotapi_version=0.38.3
# Server # Server

@ -23,6 +23,8 @@ dependencies {
api project(":postssystem.features.content.binary.server") api project(":postssystem.features.content.binary.server")
api project(":postssystem.features.publication.server") api project(":postssystem.features.publication.server")
api project(":postssystem.services.posts.server")
api project(":postssystem.targets.telegram.publication.server") api project(":postssystem.targets.telegram.publication.server")
api "io.ktor:ktor-server-netty:$ktor_version" api "io.ktor:ktor-server-netty:$ktor_version"

@ -25,7 +25,6 @@ import dev.inmo.micro_utils.ktor.server.createKtorServer
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo
import dev.inmo.postssystem.features.common.common.* import dev.inmo.postssystem.features.common.common.*
import dev.inmo.postssystem.features.content.binary.common.BinaryContentSerializerModuleConfigurator
import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentStorage import dev.inmo.postssystem.features.content.binary.server.BinaryServerContentStorage
import dev.inmo.postssystem.features.content.common.* import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
@ -36,6 +35,7 @@ import dev.inmo.postssystem.features.posts.server.ExposedServerPostsStorage
import dev.inmo.postssystem.features.posts.server.ServerPostsStorage import dev.inmo.postssystem.features.posts.server.ServerPostsStorage
import dev.inmo.postssystem.features.publication.server.PublicationManager import dev.inmo.postssystem.features.publication.server.PublicationManager
import dev.inmo.postssystem.features.publication.server.PublicationTarget import dev.inmo.postssystem.features.publication.server.PublicationTarget
import dev.inmo.postssystem.services.posts.server.ServerPostsServiceRoutingConfigurator
import dev.inmo.postssystem.targets.telegram.publication.server.PublicationTargetTelegram import dev.inmo.postssystem.targets.telegram.publication.server.PublicationTargetTelegram
import io.ktor.application.featureOrNull import io.ktor.application.featureOrNull
import io.ktor.application.log import io.ktor.application.log
@ -86,7 +86,6 @@ fun getDIModule(
return module { return module {
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { OtherContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator } singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { TextContentSerializerModuleConfigurator }
singleWithRandomQualifier<ContentSerializersModuleConfigurator.Element> { BinaryContentSerializerModuleConfigurator }
singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) } singleWithRandomQualifier<SerializersModuleConfigurator.Element> { ContentSerializersModuleConfigurator(getAll()) }
single { SerializersModuleConfigurator(getAll()) } single { SerializersModuleConfigurator(getAll()) }
@ -184,6 +183,7 @@ fun getDIModule(
singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) } singleWithBinds { UsersStorageServerRoutesConfigurator(get(), get()) }
singleWithBinds { RolesStorageReadServerRoutesConfigurator<Role>(get(), RoleSerializer, get()) } singleWithBinds { RolesStorageReadServerRoutesConfigurator<Role>(get(), RoleSerializer, get()) }
singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) } singleWithBinds { RolesManagerRolesStorageServerRoutesConfigurator(get(), get()) }
singleWithBinds { ServerPostsServiceRoutingConfigurator(get(), get(), get()) }
singleWithBinds { ClientStaticRoutingConfiguration(get<Config>().clientStatic) } singleWithBinds { ClientStaticRoutingConfiguration(get<Config>().clientStatic) }
singleWithBinds { singleWithBinds {

@ -0,0 +1,11 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.postssystem.services.posts.common.*
class ClientPostsService(
baseUrl: String,
unifiedRequester: UnifiedRequester
) : PostsService,
ReadPostsService by ClientReadPostsService(baseUrl, unifiedRequester),
WritePostsService by ClientWritePostsService(baseUrl, unifiedRequester)

@ -0,0 +1,22 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.ReadPostsService
import dev.inmo.postssystem.services.posts.common.postsRootPath
import kotlinx.serialization.builtins.nullable
class ClientReadPostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : ReadPostsService, ReadCRUDRepo<RegisteredPost, PostId> by KtorReadStandardCrudRepo(
buildStandardUrl(baseUrl, postsRootPath),
unifiedRequester,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer()
)

@ -0,0 +1,123 @@
package dev.inmo.postssystem.services.posts.client
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.client.UnifiedRequester
import dev.inmo.micro_utils.ktor.common.buildStandardUrl
import dev.inmo.micro_utils.ktor.common.encodeHex
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.client.request.forms.formData
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.http.HttpHeaders
import kotlinx.serialization.builtins.*
class ClientWritePostsService(
private val baseUrl: String,
private val unifiedRequester: UnifiedRequester
) : WritePostsService {
private val root = buildStandardUrl(baseUrl, postsRootPath)
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private val createFullPath = buildStandardUrl(
root,
createRouting
)
private val removeFullPath = buildStandardUrl(
root,
removeRoute
)
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
return if (newPost.content.any { it is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
newPost.content.forEachIndexed { i, content ->
when (content) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = content.inputProvider),
headers {
append(HttpHeaders.ContentType, content.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${content.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(ContentSerializer, content)
)
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
createFullPath,
contentsSerializer to newPost.content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost? {
return if (content.any { it.optionalT2.data is BinaryContent }) {
val answer = unifiedRequester.client.post<ByteArray>(createFullPath) {
formData {
content.forEachIndexed { i, eitherContent ->
eitherContent.onFirst {
append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}.onSecond {
when (it) {
is BinaryContent -> append(
i.toString(),
InputProvider(block = it.inputProvider),
headers {
append(HttpHeaders.ContentType, it.mimeType.raw)
append(HttpHeaders.ContentDisposition, "filename=\"${it.filename.name}\"")
}.build()
)
else -> append(
i.toString(),
unifiedRequester.serialFormat.encodeHex(contentEitherSerializer, it.either())
)
}
}
}
}
}
unifiedRequester.serialFormat.decodeFromByteArray(RegisteredPost.serializer().nullable, answer)
} else {
unifiedRequester.unipost(
buildStandardUrl(
root,
updateRouting,
postsPostIdParameter to unifiedRequester.encodeUrlQueryValue(PostId.serializer(), postId)
),
contentsEitherSerializer to content,
RegisteredPost.serializer().nullable
)
}
}
override suspend fun remove(postId: PostId) = unifiedRequester.unipost(
removeFullPath,
PostId.serializer() to postId,
Unit.serializer()
)
}

@ -12,6 +12,8 @@ kotlin {
dependencies { dependencies {
api project(":postssystem.features.common.common") api project(":postssystem.features.common.common")
api project(":postssystem.features.posts.common") api project(":postssystem.features.posts.common")
api "dev.inmo:micro_utils.repos.common:$microutils_version"
api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version"
} }
} }
} }

@ -1,3 +1,5 @@
package dev.inmo.postssystem.services.posts.common package dev.inmo.postssystem.services.posts.common
const val postsRootPath = "posts" const val postsRootPath = "posts"
const val postsPostIdParameter = "postId"

@ -0,0 +1,9 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.postssystem.features.content.common.Content
import kotlinx.serialization.Serializable
@Serializable
data class FullNewPost(
val content: List<Content>
)

@ -0,0 +1,3 @@
package dev.inmo.postssystem.services.posts.common
interface PostsService : ReadPostsService, WritePostsService

@ -0,0 +1,7 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface ReadPostsService : ReadCRUDRepo<RegisteredPost, PostId>

@ -0,0 +1,16 @@
package dev.inmo.postssystem.services.posts.common
import dev.inmo.micro_utils.common.Either
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.posts.common.PostId
import dev.inmo.postssystem.features.posts.common.RegisteredPost
interface WritePostsService {
suspend fun create(newPost: FullNewPost): RegisteredPost?
suspend fun update(
postId: PostId,
content: List<Either<ContentId, Content>>
): RegisteredPost?
suspend fun remove(postId: PostId)
}

@ -14,6 +14,8 @@ kotlin {
api project(":postssystem.features.content.server") api project(":postssystem.features.content.server")
api project(":postssystem.features.posts.server") api project(":postssystem.features.posts.server")
api project(":postssystem.features.users.server") api project(":postssystem.features.users.server")
api "dev.inmo:micro_utils.common:$microutils_version"
api "org.jetbrains.kotlinx:kotlinx-serialization-properties:$kotlin_serialisation_core_version"
} }
} }
} }

@ -0,0 +1,49 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.repos.create
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.postssystem.features.content.common.Content
import dev.inmo.postssystem.features.content.common.ContentId
import dev.inmo.postssystem.features.content.server.storage.ServerContentStorage
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.features.posts.server.ServerPostsStorage
import dev.inmo.postssystem.services.posts.common.FullNewPost
import dev.inmo.postssystem.services.posts.common.WritePostsService
class DefaultWritePostsService(
private val postsStorage: ServerPostsStorage,
private val contentStorage: ServerContentStorage<Content>
) : WritePostsService {
override suspend fun create(newPost: FullNewPost): RegisteredPost? {
val contentIds = contentStorage.create(newPost.content).map { it.id }
return postsStorage.create(NewPost(contentIds)).firstOrNull()
}
override suspend fun update(postId: PostId, content: List<Either<ContentId, Content>>): RegisteredPost? {
if (!postsStorage.contains(postId)) {
return null
}
val newContent = content.mapNotNull {
when (it) {
is EitherFirst -> {
it.t1
}
is EitherSecond -> {
contentStorage.create(it.t2).firstOrNull() ?.id
}
}
}
return postsStorage.update(
postId,
NewPost(newContent)
)
}
override suspend fun remove(postId: PostId) {
return postsStorage.deleteById(postId)
}
}

@ -0,0 +1,61 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.StandardKtorSerialFormat
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.postssystem.features.content.common.ContentSerializer
import dev.inmo.postssystem.services.posts.common.FullNewPost
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.content.PartData
import io.ktor.request.receiveMultipart
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
suspend fun PipelineContext<Unit, ApplicationCall>.downloadFullNewPost(
serialFormat: StandardKtorSerialFormat
): FullNewPost {
val multipart = call.receiveMultipart()
val map = mutableMapOf<String, Any>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
map[name] = serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
}
capturedPart is PartData.FileItem -> {
val filename = FileName(capturedPart.originalFileName ?: error("File name is unknown for default part"))
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
}
else -> {}
}
part = multipart.readPart()
}
}

@ -0,0 +1,211 @@
package dev.inmo.postssystem.services.posts.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.ktor.common.decodeHex
import dev.inmo.micro_utils.ktor.server.*
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import dev.inmo.micro_utils.mime_types.findBuiltinMimeType
import dev.inmo.micro_utils.repos.ktor.common.crud.createRouting
import dev.inmo.micro_utils.repos.ktor.common.crud.updateRouting
import dev.inmo.micro_utils.repos.ktor.common.one_to_many.removeRoute
import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes
import dev.inmo.postssystem.features.content.common.*
import dev.inmo.postssystem.features.posts.common.*
import dev.inmo.postssystem.services.posts.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.content.PartData
import io.ktor.request.isMultipart
import io.ktor.request.receiveMultipart
import io.ktor.routing.*
import io.ktor.util.asStream
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.use
import io.ktor.utils.io.streams.asInput
import kotlinx.serialization.builtins.*
class ServerPostsServiceRoutingConfigurator(
private val readPostsService: ReadPostsService,
private val writePostsService: WritePostsService? = readPostsService as? WritePostsService,
private val unifiedRouter: UnifiedRouter
) : ApplicationRoutingConfigurator.Element {
private val contentEitherSerializer = EitherSerializer(ContentId.serializer(), ContentSerializer)
private val contentsEitherSerializer = ListSerializer(contentEitherSerializer)
private val contentsSerializer = ListSerializer(ContentSerializer)
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContents(): List<Content> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Content>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
ContentSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsSerializer)
}
}
}
private suspend fun PipelineContext<Unit, ApplicationCall>.receiveContentsEithers(): List<Either<ContentId, Content>> {
return unifiedRouter.run {
if (call.request.isMultipart()) {
val multipart = call.receiveMultipart()
val list = mutableListOf<Pair<String, Either<ContentId, Content>>>()
var part = multipart.readPart()
while (part != null) {
val name = part.name
val capturedPart = part
when {
name == null -> {}
capturedPart is PartData.FormItem -> {
list.add(
name to unifiedRouter.serialFormat.decodeHex(
contentEitherSerializer,
capturedPart.value
)
)
}
capturedPart is PartData.FileItem -> {
val filename = capturedPart.originalFileName ?.let(::FileName) ?: error("File name is unknown for default part")
val mimeType = capturedPart.contentType ?.let {
findBuiltinMimeType("${it.contentType}/${it.contentSubtype}")
} ?: error("File type is unknown for default part")
val resultInput = MPPFile.createTempFile(
filename.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${filename.extension}"
).apply {
outputStream().use { fileStream ->
capturedPart.provider().asStream().copyTo(fileStream)
}
}
list.add(
name to BinaryContent(
filename,
mimeType
) { resultInput.inputStream().asInput() }.either()
)
}
else -> {}
}
part = multipart.readPart()
}
list.sortedBy { it.first }.map { it.second }
} else {
uniload(contentsEitherSerializer)
}
}
}
override fun Route.invoke() {
authenticate {
route(postsRootPath) {
configureReadStandardCrudRepoRoutes(
readPostsService,
RegisteredPost.serializer(),
RegisteredPost.serializer().nullable,
PostId.serializer(),
unifiedRouter
)
writePostsService ?.let {
unifiedRouter.apply {
post(createRouting) {
val data = receiveContents()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.create(FullNewPost(data))
)
}
post(updateRouting) {
val postId = call.decodeUrlQueryValueOrSendError(postsPostIdParameter, PostId.serializer()) ?: return@post
val data = receiveContentsEithers()
unianswer(
RegisteredPost.serializer().nullable,
writePostsService.update(
postId,
data
)
)
}
post(removeRoute) {
val postId = uniload(PostId.serializer())
unianswer(
Unit.serializer(),
writePostsService.remove(postId)
)
}
}
}
}
}
}
}