How to track changes to specific fields, Strapi Lifecycle Hooks and Middleware Events

System Information
  • Strapi Version: 5.4.2
  • Operating System: macOS Sequoia 15.1
  • Database: SQLite
  • Node Version: 20.17.0
  • NPM Version: 10.8.2

Hello everyone!

tl;dr:

  1. How to track changes from the previous version of a field value, to run operations after it has changed (API call, updating other fields, etc.)?
  2. potential bug: events other than FindOne do not seem to fire from the Strapi middleware.

ONE:
I have a collection type projects. I am trying to listen for changes to the location property, and if it has changed, fetch data from an API to update property country. I have been able to do this in @/src/api/project/content-types/project/lifecycles.ts, with a beforeUpdate hook.
The problem: the hook runs on the change of any project property, and I am unable to track whether specifically location has changed. The value of location remains the same in beforeUpdate and afterUpdate hooks, and also with polling strapi using await strapi.documents(...).findOne(...) inside either of the hooks.

import axios from "axios";

const getCountryFromCoordinates = async ({ lat, lng }) => {
  // Placeholder logic for demo purposes
  return "...";
};

export default {
  async beforeUpdate(event) {
    const { data } = event.params;
    const ctx = strapi.requestContext.get();

    const existingEntry = await strapi.documents("api::project.project").findOne({
      documentId: event.params.where.documentId,
    });

    strapi.log.debug(
      `beforeUpdate location from database: ${JSON.stringify(existingEntry?.location)}`
    );
    strapi.log.debug(
      `beforeUpdate location from event data: ${JSON.stringify(data?.location)}`
    );
    strapi.log.debug(
      `beforeUpdate location from request body: ${JSON.stringify(ctx.request.body?.location)}`
    );
  },
  async afterUpdate(event) {
    const { data } = event.params;

    // Demo call to the country function
    const country = await getCountryFromCoordinates({ lat: 0, lng: 0 });
    strapi.log.debug(`afterUpdate demo country: ${country}`);

    const ctx = strapi.requestContext.get();

    const existingEntry = await strapi.documents("api::project.project").findOne({
      documentId: event.params.where.documentId,
    });

    strapi.log.debug(
      `afterUpdate location from database: ${JSON.stringify(existingEntry?.location)}`
    );
    strapi.log.debug(
      `afterUpdate location from event data: ${JSON.stringify(data?.location)}`
    );
    strapi.log.debug(
      `afterUpdate location from request body: ${JSON.stringify(ctx.request.body?.location)}`
    );
  },
};

TWO:
After reading further in the Strapi issues page, Lifecycle hooks can't access previous values of components · Issue #20346 · strapi/strapi · GitHub, I have also tried to achieve this by adding a middleware to index.ts, which unfortunately only seems to register the findOne event. This is different to the official Strapi documentation: Extending the Document Service behavior | Strapi 5 Documentation

console.log("Strapi is starting...");

export default {
  register({ strapi }) {
    console.log("Register function executed.");
    strapi.log.error("Register function executed.");
    strapi.documents.use((context, next) => {
      const applyTo = ["api::project.project"];

      // Only run for certain content types
      if (!applyTo.includes(context.uid)) {
        return next();
      }

      strapi.log.debug(`ACTION TYPE HIGH-LEVEL: ${context.action}`);

      // Only run for update actions – this does not fire when I update a project! if it's set to FindOne, it fires OK.
      if (["update"].includes(context.action)) {
        const { documentId } = context.params;

        strapi.log.debug(`🔵🔵 TYPE: ${context.action} action triggered.`);
        strapi.log.debug(`🔵🔵 id: ${JSON.stringify(context.params.documentId)}`);
      }

      return next();
    });
  },
// bootstrap, destroy etc.
};

Any help is much appreciated!