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

@kamalyb/mongoose-explore

v1.0.2

Published

an **opinionated** and highly customizable ~~admin interface~~ interface generated using [pug](https://pugjs.org/api/getting-started.html) to _explore_ your mongoose models. strongly inspired by [adminjs](https://adminjs.co)

Downloads

58

Readme

mongoose explore

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

installation

npm install mongoose-explore
# or
yarn add mongoose-explore
# or
pnpm add mongoose-explore

prerequisites

  1. do not disable mongoose's schema types casting as 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
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      "script-src": ["'unsafe-inline'", "https://cdn.jsdelivr.net"],
      "script-src-attr": ["'unsafe-inline'"]
    }
  }
}));

quick start

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

const app = express();

const explorer = new MongooseExplorer({ mongoose });

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

await mongoose.connect();

app.listen(port);

voila! you now have an ~~admin interface~~ interface on /admin/explorer to explore and manipulate your mongoose models

configuration options

the MongooseExplorer constructor accepts a configuration object with the following options:

mongoose (required)

the mongoose instance to use for database operations

rootpath

  • type: string
  • default: /admin/explorer
  • description: the base URL path where the explorer will be mounted
const explorer = new MongooseExplorer({
  mongoose,
  rootpath: "/custom-admin"  // access via http://your-app.com/custom-admin
});

datetimeformatter

  • type: (date: Date) => string
  • default: 24-hour format with short month (e.g., "Mar 22, 2024, 13:45")
  • description: formatted string representation of dates

// default implementation
const explorer = new MongooseExplorer({
  mongoose,
  datetimeformatter: (date) => date.toLocaleString("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false
  })
});

explorables

  • type: string[]
  • description: list of model names to expose in the explorer. if not provided, all models will be available. if set, only the resources included in it are available, rendering any explorable: true setting under individual resource configuration irrelevant
const explorer = new MongooseExplorer({
  mongoose,
  explorables: ["User", "Post", "Comment"]  // only these models will be accessible
});

query_runner

  • type: boolean | string[]
  • default: false
  • description: enable custom query execution and instantly view the result. can be limited to specific models. it supports find, findOne, aggregate, countDocuments, and estimatedDocumentCount mongoose operations. query options are applicable to find and findOne operations only
// enable for all models
const explorer = new MongooseExplorer({
  mongoose,
  query_runner: true
});

// enable only for specific models
const explorer = new MongooseExplorer({
  mongoose,
  query_runner: ["User", "Post"]
});

note: the default setting for lean: true is applied to find and findOne operations and this behavior cannot be overriden

fallback_value

  • type: string
  • default: -
  • description: string or html content to display when a property has no value (null or undefined)
const explorer = new MongooseExplorer({
  mongoose,
  
  // plain text
  fallback_value: "N/A",

  // or html for styled content
  fallback_value: "<span style="color: orange">no data</span>"
});

timestamps

  • type: Timestamps
  • default: { created: "created_at", updated: "updated_at" }
  • description: maps your mongoose schema's timestamp field names to prevent their modification in the interface among other things

Timestamps definition

interface Timestamps {
  created?: string;
  updated?: string;
}
const explorer = new MongooseExplorer({
  mongoose,
  timestamps: {
    created: "insertedAt",    // if your schema uses insertedAt
    updated: "modifiedAt"     // if your schema uses modifiedAt
  }
});

titles

  • type: Titles
  • default: 'explorer'
  • description: customize html page titles for different views and operations in the interface

Titles definition

interface Titles {
  dashboard?: string;
  list?: (modelname: string) => string;
  view?: (modelname: string, doc: object) => string;
  edit?: (modelname: string, doc: object) => string;
  create?: (modelname: string) => string;
  queryrunner?: (modelname: string) => string;
}
const explorer = new MongooseExplorer({
  mongoose,
  titles: {
    // static title for dashboard
    dashboard: "Admin Dashboard",

    // dynamic titles using model names and documents
    list: (modelname) => `${modelname} List`,
    view: (modelname, doc) => `${doc.name} - ${modelname} Details`,
    edit: (modelname, doc) => `Edit ${modelname}: ${doc.name}`,
    create: (modelname) => `New ${modelname}`,
    queryrunner: (modelname) => `Query ${modelname}`
  }
});

examples of generated titles:

