Mongoose dengan cara Ketikan ...?


90

Mencoba mengimplementasikan model Mongoose di Typecript. Menjelajahi Google hanya mengungkapkan pendekatan hibrid (menggabungkan JS dan TS). Bagaimana cara menerapkan kelas User, dengan pendekatan saya yang agak naif, tanpa JS?

Ingin dapat IUserModel tanpa bagasi.

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

Usertidak bisa menjadi kelas karena membuatnya adalah operasi asinkron. Itu harus membalas janji jadi Anda harus menelepon User.create({...}).then....
Louay Alakkad

1
Secara khusus, diberikan dalam kode di OP, dapatkah Anda menjelaskan mengapa Usertidak bisa menjadi kelas?
Tim McNamara

Coba github.com/typeorm/typeorm sebagai gantinya.
Erich

@Erich mereka mengatakan bahwa typeorm tidak bekerja dengan baik dengan MongoDB, mungkin Angsa jenis adalah pilihan yang baik
PayamBeirami

Jawaban:


130

Begini cara saya melakukannya:

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
maaf, tapi bagaimana 'luwak' didefinisikan di TS?
Tim McNamara

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

1
Sesuatu seperti ini:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
Baris terakhir (ekspor default const User ...) tidak berfungsi untuk saya. Saya perlu membagi garis, seperti yang diusulkan di stackoverflow.com/questions/35821614/…
Sergio

7
Saya dapat melakukannya let newUser = new User({ iAmNotHere: true })tanpa kesalahan dalam IDE atau saat kompilasi. Jadi apa alasan untuk membuat antarmuka?
Lupurus

33

Alternatif lain jika Anda ingin melepaskan definisi tipe dan implementasi 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 */
}));

Inspirasi dari sini: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
Apakah mongoose.Schemadefinisi di sini menduplikasi bidang dari IUser? Mengingat itu IUserdidefinisikan dalam file yang berbeda , risiko bidang yang tidak sinkron seiring dengan pertumbuhan proyek dalam kompleksitas dan jumlah pengembang, cukup tinggi.
Dan Dascalescu

Ya, ini adalah argumen valid yang patut dipertimbangkan. Menggunakan pengujian integrasi komponen dapat membantu mengurangi risiko. Dan perhatikan bahwa ada pendekatan dan arsitektur di mana deklarasi tipe dan implementasi DB dipisahkan apakah itu dilakukan melalui ORM (seperti yang Anda usulkan) atau secara manual (seperti dalam jawaban ini). Tidak ada peluru perak di sana ... <(°. °)>
Gábor Imre

Satu peluru mungkin menghasilkan kode dari definisi GraphQL, untuk TypeScript dan luwak.
Dan Dascalescu

23

Maaf untuk necroposting tapi ini bisa tetap menarik untuk seseorang. Saya pikir Typegoose memberikan cara yang lebih modern dan elegan untuk menentukan model

Berikut ini contoh dari dokumen:

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

Untuk skenario koneksi yang sudah ada, Anda dapat menggunakan sebagai berikut (yang mungkin lebih mungkin terjadi dalam situasi nyata dan ditemukan di dokumen):

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
Saya juga sampai pada kesimpulan ini, tetapi khawatir itu typegoosetidak memiliki cukup dukungan ... memeriksa statistik npm mereka, itu hanya unduhan mingguan 3k, dan ada hampir 100 masalah Github terbuka, sebagian besar tidak memiliki komentar, dan beberapa di antaranya sepertinya sudah lama ditutup
Corbfon

@Corbfon Apakah Anda mencobanya? Jika ya, apa temuan Anda? Jika tidak, apakah ada hal lain yang membuat Anda memutuskan untuk tidak menggunakannya? Saya biasanya melihat beberapa orang mengkhawatirkan dukungan penuh, tetapi tampaknya mereka yang benar-benar menggunakannya cukup senang dengannya
N4ppeL

1
@ N4ppeL Saya tidak akan pergi dengan typegoose- kami akhirnya menangani pengetikan kami secara manual, mirip dengan posting ini , sepertinya ts-mongoosemungkin ada beberapa janji (seperti yang disarankan dalam jawaban nanti)
Corbfon

