Мангуст в стиле машинописного текста…?


90

Попытка реализовать модель Mongoose в Typescript. Поиск в Google выявил только гибридный подход (объединение JS и TS). Как при моем довольно наивном подходе реализовать класс User без JS?

Хотите иметь возможность 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....
Луай Алаккад

1
В частности, в коде OP, не могли бы вы уточнить, почему Userне может быть классом?
Тим Макнамара

Вместо этого попробуйте github.com/typeorm/typeorm .
Эрих

@Erich, они говорят, что typeorm плохо работает с MongoDB, возможно, Type goose - хороший вариант
PayamBeirami

Проверьте это npmjs.com/package/@types/mongoose
Гарри

Ответы:


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?
Тим Макнамара

13
import * as mongoose from 'mongoose';илиimport mongoose = require('mongoose');
Луай Алаккад

1
Примерно так:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Луай Алаккад

3
Последняя строка (экспорт по умолчанию const User ...) у меня не работает. Мне нужно разделить строку, как предлагается в stackoverflow.com/questions/35821614/…
Серджио

7
Я могу обойтись let newUser = new User({ iAmNotHere: true })без ошибок в IDE или при компиляции. Так в чем же причина создания интерфейса?
Lupurus

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? Учитывая , что IUserопределен в другом файле с риском того, что поля с рассинхронизироваться как проект растет в сложности и количества разработчиков, достаточно высока.
Дэн Даскалеску,

Да, это веский аргумент, который стоит рассмотреть. Однако использование тестов интеграции компонентов может помочь снизить риски. И обратите внимание, что существуют подходы и архитектуры, в которых объявления типов и реализации БД разделены, независимо от того, выполняется ли это через ORM (как вы предложили) или вручную (как в этом ответе). Никакой серебряной пули ... <(°. °)>
Габор Имре

Один из пунктов может заключаться в генерации кода из определения GraphQL для TypeScript и mongoose.
Дэн Даскалеску,

23

Извините за некропостинг, но это может быть кому-то интересно. Я думаю, что 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, это всего 3 тыс. Загрузок в неделю, а на Github почти 100 открытых проблем, большинство из которых без комментариев, и некоторые из них, похоже, должны были быть закрыты давным-давно
Corbfon

@Corbfon Вы пробовали? Если да, то каковы были ваши выводы? Если нет, было ли что-то еще, что заставило вас отказаться от него? Я обычно вижу, что некоторые люди беспокоятся о полной поддержке, но похоже, что те, кто на самом деле ее используют, вполне ей довольны
N4ppeL

1
@ N4ppeL Я бы не стал typegoose- мы закончили тем, что вручную обработали наш набор текста, похоже на этот пост , похоже, ts-mongooseможет быть какое-то обещание (как предлагается в последующем ответе)
Корбфон

1
Никогда не извиняйтесь за «некропостинг». [Как вы теперь знаете ...] Есть даже значок (хотя он называется Necromancer ; ^ D) именно за это! Некропостинг новой информации и идей приветствуется!
ruffin

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

1
Вау. Это замки очень гладкие. С нетерпением жду возможности попробовать!
qqilihq

1
Раскрытие: кажется, ц-мангуст был создан небом. Кажется, это самое изящное решение.
mic


11

Большинство ответов здесь повторяют поля в классе / интерфейсе TypeScript и в схеме мангуста. Отсутствие единого источника истины представляет собой риск обслуживания, поскольку проект становится все более сложным и над ним работают все больше разработчиков: поля с большей вероятностью рассинхронизируются . Это особенно плохо, когда класс находится в другом файле, а не в схеме мангуста.

Чтобы поля синхронизировались, имеет смысл определить их один раз. Для этого есть несколько библиотек:

Я еще не был полностью убежден ни в одном из них, но typegoose, похоже, активно поддерживается, и разработчик принял мои PR.

Чтобы подумать на шаг впереди: когда вы добавляете схему GraphQL в микс, появляется еще один уровень дублирования модели. Одним из способов решения этой проблемы может быть создание кода TypeScript и мангуста из схемы GraphQL.


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
Это единственно правильный ответ. Ни один из других ответов на самом деле не гарантировал совместимость типов между схемой и типом / интерфейсом.
Джейми Штраус,

@JamieStrauss: как насчет того, чтобы вообще не дублировать поля ?
Дэн Даскалеску

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

1
@rels, по const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));сути, generateJwtстановится еще одним свойством модели.
a11smiles

Вы бы просто добавили его как метод таким образом или подключили бы к свойству method?
user1790300

1
Это должен быть принятый ответ, поскольку он отделяет определение пользователя и пользовательский DAL. Если вы хотите переключиться с mongo на другого поставщика db, вам не придется менять пользовательский интерфейс.
Рафаэль дель Рио

1
@RafaeldelRio: вопрос касался использования мангуста с TypeScript. Переход на другую БД противоречит этой цели. И проблема с отделением определения схемы от IUserобъявления интерфейса в другом файле заключается в том, что риск рассинхронизации полей по мере роста сложности проекта и количества разработчиков довольно высок.
Дэн Даскалеску,

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

Я рекомендую проверить этот отличный стартовый проект, когда вы добавите TypeScript в свой проект Node.

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


1
Это дублирует каждое поле между mongoose и TypeScript, что создает риск обслуживания по мере усложнения модели. Решения любят ts-mongooseи typegooseрешают эту проблему, хотя, по общему признанию, содержат довольно много синтаксической неразберихи.
Дэн Даскалеску

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, Создание из классов ES6 с использованием 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.
Дэн Даскалеску,

Есть ли способ определить ссылки с помощью этого подхода?
Дэн Даскалеску

1

Я поклонник 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:

Недавно мы создали mongoose-tsgen для решения этой проблемы (хотелось бы получить отзывы!). Существующие решения, такие как typegoose, требовали переписывания всех наших схем и внесения различных несовместимостей. mongoose-tsgen - это простой инструмент CLI, который генерирует файл index.d.ts, содержащий интерфейсы Typescript для всех ваших схем Mongoose; он практически не требует настройки и очень легко интегрируется с любым проектом TypeScript.


0

Вот пример, основанный на README для @types/mongooseпакета.

Помимо элементов, уже включенных выше, он показывает, как включать обычные и статические методы:

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пытаются решить эту проблему, хотя, по общему признанию, имеют довольно много синтаксических ошибок.
Дэн Даскалеску,

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.