npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mongo-ts-struct

v1.2.0

Published

Mongoose wrapper for Typescript supports

Downloads

4

Readme

Mongo-TS Struct

About :

Mongoose Documents, by default, are not covered by there schema type. The common solution For type-cover Document object, is an interface for each schema, which lead to redundancy (define the schema once as a schema definition object and once as an interface) and may take more code maintenance.

By defining a class that represent your Mongoose schema, and using Typescript decorators around the class's members, your schema definitions are being stored behind the scene and a type-cover 'native' Mongoose schema is created.

Using mongo-ts your schema need to be written once as a class - and a typed cover schema will be created for you.

References :

Features :

  • Generate class definition to a native mongoose schema.
  • Support OOP writing style by enabling schema class extending.
  • Support class method & static implementation and invoking them through a document and Model.
  • Reduce redundancies by inferring the property type (using reflection).
  • Cover created and fetched documents with the schema class type definition.

Table Of Content :

Installation

npm i mongo-ts-struct -S

Quick Setup and Usage

  1. Assuming you got an existing typescript node app, install the module with npm i mongo-ts-struct -S.

    Make sure the setting on your tsconfig.json file allowing decorators :

    {
        "compilerOptions": {
            "experimentalDecorators": true,
            "emitDecoratorMetadata": true,
            ...
        }
        ...
    }
  2. Moving on to the code, The common mongoose schema setup with typescript on the background is looking something like :

    
    // on file - blog.interface.ts
    
    export interface IBlog {
            title:  string;
            author: string,
            body:   string,
            comments: { body: string, date: Date }[], 
            date: Date
            hidden: boolean,
            meta: {
                votes: number,
                favs:  number
            }
    }
    
    
    // on file - blog.schema.ts
    
    import * as mongoose from 'mongoose';
    import { IBlog } from './blog.interface';
    
    export const blogSchema = new mongoose.Schema<IBlog>({
        title:  { 
            type: String, 
            required: true 
        },
        author: String,
        body:   String,
        comments: { 
            type: [{ body: String, date: Date }], 
            default: [] 
        },
        date: { type: Date, default: Date.now },
        hidden: Boolean,
        meta: {
            votes: Number,
            favs:  Number
        }
    });
    
    
    // on file - blog.model.ts
    
    import * as mongoose from 'mongoose';
    import { blogSchema } from './blog.schema';
    
    blogSchema.methods.recentComments = function(amount: number = 5) {
        const blog = this;
        return blog.comments.sort((a, b) => a - b > 0 ? 1 : -1 ).slice(0, amount);
    }
    
    export const Blog =  mongoose.Model('blogs', blogSchema);
    

    The down falls on this approach :

    • You got three files for a single schema, you can place it all in one file, but that might be consider a bad practice.
    • The redundancy and duplicate definition of your schema are very match noticeable. Maintaining two definitions located in separate files in sync can cause some hide-of-sight bugs.
    • A method defined with schema.methods will not be type covered, meaning, the calling of the method will not be supported by typescript compiler.

    This approach can be reduce now to the following, while avoiding all the mentioned fall becks.

    The interface definition, mongoose-schema and schema-functions are all located under the same roof of a single class, just like the familiar OOP coding style.

    // on file - blog.ts  / a single file will suffice
    
    import { TypedSchema, Prop, Property, ArrayOf, Method ,toModel } from 'mongo-ts-struct';
    
    @TypedSchema() class BlogComments {
        @Prop() body: string;
        @Prop() date: Date;
    }
    
    @TypedSchema() class Blog {
    
        @Prop({ required: true }) title: string;
        @Prop() author: string;
        @Prop() body: string;
        // can use @Property instead of creating comments schema.
        @ArrayOf(BlogComments, { default: [] }) comments: BlogComments[];  
        @Prop({ default: Date.now }) date: Date;
        @Prop() hidden: boolean;
    
        @Property(votes: Number, favs:  Number}) 
            meta: {
                votes: number,
                favs:  number
            }
    
        @Method() recentComments(amount: number = 5) {
            return this.comments.sort((a, b) => a - b > 0 ? 1 : -1 ).slice(0, amount);
        }
    }
    
    export const BlogModel = toModel<Blog, typeof Blog>(Blog, 'blogs');
    
  3. The data queries are all remain the same, the returned documents are cover with the schema-class type, and the schema-class method can be invoked by the returned documents as well;

    async function getBlog(id: string): { blog: Blog, recentComments: Array<BlogComments> } {
        const blogDoc = await BlogModel.findById(id); // blogDoc of Blog type
        const recentComments = blog.recentComments(); // recentComments of BlogComments[] type 
        return { blog: blogDoc, recentComments };
    }

