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。请注意,它不是 stringReflect 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;
}

在这里你可以得出这样的结论:

  1. 如果 optionsfunction,例如 StringSchemaType,它将被直接 return 编辑并用作 Mongoose 选项。
  2. 如果 options 是一个 Array,它将 return 该数组的第一个索引并将其包装在一个数组中。
  3. 如果 options 不是 Arrayfunction,例如,如果它只是一个普通的 object,例如 { type: String, required: true },它将直接 returned 并用作 Mongoose 选项。

回答

所以要添加从 CatOwner 的引用,您可以这样做:

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);

至于如何添加从OwnerCat的引用,我们可以这样做:

@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);