Skip to content

Monarch ORM: A type-safe Object Document Mapper (ODM) for MongoDB

Notifications You must be signed in to change notification settings

princecodes247/monarch

Repository files navigation

Monarch ORM

Monarch ORM is a type-safe ORM for MongoDB, designed to provide a seamless and efficient way to interact with your MongoDB database in a type-safe manner. Monarch ensures that your data models are strictly enforced, reducing the risk of runtime errors and enhancing code maintainability.

Table of Contents

Features

  • Strongly Typed: Ensures type safety across your MongoDB operations.
  • Easy Integration: Simple setup and configuration.
  • Powerful Schema Modifiers: Define schemas with optional and required fields.
  • Intuitive API: Designed to be easy to use and understand.

Installation

NPM:

  npm install monarch-orm

Or Yarn:

  yarn add monarch-orm

Basic Usage

import { boolean, createClient, createDatabase, createSchema, number, string } from "monarch-orm";

    const UserSchema = createSchema("users", {
      name: string().nullable(),
      email: string().lowercase().optional(),
      age: number().optional().default(10),
      isVerified: boolean(),
    });

    const client = createClient(/** db uri **//)
    const { collections } = createDatabase(client.db(), {
      users: UserSchema,
    });

    const newUser = await collections.users
      .insert()
      .values({
        name: "anon",
        email: "[email protected]",
        age: 0,
        isVerified: true,
      })
      .exec();

    const users = await collections.users.find().where({}).exec();

Quick Start

Defining Schemas and connecting to the database

Use the createSchema function to define the structure of your model. Specify the fields and their types, using the available types and modifiers.

const UserSchema = createSchema("users", {
  name: string(),
  isVerified: boolean(),
});

Create a database instance using any client you deem fit and drop it into the createDatabase function

Or you can use the built-in createClient function.

Then you pass your schemas to the second arguement

const { collections } = createDatabase(client.db(), {
  users: UserSchema,
});

Inserting Documents

You can insert new documents into your collection using the insert method. Ensure that the data conforms to the defined schema.

Example: Inserting a new user

const newUser = await collections.users
  .insert()
  .values({
    name: "Alice",
    email: "[email protected]",
    age: 25,
    isVerified: true,
  })
  .exec();

Querying Documents

Retrieve documents from your collection using the find or findOne methods.

Example: Querying all users

const users = await collections.users.find().where({}).exec();
console.log(users);

// Or just...
const users = await collections.users.find({}).exec();
console.log(users);


// For finding one

const user = await collections.users.find().where({
  name: "Alice"
}).exec();
console.log(users);

// Or...
const user = await collections.users.findOne({
  name: "Alice"
}).exec();
console.log(users);

Updating Documents

Update documents in your collection using the update method. You can update a single document or multiple documents based on a filter.

Example: Updating a single user's email

const updatedUser = await collections.users
  .updateOne()
  .set({
    email: "[email protected]",
  })
  .where({
    name: "Alice",
  })
  .exec();
console.log(updatedUser);

Example: Updating multiple users' isVerified field

const updatedUsers = await collections.users
  .updateMany()
  .set({
    isVerified: true,
  })
  .where({
    isVerified: false,
  })
  .exec();
console.log(updatedUsers);

Note: The update method returns the number of documents updated.

Alternative setup

You can also decentralize the models

const { db } = createDatabase(client);

const UserSchema = createSchema("users", {
  name: string(),
  isVerified: boolean(),
});

const UserModel = db(UserSchema);
export default UserModel;

And use it like this

const user = await UserModel.findOne({
  name: "Alice"
}).exec();
console.log(users);

Types

Primitives

String - string()

Defines a field that accepts string values.

const UserSchema = createSchema("users", {
  name: string().required(),
});
  • .lowercase(): Transforms the value to lowercase before storing.
  • .uppercase(): Transforms the value to uppercase before storing.
const UserSchema = createSchema("users", {
  name: string().lowercase(),
});

Number - number()

Defines a field that accepts numeric values.

const UserSchema = createSchema("users", {
  age: number().optional(),
});

Boolean - boolean()

Defines a field that accepts boolean values (true or false).

