DEV Community

Cover image for Develop a custom hook extension for Directus
Ken Yip
Ken Yip

Posted on

Develop a custom hook extension for Directus

Introduction

Recently, I decided to use Directus as the CMS for my personal blog after comparing it with other open-source frameworks. One of the advantages of Directus is that it allows us to easily create custom extensions.

In this article, I will guide you through creating a custom hook extension that slugifies the article and throws an error if the slug already exists in the database.

Configure the Setup

First of all, we need to install Directus on our local machine to develop an extension. There are several ways to set up a Directus project. You can check the official documentation here:
https://docs.directus.io/getting-started/quickstart.html#_1-create-a-project

To enable local development, I recommend using the NPM installation method. It allows us to hot-reload the server when the extension code is changed.

  1. Setup Directus project

      npm init directus-project@latest <project-name>
    
  2. Install Nodemon to enable hot-reload

      yarn install -D nodemon
    
  3. Add a command to the scripts section of the package.json

      "dev": "npx nodemon --watch extensions --ext js --exec \"yarn start\""
    
  4. Create a collection called ‘article’ with the fields: title, slug, and content. The slug should be set as hidden.

  5. Start the program with the command:

      yarn dev
    

Create the custom hook

  1. Navigate to the extensions folder and run the following command to create a new extension.

      npx create-directus-extension@latest slugify-article-title
    
  2. You will be prompted with serveral questions. I used the TypeScript to create the extension.

  3. Install the ‘slugify’ package in the extension.

      yarn add slugify
    
  4. Create 2 folders: errors and utils, and then add the following scripts:

      // createArticleTitleAlreadyExistsError.ts
      import { createError } from '@directus/errors';
    
      export const createArticleTitleAlreadyExistsError = (title: string) =>
        createError(
          'ArticleTitleAlreadyExistsError',
          `An article with the title "${title}" already exists.`,
          409
        );
      // InvalidSetupError.ts
      import { createError } from '@directus/errors';
    
      export const InvalidSetupError = createError(
        'InvalidSetupError',
        'The article collection must have a string field named "slug" to use the slugify-article-title extension.',
        400
      );
    
      // verifySetup.ts
      import { SchemaOverview } from '@directus/types';
    
      import { InvalidSetupError } from '../errors';
    
      export const verifySetup = (schema: SchemaOverview | null) => {
        if (!schema) {
          return;
        }
        const articleSchema = schema.collections?.article;
        if (articleSchema === undefined || articleSchema.fields.slug === undefined) {
          return;
        }
        if (articleSchema.fields.slug.type !== 'string') {
          throw new InvalidSetupError();
        }
      };
    

    The verifySetup function ensures that the article collection is set up correctly. The errors contain the error message and HTTP code. The error message will be displayed as an alert on the admin portal.

  5. In the index.ts file, replace the script with the following code:

      import slugify from 'slugify';
    
      import { defineHook } from '@directus/extensions-sdk';
    
      import { createArticleTitleAlreadyExistsError } from './errors';
      import { ArticlePayload } from './types';
      import { verifySetup } from './utils';
    
      export default defineHook(({ filter }) => {
        filter('article.items.create', async (input, _meta, { database, schema }) => {
          verifySetup(schema);
    
          const payload = input as ArticlePayload;
          const slug = slugify(payload.title, { lower: true });
    
          const existingTitle = await database
            .table('article')
            .where('slug', slug)
            .first('title')
            .then((result) => result?.title);
    
          if (existingTitle !== undefined) {
            const ArticleTitleAlreadyExistsError = createArticleTitleAlreadyExistsError(existingTitle);
            throw new ArticleTitleAlreadyExistsError();
          }
    
          payload.slug = slug;
          return payload;
        });
    
        filter('article.items.update', async (input, meta, { database, schema }) => {
          verifySetup(schema);
          const updates = input as Partial<ArticlePayload>;
          if (!updates.title) {
            return updates;
          }
          const articleId: string = meta.keys[0];
          if (!articleId) {
            return updates;
          }
          const slug = slugify(updates.title, { lower: true });
    
          const existingTitle = await database
            .table('article')
            .where('slug', slug)
            .where('id', '<>', articleId)
            .first('title')
            .then((result) => result?.title);
    
          if (existingTitle !== undefined) {
            const ArticleTitleAlreadyExistsError = createArticleTitleAlreadyExistsError(existingTitle);
            throw new ArticleTitleAlreadyExistsError();
          }
    
          updates.slug = slug;
          return updates;
        });
      });
    

    The script contains two hooks that are triggered before the article item is created or updated. It slugifies the title, checks if the slug exists in the database, and either inserts the slug or throws an error (as declared in the previous step) if it already exists. You can find the available events here:
    https://docs.directus.io/extensions/hooks.html#available-events

  6. Run the following command to build the extension and test it on the admin portal. You will notice that the server and the build process are automatically restarted whenevet there is a code change!

Top comments (1)