Typescriptの方法でマングース…?


93

Typescriptでマングースモデルを実装しようとしています。Googleを精査すると、ハイブリッドアプローチ(JSとTSの組み合わせ)のみが明らかになりました。JSを使用せずに、私のかなり単純なアプローチでUserクラスを実装するにはどうすればよいでしょうか。

手荷物なしでIUserModelできるようにしたい。

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

Userクラスの作成は非同期操作であるため、クラスにすることはできません。それはあなたが電話しなければならないので約束を返さなければなりませんUser.create({...}).then...
Louay Alakkad 2015

1
具体的には、OPのコードに記載されているUserように、クラスになれない理由について詳しく教えてください。
ティムマクナマラ2015

代わりにgithub.com/typeorm/typeormを試してください。
Erich

彼らはそのtypeormはMongoDBのでうまく動作しない、多分ガチョウを入力することは良い選択肢であると言う@Erich
PayamBeirami

これをチェックしてくださいnpmjs.com/package/@types/mongoose–
ハリー

回答:


132

これが私がそれをする方法です:

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
申し訳ありませんが、TSでは「マングース」はどのように定義されていますか?
ティムマクナマラ2015

13
import * as mongoose from 'mongoose';またはimport mongoose = require('mongoose');
Louay Alakkad 2015

1
このようなもの:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad 2016年

3
最後の行(デフォルトのconstユーザーをエクスポート...)が機能しません。stackoverflow.com/questions/35821614/で
Sergio

7
let newUser = new User({ iAmNotHere: true })IDEやコンパイル時にエラーなしで実行できます。では、インターフェースを作成する理由は何ですか?
lupurus 2017年

34

タイプ定義とデータベース実装を切り離したい場合の別の方法。

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

ここからのインスピレーション:https//github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
mongoose.Schemaここでの定義はからのフィールドを複製しますIUserか?それを考えるIUserに定義されている別のファイルを持つフィールドが同期してもらうというリスクプロジェクトは複雑さと開発者の数で育つように、非常に高いです。
ダンダスカレスク

はい、これは検討する価値のある有効な議論です。ただし、コンポーネント統合テストを使用すると、リスクを軽減できる場合があります。また、型宣言とDB実装が、ORM(提案どおり)または手動(この回答のように)で行われるかどうかに関係なく分離されるアプローチとアーキテクチャーがあることに注意してください。銀の弾などないがある... <(゜。)>
ガーボルイムレ

1つの弾丸は、TypeScriptとmongooseのGraphQL定義からコード生成することかもしれません。
ダンダスカレスク

24

ネクロポストして申し訳ありませんが、これはまだ誰かにとって興味深いものになる可能性があります。Typegooseは、モデルを定義するためのよりモダンでエレガントな方法を提供すると思います

ドキュメントの例を次に示します。

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

既存の接続シナリオの場合、次のように使用できます(実際の状況ではより可能性が高く、ドキュメントで明らかにされている可能性があります)。

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
私もこの結論に達しましたが、typegoose十分なサポートがないのではないかと心配しています... npm統計を確認すると、毎週3kのダウンロードしかなく、Githubの未解決の問題が100近くあり、そのほとんどにコメントがありません。そして、彼らは長い時間前に閉鎖されている必要がありますように見えるそのうちのいくつかは
Corbfon

@Corbfonやってみましたか?もしそうなら、あなたの発見は何でしたか?そうでない場合、それを使用しないことに決めた他の何かがありましたか?私は一般的に何人かの人々は完全なサポートを心配見るが、実際にそれを利用する人は、それにはかなり満足しているようだ
N4ppeL

1
@ N4ppeL私は行くだろうtypegoose-私たちは手動に似て私たちのタイピング、取扱い終わったこのポストを、それは次のようになりますts-mongoose(後の答えで提案されているように)いくつかの約束を持っているかもしれない
Corbfon

1
「ネクロポスティング」について決して謝罪しないでください。[あなたが今知っているように...]でもバッジあります(けれどもそれがされネクロマンサーという名前だけでこれを行うために、^ D)を!新しい情報やアイデアをネクロポストすることをお勧めします!
ラフィン

1
@ruffin:私はまた、問題に対する新しい最新の解決策を投稿することに対する汚名を本当に理解していません。
ダンダスカレスク

16

試してみてくださいts-mongoose。条件付きタイプを使用してマッピングを行います。

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

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

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

1
非常に有望に見えます!共有してくれてありがとう!:)
Boriel 2018

1
ワオ。これは非常に滑らかにロックします。ぜひお試しください!
qqilihq

1
開示:ts-マングースは空によって作成されているようです。そこにある最も巧妙な解決策のようです。
マイク

1
素敵なパッケージ、あなたはまだそれを維持していますか?
ダンダスカレスク

11

ここでのほとんどの回答は、TypeScriptクラス/インターフェイスおよびマングーススキーマのフィールドを繰り返します。プロジェクトがより複雑になり、より多くの開発者がプロ​​ジェクトに取り組むため、信頼できる唯一の情報源がないことは、メンテナンスのリスクを意味します。フィールドが同期しなくなる可能性が高くなります。これは、クラスがマングーススキーマとは異なるファイルにある場合に特に問題になります。

フィールドの同期を維持するには、フィールドを一度定義するのが理にかなっています。これを行うライブラリがいくつかあります。

私はまだそれらのどれにも完全に納得していませんが、typegooseは積極的に維持されているようで、開発者は私のPRを受け入れました。

一歩先を考えると、GraphQLスキーマをミックスに追加すると、モデル複製の別のレイヤーが表示されます。この問題を克服する1つの方法は、GraphQLスキーマからTypeScriptとマングースのコード生成することです。


