Mungo nach Typoskript…?


92

Versuch, ein Mungo-Modell in Typescript zu implementieren. Das Durchsuchen von Google hat nur einen hybriden Ansatz ergeben (Kombination von JS und TS). Wie würde man die User-Klasse nach meinem eher naiven Ansatz ohne JS implementieren?

Willst du in der Lage sein, IUserModel ohne das Gepäck.

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

Userkann keine Klasse sein, da das Erstellen einer Klasse eine asynchrone Operation ist. Es muss ein Versprechen zurückgeben, damit Sie anrufen müssen User.create({...}).then....
Louay Alakkad

1
Könnten Sie bitte im OP-Code erläutern, warum Userkeine Klasse sein kann?
Tim McNamara

Versuchen Sie stattdessen github.com/typeorm/typeorm .
Erich

@ Rich sie sagen, dass Typeorm nicht gut mit der MongoDB funktioniert, vielleicht ist Type Goose eine gute Option
PayamBeirami

Überprüfen Sie dies aus npmjs.com/package/@types/mongoose
Harry

Antworten:


131

So mache ich das:

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
Entschuldigung, aber wie ist 'Mungo' in TS definiert?
Tim McNamara

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

1
So etwas in der Art:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
Die letzte Zeile (export const const User ...) funktioniert bei mir nicht. Ich muss die Linie teilen, wie in stackoverflow.com/questions/35821614/…
Sergio

7
Ich kann auf let newUser = new User({ iAmNotHere: true })Fehler in der IDE oder beim Kompilieren verzichten. Was ist der Grund für die Erstellung einer Schnittstelle?
Lupurus

34

Eine weitere Alternative, wenn Sie Ihre Typdefinitionen und die Datenbankimplementierung trennen möchten.

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 von hier: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
Ist die mongoose.SchemaDefinition hier duplizieren Sie die Felder aus IUser? Angesichts der Tatsache, dass dies IUserin einer anderen Datei definiert ist , ist das Risiko, dass die Felder mit zunehmender Komplexität und Anzahl der Entwickler nicht mehr synchron sind , recht hoch.
Dan Dascalescu

Ja, dies ist ein gültiges Argument, das es wert ist, in Betracht gezogen zu werden. Die Verwendung von Komponentenintegrationstests kann jedoch dazu beitragen, die Risiken zu verringern. Beachten Sie, dass es Ansätze und Architekturen gibt, bei denen die Typdeklarationen und die DB-Implementierungen getrennt sind, unabhängig davon, ob dies über ein ORM (wie von Ihnen vorgeschlagen) oder manuell (wie in dieser Antwort) erfolgt. Es gibt keine Silberkugel ... <(°. °)>
Gábor Imre

Eine Möglichkeit könnte darin bestehen, Code aus der GraphQL-Definition für TypeScript und Mungo zu generieren .
Dan Dascalescu

23

Entschuldigung für die Nekropostierung, aber das kann für jemanden immer noch interessant sein. Ich denke, Typegoose bietet eine modernere und elegantere Möglichkeit, Modelle zu definieren

Hier ist ein Beispiel aus den Dokumenten:

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

Für ein vorhandenes Verbindungsszenario können Sie Folgendes verwenden (was in realen Situationen wahrscheinlicher und in den Dokumenten aufgedeckt sein kann):

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
Ich bin auch zu diesem Schluss gekommen, bin aber besorgt, dass typegoosees nicht genug Unterstützung gibt ... wenn ich ihre npm-Statistiken überprüfe, sind es nur 3k wöchentliche Downloads, und rn gibt es fast 100 offene Github-Probleme, von denen die meisten keine Kommentare haben. und einige davon
scheinen vor

@Corbfon Hast du es versucht? Wenn ja, was waren Ihre Ergebnisse? Wenn nicht, gab es noch etwas, das Sie dazu bewogen hat, es nicht zu verwenden? Ich sehe im Allgemeinen einige Leute, die sich Sorgen um die vollständige Unterstützung machen, aber es scheint, dass diejenigen, die sie tatsächlich nutzen, ziemlich zufrieden damit sind
N4ppeL

1
@ N4ppeL Ich würde nicht mit gehen typegoose- wir haben unsere Eingabe manuell erledigt , ähnlich wie in diesem Beitrag , es sieht so aus, als ob ts-mongoosees ein Versprechen geben könnte (wie in der späteren Antwort vorgeschlagen)
Corbfon

