Introduction

Node.js is the best when it comes to community support and simplicity. But, when it comes to the language itself and the builtin features, Yuk! I was used to a lot of libraries in Node.js and that what held me from going all in server-side Kotlin. After the introduction of ktor and kgraphql, it was mongoose the only library left to have an alternative in kotlin!

So, I made mongoose in Kotlin 🤤

Starting the connection

The following is how to do the mangaka connection on the default mangaka instance.

suspend fun foo() {
    Mangaka.connect("mongodb://localhost:27017", "Mangaka")
}

Schema Definition

Mangaka was made to mimic the style of mongoose as possible. Obviously a real mongoose in kotlin is impossible to be maid since type-unions are not a supported thing in kotlin. But, the thing that kotlin has instead is extension functions. So, mangaka took a good advantage of that.

For example, the following with mongoose:

export interface Entity extends Document {
    value?: string;
    friendId: ObjectId;
    list: string[];
}

export const EntitySchema = new Schema<Entity>({
    value: {
        type: SchemaTypes.String,
        default: () => "Initialized",
        validate: value => value === "Invalid",
        immutable: value => value === "Immutable"
    },
    friendId: {
        type: SchemaTypes.ObjectId,
        ref: () => "Entity",
        exists: true // using module 'mongoose-extra-validators'
    },
    list: {
        type: [SchemaTypes.String],
        default: () => ["FirstElement"]
    }
});

export const EntityModel = model("Entity", EntitySchema, "EntityCol");

Has the following equivalent with mangaka:

@Serializable
data class Entity(
    var value: String? = null,
    var friendId: Id<Entity> = Id("62a49540988ff286898c46b5"),
    var list: MutableList<String?> = mutableListOf()
) : Document

val EntitySchema = DocumentSchema(Entity::class) {
    field(Entity::value) {
        this extends StringSchema()

        default { "Initialized" }
        validate { it != "Invalid" }
        immutable { it == "Frozen" }
    }
    field(Entity::friendId) {
        this extends ObjectIdSchema()

        exists { this.model }
    }
    field(Entity::list) {
        this extends ArraySchema()

        default { listOf("FirstElement") }

        items {
            this extends StringSchema()
        }
    }
}

val EntityModel = Model("Entity", EntitySchema, "EntityCol")

Model Usage

The model is a tricky one, since in mongoose the model has mongoose internal stuff with javascript specific wizardry which is not even needed in mangaka. But, still the developer experience the first priority here in mangaka. So, the code appears the same but internal it is not.

For example, the following code with mangaka:

async function foo() {
    const entity = new EntityModel()
    await EntityModel.create({
        value: "SomeValue"
    })
    await EntityModel.findOne({
        value: "SomeValue"
    })
}

Has the following equivalent with mangaka:

suspend fun foo() {
    val entity = EntityModel()
    EntityModel.create(
        Entity::value eq "SomeValue"
    )
    EntityModel.findOne(
        Entity::value eq "SomeValue"
    )
}

Document Usage

One of the best things about mongoose is the interface Document that has shortcuts for saving, deleting and validating values without the need to know the client, database or collection they came from. This is one of the easiest to mimic features that was implemented in mangaka.

For example, the following with mongoose:

async function foo(document: Document) {
    await document.validate()
    await document.save()
    await document.remove()
}

Has the following equivalent with mangaka:

suspend fun foo(document: Document) {
    document.validate()
    document.save()
    document.remove()
}

Static and Member functions

This feature doesn’t event need to be implemented in mangaka. The best thing about kotlin extension functions is that anyone can write their own extension function.

The following is an example for extension functions:

// example virtual value
val Entity.firstElement: String?
    get() = list.firstOrNull()

// example member function
suspend fun Entity.findFriend(): Entity? {
    return model.findOne(Filters.eq("_id", friendId))
}

// example static function
suspend fun Model<Entity>.findByValue(value: String): Entity? {
    return findOne(Entity::value eq value)
}

Extension Order

When applying an extension to a schema. The applying order is very important.

The following are examples of the importance of applying order:

  • The ignore (and immutable) extension is expected to be before the required and default extensions. Since the ignore extension will emit null which the required and default extensions will receive the null and think the value is missing.
  • The extends extension is expected to be the first extension applied. Since, the extends extension will override ALL the schema functions.

GitHub

View Github