Are there any best practices for referencing versioned resources?

We’ve just started using paper_trail for versioning some resources, and we want to reference those in various places. I’d like to know if others have trodden this path before.

Tl;Dr

Is there an idiomatic ash way to fetch a specific guid for a version and get back hydradeted, normal ash resource struct, with version information?

(I’m going to continue researching and may self-update if I find something. Hopefully this is still a useful log for other people in the future)

More context

I’ve considered a couple of approaches and wondered if others have trodden this path before. I’m going to explain my full thinking in case it’s useful to others coming past, but you can skip to the end for my questions.

Our app is essentially a content editor, allowing users to write documents. Each document references things like images, text snippets, etc. We’ve added versioning to those resources, and now we’re updating these internal references to be version-aware.

(We use guid resource ids)

We want a document to pin e.g. “Image of a cat - v1”. Now when the CMS updates the image (e.g. updates the alt text) the document is aware that it is holding an outdated version, and should nudge the user to update the image from the CMS copy.

Idea 1 - Continue to reference the resource only

Breaking problem: we can’t pin to a specific version, so this already doesn’t work.

Idea 2 - Reference the version only

Pros: we explicitly reference version. We only need to store 1 guid.

Cons: more lookups and extra logic to get the full picture (resource with version information).

Questions: I bet there’s a clever ash way to always return resource lookups with embedded version information, but I haven’t found it yet.

3 - Create a hybrid reference, {resource, version}

Store some hybrid ID that includes both the resource and version ID.

Pros: we have both guids, we can directly work with either. More direct representation in the database (though with guids this doesn’t matter, we are planning to switch to stripe-style object ids that would have useful prefixes on ids).

Cons: we have to store both guids! Same amount of work and lookups.

2 Likes

There is nothing built in for that, no. Depending on which versioning strategy its harder or easier, for example if you use the :snapshot strategy then you should theoretically be able to hydrate a resource with essentially an Enum.reduce, getting the corresponding attribute via Ash.Resource.Info.attribute, using Ash.Type.cast_stored(type, value, constraints) on each value, and returning a struct w/ those values.

One thing you might consider doing is making a calculation that does this logic, so you can do

thing |> Ash.load(at_version: %{version: <version_id>})

or maybe

thing |> Ash.load(version_as_of: %{timestamp: <datetime>})
1 Like

I’ll explore snapshot and full_diff and see what fits our case best.

Thanks for the pointer and calculation tip :heart:

@zachdaniel we’ve got a basic setup working now with “hydrated” resources, but it feels a bit brittle: it’s re-using Ash structs for items that aren’t “real” ash resource instances, and it feels like that could easily be mistaken for a real instance and cause some trouble. I’m thinking of ways to make it more idiomatic.

If I were to PR a change, would this idea align with Ash goals?

Added support for some kind of “read only” meta_data flag on resources, so that extensions like PaperTrail can create temporary copies of resources that were hydrated from versions, and can be read or referenced, but don’t get confused with a copy of a loaded version from the DB. And validation or something similar that would prevent write actions with that struct.

(This is deeper in the Ash internals than I’m familiar with, so I wanted to ask for direction first)

1 Like

Hmm…I’m not sure if this would make sense to have in Ash core. The records it produces could have a metadata in __metadata__ set with Ash.Resource.set_metadata and you could have logic that prevents using them in actions?