Mangosta a la manera del mecanografiado ...?


90

Intentando implementar un modelo de Mongoose en Typescript. El rastreo de Google ha revelado solo un enfoque híbrido (que combina JS y TS). ¿Cómo se implementaría la clase User, en mi enfoque bastante ingenuo, sin el JS?

Quiere poder IUserModel sin el equipaje.

import {IUser} from './user.ts';
import {Document, Schema, Model} from 'mongoose';

// mixing in a couple of interfaces
interface IUserDocument extends IUser,  Document {}

// mongoose, why oh why '[String]' 
// TODO: investigate out why mongoose needs its own data types
let userSchema: Schema = new Schema({
  userName  : String,
  password  : String,
  firstName : String,
  lastName  : String,
  email     : String,
  activated : Boolean,
  roles     : [String]
});

// interface we want to code to?
export interface IUserModel extends Model<IUserDocument> {/* any custom methods here */}

// stumped here
export class User {
  constructor() {}
}

Userno puede ser una clase porque crear una es una operación asincrónica. Tiene que devolver una promesa, así que tienes que llamar User.create({...}).then....
Louay Alakkad

1
Específicamente, dado en el código del OP, ¿podría explicar por qué Userno puede ser una clase?
Tim McNamara


@Erich dicen que typeorm no funciona bien con MongoDB, tal vez Type goose es una buena opción
PayamBeirami

Respuestas:


130

Así es como lo hago:

export interface IUser extends mongoose.Document {
  name: string; 
  somethingElse?: number; 
};

export const UserSchema = new mongoose.Schema({
  name: {type:String, required: true},
  somethingElse: Number,
});

const User = mongoose.model<IUser>('User', UserSchema);
export default User;

2
lo siento, pero ¿cómo se define 'mangosta' en TS?
Tim McNamara

13
import * as mongoose from 'mongoose';oimport mongoose = require('mongoose');
Louay Alakkad

1
Algo como esto:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
La última línea (exportar usuario constante predeterminado ...) no funciona para mí. Necesito dividir la línea, como se propone en stackoverflow.com/questions/35821614/…
Sergio

7
Puedo hacerlo let newUser = new User({ iAmNotHere: true })sin errores en el IDE o en la compilación. Entonces, ¿cuál es la razón para crear una interfaz?
Lupurus

33

Otra alternativa si desea separar sus definiciones de tipo y la implementación de la base de datos.

import {IUser} from './user.ts';
import * as mongoose from 'mongoose';

type UserType = IUser & mongoose.Document;
const User = mongoose.model<UserType>('User', new mongoose.Schema({
    userName  : String,
    password  : String,
    /* etc */
}));

Inspiración desde aquí: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
¿La mongoose.Schemadefinición aquí duplica los campos de IUser? Dado que IUserestá definido en un archivo diferente, el riesgo de que los campos se desincronicen a medida que el proyecto crece en complejidad y en número de desarrolladores, es bastante alto.
Dan Dascalescu

Sí, este es un argumento válido que vale la pena considerar. Sin embargo, el uso de pruebas de integración de componentes puede ayudar a reducir los riesgos. Y tenga en cuenta que hay enfoques y arquitecturas en las que las declaraciones de tipo y las implementaciones de la base de datos se separan, ya sea a través de un ORM (como propuso) o manualmente (como en esta respuesta). No hay una bala de plata que hay ... <(°. °)>
Gábor Imre

Una viñeta podría ser generar código a partir de la definición de GraphQL, para TypeScript y mangosta.
Dan Dascalescu

23

Perdón por la necropublicación, pero esto puede ser interesante para alguien. Creo que Typegoose proporciona una forma más moderna y elegante de definir modelos.

Aquí hay un ejemplo de los documentos:

import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';
import * as mongoose from 'mongoose';

mongoose.connect('mongodb://localhost:27017/test');

class User extends Typegoose {
    @prop()
    name?: string;
}

const UserModel = new User().getModelForClass(User);

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();