1
Jangan pernah meminta maaf untuk "necroposting". [Seperti yang Anda tahu ...] Bahkan ada lencana (meskipun itu adalah bernama Necromancer ; ^ D) untuk melakukan hal ini! Mendorong necroposting informasi dan ide baru!
ruffin

1
@ruffin: Saya juga benar-benar tidak mengerti stigma terhadap posting solusi baru dan terkini untuk masalah.
Dan Dascalescu

16

Coba ts-mongoose. Ini menggunakan tipe kondisional untuk melakukan pemetaan.

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

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

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

1
Terlihat sangat menjanjikan! Terima kasih telah berbagi! :)
Boriel

1
Wow. Kunci ini sangat rapi. Berharap untuk mencobanya!
qqilihq

1
Pengungkapan: ts-luwak tampaknya diciptakan oleh langit. Tampaknya menjadi solusi paling licin di luar sana.
mic


11

Sebagian besar jawaban di sini mengulangi bidang di kelas / antarmuka TypeScript, dan di skema luwak. Tidak memiliki satu sumber kebenaran mewakili risiko pemeliharaan, karena proyek menjadi lebih kompleks dan lebih banyak pengembang yang mengerjakannya: bidang lebih mungkin tidak sinkron . Ini sangat buruk ketika kelas berada dalam file yang berbeda vs. skema luwak.

Untuk menjaga bidang tetap sinkron, masuk akal untuk menetapkannya sekali. Ada beberapa perpustakaan yang melakukan ini:

Saya belum sepenuhnya yakin dengan salah satu dari mereka tetapi tipikal kambing tampaknya dipertahankan secara aktif, dan pengembang menerima PR saya.

Untuk berpikir selangkah lebih maju: ketika Anda menambahkan skema GraphQL ke dalam campuran, lapisan duplikasi model lain muncul. Salah satu cara untuk mengatasi masalah ini mungkin dengan menghasilkan TypeScript dan kode luwak dari skema GraphQL.


5

Berikut adalah cara mengetik yang kuat untuk mencocokkan model biasa dengan skema luwak. Kompilator akan memastikan definisi yang diteruskan ke mongoose.Schema cocok dengan antarmuka. Setelah Anda memiliki skema, Anda dapat menggunakan

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

Setelah Anda memiliki skema, Anda dapat menggunakan metode yang disebutkan dalam jawaban lain seperti

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

1
Ini satu-satunya jawaban yang benar. Tak satu pun dari jawaban lain yang benar-benar memastikan kesesuaian tipe antara skema dan tipe / antarmuka.
Jamie Strauss


1
@DanDascalescu Saya rasa Anda tidak memahami cara kerja tipe.
Jamie Strauss

5

Cukup tambahkan cara lain ( @types/mongooseharus dipasang dengan 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,
    // ...
}));

Dan perbedaan antara interfacedan type, harap baca jawaban ini

Cara ini memiliki keuntungan, Anda dapat menambahkan pengetikan metode statis Mongoose:

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

dimana anda mendefinisikan generateJwt?
rels

1
@rels const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));pada dasarnya, generateJwtmenjadi properti lain dari model.
a11iles

Apakah Anda akan menambahkannya sebagai metode dengan cara ini atau akankah Anda menghubungkannya ke properti metode?
pengguna1790300

1
Ini harus menjadi jawaban yang diterima karena melepaskan definisi pengguna dan DAL pengguna. Jika Anda ingin beralih dari mongo ke penyedia db lain, Anda tidak perlu mengubah antarmuka pengguna.
Rafael del Rio

1
@RafaeldelRio: pertanyaannya adalah tentang menggunakan luwak dengan TypeScript. Beralih ke DB lain bertentangan dengan tujuan ini. Dan masalah dengan memisahkan definisi skema dari IUserdeklarasi antarmuka dalam file yang berbeda adalah bahwa risiko bidang tidak sinkron seiring pertumbuhan proyek dalam jumlah kompleksitas dan pengembang, cukup tinggi.
Dan Dascalescu

4

Inilah cara orang-orang di Microsoft melakukannya. sini

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

Saya sarankan untuk memeriksa proyek pemula yang luar biasa ini ketika Anda menambahkan TypeScript ke proyek Node Anda.

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