5

これは、プレーンモデルをマングーススキーマと一致させるための強い型付きの方法です。コンパイラーは、mongoose.Schemaに渡された定義がインターフェースと一致することを確認します。スキーマを取得したら、次を使用できます

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

スキーマを取得したら、次のような他の回答に記載されている方法を使用できます。

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

1
これが唯一の正解です。他のどの答えも、スキーマとタイプ/インターフェースの間のタイプの互換性を実際に保証していませんでした。
ジェイミーシュトラウス


1
@DanDascalescu型がどのように機能するか理解していないと思います。
ジェイミーシュトラウス

5

別の方法を追加するだけです(@types/mongooseでインストールする必要があります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,
    // ...
}));

そしてとの違いはinterfaceこの答えtypeを読んでください

この方法には利点があり、Mongooseの静的メソッド型を追加できます。

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

どこで定義しましたgenerateJwtか?
rels 2016

1
@relsはconst User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));基本的generateJwtに、モデルの別のプロパティになります。
a11smiles 2017

この方法でメソッドとして追加するだけですか、それともメソッドプロパティに接続しますか?
user1790300 2017

1
これは、ユーザー定義とユーザーDALを切り離すため、受け入れられる答えであるはずです。mongoから別のdbプロバイダーに切り替えたい場合は、ユーザーインターフェイスを変更する必要はありません。
ラファエルデルリオ

1
@RafaeldelRio:質問はTypeScriptでマングースを使用することについてでした。別のDBに切り替えることは、この目標とは正反対です。また、スキーマ定義を別のファイルのIUserインターフェイス宣言から分離する際の問題は、プロジェクトの複雑さと開発者の数が増えるにつれて、フィールドが同期なくなるリスクが非常に高いことです。
ダンダスカレスク

4

これがマイクロソフトの人たちのやり方です。ここに

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

TypeScriptをNodeプロジェクトに追加するときは、この優れたスタータープロジェクトを確認することをお勧めします。

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


1
これにより、マングースとTypeScriptの間のすべてのフィールドが複製され、モデルがより複雑になるにつれてメンテナンスのリスクが生じます。ソリューションは好きts-mongoosetypegoose構文嫌なもののかなりのビットにかかわらず、確かに、その問題を解決します。
ダンダスカレスク

2

これvscode intellisenseで両方で動作します

  • ユーザータイプ User.findOne
  • ユーザーインスタンス u1._id

コード:

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

これは、Mongooseのドキュメントの例です。loadClass()を使用してES6クラスから作成し、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');
})();

静的findByFullNameメソッドの場合、型情報を取得する方法がわからなかったため、呼び出したいときにPersonキャスト<any>Personする必要がありました。それを修正する方法を知っている場合は、コメントを追加してください。


の回答と同様に、このアプローチでは、インターフェイスとスキーマの間でフィールドが複製されます。これは、ts-mongooseまたはを使用するなど、信頼できる唯一の情報源を持つことで回避できますtypegoose。GraphQLスキーマを定義すると、状況がさらに重複します。
ダンダスカレスク

このアプローチで参照を定義する方法はありますか?
ダンダスカレスク

2

私はPlumierのファンで、マングースヘルパーがありますがPlumier自体がなくてもスタンドアロンで使用できます。Typegooseとは異なり、Plumierの専用リフレクションライブラリを使用することで異なる道を歩み、クールなものを使用できるようになりました。

特徴

  1. 純粋なPOJO(ドメインはクラスから継承する必要も、特別なデータ型を使用する必要もありません)、T & Documentドキュメント関連のプロパティにアクセスできるため、自動的に推測されて作成されたモデル。
  2. サポートされているTypeScriptパラメータープロパティstrict:true。tsconfig構成がある場合に適しています。また、パラメータプロパティを使用すると、すべてのプロパティにデコレータが必要になるわけではありません。
  3. Typegooseなどのサポートされているフィールドプロパティ
  4. 構成はマングースと同じなので、簡単に慣れることができます。
  5. プログラミングをより自然にするサポートされた継承。
  6. モデル分析、モデル名とその適切なコレクション名、適用された構成などを示します。

使用法

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

既存のマングースプロジェクトの解決策を探している人のために:

最近、この問題に対処するためにmongoose-tsgen作成しました(フィードバックをお待ちしています!)。typegooseのような既存のソリューションでは、スキーマ全体を書き直す必要があり、さまざまな非互換性が導入されました。mongoose-tsgenは、すべてのMongooseスキーマのTypescriptインターフェイスを含むindex.d.tsファイルを生成するシンプルなCLIツールです。構成はほとんどまたはまったく必要なく、Typescriptプロジェクトと非常にスムーズに統合されます。


1

スキーマがモデルタイプを満たしていることを確認したい場合、またはその逆の場合、このソリューションは@binglesが提案したものよりも優れたタイピングを提供します。

一般的なタイプのファイル:( ToSchema.ts慌てる必要はありません!コピーして貼り付けるだけです)

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

およびモデルの例:

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

これは、@types/mongooseパッケージのREADMEに基づく例です。

上記ですでに含まれている要素に加えて、通常のメソッドと静的メソッドを含める方法を示しています。

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

一般に、このREADMEは、マングースでタイプにアプローチするための素晴らしいリソースのようです。


このアプローチでは、からIUserDocumentへのすべてのフィールドの定義が複製さUserSchemaれ、モデルがより複雑になるにつれてメンテナンスリスクが発生します。以下のようなパッケージts-mongoosetypegoose構文嫌なもののかなりのビットを明らかにかかわらず、その問題を解決しようとします。
ダンダスカレスク

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.