Support storing custom fields for plugins

Hello, I am mainly curious if custom data can be attached to notes, or if there is support for saving custom local data for a plugin. Is this functionality feasible, or even allowed?

For example, attaching a custom time to a note so I can track it and calculate how many minutes I have been typing in that note in total.

This would be separate from the existing note edited time, and persistent across app instances.

Thank you!

Hi Andrew,

That’s a good question.
Storing custom fields is interesting.
It’s technically possible to store fields like x-customField so the app can distinguish if they are custom fields.
For example:

import { actions } from "inkdrop"

const { dispatch } = inkdrop.store
dispatch(actions.editingNote.update({ 'x-customField': 'bar' }))
dispatch(actions.editor.change(true))

Note that you basically should avoid directly calling db.notes.put() to update a lot of times because that makes a revision each time.

1 Like

That solution worked wonders, thank you.

Are these fields able to be removed or are they permanent? (I only see ways to add and not remove which might be by design?)

This isn’t a large issue, mainly wanting to remove the extra fields if the user resets the plugin or something similar.

It’s not currently designed for custom fields, so it’s not possible to remove them via editingNote.update action at the moment.

yep, good point. Cleaning up is important.

The field prefix “x-” is not sufficient for cleaning up.
The plugin name should be able to be inferred by the field names like “x-plugin-name-fieldName”.
But I guess scanning all the notes to clean them up may take a while if the user has a lot of notes.

Another way to store custom data is to create separate docs something like:

{
  "_id": "plugin:PLUGIN_NAME:<CUSTOM_DOC_ID>",
  ...(custom data)
}

The “CUSTOM_DOC_ID” can be arbitrary.
In this way, you can make a doc for each note that has custom data:

{
  "_id": "plugin:PLUGIN_NAME:<NOTE_ID>",
  ...(custom data)
}

In this way, the app can support removing them anytime.
But I need further discussion on this because that makes the app complicated.
The database and sync could be broken.

I have also been looking at storing custom fields for notes to support some extra functionality.

Note that you basically should avoid directly calling db.notes.put() to update a lot of times because that makes a revision each time.

While using db.notes.put() might not be a good idea for the revisions sake, it does not even seem to work. I have tried saving some custom fields using it and they never seemed to get applied, the example you provided works but as I’m trying to use custom fields on notes you aren’t currently editing this solution doesn’t seem to be a good idea. Would it be possible to apply the fields some other way or would I have to go with the separate docs?

You also mention that using the separate docs could break the database and sync, is there an actual risk of this even if I just use db.put and db.remove specifically on my custom docs, or is this just a warning you mention to be on the safe side?

Weird. Do you get a new revision in the returned value? It doesn’t filter custom fields.

Yea, you are right.

The separate docs would be better in this case.
Because updating a note will make another copy of the whole document. That’s why you can roll back from the old revisions. That’s kind of redundant for storing custom fields. Also, that would cause unexpected conflicts between devices.
So, plugin:PLUGIN_NAME docs would solve those issues.

Yes. The app should provide custom data APIs for the plugins so that you can avoid directly call db.put, which is dangerous. I don’t want to get reports from users screaming like “my data gone!”

1 Like

I did some more testing and it seems like a new revision is created but the custom field is not stored.

What I tested

const id = "note:<a note id>"
const db = inkdrop.main.dataStore.getLocalDB()
const notes = db.notes
let note = await notes.get(id)
note["x-test"] = "test-val"
await notes.put(note)

The object returned by the promise of notes.put(note) contains a different rev than what is returned by the notes.get(id)

I then tried both await notes.get(id) and await inkdrop.main.dataStore.localPouch.get(id) and both of the returned objects are identical, also meaning that neither of them contained the custom field (I assumed that maybe notes.get() was filtering some fields when fetching).

Just for the sake of it I also tried updating one of the regular fields of the note alongside setting the custom field, the regular field was updated when calling notes.put(note) but the custom field was still note stored.

That’s due to the Electron’s remote object proxy:

So, you have to convert the remote object to a local object beforehand like so:

const id = "note:<a note id>"
const db = inkdrop.main.dataStore.getLocalDB()
const notes = db.notes
let note = await notes.get(id)
await notes.put({ ...note, "x-test": "test-val" })
1 Like

That works perfectly, thank you!

It might be worth putting this as a note in the docs info about working with the DB. I’m probably not the only one that haven’t worked with Electron before, and this kind of info can be a bit hard to find if you don’t know what you are looking for.

Edit: I see now that there is some info about Remote Objects under the Accessing the PouchDB instance (Advanced) section but unfortunately the link with more information does not work anymore.

You are right, and that’s one of the reasons why it’s not recommended to use low-level database APIs.
Please hold on storing custom fields to the note documents directly until the app officially supports plugin documents.

Fixed the broken link! Thanks.

Hello @craftzdog,
Any updates of this? For development I am using the first approach discussed but before I release anything I would like to get a stable/non-invasive method that won’t break functionality.
Is this on the roadmap?
Best,
Nick

Hi @NotApplicable
No, not yet. I’m currently focusing on the new mobile app (See my recent activities).

Another idea I recently came up with is to use YAML frontmatter.
Because it’s visible and clear to users.
It doesn’t solve every use case but is simple and viable. No need to use dangerous store APIs.

Hi @craftzdog thanks for the reply! I will checkout this approach and see where I end up!

Update: v5.6.0-beta.0 is out with its Markdown renderer update:

Now, it allows you to read the YAML frontmatter easily via the preview redux state. So, you don’t parse frontmatters yourself. Check out the new documentation for more detail here:

Note that it currently does not support updating the values.

1 Like