Tracking document changes
I'm porting data and logic for our business systems from Airtable to a custom app powered by Convex. Part of what Airtable provides as a standard feature is record history. This history has proved invaluable in solving problems that come up on occasion, so I need to replicate this in the new system.
I'm thinking that storing all changes in a separate table would be the way to go, with an ID field indexed to make searching for changes to a given document more efficient, and the app to use pagination to optimize the query process. The main thing that I'm struggling with is how to identify and collect the change data.
We have dozens of tables, and we'll need this history tracked on each of their documents. I really don't relish the idea of building that many separate history trackers. I'm thinking that some kind of "diff" helper function would be useful. I could pass it any document along with the changes to be saved to that document, and it would return the difference between the two as an object that I could stringify and save to the history table.
Is this doable? If so, I'd love any advice on how to make it work. My grasp of TypeScript is still pretty basic, so I can't quite wrap my head around how I'd approach something like this (if it's even possible with the current Convex design).
31 Replies
Thanks for posting in <#1088161997662724167>.
Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets.
- Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.)
- Use search.convex.dev to search Docs, Stack, and Discord all at once.
- Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI.
- Avoid tagging staff unless specifically instructed.
Thank you!
I think there's a component for this
@Matt Luo I looked through the component list earlier and didn't find anything. Checked it again just now in case I'd overlooked something, but I still don't see anything for this use case.
npm
convex-table-history
A table history component for Convex.. Latest version: 0.1.2, last published: 2 months ago. Start using convex-table-history in your project by running
npm i convex-table-history
. There are no other projects in the npm registry using convex-table-history.Is this helpful?
Immensely! This looks like exactly what I need. My only question is: why isn't this listed on the official components page yet?
Um, im not sure, but worst case you can read the code to be inspired
@lee Is this safe to use, or are there still bugs being worked out that would explain its absence from the official component list?
No known bugs, but it hasn't gone through the full review process
Gotcha. I'll start adding it to my project tomorrow and see how things go. I've currently got only one trackable table implemented, but by the time I'm done it'll be a few dozen. Definitely curious to see how this affects my stats as things grow.
🙏 tell us what you learn!
Absolutely!
we actually had another company just asking about this pattern too...
^ @james fyi
Development is moving slowly as I'm the lone dev and there's a Titanic-sized boatload of stuff to do. It'll be many months before this thing is ready to release to the company.
@lee Just submitted a PR to fix a minor issue I found in the docs. About to start adding this component to my project.
Got the basics set up with a single table, and it appears to be working well so far. I'd also like to save the initial state of a document as the first "change," but I'm struggling to figure out how to do that.
Taking a cue from the example "patch" function from the docs, I made the following:
I quickly found that this won't work because I don't have a document yet, so I can't type the
document
argument as a Doc
of anything. If I type it as Partial<Doc<"contacts">>
, the type checker indicates an issue with the insert
method because some of the fields in my table schema are required, and they could possibly come in as undefined
. Not sure how to fix this, or if I should take an entirely different approach.
I suppose I could change the schema so that every field is optional, but that doesn't really feel like the best solution
On a whim I switched the type of document
to the partial doc as described above, then made every field optional in the table schema. While it got rid of the type error indicator on the insert
function, the call to the table history updater still indicates a type error:
Is this because the document doesn't yet exist?
I think I wasn't taking enough cues from the example for patching. Updated the creation function to the following, and there are no more type errors:
That said, I'm still wondering if there's a better way to approach this than making all fields optional in the table schema.try
WithoutSystemFields<Doc<"contacts">>
?It wasn't the system fields causing the problem. I had several fields as optional in the table schema, and that's what led to the type checking error on the
insert
method. By passing a partial doc, any field could theoretically be undefined
, which clashed with the field requirements from the schema.how are you inserting a partial doc if the fields are required
The data I'm passing has those required fields, but because the function definition for this
createContact
helper function types the document as Partial<Doc<"contacts">>
it doesn't know that.yeah i'm suggesting you change the
createContact
function arg type
I guess I'm confused on what
WithoutSystemFields
refers to. I thought it only ignored the _id
and _creationTime
fields.right. it's used as the argument to
insert
https://github.com/get-convex/convex-js/blob/45f7c4196003a4d827f925d3781ebe64278c5546/src/server/database.ts#L160
because when you insert a document, you need all of the fields besides the _id
and _creationTime
Hmmm...I guess I don't understand how that will impact a case where non-system fields are required by the schema, but this function can't "trust" (for lack of a better term) that I'm going to pass data for all required fields.
it will behave in the same way as
ctx.db.insert
behaves (because it's the same type)Well, it worked, but I still don't fully understand the logic behind it. Maybe I'm focusing too much on the reference to system fields in the name.
Thanks for the help!
Thanks again. That definitely clears up my understanding of how all of this works.
I'm hitting an error when trying to query the history. I'm trying to follow the doc examples, but it's indicating an error in my app component. Here's the query function:
Here's how I'm trying to use it in the app component:
Here's the error indicator for
api.sales.model.contacts.getContactHistory
:
Huh that's not expected. I wonder why that would happen
Probably need some type annotations or
as const
in there somewhereThanks for the hint. A type annotation on the function reference fixed it:
api.sales.model.contacts.getContactHistory as PaginatedQueryReference
I have a fix for this; will publish new version tonight. Thanks for reporting
@lee I've got a feature request for this component. Would it be okay to discuss it here, or would you prefer that I start a new thread?
New thread in #components ?