Update record without creating a new version? Use case: page views counter

I have blog website where we want to track page views and store the number inside DatoCMS, so that we are able to do a GraphQL query for blog posts ordered by the view count (basically a “most read” list).

I have a simple API endpoint that updates the record like this:

  const item = await client.items.find(id);
  await client.items.update(id, {
    view_count: item.view_count + 1,
  });

Now the problem is that for every update, a new record version is created. This causes two issues:

  1. It pollutes the list list of versions for the CMS users, as every programmatic update of the “view_count” creates a new version, and thereby making it almost impossible to find a “real” old version of a record, if a user wants to. See screenshot:

  2. It breaks the purpose of restoring a “real” older version of a record, as this would restore the “view_count” field as well, which we don’t want to happen.

So my question is: Is it possible to update a record without creating a new version?
Or do you have other suggestion on how I can do this?

Thanks!

Does the pageview information have to live in the same record? Could it live inside another record? Something like either:

  • One standalone single-record model called “Pageviews”, that has a JSON field containing an array of all pageviews by record ID. Like pageviews: ['id1': 40144, 'id2': 12], etc. When a page gets visited, you use the CMA to update that one field directly. It lives completely separate from your real records with the editor-facing content.

  • Or you can associate each real record to a separate record-keeping record in a different model (via a single-link field). But I don’t see any advantages of doing it this way if the pagecounts are incremented programmatically anyway. Having a single JSON field/record makes it easier to keep track of everything, and doesn’t require linking (since you’re just performing a separate lookup/update anyway).


For what it’s worth, this is also the kind of thing that doesn’t necessarily have to live inside DatoCMS if you have a better place for it, like an external database, cloud KV store, analytics suite, etc. As long as you have an external list of page IDs/slugs sorted by pageviews, you can make just give that list to our GraphQL with a filter like:

query MyQuery {
  allPages(
    filter: {id: {in: ["id1", "id2", "id2"]}} # or slugs
  ) {
    id
    slug
    title
    # etc
  }
}

And the results will come back in the order of that array.

Does that help at all?

Hi Roger,
Thanks for your suggestions.

Regarding your filter query example:
Doing the filter: {id: {in: ["id1", "id2", "id3"]}} filter won’t guarantee the returned order of the records.
So in that example, “id3” might still come before “id1” in the response array.
So that does not work. Or am I missing something?

Furthermore I need to do several filters in the query, like this:

query SearchQuery {
  allBlogPosts(
    filter: {
      category: {
        in: ["category-id"]
      }
      tags: {
        anyIn: ["tag-id"]
      }
    }
    orderBy:  ... // would be nice to do "viewCount_DESC" here
  ) {
    id
    title
  }
}

… so unfortunately I can’t just grab the top 10 IDs (with most views) from the other record (as in your example) and use those IDs, as those IDs might not be within the category and tag I need.
I need to drill down to find the posts I need and then order them by the view count.
That’s why I thought it would be ideal to have the view count on the record itself, so I can natively query for it.

There is no way to do a record update via the API without creating a new version?

Ah, I see. Thank you for the explanation.

No, I don’t think so :frowning: When you update a record, it creates a new version. We don’t really have record-level user-definable metadata that sits outside of the versioning system. Changes to record content will create new versions in every instance I can think of…


Unfortunately, I don’t think our system will let you do this all on the serverside without actually editing the record itself and creating new versions.

