diff --git a/frontend/src/_util/document-search-autocomplete.js b/frontend/src/_util/document-search-autocomplete.js
index df006b76..1a8a23d0 100644
--- a/frontend/src/_util/document-search-autocomplete.js
+++ b/frontend/src/_util/document-search-autocomplete.js
@@ -218,11 +218,72 @@ function applySuggestion(searchText, cursorPos, suggestion) {
return null;
}
+/**
+ * When the cursor is inside a Date(…) or new Date(…) argument, returns the
+ * slice indices of that argument so a picker can replace it with a quoted ISO string.
+ */
+function getDatePickerInsertionRange(searchText, cursorPos) {
+ const before = searchText.slice(0, cursorPos);
+ const re = /((?:new\s+)?Date\s*\(\s*)([^)]*)$/i;
+ const m = before.match(re);
+ if (!m) {
+ return null;
+ }
+ const innerStart = m.index + m[1].length;
+ const after = searchText.slice(cursorPos);
+ let closeIdx = -1;
+ let parenDepth = 0;
+ for (let k = 0; k < after.length; k++) {
+ const ch = after[k];
+ if (ch === '(') {
+ parenDepth++;
+ } else if (ch === ')') {
+ if (parenDepth === 0) {
+ closeIdx = cursorPos + k;
+ break;
+ }
+ parenDepth--;
+ }
+ }
+ const innerEnd = closeIdx >= 0 ? closeIdx : cursorPos;
+ const needsClosingParen = closeIdx < 0;
+ return { innerStart, innerEnd, needsClosingParen };
+}
+
+function dateArgumentSliceToDatetimeLocal(slice) {
+ const t = slice.trim();
+ if (!t) {
+ return '';
+ }
+ const unquoted = (t.startsWith('"') && t.endsWith('"')) || (t.startsWith('\'') && t.endsWith('\''))
+ ? t.slice(1, -1)
+ : t;
+ const d = new Date(unquoted);
+ if (Number.isNaN(d.getTime())) {
+ return '';
+ }
+ const pad = n => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+function insertQuotedIsoInDateArgument(searchText, range, isoString) {
+ const quoted = JSON.stringify(isoString);
+ const { innerStart, innerEnd } = range;
+ const closing = range.needsClosingParen === true ? ')' : '';
+ return {
+ text: searchText.slice(0, innerStart) + quoted + closing + searchText.slice(innerEnd),
+ newCursorPos: innerStart + quoted.length + closing.length
+ };
+}
+
module.exports = {
buildAutocompleteTrie,
getAutocompleteContext,
getAutocompleteSuggestions,
applySuggestion,
+ getDatePickerInsertionRange,
+ dateArgumentSliceToDatetimeLocal,
+ insertQuotedIsoInDateArgument,
QUERY_SELECTORS,
VALUE_HELPERS,
FUNCTION_HELPERS
diff --git a/frontend/src/models/date-range-filter/date-range-filter.html b/frontend/src/models/date-range-filter/date-range-filter.html
new file mode 100644
index 00000000..a65cbcb0
--- /dev/null
+++ b/frontend/src/models/date-range-filter/date-range-filter.html
@@ -0,0 +1,205 @@
+
diff --git a/frontend/src/models/date-range-filter/date-range-filter.js b/frontend/src/models/date-range-filter/date-range-filter.js
new file mode 100644
index 00000000..e9946ef3
--- /dev/null
+++ b/frontend/src/models/date-range-filter/date-range-filter.js
@@ -0,0 +1,327 @@
+'use strict';
+
+const template = require('./date-range-filter.html');
+
+const MS_DAY = 24 * 60 * 60 * 1000;
+
+function intFromNumberInput(v) {
+ if (v === '' || v === null || v === undefined || (typeof v === 'number' && !Number.isFinite(v))) {
+ return NaN;
+ }
+ const n = Number(v);
+ return Number.isFinite(n) ? Math.trunc(n) : NaN;
+}
+
+/**
+ * Anchor from typed parts interpreted as UTC (calendar + clock in Z).
+ * The emitted filter uses toISOString(), so the numbers you type match that string (no local TZ shift).
+ */
+function utcDateFromParts(year, month, day, hour, minute) {
+ const y = intFromNumberInput(year);
+ const mo = intFromNumberInput(month);
+ const d = intFromNumberInput(day);
+ const h = intFromNumberInput(hour);
+ const mi = intFromNumberInput(minute);
+ if (![y, mo, d, h, mi].every(n => Number.isFinite(n))) {
+ return null;
+ }
+ if (mo < 1 || mo > 12 || d < 1 || d > 31 || h < 0 || h > 23 || mi < 0 || mi > 59) {
+ return null;
+ }
+ const t = Date.UTC(y, mo - 1, d, h, mi, 0, 0);
+ const dt = new Date(t);
+ if (
+ dt.getUTCFullYear() !== y ||
+ dt.getUTCMonth() !== mo - 1 ||
+ dt.getUTCDate() !== d ||
+ dt.getUTCHours() !== h ||
+ dt.getUTCMinutes() !== mi
+ ) {
+ return null;
+ }
+ return dt;
+}
+
+/** Replace the filter bar entirely with this single-field date criterion. */
+function buildDateFilterSearchText(fieldPath, filterValueFragment) {
+ const key = JSON.stringify(fieldPath);
+ return `{ ${key}: ${filterValueFragment} }`;
+}
+
+function buildMongoDateRangeClause(start, end) {
+ const a = start.toISOString();
+ const b = end.toISOString();
+ return `{ $gte: new Date(${JSON.stringify(a)}), $lt: new Date(${JSON.stringify(b)}) }`;
+}
+
+/** @param {'$gt' | '$lt'} op */
+function buildMongoCompareClause(op, instant) {
+ const iso = instant.toISOString();
+ return `{ ${op}: new Date(${JSON.stringify(iso)}) }`;
+}
+
+function parseIso8601Like(raw) {
+ if (typeof raw !== 'string') {
+ return null;
+ }
+ const t = raw.trim();
+ if (!t) {
+ return null;
+ }
+ const d = new Date(t);
+ return Number.isNaN(d.getTime()) ? null : d;
+}
+
+function collectDatePathsFromSchema(schemaPaths) {
+ if (!Array.isArray(schemaPaths)) {
+ return [];
+ }
+ const out = [];
+ for (const p of schemaPaths) {
+ if (!p || typeof p.path !== 'string') {
+ continue;
+ }
+ if (p.instance === 'Date') {
+ out.push(p.path);
+ }
+ if (p.schema && typeof p.schema === 'object') {
+ for (const subKey of Object.keys(p.schema)) {
+ const sub = p.schema[subKey];
+ if (sub && sub.instance === 'Date') {
+ out.push(`${p.path}.${subKey}`);
+ }
+ }
+ }
+ }
+ const scorePath = path => {
+ const p = String(path).toLowerCase();
+ if (p === 'createdat') {
+ return 0;
+ }
+ if (p === 'updatedat') {
+ return 1;
+ }
+ if (p.endsWith('.createdat')) {
+ return 2;
+ }
+ if (p.endsWith('.updatedat')) {
+ return 3;
+ }
+ return 10;
+ };
+ out.sort((a, b) => {
+ const ds = scorePath(a) - scorePath(b);
+ if (ds !== 0) {
+ return ds;
+ }
+ return a.localeCompare(b);
+ });
+ return out;
+}
+
+module.exports = app => app.component('date-range-filter', {
+ template,
+ props: {
+ schemaPaths: {
+ type: Array,
+ default: () => []
+ }
+ },
+ data() {
+ return {
+ panelOpen: false,
+ selectedPath: '',
+ customPath: '',
+ anchorYear: null,
+ anchorMonth: null,
+ anchorDay: null,
+ anchorHour: null,
+ anchorMinute: null,
+ anchorSeededOnce: false,
+ anchorIsoMode: false,
+ isoAnchorInput: '',
+ _onPointerDownOutside: null
+ };
+ },
+ computed: {
+ datePaths() {
+ return collectDatePathsFromSchema(this.schemaPaths);
+ },
+ useCustomPath() {
+ return this.datePaths.length === 0;
+ },
+ effectiveFieldPath() {
+ if (this.useCustomPath) {
+ return typeof this.customPath === 'string' ? this.customPath.trim() : '';
+ }
+ return typeof this.selectedPath === 'string' ? this.selectedPath.trim() : '';
+ }
+ },
+ watch: {
+ datePaths: {
+ handler(paths) {
+ if (paths.length === 0) {
+ return;
+ }
+ if (!paths.includes(this.selectedPath)) {
+ this.selectedPath = paths[0];
+ }
+ },
+ immediate: true
+ },
+ schemaPaths() {
+ if (this.datePaths.length > 0 && !this.selectedPath) {
+ this.selectedPath = this.datePaths[0];
+ }
+ }
+ },
+ methods: {
+ populateAnchorFromDate(dt) {
+ this.anchorYear = dt.getUTCFullYear();
+ this.anchorMonth = dt.getUTCMonth() + 1;
+ this.anchorDay = dt.getUTCDate();
+ this.anchorHour = dt.getUTCHours();
+ this.anchorMinute = dt.getUTCMinutes();
+ },
+ onIsoAnchorPaste(ev) {
+ const text = (ev.clipboardData && ev.clipboardData.getData('text/plain')) || '';
+ const trimmed = text.trim();
+ if (!trimmed) {
+ return;
+ }
+ ev.preventDefault();
+ this.isoAnchorInput = trimmed;
+ const d = parseIso8601Like(trimmed);
+ if (d) {
+ this.populateAnchorFromDate(d);
+ this.anchorSeededOnce = true;
+ }
+ },
+ togglePanel(ev) {
+ ev?.preventDefault?.();
+ this.panelOpen = !this.panelOpen;
+ if (this.panelOpen && !this.anchorSeededOnce) {
+ this.populateAnchorFromDate(new Date());
+ this.anchorSeededOnce = true;
+ }
+ this.attachOutsideListener();
+ },
+ closePanel() {
+ this.panelOpen = false;
+ this.detachOutsideListener();
+ },
+ attachOutsideListener() {
+ this.detachOutsideListener();
+ if (!this.panelOpen) {
+ return;
+ }
+ this._onPointerDownOutside = ev => {
+ const root = this.$refs.root;
+ if (!root || root.contains(ev.target)) {
+ return;
+ }
+ this.closePanel();
+ };
+ document.addEventListener('pointerdown', this._onPointerDownOutside, true);
+ },
+ detachOutsideListener() {
+ if (this._onPointerDownOutside) {
+ document.removeEventListener('pointerdown', this._onPointerDownOutside, true);
+ this._onPointerDownOutside = null;
+ }
+ },
+ resolveAnchorDate() {
+ let dt = null;
+ if (this.anchorIsoMode) {
+ dt = parseIso8601Like(this.isoAnchorInput);
+ if (!dt && this.$toast) {
+ this.$toast.error('Could not parse ISO 8601. Example: 2026-05-08T14:30:00.000Z');
+ }
+ } else {
+ dt = utcDateFromParts(
+ this.anchorYear,
+ this.anchorMonth,
+ this.anchorDay,
+ this.anchorHour,
+ this.anchorMinute
+ );
+ if (!dt && this.$toast) {
+ this.$toast.error('Enter a valid UTC date and time using the number fields.');
+ }
+ }
+ return dt;
+ },
+ validateField() {
+ if (!this.effectiveFieldPath) {
+ if (this.$toast) {
+ this.$toast.error('Choose or enter a date field path.');
+ }
+ return false;
+ }
+ return true;
+ },
+ syncNumbersFromResolvedAnchor(anchor) {
+ if (this.anchorIsoMode && anchor) {
+ this.populateAnchorFromDate(anchor);
+ }
+ },
+ commitDateRange(start, end) {
+ const clause = buildMongoDateRangeClause(start, end);
+ const nextFilter = buildDateFilterSearchText(this.effectiveFieldPath, clause);
+ this.$emit('apply', nextFilter);
+ this.closePanel();
+ },
+ applyAnchorComparison(op) {
+ if (op !== '$gt' && op !== '$lt') {
+ return;
+ }
+ if (!this.validateField()) {
+ return;
+ }
+ const anchor = this.resolveAnchorDate();
+ if (!anchor) {
+ return;
+ }
+ this.syncNumbersFromResolvedAnchor(anchor);
+ const clause = buildMongoCompareClause(op, anchor);
+ const nextFilter = buildDateFilterSearchText(this.effectiveFieldPath, clause);
+ this.$emit('apply', nextFilter);
+ this.closePanel();
+ },
+ presetRangeNextDays(days) {
+ if (!this.validateField()) {
+ return;
+ }
+ const anchor = this.resolveAnchorDate();
+ if (!anchor) {
+ return;
+ }
+ this.syncNumbersFromResolvedAnchor(anchor);
+ const start = anchor;
+ const end = new Date(anchor.getTime() + days * MS_DAY);
+ this.commitDateRange(start, end);
+ },
+ presetRangePreviousDays(days) {
+ if (!this.validateField()) {
+ return;
+ }
+ const anchor = this.resolveAnchorDate();
+ if (!anchor) {
+ return;
+ }
+ this.syncNumbersFromResolvedAnchor(anchor);
+ const start = new Date(anchor.getTime() - days * MS_DAY);
+ const end = anchor;
+ this.commitDateRange(start, end);
+ },
+ presetRangeNextWeeks(weeks) {
+ this.presetRangeNextDays(weeks * 7);
+ },
+ presetRangePreviousWeeks(weeks) {
+ this.presetRangePreviousDays(weeks * 7);
+ }
+ },
+ beforeUnmount() {
+ this.detachOutsideListener();
+ }
+});
diff --git a/frontend/src/models/document-search/document-search.html b/frontend/src/models/document-search/document-search.html
index f78572f1..9a67828d 100644
--- a/frontend/src/models/document-search/document-search.html
+++ b/frontend/src/models/document-search/document-search.html
@@ -5,11 +5,30 @@
type="text"
placeholder="Filter (supports JS, dates, ObjectIds, and more)"
v-model="searchText"
- @click="initFilter"
+ @click="initFilter($event); updateAutocomplete()"
@input="updateAutocomplete"
+ @keyup="onSearchKeyup"
@keydown="handleKeyDown"
/>
-
+
diff --git a/frontend/src/models/document-search/document-search.js b/frontend/src/models/document-search/document-search.js
index 339b5e17..7adc8871 100644
--- a/frontend/src/models/document-search/document-search.js
+++ b/frontend/src/models/document-search/document-search.js
@@ -4,7 +4,10 @@ const template = require('./document-search.html');
const {
buildAutocompleteTrie,
getAutocompleteSuggestions,
- applySuggestion
+ applySuggestion,
+ getDatePickerInsertionRange,
+ dateArgumentSliceToDatetimeLocal,
+ insertQuotedIsoInDateArgument
} = require('../../_util/document-search-autocomplete');
module.exports = app => app.component('document-search', {
@@ -24,7 +27,9 @@ module.exports = app => app.component('document-search', {
autocompleteSuggestions: [],
autocompleteIndex: 0,
autocompleteTrie: null,
- searchText: this.value || ''
+ searchText: this.value || '',
+ datePickerContext: null,
+ datePickerLocalValue: ''
};
},
watch: {
@@ -43,8 +48,25 @@ module.exports = app => app.component('document-search', {
},
mounted() {
this.$refs.searchInput.focus();
+ this._onDocPointerDownCloseDatePicker = this.onDocumentPointerDownCloseDatePicker.bind(this);
+ document.addEventListener('pointerdown', this._onDocPointerDownCloseDatePicker, true);
+ },
+ beforeUnmount() {
+ if (this._onDocPointerDownCloseDatePicker) {
+ document.removeEventListener('pointerdown', this._onDocPointerDownCloseDatePicker, true);
+ }
},
methods: {
+ onDocumentPointerDownCloseDatePicker(ev) {
+ const drop = this.$refs.autocompleteDropdown;
+ if (!drop || drop.contains(ev.target)) {
+ return;
+ }
+ const dateEl = this.$refs.datePickerInput;
+ if (dateEl) {
+ dateEl.blur();
+ }
+ },
focusInput() {
const input = this.$refs.searchInput;
if (input && typeof input.focus === 'function') {
@@ -66,10 +88,28 @@ module.exports = app => app.component('document-search', {
});
}
},
+ onSearchKeyup(ev) {
+ if (
+ this.autocompleteSuggestions.length > 0 &&
+ (ev.key === 'ArrowUp' || ev.key === 'ArrowDown')
+ ) {
+ return;
+ }
+ this.updateAutocomplete();
+ },
updateAutocomplete() {
const input = this.$refs.searchInput;
const cursorPos = input ? input.selectionStart : 0;
+ const dateRange = getDatePickerInsertionRange(this.searchText, cursorPos);
+ this.datePickerContext = dateRange;
+ if (dateRange) {
+ const argSlice = this.searchText.slice(dateRange.innerStart, dateRange.innerEnd);
+ this.datePickerLocalValue = dateArgumentSliceToDatetimeLocal(argSlice);
+ } else {
+ this.datePickerLocalValue = '';
+ }
+
if (this.autocompleteTrie) {
this.autocompleteSuggestions = getAutocompleteSuggestions(
this.autocompleteTrie,
@@ -82,6 +122,34 @@ module.exports = app => app.component('document-search', {
this.autocompleteSuggestions = [];
}
},
+ applyDateFromPicker(localDateTime) {
+ if (!localDateTime || !this.datePickerContext) {
+ return;
+ }
+ const picked = new Date(localDateTime);
+ if (Number.isNaN(picked.getTime())) {
+ if (this.$toast) {
+ this.$toast.error('Invalid date or time. Enter a valid date and time, then try again.');
+ }
+ return;
+ }
+ const iso = picked.toISOString();
+ const result = insertQuotedIsoInDateArgument(
+ this.searchText,
+ this.datePickerContext,
+ iso
+ );
+ const input = this.$refs.searchInput;
+ this.searchText = result.text;
+ this.autocompleteSuggestions = [];
+ this.$nextTick(() => {
+ if (input) {
+ input.focus();
+ input.setSelectionRange(result.newCursorPos, result.newCursorPos);
+ }
+ this.updateAutocomplete();
+ });
+ },
handleKeyDown(ev) {
if (this.autocompleteSuggestions.length === 0) {
return;
diff --git a/frontend/src/models/models.html b/frontend/src/models/models.html
index d0c7b49b..91beffa2 100644
--- a/frontend/src/models/models.html
+++ b/frontend/src/models/models.html
@@ -109,6 +109,10 @@
@search="search"
>
+
Loading ...
{{documents.length}}/{{numDocuments === 1 ? numDocuments + ' document' : numDocuments + ' documents'}}
diff --git a/test/document-search-autocomplete.test.js b/test/document-search-autocomplete.test.js
index 5ede496f..88c3dbe2 100644
--- a/test/document-search-autocomplete.test.js
+++ b/test/document-search-autocomplete.test.js
@@ -5,7 +5,11 @@ const {
buildAutocompleteTrie,
getAutocompleteContext,
getAutocompleteSuggestions,
- applySuggestion
+ applySuggestion,
+ getDatePickerInsertionRange,
+ dateArgumentSliceToDatetimeLocal,
+ insertQuotedIsoInDateArgument,
+ FUNCTION_HELPERS
} = require('../frontend/src/_util/document-search-autocomplete');
describe('document-search-autocomplete', function() {
@@ -277,4 +281,122 @@ describe('document-search-autocomplete', function() {
assert.strictEqual(result.text, '{ "name":');
});
});
+
+ describe('getDatePickerInsertionRange()', function() {
+ it('returns null when the cursor is not inside Date(...)', function() {
+ assert.strictEqual(getDatePickerInsertionRange('{ x: 1 }', 5), null);
+ assert.strictEqual(getDatePickerInsertionRange('{ d: 1 }', '{ d: '.length), null);
+ });
+
+ it('detects new Date( at end of before', function() {
+ const searchText = '{ x: new Date(';
+ const cursorPos = searchText.length;
+ const range = getDatePickerInsertionRange(searchText, cursorPos);
+
+ assert.ok(range);
+ assert.strictEqual(searchText.slice(0, range.innerStart).endsWith('new Date('), true);
+ assert.strictEqual(range.needsClosingParen, true);
+ assert.strictEqual(range.innerEnd, cursorPos);
+ });
+
+ it('sets needsClosingParen false when ) is present after the cursor', function() {
+ const searchText = '{ createdAt: Date() }';
+ const cursorPos = '{ createdAt: Date('.length;
+ const range = getDatePickerInsertionRange(searchText, cursorPos);
+
+ assert.ok(range);
+ assert.strictEqual(range.needsClosingParen, false);
+ assert.strictEqual(searchText.slice(range.innerStart, range.innerEnd), '');
+ assert.strictEqual(searchText[range.innerEnd], ')');
+ });
+
+ it('sets needsClosingParen true when no ) appears after the cursor', function() {
+ const searchText = '{ createdAt: Date( }';
+ const cursorPos = '{ createdAt: Date('.length;
+ const range = getDatePickerInsertionRange(searchText, cursorPos);
+
+ assert.ok(range);
+ assert.strictEqual(range.needsClosingParen, true);
+ assert.strictEqual(range.innerEnd, cursorPos);
+ });
+
+ it('finds inner slice when cursor is before the closing quote of a literal', function() {
+ const searchText = '{ createdAt: Date("2020-01-01") }';
+ const cursorPos = '{ createdAt: Date("2020-01-01'.length;
+ const range = getDatePickerInsertionRange(searchText, cursorPos);
+
+ assert.ok(range);
+ assert.strictEqual(searchText.slice(range.innerStart, range.innerEnd), '"2020-01-01"');
+ assert.strictEqual(range.needsClosingParen, false);
+ });
+ });
+
+ describe('dateArgumentSliceToDatetimeLocal()', function() {
+ it('returns empty string for blank or invalid input', function() {
+ assert.strictEqual(dateArgumentSliceToDatetimeLocal(''), '');
+ assert.strictEqual(dateArgumentSliceToDatetimeLocal(' '), '');
+ assert.strictEqual(dateArgumentSliceToDatetimeLocal('not-a-date'), '');
+ });
+
+ it('returns YYYY-MM-DDTHH:mm for a parseable slice', function() {
+ const result = dateArgumentSliceToDatetimeLocal('2020-06-15T14:30');
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(result));
+ assert.strictEqual(result.slice(0, 10), '2020-06-15');
+ });
+
+ it('strips surrounding quotes before parsing', function() {
+ const result = dateArgumentSliceToDatetimeLocal('"2021-12-25T08:00:00.000Z"');
+ assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(result));
+ });
+ });
+
+ describe('insertQuotedIsoInDateArgument()', function() {
+ it('replaces the inner range with JSON-stringified ISO and no extra ) by default', function() {
+ const searchText = '{ d: Date(OLD) }';
+ const prefix = '{ d: Date(';
+ const inner = 'OLD';
+ const innerStart = prefix.length;
+ const innerEnd = innerStart + inner.length;
+ const range = { innerStart, innerEnd, needsClosingParen: false };
+ const iso = '2022-03-04T12:00:00.000Z';
+
+ const result = insertQuotedIsoInDateArgument(searchText, range, iso);
+ const expectedQuoted = JSON.stringify(iso);
+
+ assert.strictEqual(result.text, '{ d: Date(' + expectedQuoted + ') }');
+ assert.strictEqual(result.newCursorPos, innerStart + expectedQuoted.length);
+ });
+
+ it('appends ) when needsClosingParen is true', function() {
+ const searchText = '{ d: Date( }';
+ const innerStart = '{ d: Date('.length;
+ const range = { innerStart, innerEnd: innerStart, needsClosingParen: true };
+ const iso = '2023-01-02T00:00:00.000Z';
+
+ const result = insertQuotedIsoInDateArgument(searchText, range, iso);
+ const expectedQuoted = JSON.stringify(iso);
+
+ assert.ok(result.text.includes('{ d: Date(' + expectedQuoted + ')'));
+ assert.strictEqual(result.newCursorPos, innerStart + expectedQuoted.length + 1);
+ });
+
+ it('does not append ) when needsClosingParen is omitted (falsy)', function() {
+ const searchText = '{ d: Date(x) }';
+ const innerStart = '{ d: Date('.length;
+ const range = { innerStart, innerEnd: innerStart + 1, needsClosingParen: false };
+ const result = insertQuotedIsoInDateArgument(searchText, range, '2000-01-01T00:00:00.000Z');
+
+ assert.strictEqual(result.text.indexOf('))'), -1);
+ assert.ok(result.text.endsWith(') }'));
+ });
+ });
+
+ describe('FUNCTION_HELPERS', function() {
+ it('includes Date ObjectId and objectIdRange', function() {
+ assert.ok(FUNCTION_HELPERS.has('Date'));
+ assert.ok(FUNCTION_HELPERS.has('ObjectId'));
+ assert.ok(FUNCTION_HELPERS.has('objectIdRange'));
+ assert.ok(!FUNCTION_HELPERS.has('Math'));
+ });
+ });
});