@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
Maintainers
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
- 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 - if your application utilizes
helmet
middleware, allow theunsafe-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
, andestimatedDocumentCount
mongoose operations. query options are applicable tofind
andfindOne
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 tofind
andfindOne
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 validationspost 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
- filtering properties of type map is not supported
- filtering and editing properties of array of objects is not supported yet
- sorting properties of type map and array is not supported
- 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