Skip to content

Commit 6622f67

Browse files
authored
Merge pull request #15582 from Automattic/vkarpov15/gh-15578
types(document): better support for `flattenObjectIds` and `versionKey` options for toObject() and toJSON()
2 parents 7765cc3 + e2534f2 commit 6622f67

File tree

5 files changed

+200
-13
lines changed

5 files changed

+200
-13
lines changed

test/document.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14586,6 +14586,35 @@ describe('document', function() {
1458614586
assert.strictEqual(updatedItem.nested.get('inserted').map.get('a1'), 1);
1458714587
assert.strictEqual(updatedItem.nested.get('inserted2').map.get('a1'), 1);
1458814588
});
14589+
14590+
it('removes versionKey from output if versionKey: false set on toObject() or toJSON() (gh-15578)', async function() {
14591+
const schema = new mongoose.Schema({ name: String }, { versionKey: '__v' });
14592+
const Model = db.model('Test', schema);
14593+
14594+
const doc = await Model.create({ name: 'test' });
14595+
14596+
// Default: versionKey present
14597+
let obj = doc.toObject();
14598+
assert.ok(obj.hasOwnProperty('__v'));
14599+
14600+
obj = doc.toObject();
14601+
assert.ok(obj.hasOwnProperty('__v'));
14602+
14603+
// toObject({ versionKey: false }) removes versionKey
14604+
obj = doc.toObject({ versionKey: false });
14605+
assert.ok(!obj.hasOwnProperty('__v'));
14606+
14607+
// toJSON({ versionKey: false }) removes versionKey
14608+
obj = doc.toJSON({ versionKey: false });
14609+
assert.ok(!obj.hasOwnProperty('__v'));
14610+
14611+
// If versionKey: false in schema, versionKey should not be present
14612+
const schemaNoVersion = new mongoose.Schema({ name: String }, { versionKey: false });
14613+
const ModelNoVersion = db.model('TestNoVersion', schemaNoVersion);
14614+
const docNoVersion = await ModelNoVersion.create({ name: 'test2' });
14615+
obj = docNoVersion.toObject();
14616+
assert.ok(!obj.hasOwnProperty('__v'));
14617+
});
1458914618
});
1459014619

