Documenting developer experience

First try

Create a custom create controller for the chat-message api.

  1. Search for “create custom controller strapi” via Duckduckgo

This ended me up at this page. Going through the steps as described here, I learned that I don’t want to create a new controller (since strapi generate controller creates a new controller on the same level as the default chat-message controller). I want to modify the create method of the existing controller. So I go past the top description of the “implementation” section, and take a look at the code snippet. While a bit confusing at first, it was pretty straightforward that the code snippet actually consists of three different manners to do the same thing.

  1. Set-up create controller method

I picked the await super.find(ctx); method, since I want to keep as close to the default Strapi behavior as possible, since the more I would diverge from the Strapi core, the more knowledgable I need to be upon Strapi, on which I’m currently a beginner. So I figured out that I had to replace super.find(ctx) with super.create(ctx). This resulted in a chat-message/controllers/chat-message.ts file like this:

/**
 * chat-message controller
 */

import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::chat-message.chat-message", ({ strapi }) => ({
    async create(ctx) {
        console.log(ctx)
        return await super.create(ctx) 
    }    
}))

I’ve added the console.log of ctx in here to see if I can find any user / authorization data that I can use to update the ctx.request.body value (it might not be console logged when I console.log(ctx), but the documentation says that it exists).

  1. Set “sender” property with authorized user data

I try to find how I should format the sender attribute: oneToOne relation type, and I ended up on a documentation page where it described I could use three different ways to do this. I can’t find the page anymore, but I remember that one of them seemed like this {set: [1]}. Where the number would be the id of the user (in my scenario). So I tried to extend my controller as such:

import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::chat-message.chat-message", ({ strapi }) => ({
    async create(ctx) {
        // Some validation checks are left out to improve readability
        ctx.requist.body.data.sender = {set: ctx.state.user.id}
        return await super.create(ctx) 
    }    
}))

This gives me an “Invalid relations” error, I don’t remember having that error before. So I am trying to find the documentation page where I found a fore mentioned content, but the DuckduckGo engine isn’t of much use and the Strapi documentation search is super annoying cause it includes all of the user guide results, where I definitely won’t find my answer. Would love to restrict the developer guide search box to developer guide search results. If I want to explore the user guide for answers, I would switch to the user guide section. Can’t image how annoying this would be the other way around. For a non-developers using the search box in the user guide and end up in the developer section of the docs…

Finally found the documentation page! Relations | Strapi Documentation

The three options where connect, disconnect & set. I than resolved to ChatGPT which would tell me that I should just define the id, directly onto the relational data type. But this did not work neither. I have tried dozens of different methods, but none of them worked. After a few hours I figured out that the problem was not cause by incorrect code, but by a security configuration. Behind the scenes, Strapi uses the find controller of the users & permissions plugin, which my authorized user did not have access to.
I don’t remember how I have figured this out, but eventually I did discover that I had to update the value of settings > users & permissions plugin > roles > authenticated > chat-message > create.

export default factories.createCoreController("api::chat-message.chat-message", ({ strapi }) => ({
    
    async create(ctx) {
        const user = ctx.state.user
        
        // Ensure user is authenticated
        if (!user) {
            return ctx.unauthorized("Missing credentials for new chat message")
        }

        // Allow for body data when it is missing the data wrapper
        if (!ctx.request.body.data) {
            const tmp = ctx.request.body
            ctx.request.body = {
                data: tmp
            }
        }

        if (!ctx.request.body.data.message) {
            return ctx.unauthorized("Missing required property: `message`")
        }

        // Update user, based on authorized user value (ctx.state.user)
        // https://docs.strapi.io/dev-docs/api/rest/relations
        ctx.request.body.data.sender = user.id  

        return await super.create(ctx) 
    }    
}))
  1. Simplify controller

Now that I have this code to work, I wanted to remove the addition of the sender property outside of the controller, cause the controllers are vital in a Strapi application and I’d like to keep those to basis as much as possible. So I asked ChatGPT how to create a lifecycle before the create controller. Present moment me found a migration guide on this subject. But past me did not, and implemented the solution provided by ChatGPT. Creating a lifecycle.ts file at /api/chat-message/lifecycles.ts.

module.exports = {
    async beforeCreate(event) {
        const user = event.params.context.state.user
        event.params.data.user = user.id
    },
}

Which worked in (almost) one go! I felt like I was getting the hang of it and simplified the existing controller.

export default factories.createCoreController("api::chat-message.chat-message", ({ strapi }) => ({
    
    async create(ctx) {
        // Ensure user is authenticated
        if (!ctx.state.user) {
            return ctx.unauthorized("Missing credentials for new chat message")
        }
        if (!ctx.request.body.data.message) {
            return ctx.unauthorized("Missing required property: `message`")
        }

        // Update user, based on authorized user value (ctx.state.user)
        // https://docs.strapi.io/dev-docs/api/rest/relations
        ctx.request.body.data.sender = ctx.state.user.id  

        return await super.create(ctx) 
    }    
}))

To my surprise this modification did not return the user in the post response body. So… Again with the help of ChatGPT, I modified the response body after the created entity. Which made the controller look like this:

export default factories.createCoreController("api::chat-message.chat-message", ({ strapi }) => ({
    
    async create(ctx) {
        // Ensure user is authenticated
        if (!ctx.state.user) {
            return ctx.unauthorized("Missing credentials for new chat message")
        }

        if (!ctx.request.body.data.message) {
            return ctx.unauthorized("Missing required property: `message`")
        }

        // Update user, based on authorized user value (ctx.state.user)
        // https://docs.strapi.io/dev-docs/api/rest/relations
        ctx.request.body.data.sender = ctx.state.user.id  

        const response = await super.create(ctx) 

        response.data.attributes.sender = {
            id: ctx.state.user.id,
            username: ctx.state.user.username,
        }
        return response
    }    
}))
    1. Completed?

While the basis now works, there is two things that I have left out of this experience.

One has to do with the strapi-socket-io plugin. The current version of this plug-in does not support the customization of the content that is being emitted. So I’d like to modify that code so it’ll use either the return value of the create controller (the emit method is being triggered, based en the api endpoint that is connected to this controller). Or the data should be passed in a different manner, like using the customizable object of event.params.

The other has to do with the beforeCreate lifecycle hook. Cause while it works in its current location, I discovered a migration guide that discourage this implementation, and describes that it should be moved into the /api/chat-messages/content-types/chat-messages directory. Doing so without modifying the contents of the existing file would break the endpoint though. So I’m happy to have accidentally discovered this file, just as how I was unhappy to accidentally discovered that my lifecycles.ts file was in the wrong location.