MattieBelt / mattie-strapi-bundle

Mattie plugin bundle for Strapi
https://mattie-bundle.mattiebelt.com
MIT License
40 stars 19 forks source link

Project Dead? #160

Open selected-pixel-jameson opened 1 year ago

selected-pixel-jameson commented 1 year ago

Haven't seen any new releases for this project in over a year. Is this still being worked on?

alexphiev commented 1 year ago

I am asking myself the same question. There is a quite blocking bug opened since a couple of months which makes it quite unsuable, right? https://github.com/MattieBelt/mattie-strapi-bundle/issues/139

andrew-braun commented 1 year ago

Same! Algolia has their own JS client, though, with v5 just having entered public beta, so I'm going to go with that instead. Pity--the Strapi v3 version worked very nicely.

krankos commented 1 year ago

@andrew-braun could you please tell us more about how to use the Algolia JS client to index from strapi?

andrew-braun commented 1 year ago

@krankos Sure! Here's what I did:

  1. Create a plugin using the Strapi CLI
  2. Installed Algoliasearch v5 (currently in beta, but since the changes from v4 are significant I figured it would be less hassle than upgrading from v4 later)
  3. In the services folder of the plugin I added my own file, algolia-indexing.js
  4. In that file, I added a bunch of functions that initiate an Algolia client and use it to add, update, or delete from the index. Docs for v5 are still sparse, so I ended up doing a bit of guessing and digging into the source code to figure it out.
  5. Then, I went into the lifecycles.js folder for each content type I wanted to send to Algolia. I added afterCreate, afterUpdate, and afterDelete hooks that call the appropriate Algolia functions and send the data over. Remember that for afterCreate and afterUpdate you'll also have to pass in a populate object that tells the service what data to go fetch so it can be sent to the Algolia index. I put all my populate objects in a separate file and called them dynamically based on the index name so I could mostly just copy-paste the functions.

