Nest.js 中的 Mongoose 子文档
Mongoose Subdocuments in Nest.js
我正在将我的应用程序从 express.js 移动到 Nest.js,但我找不到在另一个中引用一个 mongoose Schema 的方法,而不使用使用 [= 声明 Schema 的旧方法29=]({...}).
让我们使用文档中的示例,这样我就可以澄清我的问题:
@Schema()
export class Cat extends Document {
@Prop()
name: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
现在,我想要的是这样的:
@Schema()
export class Owner extends Document {
@Prop({type: [Cat], required: true})
cats: Cat[];
}
export const OwnerSchema = SchemaFactory.createForClass(Owner);
当我以这种方式定义模式时,我会得到一个错误,类似这样:无效的模式配置:Cat
不是有效的
在数组 cats
中键入
那么,使用这种更面向对象的方法定义架构,在另一个架构中引用一个架构的正确方法是什么?
我深入研究了源代码,了解了 Schema class 是如何通过 SchemaFactory.createForClass
方法转换的。
那么它是如何工作的?
1。看看下面这个例子:
@Schema()
export class Cat extends Document {
@Prop()
name: string;
}
export const catSchema = SchemaFactory.createForClass(Cat);
基本上,当你做 SchemaFactory.createForClass(Cat)
Nest会将class语法转换成Mongoose schema语法,所以最终转换的结果是这样的:
const schema = new mongoose.Schema({
name: { type: String } // Notice that `String` is now uppercase.
});
2。转换是如何进行的?
看看这个文件:mongoose/prop.decorator.ts at master · nestjs/mongoose · GitHub
export function Prop(options?: PropOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
options = (options || {}) as mongoose.SchemaTypeOpts<unknown>;
const isRawDefinition = options[RAW_OBJECT_DEFINITION];
if (!options.type && !Array.isArray(options) && !isRawDefinition) {
const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
if (type === Array) {
options.type = [];
} else if (type && type !== Object) {
options.type = type;
}
}
TypeMetadataStorage.addPropertyMetadata({
target: target.constructor,
propertyKey: propertyKey as string,
options,
});
};
}
在这里您可以看到 Prop()
装饰器在幕后做了什么。
当你这样做时:
@Prop()
name: string;
Prop
函数将被调用,在这种情况下没有参数。
const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
使用Reflect
API,我们可以得到你在name: string
时使用的数据类型。 type
变量的值现在设置为 String
。请注意,它不是 string
,Reflect
API 将始终 return 数据类型的构造函数版本,因此:
number
将被序列化为 Number
string
将被序列化为 String
boolean
将被序列化为 Boolean
- 等等
TypeMetadataStorage.addPropertyMetadata
然后将下面的对象存储到存储中。
{
target: User,
propertyKey: ‘name’,
options: { type: String }
}
我们来看看:mongoose/type-metadata.storage.ts at master · nestjs/mongoose · GitHub
export class TypeMetadataStorageHost {
private schemas = new Array<SchemaMetadata>();
private properties = new Array<PropertyMetadata>();
addPropertyMetadata(metadata: PropertyMetadata) {
this.properties.push(metadata);
}
}
所以基本上该对象将存储到 TypeMetadataStorageHost
中的 properties
变量中。
TypeMetadataStorageHost
是一个将存储大量此类对象的单例。
3。架构生成
要了解 SchemaFactory.createForClass(Cat)
如何生成 Mongoose 模式,请看一下:mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub
export class SchemaFactory {
static createForClass(target: Type<unknown>) {
const schemaDefinition = DefinitionsFactory.createForClass(target);
const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(
target,
);
return new mongoose.Schema(
schemaDefinition,
schemaMetadata && schemaMetadata.options,
);
}
}
最重要的部分是:
const schemaDefinition = DefinitionsFactory.createForClass(target);
。请注意,此处的目标是您的 Cat
class.
您可以在此处查看方法定义:mongoose/definitions.factory.ts at master · nestjs/mongoose · GitHub
export class DefinitionsFactory {
static createForClass(target: Type<unknown>): mongoose.SchemaDefinition {
let schemaDefinition: mongoose.SchemaDefinition = {};
schemaMetadata.properties?.forEach((item) => {
const options = this.inspectTypeDefinition(item.options as any);
schemaDefinition = {
[item.propertyKey]: options as any,
…schemaDefinition,
};
});
return schemaDefinition;
}
schemaMetadata.properties
包含您在 TypeMetadataStorage.addPropertyMetadata
:
时存储的对象
[
{
target: User,
propertyKey: ‘name’,
options: { type: String }
}
]
forEach
将产生:
{
name: { type: String }
}
最后会作为mongoose.Schema
构造函数的参数mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub:
return new mongoose.Schema(
schemaDefinition,
schemaMetadata && schemaMetadata.options,
);
4。所以回答问题:
你应该把什么作为 Prop()
参数?
还记得 Nest 何时 forEach
生成猫鼬模式吗?
schemaMetadata.properties?.forEach((item) => {
const options = this.inspectTypeDefinition(item.options as any);
schemaDefinition = {
[item.propertyKey]: options as any,
…schemaDefinition,
};
});
要获得 options
它使用 inspectTypeDefinition
方法。你可以看到下面的定义:
private static inspectTypeDefinition(options: mongoose.SchemaTypeOpts<unknown> | Function): PropOptions {
if (typeof options === 'function') {
if (this.isPrimitive(options)) {
return options;
} else if (this.isMongooseSchemaType(options)) {
return options;
}
return this.createForClass(options as Type<unknown>);
} else if (typeof options.type === 'function') {
options.type = this.inspectTypeDefinition(options.type);
return options;
} else if (Array.isArray(options)) {
return options.length > 0
? [this.inspectTypeDefinition(options[0])]
: options;
}
return options;
}
在这里你可以得出这样的结论:
- 如果
options
是 function
,例如 String
或 SchemaType
,它将被直接 return 编辑并用作 Mongoose 选项。
- 如果
options
是一个 Array
,它将 return 该数组的第一个索引并将其包装在一个数组中。
- 如果
options
不是 Array
或 function
,例如,如果它只是一个普通的 object
,例如 { type: String, required: true }
,它将直接 returned 并用作 Mongoose 选项。
回答
所以要添加从 Cat
到 Owner
的引用,您可以这样做:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Owner } from './owner.schema.ts';
@Schema()
export class Cat extends Document {
@Prop()
name: string;
@Prop({ type: MongooseSchema.Types.ObjectId, ref: Owner.name })
owner: Owner;
}
export const catSchema = SchemaFactory.createForClass(Cat);
至于如何添加从Owner
到Cat
的引用,我们可以这样做:
@Prop([{ type: MongooseSchema.Types.ObjectId, ref: Cat.name }])
更新
在评论区回答以下问题:
如何将模式嵌入到另一个模式中?
如果你正确阅读答案,你应该有足够的知识来做到这一点。但如果你没有,这里就是 TLDR 答案。
请注意,我强烈建议您在转到此处之前阅读整个答案。
图片-variant.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class ImageVariant {
@Prop()
url: string;
@Prop()
width: number;
@Prop()
height: number;
@Prop()
size: number;
}
export const imageVariantSchema = SchemaFactory.createForClass(ImageVariant);
image.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { imageVariantSchema, ImageVariant } from './imagevariant.schema';
@Schema()
export class Image extends Document {
@Prop({ type: imageVariantSchema })
large: ImageVariant;
@Prop({ type: imageVariantSchema })
medium: ImageVariant;
@Prop({ type: imageVariantSchema })
small: ImageVariant;
}
export const imageSchema = SchemaFactory.createForClass(Image);
import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Education } from '../../education/schemas';
import { RECORD_STATUS } from '../../common/common.constants';
import { Employment } from '../../employment/schemas';
import {
JOB_SEARCH_STATUS,
LANGUAGE_PROFICIENCY
} from '../user-profile.constants';
const externalLinks = {
linkedInUrl: { type: String },
githubUrl: { type: String },
twitterUrl: { type: String },
blogUrl: { type: String },
websiteUrl: { type: String },
WhosebugUrl: { type: String }
};
const address = {
line1: { type: String, required: true },
line2: { type: String },
zipCode: { type: String },
cityId: { type: Number },
countryId: { type: Number }
};
const language = {
name: { type: String, require: true },
code: { type: String, required: true },
proficiency: { type: String, required: true, enum: LANGUAGE_PROFICIENCY }
};
const options = {
timestamps: true,
};
export type UserProfileDocument = UserProfile & mongoose.Document;
@Schema(options)
export class UserProfile {
_id: string;
@Prop()
firstName: string;
@Prop()
lastName: string;
@Prop()
headline: string;
@Prop({
unique: true,
trim: true,
lowercase: true
})
email: string;
@Prop()
phoneNumber: string
@Prop(raw({
jobSearchStatus: { type: String, enum: JOB_SEARCH_STATUS, required: true }
}))
jobPreferences: Record<string, any>;
@Prop(raw(externalLinks))
externalLinks: Record<string, any>;
@Prop([String])
skills: string[];
@Prop(raw({ type: address, required: false }))
address: Record<string, any>;
@Prop()
birthDate: Date;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Employment' }] })
employments: Employment[];
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Education' }] })
educations: Education[];
@Prop(raw([language]))
languages: Record<string, any>[];
@Prop()
timeZone: string;
@Prop()
createdAt: Date;
@Prop()
updatedAt: Date;
@Prop({
enum: RECORD_STATUS,
required: true,
default: RECORD_STATUS.Active
})
recordStatus: string;
}
export const UserProfileSchema = SchemaFactory.createForClass(UserProfile);
为子文档创建 SchemaFactory.createForClass
并在文档中引用其类型。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class SubDocument {
@Prop()
name: string;
@Prop()
description: number;
}
const subDocumentSchema = SchemaFactory.createForClass(SubDocument);
@Schema()
export class Document {
@Prop()
name: string;
@Prop({ type: subDocumentSchema })
subDocument: SubDocument;
}
export const documentSchema = SchemaFactory.createForClass(Document);
我正在将我的应用程序从 express.js 移动到 Nest.js,但我找不到在另一个中引用一个 mongoose Schema 的方法,而不使用使用 [= 声明 Schema 的旧方法29=]({...}).
让我们使用文档中的示例,这样我就可以澄清我的问题:
@Schema()
export class Cat extends Document {
@Prop()
name: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
现在,我想要的是这样的:
@Schema()
export class Owner extends Document {
@Prop({type: [Cat], required: true})
cats: Cat[];
}
export const OwnerSchema = SchemaFactory.createForClass(Owner);
当我以这种方式定义模式时,我会得到一个错误,类似这样:无效的模式配置:Cat
不是有效的
在数组 cats
那么,使用这种更面向对象的方法定义架构,在另一个架构中引用一个架构的正确方法是什么?
我深入研究了源代码,了解了 Schema class 是如何通过 SchemaFactory.createForClass
方法转换的。
那么它是如何工作的?
1。看看下面这个例子:
@Schema()
export class Cat extends Document {
@Prop()
name: string;
}
export const catSchema = SchemaFactory.createForClass(Cat);
基本上,当你做 SchemaFactory.createForClass(Cat)
Nest会将class语法转换成Mongoose schema语法,所以最终转换的结果是这样的:
const schema = new mongoose.Schema({
name: { type: String } // Notice that `String` is now uppercase.
});
2。转换是如何进行的?
看看这个文件:mongoose/prop.decorator.ts at master · nestjs/mongoose · GitHub
export function Prop(options?: PropOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
options = (options || {}) as mongoose.SchemaTypeOpts<unknown>;
const isRawDefinition = options[RAW_OBJECT_DEFINITION];
if (!options.type && !Array.isArray(options) && !isRawDefinition) {
const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
if (type === Array) {
options.type = [];
} else if (type && type !== Object) {
options.type = type;
}
}
TypeMetadataStorage.addPropertyMetadata({
target: target.constructor,
propertyKey: propertyKey as string,
options,
});
};
}
在这里您可以看到 Prop()
装饰器在幕后做了什么。
当你这样做时:
@Prop()
name: string;
Prop
函数将被调用,在这种情况下没有参数。
const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
使用Reflect
API,我们可以得到你在name: string
时使用的数据类型。 type
变量的值现在设置为 String
。请注意,它不是 string
,Reflect
API 将始终 return 数据类型的构造函数版本,因此:
number
将被序列化为Number
string
将被序列化为String
boolean
将被序列化为Boolean
- 等等
TypeMetadataStorage.addPropertyMetadata
然后将下面的对象存储到存储中。
{
target: User,
propertyKey: ‘name’,
options: { type: String }
}
我们来看看:mongoose/type-metadata.storage.ts at master · nestjs/mongoose · GitHub
export class TypeMetadataStorageHost {
private schemas = new Array<SchemaMetadata>();
private properties = new Array<PropertyMetadata>();
addPropertyMetadata(metadata: PropertyMetadata) {
this.properties.push(metadata);
}
}
所以基本上该对象将存储到 TypeMetadataStorageHost
中的 properties
变量中。
TypeMetadataStorageHost
是一个将存储大量此类对象的单例。
3。架构生成
要了解 SchemaFactory.createForClass(Cat)
如何生成 Mongoose 模式,请看一下:mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub
export class SchemaFactory {
static createForClass(target: Type<unknown>) {
const schemaDefinition = DefinitionsFactory.createForClass(target);
const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(
target,
);
return new mongoose.Schema(
schemaDefinition,
schemaMetadata && schemaMetadata.options,
);
}
}
最重要的部分是:
const schemaDefinition = DefinitionsFactory.createForClass(target);
。请注意,此处的目标是您的 Cat
class.
您可以在此处查看方法定义:mongoose/definitions.factory.ts at master · nestjs/mongoose · GitHub
export class DefinitionsFactory {
static createForClass(target: Type<unknown>): mongoose.SchemaDefinition {
let schemaDefinition: mongoose.SchemaDefinition = {};
schemaMetadata.properties?.forEach((item) => {
const options = this.inspectTypeDefinition(item.options as any);
schemaDefinition = {
[item.propertyKey]: options as any,
…schemaDefinition,
};
});
return schemaDefinition;
}
schemaMetadata.properties
包含您在 TypeMetadataStorage.addPropertyMetadata
:
[
{
target: User,
propertyKey: ‘name’,
options: { type: String }
}
]
forEach
将产生:
{
name: { type: String }
}
最后会作为mongoose.Schema
构造函数的参数mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub:
return new mongoose.Schema(
schemaDefinition,
schemaMetadata && schemaMetadata.options,
);
4。所以回答问题:
你应该把什么作为 Prop()
参数?
还记得 Nest 何时 forEach
生成猫鼬模式吗?
schemaMetadata.properties?.forEach((item) => {
const options = this.inspectTypeDefinition(item.options as any);
schemaDefinition = {
[item.propertyKey]: options as any,
…schemaDefinition,
};
});
要获得 options
它使用 inspectTypeDefinition
方法。你可以看到下面的定义:
private static inspectTypeDefinition(options: mongoose.SchemaTypeOpts<unknown> | Function): PropOptions {
if (typeof options === 'function') {
if (this.isPrimitive(options)) {
return options;
} else if (this.isMongooseSchemaType(options)) {
return options;
}
return this.createForClass(options as Type<unknown>);
} else if (typeof options.type === 'function') {
options.type = this.inspectTypeDefinition(options.type);
return options;
} else if (Array.isArray(options)) {
return options.length > 0
? [this.inspectTypeDefinition(options[0])]
: options;
}
return options;
}
在这里你可以得出这样的结论:
- 如果
options
是function
,例如String
或SchemaType
,它将被直接 return 编辑并用作 Mongoose 选项。 - 如果
options
是一个Array
,它将 return 该数组的第一个索引并将其包装在一个数组中。 - 如果
options
不是Array
或function
,例如,如果它只是一个普通的object
,例如{ type: String, required: true }
,它将直接 returned 并用作 Mongoose 选项。
回答
所以要添加从 Cat
到 Owner
的引用,您可以这样做:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Owner } from './owner.schema.ts';
@Schema()
export class Cat extends Document {
@Prop()
name: string;
@Prop({ type: MongooseSchema.Types.ObjectId, ref: Owner.name })
owner: Owner;
}
export const catSchema = SchemaFactory.createForClass(Cat);
至于如何添加从Owner
到Cat
的引用,我们可以这样做:
@Prop([{ type: MongooseSchema.Types.ObjectId, ref: Cat.name }])
更新
在评论区回答以下问题:
如何将模式嵌入到另一个模式中?
如果你正确阅读答案,你应该有足够的知识来做到这一点。但如果你没有,这里就是 TLDR 答案。
请注意,我强烈建议您在转到此处之前阅读整个答案。
图片-variant.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class ImageVariant {
@Prop()
url: string;
@Prop()
width: number;
@Prop()
height: number;
@Prop()
size: number;
}
export const imageVariantSchema = SchemaFactory.createForClass(ImageVariant);
image.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { imageVariantSchema, ImageVariant } from './imagevariant.schema';
@Schema()
export class Image extends Document {
@Prop({ type: imageVariantSchema })
large: ImageVariant;
@Prop({ type: imageVariantSchema })
medium: ImageVariant;
@Prop({ type: imageVariantSchema })
small: ImageVariant;
}
export const imageSchema = SchemaFactory.createForClass(Image);
import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Education } from '../../education/schemas';
import { RECORD_STATUS } from '../../common/common.constants';
import { Employment } from '../../employment/schemas';
import {
JOB_SEARCH_STATUS,
LANGUAGE_PROFICIENCY
} from '../user-profile.constants';
const externalLinks = {
linkedInUrl: { type: String },
githubUrl: { type: String },
twitterUrl: { type: String },
blogUrl: { type: String },
websiteUrl: { type: String },
WhosebugUrl: { type: String }
};
const address = {
line1: { type: String, required: true },
line2: { type: String },
zipCode: { type: String },
cityId: { type: Number },
countryId: { type: Number }
};
const language = {
name: { type: String, require: true },
code: { type: String, required: true },
proficiency: { type: String, required: true, enum: LANGUAGE_PROFICIENCY }
};
const options = {
timestamps: true,
};
export type UserProfileDocument = UserProfile & mongoose.Document;
@Schema(options)
export class UserProfile {
_id: string;
@Prop()
firstName: string;
@Prop()
lastName: string;
@Prop()
headline: string;
@Prop({
unique: true,
trim: true,
lowercase: true
})
email: string;
@Prop()
phoneNumber: string
@Prop(raw({
jobSearchStatus: { type: String, enum: JOB_SEARCH_STATUS, required: true }
}))
jobPreferences: Record<string, any>;
@Prop(raw(externalLinks))
externalLinks: Record<string, any>;
@Prop([String])
skills: string[];
@Prop(raw({ type: address, required: false }))
address: Record<string, any>;
@Prop()
birthDate: Date;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Employment' }] })
employments: Employment[];
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Education' }] })
educations: Education[];
@Prop(raw([language]))
languages: Record<string, any>[];
@Prop()
timeZone: string;
@Prop()
createdAt: Date;
@Prop()
updatedAt: Date;
@Prop({
enum: RECORD_STATUS,
required: true,
default: RECORD_STATUS.Active
})
recordStatus: string;
}
export const UserProfileSchema = SchemaFactory.createForClass(UserProfile);
为子文档创建 SchemaFactory.createForClass
并在文档中引用其类型。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class SubDocument {
@Prop()
name: string;
@Prop()
description: number;
}
const subDocumentSchema = SchemaFactory.createForClass(SubDocument);
@Schema()
export class Document {
@Prop()
name: string;
@Prop({ type: subDocumentSchema })
subDocument: SubDocument;
}
export const documentSchema = SchemaFactory.createForClass(Document);