1459114620
describe('Check if instance function that is supplied in schema option is available', function() {

test/types/document.test.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
HydratedDocument,
88
HydratedArraySubdocument,
99
HydratedSingleSubdocument,
10-
DefaultSchemaOptions
10+
DefaultSchemaOptions,
11+
ObtainSchemaGeneric,
12+
ResolveSchemaOptions
1113
} from 'mongoose';
1214
import { DeleteResult } from 'mongodb';
1315
import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd';
@@ -475,3 +477,135 @@ async function gh15316() {
475477
expectType<string>(doc.toJSON({ virtuals: true }).upper);
476478
expectType<string>(doc.toObject({ virtuals: true }).upper);
477479
}
480+
481+
async function gh15578() {
482+
function withDocType() {
483+
interface RawDocType {
484+
_id: Types.ObjectId;
485+
testProperty: number;
486+
}
487+
488+
const ASchema = new Schema<RawDocType>({
489+
testProperty: Number
490+
});
491+
492+
const AModel = model<RawDocType>('YourModel', ASchema);
493+
494+
const a = new AModel({ testProperty: 8 });
495+
const toObjectFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ flattenObjectIds: true });
496+
const toObjectWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: true, flattenObjectIds: true });
497+
const toObjectWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: false, flattenObjectIds: true });
498+
const toJSONFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ flattenObjectIds: true });
499+
const toJSONWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: true, flattenObjectIds: true });
500+
const toJSONWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: false, flattenObjectIds: true });
501+
502+
const objWithoutVersionKey = a.toObject({ versionKey: false });
503+
const jsonWithoutVersionKey = a.toJSON({ versionKey: false });
504+
expectError(objWithoutVersionKey.__v);
505+
expectError(jsonWithoutVersionKey.__v);
506+
507+
const objWithVersionKey = a.toObject();
508+
const jsonWithVersionKey = a.toJSON();
509+
expectType<number>(objWithVersionKey.__v);
510+
expectType<number>(jsonWithVersionKey.__v);
511+
}
512+
513+
function withDocTypeAndVersionKey() {
514+
interface RawDocType {
515+
_id: Types.ObjectId;
516+
testProperty: number;
517+
}
518+
519+
const schemaOptions = { versionKey: 'taco' } as const;
520+
521+
type ModelType = Model<RawDocType, {}, {}, {}, HydratedDocument<RawDocType, {}, {}, {}, typeof schemaOptions>>;
522+
523+
const ASchema = new Schema<RawDocType, ModelType, {}, {}, {}, {}, typeof schemaOptions>({
524+
testProperty: Number
525+
}, schemaOptions);
526+
527+
const AModel = model<RawDocType, ModelType>('YourModel', ASchema);
528+
529+
const a = new AModel({ testProperty: 8 });
530+
const toObjectFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ flattenObjectIds: true });
531+
const toObjectWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: true, flattenObjectIds: true });
532+
const toObjectWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: false, flattenObjectIds: true });
533+
const toJSONFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ flattenObjectIds: true });
534+
const toJSONWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: true, flattenObjectIds: true });
535+
const toJSONWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: false, flattenObjectIds: true });
536+
537+
const objWithoutVersionKey = a.toObject({ versionKey: false });
538+
const jsonWithoutVersionKey = a.toJSON({ versionKey: false });
539+
expectError(objWithoutVersionKey.taco);
540+
expectError(jsonWithoutVersionKey.taco);
541+
542+
const objWithVersionKey = a.toObject();
543+
const jsonWithVersionKey = a.toJSON();
544+
expectType<number>(objWithVersionKey.taco);
545+
expectType<number>(jsonWithVersionKey.taco);
546+
}
547+
548+
function autoInferred() {
549+
interface RawDocType {
550+
_id: Types.ObjectId;
551+
testProperty?: number | null;
552+
}
553+
554+
const ASchema = new Schema({
555+
testProperty: Number
556+
});
557+
558+
const AModel = model('YourModel', ASchema);
559+
560+
const a = new AModel({ testProperty: 8 });
561+
const toObjectFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ flattenObjectIds: true });
562+
const toObjectWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: true, flattenObjectIds: true });
563+
const toObjectWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: false, flattenObjectIds: true });
564+
const toJSONFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ flattenObjectIds: true });
565+
const toJSONWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: true, flattenObjectIds: true });
566+
const toJSONWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: false, flattenObjectIds: true });
567+
568+
const objWithoutVersionKey = a.toObject({ versionKey: false });
569+
const jsonWithoutVersionKey = a.toJSON({ versionKey: false });
570+
expectError(objWithoutVersionKey.__v);
571+
expectError(jsonWithoutVersionKey.__v);
572+
573+
const objWithVersionKey = a.toObject();
574+
const jsonWithVersionKey = a.toJSON();
575+
expectType<number>(objWithVersionKey.__v);
576+
expectType<number>(jsonWithVersionKey.__v);
577+
}
578+
579+
function autoInferredWithCustomVersionKey() {
580+
interface RawDocType {
581+
_id: Types.ObjectId;
582+
testProperty?: number | null;
583+
}
584+
585+
const ASchema = new Schema({
586+
testProperty: Number
587+
}, { versionKey: 'taco' });
588+
589+
const AModel = model('YourModel', ASchema);
590+
591+
type TSchemaOptions = ResolveSchemaOptions<ObtainSchemaGeneric<typeof ASchema, 'TSchemaOptions'>>;
592+
593+
const a = new AModel({ testProperty: 8 });
594+
const toObjectFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ flattenObjectIds: true });
595+
const toObjectWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: true, flattenObjectIds: true });
596+
const toObjectWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toObject({ virtuals: false, flattenObjectIds: true });
597+
const toJSONFlattened: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ flattenObjectIds: true });
598+
const toJSONWithVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: true, flattenObjectIds: true });
599+
const toJSONWithoutVirtuals: Omit<RawDocType, '_id'> & { _id: string } = a.toJSON({ virtuals: false, flattenObjectIds: true });
600+
601+
const objWithoutVersionKey = a.toObject({ versionKey: false });
602+
const jsonWithoutVersionKey = a.toJSON({ versionKey: false });
603+
expectError(objWithoutVersionKey.taco);
604+
expectError(jsonWithoutVersionKey.taco);
605+
606+
const objWithVersionKey = a.toObject();
607+
const jsonWithVersionKey = a.toJSON();
608+
expectType<number>(objWithVersionKey.taco);
609+
expectType<number>(jsonWithVersionKey.taco);
610+
}
611+
}

