Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0329aaa
new projection view
IslandRhythms Feb 13, 2026
bfc8022
mongoose syntax
IslandRhythms Feb 13, 2026
d933d45
fix document duplication + dropdown input
IslandRhythms Feb 13, 2026
dbe4db2
resolve conflicts
IslandRhythms Mar 13, 2026
3de1e55
projection scoring + UI updates
IslandRhythms Mar 13, 2026
5fdb8b2
remove copy tooltip
IslandRhythms Mar 13, 2026
faef135
default 6
IslandRhythms Mar 13, 2026
4e16bff
improve UI
IslandRhythms Mar 13, 2026
1aac26a
improve projection modal
IslandRhythms Mar 13, 2026
ebaa07d
projection mode improvements
IslandRhythms Mar 18, 2026
3ce1779
support only mongoose syntax
IslandRhythms Mar 19, 2026
055695a
remove bloat
IslandRhythms Mar 19, 2026
8293628
add tests
IslandRhythms Mar 19, 2026
87a2d32
more tests
IslandRhythms Mar 19, 2026
280c395
remove useless parameter, fix buttons and layout
IslandRhythms Mar 19, 2026
eef0a4a
remove passing filter to getSuggestedProjection
IslandRhythms Mar 19, 2026
3fbbde5
Update getSuggestedProjection.js
IslandRhythms Mar 19, 2026
1dc05bb
Potential fix for pull request finding
IslandRhythms Mar 19, 2026
f230d45
fix attachment
IslandRhythms Mar 20, 2026
0f0a572
Update models.js
IslandRhythms Mar 20, 2026
15f53df
support subtractions
IslandRhythms Mar 20, 2026
94ea6ca
copilot suggestions
IslandRhythms Mar 20, 2026
02b96af
Update models.html
IslandRhythms Mar 20, 2026
f705a89
minor feature: row numbers
IslandRhythms Mar 20, 2026
dd7d3c5
fix: lint
IslandRhythms Mar 20, 2026
c5a3916
Merge branch 'main' into IslandRhythms/projection-redesign
vkarpov15 Mar 23, 2026
6c4c80c
remove scoring system
IslandRhythms Mar 25, 2026
92961e7
don't use , for delimiter
IslandRhythms Mar 25, 2026
88a7f01
fix: only use projection when enabled via url
IslandRhythms Mar 25, 2026
8d24187
show already loaded documents
vkarpov15 Mar 25, 2026
a04c025
Merge branch 'IslandRhythms/projection-redesign' of github.com:mongoo…
vkarpov15 Mar 25, 2026
ac8256b
Update frontend/src/models/models.js
vkarpov15 Mar 25, 2026
cc02c30
remove test global
vkarpov15 Mar 25, 2026
465048d
Merge branch 'IslandRhythms/projection-redesign' of github.com:mongoo…
vkarpov15 Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions backend/actions/Model/getDocuments.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const Archetype = require('archetype');
const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
const evaluateFilter = require('../../helpers/evaluateFilter');
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
const parseFieldsParam = require('../../helpers/parseFieldsParam');
const authorize = require('../../authorize');

