Skip to content
Merged
5 changes: 5 additions & 0 deletions package-lock.json

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

4 changes: 4 additions & 0 deletions packages/mongodb-constants/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"test-ci": "npm run test-cov",
"reformat": "npm run prettier -- --write ."
},
"peerDependencies": {
"bson": "^6.10.3"
},
"devDependencies": {
"@mongodb-js/eslint-config-devtools": "0.9.11",
"@mongodb-js/mocha-config-devtools": "^1.0.5",
Expand All @@ -60,6 +63,7 @@
"@types/mocha": "^9.1.1",
"@types/semver": "^7.7.0",
"@types/sinon-chai": "^3.2.5",
"bson": "^6.10.3",
"acorn": "^8.14.1",
"chai": "^4.5.0",
"depcheck": "^1.4.7",
Expand Down
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 'bson';

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 'bson';
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