// for a User model with document { name: "John Doe" }
{
  dashboard: "Admin Dashboard"
  list: "User List"
  view: "John Doe - User Details"
  edit: "Edit User: John Doe"
  create: "New User"
  queryrunner: "Query User"
}

theming

  • type: ThemingOptions
  • description: customize the visual appearance of the interface

ThemingOptions definition

interface ThemingOptions {
  colors?: Colors;
  font?: Font;
}

interface Colors {
  primary?: string;
  secondary?: string;
  danger?: string;
  warning?: string;
  success?: string;
  border?: string;
  text?: string;
  custom?: Record<string, `#${string}`>;
}

interface Font {
  url: string;
  family: string;
}

type Color =
  | "primary"
  | "secondary"
  | "success"
  | "danger"
  | "warning"
  | "dark"
  | "gray"
  | "white"
  | (string & {}); // allows custom color strings

type Variant = "filled" | "outline" | "light" | "subtle";

type Size =
  | "xs"
  | "compact-xs"
  | "sm"
  | "compact-sm"
  | "md"
  | "compact-md"
  | "lg"
  | "compact-lg"
  | "xl"
  | "compact-xl";

color customization

  • all color inputs generate CSS variables and classes
  • creates var(--color-{name}) and .color-{name}
  • background fixed at #101113 (dark shade, non-customizable)
  • custom colors can be added through custom object

default theme

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

css variables

the following CSS variables are available throughout the interface:

/* typography */
--font-family
--font-size

/* colors */
--color-primary
--color-secondary
--color-success
--color-danger
--color-warning
--color-dark
--color-gray
--color-{custom-name}  /* generated for each custom color */

/* spacing */
--spacing-xs
--spacing-sm
--spacing-md
--spacing-lg
--spacing-xl

/* border radius */
--radius-xs
--radius-sm
--radius-md
--radius-lg
--radius-xl

css classes

button classes

the .button class must be combined with variant, color, and size modifiers for styling:

/* base class (no styling by itself) */
.button

/* size modifiers */
.button.size-xs
.button.size-sm
.button.size-md
.button.size-lg
.button.size-xl

/* compact variants */
.button.size-compact-xs  /* smaller version of xs */
.button.size-compact-sm  /* smaller version of sm */
.button.size-compact-md  /* smaller version of md */
.button.size-compact-lg  /* smaller version of lg */
.button.size-compact-xl  /* smaller version of xl */

/* variant modifiers */
.button.variant-filled
.button.variant-light
.button.variant-outline
.button.variant-subtle

/* color modifiers - generated for all predefined and custom colors */
.button.color-primary
.button.color-secondary
.button.color-success
/* etc... */
form element classes
/* input */
.input
.input.size-xs
.input.size-sm
.input.size-md
.input.size-lg
.input.size-xl

/* textarea */
.textarea
.textarea.size-xs
.textarea.size-sm
.textarea.size-md
.textarea.size-lg
.textarea.size-xl

/* select */
.select
.select.size-xs
.select.size-sm
.select.size-md
.select.size-lg
.select.size-xl

configuration example

  • customize theme colors and font
  • add custom colors with custom property
  • override default theme settings
const explorer = new MongooseExplorer({
  mongoose,
  theming: {
    colors: {
      primary: "#4f46e5",
      custom: { 
        coral: "#FF7F50" 
      }
    },
    font: {
      family: '"Custom Font", sans-serif',
      url: "https://fonts.googleapis.com/css2?family=CustomFont&display=swap"
    }
  }
});

// button html example
<button class="button color-primary variant-filled size-compact-sm">
  Click me
</button>

// input html example
<input class="input size-md" type="text">

version_key

  • type: string
  • default: __v
  • description: the version key of your schema, if enabled

show_indexes

  • type: boolean
  • default: false
  • description: controls visibility of model indexes in the interface

bulk_delete

  • type: boolean
  • default: false
  • description: enable or disable bulk deletion of documents

dashboard

  • type: DashboardOptions
  • description: customize the dashboard page content and styling. by default, the dashboard is empty. any html content returned by the render function will be wrapped in the interface layout to maintain consistent navigation

DashboardOptions definition

