Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types: add JSONSerialized helper that can convert HydratedDocument to JSON output type #14981

Merged
merged 3 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/types/check-types-filename.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const checkFolder = (folder) => {
}
continue;
} else {
console.error('File ' + entry + ' is not having a valid file-extension.\n');
console.error('File ' + entry + ' does not have a valid extension, must be .d.ts or .gitignore.\n');
process.exit(1);
}
}
Expand Down
33 changes: 33 additions & 0 deletions test/types/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
InferRawDocType,
InferSchemaType,
InsertManyOptions,
JSONSerialized,
ObtainDocumentType,
ObtainSchemaGeneric,
ResolveSchemaOptions,
Expand Down Expand Up @@ -1681,3 +1682,35 @@ async function gh14902() {
expectType<Binary | null | undefined>(doc.image);
expectType<Binary | null | undefined>(doc.subdoc!.testBuf);
}

async function gh14451() {
const exampleSchema = new Schema({
myId: { type: 'ObjectId' },
myRequiredId: { type: 'ObjectId', required: true },
myBuf: { type: Buffer, required: true },
subdoc: {
type: new Schema({
subdocProp: Date
})
},
docArr: [{ nums: [Number], times: [{ type: Date }] }],
myMap: {
type: Map,
of: String
}
});

const Test = model('Test', exampleSchema);

type TestJSON = JSONSerialized<InferSchemaType<typeof exampleSchema>>;
expectType<{
myId?: string | undefined | null,
myRequiredId: string,
myBuf: { type: 'buffer', data: number[] },
subdoc?: {
subdocProp?: string | undefined | null
} | null,
docArr: { nums: number[], times: string[] }[],
myMap?: Record<string, string> | null | undefined
}>({} as TestJSON);
}
73 changes: 73 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,9 @@ declare module 'mongoose' {
[K in keyof T]: FlattenProperty<T[K]>;
};

/**
* Converts any Buffer properties into mongodb.Binary instances, which is what `lean()` returns
*/
export type BufferToBinary<T> = T extends TreatAsPrimitives ? T : T extends Record<string, any> ? {
[K in keyof T]: T[K] extends Buffer
? mongodb.Binary
Expand All @@ -718,6 +721,76 @@ declare module 'mongoose' {
: BufferToBinary<T[K]>;
} : T;

/**
* Converts any Buffer properties into { type: 'buffer', data: [1, 2, 3] } format for JSON serialization
*/
export type BufferToJSON<T> = T extends TreatAsPrimitives ? T : T extends Record<string, any> ? {
[K in keyof T]: T[K] extends Buffer
? { type: 'buffer', data: number[] }
: T[K] extends (Buffer | null | undefined)
? { type: 'buffer', data: number[] } | null | undefined
: T[K] extends Types.DocumentArray<infer ItemType>
? Types.DocumentArray<BufferToBinary<ItemType>>
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? HydratedSingleSubdocument<SubdocType>
: BufferToBinary<T[K]>;
} : T;

/**
* Converts any ObjectId properties into strings for JSON serialization
*/
export type ObjectIdToString<T> = T extends TreatAsPrimitives ? T : T extends Record<string, any> ? {
[K in keyof T]: T[K] extends mongodb.ObjectId
? string
: T[K] extends (mongodb.ObjectId | null | undefined)
? string | null | undefined
: T[K] extends Types.DocumentArray<infer ItemType>
? Types.DocumentArray<ObjectIdToString<ItemType>>
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? HydratedSingleSubdocument<ObjectIdToString<SubdocType>>
: ObjectIdToString<T[K]>;
} : T;

/**
* Converts any Date properties into strings for JSON serialization
*/
export type DateToString<T> = T extends TreatAsPrimitives ? T : T extends Record<string, any> ? {
[K in keyof T]: T[K] extends NativeDate
? string
: T[K] extends (NativeDate | null | undefined)
? string | null | undefined
: T[K] extends Types.DocumentArray<infer ItemType>
? Types.DocumentArray<DateToString<ItemType>>
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? HydratedSingleSubdocument<DateToString<SubdocType>>
: DateToString<T[K]>;
} : T;

/**
* Converts any Mongoose subdocuments (single nested or doc arrays) into POJO equivalents
*/
export type SubdocsToPOJOs<T> = T extends TreatAsPrimitives ? T : T extends Record<string, any> ? {
[K in keyof T]: T[K] extends NativeDate
? string
: T[K] extends (NativeDate | null | undefined)
? string | null | undefined
: T[K] extends Types.DocumentArray<infer ItemType>
? ItemType[]
: T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
? SubdocType
: SubdocsToPOJOs<T[K]>;
} : T;

export type JSONSerialized<T> = SubdocsToPOJOs<
vkarpov15 marked this conversation as resolved.
Show resolved Hide resolved
FlattenMaps<
BufferToJSON<
ObjectIdToString<
DateToString<T>
>
>
>
>;

/**
* Separate type is needed for properties of union type (for example, Types.DocumentArray | undefined) to apply conditional check to each member of it
* https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
Expand Down