Api Reference

Schema Class Decorator

Overview: Every schema class must be decorate with @TypedSchema. With that, the schema definitions can be collected (using field decorators) from the class, and the schema-class extending can be supported and work properly. Class decorated with @TypedSchema supports multiple function-hooks (stages) in the schema creation, using those hooks you can make your custom changes the schema creation process as you please.

@TypedSchema(config?: TypedSchemaConfig)

Description:

Example:


@TypedSchema()
class Profile {
    @Prop() firstName: string;
    @Prop() lastName: string;
    @Prop() address: string;
    @Prop() age: number;
    @Prop() img: string;
}

@TypedSchema({ options: { timestamps: true } })
class User {
    @Prop({ required: true }) username: string;
    @Prop({ unique: true, required: true }) email: string;
    @Prop() profile: Profile;
    @Prop() hash: string;

    @Method() getEmail() {
        return this.email;
    }
}

const enumKeys = (eType => (Object.values(eType).filter(e => typeof e == 'string')));
enum Permission { 'delete' ,'update', 'insert' }

@TypedSchema()
class Admin extends User {
    @Prop({ default: 2, max: 4 }) role: number;
    @Enum(enumKeys(Permission), {default: ['update', 'insert']}) 
        permissions: string[];
}
/*  Will be mapped to :  */

Admin = {
    username : { 
        type: Schema.Types.String,
        required: true, 
    },
    email: { 
        type: Schema.Types.String,
        unique: true, 
        required: true, 
    },
    profile: { 
        type: {
            firstName: { type: Schema.Types.String },
            lastName: { type: Schema.Types.String },
            address: { type: Schema.Types.String },
            age: { type: Schema.Types.Number },
            img: { type: Schema.Types.String }
        }
    },
    hash: { type: Schema.Types.String },
    role: {  
        type: Schema.Types.Number,
        default: 2, 
        max: 4, 
    },
    permissions: { 
        type: [Schema.Types.String],
        enum: ['delete' ,'update', 'insert'],
        default: ['insert', 'update']
    }
}

interface TypedSchemaConfig

Description: ::TODO:: Definition:

::TODO::

toModel<M, T extends Ctor<M>>(TypedSchemeClass: T, modelName: string,preModelCreation: PreModelCreationFunc<T>)

Description: ::TODO:: Definition:

::TODO::

Schema Creation Hooks

Overview: The process of generating a 'native' schema from schema-class, is divided to stages :

  • first there is a checkup, if a 'native' schema has been generated from that class, if so, its cached and the hook onSchemaCreated is called, else,

  • the schema definition is being constructed from the metadata of the class and the hook onConstructDefinitions is called.

  • after the schema definitions are determent, the 'native' schema is created and the hook onSchemaCreated is called,

  • then the 'native' schema object is bound to any static / class method, that was decorated in the schema-class, and the hook onSchemaBound is called.

OnConstructDefinitions

Description: An interface the schema-class can implement and apply the hook onConstructDefinitions. onConstructDefinitions function executed after the schema definition has been constructed from the schema-class metadata, the schema definition object and decorated static / class methods object is provided as an argument.

Definition:

interface OnConstructDefinitions {
    onConstructDefinitions(schemaDefinitions: object, functions?: object): void
}

OnSchemaCreated

Description: An interface the schema-class can implement and apply the hook onSchemaCreated. onSchemaCreated function executed after the 'native' schema as created, the new schema object is provided as an argument.

Definition:

interface OnSchemaCreated {
    onSchemaCreated(schema: Schema): void
}

OnSchemaBound

Description: An interface the schema-class can implement and apply the hook onSchemaBound. onSchemaBound function executed after static / class method, had been bound to the 'native' schema, the bound schema object is provided as an argument.

Definition:

interface OnSchemaBound {
    onSchemaBound(schema: Schema): void
}

OnSchemaCached

Description: An interface the schema-class can implement and apply the hook onSchemaCached. onSchemaCached function executed only if a 'native' schema has been created from the schema-class (and can be cached), in case the schema can be cached non of the other hooks will ran. The cached schema object is provided as an argument.

Definition:

interface OnSchemaCached {
    onSchemaCached(schema: Schema): void
}

Class Members Decorators

Overview: Mongo-TS uses field decorators to collect data regard the decorated class members, with that data the member's schema definition is created and mapped to relevant property on the generated schema.

@Prop(definition?: Partial<PropertyDefinition>)

Description: Decorator that infer the type's constructor of the decorated property using reflection, mapped it as the type value of the property schema definition.

Example:

class User ... {
    @Prop({ required: true, unique: true, match: /[a-z0-9]+@[a-z]+\.[a-z]+/ })
        email: string;
    ...
}
/*  Will be mapped to :  */

User = {
    email: { 
        type: Schema.Types.String,
        required: true, 
        unique: true, 
        match: /[a-z0-9]+@[a-z]+\.[a-z]+/ 
    }
    ...
}

@Ref(modelName: string, definition?: Partial<PropertyDefinition>)

Description: Decorator that define a ref type property by a provided modal name.

Example:

class User ... {
    @Ref('territory'); 
        territory: Territory | ObjectId; // assume 'Territory' in a defined type / class
    ...
}
/*  Will be mapped to :  */

User = {
    territory: { 
        ref: 'territory'
        type: Schema.Types.ObjectId,
    }
    ...
}

@ArrayRef(modelName: string, definition?: Partial<PropertyDefinition>)

Description: Decorator that define an array ref type property by a provided modal name.

Example:

class User ... {
    @ArrayRef('post', { default: [] }); 
        posts: Post[] | ObjectId[]; // assume 'Post' in a defined type / class
    ...
}

/*  Will be mapped to :  */

User = {
    posts: {
        type: [{ ref: 'post' type: Schema.Types.ObjectId }],
        default: []
    }
    ...
}

@ArrayOf(type: SupportedTypes | Function, definition: Partial<PropertyDefinition>)

Description: Decorator that get SupportedTypes (one of the string values string | number | boolean | any) as a type indicator, or a constructor type function of a schema-class (decorated with @TypedSchema), and define an array of that type. An array type field can be inferred using reflection but currently the type of that array can't be detect.

Example:

class User ... {
    @ArrayOf('string', { default: [] }); 
        tokens: string[],
    ...
}

/*  Will be mapped to :  */

User = {
    tokens: {
        type: [Schema.Types.String],
        default: []
    }
    ...
}

@Enum(enumKeys: Array<string>, definition?: Partial<PropertyDefinition>)

Description: Decorator that define an Enum type property by a provided enum keys array (an array of string). The property type can be the enum type or an array of that enum, the @Enum will infer and map the property type accordingly.

Example:

// helper, take enum type and return his keys as an array.
const enumKeys = (eType => (Object.values(eType).filter(e => typeof e == 'string')));

enum Permission { 'delete' ,'update', 'insert' }

class User ... {
    @Enum(enumKeys(Permission), { default: ['insert'] }); 
        permissions: Permission[];
    ...
}

/*  Will be mapped to :  */
User = {
    permissions: {
        type: [Schema.Types.String],
        enum: ['delete' ,'update', 'insert'],
        default: ['insert']
    }
    ...
}


enum Gender { 'female' ,'male', 'other' }

class Profile ... {
    @Enum(enumKeys(Gender), { required: true }); 
        gender: Gender;
    ...
}

/*  Will be mapped to :  */
Profile = {
    gender: {
        type: Schema.Types.String,
        enum: ['female' ,'male', 'other'],
        required: true
    }
    ...
}

@Property(type: any, definition?: Partial<PropertyDefinition>)

Description: Decorator that allows a free / custom definition of of the decorated property. Useful in any case that not supported by an out-of-the-box decorator. Note: In must cases the @Property decorator will be used, a duplication of the field type definition will be made, there for a more elegant approach will be to create a separate schema-class of for that field and decorate it with @Prop.

Example:

@TypedSchema()
class User {
    @Prop({ required: true }) username: string;
    @Property({ firstName: String; lastName: String; address: String; age: Number; img: String; }) 
    profile: { firstName: string; lastName: string; address: string; age: number; img: string; }
}

/*  Will be mapped to :  */
User = {
    username: {
        type: String,
    },
    profile: 
        type: {
            firstName: String, 
            lastName: String, 
            address: String,
            age: Number, 
            img: String
        }
    }
    ...
}

Compositions

Decorators by nature can be compose on top of each other, with decorator that set a specific definition attribute it can make more sense to utilize that behaviors. An option for setting a class member's definition can be by composing specific-attribute decorators.

@Default(value: boolean = true)

Description: Decorator that set the default definition attribute to the provided value.

Example:

@TypedSchema()
class User {  
    @Prop() 
    @Default(true)  
        subscribed: boolean;
}

@Required(value: (boolean | string) = true)

Description: Decorator that set the required definition attribute to the provided value.

