com.lightningkite.lightningserver.db.ModelRestEndpoints.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of server-core Show documentation
Show all versions of server-core Show documentation
A set of tools to fill in/replace what Ktor is lacking in.
The newest version!
package com.lightningkite.lightningserver.db
import com.lightningkite.lightningdb.*
import com.lightningkite.lightningserver.LSError
import com.lightningkite.lightningserver.core.ServerPath
import com.lightningkite.lightningserver.core.ServerPathGroup
import com.lightningkite.lightningserver.exceptions.BadRequestException
import com.lightningkite.lightningserver.exceptions.ForbiddenException
import com.lightningkite.lightningserver.exceptions.NotFoundException
import com.lightningkite.lightningserver.http.HttpStatus
import com.lightningkite.lightningserver.routes.docName
import com.lightningkite.lightningserver.serialization.Serialization
import com.lightningkite.lightningserver.typed.ApiExample
import com.lightningkite.lightningserver.typed.typed
import kotlinx.coroutines.flow.toList
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.serializer
import kotlin.random.Random
open class ModelRestEndpoints, ID : Comparable>(
path: ServerPath,
val info: ModelInfo
) : ServerPathGroup(path) {
companion object {
val all = HashSet>()
}
val collectionName get() = info.collectionName
init {
if (path.docName == null) path.docName = collectionName
all.add(this)
}
private fun exampleItem(): T? = (info as? ModelInfoWithDefault)?.exampleItem()
private fun sampleConditions(): List> {
return try {
val sample = exampleItem() ?: return listOf(Condition.Always())
listOf(Condition.Always()) + info.serialization.serializer.attemptGrabFields().entries
.take(3)
.map { Condition.OnField(it.value, Condition.Equal(it.value.get(sample))) }
} catch (e: Exception) {
listOf(Condition.Always())
}
}
private fun sampleModifications(): List> {
return try {
val sample = exampleItem() ?: return emptyList()
info.serialization.serializer.attemptGrabFields().entries
.filter { it.key != "_id" }
.take(3)
.map { Modification.OnField(it.value, Modification.Assign(it.value.get(sample))) }
} catch (e: Exception) {
listOf()
}
}
private fun sampleSorts(): List>> {
return try {
val sample = exampleItem() ?: return emptyList()
info.serialization.serializer.attemptGrabFields().entries
.filter { Serialization.Internal.module.serializer(it.value.returnType).descriptor.kind is PrimitiveKind }
.let {
(1..3).map { _ -> it.shuffled().take(2).map { SortPart(it.value, Random.nextBoolean()) } }
}
} catch (e: Exception) {
listOf()
}
}
val default = (info as? ModelInfoWithDefault)?.let {
get("_default_").typed(
authInfo = info.serialization.authInfo,
inputType = Unit.serializer(),
outputType = info.serialization.serializer,
summary = "Default",
description = "Gets a default ${collectionName} that would be useful to start creating a full one to insert. Primarily used for administrative interfaces.",
errorCases = listOf(),
examples = exampleItem()?.let { listOf(ApiExample(Unit, it)) } ?: listOf(),
implementation = { user: USER, input: Unit ->
info.defaultItem(user)
}
)
}
val list = get.typed(
authInfo = info.serialization.authInfo,
inputType = Query.serializer(info.serialization.serializer),
outputType = ListSerializer(info.serialization.serializer),
summary = "List",
description = "Gets a list of ${collectionName}s.",
errorCases = listOf(),
examples = exampleItem()?.let {
val sampleSorts = sampleSorts()
sampleConditions().map {
ApiExample(Query(it, sampleSorts.random()), List(10) { exampleItem()!! })
}
} ?: listOf(),
implementation = { user: USER, input: Query ->
info.collection(user)
.query(input)
.toList()
}
)
// This is used to GET a list objects, but rather than the query being in the parameter
// it's in the POST body.
val query = post("query").typed(
authInfo = info.serialization.authInfo,
inputType = Query.serializer(info.serialization.serializer),
outputType = ListSerializer(info.serialization.serializer),
summary = "Query",
description = "Gets a list of ${collectionName}s that match the given query.",
errorCases = listOf(),
examples = exampleItem()?.let {
val sampleSorts = sampleSorts()
sampleConditions().map {
ApiExample(Query(it, sampleSorts.random()), List(10) { exampleItem()!! })
}
} ?: listOf(),
implementation = { user: USER, input: Query ->
info.collection(user)
.query(input)
.toList()
}
)
// This is used to GET a list objects, but rather than the query being in the parameter
// it's in the POST body.
val queryPartial = post("query-partial").typed(
authInfo = info.serialization.authInfo,
inputType = QueryPartial.serializer(info.serialization.serializer),
outputType = ListSerializer(PartialSerializer(info.serialization.serializer)),
summary = "Query Partial",
description = "Gets parts of ${collectionName}s that match the given query.",
errorCases = listOf(),
examples = exampleItem()?.let {
try {
val sampleSorts = sampleSorts()
val paths = info.serialization.serializer.attemptGrabFields().entries.take(2)
.map { DataClassPathAccess(DataClassPathSelf(), it.value) }.toSet()
sampleConditions().map { cond ->
ApiExample(
QueryPartial(
paths,
condition = cond,
orderBy = sampleSorts.random()
),
List(10) { Partial(exampleItem()!!, paths) }
)
}
} catch (e: Exception) {
listOf()
}
} ?: listOf(),
implementation = { user: USER, input: QueryPartial ->
info.collection(user)
.queryPartial(input)
.toList()
}
)
// This is used get a single object with id of _id
val detail = get("{id}").typed(
authInfo = info.serialization.authInfo,
inputType = Unit.serializer(),
outputType = info.serialization.serializer,
pathType = info.serialization.idSerializer,
summary = "Detail",
description = "Gets a single ${collectionName} by ID.",
errorCases = listOf(
LSError(
http = HttpStatus.NotFound.code,
detail = "",
message = "There was no known object by that ID.",
data = ""
)
),
examples = exampleItem()?.let { listOf(ApiExample(Unit, it)) } ?: listOf(),
implementation = { user: USER, id: ID, input: Unit ->
info.collection(user)
.get(id)
?: throw NotFoundException()
}
)
val insertBulk = post("bulk").typed(
authInfo = info.serialization.authInfo,
inputType = ListSerializer(info.serialization.serializer),
outputType = ListSerializer(info.serialization.serializer),
summary = "Insert Bulk",
description = "Creates multiple ${collectionName}s at the same time.",
errorCases = listOf(),
examples = exampleItem()?.let {
val items = (1..10).map { exampleItem()!! }
listOf(ApiExample(items, items))
} ?: listOf(),
successCode = HttpStatus.Created,
implementation = { user: USER, values: List ->
try {
info.collection(user)
.insertMany(values)
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val insert = post("").typed(
authInfo = info.serialization.authInfo,
inputType = info.serialization.serializer,
outputType = info.serialization.serializer,
summary = "Insert",
description = "Creates a new ${collectionName}",
errorCases = listOf(),
examples = exampleItem()?.let { listOf(ApiExample(it, it)) } ?: listOf(),
successCode = HttpStatus.Created,
implementation = { user: USER, value: T ->
try {
info.collection(user)
.insertOne(value)
?: throw ForbiddenException("Value was not posted as requested.")
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val upsert = post("{id}").typed(
authInfo = info.serialization.authInfo,
inputType = info.serialization.serializer,
outputType = info.serialization.serializer,
pathType = info.serialization.idSerializer,
summary = "Upsert",
description = "Creates or updates a ${collectionName}",
errorCases = listOf(),
examples = exampleItem()?.let { listOf(ApiExample(it, it)) } ?: listOf(),
successCode = HttpStatus.Created,
implementation = { user: USER, id: ID, value: T ->
try {
info.collection(user)
.upsertOneById(id, value)
.new
?: throw NotFoundException()
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
// This is used replace many objects at once. This does make individual calls to the database. Kmongo does not have a many replace option.
val bulkReplace = put("").typed(
authInfo = info.serialization.authInfo,
inputType = ListSerializer(info.serialization.serializer),
outputType = ListSerializer(info.serialization.serializer),
summary = "Bulk Replace",
description = "Modifies many ${collectionName}s at the same time by ID.",
errorCases = listOf(),
examples = exampleItem()?.let {
val items = (1..10).map { exampleItem()!! }
listOf(ApiExample(items, items))
} ?: listOf(),
implementation = { user: USER, values: List ->
try {
val db = info.collection(user)
values.map { db.replaceOneById(it._id, it) }.mapNotNull { it.new }
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val replace = put("{id}").typed(
authInfo = info.serialization.authInfo,
inputType = info.serialization.serializer,
outputType = info.serialization.serializer,
pathType = info.serialization.idSerializer,
summary = "Replace",
description = "Replaces a single ${collectionName} by ID.",
errorCases = listOf(
LSError(
http = HttpStatus.NotFound.code,
detail = "",
message = "There was no known object by that ID.",
data = ""
)
),
examples = exampleItem()?.let { listOf(ApiExample(it, it)) } ?: listOf(),
implementation = { user: USER, id: ID, value: T ->
try {
info.collection(user)
.replaceOneById(id, value)
.new
?: throw NotFoundException()
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val bulkModify = patch("bulk").typed(
authInfo = info.serialization.authInfo,
inputType = MassModification.serializer(info.serialization.serializer),
outputType = Int.serializer(),
summary = "Bulk Modify",
description = "Modifies many ${collectionName}s at the same time. Returns the number of changed items.",
errorCases = listOf(),
examples = exampleItem()?.let {
val c = sampleConditions()
sampleModifications().map { m ->
ApiExample(
MassModification(
c.random(),
m
),
3
)
}
} ?: listOf(),
implementation = { user: USER, input: MassModification ->
try {
info.collection(user)
.updateManyIgnoringResult(input)
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val modifyWithDiff = patch("{id}/delta").typed(
authInfo = info.serialization.authInfo,
inputType = Modification.serializer(info.serialization.serializer),
outputType = EntryChange.serializer(info.serialization.serializer),
pathType = info.serialization.idSerializer,
summary = "Modify with Diff",
description = "Modifies a ${collectionName} by ID, returning both the previous value and new value.",
errorCases = listOf(
LSError(
http = HttpStatus.NotFound.code,
detail = "",
message = "There was no known object by that ID.",
data = ""
)
),
examples = exampleItem()?.let {
sampleModifications().map { m ->
ApiExample(
m,
EntryChange(it, m(it))
)
}
} ?: listOf(),
implementation = { user: USER, id: ID, input: Modification ->
try {
info.collection(user)
.updateOneById(id, input)
.also { if (it.old == null && it.new == null) throw NotFoundException() }
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val modify = patch("{id}").typed(
authInfo = info.serialization.authInfo,
inputType = Modification.serializer(info.serialization.serializer),
outputType = info.serialization.serializer,
pathType = info.serialization.idSerializer,
summary = "Modify",
description = "Modifies a ${collectionName} by ID, returning the new value.",
errorCases = listOf(
LSError(
http = HttpStatus.NotFound.code,
detail = "",
message = "There was no known object by that ID.",
data = ""
)
),
examples = exampleItem()?.let {
sampleModifications().map { m ->
ApiExample(
m,
m(it)
)
}
} ?: listOf(),
implementation = { user: USER, id: ID, input: Modification ->
try {
info.collection(user)
.updateOneById(id, input)
.also { if (it.old == null && it.new == null) throw NotFoundException() }
.new!!
} catch (e: UniqueViolationException) {
throw BadRequestException(detail = "unique", message = e.message, cause = e)
}
}
)
val bulkDelete = post("bulk-delete").typed(
authInfo = info.serialization.authInfo,
inputType = Condition.serializer(info.serialization.serializer),
outputType = Int.serializer(),
summary = "Bulk Delete",
description = "Deletes all matching ${collectionName}s, returning the number of deleted items.",
errorCases = listOf(),
examples = exampleItem()?.let {
sampleConditions().map { c ->
ApiExample(
c,
3
)
}
} ?: listOf(),
implementation = { user: USER, filter: Condition ->
info.collection(user)
.deleteManyIgnoringOld(filter)
}
)
val deleteItem = delete("{id}").typed(
authInfo = info.serialization.authInfo,
inputType = Unit.serializer(),
outputType = Unit.serializer(),
pathType = info.serialization.idSerializer,
summary = "Delete",
description = "Deletes a ${collectionName} by id.",
errorCases = listOf(
LSError(
http = HttpStatus.NotFound.code,
detail = "",
message = "There was no known object by that ID.",
data = ""
)
),
implementation = { user: USER, id: ID, _: Unit ->
if (!info.collection(user)
.deleteOneById(id)
) {
throw NotFoundException()
}
Unit
}
)
val countGet = get("count").typed(
authInfo = info.serialization.authInfo,
inputType = Condition.serializer(info.serialization.serializer),
outputType = Int.serializer(),
summary = "Count",
description = "Gets the total number of ${collectionName}s matching the given condition.",
errorCases = listOf(),
examples = exampleItem()?.let {
sampleConditions().map { c ->
ApiExample(
c,
3
)
}
} ?: listOf(),
implementation = { user: USER, condition: Condition ->
info.collection(user)
.count(condition)
}
)
val count = post("count").typed(
authInfo = info.serialization.authInfo,
inputType = Condition.serializer(info.serialization.serializer),
outputType = Int.serializer(),
summary = "Count",
description = "Gets the total number of ${collectionName}s matching the given condition.",
errorCases = listOf(),
examples = exampleItem()?.let {
sampleConditions().map { c ->
ApiExample(
c,
3
)
}
} ?: listOf(),
implementation =
{ user: USER, condition: Condition ->
info.collection(user)
.count(condition)
}
)
val groupCount = post("group-count").typed(
authInfo = info.serialization.authInfo,
inputType = GroupCountQuery.serializer(info.serialization.serializer),
outputType = MapSerializer(String.serializer(), Int.serializer()),
summary = "Group Count",
description = "Gets the total number of ${collectionName}s matching the given condition divided by group.",
errorCases = listOf(),
examples = exampleItem()?.let {
sampleConditions().map { c ->
val f = sampleSorts().random().random().field
ApiExample(
GroupCountQuery(c, f),
mapOf(f.getAny(it).toString() to 3)
)
}
} ?: listOf(),
implementation = { user: USER, condition: GroupCountQuery ->
@Suppress("UNCHECKED_CAST")
info.collection(user)
.groupCount(condition.condition, condition.groupBy as DataClassPath)
.mapKeys { it.key.toString() }
}
)
val aggregate = post("aggregate").typed(
authInfo = info.serialization.authInfo,
inputType = AggregateQuery.serializer(info.serialization.serializer),
outputType = Double.serializer().nullable,
summary = "Aggregate",
description = "Aggregates a property of ${collectionName}s matching the given condition.",
errorCases = listOf(),
implementation = { user: USER, condition: AggregateQuery ->
@Suppress("UNCHECKED_CAST")
info.collection(user)
.aggregate(
condition.aggregate,
condition.condition,
condition.property as DataClassPath
)
}
)
val groupAggregate = post("group-aggregate").typed(
authInfo = info.serialization.authInfo,
inputType = GroupAggregateQuery.serializer(info.serialization.serializer),
outputType = MapSerializer(String.serializer(), Double.serializer().nullable),
summary = "Group Aggregate",
description = "Aggregates a property of ${collectionName}s matching the given condition divided by group.",
errorCases = listOf(),
implementation = { user: USER, condition: GroupAggregateQuery ->
@Suppress("UNCHECKED_CAST")
info.collection(user)
.groupAggregate(
condition.aggregate,
condition.condition,
condition.groupBy as DataClassPath,
condition.property as DataClassPath
)
.mapKeys { it.key.toString() }
}
)
}