test/types/schema.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ function gh11997() {
643643
function gh12003() {
644644
const baseSchemaOptions = {
645645
versionKey: false
646-
};
646+
} as const;
647647

648648
const BaseSchema = new Schema({
649649
name: String
@@ -653,6 +653,7 @@ function gh12003() {
653653

654654
type TSchemaOptions = ResolveSchemaOptions<ObtainSchemaGeneric<typeof BaseSchema, 'TSchemaOptions'>>;
655655
expectType<'type'>({} as TSchemaOptions['typeKey']);
656+
expectType<false>({} as TSchemaOptions['versionKey']);
656657

657658
expectType<{ name?: string | null }>({} as BaseSchemaType);
658659
}

types/document.d.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,17 @@ declare module 'mongoose' {
256256
set(value: string | Record<string, any>): this;
257257

258258
/** The return value of this method is used in calls to JSON.stringify(doc). */
259-
toJSON(options: ToObjectOptions & { virtuals: true }): Default__v<Require_id<DocType & TVirtuals>, TSchemaOptions>;
260-
toJSON(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<Default__v<Require_id<DocType>, TSchemaOptions>>;
261-
toJSON(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<Default__v<Require_id<DocType>, TSchemaOptions>>;
262-
toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Default__v<Require_id<DocType>, TSchemaOptions>>>;
263-
toJSON(options: ToObjectOptions & { flattenMaps: false }): Default__v<Require_id<DocType>, TSchemaOptions>;
264-
toJSON(options: ToObjectOptions & { flattenMaps: false; flattenObjectIds: true }): ObjectIdToString<Default__v<Require_id<DocType>, TSchemaOptions>>;
259+
toJSON(options: ToObjectOptions & { versionKey: false, virtuals: true, flattenObjectIds: true }): Omit<ObjectIdToString<FlattenMaps<Require_id<DocType & TVirtuals>>>, '__v'>;
260+
toJSON(options: ToObjectOptions & { virtuals: true, flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Default__v<Require_id<DocType & TVirtuals>, ResolveSchemaOptions<TSchemaOptions>>>>;
261+
toJSON(options: ToObjectOptions & { versionKey: false, virtuals: true }): Omit<Require_id<DocType & TVirtuals>, '__v'>;
262+
toJSON(options: ToObjectOptions & { versionKey: false, flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Omit<Require_id<DocType>, '__v'>>>;
263+
toJSON(options: ToObjectOptions & { virtuals: true }): Default__v<Require_id<DocType & TVirtuals>, ResolveSchemaOptions<TSchemaOptions>>;
264+
toJSON(options: ToObjectOptions & { versionKey: false }): Omit<Require_id<DocType & TVirtuals>, '__v'>;
265+
toJSON(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>>;
266+
toJSON(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>>;
267+
toJSON(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<FlattenMaps<Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>>>;
268+
toJSON(options: ToObjectOptions & { flattenMaps: false }): Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>;
269+
toJSON(options: ToObjectOptions & { flattenMaps: false, flattenObjectIds: true }): ObjectIdToString<Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>>;
265270

266271
toJSON<T = Default__v<Require_id<DocType>, TSchemaOptions>>(options?: ToObjectOptions & { flattenMaps?: true, flattenObjectIds?: false }): FlattenMaps<T>;
267272
toJSON<T = Default__v<Require_id<DocType>, TSchemaOptions>>(options: ToObjectOptions & { flattenObjectIds: false }): FlattenMaps<T>;
@@ -270,9 +275,15 @@ declare module 'mongoose' {
270275
toJSON<T = Default__v<Require_id<DocType>, TSchemaOptions>>(options: ToObjectOptions & { flattenMaps: false, flattenObjectIds: true }): ObjectIdToString<T>;
271276

272277
/** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */
273-
toObject(options: ToObjectOptions & { virtuals: true }): Default__v<Require_id<DocType & TVirtuals>, TSchemaOptions>;
274-
toObject(options?: ToObjectOptions): Default__v<Require_id<DocType>, TSchemaOptions>;
275-
toObject<T>(options?: ToObjectOptions): Default__v<Require_id<T>, TSchemaOptions>;
278+
toObject(options: ToObjectOptions & { versionKey: false, virtuals: true, flattenObjectIds: true }): Omit<ObjectIdToString<Require_id<DocType & TVirtuals>>, '__v'>;
279+
toObject(options: ToObjectOptions & { virtuals: true, flattenObjectIds: true }): ObjectIdToString<Default__v<Require_id<DocType & TVirtuals>, ResolveSchemaOptions<TSchemaOptions>>>;
280+
toObject(options: ToObjectOptions & { versionKey: false, flattenObjectIds: true }): Omit<ObjectIdToString<Require_id<DocType & TVirtuals>>, '__v'>;
281+
toObject(options: ToObjectOptions & { versionKey: false, virtuals: true }): Omit<Require_id<DocType & TVirtuals>, '__v'>;
282+
toObject(options: ToObjectOptions & { virtuals: true }): Default__v<Require_id<DocType & TVirtuals>, ResolveSchemaOptions<TSchemaOptions>>;
283+
toObject(options: ToObjectOptions & { versionKey: false }): Omit<Require_id<DocType & TVirtuals>, '__v'>;
284+
toObject(options: ToObjectOptions & { flattenObjectIds: true }): ObjectIdToString<Default__v<Require_id<DocType & TVirtuals>, ResolveSchemaOptions<TSchemaOptions>>>;
285+
toObject(options?: ToObjectOptions): Default__v<Require_id<DocType>, ResolveSchemaOptions<TSchemaOptions>>;
286+
toObject<T>(options?: ToObjectOptions): Default__v<Require_id<T>, ResolveSchemaOptions<TSchemaOptions>>;
276287

277288
/** Clears the modified state on the specified path. */
278289
unmarkModified<T extends keyof DocType>(path: T): void;

types/index.d.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,21 @@ declare module 'mongoose' {
140140
? IfAny<U, T & { _id: Types.ObjectId }, T & Required<{ _id: U }>>
141141
: T & { _id: Types.ObjectId };
142142

143-
export type Default__v<T, TSchemaOptions = {}> = TSchemaOptions extends { versionKey: false } ? T : T extends { __v?: infer U }
143+
export type Default__v<T, TSchemaOptions = {}> = TSchemaOptions extends { versionKey: false }
144144
? T
145-
: T & { __v: number };
145+
: TSchemaOptions extends { versionKey: infer VK }
146+
? (
147+
// If VK is a *literal* string, add that property
148+
T & {
149+
[K in VK as K extends string
150+
? (string extends K ? never : K) // drop if wide string
151+
: never
152+
]: number
153+
}
154+
)
155+
: T extends { __v?: infer U }
156+
? T
157+
: T & { __v: number };
146158

147159
/** Helper type for getting the hydrated document type from the raw document type. The hydrated document type is what `new MyModel()` returns. */
148160
export type HydratedDocument<

0 commit comments

Comments
 (0)