interface DashboardOptions {
  /**
   * function to generate dashboard's html content 
   */
  render?: (req: Request, theme: Theme) => string | Promise<string>;
  /**
   * additional html to injext into the <head> section.
   * useful for including external resources like stylesheets, remote scripts, etc
   */
  head?: string;
  /**
   * javascript for the dashboard page.
   * can either be an absolute file path (e.g., "C:/users/john/project/dashboard.js")
   * or inline javascript as a string
   */
  js?: string;
  /**
   * css styles for the dashboard page.
   * can either be an absolute file path (e.g., "C:/users/john/project/dashboard.css")
   * or inline css as a string
   */
  css?: string;
  /**
   * custom css styles for different dashboard components.
   * all style properties accept css rules as strings
   */
  styles?: {
    /**
     *  style container that holds all dashboard widgets
     */
    widgets?: string;
    /**
     * style main wrapper containing both rendered html and widgets container
     */
    wrapper?: string;
    /**
     * style statistical widget containers (displays single value metrics)
     */
    stat?: string;
    /**
     *  style pie chart widget containers
     */
    piechart?: string;
    /**
     * style bar chart widget containers
     */
    barchart?: string;
    /**
     * style tabular data widget containers
     */
    tabular?: string;
    /**
     * style doughnut chart widget containers
     */
    doughnutchart?: string;
    /**
     *  style line chart widget containers
     */
    linechart?: string;
    /**
     * style radar chart widget containers
     */
    radarchart?: string;
  };
}

example usage

const explorer = new MongooseExplorer({
  mongoose,
  dashboard: {
    render: async (req, theme) => {
      const count = await User.countDocuments();
      
      return `
        <div class="dashboard-stats">
          <div class="stat-card">
            <h3>Total Users</h3>
            <p>${count}</p>
          </div>
        </div>
      `;
    },

    // include external resources
    head: `
      <link rel="stylesheet" href="https://cdn.example.com/custom-styles.css">
      <script src="https://cdn.example.com/animation-library.js"></script>
    `,

    // custom javascript (using absolute path)
    js: "C:/users/john/project/dashboard.js",
    // or inline javascript
    js: `
      // add custom event handlers
      document.querySelector(".stat-card).addEventListener("click", () => {
        //
      });
    `,
    
    css: `
      .stat-card {
        border-radius: var(--radius-md);
        box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
      }
    `,

    // custom styles for different components
    styles: {
      // style the wrapper containing everything
      wrapper: `
        padding: 2rem;
        background: #f5f5f5;
        display: grid;
        gap: 1rem;
      `,
      // style the widgets container
      widgets: `
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
        gap: 1rem;
        padding: 1rem;
      `,
      // style stat widget containers
      stat: `
        background: white;
        padding: 1.5rem;
        border-radius: 8px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      `,
      // style chart widget containers
      piechart: `
        background: white;
        padding: 1rem;
        border-radius: 8px;
        min-height: 300px;
      `
    }
  }
});

widgets

  • type: Widget[]
  • description: An array of widgets to display on the dashboard. each widget can be of different types (stat, bar chart, pie chart, tabular, doughnut chart, line chart, radar chart, custom)

Widget definition

type Widget =
  | StatWidget
  | BarChartWidget
  | PieChartWidget
  | TabularWidget
  | DoughnutWidget
  | LineWidget
  | RadarWidget
  | CustomWidget;

interface StatWidget {
  type: "stat";
  title: string;
  resolver: () => Promise<string | number>;
  /**
   * custom rendering function for the displayed value.
   * can return a plain string or html string to be rendered
   */
  render?: (value: string | number, theme: Theme) => string;
}

interface BarChartWidget {
  type: "bar chart";
  title: string;
  resolver: () => Promise<{
    labels: string[];
    datasets: {
      label: string;
      data: number[];
    }[];
  }>;
}

interface PieChartWidget {
  type: "pie chart";
  title: string;
  resolver: () => Promise<{
    labels: string[];
    data: number[];
  }>;
}

interface TabularWidget {
  type: "tabular";
  title: string;
  resolver: () => Promise<Record<string, any>[]>;
  /**
   * determines whether table header is displayed
   */
  header?: boolean;
}

interface DoughnutWidget {
  type: "doughnut chart";
  title: string;
  resolver: () => Promise<{
    labels: string[];
    data: number[];
  }>;
}

interface LineWidget {
  type: "line chart";
  title: string;
  resolver: () => Promise<{
    labels: string[];
    datasets: {
      label: string;
      data: number[];
    }[];
  }>;
}

