mongoose 8.14.3 → 8.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -235,6 +235,10 @@ SchemaDecimal128.prototype.toJSONSchema = function toJSONSchema(options) {
235
235
  return createJSONSchemaTypeDefinition('string', 'decimal', options?.useBsonType, isRequired);
236
236
  };
237
237
 
238
+ SchemaDecimal128.prototype.autoEncryptionType = function autoEncryptionType() {
239
+ return 'decimal';
240
+ };
241
+
238
242
  /*!
239
243
  * Module exports.
240
244
  */
@@ -218,6 +218,10 @@ SchemaDouble.prototype.toJSONSchema = function toJSONSchema(options) {
218
218
  return createJSONSchemaTypeDefinition('number', 'double', options?.useBsonType, isRequired);
219
219
  };
220
220
 
221
+ SchemaDouble.prototype.autoEncryptionType = function autoEncryptionType() {
222
+ return 'double';
223
+ };
224
+
221
225
  /*!
222
226
  * Module exports.
223
227
  */
@@ -260,6 +260,10 @@ SchemaInt32.prototype.toJSONSchema = function toJSONSchema(options) {
260
260
  return createJSONSchemaTypeDefinition('number', 'int', options?.useBsonType, isRequired);
261
261
  };
262
262
 
263
+ SchemaInt32.prototype.autoEncryptionType = function autoEncryptionType() {
264
+ return 'int';
265
+ };
266
+
263
267
 
264
268
  /*!
265
269
  * Module exports.
package/lib/schema/map.js CHANGED
@@ -95,6 +95,10 @@ class SchemaMap extends SchemaType {
95
95
 
96
96
  return result;
97
97
  }
98
+
99
+ autoEncryptionType() {
100
+ return 'object';
101
+ }
98
102
  }
99
103
 
100
104
  /**
@@ -304,6 +304,10 @@ SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) {
304
304
  return createJSONSchemaTypeDefinition('string', 'objectId', options?.useBsonType, isRequired);
305
305
  };
306
306
 
307
+ SchemaObjectId.prototype.autoEncryptionType = function autoEncryptionType() {
308
+ return 'objectId';
309
+ };
310
+
307
311
  /*!
308
312
  * Module exports.
309
313
  */
@@ -712,6 +712,10 @@ SchemaString.prototype.toJSONSchema = function toJSONSchema(options) {
712
712
  return createJSONSchemaTypeDefinition('string', 'string', options?.useBsonType, isRequired);
713
713
  };
714
714
 
715
+ SchemaString.prototype.autoEncryptionType = function autoEncryptionType() {
716
+ return 'string';
717
+ };
718
+
715
719
  /*!
716
720
  * Module exports.
717
721
  */
@@ -298,6 +298,10 @@ SchemaUUID.prototype.toJSONSchema = function toJSONSchema(options) {
298
298
  return createJSONSchemaTypeDefinition('string', 'binData', options?.useBsonType, isRequired);
299
299
  };
300
300
 
301
+ SchemaUUID.prototype.autoEncryptionType = function autoEncryptionType() {
302
+ return 'binData';
303
+ };
304
+
301
305
  /*!
302
306
  * Module exports.
303
307
  */
