@new binding for internal type

Hi all,
I decided to write my own little bindings for mongoose library and I stuck.
The problem is that it’s quite clear how to write @new binding for external type, but what if I use an external method that returns a constructor?

From mongoose docs:

// define a schema
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

// compile our model
const Person = mongoose.model('Person', personSchema);

// create a document
const axl = new Person({
  name: { first: 'Axl', last: 'Rose' }
});

Writing bindings for Schema and compiling model is easy:

type schema<'a>
type model<'a>

@module("mongoose") @new external createSchema: ('a) => schema<'a> = "Schema"
@send external model: (mongoose, string, schema<'a>) => model<'a> = "model"

But I can’t wrap my head around what to do with creating document. I should do something like new model<'a>() but I have no idea how to model that with Rescript.

You probably want to add a make() function to it for instantiating it.

Here are some old mongoose bindings of my own that are actually in use in a production system:

module Document = {
  type t<'a> = 'a

  @send
  external markModified: (t<'a>, string) => unit = "markModified"

  @send
  external save: t<'a> => Promise.t<unit> = "save"

  @send
  external toObject: t<'a> => 'a = "toObject"
}

module Model = {
  type t<'a>

  // https://mongoosejs.com/docs/api/model.html#model_Model-create
  @send
  external create: (t<'a>, 'a) => Promise.t<Document.t<'a>> = "create"

  // https://mongoosejs.com/docs/api/model.html#model_Model-deleteMany
  @send
  external deleteMany: (t<'a>, {..}) => Promise.t<{"deletedCount": int}> = "deleteMany"

  // https://mongoosejs.com/docs/api/model.html#model_Model-find
  @send
  external find: (t<'a>, {..}, option<{..}>, option<{..}>) => Promise.t<array<Document.t<'a>>> =
    "find"

  // https://mongoosejs.com/docs/api/model.html#model_Model-findOne
  @send
  external findOne: (t<'a>, {..}) => Promise.t<Js.Nullable.t<Document.t<'a>>> = "findOne"

  // https://mongoosejs.com/docs/api/model.html#model_Model-findOneAndUpdate
  @send
  external findOneAndUpdate_: (
    t<'a>,
    {..},
    {..},
    option<{..}>,
  ) => Promise.t<Js.Nullable.t<Document.t<'a>>> = "findOneAndUpdate"
  let findOneAndUpdate = (model: t<'a>, conditions: {..}, update: {..}, options: option<{..}>) =>
    findOneAndUpdate_(model, conditions, update, options)->Promise.thenResolve(Js.Nullable.toOption)
}

module Schema = {
  type t

  module Types = {
    type t = string

    let bool: t = "Boolean"
    let date: t = "Date"
    let mixed: t = "Mixed"
    let objectId: t = "ObjectId"
    let number: t = "Number"
    let string: t = "String"
  }

  @module("mongoose") @new
  external make: 'a => t = "Schema"
}

module Types = {
  module ObjectId = {
    type t

    @module("mongoose") @scope("Types") @new
    external make: string => t = "ObjectId"
  }
}

module Connection = {
  module ReadyState = {
    type t = int

    @dead("Connection.ReadyState.+disconnected") let disconnected: t = 0
    let connected: t = 1
    @dead("Connection.ReadyState.+connecting") let connecting: t = 2
    @dead("Connection.ReadyState.+disconnecting") let disconnecting: t = 3
  }

  type t = {
    name: string,
    readyState: ReadyState.t,
  }

  @send
  external model: (t, string, Schema.t, string) => Model.t<'a> = "model"

  @send
  external useDb: (t, string, {"useCache": bool}) => t = "useDb"

  let getModel = (conn: t, name): Model.t<'a> => {
    let models = Obj.magic(conn)["models"]
    switch models->Js.Dict.get(name) {
    | Some(model) => model
    | None => failwith(`No such model: ${name}`)
    }
  }

  let hasModel = (conn: t, name) => {
    let models = Obj.magic(conn)["models"]
    models->Js.Dict.get(name)->Belt.Option.isSome
  }
}

@module("mongoose") @scope("default")
external connection: Connection.t = "connection"

@module("mongoose")
external connect: string => Promise.t<unit> = "connect"

@module("mongoose")
external set: (string, bool) => unit = "set"
1 Like

I provided equivalent to your Schema make function:

The problem is not with instantiating a schema but with instantiating a concrete model.

@philiparvidsson you probably omit the problem in your example by using create method from mongoose that instantiates and saves at the same time, so you don’t need to call the constructor with new on Model.t<'a>.

That’s absolutely valid approach. The problem is not a deal breaker for me, and I didn’t find it common across JS libraries. Even within mongoose you can call Schema({}) to get back a new instance, but for some reason, they didn’t implement that for Model!

I’m asking the question because I’m more curious about how to model a case like that with Rescript. It’s not like I badly need it, it’s just itching my brain :smile:

The only way I know is to use raw js.

let makeDocument: (model<'a>, _) => document<'a> = %raw(`(Constructor, options) => new Constructor(options)`)
1 Like

Thanks @vdanchenkov
That’s what I’m using at the moment. It’s not that bad because it’s possible to define types, but I thought maybe there is a better way.
Using %raw I feel like using unsafe in Rust :smile:

1 Like