interface RadarWidget {
  type: "radar chart";
  title: string;
  resolver: () => Promise<{
    labels: string[];
    datasets: {
      label: string;
      data: number[];
    }[];
  }>;
}

interface CustomWidget {
  type: "custom";
  title: string;
  render: (theme: Theme) => string | Promise<string>;
  element?: {
    style?: string;
  };
}

example usage

const explorer = new MongooseExplorer({
  mongoose,
  widgets: [
    {
      type: "stat",
      title: "Total Users",
      resolver: async () => {
        return await User.countDocuments();
      },
      render: (value, theme) => `
        <div style="text-align: center">
          <h3 style="color: ${theme.primary}">${value}</h3>
          <p>registered users</p>
        </div>
      `
    },

    {
      type: "bar chart",
      title: "Monthly Signups",
      resolver: async () => {
        const data = await User.aggregate([
          {
            $group: {
              _id: { $month: "$created_at" },
              count: { $sum: 1 }
            }
          }
        ]);

        return {
          labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
          datasets: [{
            label: "New Users",
            data: data.map((d) => d.count)
          }]
        };
      }
    },

    {
      type: "custom",
      title: "Custom Content",
      render: (theme) => `
        <div style="text-align: center; padding: 1rem;">
          <h3 style="color: ${theme.primary}">Custom Widget</h3>
          <p>This is a fully custom widget!</p>
        </div>
      `,
      element: {
        style: "background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"
      }
    }
  ]
});

resources

  • type: ResourcesOptions
  • default: {}
  • description: configures how each mongoose model is handled in the explorer

ResourcesOptions definition

type ResourcesOptions = Record<string, ResourceOptions>;

interface ResourceOptions {
  /**
   * whether the resource is explorable
   * 
   * @default true
   */
  explorable?: boolean;
  /**
   * allow creating new documents
   * 
   * @default true
   */
  creatable?: boolean;
  /**
   * allow document deletion
   * 
   * @default true
   */
  deletable?: boolean | ((doc: any) => boolean);
  /**
   * allow document editing
   * 
   * @default true
   */
  editable?: boolean | ((doc: any) => boolean);
  /**
   * allow sorting
   * 
   * @default true
   */
  sortable?: boolean;
  /**
   * number of documents displayed per page.
   * can be changed from the interface
   */
  limit?: number;
  /**
   * properties which are shown in the interface.
   * this takes precedence over the `viewable` option at the property level
   */
  viewables?: string[];
  /**
   * properties which can be used as a filtering criteria.
   * this takes precedence over the `filterable` option at the property level
   */
  filterables?: string[];
  /**
   * properties which can be created.
   * this takes precedence over the `creatable` option at the property level
   */
  creatables?: string[];
  /**
   * properties which can be edited.
   * this takes precedence over the `editable` option at the property level
   */
  editables?: string[];
  /**
   * properties which can be sorted.
   * this takes precedence over the `sortable` option at the property level
   */
  sortables?: string[];
  /**
   * open referenced documents in new tab
   * 
   * @default false
   */
  ref_newtab?: boolean;
  /**
   * custom computed fields that appear in the interface
   */
  virtuals?: Record<string, (doc: any) => string>;
  /**
   * display model indexes
   * 
   * @default false
   */
  show_indexes?: boolean;
  /**
   * enable custom query execution and instantly view the result
   */
  query_runner?: {
    /**
     * this takes precedence over the option `query_runner` option at the explorer level
     * 
     * @default false
     */
    enabled?: boolean;
    /**
     * predefined queries that can be run with a single click.
     * the concept is similar to PostgreSQL views where you dont have to type the query each time you need it
     */
    presets?: {
      name: string;
      resolver: () => Promise<any>
    }[]
  };
  bulk_delete?: {
    /**
     * enables bulk deletion of documents.
     * this takes precedence over `bulk_delete` option at the explorer level
     * 
     * @default false
     */
    enabled?: boolean;
    /**
     * determines how deletion is performed.
     * documents are deleted using `Document.deleteOne()` if true, otherwise the
     * default behavior is `Model.deleteMany()`
     * 
     * @default false
     */
    use_document?: boolean;
  };
  /**
   * cascade delete behavior for dependent documents.
   * if a function is passed, the session is defined if mongodb is in a replica set.
   * uses transaction to delete documents if mongodb is in a replica set or `Promise.all` otherwise.
   * currently not supported for bulk delete
   */
  cascade_delete?: {
    /**
     * related model to delete from
     */
    model: mongoose.Model;
    relation: {
      /**
       * field in the current model
       */
      local_field: string;
      /**
       * field in the related model
       */
      foreign_field: string;
    }
  }[] | ((doc: object, session?: mongodb.ClientSession) => Promise<void>);
  /**
   * document operations and lifecycle
   */
  actions?: Actions;
  /**
   * individual property configuration
   */
  properties?: Record<string, PropertyOptions>;
}

