grammyjs/history

Stable Release (please help!)

Opened this issue · 11 comments

This is a list of pending tasks that need to be addressed in order to bring this plugin from the current state to stable.

  1. Translate the chained calls (messages.from.id(3) and so on) to the defined query language
    • do this for updates because it is the simplest thing
    • add the shortcuts for chats and users and so on, which can also be expressed using the same language
  2. Write an adapter for a different database (maybe Mongo, idc) and verify that the current abstractions work well and are reasonably easy to translate for new storage backends
  3. Store outgoing messages, and extend the query builder logic to include this (this is also possible using the same query language, but perhaps updates and method calls need to be stored differently, such as { update: Update } | { call: { method: string, payload: any, result: any } } so that we can query things)

Naturally, we will also need JSDocs, a page on https://grammy.dev, and the usual yada yada with feedback loops from the community and iterative improvements until all edge cases are ruled out.

CONTRIBUTIONS ARE WELCOME!

It seems unfamiliar to use dots,
Why do you use them instead of objects?
Something like Prisma way:

const count = await ctx.history
  .messages.select({
    where: { 
      id: ctx.chat.id,
      from: { id: ctx.from.id }
    }
}).count();

I find dots a bit easier to read and a bit easier to write. They translate into English prose, which is nice. It's familiar from chai.js to check object structures.

But to be honest, I don't have too strong opinions about it—if people agree that objects are better than dots, feel free to change it.

How would disjunctions look (queries with OR)?

Inspired by Prisma, it could be like this:

const count = await ctx.history .messages.select({ 
  where: { 
    and: {
      or: [
        { from: { username: "someusername" } },
        { from: { id: 1234 } },
      ],
      id: ctx.chat.id,
    } 
  } 
}).count();

But I find working with nested objects is hard a little bit

Maybe making something in the middle will be easier like this:

const count = ctx.history.select
  .where({ chat: { id: ctx.chat.id } })
  .or([ 
    { from: { id: 1234 } },
    { from: { username: "someusername" } } 
  ])
  .count();

Maybe making something in the middle will be easier like this:

const count = ctx.history.select
  .where({ chat: { id: ctx.chat.id } })
  .or([ 
    { from: { id: 1234 } },
    { from: { username: "someusername" } } 
  ])
  .count();

Do you find this better because you are familiar with Prisma, or because it is more readable than this?

const count = ctx.history.select.messages
    .where.chat.id.is(ctx.chat.id)
    .and.where.from.id.is(1234).or.username.is("someusername")
    .count()

Maybe making something in the middle will be easier like this:

const count = ctx.history.select
  .where({ chat: { id: ctx.chat.id } })
  .or([ 
    { from: { id: 1234 } },
    { from: { username: "someusername" } } 
  ])
  .count();

Do you find this better because you are familiar with Prisma, or because it is more readable than this?

const count = ctx.history.select.messages
    .where.chat.id.is(ctx.chat.id)
    .and.where.from.id.is(1234).or.username.is("someusername")
    .count()

I find this easier to read and write but and/or queries confused me

and/or queries confused me

Can you elaborate?

and/or queries confused me

Can you elaborate?

const count = ctx.history.select.messages
    .where.chat.id.is(ctx.chat.id)
    .and.where.from.id.is(1234).or.username.is("someusername")
    .count()

How can I make sure it is:

chat.id === ctx.chat.id && (from.id === 1234 || from.username === "someusername")

Not this:

(chat.id === ctx.chat.id && from.id === 1234) || from.username === "someusername"

And what about if I want the second one?

Ah so it's about operator precedence, I see.

Using . will go one level deeper and basically create brackets. Basically, messages.where.chat.id.is(3).and.title.is("Friends") translates to this:

messages
  where
   -> chat
      -> id is 3
      -> AND title is "Friends"

You can go back to the top and add more clauses by using where again: The query messages.where.chat.id.is(3).and.title.is("Friends").and.where.text.exists translates to this:

messages
  -> where
    -> chat
      -> id is 3
      -> AND title is "Friends"
  -> AND where
    -> text exists

Naturally, this will have the following precedence: (chat.id == 3 && chat.title == "Friends") && !!text.

Thus, you can do messages.where.chat.id.is(ctx.chat.id).and.where.from.id.is(1234).or.username.is("someusername")

which translates to

messages
  -> where
    -> chat
      -> id is ctx.chat.id
  -> AND where
    -> from
      -> id is 1234
      -> OR username is "someusername"

and therefore gives you the desired

chat.id === ctx.chat.id && (from.id === 1234 || from.username === "someusername")

In addition, if things stay on the same level, this will be done with && before || like in JS. In other words, doing a.and.b.or.c will be interpreted as a && b || c and hence turn into (a && b) || c because && binds stronger than ||.

In contrast, if you wanted to test for

(chat.id === ctx.chat.id && from.id === 1234) || from.username === "someusername"

you would have to combine the chat and the from into one, and then add a second check for from. Hence, you can use messages.where.chat.id.is(ctx.chat.id).and.where.from.id.is(1234).or.where.from.username.is("someusername") because it translates to

messages
  -> where
    -> chat
      -> id is ctx.chat.id
  -> AND where
    -> from
      -> id is 1234
  -> OR where
    -> from
      -> username is "someusername"

which gives you the desired

(chat.id === ctx.chat.id && from.id === 1234) || from.username === "someusername"

Does this make more sense now?

Now it is clear :)

I definitely prefer {"chat.id": 123} over chat.id.is(123). The first one is way easier to read for me because in the second one values and operators are mixed together.

const count = ctx.history.select.messages
    .where({"chat.id": ctx.chat.id})
    .and.where({"from.id": 1234}).or({username: "someusername"})
    .count()

The problem is that this way we can't use custom operators like < or >.

Mongo works around this with {$lt: 123} and I don't like this approach.
kysely uses arrays and strings like this ['id', '<', 123] and I like it.

Maybe we can use arrays too?

const count = ctx.history.select.messages
    .where([
      ["chat.id", "=", ctx.chat.id],
      [
        ["from.id", "=", 1234],
        "or",
        ["username", "=", "someusername"],
      ],
    ])
    .count()

I find that syntax okay, too. I don't really feel like implementing it because this will take a huge effort, but I'm sure it's possible.

Do you always have to wrap stuff in double arrays, or can we just use where(["text", "exists"])?