Mongoose the Typescript way…?

90

Próba zaimplementowania modelu Mongoose w Typescript. Przeszukiwanie Google ujawniło tylko podejście hybrydowe (łączące JS i TS). Jak można by zaimplementować klasę User, stosując moje raczej naiwne podejście, bez JS?

Chcesz mieć możliwość IUserModel bez bagażu.

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
źródło
Usernie może być klasą, ponieważ jej utworzenie jest operacją asynchroniczną. Musi zwrócić obietnicę, więc musisz zadzwonić User.create({...}).then....
Louay Alakkad
1
Konkretnie, podany w kodzie w PO, czy mógłbyś wyjaśnić, dlaczego Usernie może być klasą?
Tim McNamara,
Zamiast tego wypróbuj github.com/typeorm/typeorm .
Erich
@Erich mówią, że typeorm nie działa dobrze z MongoDB, może Type goose to dobra opcja
PayamBeirami

Odpowiedzi:

130

Oto jak to robię:

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
źródło
2
przepraszam, ale jak definiuje się termin „mangusta” w TS?
Tim McNamara
13
import * as mongoose from 'mongoose';lubimport mongoose = require('mongoose');
Louay Alakkad,
1
Coś takiego:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad
3
Ostatnia linia (eksport domyślny const User ...) nie działa dla mnie. Muszę podzielić linię, zgodnie z propozycją na stackoverflow.com/questions/35821614/ ...
Sergio
7
Mogę się obejść let newUser = new User({ iAmNotHere: true })bez żadnych błędów w IDE lub przy kompilacji. Jaki jest więc powód tworzenia interfejsu?
Lupurus
33

Inna alternatywa, jeśli chcesz odłączyć definicje typów i implementację bazy danych.

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

Inspiracja stąd: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models

Gábor Imre
źródło
1
Czy mongoose.Schemadefinicja tutaj powiela pola z IUser? Biorąc pod uwagę, że IUserjest zdefiniowana w innym pliku na ryzyko, że pola z niezsynchronizowane jak projekt rośnie w złożoności i liczby deweloperów, jest dość wysoki.
Dan Dascalescu
Tak, to ważny argument, który warto rozważyć. Korzystanie z testów integracji komponentów może jednak pomóc zmniejszyć ryzyko. Zauważ, że istnieją podejścia i architektury, w których deklaracje typu i implementacje DB są oddzielone, niezależnie od tego, czy odbywa się to za pośrednictwem ORM (jak zaproponowałeś), czy ręcznie (jak w tej odpowiedzi). Nie ma srebrnej kuli ... <(°. °)>
Gábor Imre
Jednym z punktów może być wygenerowanie kodu z definicji GraphQL dla TypeScript i Mongoose.
Dan Dascalescu
23

Przepraszamy za nekroposty, ale to może być nadal interesujące dla kogoś. Myślę, że Typegoose zapewnia bardziej nowoczesny i elegancki sposób definiowania modeli

Oto przykład z dokumentów:

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

W przypadku istniejącego scenariusza połączenia można użyć następujących (co może być bardziej prawdopodobne w rzeczywistych sytuacjach i ujawnione w dokumentacji):

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);
})();
Dimanoid
źródło
8
Ja też doszedłem do tego wniosku, ale martwię się, że typegoosenie ma wystarczającego wsparcia ... sprawdzam statystyki npm, to tylko 3k tygodniowych pobrań, a rn jest prawie 100 otwartych problemów na Githubie, z których większość nie ma komentarzy, a niektóre z nich wyglądają tak, jakby powinny były zostać zamknięte dawno temu
Corbfon
@Corbfon Czy próbowałeś tego? Jeśli tak, jakie były twoje ustalenia? Jeśli nie, czy było coś jeszcze, co sprawiło, że zdecydowałeś go nie używać? Generalnie widzę, że niektórzy martwią się o pełne wsparcie, ale wydaje się, że ci, którzy go faktycznie używają, są z niego całkiem zadowoleni
N4ppeL
1
@ N4ppeL Nie poszedłbym z typegoose- skończyło się na ręcznej obsłudze naszego pisania, podobnie jak w tym poście , wygląda na to, że ts-mongoosemoże mieć jakąś obietnicę (jak zasugerowano w późniejszej odpowiedzi)
Corbfon
1
Nigdy nie przepraszaj za „nekroposty”. [Jak już wiecie ...] Jest nawet odznaka (choć to jest nazwane Nekromantę ; ^ D) za to właśnie to! Zachęcamy do nekropozycji nowych informacji i pomysłów!
ruffin
1
@ruffin: Naprawdę nie rozumiem też piętna związanego z publikowaniem nowych i aktualnych rozwiązań problemów.
Dan Dascalescu
16