It's still fairly rough since it's just meant for use in this one project (this can probably be improved quite a bit; it's my first time making a Strapi plugin), but here's the basic idea:

plugins/strapi-algolia/server/services.js

"use strict";

function algoliaClient() {
  // Initiate the Algolia client
  const { algoliasearch } = require("algoliasearch");
  const client = algoliasearch(process.env.ALGOLIA_ID, process.env.ALGOLIA_KEY);
  return client;
}

module.exports = ({ strapi }) => ({
  async addOrReplace(event, options = { index: "", populate: "*" }) {
    /* This function can be called from a Strapi lifecycle hook--afterCreate or afterUpdate, most likely
     ** Call it with syntax: strapi.service("plugin::strapi-algolia.index").addOrReplace(event, { index: "indexname", populate: {populateObject} })
     */
    try {
      const { model } = event;
      const { where } = event.params;
      const { index, populate } = options;
      const entryId =
        event.action === "afterUpdate" ? where?.id : event?.result?.id;

      console.log(
        `${event.action === "afterCreate" ? "Adding" : "Updating"} ${
          model.singularName
        } ${entryId} ${
          event.action === "afterCreate" ? "to" : "in"
        } Algolia index`
      );

      const client = algoliaClient();

      /* Populate the object with all of its relations by default
       ** If you want to customize this, you can pass in a populate array
       ** on the options object
       */

      const populatedObject = await strapi.entityService.findOne(
        model.uid,
        entryId,
        {
          populate: populate ?? "*",
        }
      );

      // console.log(populatedObject);

      const { taskID } = await client.saveObject({
        indexName: index,
        body: { objectID: entryId, ...populatedObject },
      });

      const status = await client.waitForTask({ indexName: index, taskID });
      console.log(`Algolia indexing status: ${status.status}`);
    } catch (error) {
      console.error(`Error while updating Algolia index: ${error}`);
    }
  },
  async delete(event, options) {
      try {
        const { index, many } = options;
        const objectIDs = many
          ? event?.params?.where?.["$and"][0]?.id["$in"]
          : [event.params.where.id];

        const client = algoliaClient();

        console.log(
          `Deleting object(s) with id(s) ${objectIDs.join(
            ", "
          )} from Algolia ${index} index`
        );

        // Use Algolia client.batch API to handle either one or many delete requests
        const requests = objectIDs.map((objectID) => {
          return {
            objectID,
            action: "deleteObject",
          };
        });
        const { taskID } = await client.batch({
          indexName: index,
          batchWriteParams: { requests },
        });

        const response = await client.waitForTask({ indexName: index, taskID });

        console.log(
          `Successfully deleted object(s) with id(s) ${objectIDs.join(
            ", "
          )} from Algolia ${index} index`
        );
      } catch (error) {
        console.error(`Error while deleting from Algolia index: ${error}`);
      }
    },

/api/article/lifecycles.js

"use strict";

/**
 * Read the documentation (https://strapi.io/documentation/developer-docs/latest/development/backend-customization.html#lifecycle-hooks)
 * to customize this model
 */

const index = "articles";

const {
  articlePopulate: populate,
} = require("../../../../lib/data/algolia-populate-data");

module.exports = {
  async afterCreate(event) {
    const response = await strapi
      .service("plugin::strapi-algolia.index")
      .addOrReplace(event, { index, populate: populate });
  },

  async afterUpdate(event) {
    const response = await strapi
      .service("plugin::strapi-algolia.index")
      .addOrReplace(event, { index, populate: populate });
  },

  async afterDelete(event) {
    const response = await strapi
      .service("plugin::strapi-algolia.index")
      .delete(event, { index, many: false });
  },

  async afterDeleteMany(event) {
    const response = await strapi
      .service("plugin::strapi-algolia.index")
      .delete(event, { index, many: true });
  },
};

/lib/data/algolia-populate-data.js

module.exports = {
  articlePopulate: {
    author: {
      fields: ["authorName", "profession", "bio"],
    },
    story_category: {
      fields: ["category_name"],
    },
    country: {
      fields: ["country", "countryName"],
    },
    region: {
      fields: ["region", "regionName"],
    },
    ContentZone: true,
  },
}

I also added a separate thing for bulk-updating indexes via a POST request that tells Strapi to go collect the data from the specified content type and batch update it, but it's even rougher than this is and honestly would just make it more confusing :D

It's a very simple solution, probably not the most efficient, and is definitely missing a lot of features/convenience that a more sophisticated plugin would provide, but it didn't take me long to make and it does the job I want--updating Algolia based on Strapi lifecycle hooks.

krankos commented 1 year ago

@andrew-braun Thank you for sharing! I'll try this approach with my team and we'll try to make it index only published entries.

andrew-braun commented 1 year ago

@krankos Good luck! And that's actually a good note--I should probably add that to mine as well :D Should be simple enough to add a publicationState check.

krankos commented 1 year ago

@andrew-braun well, we finally made it work! After many attempts and errors, we found the solutions we sought. We decided to use Algolia v4. We followed your steps but did some modifications specially in the algolia-indexing.js. Here's our version with the index on publish feature. We just used the publishedAt field as a reference for the entry's state.

"use strict";

function algoliaClient() {
  const algoliasearch = require('algoliasearch');
  const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_KEY);
  return client;
}

module.exports = ({ strapi }) => ({
  async addOrReplace(event, options = { index: "", populate: "*" }) {
    try {
      const { model } = event;
      const { where } = event.params;
      const { index, populate } = options;
      const entryId =
        event.action === "afterUpdate" ? where?.id : event?.result?.id;
      console.log(
        `${event.action === "afterCreate" ? "Adding" : "Updating"}
                ${model.singularName}
                ${entryId} ${event.action === "afterCreate" ? "to" : "in"} Algolia index ${index}`
      );
      const client = algoliaClient();

      const populateObject = await strapi.entityService.findOne(model.uid, entryId, { populate: populate ?? "*", });
      if (populateObject.publishedAt === null && event.action === "afterCreate") {
        strapi.log.info("created draft")
      }
      if (populateObject.publishedAt !== null && event.action === "afterCreate") {
        strapi.log.info("created publish")
        console.log("1", { objectID: entryId, ...populateObject })
        const { taskID } = await client.initIndex(index).saveObject({

          objectID: entryId, ...populateObject
        });

        const status = await client.waitForTask({ indexName: index, taskID });
        console.log(`Algolia indexing status: ${status.status}`);

      }
      if (populateObject.publishedAt === null && event.action === "afterUpdate") {
        strapi.log.info("updated from published to draft")
        const { taskID } = await client.initIndex(index).deleteObject(entryId);
        // const status = await client.waitForTask({ indexName: index, taskID });
        // console.log(`Algolia indexing status: ${status.status}`);
      }
      if (populateObject.publishedAt !== null && event.action === "afterUpdate") {
        strapi.log.info("updated from draft to published")

        console.log("2", { objectID: entryId, ...populateObject })
        const { taskID } = await client.initIndex(index).saveObject({
          objectID: entryId, ...populateObject
        });

        // const status = await client.waitForTask({ indexName: index, taskID });
        // console.log(`Algolia indexing status: ${status.status}`);
      }
    }
    catch (error) {
      console.log(`Error while updating Algolia index: ${JSON.stringify(error)}`);
    }
  },
  async delete(event, options) {
    try {
      const { index, many } = options;
      const objectIDs = many ? event?.params?.where?.["$and"][0]?.id["$in"] : [event.params.where.id];
      const client = algoliaClient();

      console.log(`Deleting object(s) with ID(s) ${objectIDs.join(",")} from Algolia index ${index}`);

      const requests = objectIDs.map((objectID) => {
        return {
          objectID,
          action: "deleteObject",
        };
      });
      // const { taskID } = await client.batch({ indexName: index, batchWriteParams: { requests }, });
      const { taskID } = await client.initIndex(index).deleteObjects(objectIDs);
      // const response = await client.waitForTask({ indexName: index, taskID });

      console.log(`Successfully deleted object(s) with ID(s) ${objectIDs.join(",")} from Algolia index ${index}`);
    } catch (error) {
      console.log(`Error while deleting object(s) from Algolia index: ${error}`);
    }
  },
});
SamSaprykin commented 1 year ago

hey guys @krankos @andrew-braun, thanks for sharing your solutions here. Did you have any issues after publishing this custom plugin to production? I mean for me everything works great on localhost but when I try to use strpapi cloud instance, indexing doesn't work for some reason. Thanks in advance!

krankos commented 1 year ago

In our case it's working in prod. However, we're not using strapi cloud, we're self hosting since we've been using strapi before strapi cloud was launched. Check your environmental variables. The issue might be as simple as that.

hunterxp commented 1 year ago

@andrew-braun @krankos thanks for sharing the ideas and the code!

andrew-braun commented 1 year ago

hey guys @krankos @andrew-braun, thanks for sharing your solutions here. Did you have any issues after publishing this custom plugin to production? I mean for me everything works great on localhost but when I try to use strpapi cloud instance, indexing doesn't work for some reason. Thanks in advance!

Nope, it works in all my environments. If you're still having issues, feel free to drop logs of any errors you might be getting and I'll see if I can spot anything!