Nest.js 中的猫鼬子文档

LBe*_*rus 23 schema reference subdocument nestjs

我正在将我的应用程序从 express.js 移动到 Nest.js,并且我找不到一种方法来引用另一个 mongoose Schema,而不使用使用 mongoose.Schema({...}) 声明 Schema 的旧方法。

让我们使用文档中的示例,以便我可以澄清我的问题:

@Schema()
  export class Cat extends Document {
  @Prop()
  name: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);
Run Code Online (Sandbox Code Playgroud)

现在,我想要的是这样的:

@Schema()
export class Owner extends Document {
  @Prop({type: [Cat], required: true})
  cats: Cat[];
}

export const OwnerSchema = SchemaFactory.createForClass(Owner);
Run Code Online (Sandbox Code Playgroud)

当我以这种方式定义模式时,我会收到一个错误,类似于:无效的模式配置:Cat不是数组中的有效类型cats

那么,使用这种更面向对象的方法来定义架构,在另一个架构中引用一个架构的正确方法是什么?

Edw*_*ony 45

我深入研究了源代码,了解了该SchemaFactory.createForClass方法如何转换 Schema 类。

那么它是如何工作的呢?

1. 看看下面这个例子:

@Schema()
export class Cat extends Document {
  @Prop()
  name: string;
}
export const catSchema = SchemaFactory.createForClass(Cat);
Run Code Online (Sandbox Code Playgroud)

基本上,当你做 SchemaFactory.createForClass(Cat)

Nest 会将 class 语法转换为 Mongoose schema 语法,所以最终转换的结果是这样的:

const schema = new mongoose.Schema({
    name: { type: String } // Notice that `String` is now uppercase.
});
Run Code Online (Sandbox Code Playgroud)

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,
    });
  };
}
Run Code Online (Sandbox Code Playgroud)

在这里你可以看到Prop()装饰器在幕后做了什么。当你这样做时:

@Prop()
name: string;
Run Code Online (Sandbox Code Playgroud)

Prop 函数将被调用,在这种情况下没有参数。

const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
Run Code Online (Sandbox Code Playgroud)

使用ReflectAPI,我们可以获得您在执行时使用的数据类型name: stringtype变量的值现在设置为String。请注意,它不是stringReflectAPI 将始终返回数据类型的构造函数版本,因此:

  • number 将被序列化为 Number
  • string 将被序列化为 String
  • boolean 将被序列化为 Boolean
  • 等等

TypeMetadataStorage.addPropertyMetadata 然后将下面的对象存储到商店中。

{
    target: User,
    propertyKey: ‘name’,
    options: { type: String }
}
Run Code Online (Sandbox Code Playgroud)

我们来看看: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);
  }
}
Run Code Online (Sandbox Code Playgroud)

所以基本上这个对象将被存储到properties变量TypeMetadataStorageHostTypeMetadataStorageHost是一个将存储大量这些对象的单例。

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,
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

最重要的部分是: const schemaDefinition = DefinitionsFactory.createForClass(target);。请注意,这里的目标是您的Cat班级。

你可以在这里看到方法定义: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;
}
Run Code Online (Sandbox Code Playgroud)

schemaMetadata.properties包含您在执行时存储的对象TypeMetadataStorage.addPropertyMetadata

[
    {
        target: User,
        propertyKey: ‘name’,
        options: { type: String }
    }
]
Run Code Online (Sandbox Code Playgroud)

forEach会产生:

{
    name: { type: String }
}
Run Code Online (Sandbox Code Playgroud)

最后,它将被用作master · nestjs/mongoose · GitHub 上mongoose.Schema构造函数mongoose/schema.factory.ts的参数:

return new mongoose.Schema(
    schemaDefinition,
    schemaMetadata && schemaMetadata.options,
);
Run Code Online (Sandbox Code Playgroud)

4.所以回答这个问题:

你应该把什么作为 Prop()论点?

还记得 Nest 是在什么时候forEach生成 Mongoose Schema 的吗?

schemaMetadata.properties?.forEach((item) => {
  const options = this.inspectTypeDefinition(item.options as any);
  schemaDefinition = {
    [item.propertyKey]: options as any,
    …schemaDefinition,
  };
});
Run Code Online (Sandbox Code Playgroud)

要获得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;
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 如果optionsfunction诸如String或一个SchemaType将被直接返回,并用作猫鼬选项。
  2. 如果optionsArray,它将返回该数组的第一个索引并将其包装在一个数组中。
  3. 如果options不是 anArrayfunction,例如,如果它只是一个普通的,object例如{ type: String, required: true },它将直接返回并用作 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);
Run Code Online (Sandbox Code Playgroud)

至于如何添加从基准OwnerCat,我们可以这样做:

@Prop([{ type: MongooseSchema.Types.ObjectId, ref: Cat.name }])
Run Code Online (Sandbox Code Playgroud)

更新

回答评论部分中的问题:

如何在另一个模式中嵌入模式?

如果你正确阅读了答案,你应该有足够的知识来做到这一点。但如果你没有,这里是 TLDR 答案。

请注意,我强烈建议您在去这里之前阅读整个答案。

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

Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

  • 奇迹般有效!很好的解释,非常感谢! (2认同)
  • @Sinandro 问题是“在另一个模式中引用一个模式”而不是“在另一个模式中嵌入模式”。你的问题是另一个问题。请看一下“inspectTypeDefinition”方法中的“else if (typeof options.type === 'function')”。这就是你想要的答案。 (2认同)