Spróbuj ts-mongoose. Do mapowania używa typów warunkowych.

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

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

const User = typedModel('User', UserSchema);
niebo
źródło
1
Wygląda bardzo obiecująco! Dzięki za udostępnienie! :)
Boriel
1
Łał. To zamki bardzo eleganckie. Nie mogę się doczekać, aby spróbować!
qqilihq
1
Ujawnienie: wydaje się, że ts-mongoose została stworzona przez niebo. Wydaje się, że jest to najsprytniejsze rozwiązanie.
mikrofon
1
Ładny pakiet, czy nadal go utrzymujesz ?
Dan Dascalescu
11

Większość odpowiedzi tutaj powtarza pola w klasie / interfejsie TypeScript oraz w schemacie mangusta. Brak jednego źródła prawdy stwarza ryzyko związane z konserwacją, ponieważ projekt staje się bardziej złożony i pracuje nad nim więcej programistów: istnieje większe prawdopodobieństwo, że pola stracą synchronizację . Jest to szczególnie złe, gdy klasa znajduje się w innym pliku niż schemat mangusty.

Aby zachować synchronizację pól, warto je raz zdefiniować. Jest kilka bibliotek, które to robią:

Nie byłem jeszcze w pełni przekonany do żadnego z nich, ale typegoose wydaje się aktywnie utrzymywany, a deweloper zaakceptował moje PR.

Aby pomyśleć o krok naprzód: po dodaniu schematu GraphQL do miksu pojawia się kolejna warstwa powielania modelu. Jednym ze sposobów rozwiązania tego problemu może być wygenerowanie kodu TypeScript i kodu mongoose ze schematu GraphQL.

Dan Dascalescu
źródło
5

Oto skuteczny sposób na dopasowanie prostego modelu do schematu mangusty. Kompilator upewni się, że definicje przekazane do mongoose.Schema są zgodne z interfejsem. Gdy masz już schemat, możesz użyć

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

Gdy masz już swój schemat, możesz użyć metod wymienionych w innych odpowiedziach, takich jak

const userModel = mongoose.model<User & mongoose.Document>('User', schema);
bingles
źródło
1
To jedyna poprawna odpowiedź. Żadna z pozostałych odpowiedzi w rzeczywistości nie zapewniała zgodności typu między schematem a typem / interfejsem.
Jamie Strauss
@JamieStrauss: co powiesz na to, żeby nie powielać pól ?
Dan Dascalescu
1
@DanDascalescu Myślę, że nie rozumiesz, jak działają typy.
Jamie Strauss
5

Po prostu dodaj inny sposób ( @types/mongoosemusi być zainstalowany z 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,
    // ...
}));

A różnica między interfacei type, przeczytaj tę odpowiedź

Ten sposób ma tę zaletę, że możesz dodać statyczne typy metod Mongoose:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}
Hongbo Miao
źródło
gdzie zdefiniowałeś generateJwt?
relacja z
1
const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));Zasadniczo @rels generateJwtstaje się kolejną właściwością modelu.
a11smiles
Czy dodałbyś go po prostu jako metodę w ten sposób, czy też połączyłbyś go z właściwością Methods?
user1790300,
1
Powinna to być akceptowana odpowiedź, ponieważ odłącza definicję użytkownika i DAL użytkownika. Jeśli chcesz przełączyć się z mongo na innego dostawcę bazy danych, nie będziesz musiał zmieniać interfejsu użytkownika.
Rafael del Rio
1
@RafaeldelRio: pytanie dotyczyło używania mangusty z TypeScript. Przejście do innej bazy danych jest sprzeczne z tym celem. A problem z oddzieleniem definicji schematu od IUserdeklaracji interfejsu w innym pliku polega na tym, że ryzyko braku synchronizacji pól w miarę wzrostu złożoności projektu i liczby programistów jest dość wysokie.
Dan Dascalescu
4

Oto, jak robią to ludzie z Microsoftu. tutaj

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

Polecam sprawdzić ten doskonały projekt początkowy po dodaniu TypeScript do projektu Node.

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

