papr Documentation

repository·main·Indexed 19 days ago

https://github.com/plexinc/papr

A lightweight TypeScript library for MongoDB that provides strong validation and type safety by leveraging MongoDB's native JSON Schema validation. It acts as a thin wrapper around the official mongodb NodeJS driver, offloading runtime validation to the database server for all operations, including inserts, updates, and bulkWrite.

Tokens
23.1K
Snippets
96
Records
108
Agent score
66%

What's inside papr

  1. Overview of Papr

    main
    Papr is a lightweight, TypeScript-safe library built around the MongoDB NodeJS driver. It leverages MongoDB's native JSON Schema validation (available in MongoDB 3.6+) to enforce document schema validation at runtime during writes. It provides a familiar API for developers who have used the raw mongodb driver, offering a more structured way to define models and schemas while maintaining high performance.
  2. What is a Model in Papr?

    main

    A Model is the primary public interface in papr for interacting with a specific MongoDB collection. It provides a type-safe wrapper around standard MongoDB collection methods, ensuring that queries, updates, and insertions adhere to a defined schema.

    To create a model, you first define a schema using schema() and then pass it to papr.model() along with the collection name.

    const userSchema = schema({
      active: types.boolean(),
      age: types.number(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    });
    const User = papr.model('users', userSchema);
  3. Configure schema defaults

    main

    The defaults option in the schema function allows you to specify values that should be applied to documents. It supports three modes:

    1. Static Object: A simple object containing default values.
    2. Synchronous Function: A function that returns a default object (e.g., for generating a new Date()).
    3. Asynchronous Function: An async function that returns a Promise resolving to a default object.

    Note on Enums: When using const enums in static defaults, you must use a full type cast to satisfy TypeScript.

    import { schema, types } from 'papr';
    
    // 1. Static defaults with Enum casting
    const statuses = ['processing', 'shipped'] as const;
    type Status = (typeof statuses)[number];
    
    const orderSchema = schema(
      {
        _id: types.number({ required: true }),
        user: types.objectId({ required: true }),
        status: types.enum(statuses, { required: true }),
      },
      {
        defaults: {
          status: 'processing' as Status,
        },
      }
    );
    
    // 2. Synchronous dynamic defaults
    const userSchemaSync = schema({
      active: types.boolean(),
      birthDate: types.date(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    }, {
      defaults: () => ({
        birthDate: new Date(),
      })
    });
    
    // 3. Asynchronous dynamic defaults
    const userSchemaAsync = schema({
      active: types.boolean(),
      birthDate: types.date(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    }, {
      defaults: async () => ({
        birthDate: await Promise.resolve(new Date()),
      })
    });
  4. Understand PaprFilter and compatibility with MongoDB driver

    main

    Papr v11 uses enhanced strict types (PaprFilter and PaprUpdateFilter) to provide type safety for queries and updates, including support for dot notation.

    Important Compatibility Note: PaprFilter is not directly compatible with the standard Filter type from the mongodb driver. If you need to interact directly with a MongoDB collection via model.collection, you must cast your Papr filter to the standard Filter type.

    import { Filter } from 'mongodb';
    import { PaprFilter } from 'papr';
    import User, { UserDocument } from './user';
    
    const filter: PaprFilter<UserDocument> = {
      firstName: 'John',
    };
    
    // Use directly with Papr model methods
    await User.find(filter);
    
    // Cast to Filter when using the underlying driver collection
    await User.collection.find(filter as Filter<UserDocument>);
  5. Compare Papr to Mongoose

    main

    While Mongoose is a full-featured ORM, Papr is designed as a 'paper-thin' layer over the MongoDB driver for performance and simplicity.

    Key differences include:

    • Validation Location: Mongoose validates in the application; Papr uses MongoDB's native JSON Schema validation on the server.
    • Feature Set: Papr deliberately excludes features like populate (joins), conditional default values, and virtuals to maintain speed and simplicity.
    • Validation Coverage: Papr validates all operations (inserts, updates, and bulkWrite) by default.
  6. How Papr provides type safety and validation

    main

    Papr uses a single schema definition to provide two layers of protection:

    1. Compile-time safety: The schemas generate TypeScript types used throughout your application, ensuring your code interacts with documents correctly (including handling query projections).
    2. Runtime safety: The same schemas are converted into JSON schemas and applied directly to your MongoDB collections. This offloads validation to the MongoDB server, ensuring that all operations (inserts, updates, and bulkWrite) are validated at the database level.

    This approach differs from libraries like Mongoose, which perform validation within the application runtime.

  7. Quickstart with Papr

    main

    Papr is a lightweight TypeScript library that provides strong validation and type safety for MongoDB by leveraging MongoDB's native JSON Schema validation. It acts as a thin wrapper around the official mongodb NodeJS driver.

    To use Papr, you initialize it with a MongoDB database instance, define models using papr.model() with a schema, and call updateSchemas() to apply those schemas to the MongoDB server. Once configured, you can use the model to perform queries like .find() with full TypeScript support.

    import { MongoClient } from 'mongodb';
    import Papr, { schema, types } from 'papr';
    
    const papr = new Papr();
    
    const connection = await MongoClient.connect('mongodb://localhost:27017');
    // Initialize papr with a specific database
    papr.initialize(connection.db('test'));
    
    // Define a model with a schema
    const User = papr.model('users', schema({
      age: types.number(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    }));
    
    // Apply the schema to the MongoDB collection
    await papr.updateSchemas();
    
    // Query the collection with type safety
    const johnWick = await User.find({ firstName: 'John', lastName: 'Wick' });
  8. Initialize Papr with MongoDB

    main

    To use Papr, you must connect to a MongoDB server (v3.6+) and initialize the Papr instance with a MongoDB database object using papr.initialize(db). It is also recommended to call papr.updateSchemas() to ensure your MongoDB collections are synchronized with your defined schemas.

    Prerequisites:

    • MongoDB server (v3.6+)
    • NodeJS (v16+)
    import { MongoClient } from 'mongodb';
    import Papr from 'papr';
    
    const papr = new Papr();
    
    export async function connect() {
      const client = await MongoClient.connect('mongodb://localhost:27017');
      // Initialize with a specific database
      papr.initialize(client.db('test'));
      // Sync schemas with MongoDB
      await papr.updateSchemas();
    }
  9. Sync Papr schemas to MongoDB JSON Schema

    main

    To enforce schema validation at the database level, use papr to sync your TypeScript schemas to MongoDB's JSON Schema validator. This is done by calling updateSchema on a model.

    Calling papr.updateSchema(UserModel) issues a collMod command to MongoDB, applying a $jsonSchema validator to the collection. Once applied, any write operations (like update) that violate the schema will either fail (if MongoDB's validationAction is set to error) or log an error (if validationAction is set to warn).

    papr.updateSchema(UserModel);
    papr.updateSchema(UserModel);
  10. Define a model with schema and types

    main

    Models in Papr are created by defining a schema using schema() and the provided types helpers. You can then register this schema with the Papr instance using papr.model(collectionName, schema).

    This approach provides two layers of validation:

    1. TypeScript validation: The generated type from the schema ensures type safety during development.
    2. Runtime validation: MongoDB will validate documents against the schema at runtime, throwing errors for inconsistencies.
    import { types, schema } from 'papr';
    import papr from './papr';
    
    const userSchema = schema({
      age: types.number(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    });
    
    // Export the TypeScript type for use in your application
    export type UserDocument = (typeof userSchema)[0];
    
    // Create the model for the 'users' collection
    const User = papr.model('users', userSchema);
    
    export default User;
  11. Define a schema in Papr

    main

    Schemas in papr define the expected shape of documents in a MongoDB collection, providing both runtime validation (via JSON Schema) and compile-time type safety (via TypeScript).

    To define a schema, use the schema function and the types object. You can specify if a field is required using the { required: true } option within the type definition.

    After defining a schema, you should derive two key components:

    1. The Document Type: A TypeScript type representing the shape of a document in the collection.
    2. The Model: A papr model instance used to interact with the collection.
    import { schema, types } from 'papr';
    
    const papr = new Papr();
    
    const userSchema = schema({
      age: types.number(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    });
    
    // Derive the Document type
    type UserDocument = (typeof userSchema)[0];
    
    // Create the Model
    const UserModel = papr.model('users', userSchema);
    import { schema, types } from 'papr';
    
    const papr = new Papr();
    
    const userSchema = schema({
      age: types.number(),
      firstName: types.string({ required: true }),
      lastName: types.string({ required: true }),
    });
    
    type UserDocument = (typeof userSchema)[0];
    const UserModel = papr.model('users', userSchema);
  12. Migrate Mongoose models to Papr

    main

    When migrating from Mongoose to Papr, note that Papr is not a full ORM. You must manually handle custom instance/static methods, query helpers, and index definitions.

    Key migration differences:

    Timestamps

    Timestamp support is identical to Mongoose. Enable it in the schema options.

    Default Values

    In Mongoose, defaults are defined per-property. In Papr, defaults are defined in the schema options object. Defaults are only applied to paths where no value is set during insertion.

    • Static Defaults: Provide an object in the schema options.
    • Dynamic Defaults: Provide a function in the schema options that returns an object of values.

    Version Key

    Mongoose automatically adds a versionKey (default __v). When migrating, you must either remove this key from your existing MongoDB collections or explicitly include it in your Papr schema to prevent conflicts.