Example:

@TypedSchema()
class User {   
    @Prop() 
    @Required()  
        email: string;
}

@Unique(value: boolean = true)

Description: Decorator that set the unique definition attribute to the provided value.

Example:

@TypedSchema()
class User {   
    @Prop() 
    @Unique()  
        email: string;
}

@Match(value: RegExp | string)

Description: Decorator that set the match definition attribute to the provided value.

Example:

@TypedSchema()
class User {
    @Prop() 
    @Match(/^[\w\.-]+@[\w-]+\.[\w\.-]+$/) 
        email: string;
}

Static & Class Method Decorators

Overview Mongo-TS uses method decorators to reference and apply the decorated method (class method or static method) to the document and Model.

@Method()

Description: Decorator that define a class method as a schema method for any document to use.

Example:

@TypedSchema({ options: { timestamps: true } })
class User {
    @Prop({ required: true }) first: string;
    @Prop({ required: true }) last: string;

    @Method() getFullName() {
        const caps = (s) => s.charAt(0).toUpperCase() + s.slice(1);
        return `${caps(this.first)} ${caps(this.last)}`; 
    } 
}


const UserModel = toModel<User, typeof User>(User, 'users');

UserModel.findById(id).then((user) => {
    console.log(user.getFullName()); // will print the user full name 
});

@Static()

Description: Decorator that define a static method as a schema method for any model to use.

Example:

@TypedSchema({ options: { timestamps: true } })
class User {
    @Prop({ required: true }) first: string;
    @Prop({ required: true }) last: string;

    @Static() static async searchByName(searchValue: string) {
        const _this = UserModel;

        const fieldsForSearch = ['first', 'last'];
        const toPipeLine = ( 
            (s: string) => 
                fieldsForSearch.map(f => ({ [f]: { $regex: searchValue, $options: 'i' } })) 
        );
        
        const searchResult = await _this.aggregate([ { 
            $or: [
                { $text:  {  $search: new RegExp(searchValue) } },
                ... toPipeLine(searchValue)
            ]
        } ]).exec();

        return searchResult;
    }
}


const UserModel = toModel<User, typeof User>(User, 'users');


// calling this method in a static like syntax - will be supported in compile time as well as run time!
UserModel.searchByName('bob').then((users) => {
    try{
        console.log(users); 
    } catch(e) {
        console.log(e); 
    }
});

Important ! Document object that been 'leaned' will not be able to invoke any of his bounded class methods, altho, in compile time, the method under the document's type can be access.

import { TypedSchema, Prop, Method, toModel } from 'mongo-ts';

@TypedSchema({ options: { timestamps: true } })
class User {
    @Prop({ required: true }) username: string;
    @Prop({ unique: true, required: true }) email: string;

    @Method() getEmailAccountProvider() {
        const email = this.email;
        return email.replace(/.+\@(.+)\.[a-z]+$/, '$1');
    }
}

const UserModel = toModel<User, typeof User>(User, 'users');

UserModel.findById(id).lean().then((user) => {
    try{
        console.log(user.getEmailAccountProvider()); // will throw as error.
    } catch(e) {
        console.log(e); // log : "user.getEmailAccountProvider is not a function"
    }
});

Note: If .lean() was not chained before the .then() than the method user.getEmailAccountProvider() would have been called as expected.

Custom Default Schema Definition

Overview ::TODO::

Fallbacks

This module works best with flat schemas (zero redundancies). The solution for multilayered schema, is to cover each complex (let say, more then three members) layer with a class and use it in the parent layer.

E.g, you could write your class like :

@TypedSchema()
class BaseUser {
    @Prop({ required: true }) name: string;
    @Prop({ unique: true, required: true }) email: string;
    @Prop() hash: string;
    @Property({ firstName: String; lastName: String; address: String; age: Number; img: String; }) 
    profile: { firstName: string; lastName: string; address: string; age: number; img: string; }

    @Method() getEmail() {
        return this.email;
    }
}

You can see that there is a repeating definition of the member profile - that is the redundancy we aim to loos. So by separating this class in to two layers, you can elegantly write it like :


@TypedSchema()
class Profile {
    @Prop() firstName: string;
    @Prop() lastName: string;
    @Prop() address: string;
    @Prop() age: number;
    @Prop() img: string;
}


@TypedSchema()
class BaseUser {
    @Prop({ required: true }) name: string;
    @Prop({ unique: true, required: true }) email: string;
    @Prop() profile: Profile;
    @Prop() hash: string;

    @Method() getEmail() {
        return this.email;
    }
}