From 67aa629542b27a02632a147d11b4716c140deba4 Mon Sep 17 00:00:00 2001 From: luisbritos <60328073+luisbritos@users.noreply.github.com> Date: Wed, 21 Apr 2021 15:58:03 -0300 Subject: [PATCH] [DXEX-1316] update actions handlers (#142) * update actions handlers * don't update code action when current_version changes * tweak triggers schema * update action fields independently * lint fix * 5.5.0 --- package-lock.json | 2 +- package.json | 2 +- src/auth0/handlers/actionBindings.js | 195 ------------------- src/auth0/handlers/actionVersions.js | 154 --------------- src/auth0/handlers/actions.js | 250 +++++++++++++++++++------ src/auth0/handlers/index.js | 2 + src/auth0/handlers/triggers.js | 135 +++++++++++++ src/constants.js | 1 + src/utils.js | 5 +- tests/auth0/handlers/actions.tests.js | 128 +------------ tests/auth0/handlers/triggers.tests.js | 151 +++++++++++++++ 11 files changed, 498 insertions(+), 527 deletions(-) delete mode 100644 src/auth0/handlers/actionBindings.js delete mode 100644 src/auth0/handlers/actionVersions.js create mode 100644 src/auth0/handlers/triggers.js create mode 100644 tests/auth0/handlers/triggers.tests.js diff --git a/package-lock.json b/package-lock.json index e2c6bf3..699672e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "5.4.0", + "version": "5.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0704876..a4bb8ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "5.4.0", + "version": "5.5.0", "description": "Supporting tools for the Source Control extensions", "main": "lib/index.js", "scripts": { diff --git a/src/auth0/handlers/actionBindings.js b/src/auth0/handlers/actionBindings.js deleted file mode 100644 index a15f4df..0000000 --- a/src/auth0/handlers/actionBindings.js +++ /dev/null @@ -1,195 +0,0 @@ -import DefaultHandler from './default'; -import log from '../../logger'; - -const triggers = [ - 'post-login', - 'credentials-exchange' -]; - -function wait(n) { return new Promise(resolve => setTimeout(resolve, n)); } - -async function createBindingWithRetryAndTimeout(client, params, data, retry) { - let binding = {}; - try { - if (retry > 0) { - binding = await client.createActionBinding(params, data); - } - } catch (err) { - await wait(1000); - binding = await createBindingWithRetryAndTimeout(client, params, data, retry - 1); - } - return binding; -} - -export default class ActionBindingHandler extends DefaultHandler { - constructor(options) { - super({ - ...options, - type: 'actionBindings' - }); - } - - async getType() { - if (this.existing) { - return this.existing; - } - - // in case client version does not support actions - if ( - !this.client.actionBindings - || typeof this.client.actionBindings.getAll !== 'function' - ) { - return []; - } - const bindingsResult = []; - try { - await Promise.all( - triggers.map(trigger => this.client.actionBindings - .getAll({ trigger_id: trigger, detached: true }) - .then(b => b.bindings.forEach(binding => bindingsResult.push({ detached: true, ...binding })))) - ); - // get all attached bindings - await Promise.all( - triggers.map(trigger => this.client.actionBindings - .getAll({ trigger_id: trigger, detached: false }) - .then(b => b.bindings.forEach(binding => bindingsResult.push({ detached: false, ...binding })))) - ); - this.existing = bindingsResult; - return this.existing; - } catch (err) { - if (err.statusCode === 404 || err.statusCode === 501) { - return []; - } - throw err; - } - } - - async detachBinding(binding, detached) { - const params = { trigger_id: binding.trigger_id }; - const existing = await this.getType(); - let attachedList = []; - existing.forEach((existingBinding) => { - if (!existingBinding.detached) { - attachedList.push({ id: existingBinding.id }); - } - }); - if (!detached) { - attachedList.push({ id: binding.id }); - } else { - attachedList = attachedList.filter(id => id === binding.id); - } - delete params.binding_id; - await this.client.actionBindings.updateList(params, { - bindings: attachedList - }); - } - - // Create Binding creates a atacched binding - async createActionBinding(data) { - const retries = 10; - const params = { trigger_id: data.trigger_id }; - const actionBinding = { action_id: data.action_id, display_name: data.display_name }; - const created = await createBindingWithRetryAndTimeout(this.client, params, actionBinding, retries); - - if (!created) { - throw new Error(`Couldn't create binding after ${retries} retries`); - } - // connect binding - await this.detachBinding(created, false); - - return created; - } - - async createActionBindings(creates) { - await this.client.pool - .addEachTask({ - data: creates || [], - generator: item => this.createActionBinding(item) - .then((data) => { - this.didCreate({ binding_id: data.id }); - this.created += 1; - }) - .catch((err) => { - throw new Error( - `Problem creating ${this.type} ${this.objString(item)}\n${err}` - ); - }) - }) - .promise(); - } - - async deleteActionBinding(binding) { - // detach binding - await this.detachBinding(binding, true); - // delete binding - await this.client.actionBindings.delete({ - trigger_id: binding.trigger_id, - binding_id: binding.id - }); - } - - async deleteActionBindings(dels) { - if (this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true) { - await this.client.pool - .addEachTask({ - data: dels || [], - generator: item => this.deleteActionBinding(item) - .then(() => { - this.didDelete({ binding_id: item.id }); - this.deleted += 1; - }) - .catch((err) => { - throw new Error(`Problem deleting ${this.type} ${this.objString(item)}\n${err}`); - }) - }) - .promise(); - } else { - log.warn(`Detected the following actions bindings should be deleted. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config - \n${dels.map(i => this.objString(i)).join('\n')}`); - } - } - - async calcChanges(action, bindingsTriggers, existing) { - // Calculate the changes required between two sets of assets. - let del = [ ...existing ]; - const create = []; - - bindingsTriggers.forEach((binding) => { - const found = existing.find( - existingActionBinding => existingActionBinding.trigger_id === binding.trigger_id - ); - if (found) { - del = del.filter(e => e !== found); - } else { - create.push({ - display_name: action.name, - action_id: action.id, - trigger_id: binding.trigger_id - }); - } - }); - // Figure out what needs to be deleted and created - return { del, create }; - } - - async processChanges(changes) { - log.info( - `Start processChanges for actions bindings [delete:${changes.del.length}], [create:${changes.create.length}]` - ); - const myChanges = [ { del: changes.del }, { create: changes.create } ]; - await Promise.all( - myChanges.map(async (change) => { - switch (true) { - case change.del && change.del.length > 0: - await this.deleteActionBindings(change.del); - break; - case change.create && change.create.length > 0: - await this.createActionBindings(changes.create); - break; - default: - break; - } - }) - ); - } -} diff --git a/src/auth0/handlers/actionVersions.js b/src/auth0/handlers/actionVersions.js deleted file mode 100644 index 6a39d97..0000000 --- a/src/auth0/handlers/actionVersions.js +++ /dev/null @@ -1,154 +0,0 @@ -import DefaultHandler from './default'; -import log from '../../logger'; -import { isArrayEqual } from '../../utils'; - -export default class ActionVersionHandler extends DefaultHandler { - constructor(options) { - super({ - ...options, - type: 'actionVersions' - }); - } - - async getType(actionId) { - if (!actionId) { - return []; - } - - if (this.existing) { - return this.existing; - } - - // in case client version does not support actionVersions - if (!this.client.actionVersions || typeof this.client.actionVersions.getAll !== 'function') { - return []; - } - - try { - this.existing = await this.client.actionVersions.getAll({ action_id: actionId }); - return this.existing; - } catch (err) { - if (err.statusCode === 404 || err.statusCode === 501) { - return []; - } - throw err; - } - } - - async getVersionById(actionId, currentVersion) { - // in case client version does not support actionVersions - if (!this.client.actionVersions || typeof this.client.actionVersions.get !== 'function') { - return null; - } - // in case action doesn't have a current version yet - if (!currentVersion) { - return null; - } - - try { - return await this.client.actionVersions.get({ action_id: actionId, version_id: currentVersion.id }); - } catch (err) { - if (err.statusCode === 404 || err.statusCode === 501) { - return null; - } - throw err; - } - } - - async createActionVersion(version) { - const actionId = version.action_id; - const versionToCreate = { - code: version.code, - dependencies: version.dependencies, - secrets: version.secrets, - runtime: version.runtime - }; - const newVersion = await this.client.actionVersions.create({ action_id: actionId }, versionToCreate); - // create draft version - await this.client.actionVersions.upsertDraft({ action_id: actionId, version_id: 'draft' }, versionToCreate); - return newVersion; - } - - async createActionVersions(creates) { - await this.client.pool.addEachTask({ - data: creates || [], - generator: item => this.createActionVersion(item).then((data) => { - this.didCreate({ version_id: data.id }); - this.created += 1; - }).catch((err) => { - throw new Error(`Problem creating ${this.type} ${this.objString(item)}\n${err}`); - }) - }).promise(); - } - - async deleteActionVersion(version) { - await this.client.actionVersions.delete({ action_id: version.action.id, version_id: version.id }); - } - - async deleteActionVersions(dels) { - if (this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true) { - await this.client.pool.addEachTask({ - data: dels || [], - generator: actionVersion => this.deleteActionVersion(actionVersion).then(() => { - this.didDelete({ version_id: actionVersion.id }); - this.deleted += 1; - }).catch((err) => { - throw new Error(`Problem deleting ${this.type} ${this.objString(actionVersion)}\n${err}`); - }) - }).promise(); - } else { - log.warn(`Detected the following action versions should be deleted. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config - \n${dels.map(i => this.objString(i)).join('\n')}`); - } - } - - calcCurrentVersionChanges(actionId, currentVersionAssets, existing) { - const del = []; - const create = []; - - // Figure out what needs to be deleted or created - if (!currentVersionAssets && !existing) { - return { del, create }; - } - - if (!currentVersionAssets && existing) { - del.push({ ...existing, action_id: actionId }); - return { del, create }; - } - - if (currentVersionAssets && !existing) { - create.push({ ...currentVersionAssets, action_id: actionId }); - return { del, create }; - } - - if (currentVersionAssets.code !== existing.code - || currentVersionAssets.runtime !== existing.runtime - || !isArrayEqual(currentVersionAssets.dependencies, existing.dependencies) - || !isArrayEqual((currentVersionAssets.secrets || []).map(s => s.name), (existing.secrets || []).map(s => s.name))) { - create.push({ ...currentVersionAssets, action_id: actionId }); - } - - return { - del, - create - }; - } - - async processChanges(changes) { - log.info(`Start processChanges for action versions [delete:${changes.del.length}], [create:${changes.create.length}]`); - - const myChanges = [ { del: changes.del }, { create: changes.create } ]; - await Promise.all(myChanges.map(async (change) => { - switch (true) { - case change.del && change.del.length > 0: - await this.deleteActionVersions(change.del); - break; - case change.create && change.create.length > 0: - await this.createActionVersions(changes.create); - break; - default: - break; - } - })); - } -} diff --git a/src/auth0/handlers/actions.js b/src/auth0/handlers/actions.js index 5443739..f0e999b 100644 --- a/src/auth0/handlers/actions.js +++ b/src/auth0/handlers/actions.js @@ -1,17 +1,46 @@ /* eslint-disable consistent-return */ +import _ from 'lodash'; import DefaultHandler, { order } from './default'; import log from '../../logger'; -import ActionVersionHandler from './actionVersions'; -import ActionBindingsHandler from './actionBindings'; +import { areArraysEquals } from '../../utils'; + +const WAIT_FOR_DEPLOY = 60; // seconds to wait for the version to deploy +const HIDDEN_SECRET_VALUE = '_VALUE_NOT_SHOWN_'; // With this schema, we can only validate property types but not valid properties on per type basis export const schema = { type: 'array', items: { type: 'object', - required: [ 'name', 'supported_triggers' ], + required: [ 'name', 'supported_triggers', 'code', 'runtime' ], additionalProperties: false, properties: { + code: { type: 'string', default: '' }, + dependencies: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { type: 'string' }, + version: { type: 'string' }, + registry_url: { type: 'string' } + } + } + }, + status: { type: 'string', default: '' }, + runtime: { type: 'string', default: '' }, + secrets: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + value: { type: 'string' }, + updated_at: { type: 'string', format: 'date-time' } + } + } + }, name: { type: 'string', default: '' }, supported_triggers: { type: 'array', @@ -82,31 +111,65 @@ export const schema = { } } } - }, - required: [ 'code', 'dependencies', 'runtime' ] - }, - bindings: { - type: 'array', - items: { - type: 'object', - required: [ 'trigger_id' ], - properties: { - trigger_id: { type: 'string' } - } } } } } }; +function wait(n) { return new Promise(resolve => setTimeout(resolve, n)); } + +function mapSecrets(secrets) { + if (secrets) { + return secrets.map(secret => ({ ...secret, value: HIDDEN_SECRET_VALUE })); + } +} + +function mapCurrentVersion(currentVersion) { + if (currentVersion) { + return ({ ...currentVersion, secrets: mapSecrets(currentVersion.secrets) }); + } +} + +async function waitUntilVersionIsDeployed(client, actionId, versionId, retries) { + const version = await client.actions.getVersion({ action_id: actionId, version_id: versionId }); + if (retries > 0 && !version.deployed) { + await wait(1000); + await waitUntilVersionIsDeployed(client, actionId, versionId, retries - 1); + } + + if (retries <= 0) { + throw new Error(`Couldn't deploy version after ${WAIT_FOR_DEPLOY} retries`); + } +} + + export default class ActionHandler extends DefaultHandler { constructor(options) { super({ ...options, type: 'actions' }); - this.actionVersionHandler = new ActionVersionHandler(options); - this.actionBindingsHandler = new ActionBindingsHandler(options); + } + + async getVersionById(actionId, currentVersion) { + // in case client version does not support actionVersions + if (typeof this.client.actions.getVersions !== 'function') { + return null; + } + // in case action doesn't have a current version yet + if (!currentVersion) { + return null; + } + + try { + return await this.client.actions.getVersions({ action_id: actionId, version_id: currentVersion.id }); + } catch (err) { + if (err.statusCode === 404 || err.statusCode === 501) { + return null; + } + throw err; + } } async getType() { @@ -121,13 +184,10 @@ export default class ActionHandler extends DefaultHandler { try { const actions = await this.client.actions.getAll(); - // need to get complete current version and all bindings for each action + // need to get complete current version for each action // the current_version inside the action doesn't have all the necessary information - this.existing = await Promise.all(actions.actions.map(action => this.actionVersionHandler.getVersionById(action.id, action.current_version) - .then(async (currentVersion) => { - const bindings = await this.getActionBinding(action.id); - return ({ ...action, current_version: currentVersion, bindings: bindings }); - }))); + this.existing = await Promise.all(actions.actions.map(action => this.getVersionById(action.id, action.current_version) + .then(async currentVersion => ({ ...action, secrets: mapSecrets(action.secrets), current_version: mapCurrentVersion(currentVersion) })))); return this.existing; } catch (err) { if (err.statusCode === 404 || err.statusCode === 501) { @@ -137,25 +197,114 @@ export default class ActionHandler extends DefaultHandler { } } + + async createVersion(version) { + const actionId = version.action_id; + const versionToCreate = { + code: version.code, + dependencies: version.dependencies, + secrets: version.secrets.filter(secret => secret.value !== HIDDEN_SECRET_VALUE), + runtime: version.runtime + }; + const newVersion = await this.client.actions.createVersion({ action_id: actionId }, versionToCreate); + + // wait WAIT_FOR_DEPLOY seconds for version deploy, if can't deploy an error will arise + await waitUntilVersionIsDeployed(this.client, actionId, newVersion.id, WAIT_FOR_DEPLOY); + + return newVersion; + } + + async calcCurrentVersionChanges(actionId, currentVersionAssets, existing) { + const create = []; + + // Figure out what needs to be deleted or created + if (!currentVersionAssets && !existing) { + return { create }; + } + + if (currentVersionAssets && !existing) { + create.push({ ...currentVersionAssets, action_id: actionId }); + return { create }; + } + if (currentVersionAssets.code !== existing.code + || currentVersionAssets.runtime !== existing.runtime + || !areArraysEquals(currentVersionAssets.dependencies, existing.dependencies) + || !areArraysEquals((currentVersionAssets.secrets || []).map(s => s.name), (existing.secrets || []).map(s => s.name))) { + create.push({ ...currentVersionAssets, action_id: actionId }); + } + return { + create: create + }; + } + + async createVersions(creates) { + await this.client.pool.addEachTask({ + data: creates || [], + generator: item => this.createVersion(item).then((data) => { + this.didCreate({ version_id: data.id }); + this.created += 1; + }).catch((err) => { + throw new Error(`Problem creating ${this.type} ${this.objString(item)}\n${err}`); + }) + }).promise(); + } + + async processVersionsChanges(changes) { + log.info(`Start processChanges for action versions [create:${changes.create.length}]`); + + const myChanges = [ { create: changes.create } ]; + await Promise.all(myChanges.map(async (change) => { + switch (true) { + case change.create && change.create.length > 0: + await this.createVersions(changes.create); + break; + default: + break; + } + })); + } + + async actionChanges(action, found) { + const actionChanges = {}; + + if (action.name !== found.name) { + actionChanges.name = action.name; + } + if (action.code !== found.code) { + actionChanges.code = action.code; + } + + if (!areArraysEquals(action.dependencies, found.dependencies)) { + actionChanges.dependencies = action.dependencies; + } + + if (!areArraysEquals((action.secrets || []).map(s => s.name), (found.secrets || []).map(s => s.name))) { + actionChanges.secrets = action.secrets; + } + + if (action.runtime !== found.runtime) { + actionChanges.runtime = action.runtime; + } + + return actionChanges; + } + async createAction(data) { const action = { ...data }; const currentVersion = action.current_version; // eslint-disable-next-line prefer-destructuring - const bindings = action.bindings; const actionToCreate = { name: action.name, - supported_triggers: action.supported_triggers + supported_triggers: action.supported_triggers, + code: action.code, + dependencies: action.dependencies, + secrets: action.secrets, + runtime: action.runtime }; const created = await this.client.actions.create(actionToCreate); if (currentVersion) { - await this.actionVersionHandler.createActionVersions([ { ...currentVersion, action_id: created.id } ]); - } - - if (bindings) { - const bindingsToCreate = []; - bindings.forEach(f => bindingsToCreate.push({ trigger_id: f.trigger_id, display_name: action.name, action_id: created.id })); - await this.actionBindingsHandler.createActionBindings(bindingsToCreate); + await this.createVersions([ { ...currentVersion, action_id: created.id } ]); } return created; } @@ -173,8 +322,8 @@ export default class ActionHandler extends DefaultHandler { } async deleteAction(action) { - await this.actionBindingsHandler.deleteActionBindings(action.bindings); - await this.client.actions.delete({ action_id: action.id }); + // force=true forced bound actions to delete + await this.client.actions.delete({ action_id: action.id, force: true }); } async deleteActions(dels) { @@ -196,18 +345,16 @@ export default class ActionHandler extends DefaultHandler { async updateAction(action, existing) { const found = existing.find(existingAction => existingAction.name === action.name); - // update current version - const currentVersionChanges = this.actionVersionHandler.calcCurrentVersionChanges(found.id, action.current_version, found.current_version); - if (currentVersionChanges.del.length > 0 || currentVersionChanges.create.length > 0) { - await this.actionVersionHandler.processChanges(currentVersionChanges); + const currentVersionChanges = await this.calcCurrentVersionChanges(found.id, action.current_version, found.current_version); + if (currentVersionChanges.create.length > 0) { + await this.processVersionsChanges(currentVersionChanges); } - - const bindingChanges = await this.actionBindingsHandler.calcChanges(found, action.bindings, found.bindings); - if (bindingChanges.del.length > 0 || bindingChanges.create.length > 0) { - await this.actionBindingsHandler.processChanges(bindingChanges); + const updatedFields = await this.actionChanges(action, found); + // Update action if there is something to update + if (!_.isEmpty(updatedFields)) { + await this.client.actions.update({ action_id: found.id }, updatedFields); } - return found; } @@ -223,30 +370,23 @@ export default class ActionHandler extends DefaultHandler { }).promise(); } - async getActionBinding(actionId) { - const bindings = await this.actionBindingsHandler.getType(); - return bindings.filter(b => b.action.id === actionId); - } - async calcChanges(actionsAssets, existing) { // Calculate the changes required between two sets of assets. const update = []; let del = [ ...existing ]; const create = []; - actionsAssets.forEach(async (action) => { const found = existing.find(existingAction => existingAction.name === action.name); if (found) { del = del.filter(e => e.id !== found.id); // current version changes - const currentVersionChanges = this.actionVersionHandler.calcCurrentVersionChanges(found.id, action.current_version, found.current_version); - const hasCurrentVersionChanges = currentVersionChanges.del.length > 0 || currentVersionChanges.create.length > 0; - // bindings changes - const bindingChanges = await this.actionBindingsHandler.calcChanges(found, action.bindings, found.bindings); - const hasBindingChanges = bindingChanges.del.length > 0 || bindingChanges.create.length > 0; + const currentVersionChanges = await this.calcCurrentVersionChanges(found.id, action.current_version, found.current_version); if (action.name !== found.name - || hasCurrentVersionChanges - || hasBindingChanges) { + || action.code !== found.code + || !areArraysEquals(action.dependencies, found.dependencies) + || !areArraysEquals((action.secrets || []).map(s => s.name), (found.secrets || []).map(s => s.name)) + || action.runtime !== found.runtime + || currentVersionChanges.create.length > 0) { update.push(action); } } else { diff --git a/src/auth0/handlers/index.js b/src/auth0/handlers/index.js index 614b3d3..aab6db0 100644 --- a/src/auth0/handlers/index.js +++ b/src/auth0/handlers/index.js @@ -21,6 +21,7 @@ import * as branding from './branding'; import * as prompts from './prompts'; import * as migrations from './migrations'; import * as actions from './actions'; +import * as triggers from './triggers'; import * as organizations from './organizations'; export { @@ -47,5 +48,6 @@ export { prompts, migrations, actions, + triggers, organizations }; diff --git a/src/auth0/handlers/triggers.js b/src/auth0/handlers/triggers.js new file mode 100644 index 0000000..1e285bf --- /dev/null +++ b/src/auth0/handlers/triggers.js @@ -0,0 +1,135 @@ +/* eslint-disable consistent-return */ +import _ from 'lodash'; +import DefaultHandler, { order } from './default'; +import log from '../../logger'; +import { areArraysEquals } from '../../utils'; + +export const schema = { + type: 'object', + items: { + type: 'object', + additionalProperties: true, + properties: { + trigger_id: { + type: 'object', + properties: { + action_name: { type: 'string', default: '' }, + display_name: { type: 'string', default: '' } + } + } + } + } +}; + +export default class TriggersHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'triggers' + }); + } + + async getType() { + if (this.existing) { + return this.existing; + } + + // in case client version does not support actions + if ( + !this.client.actions + || typeof this.client.actions.getAllTriggers !== 'function' + ) { + return []; + } + + const res = await this.client.actions.getAllTriggers(); + + const triggers = _(res.triggers).map('id').uniq().value(); + + const triggerBindings = {}; + + for (let i = 0; i < triggers.length; i++) { + const triggerId = triggers[i]; + + try { + const { bindings } = await this.client.actions.getTriggerBindings({ trigger_id: triggerId }); + triggerBindings[triggerId] = bindings.map(binding => ({ action_name: binding.action.name, display_name: binding.display_name })); + } catch (err) { + if (err.statusCode === 404 || err.statusCode === 501) { + return null; + } + throw err; + } + } + + return triggerBindings; + } + + async updateTrigger(updates) { + const triggerId = updates.trigger_id; + const bindings = updates.bindings.map(binding => ({ ref: { type: 'action_name', value: binding.action_name }, display_name: binding.display_name })); + const data = { bindings: bindings }; + const params = { trigger_id: triggerId }; + try { + await this.client.actions.updateTriggerBindings(params, data); + } catch (err) { + if (err.statusCode === 404 || err.statusCode === 501) { + return null; + } + throw err; + } + return triggerId; + } + + async updateTriggers(updates) { + await this.client.pool.addEachTask({ + data: updates || [], + generator: item => this.updateTrigger(item).then((triggerId) => { + this.didUpdate({ trigger: triggerId }); + this.updated += 1; + }).catch((err) => { + throw new Error(`Problem updating ${this.type} ${this.objString(item)}\n${err}`); + }) + }).promise(); + } + + async calcChanges(triggerId, bindings, existing) { + // Calculate the changes required between two sets of assets. + const update = []; + + if (!areArraysEquals(bindings, existing)) { + update.push({ trigger_id: triggerId, bindings: bindings }); + } + // Figure out what needs to be deleted and created + return { update }; + } + + @order('80') + async processChanges(assets) { + // eslint-disable-next-line prefer-destructuring + const triggers = assets.triggers; + + // Do nothing if not set + if (!triggers) return {}; + + const existing = await this.getType(); + + /* eslint-disable guard-for-in */ + /* eslint-disable no-restricted-syntax */ + for (const triggerId in existing) { + const changes = await this.calcChanges(triggerId, triggers[triggerId], existing[triggerId]); + + log.info(`Start processChanges for trigger ${triggerId} [update:${changes.update.length}]`); + const myChanges = [ { update: changes.update } ]; + await Promise.all(myChanges.map(async (change) => { + switch (true) { + case change.update && change.update.length > 0: + await this.updateTriggers(change.update); + break; + default: + break; + } + })); + } + } +} diff --git a/src/constants.js b/src/constants.js index 3de9a5a..78eaf34 100644 --- a/src/constants.js +++ b/src/constants.js @@ -10,6 +10,7 @@ constants.HOOKS_HIDDEN_SECRET_VALUE = '_VALUE_NOT_SHOWN_'; constants.HOOKS_DIRECTORY = 'hooks'; constants.ACTIONS_DIRECTORY = 'actions'; +constants.TRIGGERS_DIRECTORY = 'triggers'; constants.RULES_CONFIGS_DIRECTORY = 'rules-configs'; diff --git a/src/utils.js b/src/utils.js index b351c48..b9b0dcd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -232,6 +232,9 @@ export function filterExcluded(changes, exclude) { }; } -export function isArrayEqual(x, y) { +export function areArraysEquals(x, y) { + if (x.length !== y.length) { + return false; + } return _(x).differenceWith(y, _.isEqual).isEmpty(); } diff --git a/tests/auth0/handlers/actions.tests.js b/tests/auth0/handlers/actions.tests.js index 85becd1..97c059b 100644 --- a/tests/auth0/handlers/actions.tests.js +++ b/tests/auth0/handlers/actions.tests.js @@ -89,8 +89,7 @@ describe('#actions handler', () => { dependencies: [], secrets: [], runtime: 'node12' - }, - bindings: [ { trigger_id: 'post-login' } ] + } } ]; @@ -145,21 +144,8 @@ describe('#actions handler', () => { } return Promise.resolve({ actions: [ { name: action.name, supported_triggers: action.supported_triggers, id: actionId } ] }); - } - }, - actionVersions: { - create: () => Promise.resolve(version), - upsertDraft: () => Promise.resolve(version) - }, - actionBindings: { - getAll: () => Promise.resolve({ bindings: [] }), - create: () => Promise.resolve({ - id: '35409a5b-0326-4e81-ad9b-ac19502cee58', - trigger_id: 'post-login', - created_at: '2020-12-08T21:26:21.982298158Z', - updated_at: '2020-12-08T21:26:21.982298158Z', - display_name: 'action-test' - }) + }, + createVersion: () => Promise.resolve(version) }, pool, getAllCalled: false @@ -186,23 +172,21 @@ describe('#actions handler', () => { { id: 'action-id-1', name: 'action-test-1', + secrets: [], supported_triggers: [ { id: 'post-login', version: 'v1' } ], - current_version: { id: version.id }, - bindings: [] + current_version: { id: version.id } } ]; const auth0 = { actions: { - getAll: () => Promise.resolve({ actions: actionsData }) - }, - actionVersions: { - get: (params) => { + getAll: () => Promise.resolve({ actions: actionsData }), + getVersions: (params) => { expect(params.action_id).to.equal('action-id-1'); expect(params.version_id).to.equal('version-id'); return Promise.resolve(version); @@ -269,85 +253,6 @@ describe('#actions handler', () => { } }); - - it('should update action creating binding', async () => { - const action = { - name: 'action-test', - supported_triggers: [ { - id: 'post-login', - version: 'v1' - } ], - current_version: { - code: '/** @type {PostLoginAction} */\nmodule.exports = async (event, context) => {\n console.log(\'new version\');\n return {};\n };\n ', - dependencies: [], - secrets: [], - runtime: 'node12' - }, - bindings: [ { trigger_id: 'post-login' } ] - }; - - const auth0 = { - actions: { - create: () => Promise.resolve([]), - update: () => Promise.resolve([]), - delete: () => Promise.resolve([]), - getAll: () => Promise.resolve({ - actions: [ - { - id: '1', - name: 'action-test', - supported_triggers: [ - { - id: 'post-login', - version: 'v1' - } - ], - current_version: { - code: '/** @type {PostLoginAction} */\nmodule.exports = async (event, context) => {\n console.log(\'new version\');\n return {};\n };\n ', - dependencies: [], - secrets: [], - runtime: 'node12' - } - } - ] - }) - }, - actionBindings: { - getAll: () => Promise.resolve({ bindings: [] }), - updateList: () => Promise.resolve() - }, - actionVersions: { - get: () => Promise.resolve({ - action: {}, - code: - '/** @type {PostLoginAction} */\nmodule.exports = async (event, context) => {\n console.log(\'new version\');\n return {};\n };\n ', - dependencies: [], - runtime: 'node12', - id: '0906fe5b-f4d6-44ec-a8f1-3c05fc186483', - deployed: true, - number: 1, - built_at: '2020-12-03T15:20:54.413725492Z', - status: 'built', - created_at: '2020-12-03T15:20:52.094497448Z', - updated_at: '2020-12-03T15:20:54.415669983Z' - }) - }, - createActionBinding: () => Promise.resolve({ - id: '35409a5b-0326-4e81-ad9b-ac19502cee58', - trigger_id: 'post-login', - created_at: '2020-12-08T21:26:21.982298158Z', - updated_at: '2020-12-08T21:26:21.982298158Z', - display_name: 'action-test' - }), - pool - }; - - const handler = new actions.default({ client: auth0, config }); - const stageFn = Object.getPrototypeOf(handler).processChanges; - - await stageFn.apply(handler, [ { actions: [ action ] } ]); - }); - it('should remove action', async () => { const auth0 = { actions: { @@ -371,25 +276,8 @@ describe('#actions handler', () => { ] } ] - }) - }, - actionBindings: { - getAll: () => Promise.resolve({ - bindings: [ { - id: '35409a5b-0326-4e81-ad9b-ac19502cee58', - trigger_id: 'post-login', - display_name: 'action-test' - } ] }), - delete: (data) => { - expect(data).to.be.an('object'); - expect(data.id).to.equal('35409a5b-0326-4e81-ad9b-ac19502cee58'); - expect(data.trigger_id).to.equal('post-login'); - return Promise.resolve(data); - } - }, - actionVersions: { - get: () => Promise.resolve({ + getVersion: () => Promise.resolve({ action: {}, code: '/** @type {PostLoginAction} */\nmodule.exports = async (event, context) => {\n console.log(\'new version\');\n return {};\n };\n ', diff --git a/tests/auth0/handlers/triggers.tests.js b/tests/auth0/handlers/triggers.tests.js new file mode 100644 index 0000000..f1ae002 --- /dev/null +++ b/tests/auth0/handlers/triggers.tests.js @@ -0,0 +1,151 @@ +const { expect } = require('chai'); +const triggers = require('../../../src/auth0/handlers/triggers'); + +const pool = { + addEachTask: (data) => { + if (data.data && data.data.length) { + data.generator(data.data[0]); + } + return { promise: () => null }; + } +}; + +describe('#triggers handler', () => { + const config = function(key) { + return config.data && config.data[key]; + }; + + describe('#Triggers validate', () => { + it('should pass validation', async () => { + const auth0 = { + actions: { + getTriggerBindings: () => [] + } + }; + + const handler = new triggers.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).validate; + const data = { + 'post-login': [ { action_name: 'action-one', display_name: 'dysplay-name' } ], + 'credentials-exchange': [], + 'pre-user-registration': [], + 'post-user-registration': [], + 'post-change-password': [], + 'send-phone-message': [] + }; + + await stageFn.apply(handler, [ { triggers: data } ]); + }); + }); + + describe('#triggers process', () => { + it('should bind a trigger', async () => { + const triggersBindings = { + 'post-login': [ { action_name: 'action-one', display_name: 'dysplay-name' } ], + 'credentials-exchange': [], + 'pre-user-registration': [], + 'post-user-registration': [], + 'post-change-password': [], + 'send-phone-message': [] + }; + + const auth0 = { + actions: { + getAllTriggers: () => Promise.resolve(triggersBindings), + updateTriggerBindings: () => Promise.resolve([]) + }, + pool, + getAllCalled: false + }; + + const handler = new triggers.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { triggers: triggers } ]); + }); + + it('should get all triggers', async () => { + const triggersBindings = { + 'post-login': [ { action_name: 'action-one', display_name: 'display-name' } ], + 'credentials-exchange': [], + 'pre-user-registration': [], + 'post-user-registration': [], + 'post-change-password': [], + 'send-phone-message': [] + }; + + const auth0 = { + actions: { + getAllTriggers: () => Promise.resolve({ triggers: [ { id: 'post-login' }, { id: 'credentials-exchange' }, { id: 'pre-user-registration' }, { id: 'post-user-registration' }, { id: 'post-change-password' }, { id: 'send-phone-message' } ] }), + getTriggerBindings: (params) => { + let res = {}; + switch (params.trigger_id) { + case 'post-login': + res = { bindings: [ { action: { name: 'action-one' }, display_name: 'display-name' } ] }; + break; + case 'credentials-exchange': + res = { bindings: [] }; + break; + case 'pre-user-registration': + res = { bindings: [] }; + break; + case 'post-user-registration': + res = { bindings: [] }; + break; + case 'post-change-password': + res = { bindings: [] }; + break; + case 'send-phone-message': + res = { bindings: [] }; + break; + default: + break; + } + return Promise.resolve(res); + } + } + }; + + const handler = new triggers.default({ client: auth0, config }); + const data = await handler.getType(); + expect(data).to.deep.equal(triggersBindings); + }); + + it('should return an empty array for 404 status code', async () => { + const auth0 = { + actions: { + getTriggerBindings: () => { + const error = new Error('Not found'); + error.statusCode = 404; + throw error; + } + }, + pool + }; + + const handler = new triggers.default({ client: auth0, config }); + const data = await handler.getType(); + expect(data).to.deep.equal([]); + }); + + it('should throw an error for all other failed requests', async () => { + const auth0 = { + actions: { + getTriggerBindings: () => { + const error = new Error('Bad request'); + error.statusCode = 500; + throw error; + } + }, + pool + }; + + const handler = new triggers.default({ client: auth0, config }); + try { + await handler.getType(); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + } + }); + }); +});