Мангустувати машинописним способом ...?


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....
Louay Alakkad

1
Зокрема, наведено в коді в ОП, чи не могли б Ви детальніше пояснити, чому Userне може бути класом?
Тім Макнамара

Спробуйте натомість github.com/typeorm/typeorm .
Еріх

@Erich кажуть, що typeorm погано працює з MongoDB, можливо, Type
gose

Відповіді:


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 або при компіляції. То яка причина створення інтерфейсу?
Лупурус,

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 та мангуста.
Дан Даскалеску,

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 тис. Завантажень щотижня, і rn є майже 100 відкритих питань Github, більшість з яких не мають коментарів, і деякі з них схожі на те, що їх уже давно слід було закрити
Корбфон,

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

1
@ N4ppeL, з яким я б не пішов typegoose- ми в кінцевому підсумку обробляли набір тексту, подібно до цього допису , схоже, це ts-mongooseмало певні обіцянки (як пропонується у подальшій відповіді)
Корбфон,

1
Ніколи не просіть вибачення за "некропостінг". [Як ви тепер знаєте ...] Там навіть знак (хоча це буде названий некромант ; ^ D) для виконання всього цього! Заохочується некропостування нової інформації та ідей!
jorffin

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
Розкриття інформації: ts-мангуст, здається, створений небом. Здається, це найгарячіше рішення.
мікрофон


11

Більшість відповідей тут повторюють поля класу / інтерфейсу TypeScript та схеми мангуста. Відсутність єдиного джерела істини представляє ризик для обслуговування, оскільки проект ускладнюється, і більше розробників працює над ним: поля, швидше за все, виходять з синхронізації . Це особливо погано, коли клас знаходиться в іншому файлі порівняно зі схемою мангуста.

Щоб синхронізувати поля, має сенс визначити їх один раз. Є кілька бібліотек, які роблять це:

Я ще не до кінця переконаний будь-яким із них, але typegoose, здається, активно підтримується, і розробник прийняв мої PR.

Щоб подумати на крок вперед: коли ви додаєте схему GraphQL в мікс, з’являється інший шар дублювання моделі. Одним із способів подолання цієї проблеми може бути створення TypeScript та мангустського коду зі схеми GraphQL.


5

Ось сильний набраний спосіб порівняння простої моделі зі схемою мангуста. Компілятор забезпечить, щоб визначення, передані mongoose.Schema, відповідали інтерфейсу. Отримавши схему, ви можете використовувати її

загальний.ц

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

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

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

1
Це має бути прийнятою відповіддю, оскільки це від'єднує визначення користувача та DAL користувача. Якщо ви хочете перейти від mongo до іншого постачальника баз даних, вам не доведеться міняти користувальницький інтерфейс.
Рафаель дель Ріо

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
Це продублює кожне окреме поле між мангустом і 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.