interface Actions {
  custom?: CustomActions;
  create?: CreateActions;
  delete?: DeleteActions;
  update?: UpdateActions;
}

interface CustomActions {
  /**
   * actions that can be performed on multiple selected documents
   */
  bulk?: {
    /**
     * must be unique
     */
    operation: string;
    /**
     * operation implementation
     */
    execute: (doc: any) => void | Promise<void>;
    /**
     * window.confirm prompt message before executing handler.
     * if true default message "are you sure?" is used, otherwise no prompt
     * and handler is executed immediately
     * 
     * @default false
     */
    guard?: boolean | string;
    element?: {
      /**
       * visual variant for the button, `filled`, `outline`, `light`, or `subtle`
       * 
       * @default outline
       */
      variant?: Variant;
      /**
       * color of the button. can be one of the predefined colors (primary, secondary, success,
       * danger, warning, dark, gray, white) or any custom color defined in theming.colors.custom
       * 
       * @default white
       */
      color?: Color;
      /**
       * size of the button, `xs`, `compact-xs`, `sm`, `compact-sm`, `md`, `compact-md`, `lg`,
       * `compact-lg`, `xl` and `compact-xl`
       * 
       * @default compact-sm
       */
      size?: Size;
      /**
       * label of the operation's button
       * 
       * @default ${operation}
       */
      label?: string;
      /**
       * extra styling for the operation's button
       */
      style?: string;
    };
  }[];
  /**
   * actions that can be performed on individual documents
   */
  document?: {
    /**
     * must be unique
     */
    operation: string;
    execute: (doc: any) => void | Promise<void>;
    /**
     * determines whether the action is applicable to the document
     */
    applicable?: (doc: any) => boolean;
    guard?: boolean | string;
    element?: {
      variant?: Variant;
      color?: Color;
      size?: Size;
      label?: string;
      style?: string;
    };
  }[];
}

interface CreateActions {
  /**
   * override default document creation (Model.create)
   */
  handler?: (payload: object) => Promise<mongoose.Document>;
  /**
   * hook that runs before document creation.
   * runs before mongoose's validations/hooks, if any
   */
  pre?: (payload: object) => void | Promise<void>;

  /**
   * hook that runs after successful document creation.
   * runs after mongoose's post hooks, if any
   */
  post?(doc: any): void | Promise<void>;
}

interface DeleteActions {
  /**
   * override default deletion (Document.deleteOne)
   */
  handler?(doc: any): Promise<mongoose.Document>;

  /**
   * hook that runs after successful deletion
   */
  post?(doc: any): void | Promise<void>;
}

interface UpdateActions {
  /**
   * override default update (Document.set().save())
   */
  handler?(doc: any, payload: object): Promise<mongoose.Document>;

  /**
   * hook that runs before document update
   */
  pre?: (doc_id: string, payload: object) => void | Promise<void>;

  /**
   * hook that runs after successful update
   */
  post?(doc: any, payload: object): void | Promise<void>;
}

interface PropertyOptions {
  /**
   * custom display label for the field in the interface
   */
  label?: string;
  /**
   * controls whether this field can be modified
   */
  editable?: boolean;
  /**
   * enables sorting by this field
   */
  sortable?: boolean;
  /**
   * controls field visibility in the interface
   */
  viewable?: boolean;
  /**
   * determines whether this field can be used as a filtering criteria
   */
  filterable?: boolean;
  /**
   * determines if this field is required when creating and editing.
   * this is purely for UI purposes and does not modify it at the schema level
   */
  required?: boolean | ((value?: any) => boolean);
  /**
   * allows the creation of this field
   */
  creatable?: boolean;
  /**
   * use a textarea input instead of a regular text input.
   * this is only considered if property is a string
   */
  textarea?: boolean;
  /**
   * displays the field value as an image instead of the url string,
   * assuming the value is a valid image url
   */
  as_image?: boolean;
  /**
   * customize how fields are displayed for different views.
   * renderer functions are expected to return html as a string, and input
   * elements in forms are expected to contain `name` attribute matching the field name
   */
  renderers?: Renderers;
}