Para un escenario de conexión existente, puede usar lo siguiente (que puede ser más probable en situaciones reales y descubierto en los documentos):

import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';
import * as mongoose from 'mongoose';

const conn = mongoose.createConnection('mongodb://localhost:27017/test');

class User extends Typegoose {
    @prop()
    name?: string;
}

// Notice that the collection name will be 'users':
const UserModel = new User().getModelForClass(User, {existingConnection: conn});

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();

8
También llegué a esta conclusión, pero me preocupa que typegooseno tenga suficiente soporte ... verificando sus estadísticas de npm, son solo 3k descargas semanales y hay casi 100 problemas abiertos de Github, la mayoría de los cuales no tienen comentarios, y algunos de los cuales parece que deberían haberse cerrado hace mucho tiempo
Corbfon

@Corbfon ¿Lo probaste? Si es así, ¿cuáles fueron sus hallazgos? Si no es así, ¿hubo algo más que le hizo decidir no usarlo? En general, veo a algunas personas preocupadas por el soporte completo, pero parece que aquellos que realmente lo usan están bastante contentos con él
N4ppeL

1
@ N4ppeL No iría con typegoose: terminamos manejando manualmente nuestra escritura, similar a esta publicación , parece que ts-mongoosepodría tener alguna promesa (como se sugiere en la respuesta posterior)
Corbfon

1
Nunca te disculpes por "necropostar". [Como ya sabes ...] Hay incluso una insignia (aunque él se nombró Nigromante ; ^ D) para hacer precisamente esto! Se anima a necropostar nueva información e ideas.
Ruffin

1
@ruffin: Tampoco entiendo el estigma de publicar soluciones nuevas y actualizadas a los problemas.
Dan Dascalescu

16

Prueba ts-mongoose. Utiliza tipos condicionales para hacer el mapeo.

import { createSchema, Type, typedModel } from 'ts-mongoose';

const UserSchema = createSchema({
  username: Type.string(),
  email: Type.string(),
});

const User = typedModel('User', UserSchema);

1
¡Parece muy prometedor! ¡Gracias por compartir! :)
Boriel

1
Guau. Esto bloquea muy elegante. ¡Espero poder probarlo!
qqilihq

1
Divulgación: ts-mongoose parece ser creado por el cielo. Parece ser la solución más hábil que existe.
micrófono


11

La mayoría de las respuestas aquí repiten los campos en la clase / interfaz de TypeScript y en el esquema de mangosta. No tener una sola fuente de verdad representa un riesgo de mantenimiento, ya que el proyecto se vuelve más complejo y más desarrolladores trabajan en él: es más probable que los campos se desincronicen. . Esto es particularmente malo cuando la clase está en un archivo diferente al esquema de mangosta.

Para mantener los campos sincronizados, tiene sentido definirlos una vez. Hay algunas bibliotecas que hacen esto:

Ninguno de ellos me ha convencido todavía por completo, pero parece que typegoose se mantiene activamente y el desarrollador aceptó mis PR.

Para pensar un paso adelante: cuando agrega un esquema GraphQL a la mezcla, aparece otra capa de duplicación del modelo. Una forma de superar este problema podría ser generar código TypeScript y mongoose a partir del esquema GraphQL.


5

Aquí hay una forma fuerte de escribir un modelo sencillo con un esquema de mangosta. El compilador se asegurará de que las definiciones pasadas a mongoose.Schema coincidan con la interfaz. Una vez que tenga el esquema, puede usar

common.ts

export type IsRequired<T> =
  undefined extends T
  ? false
  : true;

export type FieldType<T> =
  T extends number ? typeof Number :
  T extends string ? typeof String :
  Object;

export type Field<T> = {
  type: FieldType<T>,
  required: IsRequired<T>,
  enum?: Array<T>
};

export type ModelDefinition<M> = {
  [P in keyof M]-?:
    M[P] extends Array<infer U> ? Array<Field<U>> :
    Field<M[P]>
};

user.ts

