Mangusta alla maniera del dattiloscritto ...?


94

Tentativo di implementare un modello Mongoose in Typescript. Perlustrare Google ha rivelato solo un approccio ibrido (che combina JS e TS). Come si potrebbe implementare la classe User, con il mio approccio piuttosto ingenuo, senza JS?

Vuoi essere in grado di IUserModel senza il bagaglio.

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

Usernon può essere una classe perché crearne una è un'operazione asincrona. Deve restituire una promessa, quindi devi chiamare User.create({...}).then....
Louay Alakkad

1
Nello specifico, dato nel codice nell'OP, potresti approfondire il motivo per cui Usernon può essere una classe?
Tim McNamara


@Erich dicono che typeorm non funziona bene con MongoDB, forse Type goose è una buona opzione
PayamBeirami

Risposte:


133

Ecco come lo faccio:

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
scusa, ma come viene definita "mangusta" in TS?
Tim McNamara

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

1
Qualcosa del genere:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
L'ultima riga (export default const User ...) non funziona per me. Devo dividere la linea, come proposto in stackoverflow.com/questions/35821614/…
Sergio

7
Posso fare a let newUser = new User({ iAmNotHere: true })meno di errori nell'IDE o nella compilazione. Allora qual è il motivo per creare un'interfaccia?
Lupurus

34

Un'altra alternativa se si desidera scollegare le definizioni del tipo e l'implementazione del database.

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

Ispirazione da qui: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
La mongoose.Schemadefinizione qui duplica i campi da IUser? Dato che IUserè definito in un file diverso, il rischio che i campi non siano sincronizzati al crescere della complessità e del numero di sviluppatori del progetto è piuttosto alto.
Dan Dascalescu

Sì, questo è un argomento valido che vale la pena considerare. L'uso dei test di integrazione dei componenti può aiutare a ridurre i rischi. E nota che ci sono approcci e architetture in cui le dichiarazioni di tipo e le implementazioni DB sono separate sia che avvenga tramite un ORM (come hai proposto) o manualmente (come in questa risposta). Nessun proiettile d'argento c'è ... <(°. °)>
Gábor Imre

Un punto potrebbe essere quello di generare codice dalla definizione GraphQL, per TypeScript e mongoose.
Dan Dascalescu

24

Ci scusiamo per il necroposting ma questo può essere comunque interessante per qualcuno. Penso che Typegoose offra un modo più moderno ed elegante per definire i modelli

Ecco un esempio dalla documentazione:

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

Per uno scenario di connessione esistente, puoi utilizzare come segue (che potrebbe essere più probabile nelle situazioni reali e scoperto nei documenti):

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
Sono anche giunto a questa conclusione, ma sono preoccupato che typegoosenon abbia abbastanza supporto ... controllando le loro statistiche npm, sono solo 3k download settimanali e ci sono quasi 100 problemi Github aperti, molti dei quali non hanno commenti, e alcuni dei quali sembra che avrebbero dovuto essere chiusi molto tempo fa
Corbfon

@ Corbfon L'hai provato? In caso affermativo, quali sono state le tue scoperte? In caso contrario, c'era qualcos'altro che ti ha fatto decidere di non usarlo? In genere vedo alcune persone preoccuparsi del supporto completo, ma sembra che coloro che lo usano effettivamente ne siano abbastanza contenti
N4ppeL

1
@ N4ppeL Non andrei con typegoose- abbiamo finito per gestire manualmente la nostra digitazione, simile a questo post , sembra che ts-mongoosepotrebbe avere qualche promessa (come suggerito nella risposta successiva)
Corbfon

1
Non chiedere mai scusa per "necroposting". [Come sai ...] C'è anche un badge (anche se esso è chiamato Negromante ; ^ D) per fare proprio questo! È incoraggiato il necroposting di nuove informazioni e idee!
ruffin

1
@ruffin: Inoltre non capisco lo stigma contro la pubblicazione di soluzioni nuove e aggiornate ai problemi.
Dan Dascalescu

16

Prova ts-mongoose. Utilizza i tipi condizionali per eseguire la mappatura.

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

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

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

1
Sembra molto promettente! Grazie per la condivisione! :)
Boriel

1
Wow. Questo si blocca molto elegante. Non vedo l'ora di provarlo!
qqilihq

1
Divulgazione: la ts-mangusta sembra essere stata creata dal cielo. Sembra essere la soluzione più intelligente in circolazione.
microfono

1
Bel pacchetto, lo mantieni ancora ?
Dan Dascalescu

11

La maggior parte delle risposte qui ripetono i campi nella classe / interfaccia TypeScript e nello schema mongoose. Non avere un'unica fonte di verità rappresenta un rischio di manutenzione, poiché il progetto diventa più complesso e più sviluppatori ci lavorano: i campi hanno maggiori probabilità di perdere la sincronizzazione . Ciò è particolarmente negativo quando la classe si trova in un file diverso rispetto allo schema mongoose.

Per mantenere i campi sincronizzati, ha senso definirli una volta. Ci sono alcune librerie che fanno questo:

Non sono ancora stato completamente convinto da nessuno di loro, ma typegoose sembra attivamente mantenuto e lo sviluppatore ha accettato i miei PR.

Per pensare un passo avanti: quando aggiungi uno schema GraphQL alla combinazione, appare un altro livello di duplicazione del modello. Un modo per superare questo problema potrebbe essere generare il codice TypeScript e mongoose dallo schema GraphQL.


5

Ecco un modo forte per abbinare un modello semplice con uno schema mongoose. Il compilatore garantirà che le definizioni passate a mongoose.Schema corrispondano all'interfaccia. Una volta che hai lo schema, puoi usare

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 volta ottenuto lo schema, puoi utilizzare i metodi menzionati in altre risposte come

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