const UserSchema = createSchema("users", {
  isVerified: boolean(),
});

Date - date()

Defines a field that accepts JavaScript Date objects.

const UserSchema = createSchema("users", {
  birthDate: date(),
});

Date String - dateString()

Defines a field that accepts date strings in ISO format.

const UserSchema = createSchema("users", {
  registrationDate: dateString(),
});

General Modifiers

  • .nullable(): Allows the field to accept null values.
  • .default(): Sets a default value if none is provided.
  • .optional(): Makes the field optional, allowing it to be omitted.

Literals

The literal() type allows you to define a schema with fixed possible values, similar to enums in TypeScript. This is useful for enforcing specific, predefined values for a field.

  const UserRoleSchema = createSchema("userRoles", {
  role: literal("admin", "moderator", "customer"),
});

const user = {
  role: "admin", // Valid
};

// Invalid example will throw a type error
const invalidUser = {
  role: "guest", // Error: Type '"guest"' is not assignable to type '"admin" | "moderator" | "customer"'
};

Objects

 
// all properties are required by default
const UserSchema = object({
  name: string(),
  age: number(),
});

// extract the inferred type like this
type User = InferSchemaInput<typeof UserSchema>;

// equivalent to:
type User = {
  name: string;
  age: number;
};

Records

A record() allows you to define a flexible schema where each user can have a varying number of subjects and grades without needing to define a fixed schema for each subject.

 
// Define the User schema with a record for grades
const UserSchema = createSchema("users", {
  name: string().required(),
  email: string().required(),
  grades: record(number()), // Each subject will have a numeric grade
});


// Example of inserting a user with grades
const { collections } = createDatabase(client.db(), {
  users: UserSchema,
});

// Inserting a new user with grades for different subjects
const newUser = await collections.users
  .insert()
  .values({
    name: "Alice",
    email: "[email protected]",
    grades: {
      math: 90,
      science: 85,
      history: 88,
    },
  })
  .exec();

// Querying the user to retrieve grades
const user = await collections.users.findOne().where({ email: "[email protected]" }).exec();
console.log(user.grades); 
// Output: { math: 90, science: 85, history: 88 }

Arrays

 
// For Example
const ResultSchema = object({
  name: string(),
  scores: array(number()),
});

// extract the inferred type like this
type Result = InferSchemaInput<typeof ResultSchema>;

// equivalent to:
type Result = {
  name: string;
  scores: number[];
};

Tuples

Unlike arrays, A tuple() has a fixed number of elements but each element can have a different type.

 
// all properties are required by default
const ControlSchema = object({
  location: tuple([number(), number()]),
});

// extract the inferred type like this
type Control = InferSchemaInput<typeof ControlSchema>;

// equivalent to:
type Control = {
  location: [number, number];
};

Tagged Union

The taggedUnion() allows you to define a schema for related types, each with its own structure, distinguished by a common "tag" field. This is useful for representing variable types in a type-safe manner.

// You need:
// - a tag: A string identifying the type
// value: An object containing specific fields for that type.

const NotificationSchema = createSchema("notifications", {
  notification: taggedUnion({
    email: object({
      subject: string(),
      body: string(),
    }),
    sms: object({
      phoneNumber: string(),
      message: string(),
    }),
    push: object({
      title: string(),
      content: string(),
    }),
  }),
});

const notification = ;
await collections.notifications.insert().values({ notification: {
  tag: "email",
  value: {
    subject: "Welcome!",
    body: "Thank you for joining us.",
  },
} }).exec();

Union

The union() type allows you to define a field that can accept multiple different types. It's useful when a field can legitimately contain values of different types. Each type provided to union() acts as a possible variant for the field.

const MilfSchema = createSchema("milf", {
  phoneOrEmail: union(string(), number()),
});

// Output Type : { 
//   phoneOrEmail: string | number
// }

Mixed

Mixed

The mixed() type allows you to define a field that can accept any type of value. This is useful when you need maximum flexibility for a field's contents. However, use it sparingly as it bypasses TypeScript's type checking.

const AnythingSchema = createSchema("help", {
  anything: mixed(),
});

About

Monarch ORM: A type-safe Object Document Mapper (ODM) for MongoDB

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •