mongoose-explore
TypeScript icon, indicating that this package has built-in type declarations

4.3.2 • Public • Published

mongoose-explore

an opinionated and highly customizable admin interface interface generated using pug to explore your mongoose models. strongly inspired by adminjs

why not adminjs?

  1. you don't want a full-blown admin interface. just a simple database explorer to view and manipulate your data
  2. adminjs is esm only
  3. all operations trigger your schema's validations and hooks, if any

installation

using npm

npm install pug mongoose-paginate-v2 mongoose-explore

using yarn

yarn add pug mongoose-paginate-v2 mongoose-explore

using pnpm

pnpm add pug mongoose-paginate-v2 mongoose-explore

prerequisites

  1. set pug as view engine
  2. enable url-encoded parsing
  3. configure mongoose-paginate-v2 plugin on all your models, and rename the properties' labels from using camel case to snake case, as follows:
paginate.paginate.options = {
  customLabels: {
    nextPage: "next_page",
    prevPage: "prev_page",
    totalDocs: "total_docs",
    totalPages: "total_pages"
  }
};

it's also imperative you do not modify the default docs property

  1. do not disable mongoose's schema types casting. specifically for String, Number, Boolean, and Date as internally Model.castObject() is used to accurately convert url-encoded data to align with the corresponding model's schema when creating and editing documents
  2. if your application utilizes helmet middleware, allow the unsafe-inline script source directive in your content security policy, as follows:
app.use(helmet({
  contentSecurityPolicy: {
    useDefaults: true,
    directives: {
      "script-src": ["'unsafe-inline'", "https://cdn.jsdelivr.net"]
    }
  }
}));

simple usage

import express from "express";
import mongoose from "mongoose";
import { MongooseExplorer } from "mongoose-explore";
import paginate from "mongoose-paginate-v2";

mongoose.plugin(paginate);

paginate.paginate.options = {
  customLabels: {
    nextPage: "next_page",
    prevPage: "prev_page",
    totalDocs: "total_docs",
    totalPages: "total_pages"
  }
};

const app = express();

app.set("view engine", "pug");
app.use(express.urlencoded());

const explorer = new MongooseExplorer({ mongoose });

app.use(explorer.rootpath, explorer.router());

(async () => {
  await mongoose.connect(url);

  app.listen(port);
})();
voila! you now have an admin interface interface on /admin/explorer to explore and manipulate your mongoose models

configuration

all configuration options

interface MongooseExplorerOptions {
  mongoose: typeof mongoose;
  rootpath?: string;
  datetimeformatter?: (date: Date) => string;
  explorables?: string[];
  query_runner?: boolean | string[];
  fallback_value?: string;
  version_key?: string;
  show_indexes?: string;
  timestamps?: {
    created?: string;
    updated?: string;
  };
  widgets?: (
    | {
        title: string;
        type: "stat";
        resolver: () => Promise<string | number>;
        render?: (value: string | number) => string;
      }
    | {
        title: string;
        type: "tabular";
        resolver: () => Promise<Record<string, any>>;
        header?: boolean;
      }
    | {
        title: string;
        type: "doughnut chart";
        resolver: () => Promise<Record<{
          labels: string[];
          dataset: {
            label?: string;
            data: number[];
          }[];
        }>;
      }
    | {
        title: string;
        type: "pie chart";
        resolver: () => Promise<{
          labels: string[];
          dataset: {
            label?: string;
            data: number[];
          }[];
        }>;
      }
    | {
        title: string;
        type: "line chart";
        resolver: () => Promise<{
          labels: string[];
          dataset: {
            label: string;
            data: number[];
          }[];
        }>;
      }
    | {
        title: string;
        type: "radar chart";
        resolver: () => Promise<{
          labels: string[];
          dataset: {
            label: string;
            data: number[];
          }[];
        }>;
      }
    | {
        title: string;
        type: "bar chart";
        resolver: () => Promise<{
          labels: string[];
          dataset: {
            label: string;
            data: number[];
          }[];
        }>;
      }
    | {
        title: string;
        type: "custom";
        render: (ds: DesignSystem) => string | Promise<string>;
        wrapper_style?: string;
      }
  )[];
  resources: Record<string, {
    explorable?: string;
    creatable?: boolean;
    deletable?: boolean | ((doc: mongoose.LeanDocument) => boolean);
    editable?: boolean | ((doc: mongoose.LeanDocument) => boolean);
    sortable?: boolean;
    limit?: number;
    properties?: Record<string, {
      label?: string;
      editable?: boolean;
      sortable?: boolean;
      viewable?: boolean;
      filterable?: boolean;
      required?: boolean | ((value?: any) => boolean);
      creatable?: boolean;
      textarea?: boolean;
      as_image?: boolean;
      renderers?: {
        list?: (value: any) => string;
        create?: (enumvalues: any[] | null) => string;
        filter?: (enumvalues: any[] | null) => string;
        view?: (value: any) => string;
        edit?: (params: { value: any; enumvalues: any[] | null }) => string;
      }
    }>;
    viewables?: string[];
    filterables?: string[];
    creatables?: string[];
    editables?: string[];
    sortables?: string[];
    ref_newtab?: boolean;
    virtuals?: Record<string, (doc: mongoose.LeanDocument) => string>;
    show_indexes?: boolean;
    query_runner?: boolean;
    bulk_delete?: {
      enabled: boolean;
      use_document?: boolean;
    };
    actions?: {
      customs?: {
        bulk?: {
          operation: string;
          handler: (docs: string[] | mongoose.Document[]) => void | Promise<void>;
          as_documents?: boolean;
          guard?: boolean | string;
          element?: {
            variant?: "primary" | "secondary" | "outline" | "danger" | "success" | "warning";
            label?: string;
            style?: string;
            wrapper_style?: string;
          };
        }[];
        document?: {
          operation: string;
          handler: (doc: mongoose.Document) => void | Promise<void>;
          applicable?: (doc: mongoose.Document) => boolean;
          guard?: boolean | string;
          element?: {
            variant?: "primary" | "secondary" | "outline" | "danger" | "success" | "warning";
            label?: string;
            style?: string;
            wrapper_style?: string;
          };
        }[];
      };
      create?: {
        handler?: (body: Record<string, any>) => Promise<mongoose.Document>;
        pre?: (body: Record<string, any>) => void | Promise<void>;
        post?: (doc: mongoose.Document) => void | Promise<void>;
      };
      delete?: {
        handler?: (doc: mongoose.Document) => Promise<mongoose.Document>;
        post?: (doc: mongoose.Document) => void | Promise<void>;
      };
      update?: {
        handler?: (doc: mongoose.Document,body: Record<string, any>) => Promise<mongoose.Document>;
        pre?: (doc_id: string, body: Record<string, any>) => void | Promise<void>;
        post?: (doc: mongoose.Document, body: Record<string, any>) => void | Promise<void>;
      };
    };
    views?: {
      name: string;
      execute: (t: { limit: number; page?: number }) => Promise<{
        docs: Record<string, any>;
        total_docs?: number;
        total_pages?: number;
        page?: number;
        prev_page?: number | null;
        next_page?: number | null;
      }[]>;
      newtab?: boolean;
      limit?: number;
      properties?: Record<string, {
        render: (value: any) => string;
      }>;
      title?: string;
      render?: string | ((t: { data: any[]; ds: DesignSystem }) => string);
      design_system?: boolean;
    }[];
    pages?: {
      path: string;
      pug: string;
      label?: string;
      newtab?: boolean;
      design_system?: boolean;
      locals?: (req: express.Request) => Record<string, any> | Promise<Record<string, any>>;
    }[];
    titles?: {
      home?: string;
      list?: (modelname: string) => string;
      view?: (modelname: string, doc: mongoose.LeanDocument) => string;
      edit?: (modelname: string, doc: mongoose.LeanDocument) => string;
      create?: (modelname: string) => string;
      queryrunner?: (modelname: string) => string;
    };
  }>;
  theming?: {
    colors?: {
      primary?: string;
      secondary?: string;
      danger?: string;
      warning?: string;
      success?: string;
      border?: string;
      text?: string;
    };
    font?: {
      url: string;
      family: string;
    };
  };
}
consider this model
mongoose.model(
  "User",
  new mongoose.Schema({
    email: {
      type: String,
      required: true
    },
    password: {
      type: String,
      required: true
    },
    profile: {
      name: String,
      settings: {
        notifications: Boolean
      }
    },
    role: {
      type: String,
      enum: ["default", "admin"],
      default: "default"
    },
    is_banned: {
      type: Boolean,
      default: false
    },
    created_at: {
      type: Date,
      default: () => new Date()
    },
    updated_at: {
      type: Date,
      default: () => new Date()
    }
  })
);

rootpath

changes the default rootpath /admin/explorer where the interface is mounted

new MongooseExplorer({ mongoose, rootpath: "/some-other-path" });

datetimeformatter

formatted string representation of dates

new MongooseExplorer({ 
  mongoose, 
  datetimeformatter: (date) => datefns.format(date, "MM/dd/yyyy") 
});

defaults to

date.toLocaleString("en-US", {
  hour: "2-digit",
  minute: "2-digit",
  hour12: true,
  day: "2-digit",
  month: "long",
  year: "numeric"
}); // January 1, 1970 at 12:00 AM

explorables

defines which resources should be explorable. if set, only the resources included in it are explorable, rendering any explorable: true setting under individual resource configuration irrelevant

new MongooseExplorer({
  mongoose,
  explorables: ["User"]
});

query_runner

defines whether query runner is enabled for specific resources or for all resources. set to true to enable for all resources, or provide an array of string resource names to enable it selectively for the specified resources, rendering individual query_runner configurations for each resource irrelevant

new MongooseExplorer({
  mongoose,
  query_runner: ["User"]
});

fallback_value

html or string to be rendered if a property has no value. defaults to -

new MongooseExplorer({
  mongoose,
  fallback_value: "<span style='font-style: italic'>???</span>"
});

version_key

the version key of your schema, if enabled. defaults to __v

show_indexes

determines whether indexes for all resources should be shown or not

timestamps

prevents these fields from being modified among other things

new MongooseExplorer({
  mongoose,
  timestamps: {
    /**
     * default
     */
    created: "created_at",
    updated: "updated_at"
  }
});

widgets

  • stat
new MongooseExplorer({
  mongoose,
  widgets: [
    {
      /**
       * represents a statistical widget displaying a single value.
       * useful for showcasing aggregated data or quick insights
       */
      type: "stat",

      /**
       * title of the widget
       */
      title: "Total Users",

      /**
       * a function that resolves and retrieves the statistical value
       */
      resolver: () => Promise.resolve(20000),

      /**
       * custom rendering function for the displayed value.
       * can return a plain string or html string to be rendered instead of
       * the default value (resolver's return)
       */
      render: (value) => `<p style="font-style: italic">${value.toLocaleString()}</p>`
    },
    ...
  ]
});

sample stat widget

  • tabular
{
  type: "tabular",
  title: "Recent Orders",
  resolver: () => Promise.resolve([
    {
      customer: "john",
      product: "laptop",
      quantity: 2,
      date: "2024-03-05 10:30",
      status: "shipped"
    },
    {
      customer: "mary",
      product: "smartphone",
      quantity: 1,
      date: "2024-03-04 15:45",
      status: "delivered"
    },
    ...
  ]),

  /**
   * determines whether table header is shown or not.
   * defaults to 'false'
   */
  header: true
}

sample tabular widget

  • bar chart
{
  type: "bar chart",
  title: "Employee Performance",
  resolver: () => Promise.resolve({
    labels: ["john", "sarah", "michael", "emily", "james"],
    dataset: [
      {
        label: "quarter 1",
        data: [85, 92, 78, 88, 95]
      },
      {
        label: "quarter 2",
        data: [78, 85, 90, 75, 88]
      },
      {
        label: "quarter 3",
        data: [92, 88, 95, 82, 90]
      }
    ]
  })
}

sample bar chart widget

  • pie chart
{
  type: "pie chart",
  title: "Expenses",
  resolver: () => Promise.resolve({
    labels: ["rent", "utilities", "groceries", "entertainment", "others"],
    dataset: [{
      data: [800, 150, 200, 100, 50]
    }]
  })
}

sample pie chart widget

  • doughnut chart
{
  type: "doughnut chart",
  title: "Project Tasks Distribution",
  resolver: () => Promise.resolve({
    labels: ["design", "development", "testing", "documentation"],
    dataset: [{
      data: [25, 40, 20, 15]
    }]
  })
}

sample doughnut chart widget

  • line chart
{
  type: "line chart",
  title: "Project Progress",
  resolver: () => Promise.resolve({
    labels: ["week 1", "week 2", "week 3", "week 4", "week 5"],
    dataset: [
      {
        label: "development progress",
        data: [20, 40, 60, 80, 100]
      },
      {
        label: "degradation progress",
        data: [100, 80, 60, 40, 20]
      }
    ]
  })
}

sample line chart widget

  • radar chart
{
  type: "radar chart",
  title: "Skills",
  resolver: () => Promise.resolve({
    labels: [
      "coding",
      "design",
      "communication",
      "problem solving",
      "time management",
      "teamwork"
    ],
    dataset: [
      {
        label: "team A",
        data: [80, 70, 85, 90, 75, 65]
      },
      {
        label: "team B",
        data: [90, 65, 80, 85, 70]
      }
    ]
  })
}

sample radar chart widget

  • custom

see custom pages section for more details on ds (design system)

{
  type: "custom",
  title: "Custom Widget",
  
  render: (ds) => `<div>${ds.components.button({ label: "click", variant: "primary" })}</div>`,

  /**
   * extra styling to be applied to the wrapper element (widget element).
   */
  wrapper_style: "width: 250px; height: 250px"
}

sample custom widget

resources

new MongooseExplorer({
  mongoose,
  /**
   * each resource corresponds a to mongoose model name
   */
  resources: {
    User: {
      /**
       * determines whether the model is explorable.
       * set to 'false' to disable
       */
      explorable: true,

      /**
       * determines whether new documents can be created for this model.
       * set to 'false' to prevent the creation of new documents
       */
      creatable: true,

      /**
       * determines whether documents of the model are deletable.
       * set to 'false' to prevent the deletion of documents all documents and 'true'
       * to allow deletion of all documents. defaults to 'true'.
       * can also be a function which takes in the mongoose lean document as the argument
       * and should return boolean to either allow or disallow deletion of all documents
       * that meet the specified condition
       */
      deletable: (user) => user.role !== "admin",

      /**
       * determines whether documents are editable.
       * set to 'false' to prevent the edition of all documents and 'true' to allow
       * edition of all documents. defaults to 'true'.
       * can also be a function which takes in the mongoose lean document as the argument
       * and should return boolean to either allow or disallow edition of all documents
       * that meet the specified condition
       */
      editable: true,

      /**
       * enables or disables sorting criteria.
       * set to 'true' to allow sorting (default), or 'false' to prevent sorting
       */
      sortable: true,

      /**
       * number of documents returned per query.
       * can also be changed from the UI.
       * defaults to '15'
       */
      limit: 10,

      /**
       * defines properties which are shown in the UI.
       * this takes precedence over the viewable options on the property level
       */
      viewables: ["_id", "email", "profile.name"],

      /**
       * defines properties which can be used as a filtering criteria.
       * this takes precedence over the filterable options on the property level
       */
      filterables: ["_id", "email", "created_at", "updated_at"],

      /**
       * defines properties which can be created in the UI.
       * this takes precedence over the creatable options on the property level
       */
      creatables: ["email", "password", "profile.name"],

      /**
       * defines properties which can be edited.
       * this takes precedence over the editable options on the property level
       */
      editables: ["profile.name"],

      /**
       * defines properties which can be sorted.
       * this takes precedence over the sortable options on the property level
       */
      sortables: ["created_at"],

      /**
       * determines whether to open a referenced document in a new tab or not.
       * set to 'true' to make referenced documents open in a new tab
       */
      ref_newtab: false,

      /**
       * avoid confusing this with mongoose's virtuals, as they are distinct
       * and separate functionalities.
       *
       * dynamically inject additional properties when listing and viewing
       * documents. when listing documents (i.e, in a table), this will
       * dynamically add a new table header. the header corresponds to the
       * object's key, while the resulting html or string represents the
       * content in the table data cell. likewise, when viewing an individual
       * document, this will dynamically add a new property. the property key
       * corresponds to the object's key, while the resulting html or string
       * represents the content associated with that key
       */
      virtuals: {
        another: (user) => "i will be rendered under 'another' property",

        field: (user) => "<p style='font-style: italic'>so will i, under 'field' property</p>"
      },

      /**
       * determines whether the indexes should be shown or not
       */
      show_indexes: true,

      /**
       * query runner feature enables you to dynamically compose and execute
       * custom queries on the associated mongoose model and instantly view the result. 
       * when enabled, you can interactively input queries and query options.
       * it supports `find`, `findOne`, `aggregate`, `countDocuments`, and
       * `estimatedDocumentCount` mongoose operations.
       * query options are applicable to `find` and `findOne` operations only.
       * not enabled by default
       * 
       * note: the default setting for `lean: true` is applied to `find` and `findOne`
       * operations, and this behavior cannot be overridden
       */
      query_runner: true,

      bulk_delete: {
        /**
         * determines whether bulk deletion of documents is enabled.
         * defaults to 'true'
         */
        enabled: false,
        /**
         * determines how the deletion is performed.
         * if set to 'true', individual documents are deleted using
         * `Document.deleteOne()`. if not specified or set to 'false', the
         * default behavior is to use `Model.deleteMany()`, which triggers the
         * 'deleteMany' hook (if any). if you opt to use `use_document: true`,
         * ensure that your 'deleteOne' hook (if any) is appropriately
         * configured
         *
         */
        use_document: true
      },

      /**
       * configuring each property of your mongoose schema
       */
      properties: {
        password: {
          /**
           * allows you to specify a custom display name for the property key
           * in the UI. when set, this label will be used in place of the
           * default property key when rendering in the interface
           */
          label: "secret",

          /**
           * determines whether password is editable.
           * set to 'false' to prevent the edition of passwords
           */
          editable: true,

          /**
           * determines if the property can be used for sorting criteria
           */
          sortable: true,

          /**
           * determines whether password is shown in the UI.
           * set to 'false' to disallow it from being shown
           */
          viewable: true,

          /**
           * determines whether password can be used as a filtering criteria
           * when querying. set to 'false' to disable
           */
          filerable: true,

          /**
           * determines whether password is a required property when creating
           * and editing. set to 'false' to make it optional.
           * note: this is purely for UI purposes and does not modify it on the
           * schema level.
           * can also be a function which returns a boolean. it takes the value
           * of the property as a function parameter only in the context of editing,
           * and is `undefined` in other contexts
           */
          required: true,

          /**
           * allows the creation of password field in the UI.
           * set to 'false' to disallow
           */
          creatable: true,

          /**
           * specifies whether password should be rendered as a textarea
           * note: this is only considered if property is a String
           */
          textarea: false,
          
          /**
           * if set to 'true', displays the image instead of the url string,
           * assuming the value is a valid image url
           */
          as_image: true,

          /**
           * render functions for generating html specific to various operations.
           */
          renderers: {
            /**
             * function for generating html which will be rendered in this
             * property's field (password, in this case) when listing (i.e, the
             * table view) documents. returned string or html is rendered inside
             * a '<td>' fyi.
             * useful if customization is desired, as the default behavior is to render the value
             */
            list: (value) => `${value}`,

            /**
             * function for generating html which will be rendered in this
             * property's field when creating documents. enum values maybe null
             * or array of values (depending on the value type on the schema).
             * you may loop over the enum and render a select as you wish.
             * ensure that you include the 'name' property
             */
            create: (enumvalues) => `<input type="password" name="password" />`,

            /**
             * function for generating html which will be rendered in this
             * property's field when adding sorting criteria. enumvalues maybe
             * be null or array of values (depending on the value type on the
             * schema). you may loop over the enum and render a select
             * as you wish.
             * ensure that you include the 'name' property
             */
            filter: (enumvalues) => `<input type="text" name="password" />`,

            /**
             * function for generating html which will be rendered in this
             * property's field when viewing an individual document.
             * useful if customization is desired, as the default behavior is to render the value
             * in a <p> tag if value is primitive. it the value is an array or object, the array
             * or object is looped over and displayed in a <ul><li> structure, with the actual
             * values in <span> tags
             */
            view: (value) => `<p>${value}</p>`,

            /**
             * function for generating html which will be rendered in this
             * property's field when editing a document.
             * ensure that you include the 'name' property
             */
            edit: ({ value, enumvalues }) => `<input type="password" name="password" />`
          }
        },

        /**
         * target properties nested within an object by using dot notation
         */
        "profile.settings.notifications": {}
      },

      actions: {
        customs: {
          /**
           * custom actions for multiple selected documents
           */
          bulk: [{
            /**
             * must be unique and contain only lowercase letters and '-'
             */
            operation: "ban-all",

            /**
             * bulk-ban operation implementation.
             * the 'docs' parameter can either be an array of strings
             * containing the ids of the selected documents or an array of
             * lean documents if 'as_documents' is set to 'true'
             */
            handler: (docs) => {},

            /**
             * determines the format of the 'docs' parameter in the bulk
             * operation. if set to 'true', the 'docs' parameter will be an
             * array of lean documents. if set to 'false' or omitted, the
             * 'docs' parameter will be an array of strings containing document
             * ids. defaults to 'false'
             */
            as_documents: true,

            /**
             * window.confirm prompt message before executing handler.
             * if set to 'true' default message "are you sure?" is used
             * otherwise no prompt, and handler is executed immediately.
             * defaults to 'false'
             */
            guard: "are you sure you want to ban these users?",

            /**
             * html button (actually an <a>) the operation is rendered in
             */
            element: {
              /**
               * specifices the visual variant for the button.
               * defaults to 'outline'
               */
              variant: "danger",
              /**
               * label of the operation's button
               * defaults to the operation name
               */
              label: "ban all",

              /**
               * extra styling to the operation's button
               */
              style: "color: red; font-size: 12px",

              /**
               * extra styling to the operation's button wrapper element
               */
              wrapper_style: "border: 1px solid gold"
            }
          }],

          /**
           * custom actions for individual documents
           */
          document: [{
            /**
             * must be unique and contain only lowercase letters and '-'
             */
            operation: "ban-user",

            /**
             * ban-user operation implementation
             */
            handler: async (user) => {
              user.is_banned = true;
              await user.save();
            },

            /**
             * determines whether the action is applicable to the document
             * 
             */
            applicable: (user) => user.role !== "admin",

            guard: "are you sure you want to ban this user?",

            element: {
              variant: "danger",
              label: "ban",
              style: "color: red; font-size: 12px",
              wrapper_style: "border: 1px solid gold"
            }
          }]
        },

        create: {
          /**
           * function to override how documents are created.
           * defaults to `Model.create()`
           */
          handler: (body: req.body) => Promise<mongoose.Document>,

          /**
           * function executed before creating a document. think of it as 'pre
           * hook'. runs before mongoose's validations/hooks, if any
           */
          pre: (body: req.body) => void | Promise<void>,

          /**
           * function executed after creating a document. think of it as 'post
           * hook'. runs after mongoose's post hooks, if any
           */
          post: (user: mongoose.Document) => void | Promise<void>
        },

        delete: {
          /**
           * function to override how documents are deleted. must return the
           * mongoose document. defaults to `Document.deleteOne()`
           */
          handler: (user: mongoose.Document) => Promise<mongoose.Document>,

          /**
           * function executed after deleting a document. think of it as 'post
           * hook'. runs after mongoose's post hooks, if any.
           */
          post: (user: mongoose.Document) => void | Promise<void>
        },

        /**
         * note: if you find that none of your update actions are running, it's
         * due to an empty `req.body`
         */
        update: {
          /**
           * function to override how documents are updated. must return the
           * mongoose document. defaults to `Document.set(body).save()`
           */
          handler: (user: mongoose.Document, body: req.body) => Promise<mongoose.Document>,

          /**
           * function executed before updating a document.
           * think of it as 'pre hook'.
           * runs before mongoose's validations/hooks, if any
           */
          pre: (doc_id: string, body: req.body) => void | Promise<void>,

          /**
           * function executed after updating a document.
           * think of it as 'post hook'.
           * runs after mongoose's post hooks, if any
           */
          post: (user: mongoose.Document, body: req.body) => void | Promise<void>
        }
      }
    }
  }
});
all able's are true by default

views

configuration for dynamic data views in the UI. allows you to define custom queries which dynamically fetch and provide the data for specific sections in the UI. the concept is similar to PostgreSQL views where you dont have to type the query each time you need it

new MongooseExplorer({
  mongoose,
  views: [{
    name: "new users",
    execute: async ({ limit, page = 1 }) => {
      const thirtydays = new Date();
      thirtydays.setDate(thirtydays.getDate() - 30);

      const users = await User.aggregate([
        {
          $match: {
            role: "default",
            created_at: {
              $gte: thirtydays
            }
          }
        },
        {
          $limit: limit
        },
        {
          $addFields: {
            a_new_property: "your value here"
          }
        }
      ]);

      return { docs: users };

      /**
       * if pagination is desired for your query, include the computed values of
       * `total_docs`, `total_pages`, `prev_page`, `next_page` and `page` properties
       * in the return object. this enables pagination controls in the UI
       */
    },

    /**
     * determines if the view is to be opened in a new tab.
     * defaults to 'false'
     */
    newtab: true,

    /**
     * default number of documents returned per query.
     * can be changed from the UI and defaults to 15.
     * accessible via the `limit` variable in the execute function
     */
    limit: 20,

    /**
     * the view's page title
     */
    title: "New Users",

    /**
     * defines the rendering mechanism for the view's data.
     * if a string, it must be an absolute path to a pug file, where
     * 'data' (the return value from execute) and ds (Design system)
     *  are accessible as local variables.
     * if a function, it accepts 'data' (from execute function) and 'ds'
     * (design system) as parameters, and must return html markup as string.
     * 'ds' may be undefined if 'design_system' is set to 'false'
     */ 
    render: path.join(process.cwd(), "assets", "views", "new-users.pug"),

    /**
     * determines whether to inject our default styles to your page/markup
     */
    design_system: true,

    /**
     * configuration of each property from the data returned from the execute query
     */
    properties: {
      a_new_property: {
        /**
         * returned string or html is rendered in this property's field inside a '<td>' tag.
         * useful if customization is desired, as the default behavior is to render the value
         */
        render: (value) => `<span></span>`;
      }
    }
  }]
});

titles

new MongooseExplorer({
  mongoose,
  titles: {
    home: "explorer",
    list: (modelname) => `${modelname} model`,
    view: (modelname, doc) => `view ${modelname} document - ${doc._id}`,
    edit: (modelname, doc) => `edit ${modelname} document - ${doc._id}`,
    create: (modelname) => `create ${modelname} document`,
    queryrunner: (modelname) => `${modelname} query runner`
  }
});

pages

new MongooseExplorer({
  mongoose,
  pages: [
    {
      /**
       * path where the page will be mounted.
       * notice how it's not prefixed with '/'
       */
      path: "custom",

      /**
       * absolute path to the custom page's pug file
       */
      pug: path.join(process.cwd(), "views", "custom.pug"),

      /**
       * label of the page's link element
       * defaults to 'path' value
       */
      label: "Custom",

      /**
       * determines where the page should be opened in a new tab or not.
       * set to 'true' to open the page in a new tab
       */
      newtab: false,

      /**
       * determines whether to inject default styles into your custom page using our design system.
       * this also ships a 'ds' locals object accessible in your pages and exposes 
       * '--primary', '--danger', '--secondary', '--success', '--warning', '--text-color', and
       * '--border-color' css variables
       *
       * ```
       *  type Variant = "primary" | "secondary" | "outline" | "success" | "warning" | "danger";
       * 
       *  const ds = {
       *    font: {
       *      url: "",
       *      family: "",
       *    },
       *    colors: {
       *      primary: "",
       *      secondary: "",
       *      danger: "",
       *      warning: "",
       *      success: "",
       *      text: "",
       *      border: "",
       *    },
       *    styles: {
       *      /**
       *       * default styling for the main content container use in pages. its a <div>
       *       * encapsulating the primary content of each page to ensure consistent styling,
       *       * including a border, fixed width of 400px, centered alignment, and padding
       *       */
       *      contentdiv: ""
       *    },
       *    components: {
       *      title: (t: { label: string; style?: string; }) => <h2>,
       *      link: (t: { label: string; href: string; newtab?: boolean; style?: string; }) => <a>,
       *      dash: (t?: { style?: string; }) => <hr>,
       *      button: (t: { label: string; variant?: Variant; style?: string }) => <button>
       *    }
       *  }
       * ```
       */
      design_system: true,

      /**
       * provide local variables for your custom page. the returned
       * object will be merged with the below default locals object
       *
       * ```
       * const locals = {
       *  /*
       *   * use this function to build links to other custom pages.
       *   * ex: `buildlink("another-custom-page")`
       *   */
       *  buidlink: (path: string) => string,
       *  rootpath: "",
       *  ds: "**see above**"
       * }
       * ```
       */
      locals: (req) => ({ user: req.user })
    }
  ]
});

theming

self-explanatory

new MongooseExplorer({
  mongoose,
  theming: {
    colors: {
      primary: "gold",
      secondary: "purple",
      danger: "red",
      success: "green",
      warning: "yellow",
      border: "gray",
      text: "black"
    },
    font: {
      url: "google font url here",
      /**
       * must be of this format: `"Somefont", sans-serif;`.
       * notice the `;` at the end
       */
      family: "font family here"
    }
  }
});

defaults to

{
  colors: {
    primary: "#4f46e5",
    secondary: "#DCE546",
    danger: "#dc2626",
    success: "#059669",
    warning: "#d97706",
    border: "#4B5563",
    text: "#fff",
  },
  font: {
    url: "https://fonts.googleapis.com/css2?family=Finlandica&display=swap",
    family: `"Finlandica", sans-serif;`
  }
}

background-color is set to #101113, which is a very dark shade and is not customizable. if you choose to customize the colors, be mindful of the background color as it sets the foundation for the overall appearance of the interface

important notes

  1. string filtering supports regex patterns enclosed within / characters. simply place your regex pattern between these delimeters. for example, /doe/i/, gets converted to /doe/i and translated to the mongodb $regex operator
  2. if an error is thrown from any pre hook, the request fails and the operation is not executed; however, if an error is thrown from any post hook, the request still succeeds, and the error is suppressed

warning

using too many filtering criteria or applying them in the wrong way can make your queries slow and expensive. be sure to index your mongo database appropriately to avoid this

limitations

  1. filtering properties of type map is not supported
  2. filtering and editing properties of array of objects is not supported yet
  3. sorting properties of type map and array is not supported
  4. not explicitly tested or designed with schema type mixed in mind. i don't think you should be using it anyway. if any of your properties are of type mixed, you should disable the property by setting viewable: false as not doing so might lead to unexpected behaviors

Package Sidebar

Install

npm i mongoose-explore

Weekly Downloads

36

Version

4.3.2

License

MIT

Unpacked Size

160 kB

Total Files

39

Last publish

Collaborators

  • kamalyusuf