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


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.