import * as mongoose from 'mongoose';
import { ModelDefinition } from "./common";

interface User {
  userName  : string,
  password  : string,
  firstName : string,
  lastName  : string,
  email     : string,
  activated : boolean,
  roles     : Array<string>
}

// The typings above expect the more verbose type definitions,
// but this has the benefit of being able to match required
// and optional fields with the corresponding definition.
// TBD: There may be a way to support both types.
const definition: ModelDefinition<User> = {
  userName  : { type: String, required: true },
  password  : { type: String, required: true },
  firstName : { type: String, required: true },
  lastName  : { type: String, required: true },
  email     : { type: String, required: true },
  activated : { type: Boolean, required: true },
  roles     : [ { type: String, required: true } ]
};

const schema = new mongoose.Schema(
  definition
);

Una vez que tenga su esquema, puede usar los métodos mencionados en otras respuestas, como

const userModel = mongoose.model<User & mongoose.Document>('User', schema);

1
Ésta es la única respuesta correcta. Ninguna de las otras respuestas aseguró realmente la compatibilidad de tipos entre el esquema y el tipo / interfaz.
Jamie Strauss

@JamieStrauss: ¿qué hay de no duplicar los campos en primer lugar ?
Dan Dascalescu

1
@DanDascalescu No creo que entiendas cómo funcionan los tipos.
Jamie Strauss

5

Simplemente agregue otra forma ( @types/mongoosedebe instalarse con npm install --save-dev @types/mongoose)

import { IUser } from './user.ts';
import * as mongoose from 'mongoose';

interface IUserModel extends IUser, mongoose.Document {}

const User = mongoose.model<IUserModel>('User', new mongoose.Schema({
    userName: String,
    password: String,
    // ...
}));

Y la diferencia entre interfacey type, lea esta respuesta

De esta manera tiene una ventaja, puede agregar tipos de métodos estáticos de Mongoose:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}

¿dónde se define generateJwt?
rels

1
@rels const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));básicamente, se generateJwtconvierte en otra propiedad del modelo.
a11smiles

¿Lo agregaría simplemente como un método de esta manera o lo conectaría a la propiedad de métodos?
user1790300

1
Esta debería ser la respuesta aceptada, ya que separa la definición de usuario y la DAL de usuario. Si desea cambiar de mongo a otro proveedor de base de datos, no tendrá que cambiar la interfaz de usuario.
Rafael del Rio

1
@RafaeldelRio: la pregunta era sobre el uso de mangosta con TypeScript. Cambiar a otra base de datos es antitético para este objetivo. Y el problema de separar la definición del esquema de la IUserdeclaración de interfaz en un archivo diferente es que el riesgo de que los campos se desincronicen a medida que el proyecto aumenta en complejidad y desarrolladores, es bastante alto.
Dan Dascalescu

4

Así es como lo hacen los chicos de Microsoft. aquí

import mongoose from "mongoose";

export type UserDocument = mongoose.Document & {
    email: string;
    password: string;
    passwordResetToken: string;
    passwordResetExpires: Date;
...
};

const userSchema = new mongoose.Schema({
    email: { type: String, unique: true },
    password: String,
    passwordResetToken: String,
    passwordResetExpires: Date,
...
}, { timestamps: true });

export const User = mongoose.model<UserDocument>("User", userSchema);

Recomiendo revisar este excelente proyecto de inicio cuando agregue TypeScript a su proyecto Node.

https://github.com/microsoft/TypeScript-Node-Starter


1
Eso duplica cada campo entre mangosta y TypeScript, lo que crea un riesgo de mantenimiento a medida que el modelo se vuelve más complejo. A las soluciones les gusta ts-mongoosey typegooseresuelven ese problema, aunque es cierto que con un poco de cruft sintáctico.
Dan Dascalescu

2

Con esto vscode intellisensefunciona en ambos

  • Tipo de usuario User.findOne
  • instancia de usuario u1._id

El código:

// imports
import { ObjectID } from 'mongodb'
import { Document, model, Schema, SchemaDefinition } from 'mongoose'

