@@ -25,6 +25,7 @@ const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtual
2525const setupTimestamps = require ( './helpers/timestamps/setupTimestamps' ) ;
2626const utils = require ( './utils' ) ;
2727const validateRef = require ( './helpers/populate/validateRef' ) ;
28+ const { inferBSONType } = require ( './encryption_utils' ) ;
2829
2930const hasNumericSubpathRegex = / \. \d + ( \. | $ ) / ;
3031
@@ -128,6 +129,8 @@ function Schema(obj, options) {
128129 // For internal debugging. Do not use this to try to save a schema in MDB.
129130 this . $id = ++ id ;
130131 this . mapPaths = [ ] ;
132+ this . encryptedFields = { } ;
133+ this . _encryptionType = options ?. encryptionType ;
131134
132135 this . s = {
133136 hooks : new Kareem ( )
@@ -166,7 +169,7 @@ function Schema(obj, options) {
166169
167170 // ensure the documents get an auto _id unless disabled
168171 const auto_id = ! this . paths [ '_id' ] &&
169- ( this . options . _id ) && ! _idSubDoc ;
172+ ( this . options . _id ) && ! _idSubDoc ;
170173
171174 if ( auto_id ) {
172175 addAutoId ( this ) ;
@@ -463,6 +466,8 @@ Schema.prototype._clone = function _clone(Constructor) {
463466
464467 s . aliases = Object . assign ( { } , this . aliases ) ;
465468
469+ s . encryptedFields = clone ( this . encryptedFields ) ;
470+
466471 return s ;
467472} ;
468473
@@ -495,7 +500,17 @@ Schema.prototype.pick = function(paths, options) {
495500 }
496501
497502 for ( const path of paths ) {
498- if ( this . nested [ path ] ) {
503+ if ( path in this . encryptedFields ) {
504+ const encrypt = this . encryptedFields [ path ] ;
505+ const schemaType = this . path ( path ) ;
506+ newSchema . add ( {
507+ [ path ] : {
508+ encrypt,
509+ [ this . options . typeKey ] : schemaType
510+ }
511+ } ) ;
512+ }
513+ else if ( this . nested [ path ] ) {
499514 newSchema . add ( { [ path ] : get ( this . tree , path ) } ) ;
500515 } else {
501516 const schematype = this . path ( path ) ;
@@ -506,6 +521,10 @@ Schema.prototype.pick = function(paths, options) {
506521 }
507522 }
508523
524+ if ( ! this . _hasEncryptedFields ( ) ) {
525+ newSchema . _encryptionType = null ;
526+ }
527+
509528 return newSchema ;
510529} ;
511530
@@ -534,9 +553,9 @@ Schema.prototype.omit = function(paths, options) {
534553 if ( ! Array . isArray ( paths ) ) {
535554 throw new MongooseError (
536555 'Schema#omit() only accepts an array argument, ' +
537- 'got "' +
538- typeof paths +
539- '"'
556+ 'got "' +
557+ typeof paths +
558+ '"'
540559 ) ;
541560 }
542561
@@ -667,6 +686,20 @@ Schema.prototype._defaultToObjectOptions = function(json) {
667686 return defaultOptions ;
668687} ;
669688
689+ /**
690+ * Sets the encryption type of the schema, if a value is provided, otherwise
691+ * returns the encryption type.
692+ *
693+ * @param {'csfle' | 'queryable encryption' | undefined } encryptionType plain object with paths to add, or another schema
694+ */
695+ Schema . prototype . encryptionType = function encryptionType ( encryptionType ) {
696+ if ( typeof encryptionType === 'string' || encryptionType === null ) {
697+ this . _encryptionType = encryptionType ;
698+ } else {
699+ return this . _encryptionType ;
700+ }
701+ } ;
702+
670703/**
671704 * Adds key path / schema type pairs to this schema.
672705 *
@@ -735,7 +768,7 @@ Schema.prototype.add = function add(obj, prefix) {
735768 if (
736769 key !== '_id' &&
737770 ( ( typeof val !== 'object' && typeof val !== 'function' && ! isMongooseTypeString ) ||
738- val == null )
771+ val == null )
739772 ) {
740773 throw new TypeError ( `Invalid schema configuration: \`${ val } \` is not ` +
741774 `a valid type at path \`${ key } \`. See ` +
@@ -818,15 +851,64 @@ Schema.prototype.add = function add(obj, prefix) {
818851 }
819852 }
820853 }
854+
855+ if ( val . instanceOfSchema && val . encryptionType ( ) != null ) {
856+ // schema.add({ field: <instance of encrypted schema> })
857+ if ( this . encryptionType ( ) != val . encryptionType ( ) ) {
858+ throw new Error ( 'encryptionType of a nested schema must match the encryption type of the parent schema.' ) ;
859+ }
860+
861+ for ( const [ encryptedField , encryptedFieldConfig ] of Object . entries ( val . encryptedFields ) ) {
862+ const path = fullPath + '.' + encryptedField ;
863+ this . _addEncryptedField ( path , encryptedFieldConfig ) ;
864+ }
865+ }
866+ else if ( typeof val === 'object' && 'encrypt' in val ) {
867+ // schema.add({ field: { type: <schema type>, encrypt: { ... }}})
868+ const { encrypt } = val ;
869+
870+ if ( this . encryptionType ( ) == null ) {
871+ throw new Error ( 'encryptionType must be provided' ) ;
872+ }
873+
874+ this . _addEncryptedField ( fullPath , encrypt ) ;
875+ } else {
876+ // if the field was already encrypted and we re-configure it to be unencrypted, remove
877+ // the encrypted field configuration
878+ this . _removeEncryptedField ( fullPath ) ;
879+ }
821880 }
822881
823882 const aliasObj = Object . fromEntries (
824883 Object . entries ( obj ) . map ( ( [ key ] ) => ( [ prefix + key , null ] ) )
825884 ) ;
826885 aliasFields ( this , aliasObj ) ;
886+
827887 return this ;
828888} ;
829889
890+ /**
891+ * @param {string } path
892+ * @param {object } fieldConfig
893+ * @returns
894+ */
895+ Schema . prototype . _addEncryptedField = function _addEncryptedField ( path , fieldConfig ) {
896+ const type = inferBSONType ( this , path ) ;
897+ if ( type == null ) {
898+ throw new Error ( 'unable to determine bson type for field `' + path + '`' ) ;
899+ }
900+
901+ this . encryptedFields [ path ] = clone ( fieldConfig ) ;
902+ } ;
903+
904+ Schema . prototype . _removeEncryptedField = function _removeEncryptedField ( path ) {
905+ delete this . encryptedFields [ path ] ;
906+ } ;
907+
908+ Schema . prototype . _hasEncryptedFields = function _hasEncryptedFields ( ) {
909+ return Object . keys ( this . encryptedFields ) . length > 0 ;
910+ } ;
911+
830912/**
831913 * Add an alias for `path`. This means getting or setting the `alias`
832914 * is equivalent to getting or setting the `path`.
@@ -1008,23 +1090,23 @@ Schema.prototype.reserved = Schema.reserved;
10081090const reserved = Schema . reserved ;
10091091// Core object
10101092reserved [ 'prototype' ] =
1011- // EventEmitter
1012- reserved . emit =
1013- reserved . listeners =
1014- reserved . removeListener =
1015-
1016- // document properties and functions
1017- reserved . collection =
1018- reserved . errors =
1019- reserved . get =
1020- reserved . init =
1021- reserved . isModified =
1022- reserved . isNew =
1023- reserved . populated =
1024- reserved . remove =
1025- reserved . save =
1026- reserved . toObject =
1027- reserved . validate = 1 ;
1093+ // EventEmitter
1094+ reserved . emit =
1095+ reserved . listeners =
1096+ reserved . removeListener =
1097+
1098+ // document properties and functions
1099+ reserved . collection =
1100+ reserved . errors =
1101+ reserved . get =
1102+ reserved . init =
1103+ reserved . isModified =
1104+ reserved . isNew =
1105+ reserved . populated =
1106+ reserved . remove =
1107+ reserved . save =
1108+ reserved . toObject =
1109+ reserved . validate = 1 ;
10281110reserved . collection = 1 ;
10291111
10301112/**
@@ -1104,10 +1186,10 @@ Schema.prototype.path = function(path, obj) {
11041186 }
11051187 if ( typeof branch [ sub ] !== 'object' ) {
11061188 const msg = 'Cannot set nested path `' + path + '`. '
1107- + 'Parent path `'
1108- + fullPath
1109- + '` already set to type ' + branch [ sub ] . name
1110- + '.' ;
1189+ + 'Parent path `'
1190+ + fullPath
1191+ + '` already set to type ' + branch [ sub ] . name
1192+ + '.' ;
11111193 throw new Error ( msg ) ;
11121194 }
11131195 branch = branch [ sub ] ;
@@ -1375,6 +1457,16 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
13751457 let type = obj [ options . typeKey ] && ( obj [ options . typeKey ] instanceof Function || options . typeKey !== 'type' || ! obj . type . type )
13761458 ? obj [ options . typeKey ]
13771459 : { } ;
1460+
1461+ if ( type instanceof SchemaType ) {
1462+ if ( type . path === path ) {
1463+ return type ;
1464+ }
1465+ const clone = type . clone ( ) ;
1466+ clone . path = path ;
1467+ return clone ;
1468+ }
1469+
13781470 let name ;
13791471
13801472 if ( utils . isPOJO ( type ) || type === 'mixed' ) {
@@ -1404,8 +1496,8 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
14041496 return new MongooseTypes . DocumentArray ( path , cast , obj ) ;
14051497 }
14061498 if ( cast &&
1407- cast [ options . typeKey ] &&
1408- cast [ options . typeKey ] . instanceOfSchema ) {
1499+ cast [ options . typeKey ] &&
1500+ cast [ options . typeKey ] . instanceOfSchema ) {
14091501 if ( ! ( cast [ options . typeKey ] instanceof Schema ) ) {
14101502 if ( this . options . _isMerging ) {
14111503 cast [ options . typeKey ] = new Schema ( cast [ options . typeKey ] ) ;
@@ -1739,7 +1831,7 @@ Schema.prototype.hasMixedParent = function(path) {
17391831 for ( let i = 0 ; i < subpaths . length ; ++ i ) {
17401832 path = i > 0 ? path + '.' + subpaths [ i ] : subpaths [ i ] ;
17411833 if ( this . paths . hasOwnProperty ( path ) &&
1742- this . paths [ path ] instanceof MongooseTypes . Mixed ) {
1834+ this . paths [ path ] instanceof MongooseTypes . Mixed ) {
17431835 return this . paths [ path ] ;
17441836 }
17451837 }
@@ -2516,6 +2608,8 @@ Schema.prototype.remove = function(path) {
25162608
25172609 delete this . paths [ name ] ;
25182610 _deletePath ( this , name ) ;
2611+
2612+ this . _removeEncryptedField ( name ) ;
25192613 } , this ) ;
25202614 }
25212615 return this ;
@@ -2611,9 +2705,9 @@ Schema.prototype.removeVirtual = function(path) {
26112705Schema . prototype . loadClass = function ( model , virtualsOnly ) {
26122706 // Stop copying when hit certain base classes
26132707 if ( model === Object . prototype ||
2614- model === Function . prototype ||
2615- model . prototype . hasOwnProperty ( '$isMongooseModelPrototype' ) ||
2616- model . prototype . hasOwnProperty ( '$isMongooseDocumentPrototype' ) ) {
2708+ model === Function . prototype ||
2709+ model . prototype . hasOwnProperty ( '$isMongooseModelPrototype' ) ||
2710+ model . prototype . hasOwnProperty ( '$isMongooseDocumentPrototype' ) ) {
26172711 return this ;
26182712 }
26192713
0 commit comments