Mangouste à la manière du Typescript…?

90

Essayer d'implémenter un modèle Mongoose dans Typescript. La fouille de Google n'a révélé qu'une approche hybride (combinant JS et TS). Comment implémenter la classe User, sur mon approche plutôt naïve, sans le JS?

Vous voulez pouvoir IUserModel sans bagages.

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() {}
}
Tim McNamara
la source
Userne peut pas être une classe car en créer une est une opération asynchrone. Il doit retourner une promesse donc vous devez appeler User.create({...}).then....
Louay Alakkad
1
Plus précisément, comme indiqué dans le code de l'OP, pourriez-vous expliquer pourquoi Userne peut pas être une classe?
Tim McNamara
Essayez plutôt github.com/typeorm/typeorm .
Erich
@Erich ils disent que typeorm ne fonctionne pas bien avec MongoDB, peut-être que Type goose est une bonne option
PayamBeirami
Vérifiez ceci sur npmjs.com/package/@types/mongoose
Harry

Réponses:

130

Voici comment je fais:

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;
Louay Alakkad
la source
2
désolé, mais comment la «mangouste» est-elle définie dans TS?
Tim McNamara
13
import * as mongoose from 'mongoose';ouimport mongoose = require('mongoose');
Louay Alakkad
1
Quelque chose comme ça:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad
3
La dernière ligne (exporter par défaut l'utilisateur const ...) ne fonctionne pas pour moi. J'ai besoin de diviser la ligne, comme proposé dans stackoverflow.com/questions/35821614/…
Sergio
7
Je peux me passer let newUser = new User({ iAmNotHere: true })d'erreurs dans l'IDE ou lors de la compilation. Alors, quelle est la raison de la création d'une interface?
Lupurus
33

Une autre alternative si vous souhaitez détacher vos définitions de type et l'implémentation de la base de données.

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 */
}));

Inspiration d'ici: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models

Gábor Imre
la source
1
La mongoose.Schemadéfinition ici duplique-t-elle les champs à partir de IUser? Étant donné que cela IUserest défini dans un fichier différent, le risque de désynchronisation des champs à mesure que le projet augmente en complexité et en nombre de développeurs est assez élevé.
Dan Dascalescu le
Oui, c'est un argument valable à considérer. L'utilisation de tests d'intégration de composants peut cependant aider à réduire les risques. Et notez qu'il existe des approches et des architectures où les déclarations de type et les implémentations DB sont séparées, que ce soit via un ORM (comme vous l'avez proposé) ou manuellement (comme dans cette réponse). Il n'y a pas de solution miracle ... <(°. °)>
Gábor Imre le
Une puce pourrait être de générer du code à partir de la définition GraphQL, pour TypeScript et mangouste.
Dan Dascalescu le
23

Désolé pour le nécropostage mais cela peut encore être intéressant pour quelqu'un. Je pense que Typegoose offre une manière plus moderne et élégante de définir les modèles

Voici un exemple tiré de la documentation:

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

Pour un scénario de connexion existant, vous pouvez utiliser comme suit (ce qui peut être plus probable dans les situations réelles et découvert dans la documentation):

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);
})();
Dimanoïde
la source
8
Je suis également arrivé à cette conclusion, mais je crains que le typegoosesupport ne soit pas suffisant ... en vérifiant leurs statistiques npm, ce ne sont que 3k téléchargements hebdomadaires, et rn il y a presque 100 problèmes Github ouverts, dont la plupart n'ont pas de commentaires, et dont certains semblent avoir dû être fermés il y a longtemps
Corbfon
@Corbfon Avez-vous essayé? Si oui, quelles ont été vos conclusions? Sinon, y a-t-il autre chose qui vous a incité à ne pas l'utiliser? Je vois généralement certaines personnes s'inquiéter pour un support complet, mais il semble que ceux qui l'utilisent en soient assez satisfaits
N4ppeL
1
@ N4ppeL Je n'irais pas avec typegoose- nous avons fini par gérer manuellement notre frappe, similaire à cet article , il semble que cela ts-mongoosepourrait avoir des promesses (comme suggéré dans la réponse ultérieure)
Corbfon
1
Ne vous excusez jamais pour "necroposting". [Comme vous le savez maintenant ...] Il y a même un badge (bien qu'il soit nommé Nécromancien ; ^ D) pour faire juste cela! Necroposting de nouvelles informations et idées est encouragé!
ruffin
1
@ruffin: Je ne comprends vraiment pas non plus la stigmatisation contre la publication de solutions nouvelles et à jour aux problèmes.
Dan Dascalescu le
16

Essayez ts-mongoose. Il utilise des types conditionnels pour effectuer le mappage.

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

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

const User = typedModel('User', UserSchema);
ciel
la source
1
Ça a l'air très prometteur! Merci d'avoir partagé! :)
Boriel
1
Sensationnel. Cette serrure très élégante. Au plaisir de l'essayer!
qqilihq
1
Divulgation: ts-mongoose semble être créé par le ciel. Semble être la solution la plus astucieuse qui soit.
micro le
1
Bon paquet, le maintenez-vous toujours ?
Dan Dascalescu le
11