import { authSchema, IAuthSchema } from './userAuth'

// the model

export interface IUser {
  _id: ObjectID, // !WARNING: No default value in Schema
  auth: IAuthSchema
}

// IUser will act like it is a Schema, it is more common to use this
// For example you can use this type at passport.serialize
export type IUserSchema = IUser & SchemaDefinition
// IUser will act like it is a Document
export type IUserDocument = IUser & Document

export const userSchema = new Schema<IUserSchema>({
  auth: {
    required: true,
    type: authSchema,
  }
})

export default model<IUserDocument>('user', userSchema)


2

Aquí está el ejemplo de la documentación de Mongoose, Creación de clases de ES6 usando loadClass () , convertido a TypeScript:

import { Document, Schema, Model, model } from 'mongoose';
import * as assert from 'assert';

const schema = new Schema<IPerson>({ firstName: String, lastName: String });

export interface IPerson extends Document {
  firstName: string;
  lastName: string;
  fullName: string;
}

class PersonClass extends Model {
  firstName!: string;
  lastName!: string;

  // `fullName` becomes a virtual
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(v) {
    const firstSpace = v.indexOf(' ');
    this.firstName = v.split(' ')[0];
    this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1);
  }

  // `getFullName()` becomes a document method
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // `findByFullName()` becomes a static
  static findByFullName(name: string) {
    const firstSpace = name.indexOf(' ');
    const firstName = name.split(' ')[0];
    const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1);
    return this.findOne({ firstName, lastName });
  }
}

schema.loadClass(PersonClass);
const Person = model<IPerson>('Person', schema);

(async () => {
  let doc = await Person.create({ firstName: 'Jon', lastName: 'Snow' });
  assert.equal(doc.fullName, 'Jon Snow');
  doc.fullName = 'Jon Stark';
  assert.equal(doc.firstName, 'Jon');
  assert.equal(doc.lastName, 'Stark');

  doc = (<any>Person).findByFullName('Jon Snow');
  assert.equal(doc.fullName, 'Jon Snow');
})();

Para el findByFullNamemétodo estático , no pude imaginar cómo obtener la información de tipo Person, así que tuve que lanzar <any>Personcuando quería llamarlo. Si sabe cómo solucionarlo, agregue un comentario.


Como otras respuestas , este enfoque duplica los campos entre la interfaz y el esquema. Eso podría evitarse teniendo una única fuente de verdad, por ejemplo, utilizando ts-mongooseo typegoose. La situación se duplica aún más al definir el esquema GraphQL.
Dan Dascalescu

¿Alguna forma de definir refs con este enfoque?
Dan Dascalescu

1

Soy fanático de Plumier, tiene un ayudante de mangosta , pero se puede usar de forma independiente sin Plumier. . A diferencia de Typegoose, tomó un camino diferente al usar la biblioteca de reflexión dedicada de Plumier, que hace posible usar cosas frescas.

Caracteristicas

  1. POJO puro (el dominio no necesita heredar de ninguna clase, ni utilizar ningún tipo de datos especial), el modelo creado se infiere automáticamente, por T & Documentlo que es posible acceder a las propiedades relacionadas con el documento.
  2. Propiedades de parámetros de TypeScript admitidas, es bueno cuando tiene strict:true configuración tsconfig. Y con propiedades de parámetro no requiere decorador en todas las propiedades.
  3. Propiedades de campo admitidas como Typegoose
  4. La configuración es la misma que la de la mangosta, por lo que se familiarizará fácilmente con ella.
  5. Herencia admitida que hace que la programación sea más natural.
  6. Análisis del modelo, mostrando los nombres del modelo y su nombre de colección apropiado, configuración aplicada, etc.

Uso

import model, {collection} from "@plumier/mongoose"


@collection({ timestamps: true, toJson: { virtuals: true } })
class Domain {
    constructor(
        public createdAt?: Date,
        public updatedAt?: Date,
        @collection.property({ default: false })
        public deleted?: boolean
    ) { }
}