package/lib/schema.js CHANGED
@@ -86,6 +86,7 @@ const numberRE = /^\d+$/;
86
86
  * - [pluginTags](https://mongoosejs.com/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
87
87
  * - [virtuals](https://mongoosejs.com/docs/tutorials/virtuals.html#virtuals-via-schema-options): object - virtuals to define, alias for [`.virtual`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.virtual())
88
88
  * - [collectionOptions]: object with options passed to [`createCollection()`](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/) when calling `Model.createCollection()` or `autoCreate` set to true.
89
+ * - [encryptionType]: the encryption type for the schema. Valid options are `csfle` or `queryableEncryption`. See https://mongoosejs.com/docs/field-level-encryption.
89
90
  *
90
91
  * #### Options for Nested Schemas:
91
92
  *
@@ -128,6 +129,7 @@ function Schema(obj, options) {
128
129
  // For internal debugging. Do not use this to try to save a schema in MDB.
129
130
  this.$id = ++id;
130
131
  this.mapPaths = [];
132
+ this.encryptedFields = {};
131
133
 
132
134
  this.s = {
133
135
  hooks: new Kareem()
@@ -463,6 +465,8 @@ Schema.prototype._clone = function _clone(Constructor) {
463
465
 
464
466
  s.aliases = Object.assign({}, this.aliases);
465
467
 
468
+ s.encryptedFields = clone(this.encryptedFields);
469
+
466
470
  return s;
467
471
  };
468
472
 
@@ -495,7 +499,16 @@ Schema.prototype.pick = function(paths, options) {
495
499
  }
496
500
 
497
501
  for (const path of paths) {
498
- if (this.nested[path]) {
502
+ if (this._hasEncryptedField(path)) {
503
+ const encrypt = this.encryptedFields[path];
504
+ const schemaType = this.path(path);
505
+ newSchema.add({
506
+ [path]: {
507
+ encrypt,
508
+ [this.options.typeKey]: schemaType
509
+ }
510
+ });
511
+ } else if (this.nested[path]) {
499
512
  newSchema.add({ [path]: get(this.tree, path) });
500
513
  } else {
501
514
  const schematype = this.path(path);
@@ -506,6 +519,10 @@ Schema.prototype.pick = function(paths, options) {
506
519
  }
507
520
  }
508
521
 
522
+ if (!this._hasEncryptedFields()) {
523
+ newSchema.options.encryptionType = null;
524
+ }
525
+
509
526
  return newSchema;
510
527
  };
511
528
 
@@ -667,6 +684,22 @@ Schema.prototype._defaultToObjectOptions = function(json) {
667
684
  return defaultOptions;
668
685
  };
669
686
 
687
+ /**
688
+ * Sets the encryption type of the schema, if a value is provided, otherwise
689
+ * returns the encryption type.
690
+ *
691
+ * @param {'csfle' | 'queryableEncryption' | null | undefined} encryptionType plain object with paths to add, or another schema
692
+ */
693
+ Schema.prototype.encryptionType = function encryptionType(encryptionType) {
694
+ if (arguments.length === 0) {
695
+ return this.options.encryptionType;
696
+ }
697
+ if (!(typeof encryptionType === 'string' || encryptionType === null)) {
698
+ throw new Error('invalid `encryptionType`: ${encryptionType}');
699
+ }
700
+ this.options.encryptionType = encryptionType;
701
+ };
702
+
670
703
  /**
671
704
  * Adds key path / schema type pairs to this schema.
672
705
  *
@@ -689,7 +722,6 @@ Schema.prototype._defaultToObjectOptions = function(json) {
689
722
  Schema.prototype.add = function add(obj, prefix) {
690
723
  if (obj instanceof Schema || (obj != null && obj.instanceOfSchema)) {
691
724
  merge(this, obj);
692
-
693
725
  return this;
694
726
  }
695
727
 
@@ -818,6 +850,31 @@ Schema.prototype.add = function add(obj, prefix) {
818
850
  }
819
851
  }
820
852
  }
853
+
854
+ if (val.instanceOfSchema && val.encryptionType() != null) {
855
+ // schema.add({ field: <instance of encrypted schema> })
856
+ if (this.encryptionType() != val.encryptionType()) {
857
+ throw new Error('encryptionType of a nested schema must match the encryption type of the parent schema.');
858
+ }
859
+
860
+ for (const [encryptedField, encryptedFieldConfig] of Object.entries(val.encryptedFields)) {
861
+ const path = fullPath + '.' + encryptedField;
862
+ this._addEncryptedField(path, encryptedFieldConfig);
863
+ }
864
+ } else if (typeof val === 'object' && 'encrypt' in val) {
865
+ // schema.add({ field: { type: <schema type>, encrypt: { ... }}})
866
+ const { encrypt } = val;
867
+
868
+ if (this.encryptionType() == null) {
869
+ throw new Error('encryptionType must be provided');
870
+ }
871
+
872
+ this._addEncryptedField(fullPath, encrypt);
873
+ } else {
874
+ // if the field was already encrypted and we re-configure it to be unencrypted, remove
875
+ // the encrypted field configuration
876
+ this._removeEncryptedField(fullPath);
877
+ }
821
878
  }
822
879
 
823
880
  const aliasObj = Object.fromEntries(
@@ -827,6 +884,117 @@ Schema.prototype.add = function add(obj, prefix) {
827
884
  return this;
828
885
  };
829
886
 
887
+ /**
888
+ * @param {string} path
889
+ * @param {object} fieldConfig
890
+ *
891
+ * @api private
892
+ */
893
+ Schema.prototype._addEncryptedField = function _addEncryptedField(path, fieldConfig) {
894
+ const type = this.path(path).autoEncryptionType();
895
+ if (type == null) {
896
+ throw new Error(`Invalid BSON type for FLE field: '${path}'`);
897
+ }
898
+
899
+ this.encryptedFields[path] = clone(fieldConfig);
900
+ };
901
+
902
+ /**
903
+ * @param {string} path
904
+ *
905
+ * @api private
906
+ */
907
+ Schema.prototype._removeEncryptedField = function _removeEncryptedField(path) {
908
+ delete this.encryptedFields[path];
909
+ };
910
+
911
+ /**
912
+ * @api private
913
+ *
914
+ * @returns {boolean}
915
+ */
916
+ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() {
917
+ return Object.keys(this.encryptedFields).length > 0;
918
+ };
919
+
920
+ /**
921
+ * @param {string} path
922
+ * @returns {boolean}
923
+ *
924
+ * @api private
925
+ */
926
+ Schema.prototype._hasEncryptedField = function _hasEncryptedField(path) {
927
+ return path in this.encryptedFields;
928
+ };
929
+
930
+
931
+ /**
932
+ * Builds an encryptedFieldsMap for the schema.
933
+ *
934
+ * @api private
935
+ */
936
+ Schema.prototype._buildEncryptedFields = function() {
937
+ const fields = Object.entries(this.encryptedFields).map(
938
+ ([path, config]) => {
939
+ const bsonType = this.path(path).autoEncryptionType();
940
+ // { path, bsonType, keyId, queries? }
941
+ return { path, bsonType, ...config };
942
+ });
943
+
944
+ return { fields };
945
+ };
946
+
947
+ /**
948
+ * Builds a schemaMap for the schema, if the schema is configured for client-side field level encryption.
949
+ *
950
+ * @api private
951
+ */
952
+ Schema.prototype._buildSchemaMap = function() {
953
+ /**
954
+ * `schemaMap`s are JSON schemas, which use the following structure to represent objects:
955
+ * { field: { bsonType: 'object', properties: { ... } } }
956
+ *
957
+ * for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as
958
+ * `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }`
959
+ *
960
+ * This function takes an array of path segments, an output object (that gets mutated) and
961
+ * a value to be associated with the full path, and constructs a valid CSFLE JSON schema path for
962
+ * the object. This works for deeply nested properties as well.
963
+ *
964
+ * @param {string[]} path array of path components
965
+ * @param {object} object the object in which to build a JSON schema of `path`'s properties
966
+ * @param {object} value the value to associate with the path in object
967
+ */
968
+ function buildNestedPath(path, object, value) {
969
+ let i = 0, component = path[i];
970
+ for (; i < path.length - 1; ++i, component = path[i]) {
971
+ object[component] = object[component] == null ? {
972
+ bsonType: 'object',
973
+ properties: {}
974
+ } : object[component];
975
+ object = object[component].properties;
976
+ }
977
+ object[component] = value;
978
+ }
979
+
980
+ const schemaMapPropertyReducer = (accum, [path, propertyConfig]) => {
981
+ const bsonType = this.path(path).autoEncryptionType();
982
+ const pathComponents = path.split('.');
983
+ const configuration = { encrypt: { ...propertyConfig, bsonType } };
984
+ buildNestedPath(pathComponents, accum, configuration);
985
+ return accum;
986
+ };
987
+
988
+ const properties = Object.entries(this.encryptedFields).reduce(
989
+ schemaMapPropertyReducer,
990
+ {});
991
+
992
+ return {
993
+ bsonType: 'object',
994
+ properties
995
+ };
996
+ };
997
+
830
998
  /**
831
999
  * Add an alias for `path`. This means getting or setting the `alias`
832
1000
  * is equivalent to getting or setting the `path`.
@@ -1378,6 +1546,16 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
1378
1546
  let type = obj[options.typeKey] && (obj[options.typeKey] instanceof Function || options.typeKey !== 'type' || !obj.type.type)
1379
1547
  ? obj[options.typeKey]
1380
1548
  : {};
1549
+
1550
+ if (type instanceof SchemaType) {
1551
+ if (type.path === path) {
1552
+ return type;
1553
+ }
1554
+ const clone = type.clone();
1555
+ clone.path = path;
1556
+ return clone;
1557
+ }
1558
+
1381
1559
  let name;
1382
1560
 
1383
1561
  if (utils.isPOJO(type) || type === 'mixed') {
@@ -2523,6 +2701,8 @@ Schema.prototype.remove = function(path) {
2523
2701
 
2524
2702
  delete this.paths[name];
2525
2703
  _deletePath(this, name);
2704
+
2705
+ this._removeEncryptedField(name);
2526
2706
  }, this);
2527
2707
  }
2528
2708
  return this;
package/lib/schemaType.js CHANGED
@@ -1783,6 +1783,14 @@ SchemaType.prototype.toJSONSchema = function toJSONSchema() {
1783
1783
  throw new Error('Converting unsupported SchemaType to JSON Schema: ' + this.instance);
1784
1784
  };
1785
1785
 
1786
+ /**
1787
+ * Returns the BSON type that the schema corresponds to, for automatic encryption.
1788
+ * @api private
1789
+ */
1790
+ SchemaType.prototype.autoEncryptionType = function autoEncryptionType() {
1791
+ return null;
1792
+ };
1793
+
1786
1794
  /*!
1787
1795
  * Module exports.
1788
1796
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "8.14.3",
4
+ "version": "8.15.1",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -31,6 +31,7 @@
31
31
  "devDependencies": {
32
32
  "@babel/core": "7.27.1",
33
33
  "@babel/preset-env": "7.27.1",
34
+ "@mongodb-js/mongodb-downloader": "^0.3.9",
34
35
  "@typescript-eslint/eslint-plugin": "^8.19.1",
35
36
  "@typescript-eslint/parser": "^8.19.1",
36
37
  "acquit": "1.3.0",
@@ -58,6 +59,7 @@
58
59
  "mocha": "11.2.2",
59
60
  "moment": "2.30.1",
60
61
  "mongodb-memory-server": "10.1.4",
62
+ "mongodb-runner": "^5.8.2",
61
63
  "ncp": "^2.0.0",
62
64
  "nyc": "15.1.0",
63
65
  "pug": "3.0.3",
@@ -104,7 +106,7 @@
104
106
  "test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.mjs",
105
107
  "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js",
106
108
  "test-tsd": "node ./test/types/check-types-filename && tsd",
107
- "setup-test-encryption": "bash scripts/configure-cluster-with-encryption.sh",
109
+ "setup-test-encryption": "node scripts/setup-encryption-tests.js",
108
110
  "test-encryption": "mocha --exit ./test/encryption/*.test.js",
109
111
  "tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}",
110
112
  "test-coverage": "nyc --reporter=html --reporter=text npm test",
package/types/index.d.ts CHANGED
@@ -657,9 +657,43 @@ declare module 'mongoose' {
657
657
 
658
658
  export type ReturnsNewDoc = { new: true } | { returnOriginal: false } | { returnDocument: 'after' };
659
659
 
660
- export type ProjectionElementType = number | string;
661
- export type ProjectionType<T> = { [P in keyof T]?: ProjectionElementType } | AnyObject | string;
662
-
660
+ type ArrayOperators = { $slice: number | [number, number]; $elemMatch?: never } | { $elemMatch: Record<string, any>; $slice?: never };
661
+ /**
662
+ * This Type Assigns `Element | undefined` recursively to the `T` type.
663
+ * if it is an array it will do this to the element of the array, if it is an object it will do this for the properties of the object.
664
+ * `Element` is the truthy or falsy values that are going to be used as the value of the projection.(1 | true or 0 | false)
665
+ * For the elements of the array we will use: `Element | `undefined` | `ArrayOperators`
666
+ * @example
667
+ * type CalculatedType = Projector<{ a: string, b: number, c: { d: string }, d: string[] }, true>
668
+ * type CalculatedType = {
669
+ a?: true | undefined;
670
+ b?: true | undefined;
671
+ c?: true | {
672
+ d?: true | undefined;
673
+ } | undefined;
674
+ d?: true | ArrayOperators | undefined;
675
+ }
676
+ */
677
+ type Projector<T, Element> = T extends Array<infer U>
678
+ ? Projector<U, Element> | ArrayOperators
679
+ : T extends TreatAsPrimitives
680
+ ? Element
681
+ : T extends Record<string, any>
682
+ ? {
683
+ [K in keyof T]?: T[K] extends Record<string, any> ? Projector<T[K], Element> | Element : Element;
684
+ }
685
+ : Element;
686
+ type _IDType = { _id?: boolean | 1 | 0 };
687
+ export type InclusionProjection<T> = IsItRecordAndNotAny<T> extends true
688
+ ? Omit<Projector<WithLevel1NestedPaths<T>, true | 1>, '_id'> & _IDType
689
+ : AnyObject;
690
+ export type ExclusionProjection<T> = IsItRecordAndNotAny<T> extends true
691
+ ? Omit<Projector<WithLevel1NestedPaths<T>, false | 0>, '_id'> & _IDType
692
+ : AnyObject;
693
+
694
+ export type ProjectionType<T> = (InclusionProjection<T> & AnyObject)
695
+ | (ExclusionProjection<T> & AnyObject)
696
+ | string;
663
697
  export type SortValues = SortOrder;
664
698
 
665
699
  export type SortOrder = -1 | 1 | 'asc' | 'ascending' | 'desc' | 'descending';
package/types/models.d.ts CHANGED
@@ -919,5 +919,11 @@ declare module 'mongoose' {
919
919
  'find',
920
920
  TInstanceMethods & TVirtuals
921
921
  >;
922
+
923
+ /**
924
+ * If auto encryption is enabled, returns a ClientEncryption instance that is configured with the same settings that
925
+ * Mongoose's underlying MongoClient is using. If the client has not yet been configured, returns null.
926
+ */
927
+ clientEncryption(): mongodb.ClientEncryption | null;
922
928
  }
923
929
  }
@@ -319,10 +319,11 @@ declare module 'mongoose' {
319
319
  export interface VectorSearch {
320
320
  /** [`$vectorSearch` reference](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/) */
321
321
  $vectorSearch: {
322
+ exact?: boolean;
322
323
  index: string,
323
324
  path: string,
324
325
  queryVector: number[],
325
- numCandidates: number,
326
+ numCandidates?: number,
326
327
  limit: number,
327
328
  filter?: Expression,
328
329
  }
package/types/query.d.ts CHANGED
@@ -160,7 +160,7 @@ declare module 'mongoose' {
160
160
  * Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
161
161
  */
162
162
  overwriteImmutable?: boolean;
163
- projection?: ProjectionType<DocType>;
163
+ projection?: { [P in keyof DocType]?: number | string } | AnyObject | string;
164
164
  /**
165
165
  * if true, returns the full ModifyResult rather than just the document
166
166
  */
@@ -259,6 +259,11 @@ declare module 'mongoose' {
259
259
  * @default false
260
260
  */
261
261
  overwriteModels?: boolean;
262
+
263
+ /**
264
+ * Required when the schema is encrypted.
265
+ */
266
+ encryptionType?: 'csfle' | 'queryableEncryption';
262
267
  }
263
268
 
264
269
  interface DefaultSchemaOptions {
@@ -1,3 +1,5 @@
1
+ import * as BSON from 'bson';
2
+
1
3
  declare module 'mongoose' {
2
4
 
3
5
  /** The Mongoose Date [SchemaType](/docs/schematypes.html). */
@@ -210,6 +212,11 @@ declare module 'mongoose' {
210
212
  maxlength?: number | [number, string] | readonly [number, string];
211
213
 
212
214
  [other: string]: any;
215
+
216
+ /**
217
+ * If set, configures the field for automatic encryption.
218
+ */
219
+ encrypt?: EncryptSchemaTypeOptions;
213
220
  }
214
221
 
215
222
  interface Validator<DocType = any> {
@@ -221,6 +228,28 @@ declare module 'mongoose' {
221
228
 
222
229
  type ValidatorFunction<DocType = any> = (this: DocType, value: any, validatorProperties?: Validator) => any;
223
230
 
231
+ interface QueryEncryptionEncryptOptions {
232
+ /** The id of the dataKey to use for encryption. Must be a BSON binary subtype 4 (UUID). */
233
+ keyId: BSON.Binary;
234
+
235
+ /**
236
+ * Specifies the type of queries that the field can be queried on the encrypted field.
237
+ */
238
+ queries?: 'equality' | 'range';
239
+ }
240
+
241
+ interface ClientSideEncryptionEncryptOptions {
242
+ /** The id of the dataKey to use for encryption. Must be a BSON binary subtype 4 (UUID). */
243
+ keyId: [BSON.Binary];
244
+
245
+ /**
246
+ * The algorithm to use for encryption.
247
+ */
248
+ algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic' | 'AEAD_AES_256_CBC_HMAC_SHA_512-Random';
249
+ }
250
+
251
+ export type EncryptSchemaTypeOptions = QueryEncryptionEncryptOptions | ClientSideEncryptionEncryptOptions;
252
+
224
253
  class SchemaType<T = any, DocType = any> {
225
254
  /** SchemaType constructor */
226
255
  constructor(path: string, options?: AnyObject, instance?: string);
package/types/types.d.ts CHANGED
@@ -60,7 +60,7 @@ declare module 'mongoose' {
60
60
 
61
61
  class Decimal128 extends mongodb.Decimal128 { }
62
62
 
63
- class DocumentArray<T, THydratedDocumentType extends Types.Subdocument<any> = Types.Subdocument<InferId<T>, any, T> & T> extends Types.Array<THydratedDocumentType> {
63
+ class DocumentArray<T, THydratedDocumentType extends Types.Subdocument<any, any, T> = Types.Subdocument<InferId<T>, any, T> & T> extends Types.Array<THydratedDocumentType> {
64
64
  /** DocumentArray constructor */
65
65
  constructor(values: AnyObject[]);
66
66
 
@@ -85,7 +85,7 @@ declare module 'mongoose' {
85
85
  class ObjectId extends mongodb.ObjectId {
86
86
  }
87
87
 
88
- class Subdocument<IdType = unknown, TQueryHelpers = any, DocType = any> extends Document<IdType, TQueryHelpers, DocType> {
88
+ class Subdocument<IdType = any, TQueryHelpers = any, DocType = any> extends Document<IdType, TQueryHelpers, DocType> {
89
89
  $isSingleNested: true;
90
90
 
91
91
  /** Returns the top level document of this sub-document. */
@@ -4,20 +4,44 @@ declare module 'mongoose' {
4
4
 
5
5
  type WithLevel1NestedPaths<T, K extends keyof T = keyof T> = {
6
6
  [P in K | NestedPaths<Required<T>, K>]: P extends K
7
- ? T[P]
7
+ // Handle top-level paths
8
+ // First, drill into documents so we don't end up surfacing `$assertPopulated`, etc.
9
+ ? Extract<NonNullable<T[P]>, Document> extends never
10
+ // If not a document, then return the type. Otherwise, get the DocType.
11
+ ? NonNullable<T[P]>
12
+ : Extract<NonNullable<T[P]>, Document> extends Document<any, any, infer DocType, any>
13
+ ? DocType
14
+ : never
15
+ // Handle nested paths
8
16
  : P extends `${infer Key}.${infer Rest}`
9
17
  ? Key extends keyof T
10
- ? Rest extends keyof NonNullable<T[Key]>
11
- ? NonNullable<T[Key]>[Rest]
12
- : never
18
+ ? T[Key] extends (infer U)[]
19
+ ? Rest extends keyof NonNullable<U>
20
+ ? NonNullable<U>[Rest]
21
+ : never
22
+ : Rest extends keyof NonNullable<T[Key]>
23
+ ? NonNullable<T[Key]>[Rest]
24
+ : never
13
25
  : never
14
26
  : never;
15
27
  };
16
28
 
17
29
  type NestedPaths<T, K extends keyof T> = K extends string
18
- ? T[K] extends Record<string, any> | null | undefined
19
- ? `${K}.${keyof NonNullable<T[K]> & string}`
20
- : never
30
+ ? T[K] extends TreatAsPrimitives
31
+ ? never
32
+ : Extract<NonNullable<T[K]>, Document> extends never
33
+ ? T[K] extends Array<infer U>
34
+ ? U extends Record<string, any>
35
+ ? `${K}.${keyof NonNullable<U> & string}`
36
+ : never
37
+ : T[K] extends Record<string, any> | null | undefined
38
+ ? `${K}.${keyof NonNullable<T[K]> & string}`
39
+ : never
40
+ : Extract<NonNullable<T[K]>, Document> extends Document<any, any, infer DocType, any>
41
+ ? DocType extends Record<string, any>
42
+ ? `${K}.${keyof NonNullable<DocType> & string}`
43
+ : never
44
+ : never
21
45
  : never;
22
46
 
23
47
  type WithoutUndefined<T> = T extends undefined ? never : T;