Skip to content

Instance Methods

Add custom methods to documents returned from queries.

Defining Methods

typescript
const Users = collection({
  name: 'users',
  schema: {
    _id: field.id(),
    firstName: field.string(),
    lastName: field.string(),
    email: field.email(),
    role: field.enum(['user', 'admin']).default('user'),
  },
  methods: {
    // First argument (doc) is auto-injected
    fullName: (doc) => `${doc.firstName} ${doc.lastName}`,

    isAdmin: (doc) => doc.role === 'admin',

    // Methods can accept additional arguments
    greet: (doc, greeting = 'Hello') => `${greeting}, ${doc.firstName}!`,

    // Methods can be async
    sendEmail: async (doc, subject: string, body: string) => {
      await emailService.send({
        to: doc.email,
        subject,
        body,
      });
    },
  },
});

Using Methods

Methods are available on documents returned from queries:

typescript
const user = await Users.findOne({ email: 'alice@example.com' });

if (user) {
  console.log(user.fullName());     // 'Alice Smith'
  console.log(user.isAdmin());      // false
  console.log(user.greet('Hi'));    // 'Hi, Alice!'

  await user.sendEmail('Welcome!', 'Thanks for signing up.');
}

Method Availability

Methods are attached to documents returned from:

  • findOne()
  • findById()
  • find().toArray() (each document)
  • insertOne()
  • insertMany() (each document)
  • updateOne()
  • findOneAndUpdate()
  • findOneAndDelete()
  • findOneAndReplace()

Type Safety

Methods are fully typed:

typescript
const Users = collection({
  name: 'users',
  schema: {
    _id: field.id(),
    points: field.number().default(0),
    level: field.enum(['bronze', 'silver', 'gold']).default('bronze'),
  },
  methods: {
    getLevel: (doc): 'bronze' | 'silver' | 'gold' => {
      if (doc.points >= 1000) return 'gold';
      if (doc.points >= 500) return 'silver';
      return 'bronze';
    },

    addPoints: async (doc, amount: number) => {
      return await Users.updateOne(
        { _id: doc._id },
        { $inc: { points: amount } }
      );
    },
  },
});

const user = await Users.findById(id);
if (user) {
  const level = user.getLevel();  // Type: 'bronze' | 'silver' | 'gold'
  await user.addPoints(100);      // Type-checked argument
}

Common Patterns

Computed Properties

typescript
methods: {
  fullName: (doc) => `${doc.firstName} ${doc.lastName}`,

  age: (doc) => {
    const today = new Date();
    const birth = doc.birthDate;
    let age = today.getFullYear() - birth.getFullYear();
    if (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate())) {
      age--;
    }
    return age;
  },

  displayPrice: (doc) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: doc.price.currency,
    }).format(doc.price.amount / 100);
  },
}

Self-Updating Methods

typescript
methods: {
  markAsRead: async (doc) => {
    return await Notifications.updateOne(
      { _id: doc._id },
      { $set: { readAt: new Date() } }
    );
  },

  incrementViews: async (doc) => {
    return await Posts.updateOne(
      { _id: doc._id },
      { $inc: { views: 1 } }
    );
  },

  softDelete: async (doc) => {
    return await Users.updateOne(
      { _id: doc._id },
      { $set: { deletedAt: new Date(), status: 'deleted' } }
    );
  },
}

Relationship Loading

typescript
methods: {
  getAuthor: async (doc) => {
    return await Users.findById(doc.authorId);
  },

  getComments: async (doc) => {
    return await Comments.find({ postId: doc._id }).toArray();
  },

  getCommentsCount: async (doc) => {
    return await Comments.count({ postId: doc._id });
  },
}

Validation Methods

typescript
methods: {
  canEdit: (doc, userId: string) => {
    return doc.authorId.equals(userId) || doc.editors.includes(userId);
  },

  isExpired: (doc) => {
    return doc.expiresAt && doc.expiresAt < new Date();
  },

  hasPermission: (doc, permission: string) => {
    return doc.permissions.includes(permission) || doc.role === 'admin';
  },
}

Methods vs Statics

FeatureInstance MethodsStatic Methods
AccessOn documentsOn collection
First argDocument (auto-injected)Collection
Use caseDocument operationsCollection operations
Exampleuser.fullName()Users.findByEmail(email)

Use instance methods when the operation is specific to a single document. Use static methods for operations that work with the collection as a whole.

Serialization Note

Methods are not included when you serialize a document:

typescript
const user = await Users.findOne({ email: 'alice@example.com' });

// Before serialization - methods available
user.fullName();  // Works

// After serialization - plain object, no methods
const plain = user.serialize();
plain.fullName;   // undefined - methods are not serialized

If you need computed values in serialized output, compute them before serializing:

typescript
const user = await Users.findOne({ email: 'alice@example.com' });
const serialized = {
  ...user.serialize(),
  fullName: user.fullName(),
  isAdmin: user.isAdmin(),
};

Released under the MIT License.