Typescript 방식의 몽구스…?


90

Typescript에서 Mongoose 모델을 구현하려고합니다. 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시겠습니까?
Tim McNamara 2015

대신 github.com/typeorm/typeorm 을 사용해보십시오 .
Erich

@Erich 그들은 typeorm이 MongoDB와 잘 작동하지 않는다고 말합니다. 아마도 Type goose가 좋은 옵션 일 것입니다
PayamBeirami

답변:


130

방법은 다음과 같습니다.

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에서 '몽구스'는 어떻게 정의됩니까?
Tim McNamara 2015

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

1
이와 같은 것 :import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
마지막 줄 (기본 const 사용자 내보내기 ...)이 작동하지 않습니다. stackoverflow.com/questions/35821614/…
Sergio

7
let newUser = new User({ iAmNotHere: true })IDE 또는 컴파일시 오류없이 할 수 있습니다 . 그렇다면 인터페이스를 만드는 이유는 무엇입니까?
Lupurus 2017

33

유형 정의 및 데이터베이스 구현을 분리하려는 경우 또 다른 대안입니다.

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 ? 그 감안할 때 IUserA의 정의 된 다른 파일 필드와 동기화 밖으로 얻을 위험 이 프로젝트는 복잡성과 개발자의 수의 증가에 따라이 매우 높다.
Dan Dascalescu

예, 이것은 고려할 가치가있는 유효한 주장입니다. 구성 요소 통합 테스트를 사용하면 위험을 줄이는 데 도움이 될 수 있습니다. 그리고 ORM (제안한대로) 또는 수동 (이 답변과 같이)을 통해 수행되는지 여부에 관계없이 유형 선언과 DB 구현이 분리되는 접근 방식과 아키텍처가 있습니다. 은색 총알이 없습니다 ... <(°. °)>
Gábor Imre

한 가지 핵심은 TypeScript 및 mongoose에 대해 GraphQL 정의에서 코드생성 하는 것입니다.
Dan Dascalescu

23

necroposting에 대해 죄송하지만 누군가에게는 여전히 흥미로울 수 있습니다. 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지원이 충분하지 않다는 점이 걱정 됩니다. 어느 외모 중 일부는 오래 전에 폐쇄되어 있어야처럼
Corbfon

@Corbfon 시도 했습니까? 만약 그렇다면, 당신의 결과는 무엇입니까? 그렇지 않다면 사용하지 않기로 결정한 다른 이유가 있습니까? 나는 일반적으로 완전한 지원에 대해 걱정하는 일부 사람들을 봅니다. 그러나 실제로 그것을 사용하는 사람들은 그것에 상당히 만족하는 것 같습니다
N4ppeL

1
@ N4ppeL 나는 함께하지 않을 것입니다 typegoose-우리는 이 게시물 과 유사하게 수동으로 타이핑을 처리 했습니다. ts-mongoose(나중의 답변에서 제안한 것처럼) 약속이있을 수 있습니다.
Corbfon

1
"네크로 포스팅"에 대해 사과하지 마십시오. [당신은 지금 알다시피 ...] 심지어 배지있다 (비록 그것이 한다 네크로맨서 이름이 바로이 일을 위해 ^ D)를! 새로운 정보와 아이디어를 네크로 포스팅하는 것이 좋습니다!
ruffin

1
@ruffin : 또한 문제에 대한 새롭고 최신 솔루션을 게시하는 것에 대한 오명을 정말로 이해하지 못합니다.
Dan Dascalescu

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 dec

1
와. 이것은 매우 매끄 럽습니다. 그것을 시도하기를 기대합니다!
qqilihq

1
공개 : ts-mongoose는 하늘이 만든 것 같습니다. 가장 매끄러운 솔루션 인 것 같습니다.
마이크

1
좋은 패키지, 아직 유지하고 있습니까?
Dan Dascalescu

11

여기에서 대부분의 답변은 TypeScript 클래스 / 인터페이스 및 몽구스 스키마의 필드를 반복합니다. 단일 소스 소스가 없다는 것은 프로젝트가 더 복잡해지고 더 많은 개발자가 작업하기 때문에 유지 관리 위험을 나타냅니다. 필드가 동기화되지 않을 가능성이 더 큽니다 . 이것은 클래스가 몽구스 스키마와 다른 파일에있을 때 특히 나쁩니다.

필드를 동기화 상태로 유지하려면 필드를 한 번 정의하는 것이 좋습니다. 이를 수행하는 몇 가지 라이브러리가 있습니다.