Główny programista
źródło
1
To powiela każde pole między mongoose i TypeScript, co stwarza ryzyko konserwacji, gdy model staje się bardziej złożony. Rozwiązania lubią ts-mongoosei typegooserozwiązują ten problem, choć trzeba przyznać, że zawiera sporo syntaktycznego okrucieństwa.
Dan Dascalescu
2

Dzięki temu vscode intellisensedziała na obu

  • Typ użytkownika User.findOne
  • instancja użytkownika u1._id

Kod:

// 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
źródło
2

Oto przykład z dokumentacji Mongoose, Tworzenie z klas ES6 przy użyciu loadClass () , przekonwertowany na 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');
})();

W przypadku findByFullNamemetody statycznej nie mogłem dowiedzieć się, jak uzyskać informacje o typie Person, więc musiałem rzutować, <any>Persongdy chcę to wywołać. Jeśli wiesz, jak to naprawić, dodaj komentarz.

orad
źródło
Podobnie jak inne odpowiedzi , to podejście powiela pola między interfejsem a schematem. Można tego uniknąć, mając jedno źródło prawdy, np. Używając ts-mongooselub typegoose. Sytuacja ulega dalszemu powieleniu podczas definiowania schematu GraphQL.
Dan Dascalescu
Czy jest jakiś sposób na zdefiniowanie referencji za pomocą tego podejścia?
Dan Dascalescu
1

Jestem fanem Plumiera, ma pomocnika mangusty , ale można go używać samodzielnie bez samego Plumiera . W przeciwieństwie do Typegoose, wybrał inną ścieżkę, używając dedykowanej biblioteki refleksji Plumier, która umożliwia korzystanie z fajnych rzeczy.

cechy

  1. Czysty POJO (domena nie musi dziedziczyć z żadnej klasy ani używać żadnego specjalnego typu danych), Model tworzony automatycznie, ponieważ T & Documentumożliwia dostęp do właściwości związanych z dokumentem.
  2. Obsługiwane właściwości parametru TypeScript, dobrze, gdy masz strict:truekonfigurację tsconfig. A z parametrami właściwości nie wymaga dekoratora we wszystkich właściwościach.
  3. Obsługiwane właściwości pola, takie jak Typegoose
  4. Konfiguracja jest taka sama jak w przypadku mangusty, więc łatwo się z nią zaznajomisz.
  5. Obsługiwane dziedziczenie, dzięki czemu programowanie jest bardziej naturalne.
  6. Analiza modelu, pokazująca nazwy modelu i jego odpowiednią nazwę kolekcji, zastosowaną konfigurację itp.

Stosowanie

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
źródło
1

Dla każdego, kto szuka rozwiązania dla istniejących projektów Mongoose:

Niedawno stworzyliśmy mongoose-tsgen, aby rozwiązać ten problem (chcielibyśmy poznać opinie!). Istniejące rozwiązania, takie jak typegoose, wymagały przepisania całych naszych schematów i wprowadziły różne niezgodności. mongoose-tsgen to proste narzędzie CLI, które generuje plik index.d.ts zawierający interfejsy Typescript dla wszystkich schematów Mongoose; wymaga niewielkiej konfiguracji i bardzo płynnie integruje się z każdym projektem Typescript.

Francesco Virga
źródło
0

Oto przykład oparty na pliku README @types/mongoosepakietu.

Oprócz elementów już zawartych powyżej pokazuje, jak uwzględnić metody regularne i statyczne:

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

Ogólnie rzecz biorąc, ten plik README wydaje się być fantastycznym źródłem informacji o podejściu do gatunków z mangustą.

webelo
źródło
To podejście powiela definicję każdego pola od IUserDocumentdo UserSchema, co stwarza ryzyko konserwacji, gdy model staje się bardziej złożony. Pakiety lubią ts-mongoosei typegoosepróbują rozwiązać ten problem, choć trzeba przyznać, że zawierają sporo syntaktycznego okrucieństwa.
Dan Dascalescu
0

Jeśli chcesz mieć pewność, że Twój schemat jest zgodny z typem modelu i odwrotnie, to rozwiązanie oferuje lepsze wpisywanie niż sugerowane @bingles:

Typowy plik typu: ToSchema.ts(Nie panikuj! Po prostu skopiuj i wklej)

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

i przykładowy model:

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
źródło