1
Entschuldigen Sie sich niemals für "Nekroposting". [Wie Sie jetzt wissen ...] Es gibt sogar ein Abzeichen (obwohl es wird Necromancer genannt ; ^ D) für ihn nur tun! Das Nekroposting neuer Informationen und Ideen wird empfohlen!
Ruffin

1
@ruffin: Ich verstehe auch nicht wirklich das Stigma, neue und aktuelle Lösungen für Probleme zu veröffentlichen.
Dan Dascalescu

16

Versuchen Sie es ts-mongoose. Für die Zuordnung werden bedingte Typen verwendet.

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

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

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

1
Sieht sehr vielversprechend aus! Danke für das Teilen! :)
Boriel

1
Beeindruckend. Dies sperrt sehr schlank. Ich freue mich darauf, es zu versuchen!
Qqilihq

1
Offenlegung: Ts-Mungo scheint vom Himmel geschaffen zu sein. Scheint die schickste Lösung da draußen zu sein.
Mikrofon

1
Schönes Paket, pflegen Sie es noch ?
Dan Dascalescu

11

Die meisten Antworten hier wiederholen die Felder in der TypeScript-Klasse / -Schnittstelle und im Mungoschema. Das Fehlen einer einzigen Wahrheitsquelle stellt ein Wartungsrisiko dar, da das Projekt komplexer wird und mehr Entwickler daran arbeiten: Es ist wahrscheinlicher, dass Felder nicht mehr synchron sind . Dies ist besonders schlimm, wenn sich die Klasse in einer anderen Datei befindet als das Mungoschema.

Um Felder synchron zu halten, ist es sinnvoll, sie einmal zu definieren. Es gibt einige Bibliotheken, die dies tun:

Ich war noch nicht vollständig von einem von ihnen überzeugt, aber typegoose scheint aktiv gepflegt zu sein, und der Entwickler akzeptierte meine PRs.

Um einen Schritt voraus zu sein: Wenn Sie dem Mix ein GraphQL-Schema hinzufügen, wird eine weitere Ebene der Modellduplizierung angezeigt. Eine Möglichkeit, dieses Problem zu lösen, besteht darin , TypeScript und Mungo-Code aus dem GraphQL-Schema zu generieren .


5

Hier ist eine stark typisierte Methode, um ein einfaches Modell mit einem Mungoschema abzugleichen. Der Compiler stellt sicher, dass die an mongoose.Schema übergebenen Definitionen mit der Schnittstelle übereinstimmen. Sobald Sie das Schema haben, können Sie es verwenden

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

Sobald Sie Ihr Schema haben, können Sie Methoden verwenden, die in anderen Antworten erwähnt werden, wie z

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

1
Dies ist die einzig richtige Antwort. Keine der anderen Antworten stellte tatsächlich die Typkompatibilität zwischen dem Schema und dem Typ / der Schnittstelle sicher.
Jamie Strauss


1
@ DanDascalescu Ich glaube nicht, dass Sie verstehen, wie Typen funktionieren.
Jamie Strauss

5

Fügen Sie einfach einen anderen Weg hinzu ( @types/mongoosemuss mit installiert werden 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,
    // ...
}));

Und der Unterschied zwischen interfaceund type, bitte lesen Sie diese Antwort

Auf diese Weise haben Sie den Vorteil, dass Sie statische Mungose-Methodentypen hinzufügen können:

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

wo haben Sie definieren generateJwt?
rels

1
@rels wird im const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));Grunde generateJwteine weitere Eigenschaft des Modells.
a11smiles

Würden Sie es einfach auf diese Weise als Methode hinzufügen oder mit der Methode properties verbinden?
user1790300

1
Dies sollte die akzeptierte Antwort sein, da Benutzerdefinition und Benutzer-DAL getrennt werden. Wenn Sie von Mongo zu einem anderen Datenbankanbieter wechseln möchten, müssen Sie die Benutzeroberfläche nicht ändern.
Rafael del Rio

1
@ RafaeldelRio: Die Frage betraf die Verwendung von Mungo mit TypeScript. Der Wechsel zu einer anderen Datenbank steht im Widerspruch zu diesem Ziel. Das Problem beim Trennen der Schemadefinition von der IUserSchnittstellendeklaration in einer anderen Datei besteht darin, dass das Risiko, dass Felder nicht mehr synchron sind, wenn das Projekt an Komplexität und Entwicklern zunimmt, sehr hoch ist.
Dan Dascalescu

4

So machen es die Mitarbeiter von Microsoft. Hier

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

Ich empfehle, dieses hervorragende Starterprojekt zu testen, wenn Sie TypeScript zu Ihrem Node-Projekt hinzufügen.

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