@collection()
class User extends Domain {
    constructor(
        @collection.property({ unique: true })
        public email: string,
        public password: string,
        public firstName: string,
        public lastName: string,
        public dateOfBirth: string,
        public gender: string
    ) { super() }
}

// create mongoose model (can be called multiple time)
const UserModel = model(User)
const user = await UserModel.findById()

1

Para cualquiera que busque una solución para proyectos existentes de Mongoose:

Recientemente creamos mongoose-tsgen para abordar este problema (¡me encantaría recibir comentarios!). Las soluciones existentes como typegoose requirieron reescribir todos nuestros esquemas e introdujeron varias incompatibilidades. mongoose-tsgen es una herramienta CLI simple que genera un archivo index.d.ts que contiene interfaces de Typecript para todos sus esquemas de Mongoose; requiere poca o ninguna configuración y se integra muy bien con cualquier proyecto de TypeScript.


0

A continuación, se muestra un ejemplo basado en el archivo README del @types/mongoosepaquete.

Además de los elementos ya incluidos arriba, muestra cómo incluir métodos regulares y estáticos:

import { Document, model, Model, Schema } from "mongoose";

interface IUserDocument extends Document {
  name: string;
  method1: () => string;
}
interface IUserModel extends Model<IUserDocument> {
  static1: () => string;
}

var UserSchema = new Schema<IUserDocument & IUserModel>({
  name: String
});

UserSchema.methods.method1 = function() {
  return this.name;
};
UserSchema.statics.static1 = function() {
  return "";
};

var UserModel: IUserModel = model<IUserDocument, IUserModel>(
  "User",
  UserSchema
);
UserModel.static1(); // static methods are available

var user = new UserModel({ name: "Success" });
user.method1();

En general, este archivo README parece ser un recurso fantástico para abordar tipos con mangostas.


Este enfoque duplica la definición de cada campo desde IUserDocumenthacia UserSchema, lo que crea un riesgo de mantenimiento a medida que el modelo se vuelve más complejo. A los paquetes les gusta ts-mongoosee typegooseintentan resolver ese problema, aunque es cierto que con un poco de cruft sintáctico.
Dan Dascalescu

0

Si desea asegurarse de que su esquema satisfaga el tipo de modelo y viceversa, esta solución ofrece una mejor escritura de la que sugirió @bingles:

El archivo de tipo común: ToSchema.ts(¡Que no cunda el pánico! Simplemente cópielo y péguelo)

import { Document, Schema, SchemaType, SchemaTypeOpts } from 'mongoose';

type NonOptionalKeys<T> = { [k in keyof T]-?: undefined extends T[k] ? never : k }[keyof T];
type OptionalKeys<T> = Exclude<keyof T, NonOptionalKeys<T>>;
type NoDocument<T> = Exclude<T, keyof Document>;
type ForceNotRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required?: false };
type ForceRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required: SchemaTypeOpts<any>['required'] };

export type ToSchema<T> = Record<NoDocument<NonOptionalKeys<T>>, ForceRequired | Schema | SchemaType> &
   Record<NoDocument<OptionalKeys<T>>, ForceNotRequired | Schema | SchemaType>;

y un modelo de ejemplo:

import { Document, model, Schema } from 'mongoose';
import { ToSchema } from './ToSchema';

export interface IUser extends Document {
   name?: string;
   surname?: string;
   email: string;
   birthDate?: Date;
   lastLogin?: Date;
}

const userSchemaDefinition: ToSchema<IUser> = {
   surname: String,
   lastLogin: Date,
   role: String, // Error, 'role' does not exist
   name: { type: String, required: true, unique: true }, // Error, name is optional! remove 'required'
   email: String, // Error, property 'required' is missing
   // email: {type: String, required: true}, // correct 👍
   // Error, 'birthDate' is not defined
};

const userSchema = new Schema(userSchemaDefinition);

export const User = model<IUser>('User', userSchema);


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.