Skip to content

feat(mongodb-constants): add new utils for views COMPASS:9700 #567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
30 changes: 16 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/mongodb-constants/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"typescript": "^5.0.4"
},
"dependencies": {
"mongodb": "^6.18.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have this instead in peerDependencies and devDependencies similar to how the devtools-connect is doing it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, done!

"semver": "^7.7.1"
}
}
1 change: 1 addition & 0 deletions packages/mongodb-constants/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('constants', function () {
'VALIDATION_TEMPLATE',
'ATLAS_SEARCH_TEMPLATES',
'ATLAS_VECTOR_SEARCH_TEMPLATE',
'VIEW_PIPELINE_UTILS',
]);
});
});
1 change: 1 addition & 0 deletions packages/mongodb-constants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export type {
FilterOptions as CompletionFilterOptions,
} from './filter';
export * from './atlas-search-templates';
export * from './views';
175 changes: 175 additions & 0 deletions packages/mongodb-constants/src/views.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { expect } from 'chai';
import { VIEW_PIPELINE_UTILS } from './views';
import type { Document } from 'mongodb';

describe('views', function () {
describe('isPipelineSearchQueryable', function () {
it('should return true for a valid pipeline with $addFields stage', function () {
const pipeline: Document[] = [{ $addFields: { testField: 'testValue' } }];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
true,
);
});

it('should return true for a valid pipeline with $set stage', function () {
const pipeline: Document[] = [{ $set: { testField: 'testValue' } }];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
true,
);
});

it('should return true for a valid pipeline with $match stage using $expr', function () {
const pipeline: Document[] = [
{ $match: { $expr: { $eq: ['$field', 'value'] } } },
];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
true,
);
});

it('should return false for a pipeline with an unsupported stage', function () {
const pipeline: Document[] = [{ $group: { _id: '$field' } }];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
false,
);
});

it('should return false for a $match stage without $expr', function () {
const pipeline: Document[] = [{ $match: { nonExprKey: 'someValue' } }];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
false,
);
});

it('should return false for a $match stage with $expr and additional fields', function () {
const pipeline: Document[] = [
{
$match: {
$expr: { $eq: ['$field', 'value'] },
anotherField: 'value',
},
},
];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
false,
);
});

it('should return true for an empty pipeline', function () {
const pipeline: Document[] = [];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
true,
);
});

it('should return false if any stage in the pipeline is invalid', function () {
const pipeline: Document[] = [
{ $addFields: { testField: 'testValue' } },
{ $match: { $expr: { $eq: ['$field', 'value'] } } },
{ $group: { _id: '$field' } },
];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
false,
);
});

it('should handle a pipeline with multiple valid stages', function () {
const pipeline: Document[] = [
{ $addFields: { field1: 'value1' } },
{ $match: { $expr: { $eq: ['$field', 'value'] } } },
{ $set: { field2: 'value2' } },
];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
true,
);
});

it('should return false for a $match stage with no conditions', function () {
const pipeline: Document[] = [{ $match: {} }];
expect(VIEW_PIPELINE_UTILS.isPipelineSearchQueryable(pipeline)).to.equal(
false,
);
});
});

describe('isVersionSearchCompatibleForViewsDataExplorer', function () {
it('should return true for a version greater than or equal to 8.0.0', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'8.0.0',
),
).to.equal(true);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'8.0.1',
),
).to.equal(true);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'8.1.0',
),
).to.equal(true);
});

it('should return false for a version less than 8.0.0', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'7.9.9',
),
).to.equal(false);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'7.0.0',
),
).to.equal(false);
});

it('should handle invalid version format by returning false', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(
'invalid-version',
),
).to.equal(false);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsDataExplorer(''),
).to.equal(false);
});
});

describe('isVersionSearchCompatibleForViewsCompass', function () {
it('should return true for a version greater than or equal to 8.1.0', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('8.1.0'),
).to.equal(true);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('8.1.1'),
).to.equal(true);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('8.2.0'),
).to.equal(true);
});

it('should return false for a version less than 8.1.0', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('8.0.9'),
).to.equal(false);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('8.0.0'),
).to.equal(false);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass('7.9.9'),
).to.equal(false);
});

it('should handle invalid version format by returning false', function () {
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass(
'invalid-version',
),
).to.equal(false);
expect(
VIEW_PIPELINE_UTILS.isVersionSearchCompatibleForViewsCompass(''),
).to.equal(false);
});
});
});
89 changes: 89 additions & 0 deletions packages/mongodb-constants/src/views.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/** utils related to view pipeline **/

import type { Document } from 'mongodb';
import semver from 'semver';

const MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_DE = '8.0.0';
const MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_COMPASS = '8.1.0';

/**
* A view pipeline is searchQueryable (ie: a search index can be created on view) if
* a pipeline consists of only addFields, set and match with expr stages
*
* @param pipeline the view pipeline
* @returns whether pipeline is search queryable
*/
const isPipelineSearchQueryable = (pipeline: Document[]): boolean => {
for (const stage of pipeline) {
const stageKey = Object.keys(stage)[0];

// Check if the stage is $addFields, $set, or $match
if (
!(
stageKey === '$addFields' ||
stageKey === '$set' ||
stageKey === '$match'
)
) {
return false;
}

// If the stage is $match, check if it uses $expr
if (stageKey === '$match') {
const matchStage = stage['$match'] as Document;
const matchKeys = Object.keys(matchStage || {});

if (matchKeys.length !== 1 || !matchKeys.includes('$expr')) {
return false;
}
}
}

return true;
};

/**
* Views allow search indexes to be made on them in DE for server version 8.1+
*
* @param serverVersion the server version
* @returns whether serverVersion is search compatible for views in DE
*/
const isVersionSearchCompatibleForViewsDataExplorer = (
serverVersion: string,
): boolean => {
try {
return semver.gte(
serverVersion,
MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_DE,
);
} catch {
return false;
}
};

/**
* Views allow search indexes to be made on them in compass for mongodb version 8.0+
*
* @param serverVersion the server version
* @returns whether serverVersion is search compatible for views in Compass
*/
const isVersionSearchCompatibleForViewsCompass = (
serverVersion: string,
): boolean => {
try {
return semver.gte(
serverVersion,
MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_COMPASS,
);
} catch {
return false;
}
};

export const VIEW_PIPELINE_UTILS = {
MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_DE,
MIN_VERSION_FOR_VIEW_SEARCH_COMPATIBILITY_COMPASS,
isPipelineSearchQueryable,
isVersionSearchCompatibleForViewsDataExplorer,
isVersionSearchCompatibleForViewsCompass,
};
Loading