Use Hooks to Validate Phone Numbers With Twilio
Hooks allow you to trigger your own code when events are emitted from Directus. This guide will show you how to prevent a record from saving if a phone number is not valid using the Twilio Lookup API.
Install Dependencies
Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate code for your display.
npx create-directus-extension@latest
npx create-directus-extension@latest
A list of options will appear (choose hook), and type a name for your extension (for example, directus-hook-phone-validation
). For this guide, select JavaScript.
Now the boilerplate has been created, install the twilio package, and then open the directory in your code editor.
cd directus-hook-phone-validation
npm install twilio @directus/errors
cd directus-hook-phone-validation
npm install twilio @directus/errors
Build the Hook
Create a collection called Customers with a text field called phone_number
. This hook will be used to validate the item when a record is saved.
Open the index.js
file inside the src directory. Delete all the existing code and start with the import of the Twilio library and the invalid payload error:
import twilio from 'twilio';
import { InvalidPayloadError } from "@directus/errors";
import twilio from 'twilio';
import { InvalidPayloadError } from "@directus/errors";
Create an initial export. This hook will need to intercept the save function with filter
and include env
for the environment variables:
export default ({ filter }, { env }) => {};
export default ({ filter }, { env }) => {};
Next, capture the items.create
stream using filter
and include the input
and collection
associated with the stream:
filter('items.create', async (input, { collection }) => {});
filter('items.create', async (input, { collection }) => {});
When using filters and actions, it’s important to remember this will capture all events so you should set some restrictions. Inside the filter, exclude anything that’s not in the customers collection.
filter('items.create', async (input, { collection }) => {
if (collection !== 'customers') return input;
});
filter('items.create', async (input, { collection }) => {
if (collection !== 'customers') return input;
});
Prevent saving an event if the phone_number
is undefined
, by reporting this back to the user. Add this line underneath the collection restriction.
if (input.phone_number === undefined) {
throw new InvalidPayloadError({ reason: 'No Phone Number has been provided' });
}
if (input.phone_number === undefined) {
throw new InvalidPayloadError({ reason: 'No Phone Number has been provided' });
}
Set up your Twilio phone number lookup:
const accountSid = env.TWILIO_ACCOUNT_SID;
const authToken = env.TWILIO_AUTH_TOKEN;
const client = new twilio(accountSid, authToken);
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {});
const accountSid = env.TWILIO_ACCOUNT_SID;
const authToken = env.TWILIO_AUTH_TOKEN;
const client = new twilio(accountSid, authToken);
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {});
env
looks inside the Directus environment variables for TWILIO_ACCOUNT_SID
and TWILIO_AUTH_TOKEN
. In order to start using this hook, these variables must be added to the .env
file.
The lookup is performed with the phone_number
from the input object.
Inside the callback, provide a response for when the phone number is invalid, otherwise continue as normal. Twilio provides a very helpful boolean response called valid
.
Use this to throw an error if false
, or return the input to the stream and end the hook if true
:
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {
if (!phoneNumber.valid) {
throw new InvalidPayloadError({ reason: 'Phone Number is not valid' });
}
return input;
});
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {
if (!phoneNumber.valid) {
throw new InvalidPayloadError({ reason: 'Phone Number is not valid' });
}
return input;
});
Build the hook with the latest changes.
npm run build
npm run build
Add Hook to Directus
When Directus starts, it will look in the extensions
directory for any subdirectory starting with directus-extension-
, and attempt to load them.
To install an extension, copy the entire directory with all source code, the package.json
file, and the dist
directory into the Directus extensions
directory. Make sure the directory with your extension has a name that starts with directus-extension
. In this case, you may choose to use directus-extension-hook-phone-validation
.
Ensure the .env
file has TWILIO_ACCOUNT_SID
and TWILIO_AUTH_TOKEN
variables.
Restart Directus to load the extension.
Required files
Only the package.json
and dist
directory are required inside of your extension directory. However, adding the source code has no negative effect.
Summary
With Twilio now integrated in this hook, whenever a record attempts to save for the first time, this hook will validate the phone number with Twilio and respond with true or false. If false, the record is prevented from saving until a valid phone number is supplied. Now that you know how to interact with the Twilio API, you can investigate other endpoints that Twilio has to offer.
Complete Code
index.js
import { InvalidPayloadError } from "@directus/errors";
export default ({ filter }, { env }) => {
filter('items.create', async (input, { collection }) => {
if (collection !== 'customers') return input;
if (input.phone_number === undefined) {
throw new InvalidPayloadError({ reason: 'No Phone Number has been provided' });
}
const accountSid = env.TWILIO_ACCOUNT_SID;
const authToken = env.TWILIO_AUTH_TOKEN;
const client = new twilio(accountSid, authToken);
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {
if (!phoneNumber.valid) {
throw new InvalidPayloadError({ reason: 'Phone Number is not valid' });
}
return input;
});
});
};
import { InvalidPayloadError } from "@directus/errors";
export default ({ filter }, { env }) => {
filter('items.create', async (input, { collection }) => {
if (collection !== 'customers') return input;
if (input.phone_number === undefined) {
throw new InvalidPayloadError({ reason: 'No Phone Number has been provided' });
}
const accountSid = env.TWILIO_ACCOUNT_SID;
const authToken = env.TWILIO_AUTH_TOKEN;
const client = new twilio(accountSid, authToken);
client.lookups.v2
.phoneNumbers(input.phone_number)
.fetch()
.then((phoneNumber) => {
if (!phoneNumber.valid) {
throw new InvalidPayloadError({ reason: 'Phone Number is not valid' });
}
return input;
});
});
};