diff --git a/backend/actions/Model/getDocuments.js b/backend/actions/Model/getDocuments.js index 53873b60..886e5c27 100644 --- a/backend/actions/Model/getDocuments.js +++ b/backend/actions/Model/getDocuments.js @@ -35,6 +35,9 @@ const GetDocumentsParams = new Archetype({ fields: { $type: 'string' }, + maxTimeMS: { + $type: 'number' + }, roles: { $type: ['string'] } @@ -45,7 +48,7 @@ module.exports = ({ db }) => async function getDocuments(params) { const { roles } = params; await authorize('Model.getDocuments', roles); - const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params; + const { model, limit, skip, sortKey, sortDirection, searchText, fields, maxTimeMS } = params; const Model = db.models[model]; if (Model == null) { @@ -72,6 +75,9 @@ module.exports = ({ db }) => async function getDocuments(params) { if (projection != null) { query = query.select(projection); } + if (typeof maxTimeMS === 'number' && maxTimeMS > 0) { + query = query.maxTimeMS(maxTimeMS); + } const cursor = await query.cursor(); const docs = []; for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) { diff --git a/backend/actions/Model/getDocumentsStream.js b/backend/actions/Model/getDocumentsStream.js index 6cc4d2fd..3fe228bb 100644 --- a/backend/actions/Model/getDocumentsStream.js +++ b/backend/actions/Model/getDocumentsStream.js @@ -35,6 +35,9 @@ const GetDocumentsParams = new Archetype({ fields: { $type: 'string' }, + maxTimeMS: { + $type: 'number' + }, roles: { $type: ['string'] } @@ -45,7 +48,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) { const { roles } = params; await authorize('Model.getDocumentsStream', roles); - const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params; + const { model, limit, skip, sortKey, sortDirection, searchText, fields, maxTimeMS } = params; const Model = db.models[model]; if (Model == null) { @@ -72,6 +75,9 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) { if (projection != null) { query = query.select(projection); } + if (typeof maxTimeMS === 'number' && maxTimeMS > 0) { + query = query.maxTimeMS(maxTimeMS); + } const schemaPaths = {}; for (const path of Object.keys(Model.schema.paths)) { diff --git a/frontend/src/models/models.html b/frontend/src/models/models.html index 866b808a..f27d0168 100644 --- a/frontend/src/models/models.html +++ b/frontend/src/models/models.html @@ -197,6 +197,13 @@ > {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }} + + Query Timeout: {{ queryTimeoutSeconds }} {{ queryTimeoutSeconds === 1 ? 'second' : 'seconds' }} + Are you sure you want to delete {{selectedDocuments.length}} documents? + + + × + Query Timeout + + Set the default timeout for document queries in this browser. + + + Timeout (seconds) + + + + + Cancel + + + Save + + + + app.component('models', { shouldShowCollectionInfoModal: false, shouldShowUpdateMultipleModal: false, shouldShowDeleteMultipleModal: false, + shouldShowQueryTimeoutModal: false, shouldExport: {}, sortBy: {}, query: {}, @@ -121,6 +124,8 @@ module.exports = app => app.component('models', { recentlyViewedModels: [], showModelSwitcher: false, showRowNumbers: true, + queryTimeoutSeconds: DEFAULT_QUERY_TIMEOUT_SECONDS, + queryTimeoutDraftSeconds: String(DEFAULT_QUERY_TIMEOUT_SECONDS), suppressScrollCheck: false, scrollTopToRestore: null }), @@ -130,6 +135,7 @@ module.exports = app => app.component('models', { this.loadOutputPreference(); this.loadSelectedGeoField(); this.loadShowRowNumbersPreference(); + this.loadQueryTimeoutPreference(); this.loadRecentlyViewedModels(); this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1'; }, @@ -637,9 +643,47 @@ module.exports = app => app.component('models', { if (fieldsParam) { params.fields = fieldsParam; } + params.maxTimeMS = this.getQueryTimeoutMS(); return params; }, + getQueryTimeoutMS() { + const parsedSeconds = Number(this.queryTimeoutSeconds); + const normalizedSeconds = Number.isFinite(parsedSeconds) && parsedSeconds > 0 ? + parsedSeconds : + DEFAULT_QUERY_TIMEOUT_SECONDS; + return Math.floor(normalizedSeconds * 1000); + }, + loadQueryTimeoutPreference() { + if (typeof window === 'undefined' || !window.localStorage) { + return; + } + const stored = window.localStorage.getItem(QUERY_TIMEOUT_SECONDS_STORAGE_KEY); + const parsed = Number(stored); + if (Number.isFinite(parsed) && parsed > 0) { + this.queryTimeoutSeconds = parsed; + this.queryTimeoutDraftSeconds = String(parsed); + } + }, + openQueryTimeoutModal() { + this.closeActionsMenu(); + this.queryTimeoutDraftSeconds = String(this.queryTimeoutSeconds); + this.shouldShowQueryTimeoutModal = true; + }, + saveQueryTimeoutPreference() { + const parsed = Number(this.queryTimeoutDraftSeconds); + if (!Number.isFinite(parsed) || parsed <= 0) { + this.$toast.error('Query timeout must be a positive number of seconds.'); + return; + } + const normalized = Math.round(parsed); + this.queryTimeoutSeconds = normalized; + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.setItem(QUERY_TIMEOUT_SECONDS_STORAGE_KEY, String(normalized)); + } + this.shouldShowQueryTimeoutModal = false; + this.$toast.success('Query timeout updated.'); + }, setSearchTextFromRoute() { if (this.$route.query?.search) { this.searchText = this.$route.query.search; @@ -846,7 +890,8 @@ module.exports = app => app.component('models', { model: this.currentModel, limit: 1, sortKey: '_id', - sortDirection: 1 + sortDirection: 1, + maxTimeMS: this.getQueryTimeoutMS() }); if (!Array.isArray(docs) || docs.length === 0) { throw new Error('No documents found');
+ Set the default timeout for document queries in this browser. +