1
Itu menduplikasi setiap bidang antara luwak dan TypeScript, yang menciptakan risiko pemeliharaan karena model menjadi lebih kompleks. Solusi menyukai ts-mongoosedan typegoosememecahkan masalah itu, meskipun diakui dengan sedikit sintaksis cruft.
Dan Dascalescu

2

Dengan ini vscode intellisensebekerja pada keduanya

  • Jenis Pengguna User.findOne
  • instance pengguna u1._id

Kode:

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

Berikut ini contoh dari dokumentasi Mongoose, Membuat dari ES6 Classes Using loadClass () , dikonversi ke 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');
})();

Untuk findByFullNamemetode statis , saya tidak tahu bagaimana mendapatkan informasi tipe Person, jadi saya harus melakukan cast <any>Personketika saya ingin memanggilnya. Jika Anda tahu cara memperbaikinya, silakan tambahkan komentar.


Seperti jawaban lain , pendekatan ini menduplikasi bidang antara antarmuka dan skema. Itu bisa dihindari dengan memiliki satu sumber kebenaran, misalnya dengan menggunakan ts-mongooseatau typegoose. Situasi ini semakin terduplikasi saat mendefinisikan skema GraphQL.
Dan Dascalescu

Adakah cara untuk mendefinisikan ref dengan pendekatan ini?
Dan Dascalescu

1

Saya penggemar Plumier, ia memiliki penolong luwak , tetapi dapat digunakan secara mandiri tanpa Plumier itu sendiri . Tidak seperti Typegoose yang mengambil jalur berbeda dengan menggunakan perpustakaan refleksi khusus Plumier, yang memungkinkan untuk menggunakan hal-hal keren.

fitur

  1. POJO murni (domain tidak perlu mewarisi dari kelas apa pun, atau menggunakan tipe data khusus apa pun), Model dibuat secara otomatis disimpulkan T & Documentsehingga memungkinkan untuk mengakses properti terkait dokumen.
  2. Properti parameter TypeScript yang didukung, ada baiknya jika Anda memiliki strict:truekonfigurasi tsconfig. Dan dengan properti parameter tidak membutuhkan dekorator pada semua properti.
  3. Properti bidang yang didukung seperti Typegoose
  4. Konfigurasinya sama dengan luwak sehingga Anda akan dengan mudah terbiasa dengannya.
  5. Warisan yang didukung yang membuat pemrograman lebih alami.
  6. Analisis model, menunjukkan nama model dan nama koleksi yang sesuai, konfigurasi yang diterapkan, dll.

Pemakaian

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

Untuk siapa saja yang mencari solusi untuk proyek Mongoose yang sudah ada:

Kami baru-baru ini membuat luwak-tsgen untuk mengatasi masalah ini (kami akan sangat menyukai umpan balik!). Solusi yang ada seperti typegoose mengharuskan penulisan ulang seluruh skema kami dan menimbulkan berbagai ketidakcocokan. mongoose-tsgen adalah alat CLI sederhana yang menghasilkan file index.d.ts yang berisi antarmuka Typecript untuk semua skema Mongoose Anda; itu membutuhkan sedikit atau tanpa konfigurasi dan terintegrasi dengan sangat lancar dengan proyek Typecript.


0

Berikut adalah contoh berdasarkan README untuk @types/mongoosepaket tersebut.

Selain elemen yang sudah disertakan di atas, ini menunjukkan bagaimana memasukkan metode reguler dan statis:

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

Secara umum, README ini tampaknya menjadi sumber yang bagus untuk mendekati tipe dengan luwak.


Pendekatan ini menduplikasi definisi setiap bidang dari IUserDocumentke dalam UserSchema, yang menciptakan risiko pemeliharaan karena model menjadi lebih kompleks. Paket suka ts-mongoosedan typegoosemencoba memecahkan masalah itu, meskipun diakui dengan sedikit kesalahan sintaksis.
Dan Dascalescu

0

Jika Anda ingin memastikan bahwa skema Anda memenuhi jenis model dan sebaliknya, solusi ini menawarkan pengetikan yang lebih baik daripada yang disarankan @bingles:

Jenis file yang umum: ToSchema.ts(Jangan panik! Cukup salin dan tempel)

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

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


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.