const GetDocumentsParams = new Archetype({
Expand All @@ -30,6 +32,9 @@ const GetDocumentsParams = new Archetype({
sortDirection: {
$type: 'number'
},
fields: {
$type: 'string'
},
roles: {
$type: ['string']
}
Expand All @@ -40,7 +45,7 @@ module.exports = ({ db }) => async function getDocuments(params) {
const { roles } = params;
await authorize('Model.getDocuments', roles);

const { model, limit, skip, sortKey, sortDirection, searchText } = params;
const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params;

const Model = db.models[model];
if (Model == null) {
Expand All @@ -61,12 +66,13 @@ module.exports = ({ db }) => async function getDocuments(params) {
if (!sortObj.hasOwnProperty('_id')) {
sortObj._id = -1;
}
const cursor = await Model.
find(filter).
limit(limit).
skip(skip).
sort(sortObj).
cursor();

let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj);
const projection = parseFieldsParam(fields);
if (projection != null) {
query = query.select(projection);
}
const cursor = await query.cursor();
const docs = [];
for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
docs.push(doc);
Expand Down Expand Up @@ -101,9 +107,12 @@ module.exports = ({ db }) => async function getDocuments(params) {
await Model.estimatedDocumentCount() :
await Model.countDocuments(filter);

const suggestedFields = getSuggestedProjection(Model);

return {
docs: docs.map(doc => doc.toJSON({ virtuals: false, getters: false, transform: false })),
schemaPaths,
suggestedFields,
numDocs: numDocuments
};
};
25 changes: 16 additions & 9 deletions backend/actions/Model/getDocumentsStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const Archetype = require('archetype');
const removeSpecifiedPaths = require('../../helpers/removeSpecifiedPaths');
const evaluateFilter = require('../../helpers/evaluateFilter');
const getRefFromSchemaType = require('../../helpers/getRefFromSchemaType');
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
const parseFieldsParam = require('../../helpers/parseFieldsParam');
const authorize = require('../../authorize');

const GetDocumentsParams = new Archetype({
Expand All @@ -30,6 +32,9 @@ const GetDocumentsParams = new Archetype({
sortDirection: {
$type: 'number'
},
fields: {
$type: 'string'
},
roles: {
$type: ['string']
}
Expand All @@ -40,7 +45,7 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
const { roles } = params;
await authorize('Model.getDocumentsStream', roles);

const { model, limit, skip, sortKey, sortDirection, searchText } = params;
const { model, limit, skip, sortKey, sortDirection, searchText, fields } = params;

const Model = db.models[model];
if (Model == null) {
Expand All @@ -62,6 +67,12 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
sortObj._id = -1;
}

let query = Model.find(filter).limit(limit).skip(skip).sort(sortObj).batchSize(1);
const projection = parseFieldsParam(fields);
if (projection != null) {
query = query.select(projection);
}

const schemaPaths = {};
for (const path of Object.keys(Model.schema.paths)) {
const schemaType = Model.schema.paths[path];
Expand All @@ -87,20 +98,16 @@ module.exports = ({ db }) => async function* getDocumentsStream(params) {
}
removeSpecifiedPaths(schemaPaths, '.$*');

yield { schemaPaths };
const suggestedFields = getSuggestedProjection(Model);

yield { schemaPaths, suggestedFields };

// Start counting documents in parallel with streaming documents
const numDocsPromise = (parsedFilter == null)
? Model.estimatedDocumentCount().exec()
: Model.countDocuments(filter).exec();

const cursor = await Model.
find(filter).
limit(limit).
skip(skip).
sort(sortObj).
batchSize(1).
cursor();
const cursor = await query.cursor();

let numDocsYielded = false;
let numDocumentsPromiseResolved = false;
Expand Down
33 changes: 33 additions & 0 deletions backend/actions/Model/getSuggestedProjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const Archetype = require('archetype');
const getSuggestedProjection = require('../../helpers/getSuggestedProjection');
const authorize = require('../../authorize');

const GetSuggestedProjectionParams = new Archetype({
model: {
$type: 'string',
$required: true
},
roles: {
$type: ['string']
}
}).compile('GetSuggestedProjectionParams');

module.exports = ({ db }) => async function getSuggestedProjectionAction(params) {
params = new GetSuggestedProjectionParams(params);
const { roles } = params;
await authorize('Model.getSuggestedProjection', roles);

const { model } = params;

const Model = db.models[model];
if (Model == null) {
throw new Error(`Model ${model} not found`);
}

// Default columns: first N schema paths (no scoring).
const suggestedFields = getSuggestedProjection(Model);

return { suggestedFields };
};
1 change: 1 addition & 0 deletions backend/actions/Model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exports.exportQueryResults = require('./exportQueryResults');
exports.getDocument = require('./getDocument');
exports.getDocuments = require('./getDocuments');
exports.getDocumentsStream = require('./getDocumentsStream');
exports.getSuggestedProjection = require('./getSuggestedProjection');
exports.getCollectionInfo = require('./getCollectionInfo');
exports.getIndexes = require('./getIndexes');
exports.getEstimatedDocumentCounts = require('./getEstimatedDocumentCounts');
Expand Down
1 change: 1 addition & 0 deletions backend/authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const actionsToRequiredRoles = {
'Model.getDocument': ['owner', 'admin', 'member', 'readonly'],
'Model.getDocuments': ['owner', 'admin', 'member', 'readonly'],
'Model.getDocumentsStream': ['owner', 'admin', 'member', 'readonly'],
'Model.getSuggestedProjection': ['owner', 'admin', 'member', 'readonly'],
'Model.getEstimatedDocumentCounts': ['owner', 'admin', 'member', 'readonly'],
'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'],
'Model.listModels': ['owner', 'admin', 'member', 'readonly'],
Expand Down
37 changes: 37 additions & 0 deletions backend/helpers/getSuggestedProjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

/** Max number of paths to use for the default table projection. */
const DEFAULT_SUGGESTED_LIMIT = 6;

/**
* Default projection for the models table: the first N schema paths (definition order),
* excluding Mongoose internals. No scoring — stable and predictable.
*
* @param {import('mongoose').Model} Model - Mongoose model
* @param {{ limit?: number }} options - max paths returned
* @returns {string[]} Path names in schema order
*/
function getSuggestedProjection(Model, options = {}) {
const limit = typeof options.limit === 'number' && options.limit > 0
? options.limit
: DEFAULT_SUGGESTED_LIMIT;

const pathNames = Object.keys(Model.schema.paths).filter(key =>
!key.includes('.$*') &&
key !== '__v'
);

pathNames.sort((k1, k2) => {
if (k1 === '_id' && k2 !== '_id') {
return -1;
}
if (k1 !== '_id' && k2 === '_id') {
return 1;
}
return 0;
});

return pathNames.slice(0, limit);
}

module.exports = getSuggestedProjection;
36 changes: 36 additions & 0 deletions backend/helpers/parseFieldsParam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

/**
* Parse the `fields` request param for Model.getDocuments / getDocumentsStream.
* Expects JSON: either `["a","b"]` (inclusion list) or `{"a":1,"b":1}` (Mongo projection).
*
* @param {string|undefined} fields
* @returns {string|object|null} Argument suitable for Query.select(), or null when unset/invalid.
*/
function parseFieldsParam(fields) {
if (fields == null || typeof fields !== 'string') {
return null;
}
const trimmed = fields.trim();
if (!trimmed) {
return null;
}

let parsed;
try {
parsed = JSON.parse(trimmed);
} catch (e) {
return null;
}

if (Array.isArray(parsed)) {
const list = parsed.map(x => String(x).trim()).filter(Boolean);
return list.length > 0 ? list.join(' ') : null;
}
if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
return Object.keys(parsed).length > 0 ? parsed : null;
}
Comment thread
vkarpov15 marked this conversation as resolved.
return null;
}

module.exports = parseFieldsParam;
8 changes: 7 additions & 1 deletion frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,12 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
getDocuments: function getDocuments(params) {
return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
},
getSuggestedProjection: function getSuggestedProjection(params) {
return client.post('', { action: 'Model.getSuggestedProjection', ...params }).then(res => res.data);
},
getDocumentsStream: async function* getDocumentsStream(params) {
const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
yield { schemaPaths: data.schemaPaths };
yield { schemaPaths: data.schemaPaths, suggestedFields: data.suggestedFields };
yield { numDocs: data.numDocs };
for (const doc of data.docs) {
yield { document: doc };
Expand Down Expand Up @@ -342,6 +345,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
getDocuments: function getDocuments(params) {
return client.post('/Model/getDocuments', params).then(res => res.data);
},
getSuggestedProjection: function getSuggestedProjection(params) {
return client.post('/Model/getSuggestedProjection', params).then(res => res.data);
},
getDocumentsStream: async function* getDocumentsStream(params) {
const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/dashboard/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ module.exports = {
return this.dashboardResults.length > 0 ? this.dashboardResults[0] : null;
}
},
mounted: async function () {
mounted: async function() {
window.pageState = this;

document.addEventListener('click', this.handleDocumentClick);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/list-mixed/list-mixed.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<div class="list-mixed tooltip">
Comment thread
vkarpov15 marked this conversation as resolved.
<pre>
<code ref="MixedCode" class="language-javascript">{{shortenValue}}</code>
<span class="tooltiptext" @click.stop="copyText(value)">copy &#x1F4CB;</span>
</pre>
</div>

1 change: 0 additions & 1 deletion frontend/src/list-string/list-string.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<div class="list-string tooltip" ref="itemData">
{{displayValue}}
Comment thread
vkarpov15 marked this conversation as resolved.
<span class="tooltiptext" @click.stop="copyText(value)">copy &#x1F4CB;</span>
</div>
Loading
Loading