mongrove
v1.0.3
Published
MongoDB toolkit for typesafe schema creation and runtime validation
Downloads
6
Maintainers
Readme
Mongrove is an NPM library that combines the power of MongoDB with Zod, a powerful TypeScript-first schema validation library, to provide a comprehensive toolkit for creating typesafe schemas and performing runtime validation in your Node.js applications.
Table of contents
Features
- ⚔️ Typesafe Schema Creation: Leverage the expressive schema definition capabilities of Zod to create MongoDB schemas with TypeScript types. This ensures that your data remains consistent and adheres to your defined structure.
- ✅ Runtime Validation: With Mongrove, you can easily validate data against your defined schemas at runtime, catching potential issues early in your application's workflow.
- ♻️ Inheritance Support: Benefit from schema inheritance feature to create reusable schema definitions, enabling you to build complex data structures effortlessly with query field completion.
- 🚫 Validation Error Handling: When data fails validation, Mongrove provides detailed error information, making it easier to identify and fix the root cause of the validation failure.
- 💤 Easy Integration: Mongrove seamlessly integrates with your existing MongoDB setup, allowing you to incorporate advanced schema definition and validation into your projects without any hassle.
Installation
Requirements
- Typescript 4.7+
- Enabled
strict
mode in yourtsconfig.json
- Set
moduleResolution
to"node16"
or"nodenext"
or"bundler"
in yourtsconfig.json
To install Mongrove, use PNPM:
pnpm add mongrove
P.S. If still using Javascript, then set moduleResolution options also to one of provided above into jsconfig.json
file.
Example for tsconfig/jsconfig.json
:
{
"compilerOptions": {
"moduleResolution": "node16"
}
}
Usage
Step 1: Define Your Schema
First, create a schema that matches the structure of your MongoDB collection. You can define this schema in a separate file, making it easy to import into your code.
// src/client/index.ts
import { Schema, Collection, string, email, date, objectId } from "mongrove/schema";
const schema = new Schema({
users: Collection({
username: string(),
displayName: string({ optional: true, min: 3, max: 32 }),
email: email(),
createdAt: date({ default: () => new Date() })
}),
posts: Collection({
title: string(),
content: string({ max: 2000 }),
userId: objectId()
})
});
Step 2: Create client
Afterward, create a client and provide the connection string along with the previously defined schema.
// src/client/index.ts
import { MongroveClient } from "mongrove";
// ...
export const mongrove = new MongroveClient(process.env.MONGO_URI!, schema);
Step 3: Export schema type (optional)
Extract and export schema type, so you can use schema collection types outside query statements.
// src/client/index.ts
import { ExtractSchemaType } from "mongrove/utils";
const schema = new Schema({...});
export type SchemaType = ExtractSchemaType<typeof schema>;
Final code
Now, the client is ready for use. It will integrate the schema and offer typesafe field autocomplete and runtime validation for queries.
// src/client/index.ts
import { MongroveClient } from "mongrove";
import { Schema, Collection, string, email, date, objectId } from "mongrove/schema";
import { ExtractSchemaType } from "mongrove/utils";
// Defining schema
const schema = new Schema({
users: Collection({
username: string(),
displayName: string({ optional: true, min: 3, max: 32 }),
email: email(),
createdAt: date({ default: () => new Date() })
}),
posts: Collection({
title: string(),
content: string({ max: 2000 }),
userId: objectId()
})
});
// Exporting schema type
export type SchemaType = ExtractSchemaType<typeof schema>;
// Creating client and exporting
export const mongrove = new MongroveClient(process.env.MONGO_URI!, schema);
Use in your code
Finally, it's ready to be used in your code. Just import it and start utilizing its features.
// src/index.ts
import { mongrove, type SchemaType } from "./client";
import { SchemaOf } from "mongrove/utils";
type UserType = SchemaOf<SchemaType, "users", "input">;
async function main() {
// Can select database,
// if not provided, using connection string defined
const db = mongrove.db("main");
// Collection names provided by intellisence
const Users = db.collection("users");
const Posts = db.collection("posts");
/* { acknowledged: boolean; insertedId: ObjectId; } */
const userInsert = await Users.insertOne({
username: "jeff",
email: "[email protected]"
});
// Use type for annotation
const newUser: UserType = {
username: "ben",
displayName: "Ben",
email: "[email protected]"
};
await Users.insertOne(newUser);
await Posts.insertOne({
title: "Mongrove",
content: "Toolkit for MongoDb database",
userId: userInsert.insertedId
});
/* {
_id: ObjectId
username: string;
email: string;
created_at: Date;
displayName?: string | undefined;
} */
const user = await Users.findOne();
console.log(user);
}
Index creation
Mongrove provides way to create indexes automatically from your defined schema.
To make schema field indexable, put index
property in field's options.
Example:
user: Collection({
username: string({ index: { unique: true } }),
email: string({ index: {} })
});
You can leave index options empty for default options or provide them. NOTICE: For now it is possible to index only 1st level fields.
To start creating indexes simply execute createIndexes
method from client:
import { mongrove } from "./client";
// Creates only indexes that are missing in database
// Do not recreate existing ones and not updating options
await mongrove.createIndexes();
// Drops all indexes and creates them again with new options
await mongrove.createIndexes({ recreate: true });
Documentation
Structure objects
Objects that shape your schema
- Collection
- Object (NObject)
Types:
Collection(fields: Fields, options?: ValidatorCollectionOptions);
NObject(fields: Fields, options?: ValidatorObjectOptions);
type ValidatorObjectOptions = {
optional?: boolean;
nullable?: boolean;
};
Schema options
- filterDatabases
- Used for filtering databases, that are used with this schema.
{
filterDatabases?: ((dbName: string) => boolean)
}
Fields
Fields are way to define field type, validation and options. These are used only in schema to define fields.
Types:
string(options?: ValidatorStringOptions);
number(options?: ValidatorNumberOptions);
boolean(options?: ValidatorBooleanOptions);
date(options?: ValidatorDateOptions);
objectId(options?: ValidatorObjectIdOptions);
enumField(enumVal: [string, ...string[]], options?: ValidatorEnumOptions);
array(field: Field, options?: ValidatorArrayOptions);
Field options
Default
Accessible in all fields
type ValidatorDefaultOptions<T> = {
index?: { unique?: boolean };
default?: T;
optional?: boolean;
nullable?: boolean;
};
- index:
- Set options for indexing field, where this option is used.
- Example:
string({ index: { unique: true } })
- default:
- Set default value, that will be used when inserting data, if this field is not provided in query.
- Example 1:
number({ default: 0 })
- Example 2:
date({ default: () => new Date() })
- optional:
- Set field as optional field. In queries this field can be not provided with value and can be later updated with value.
- Example:
string({ optional: true })
- nullable:
- Set field as nullable field, so it can accept also
null
value for field, that should be defined, but value can not be provided in beginning. - Example:
number({ nullable: true })
- Set field as nullable field, so it can accept also
Notice. You can use only one of options: {default or optional or nullable}
String
Provided also in:
uuid, url, ip, emoji, email, cuid, cuid2
type ValidatorStringOptions = {
min?: number;
max?: number;
regex?: RegExp;
includes?: string;
startsWith?: string;
endsWith?: string;
};
- min:
- Sets minimal length of string
- max:
- Sets maximal length of string
- regex:
- Sets regex for validating provided string
- includes:
- Sets check if input string includes provided string
- startsWith:
- Sets check if input string starts with provided string
- endsWith:
- Sets check if input string ends with provided string
Number
type ValidatorNumberOptions = {
int?: boolean;
type?: "positive" | "nonnegative" | "negative" | "nonpositive";
};
- int:
- If
true
, accepts only integers (whole numbers)
- If
- type:
- Sets type of number
Boolean
No additional options
type ValidatorBooleanOptions = {};
Date
type ValidatorDateOptions{
min?: Date;
max?: Date;
}
- min:
- Sets minimal date allowed
- max:
- Sets maximal date allowed
ObjectId
No additional options
type ValidatorObjectIdOptions = {};
Enum
No additional options
type ValidatorEnumOptions = {};
Array
type ValidatorArrayOptions = {
nonempty?: boolean;
min?: number;
max?: number;
length?: number;
};
- nonempty:
- Sets if should there be at least one item
- min:
- Sets minimal count of items
- max:
- Sets maximal count of items
- length:
- Sets exact count of items
Events
There are some events you can listen to. They are provided on MongroveClient.events
:
- databaseConnected
- Fires when database is connected
- databaseDisconneced
- Fires when database is disconnected
Errors
- MongroveCommonError
- For simple error
- MongroveValidationError
- When data validation fails on queries
- MongroveServerError
- When data saving fails on database
- MongroveApiError
- When some properties are not valid in client
- MongroveSchemaError
- When there is issue with schema options
Error codes
You can access errors by using enum MongroveErrorCodes
from mongrove/errors
.
- Common:
- M1001 (Con_FailedConnect) - Failed to connect to database
- M1002 (Con_FailedDisconnect) - Failed to disconnect from database
- M2003 (Idx_FailedCreateIndexes) - Failed to create indexes
- Action:
- M4001 (Act_InsertFailed) - Insert failed
- M4002 (Act_ReadFailed) - Read failed
- M4003 (Act_UpdateFailed) - Update failed
- M4004 (Act_ReplaceFailed) - Replace failed
- M4005 (Act_DeleteFailed) - Delete failed
- Other:
- M6001 (MissingFunction) - Missing function
Error handling
Errors can be caught using try/catch
blocks.
import { MongroveValidationError } from "mongrove/errors";
try {
await Users.insertOne({
username: "Jeff",
displayName: "Jo",
email: "[email protected]"
});
} catch (err) {
if (MongroveValidationError.isInstanceOf(err)) {
console.log("There was validation error:", err);
// or
console.log(err.toString()); // for formatted error
// [<code>] <name>: <message> {<meta>}
}
}
Schema Types
You can use utilities to get collection field types from main schema.
There is 2 utility types for extracting type from ready structures:
First, assign collection into variable and then use SchemaFromCollection
to get collection field types. Additionally provide schema type.
import type { SchemaFromCollection } from "mongrove/utils";
const Users = client.db().collection("users");
type UserType = SchemaFromCollection<typeof Users, "input">;
Second, use SchemaType
, that is exported from schema and use SchemaOf
to get collection field types. Provide collection name and schema type.
import type { SchemaOf } from "mongrove/utils";
import { type SchemaType } from "./client/index.ts";
const UserType = SchemaOf<SchemaType, "users", "input">;
Validation
Validation is enabled in all write queries by default. You can opt-out validation by providing validate: false
in query options.
Additional Tips
- Use Typescript: For better code intellisence recommended to use Typescript, so collection names, field names, options, etc. will be automatically hinted.
- Schema Evolution: As your application evolves, you might need to update your schemas. It is simple. Add new fields to schema and it will update in real time without using console commands after each update.
- Partial and Nullable Fields: Depending on your use case, you may have certain fields that are optional or nullable during data manipulation. Utilize
optional
andnullable
options to express these distinctions in your schema. - Handling Default Values: Fields options supports setting default values for schema fields using the
default
option. This can be helpful when inserting documents without providing values for all fields. - Advanced Validation Rules: Mongrove provides various built-in validation methods, such as
min
,max
,regex
, etc., which allow you to impose stricter rules on your data. Utilize these features to enforce specific constraints on your schema fields. - Preventing Duplicate Data: Leverage fields unique index option to prevent duplicate data in your collections. This helps maintain data integrity and consistency.
Contributing
I welcome contributions from the community! If you have any ideas, bug fixes, or feature requests, please feel free to open an issue or submit a pull request on GitHub repository, or share your idea Discord server
Changelog
View the changelog at CHANGELOG.md
License
This library is released under the MIT License.