MongoKitten Documentation

repository·main·Indexed 20 days ago

https://github.com/orlandos-nl/mongokitten

A high-performance, fully asynchronous MongoDB driver for Swift built on Swift NIO. Designed for server-side Swift environments like Vapor and Hummingbird, it supports standard and embedded MongoDB deployments. Features include CRUD operations using async/await, Codable BSON support, GridFS for file storage, Change Streams, and the optional Meow ORM for type-safe modeling.

Tokens
12.5K
Snippets
42
Records
46
Agent score
73%

What's inside MongoKitten

  1. Work with BSON Documents

    main

    MongoKitten uses the Document type to represent BSON data.

    Key Characteristics

    • Versatility: A Document can represent either a dictionary (key-value pairs) or an array (ordered elements).
    • Initialization: You can use Swift literals to create documents.
    • Protocols: Document conforms to Collection, allowing you to iterate over keys/values or use standard collection APIs.
    • Access: Use subscripts to access elements. Subscripts return Primitive?, so you must cast them (e.g., as? String) to use them as specific types.

    Note: Avoid frequent conversion between Document and Swift Dictionary as Document is highly optimized for BSON operations.

    // Dictionary-style
    let documentA: Document = ["_id": ObjectId(), "username": "kitty"]
    
    // Array-style
    let documentB: Document = ["kitty", 4]
    
    // Accessing values
    let username = documentA["username"] as? String
    
    // Iterating
    for (key, value) in documentA {
        // ...
    }
  2. Connect vs. LazyConnect

    main

    MongoKitten provides two ways to establish a connection to your MongoDB instance:

    1. connect(to:): An async throws method that attempts to establish a connection immediately. If the connection fails, it throws an error. This is preferred for production servers to ensure the database is available at boot time.
    2. lazyConnect(to:): A throws method that defers the connection until it is actually needed. This is useful during development to avoid waiting for a connection during app startup or to allow the app to boot even if the cluster is temporarily unavailable.
  3. Use GridFS for file storage

    main

    MongoKitten supports GridFS via the GridFSBucket class. You can use it to upload, download, and stream files.

    Uploading Files

    • Use GridFSBucket.upload(_:filename:metadata:) for single ByteBuffer uploads.
    • Use GridFSFileWriter for chunked/streamed uploads (e.g., from an HTTP request body). Always call cancel() in a catch block if an upload fails to clean up chunks, and finalize() to make the file available.

    Reading Files

    • Find a file using gridFS.findFile(query).
    • Read the entire file into a single buffer using file.reader.readByteBuffer().
    • Stream the file by iterating over the GridFSFile as an AsyncSequence (yielding ByteBuffer chunks).
    let database: MongoDatabase = ...
    let gridFS = GridFSBucket(in: database)
    
    // Uploading a single blob
    let blob: ByteBuffer = ...
    let file = try await gridFS.upload(
      blob,
      filename: "invoice.pdf",
      metadata: ["invoiceNumber": 1234]
    )
    
    // Streaming an upload with GridFSFileWriter
    let writer = GridFSFileWriter(toBucket: gridFS)
    do {
      for try await chunk in request.body {
        try await writer.write(data: chunk)
      }
      let file = try await writer.finalize(filename: "invoice.pdf", metadata: ["invoiceNumber": 1234])
    } catch {
      try await writer.cancel()
      throw error
    }
    
    // Reading/Streaming a file
    if let file = try await gridFS.findFile("metadata.invoiceNumber" == 1234) {
      // Stream chunks
      for try await chunk in file {
        // chunk is ByteBuffer
      }
    }
  4. Quick Start with MongoKitten

    main

    MongoKitten is a fast, pure Swift MongoDB driver built on Swift NIO for Server Side Swift. It supports both server and embedded MongoDB environments and is fully asynchronous.

    Requirements

    • Swift: 5.5 or later
    • MongoDB: 3.6 or later
    • Platforms: macOS, iOS, Linux

    Basic Workflow

    1. Connect to a database using MongoDatabase.connect(to:).
    2. Access a collection using subscript syntax: db["collection_name"].
    3. Perform CRUD operations (insert, find, update, delete) using async/await.
    4. Use Codable to decode BSON documents into Swift types.
    import MongoKitten
    
    // Connect to database
    let db = try await MongoDatabase.connect(to: "mongodb://localhost/my_database")
    
    // Insert a document
    try await db["users"].insert(["name": "Alice", "age": 30])
    
    // Query documents
    let users = try await db["users"].find("age" >= 18).drain()
    
    // Use with Codable
    struct User: Codable {
        let name: String
        let age: Int
    }
    
    let typedUsers = try await db["users"]
        .find()
        .decode(User.self)
        .drain()
  5. Use Meow ORM for type-safe MongoDB models

    main

    Meow is a lightweight ORM layer for MongoKitten.

    Setup

    1. Add Meow as a dependency in your Swift Package Manager configuration.
    2. import Meow in your files.
    3. For Vapor, extend Application and Request to provide access to MeowDatabase.

    Defining Models

    • Your type must conform to Model and be Codable and Hashable.
    • Every field must be marked with the @Field property wrapper. This applies to nested types as well.
    • The _id field is required by MongoDB.

    Querying

    • Access a collection using a typed subscript: meow[User.self].
    • Use type-safe closures for queries. Prefix field names with $ to access the @Field property wrapper within the closure.

    References

    • Use the Reference<T> type in your models to avoid manual ID management. It is Codable and LosslessStringConvertible, making it ideal for use in web frameworks like Vapor (e.g., as a route parameter or JWT subject).
    import Meow
    
    struct UserProfile: Model {
      @Field var firstName: String?
      @Field var lastName: String?
      @Field var age: Int
    }
    
    struct User: Model {
      @Field var _id: ObjectId
      @Field var email: String
      @Field var profile: UserProfile
    }
    
    // Querying
    let users = meow[User.self]
    
    // Type-safe count
    let adultCount = try await users.count(matching: { user in
      user.$profile.$age >= 18
    })
    
    // Type-safe find
    let kids = try await users.find(matching: { user in
      user.$profile.$age < 18
    })
    
    // Using Reference in Vapor
    app.get("users", ":id") { req async throws -> User in
      let id: Reference<User> = req.parameters.require("id")
      return try await id.resolve(in: req.meow)
    }
  6. Install MongoKitten via Swift Package Manager

    main

    Add MongoKitten to your Package.swift dependencies. You must also add the MongoKitten product to your target's dependencies.

    To include the optional Meow ORM, add the Meow product as well.

    // In Package.swift
    .package(url: "https://github.com/orlandos-nl/MongoKitten.git", from: "7.9.0")
    
    // In your target dependencies
    .product(name: "MongoKitten", package: "MongoKitten"),
    
    // Optional: Add Meow ORM
    .product(name: "Meow", package: "MongoKitten")
  7. Perform CRUD operations

    main

    Once you have a reference to a collection (e.g., let users = db["users"]), you can perform standard CRUD operations.

    Create

    Insert a Document into a collection. The _id is automatically generated if not provided.

    Read

    • findOne: Find a single document matching a query.
    • find: Returns a FindQueryBuilder (a cursor) that can be iterated over or drained.
    • drain(): Fetches all results from a cursor into an array. Warning: Use with caution on large result sets to avoid memory exhaustion.
    • decode(_:): A helper to transform cursor results into Decodable types.

    Update & Delete

    • updateMany: Updates multiple documents matching a filter.
    • deleteOne: Deletes the first document matching a filter.
    • deleteAll: Deletes all documents matching a filter. Returns a result object containing the count of deleted items.
    let users = db["users"]
    
    // Create
    try await users.insert(["username": "kitty", "password": "meow"])
    
    // Read (Single)
    if let kitty = try await users.findOne("username" == "kitty") { /* ... */ }
    
    // Read (Multiple/Cursor)
    for try await user in users.find("age" <= 16 || "age" == nil) { /* ... */ }
    
    // Read (Typed/Codable)
    let typedUsers: [User] = try await users.find().decode(User.self).drain()
    
    // Update
    try await users.updateMany(where: "username" == "kitty", setting: ["age": 3], unsetting: nil)
    
    // Delete
    try await users.deleteOne(where: "username" == "kitty")
    let reply = try await users.deleteAll(where: "furType" == "fluffy")
    print("Deleted \(reply.deletes) kitties")
  8. Use dryRun to test unique index conversion

    main

    When converting an existing index to a unique index (available in MongoDB 6.0+), you can set the dryRun property on the CollMod command to true. This checks the index for unique constraint violations without actually applying the changes, allowing you to validate the conversion safely.

    let command = CollMod(
        collection: "users",
        index: .init(name: "email_1", unique: true),
        dryRun: true
    )
  9. Set up a MongoDB Replica Set using Docker Compose

    main

    To use MongoKitten with a MongoDB replica set for testing or development, you can use the provided docker-compose.yaml configuration. This setup spins up three MongoDB instances (mongo-1, mongo-2, and mongo-3) configured with the --replSet rs0 command, and a mongosetup service that executes a setup script to initialize the replica set.

    Service Details

    • mongo-1: Primary entry point, mapped to host port 27017.
    • mongo-2: Mapped to host port 27018.
    • mongo-3: Mapped to host port 27019.
    • mongosetup: A helper service that runs a setup.sh script from the local ./scripts directory to automate the replica set configuration.

    Requirements

    • Docker and Docker Compose installed.
    • A ./scripts/setup.sh file in your local directory to allow the mongosetup service to initialize the cluster.
    version: "3"
    
    services:
      mongo-1:
        image: mongo:5.0
        ports:
          - "27017:27017"
        container_name: mongo-1
        hostname: mongo-1
        networks: 
          - mongo_cluster
        command: mongod --replSet rs0
    
      mongo-2:
        image: mongo:5.0
        ports:
          - "27018:27017"
        container_name: mongo-2
        hostname: mongo-2
        networks: 
          - mongo_cluster
        command: mongod --replSet rs0
        depends_on:
          - mongo-1
    
      mongo-3:
        image: mongo:5.0
        ports:
          - "27019:27017"
        container_name: mongo-3
        hostname: mongo-3
        networks: 
          - mongo_cluster
        command: mongod --replSet rs0
        depends_on:
          - mongo-2
    
      mongosetup:
        image: mongo:5.0
        networks:
        - mongo_cluster
        volumes:
        - ./scripts:/scripts
        command: bash -c "chmod +x /scripts/setup.sh && /scripts/setup.sh"
    
    networks: 
      mongo_cluster:
        driver: bridge
  10. Troubleshoot MongoDB connection and performance

    main

    Authentication Failures

    • Ensure you specify authSource=admin in your connection string unless you have a specific authSource configured. MongoDB's default behavior can be non-intuitive.
    • If you specified an authMechanism, try removing it; MongoKitten can often detect the correct mechanism automatically.

    Change Streams Issues

    • Ensure you are connected to a replica set or a sharded cluster (Change Streams do not work on standalone instances).
    • Verify the user has the necessary privileges.
    • Do not attempt to watch system collections.

    Performance Issues

    • Use .explain() to check your indexes.
    • Monitor your connection pool usage.
    • Use projections to fetch only the fields you actually need.
    • Use batch operations for bulk updates/inserts.
    • Use change streams instead of polling for data changes.
  11. Monitor real-time changes with Change Streams

    main

    Use watch() on a collection to monitor real-time operations (insert, update, delete).

    Usage Modes

    1. Untyped: Iterating over the stream provides a change object where you can inspect operationType and access fullDocument or updateDescription.
    2. Type-safe: Pass a Codable type to watch(type:) to receive change events with the fullDocument already decoded into your model.
    // Untyped stream
    let stream = try await users.watch()
    for try await change in stream {
        switch change.operationType {
        case .insert: print(change.fullDocument)
        case .update: print(change.updateDescription?.updatedFields)
        case .delete: print(change.documentKey)
        default: break
        }
    }
    
    // Type-safe stream
    struct User: Codable {
        let id: ObjectId
        let name: String
    }
    
    let typedStream = try await users.watch(type: User.self)
    for try await change in typedStream {
        if let user = change.fullDocument {
            print("User: \(user.name)")
        }
    }
  12. Execute Transactions

    main

    Run multiple operations atomically within a transaction block. Changes are only committed to the database if the entire block completes without errors. If an error is thrown inside the block, the transaction is aborted.

    try await db.transaction { session in
        let users = db["users"]
        let accounts = db["accounts"]
        
        try await users.insert(newUser)
        try await accounts.insert(newAccount)
        
        // Changes are only committed if no errors occur
    }