1
Dadurch wird jedes einzelne Feld zwischen Mungo und TypeScript dupliziert, wodurch ein Wartungsrisiko entsteht, wenn das Modell komplexer wird. Lösungen mögen ts-mongooseund typegooselösen dieses Problem, allerdings mit einigem syntaktischen Cruft.
Dan Dascalescu

2

Damit vscode intellisensefunktioniert auf beiden

  • Benutzertyp User.findOne
  • Benutzerinstanz u1._id

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


2

Hier ist das Beispiel aus der Mongoose-Dokumentation: Erstellen aus ES6-Klassen mit loadClass () , konvertiert 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');
})();

Für die statische findByFullNameMethode konnte ich nicht herausfinden, wie die Typinformationen abgerufen werden Person, daher musste ich sie umwandeln, <any>Personwenn ich sie aufrufen wollte. Wenn Sie wissen, wie Sie das beheben können, fügen Sie bitte einen Kommentar hinzu.


Wie andere Antworten dupliziert dieser Ansatz die Felder zwischen der Schnittstelle und dem Schema. Dies könnte vermieden werden, indem eine einzige Quelle der Wahrheit vorhanden ist, z . B. indem ts-mongooseoder verwendet wird typegoose. Die Situation wird beim Definieren des GraphQL-Schemas weiter dupliziert.
Dan Dascalescu

Gibt es eine Möglichkeit, Refs mit diesem Ansatz zu definieren?
Dan Dascalescu

2

Ich bin ein Fan von Plumier, es hat einen Mungo-Helfer , aber es kann ohne Plumier selbst verwendet werden . Im Gegensatz zu Typegoose hat es mithilfe der speziellen Reflexionsbibliothek von Plumier einen anderen Weg eingeschlagen, der es ermöglicht, coole Sachen zu verwenden.

Eigenschaften

  1. Reines POJO (Domäne muss weder von einer Klasse erben noch einen speziellen Datentyp verwenden), das automatisch erstellte Modell wird abgeleitet, T & Documentsodass auf dokumentbezogene Eigenschaften zugegriffen werden kann.
  2. Unterstützte TypeScript-Parametereigenschaften, es ist gut, wenn Sie haben strict:true Dies tsconfig-Konfiguration . Und mit Parametereigenschaften ist nicht für alle Eigenschaften ein Dekorator erforderlich.
  3. Unterstützte Feldeigenschaften wie Typegoose
  4. Die Konfiguration entspricht der von Mungo, sodass Sie sich leicht damit vertraut machen können.
  5. Unterstützte Vererbung, die die Programmierung natürlicher macht.
  6. Modellanalyse mit Angabe der Modellnamen und des entsprechenden Sammlungsnamens, der angewendeten Konfiguration usw.

Verwendung

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

Für alle, die nach einer Lösung für bestehende Mongoose-Projekte suchen:

Wir haben kürzlich Mungo-Tsgen gebaut , um dieses Problem zu beheben (würde mich über Feedback freuen !). Bestehende Lösungen wie typegoose erforderten ein Umschreiben unserer gesamten Schemata und führten zu verschiedenen Inkompatibilitäten. mongoose-tsgen ist ein einfaches CLI-Tool, das eine index.d.ts-Datei mit Typescript-Schnittstellen für alle Ihre Mongoose-Schemas generiert. Es erfordert wenig bis gar keine Konfiguration und lässt sich sehr reibungslos in jedes Typescript-Projekt integrieren.


1

Wenn Sie sicherstellen möchten, dass Ihr Schema dem Modelltyp entspricht und umgekehrt, bietet diese Lösung eine bessere Typisierung als von @bingles vorgeschlagen:

Die gängige Typdatei: ToSchema.ts(Keine Panik! Kopieren Sie sie einfach und fügen Sie sie ein.)

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

und ein Beispielmodell:

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

Hier ist ein Beispiel basierend auf der README-Datei für das @types/mongoosePaket.

Neben den oben bereits enthaltenen Elementen wird gezeigt, wie reguläre und statische Methoden eingeschlossen werden:

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

Im Allgemeinen scheint diese README eine fantastische Ressource für die Annäherung an Typen mit Mungo zu sein.


Dieser Ansatz dupliziert die Definition jedes Felds von IUserDocumentin nach UserSchema, was ein Wartungsrisiko darstellt, wenn das Modell komplexer wird. Pakete mögen ts-mongooseund typegooseversuchen, dieses Problem zu lösen, allerdings mit einigem syntaktischen Cruft.
Dan Dascalescu
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.