La plupart des réponses ici répètent les champs de la classe / interface TypeScript et du schéma mangouste. Ne pas avoir une seule source de vérité représente un risque de maintenance, car le projet devient plus complexe et de plus en plus de développeurs y travaillent: les champs sont plus susceptibles de se désynchroniser . Ceci est particulièrement mauvais lorsque la classe est dans un fichier différent du schéma mangouste.

Pour garder les champs synchronisés, il est logique de les définir une fois. Il existe quelques bibliothèques qui font cela:

Je n'ai encore été pleinement convaincu par aucun d'entre eux, mais typegoose semble activement maintenu, et le développeur a accepté mes PR.

Pour avoir une longueur d'avance: lorsque vous ajoutez un schéma GraphQL dans le mix, une autre couche de duplication de modèle apparaît. Une façon de surmonter ce problème pourrait être de générer du code TypeScript et mangouste à partir du schéma GraphQL.

Dan Dascalescu
la source
5

Voici un moyen typé fort de faire correspondre un modèle simple avec un schéma de mangouste. Le compilateur s'assurera que les définitions passées à mongoose.Schema correspondent à l'interface. Une fois que vous avez le schéma, vous pouvez utiliser

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

Une fois que vous avez votre schéma, vous pouvez utiliser les méthodes mentionnées dans d'autres réponses telles que

const userModel = mongoose.model<User & mongoose.Document>('User', schema);
bingles
la source
1
C'est la seule bonne réponse. Aucune des autres réponses n'assurait réellement la compatibilité de type entre le schéma et le type / l'interface.
Jamie Strauss
@JamieStrauss: pourquoi ne pas dupliquer les champs en premier lieu ?
Dan Dascalescu le
1
@DanDascalescu Je ne pense pas que vous compreniez comment fonctionnent les types.
Jamie Strauss le
5

Ajoutez simplement un autre moyen ( @types/mongoosedoit être installé avec 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,
    // ...
}));

Et la différence entre interfaceet type, veuillez lire cette réponse

Cette façon a un avantage, vous pouvez ajouter des typages de méthode statique Mongoose:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}
Hongbo Miao
la source
où avez-vous défini generateJwt?
rels
1
@rels devient const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));essentiellement generateJwtune autre propriété du modèle.
a11smiles
Souhaitez-vous simplement l'ajouter en tant que méthode de cette manière ou le connecteriez-vous à la propriété methods?
user1790300 du
1
Cela devrait être la réponse acceptée car elle détache la définition d'utilisateur et la DAL d'utilisateur. Si vous souhaitez passer de mongo à un autre fournisseur de base de données, vous n'aurez pas à changer d'interface utilisateur.
Rafael del Rio
1
@RafaeldelRio: la question portait sur l'utilisation de la mangouste avec TypeScript. Le passage à une autre base de données est contraire à cet objectif. Et le problème avec la séparation de la définition de schéma de la IUserdéclaration d'interface dans un fichier différent est que le risque de désynchronisation des champs à mesure que le projet augmente en termes de complexité et de développeurs, est assez élevé.
Dan Dascalescu le
4

Voici comment les gars de Microsoft le font. ici

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