There’s a hackish workaround you can do with linked records in a different model and inverse relationships, but it’ll require you to do some clientside sorting. I sent you an email invite to an example project (https://view-counts.admin.datocms.com/editor), but the end result looks something like this:

query MyQuery {
  allPosts(filter: {category: {in: ["one"]}}) {
    id
    title
    category
    _allReferencingPageviewDataModels {
      pageviews
    }
  }
}
{
  "data": {
    "allPosts": [
      {
        "id": "efiUCKjMRl6c6QSY7LAyYg",
        "title": "This has the least pageviews",
        "category": "one",
        "_allReferencingPageviewDataModels": [
          {
            "pageviews": 1
          }
        ]
      },
      {
        "id": "bM3bchBmRO-DVVhsNJtewQ",
        "title": "This has the most pageviews",
        "category": "one",
        "_allReferencingPageviewDataModels": [
          {
            "pageviews": 100
          }
        ]
      },
      {
        "id": "cXzP7v_WT4SzpS80XwsYAA",
        "title": "This one is in between",
        "category": "one",
        "_allReferencingPageviewDataModels": [
          {
            "pageviews": 51
          }
        ]
      }
    ]
  }
}

Which you’d then have to filter client-side using something like:

// allPosts will be sorted in-place by descending value of its related `pageviews` field
allPosts.sort((a,b) => b['_allReferencingPageviewDataModels'][0]['pageviews'] - a['_allReferencingPageviewDataModels'][0]['pageviews'])

/* allPosts now becomes [
    {
        "id": "bM3bchBmRO-DVVhsNJtewQ",
        "title": "This has the most pageviews",
        "category": "one",
        "_allReferencingPageviewDataModels": [
            {
                "pageviews": 100
            }
        ]
    },
    {
        "id": "cXzP7v_WT4SzpS80XwsYAA",
        "title": "This one is in between",
        "category": "one",
        "_allReferencingPageviewDataModels": [
            {
                "pageviews": 51
            }
        ]
    },
    {
        "id": "efiUCKjMRl6c6QSY7LAyYg",
        "title": "This has the least pageviews",
        "category": "one",
        "_allReferencingPageviewDataModels": [
            {
                "pageviews": 1
            }
        ]
    }
]*/

It’s pretty hacky though. It requires:

  • A separate model to hold post metadata separately from the Post themselves
  • Each record in the metadata model links uniquely to a Post, and has an additional integer field for recording that Post’s pageviews
  • The Posts model must have Inverse Relationships enabled
  • You can filter serverside (as in this example), but you’ll have to sort on the clientside. If you have more than 100 records, you have to paginate through them (or make several cursored queries in the same GraphQL call) and then combine and sort them clientside.

But it does kinda work and let you modify pageviews independent of Post revisions. It’s just a lot of work and a fair bit of mental complexity :frowning:

Sorry about that!

Thank you for all your suggestions, Roger, I really appreciate it.

You are right, this could in theory work, but in reality its a bit too hacky and not an optimal solution because we have more than 1000+ blog posts… :face_with_diagonal_mouth:

If only I could update the record without creating a version, then it would all “just work” natively out of the box, with the filtering and ordering already supported in GraphQL, like this:

query SearchQuery {
  allBlogPosts(
    orderBy: viewCount_DESC <-- this would do the trick
    filter: {
      category: {
        in: ["category1"]
      }
      tags: {
        anyIn: ["tag1", "tag2"]
      }
    }
  ) {
    id
    title
    viewCount
  }
}

Is it possible that you could turn this ticket into a feature request?
It could be a body parameter on the “update” method, like skip_version: true or something like that.

On this API:

Yes, of course. I turned it into a feature request for you (please remember to vote for it at the top).

The use case makes total sense. But I’m not quite sure that the underlying implementation should be “allow record updates without incrementing versions”… that seems like it would have a high potential for confusion, as it basically creates invisible changes in a record that aren’t recorded anywhere. Automations could edit records in the background and human editors would not have any indication as to how it happened.

I wonder if something else could be done instead, like maybe a “metadata side channel” with custom fields, similar to what we have for assets already…

@mat_jack1 / @m.finamor, any thoughts or better workaround ideas here? (They’re also on the support team).

I agree that using links and/or inverse relationships looks like a good workaround.

Unfortunately it’s not possible to update without creating a new version, so the links are the only idea that comes to my mind.

Bear in mind that we aggregate versions automatically, so yes, it’s still going to be difficult to find human updates, but API-based versions and human versions should still be separate. I mean, I know it’s still messy, but it should be separate nonetheless.

Input for the feature request: Could it be a setting on the field itself that “changes to this field will not create a new version”?