1
Questa è l'unica risposta corretta. Nessuna delle altre risposte garantiva effettivamente la compatibilità del tipo tra lo schema e il tipo / interfaccia.
Jamie Strauss

@ JamieStrauss: che ne dici di non duplicare i campi in primo luogo ?
Dan Dascalescu

1
@DanDascalescu Non credo che tu capisca come funzionano i tipi.
Jamie Strauss

5

Basta aggiungere un altro modo ( @types/mongoosedeve essere installato 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,
    // ...
}));

E la differenza tra interfacee type, per favore leggi questa risposta

In questo modo ha un vantaggio, puoi aggiungere i tipi di metodo statico Mongoose:

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

dove l'hai definita generateJwt?
riferisce il

1
@rels const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));fondamentalmente, generateJwtdiventa un'altra proprietà del modello.
a11smiles

Lo aggiungeresti semplicemente come metodo in questo modo o lo collegheresti alla proprietà methods?
user1790300

1
Questa dovrebbe essere la risposta accettata in quanto rimuove la definizione dell'utente e l'utente DAL. Se vuoi passare da mongo a un altro provider di database, non dovrai cambiare interfaccia utente.
Rafael del Rio

1
@RafaeldelRio: la domanda riguardava l'utilizzo della mangusta con TypeScript. Il passaggio a un altro DB è antitetico a questo obiettivo. E il problema con la separazione della definizione dello schema dalla IUserdichiarazione dell'interfaccia in un file diverso è che il rischio che i campi non siano sincronizzati con l'aumentare della complessità e degli sviluppatori del progetto è piuttosto alto.
Dan Dascalescu

4

Ecco come lo fanno i ragazzi di Microsoft. Qui

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

Consiglio di controllare questo eccellente progetto iniziale quando aggiungi TypeScript al tuo progetto Node.

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


1
Ciò duplica ogni singolo campo tra mongoose e TypeScript, il che crea un rischio di manutenzione man mano che il modello diventa più complesso. Soluzioni come ts-mongoosee typegooserisolvono questo problema, anche se è vero con un po 'di cruft sintattico.
Dan Dascalescu

2

Con questo vscode intellisensefunziona su entrambi

  • Tipo di utente User.findOne
  • istanza utente u1._id

Il codice:

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

Ecco l'esempio dalla documentazione di Mongoose, Creazione da classi ES6 utilizzando loadClass () , convertito in 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');
})();

Per il findByFullNamemetodo statico , non riuscivo a capire come ottenere le informazioni sul tipo Person, quindi ho dovuto eseguire il cast <any>Personquando volevo chiamarlo. Se sai come risolverlo, aggiungi un commento.


Come altre risposte , questo approccio duplica i campi tra l'interfaccia e lo schema. Ciò potrebbe essere evitato disponendo di un'unica fonte di verità, ad esempio utilizzando ts-mongooseo typegoose. La situazione viene ulteriormente duplicata quando si definisce lo schema GraphQL.
Dan Dascalescu

Qualche modo per definire i ref con questo approccio?
Dan Dascalescu

2

Sono un fan di Plumier, ha l' aiutante della mangusta , ma può essere usato da solo senza Plumier stesso . A differenza di Typegoose, ha preso un percorso diverso utilizzando la libreria di riflessione dedicata di Plumier, che rende possibile l'uso di elementi di raffreddamento.

Caratteristiche

  1. POJO puro (il dominio non deve ereditare da alcuna classe, né utilizzare alcun tipo di dati speciale), Modello creato automaticamente dedotto come T & Document possibile accedere alle proprietà relative al documento.
  2. Proprietà del parametro TypeScript supportate, va bene quando hai strict:true configurazione tsconfig. E con le proprietà dei parametri non è necessario decoratore su tutte le proprietà.
  3. Proprietà di campo supportate come Typegoose
  4. La configurazione è la stessa della mangusta, quindi la familiarizzerai facilmente.
  5. Eredità supportata che rende la programmazione più naturale.
  6. Analisi del modello, che mostra i nomi dei modelli e il nome della raccolta appropriato, la configurazione applicata ecc.

Utilizzo

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

Per chiunque cerchi una soluzione per i progetti esistenti di Mongoose:

Abbiamo recentemente creato mongoose-tsgen per risolvere questo problema (mi piacerebbe ricevere un feedback!). Le soluzioni esistenti come typegoose richiedevano la riscrittura dei nostri interi schemi e introducevano varie incompatibilità. mongoose-tsgen è un semplice strumento CLI che genera un file index.d.ts contenente interfacce Typescript per tutti i tuoi schemi Mongoose; richiede poca o nessuna configurazione e si integra perfettamente con qualsiasi progetto Typescript.


1

Se vuoi assicurarti che il tuo schema soddisfi il tipo di modello e viceversa, questa soluzione offre una digitazione migliore di quanto suggerito da @bingles:

Il file di tipo comune: ToSchema.ts(Niente panico! Basta copiarlo e incollarlo)

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

e un modello di esempio:

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



0

Ecco un esempio basato sul README per il @types/mongoosepacchetto.

Oltre agli elementi già inclusi sopra, mostra come includere metodi regolari e statici:

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

In generale, questo README sembra essere una risorsa fantastica per avvicinarsi ai tipi con la mangusta.


Questo approccio duplica la definizione di ogni campo da IUserDocumentin UserSchema, il che crea un rischio di manutenzione man mano che il modello diventa più complesso. I pacchetti gradiscono ts-mongoosee typegoosetentano di risolvere quel problema, anche se, ammettiamolo, con un po 'di cruft sintattico.
Dan Dascalescu

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.