Je recommande de vérifier cet excellent projet de démarrage lorsque vous ajoutez TypeScript à votre projet Node.

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

Développeur principal
la source
1
Cela duplique chaque champ entre mangouste et TypeScript, ce qui crée un risque de maintenance à mesure que le modèle devient plus complexe. Les solutions aiment ts-mongooseet typegooserésolvent ce problème, bien que certes avec un peu de cruauté syntaxique.
Dan Dascalescu le
2

Avec cela vscode intellisensefonctionne à la fois

  • Type d'utilisateur User.findOne
  • instance utilisateur u1._id

Le code:

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

tomitheninja
la source
2

Voici l'exemple de la documentation Mongoose, Création à partir de classes ES6 à l'aide de loadClass () , converti en 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');
})();

Pour la findByFullNameméthode statique , je ne pouvais pas comprendre comment obtenir les informations de type Person, j'ai donc dû effectuer un cast <any>Personquand je voulais l'appeler. Si vous savez comment résoudre ce problème, veuillez ajouter un commentaire.

orad
la source
Comme d' autres réponses , cette approche duplique les champs entre l'interface et le schéma. Cela pourrait être évité en ayant une seule source de vérité, par exemple en utilisant ts-mongooseou typegoose. La situation se duplique davantage lors de la définition du schéma GraphQL.
Dan Dascalescu le
Une façon de définir les références avec cette approche?
Dan Dascalescu le
1

Je suis un fan de Plumier, il a un assistant mangouste , mais il peut être utilisé seul sans Plumier lui-même . Contrairement à Typegoose, il a emprunté un chemin différent en utilisant la bibliothèque de réflexion dédiée de Plumier, qui permet d'utiliser des éléments refroidis.

traits

  1. POJO pur (le domaine n'a pas besoin d'hériter d'une classe, ni d'utiliser un type de données spécial), modèle créé automatiquement inféré comme il est T & Documentdonc possible d'accéder aux propriétés liées au document.
  2. Propriétés des paramètres TypeScript prises en charge, c'est bien lorsque vous avez la strict:trueconfiguration tsconfig. Et avec les propriétés de paramètre ne nécessite pas de décorateur sur toutes les propriétés.
  3. Propriétés de champ prises en charge telles que Typegoose
  4. La configuration est la même que celle de la mangouste, vous vous familiariserez donc facilement avec elle.
  5. L'héritage pris en charge qui rend la programmation plus naturelle.
  6. Analyse du modèle, montrant les noms de modèle et son nom de collection approprié, la configuration appliquée, etc.

Usage

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()
bonjorno
la source
1

Pour tous ceux qui recherchent une solution pour les projets Mongoose existants:

Nous avons récemment construit mongoose-tsgen pour résoudre ce problème (nous aimerions avoir vos commentaires!). Les solutions existantes comme typegoose ont nécessité la réécriture de l'ensemble de nos schémas et introduit diverses incompatibilités. mongoose-tsgen est un simple outil CLI qui génère un fichier index.d.ts contenant des interfaces Typescript pour tous vos schémas Mongoose; il nécessite peu ou pas de configuration et s'intègre très facilement à tout projet Typescript.

Francesco Virga
la source
0

Voici un exemple basé sur le README du @types/mongoosepackage.

Outre les éléments déjà inclus ci-dessus, il montre comment inclure des méthodes régulières et statiques:

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 général, ce README semble être une ressource fantastique pour aborder les types avec mangouste.

Webelo
la source
Cette approche duplique la définition de chaque champ de IUserDocumentvers UserSchema, ce qui crée un risque de maintenance à mesure que le modèle devient plus complexe. Les packages aiment ts-mongooseet typegoosetentent de résoudre ce problème, bien que certes avec un peu de cruauté syntaxique.
Dan Dascalescu le
0

Si vous voulez vous assurer que votre schéma satisfait le type de modèle et vice versa, cette solution offre un meilleur typage que ce que @bingles a suggéré:

Le fichier de type commun: ToSchema.ts(Pas de panique! Il suffit de le copier et de le coller)

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

et un exemple de modèle:

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


Morteza Faghih Shojaie
la source