나는 아직 그들 중 누구에게도 완전히 확신하지 못했지만 typegoose는 적극적으로 유지되는 것처럼 보이며 개발자는 내 PR을 수락했습니다.

한발 앞서 생각하기 : GraphQL 스키마를 믹스에 추가하면 다른 계층의 모델 복제가 나타납니다. 이 문제를 극복하는 한 가지 방법 은 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
이것은 유일한 정답입니다. 다른 답변은 실제로 스키마와 유형 / 인터페이스 간의 유형 호환성을 보장하지 않았습니다.
Jamie Strauss


1
@DanDascalescu 유형이 작동하는 방식을 이해하지 못한다고 생각합니다.
Jamie Strauss

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

그리고 사이의 차이 interfacetype, 읽어 보시기 바랍니다 이 답변

이 방법은 장점이 있습니다. Mongoose 정적 메서드 입력을 추가 할 수 있습니다.

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

당신은 어디에 정의 generateJwt했습니까?
rels

1
@rels는 const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));기본적으로 generateJwt모델의 또 다른 속성이됩니다.
a11smiles

이 방식으로 메소드로 추가 하시겠습니까, 아니면 methods 속성에 연결 하시겠습니까?
user1790300 jul.

1
이것은 사용자 정의와 사용자 DAL을 분리하므로 허용되는 대답이어야합니다. mongo에서 다른 db 공급자로 전환하려는 경우 사용자 인터페이스를 변경할 필요가 없습니다.
Rafael del Rio

1
@RafaeldelRio : 질문은 TypeScript와 함께 mongoose를 사용하는 것에 관한 것이 었습니다. 다른 DB로 전환하는 것은이 목표에 반대됩니다. 그리고에서 스키마 정의 분리의 문제 IUser인터페이스 선언을 다른 파일에이 이다 필드의 리스크가 동기화 점점 프로젝트의 복잡성과 개발자의 수가 증가함에 따라, 매우 높다.
Dan Dascalescu

4

다음은 Microsoft 직원들이하는 방법입니다. 여기

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

Node 프로젝트에 TypeScript를 추가 할 때이 훌륭한 시작 프로젝트를 확인하는 것이 좋습니다.

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


1
이는 mongoose와 TypeScript 사이의 모든 단일 필드를 복제하므로 모델이 더 복잡 해짐에 따라 유지 관리 위험이 발생합니다. 솔루션은 그 문제를 좋아 ts-mongoose하고 typegoose해결합니다. 물론 약간의 구문이 복잡해집니다.
Dan Dascalescu

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 설명서의 Create from ES6 Classes Using loadClass () 를 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 스키마를 정의 할 때 상황이 더욱 중복됩니다.
Dan Dascalescu

이 접근 방식으로 심판을 정의하는 방법은 무엇입니까?
Dan Dascalescu

1

나는 Plumier의 팬이며 몽구스 도우미있지만 Plumier 자체없이 독립형으로 사용할 수 있습니다. . Typegoose와 달리 Plumier의 전용 리플렉션 라이브러리를 사용하여 다른 경로를 사용하여 멋진 물건을 사용할 수 있습니다.

풍모

  1. 순수 POJO (도메인은 클래스에서 상속 할 필요가 없으며 특수 데이터 유형을 사용할 필요도 없음), 모델은 T & Document문서 관련 속성에 액세스 할 수있는 것으로 자동으로 추론됩니다 .
  2. 지원되는 TypeScript 매개 변수 속성입니다. strict:true tsconfig 구성 . 그리고 매개 변수 속성을 사용하면 모든 속성에 데코레이터가 필요하지 않습니다.
  3. Typegoose와 같은 지원되는 필드 속성
  4. 구성은 mongoose와 동일하므로 쉽게 익힐 수 있습니다.
  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 프로젝트에 대한 솔루션을 찾고있는 사람 :

우리는 최근 이 문제를 해결하기 위해 mongoose-tsgen 을 구축했습니다 . typegoose와 같은 기존 솔루션은 전체 스키마를 다시 작성해야하고 다양한 비 호환성을 도입했습니다. mongoose-tsgen 은 모든 Mongoose 스키마에 대한 Typescript 인터페이스를 포함하는 index.d.ts 파일을 생성하는 간단한 CLI 도구입니다. 구성이 거의 필요하지 않으며 Typescript 프로젝트와 매우 원활하게 통합됩니다.


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-mongoose하고 typegoose해결하려고 시도하지만, 약간의 구문이 복잡하다는 것은 인정합니다.
Dan Dascalescu

0

스키마가 모델 유형을 충족하는지 또는 그 반대인지 확인하려는 경우이 솔루션은 @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);


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.