UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

126 lines (125 loc) 6.38 kB
import { randomUUID } from 'node:crypto'; import { parseJSON, toArray } from '@directus/utils'; // To avoid typos const TABLE_WEBHOOKS = 'directus_webhooks'; const TABLE_FLOWS = 'directus_flows'; const TABLE_OPERATIONS = 'directus_operations'; const NEW_COLUMN_WAS_ACTIVE = 'was_active_before_deprecation'; const NEW_COLUMN_FLOW = 'migrated_flow'; /** * 0. Identify and persist which webhooks were active before deprecation * 1. Migrate existing webhooks over to identically behaving Flows * 2. Disable existing webhooks * 3. Dont drop webhooks yet */ export async function up(knex) { // 0. Identify and persist which webhooks were active before deprecation await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => { table.boolean(NEW_COLUMN_WAS_ACTIVE).notNullable().defaultTo(false); table.uuid(NEW_COLUMN_FLOW).nullable(); table.foreign(NEW_COLUMN_FLOW).references(`${TABLE_FLOWS}.id`).onDelete('SET NULL'); }); await knex(TABLE_WEBHOOKS) .update({ [NEW_COLUMN_WAS_ACTIVE]: true }) .where({ status: 'active' }); const webhooks = await knex.select('*').from(TABLE_WEBHOOKS); // 1. Migrate existing webhooks over to identically behaving Flows await knex.transaction(async (trx) => { for (const webhook of webhooks) { const flowId = randomUUID(); const operationIdRunScript = randomUUID(); const operationIdWebhook = randomUUID(); const newFlow = { id: flowId, name: webhook.name, icon: 'webhook', // color: null, description: 'Auto-generated by Directus Migration\n\nWebhooks were deprecated and have been moved into Flows for you automatically.', status: webhook.status, // Only activate already active webhooks! trigger: 'event', // accountability: "all", options: { type: 'action', scope: toArray(webhook.actions) .filter((action) => action.trim() !== '') .map((scope) => `items.${scope}`), collections: toArray(webhook.collections).filter((collection) => collection.trim() !== ''), }, operation: null, // Fill this in later --> `operationIdRunScript` // date_created: Default Date, // user_created: null, }; const operationRunScript = { id: operationIdRunScript, name: 'Transforming Payload for Webhook', key: 'payload-transform', type: 'exec', position_x: 19, position_y: 1, options: { code: '/**\n * Webhook data slightly differs from Flow trigger data.\n * This flow converts the new Flow format into the older Webhook shape\n * in order to not break existing logic of consumers.\n */ \nmodule.exports = async function(data) {\n const crudOperation = data.$trigger.event.split(".").at(-1);\n const keyOrKeys = crudOperation === "create" ? "key" : "keys";\n return {\n event: `items.${crudOperation}`,\n accountability: { user: data.$accountability.user, role: data.$accountability.role },\n payload: data.$trigger.payload,\n [keyOrKeys]: data.$trigger?.[keyOrKeys],\n collection: data.$trigger.collection,\n };\n}', }, resolve: operationIdWebhook, reject: null, flow: flowId, // date_created: "", // user_created: "", }; const operationWebhook = { id: operationIdWebhook, name: 'Webhook', key: 'webhook', type: 'request', position_x: 37, position_y: 1, options: { url: webhook.url, body: webhook.data ? '{{$last}}' : undefined, method: webhook.method, headers: typeof webhook.headers === 'string' ? parseJSON(webhook.headers) : webhook.headers, }, resolve: null, reject: null, flow: flowId, // date_created: "", // user_created: "", }; await trx(TABLE_FLOWS).insert(newFlow); // Only need to transform the payload if the webhook enabled transmitting it if (webhook.data && webhook.method !== 'GET') { // Order is important due to IDs await trx(TABLE_OPERATIONS).insert(operationWebhook); await trx(TABLE_OPERATIONS).insert(operationRunScript); await trx(TABLE_FLOWS).update({ operation: operationIdRunScript }).where({ id: flowId }); } else { operationWebhook.position_x = 19; // Reset it because the Run-Script will be missing await trx(TABLE_OPERATIONS).insert(operationWebhook); await trx(TABLE_FLOWS).update({ operation: operationIdWebhook }).where({ id: flowId }); } // Persist new Flow/s so that we can retroactively remove them on potential down-migrations await trx(TABLE_WEBHOOKS) .update({ [NEW_COLUMN_FLOW]: flowId }) .where({ id: webhook.id }); // 2. Disable existing webhooks await trx(TABLE_WEBHOOKS).update({ status: 'inactive' }); } }); } /** * Dont completely drop Webhooks yet. * Lets preserve the data and drop them in the next release to be extra safe with users data. */ export async function down(knex) { // Remove Flows created by the up-migration const migratedFlowIds = (await knex(TABLE_WEBHOOKS).select(NEW_COLUMN_FLOW).whereNotNull(NEW_COLUMN_FLOW)).map((col) => col[NEW_COLUMN_FLOW]); await knex(TABLE_FLOWS).delete().whereIn('id', migratedFlowIds); // Restore previously activated webhooks await knex(TABLE_WEBHOOKS) .update({ status: 'active' }) .where({ [NEW_COLUMN_WAS_ACTIVE]: true }); // Cleanup of up-migration await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => { table.dropColumns(NEW_COLUMN_WAS_ACTIVE, NEW_COLUMN_FLOW); }); }