Skip to content

Commit dc4900c

Browse files
authored
feat(NODE-7537): promote QE string queries (prefix/suffix/substring) (#4996)
1 parent 72c0296 commit dc4900c

32 files changed

Lines changed: 3974 additions & 565 deletions

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@mongodb-js/zstd": "^7.0.0",
3535
"gcp-metadata": "^7.0.1",
3636
"kerberos": "^7.0.0",
37-
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
37+
"mongodb-client-encryption": "^7.2.0",
3838
"snappy": "^7.3.2",
3939
"socks": "^2.8.6"
4040
},
@@ -96,7 +96,7 @@
9696
"js-yaml": "^4.2.0",
9797
"mocha": "^11.7.6",
9898
"mocha-sinon": "^2.1.2",
99-
"mongodb-client-encryption": "^7.0.0",
99+
"mongodb-client-encryption": "^7.2.0",
100100
"nyc": "^17.1.0",
101101
"prettier": "^3.6.2",
102102
"semver": "^7.7.2",

src/client-side-encryption/client_encryption.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,16 @@ export class ClientEncryption {
746746
expressionMode: boolean,
747747
options: ClientEncryptionEncryptOptions
748748
): Promise<Binary> {
749-
const { algorithm, keyId, keyAltName, contentionFactor, queryType, rangeOptions, textOptions } =
750-
options;
749+
const {
750+
algorithm,
751+
keyId,
752+
keyAltName,
753+
contentionFactor,
754+
queryType,
755+
rangeOptions,
756+
stringOptions,
757+
textOptions
758+
} = options;
751759
const contextOptions: ExplicitEncryptionContextOptions = {
752760
expressionMode,
753761
algorithm
@@ -780,8 +788,9 @@ export class ClientEncryption {
780788
contextOptions.rangeOptions = serialize(rangeOptions);
781789
}
782790

783-
if (typeof textOptions === 'object') {
784-
contextOptions.textOptions = serialize(textOptions);
791+
const resolvedStringOptions = stringOptions ?? textOptions;
792+
if (typeof resolvedStringOptions === 'object') {
793+
contextOptions.textOptions = serialize(resolvedStringOptions);
785794
}
786795

787796
const valueBuffer = serialize({ v: value });
@@ -815,6 +824,8 @@ export interface ClientEncryptionEncryptOptions {
815824
| 'Indexed'
816825
| 'Unindexed'
817826
| 'Range'
827+
| 'String'
828+
/** @deprecated Use `'String'` instead. */
818829
| 'TextPreview';
819830

820831
/**
@@ -833,26 +844,39 @@ export interface ClientEncryptionEncryptOptions {
833844
/**
834845
* The query type.
835846
*/
836-
queryType?: 'equality' | 'range' | 'prefixPreview' | 'suffixPreview' | 'substringPreview';
847+
queryType?:
848+
| 'equality'
849+
| 'range'
850+
| 'prefix'
851+
| 'suffix'
852+
| 'substring'
853+
/** @deprecated Use `'prefix'` instead. */
854+
| 'prefixPreview'
855+
/** @deprecated Use `'suffix'` instead. */
856+
| 'suffixPreview'
857+
/** @deprecated Use `'substring'` instead. */
858+
| 'substringPreview';
837859

838860
/** The index options for a Queryable Encryption field supporting "range" queries.*/
839861
rangeOptions?: RangeOptions;
840862

863+
/** Options for a Queryable Encryption field supporting string queries. Only valid when `algorithm` is `'String'` or the deprecated `'TextPreview'`. */
864+
stringOptions?: StringQueryOptions;
865+
841866
/**
842-
* Options for a Queryable Encryption field supporting text queries. Only valid when `algorithm` is `TextPreview`.
867+
* Options for a Queryable Encryption field supporting text queries. Only valid when `algorithm` is `'String'` or the deprecated `'TextPreview'`.
843868
*
844-
* @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time.
869+
* @deprecated Use `stringOptions` instead.
845870
*/
846-
textOptions?: TextQueryOptions;
871+
textOptions?: StringQueryOptions;
847872
}
848873

849874
/**
850-
* Options for a Queryable Encryption field supporting text queries.
875+
* Options for a Queryable Encryption field supporting string queries.
851876
*
852877
* @public
853-
* @experimental Public Technical Preview: `textPreview` is an experimental feature and may break at any time.
854878
*/
855-
export interface TextQueryOptions {
879+
export interface StringQueryOptions {
856880
/** Indicates that text indexes for this field are case sensitive */
857881
caseSensitive: boolean;
858882
/** Indicates that text indexes for this field are diacritic sensitive. */
@@ -882,6 +906,12 @@ export interface TextQueryOptions {
882906
};
883907
}
884908

909+
/**
910+
* @public
911+
* @deprecated Use {@link StringQueryOptions} instead.
912+
*/
913+
export type TextQueryOptions = StringQueryOptions;
914+
885915
/**
886916
* @public
887917
* @experimental

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export type {
241241
GCPEncryptionKeyOptions,
242242
KMIPEncryptionKeyOptions,
243243
RangeOptions,
244+
StringQueryOptions,
244245
TextQueryOptions
245246
} from './client-side-encryption/client_encryption';
246247
export {

test/integration/client-side-encryption/client_side_encryption.prose.25.lookup.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#25-test-lookup */
2+
13
import * as fs from 'node:fs/promises';
24
import * as path from 'node:path';
35

@@ -37,7 +39,7 @@ const newEncryptedClient = ({ configuration }: { configuration: TestConfiguratio
3739
}
3840
);
3941

40-
describe('$lookup support', defaultMetadata, function () {
42+
describe('25. Test $lookup', defaultMetadata, function () {
4143
before(async function () {
4244
const mochaTest = { metadata: defaultMetadata };
4345

@@ -83,6 +85,7 @@ describe('$lookup support', defaultMetadata, function () {
8385
* db.qe2 with options: { "encryptedFields": "<schema-qe2.json>"}.
8486
* db.no_schema with no options.
8587
* db.no_schema2 with no options.
88+
* db.non_csfle_schema with options: { "validator": { "$jsonSchema": "<schema-non-csfle.json>"}}.
8689
* ```
8790
*/
8891
const collections = [
@@ -115,6 +118,11 @@ describe('$lookup support', defaultMetadata, function () {
115118
name: 'no_schema2',
116119
options: {},
117120
document: { no_schema2: 'no_schema2' }
121+
},
122+
{
123+
name: 'non_csfle_schema',
124+
options: { validator: { $jsonSchema: await readFixture('schema-non-csfle.json') } },
125+
document: { non_csfle_schema: 'non_csfle_schema' }
118126
}
119127
];
120128

@@ -126,6 +134,7 @@ describe('$lookup support', defaultMetadata, function () {
126134
unencryptedClient = this.configuration.newClient({}, { writeConcern: { w: 'majority' } });
127135

128136
/**
137+
* Insert documents with encryptedClient:
129138
* ```
130139
* {"csfle": "csfle"} into db.csfle
131140
* Use the unencrypted client to retrieve it. Assert the csfle field is BSON binary.
@@ -137,12 +146,13 @@ describe('$lookup support', defaultMetadata, function () {
137146
* Use the unencrypted client to retrieve it. Assert the qe2 field is BSON binary.
138147
* {"no_schema": "no_schema"} into db.no_schema
139148
* {"no_schema2": "no_schema2"} into db.no_schema2
149+
* {"non_csfle_schema": "non_csfle_schema"} into db.non_csfle_schema
140150
* ```
141151
*/
142152
for (const { name, document } of collections) {
143153
const { insertedId } = await encryptedClient.db('db').collection(name).insertOne(document);
144154

145-
if (name.startsWith('no_')) continue;
155+
if (name.startsWith('no_') || name === 'non_csfle_schema') continue;
146156

147157
expect(await unencryptedClient.db('db').collection(name).findOne(insertedId))
148158
.to.have.property(Object.keys(document)[0])
@@ -325,6 +335,11 @@ describe('$lookup support', defaultMetadata, function () {
325335
{ requires: { ...defaultMetadata.requires, mongodb: '>=8.1.0' } }
326336
);
327337

338+
// Case 8 expects one of two error messages depending on mongocryptd/crypt_shared and libmongocrypt versions:
339+
// - mongocryptd/crypt_shared <8.2 or libmongocrypt <1.17.0: "not supported"
340+
// - mongocryptd/crypt_shared 8.2+ and libmongocrypt 1.17.0+:
341+
// "Cannot specify both encryptionInformation and csfleEncryptionSchemas unless
342+
// csfleEncryptionSchemas only contains non-encryption JSON schema validators"
328343
test(
329344
'Case 8: db.csfle joins db.qe',
330345
'csfle',
@@ -339,7 +354,7 @@ describe('$lookup support', defaultMetadata, function () {
339354
},
340355
{ $project: { _id: 0 } }
341356
],
342-
/not supported/i,
357+
/not supported|Cannot specify both encryptionInformation and csfleEncryptionSchemas/i,
343358
{ requires: { ...defaultMetadata.requires, mongodb: '>=8.1.0' } }
344359
);
345360

@@ -360,4 +375,26 @@ describe('$lookup support', defaultMetadata, function () {
360375
/Upgrade/i,
361376
{ requires: { ...defaultMetadata.requires, mongodb: '>=7.0.0 <8.1.0' } }
362377
);
378+
379+
// Case 10 requires server 8.2+, mongocryptd/crypt_shared 8.2+, and libmongocrypt 1.17.0+.
380+
test(
381+
'Case 10: db.qe joins db.non_csfle_schema',
382+
'qe',
383+
[
384+
{ $match: { qe: 'qe' } },
385+
{
386+
$lookup: {
387+
from: 'non_csfle_schema',
388+
as: 'matched',
389+
pipeline: [
390+
{ $match: { non_csfle_schema: 'non_csfle_schema' } },
391+
{ $project: { _id: 0, __safeContent__: 0 } }
392+
]
393+
}
394+
},
395+
{ $project: { _id: 0, __safeContent__: 0 } }
396+
],
397+
{ qe: 'qe', matched: [{ non_csfle_schema: 'non_csfle_schema' }] },
398+
{ requires: { ...defaultMetadata.requires, mongodb: '>=8.2.0' } }
399+
);
363400
});

0 commit comments

Comments
 (0)