Skip to main content

CRUD Operations

Sluice wraps every MongoDB CRUD operation with full type safety. All operations return builder objects — call .execute() for mutations and .toList() / .toOne() for reads.

Setup

import { Schema as S } from "@effect/schema";
import { registry } from "sluice-orm";

const UserSchema = S.Struct({
_id: S.String,
name: S.String,
email: S.String,
age: S.Number,
active: S.Boolean,
tags: S.Array(S.String),
});

const db = registry("8.0", { users: UserSchema });
const { users } = db(client.db("myapp"));

Find

find()

Returns multiple documents as an array.

// No filter — find all
const allUsers = await users.find().toList();
// Type: User[]

// With filter
const adults = await users
.find($ => ({ age: { $gte: 18 }, active: true }))
.toList();
// Type: User[]

// With options (sort, limit, skip)
const page = await users
.find($ => ({ active: true }), {
sort: { name: 1 },
limit: 10,
skip: 20,
})
.toList();

findOne()

Returns a single document or null.

const user = await users
.findOne($ => ({ _id: "user123" }))
.toOne();
// Type: User | null

Insert

insertOne()

Inserts a single document. The document must match the schema type exactly.

await users.insertOne({
_id: "user1",
name: "Alice",
email: "alice@example.com",
age: 30,
active: true,
tags: ["admin"],
}).execute();

// TypeScript error if schema doesn't match:
// await users.insertOne({ name: "Alice" }).execute();
// ❌ Missing _id, email, age, active, tags

insertMany()

Inserts multiple documents.

await users.insertMany([
{ _id: "user1", name: "Alice", email: "alice@example.com", age: 30, active: true, tags: ["admin"] },
{ _id: "user2", name: "Bob", email: "bob@example.com", age: 25, active: false, tags: [] },
]).execute();

Update

updateOne() / updateMany()

Updates documents using MongoDB update operators ($set, $inc, $unset, etc). The update operators are type-checked against the schema.

// Update one document
await users.updateOne(
$ => ({ _id: "user1" }),
{ $set: { name: "Alice Smith" }, $inc: { age: 1 } },
).execute();

// Update many documents
await users.updateMany(
$ => ({ active: false }),
{ $set: { active: true } },
).execute();

replaceOne()

Replaces an entire document. The replacement must match the schema.

await users.replaceOne(
$ => ({ _id: "user1" }),
{ _id: "user1", name: "Alice New", email: "new@example.com", age: 31, active: true, tags: [] },
).execute();

Delete

deleteOne() / deleteMany()

await users.deleteOne($ => ({ _id: "user1" })).execute();

await users.deleteMany($ => ({ active: false })).execute();

Find and Modify

Atomic find-and-modify operations. Return the document (or null).

findOneAndUpdate()

const updated = await users.findOneAndUpdate(
$ => ({ _id: "user1" }),
{ $set: { lastLogin: new Date() } },
).execute();
// Type: User | null

findOneAndReplace()

const replaced = await users.findOneAndReplace(
$ => ({ _id: "user1" }),
{ _id: "user1", name: "Replaced", email: "r@example.com", age: 0, active: false, tags: [] },
).execute();
// Type: User | null

findOneAndDelete()

const deleted = await users.findOneAndDelete(
$ => ({ _id: "user1" }),
).execute();
// Type: User | null

Count and Distinct

countDocuments()

// Count all
const total = await users.countDocuments().execute();
// Type: number

// Count with filter
const activeCount = await users
.countDocuments($ => ({ active: true }))
.execute();
// Type: number

estimatedDocumentCount()

Faster than countDocuments() but less accurate — uses collection metadata.

const estimated = await users.estimatedDocumentCount().execute();
// Type: number

distinct()

Returns distinct values for a field. The field name is type-checked and the return type matches the field's type.

const names = await users.distinct("name").execute();
// Type: string[]

const ages = await users.distinct("age").execute();
// Type: number[]

// With filter
const activeNames = await users
.distinct("name", $ => ({ active: true }))
.execute();
// Type: string[]

Bulk Write

Performs multiple write operations in a single command.

const result = await users.bulkWrite([
{ insertOne: { document: { _id: "u1", name: "A", email: "a@x.com", age: 20, active: true, tags: [] } } },
{ updateOne: { filter: { _id: "u2" }, update: { $set: { age: 30 } } } },
{ updateMany: { filter: { active: false }, update: { $set: { active: true } } } },
{ deleteOne: { filter: { _id: "u3" } } },
{ replaceOne: {
filter: { _id: "u4" },
replacement: { _id: "u4", name: "Z", email: "z@x.com", age: 0, active: false, tags: [] },
}},
]).execute();

// Ordered execution (operations run sequentially, stop on first error)
await users.bulkWrite([
{ insertOne: { document: { _id: "seq1", name: "First", email: "1@x.com", age: 1, active: true, tags: [] } } },
{ updateOne: { filter: { _id: "seq1" }, update: { $set: { age: 42 } } } },
]).execute({ ordered: true });

Effect Integration

With the Effect registry, all CRUD operations return Effect<T, MongoError>:

import { registryEffect } from "sluice-orm";
import { Effect } from "effect";

const program = Effect.gen(function* () {
const reg = yield* registryEffect("8.0", { users: UserSchema });

// All operations are effectful
const allUsers = yield* reg.users.find().toList();

yield* reg.users
.updateOne(() => ({ _id: "1" }), { $set: { age: 31 } })
.execute();

const deleted = yield* reg.users
.deleteOne(() => ({ _id: "1" }))
.execute();
});