interface Renderers {
  /**
   * customize how the value appears in the list view
   */
  list?: (value: any) => string;
  /**
   * customize the input field in create form
   */
  create?: (enumvalues: any[] | null) => string;
  /**
   * customize how the filter input appears
   */
  filter?: (enumvalues: any[] | null) => string;
  /**
   * customize how the value appears in detail view
   */
  view?: (value: any) => string;
  /**
   * customize the input field in edit form
   */
  edit?: (params: { value: any; enumvalues: any[] | null }) => string;
}

complete example

const explorer = new MongooseExplorer({
  mongoose,
  resources: {
    User: {
      explorable: true,
      creatable: false,
      deletable: (user) => doc.role !== "admin",
      editable: (user) => !user.active,
      sortable: true,
      limit: 50,
      viewables: ["email", "role"],
      filterables: ["role"],
      creatables: ["email", "username", "role"], // redundant since creatable is false
      editables: ["role", "active"],
      sortables: ["role", "active"],
      ref_newtab: true,
      virtuals: {
        info: (user) => `${user.username} ${user.email} - ${user.role}`
      },
      show_indexes: true,
      query_runner: {
        enabled: true,
        presets: [
          {
            name: "Inactive Users",
            resolver: async () => {
              return User.find({ last_login: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } })
            }
          }
        ]
      },
      actions: {
        create: {
          handler: async (body) => {
            const user = await User.create(body);

            await service.email.verification(user.email);

            return user;
          }
        },
        delete: {
          handler: async (user) => {
            await user.deleteOne();

            await posthog.capture("user.deleted", doc.toJSON());

            return user;
          }
        },
        custom: {
          bulk: [
            {
              operation: "ban",
              execute: async (ids) => {
                await User.updateMany({ _id: { $in: ids } }, { $set: { is_banned: true } });
              },
              guard: true,
              element: {
                variant: "filled",
                color: "danger",
                size: "lg",
                label: "BAN",
                style: "border-radius: 10px"
              }
            }
          ],
          document: [
            {
              operation: "promote-to-admin",
              execute: async (user) => {
                await user.set("role", "admin").save();
              },
              applicable: (user) => user.role !== "admin",
              guard: "promote to admin fr?",
              element: {
                variant: "outline",
                color: "primary",
                size: "md",
                label: "Promote To Admin",
                style: "font-style: italic; font-weight: bold"
              }
            }
          ]
        }
      },
      bulk_delete: {
        enabled: true,
        use_document: true
      },
      cascade_delete: [
        {
          model: Post,
          relation: {
            local_field: "_id",
            foreign_field: "author_id"
          }
        },
        {
          model: Notification,
          relation: {
            local_field: "_id",
            foreign_field: "recipient_id"
          }
        }
      ],
      // or manually
      cascade_delete: async (doc, session) => {
        await Post.deleteMany({ author_id: doc._id }, { session });
        
        await Notification.deleteMany({ recipient_id: doc._id }, { session });
      },
      properties: {
        email: {
          label: "E-Mail",
          editable: false,
          sortable: false,
          viewable: true,
          filterable: false,
          required: true,
          creatable: true,
          textarea: false,
          as_image: false,
          renderers: {
            list: (email) => mask(email),
            create: () => `
              <input type="email" name="email" class="input size-sm" style="border-radius: 20px"></input>
            `,
            view: (email) => mask(email)
          }
        },
        /**
         * target properties nested within an object by using dot notation
         */
        "profile.settings.notificatons": {}
      }
    }
  }
});

features and behavior

1. string filtering with regex

the interface supports filtering strings using regex patterns enclosed within / delimiters.
example: a filter input of /doe/i/ is processed as /doe/i and applied using mongodb's $regex operator

2. pre and post hook execution

  • pre hooks:
    custom pre hooks are executed before mongoose's hooks and validations

  • post hooks:
    custom post hooks are executed after mongoose's post hooks, if any

3. error handling in hooks

  • if a pre hook throws an error, the request fails, and the operation is not executed
  • if a post hook throws an error, the request succeeds, and the error is suppressed

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