From b08b2cd271365adcde3f40c912da8037709f8c65 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Thu, 14 Mar 2024 20:38:28 -0600 Subject: [PATCH 1/9] feat: postInstallAction --- src/constants/app-config-schema.ts | 11 +++++ src/types/mt-app-config.ts | 1 + src/util/app-config.ts | 24 +++++++++++ .../app-config-schema.test.ts.snap | 14 +++++++ .../__snapshots__/app-config.test.ts.snap | 35 ++++++++++++++-- tests/unit/app-config-schema.test.ts | 42 +++++++++++++++++++ tests/unit/app-config.test.ts | 28 +++++++++++++ 7 files changed, 152 insertions(+), 3 deletions(-) diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index 74357b1..47aa60c 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -103,6 +103,13 @@ const ACTION_WITH_SLUG_SCHEMA = ACTION_SCHEMA.concat( }), ); +const POWERUP_SCHEMA = Joi.object({ + slug: Joi.string().required(), + name: Joi.string().optional(), + data: Joi.object().optional(), + imageFileReference: Joi.object().optional(), +}); + export const PRODUCT_VARIANT_SCHEMA = Joi.object({ slug: Joi.string().required(), name: Joi.string().optional(), @@ -131,4 +138,8 @@ export const APP_CONFIG_SCHEMA = Joi.object({ countables: Joi.array().items(COUNTABLE_SCHEMA).optional(), products: Joi.array().items(PRODUCT_SCHEMA).optional(), actions: Joi.array().items(ACTION_WITH_SLUG_SCHEMA).optional(), + powerups: Joi.array().items(POWERUP_SCHEMA).optional(), + postInstallAction: Joi.alternatives() + .try(Joi.string(), ACTION_SCHEMA) + .optional(), }); diff --git a/src/types/mt-app-config.ts b/src/types/mt-app-config.ts index ea14d1b..75774ad 100644 --- a/src/types/mt-app-config.ts +++ b/src/types/mt-app-config.ts @@ -9,6 +9,7 @@ export interface MothertreeAppConfig { countables: MothertreeCountableConfig[]; products: MothertreeProductConfig[]; productVariants: MothertreeProductVariantConfig[]; + postInstallActionPath?: string; } export interface MothertreeEmbedConfig { diff --git a/src/util/app-config.ts b/src/util/app-config.ts index 6c18f70..339d5cd 100644 --- a/src/util/app-config.ts +++ b/src/util/app-config.ts @@ -226,6 +226,30 @@ export function convertAppConfigToMothertreeConfig(appConfig: AppConfig) { ); } + if (typeof appConfig.postInstallAction === 'string') { + mtAppConfig.postInstallActionPath = appConfig.postInstallAction; + } + // if postInstallAction is an object, we need to convert it to a MothertreeActionConfig, + // and add it to the actions array, and set the postInstallActionPath + else if (appConfig.postInstallAction != null) { + // add a slug to the action config + const postInstallAction = { + ...appConfig.postInstallAction, + slug: 'post-install-action', + }; + + // add the action to the actions array + convertActionConfigsToMothertreeActionConfigs( + [postInstallAction], + mtAppConfig, + ); + + // set the postInstallActionPath + mtAppConfig.postInstallActionPath = makeLocalActionPath( + postInstallAction.slug, + ); + } + if (appConfig.products) { convertProductConfigsToMothertreeProductAndVariantConfigs( appConfig.products, diff --git a/tests/unit/__snapshots__/app-config-schema.test.ts.snap b/tests/unit/__snapshots__/app-config-schema.test.ts.snap index 2f396f1..589bea3 100644 --- a/tests/unit/__snapshots__/app-config-schema.test.ts.snap +++ b/tests/unit/__snapshots__/app-config-schema.test.ts.snap @@ -2,6 +2,20 @@ exports[`app-config-schema > ACTION_SCHEMA > should throw an error if a sub-action is invalid 1`] = `[ValidationError: "actions[0].url" is required]`; +exports[`app-config-schema > APP_CONFIG_SCHEMA > should throw an error if postInstallAction is invalid 1`] = ` +[ValidationError: { + "path": "@truffle/test-app", + "name": "test-app", + "cliVersion": "1.0.0", + "postInstallAction": { + "operation": "webhook", + "url" [1]: -- missing -- + } +} + +[1] "postInstallAction.url" is required] +`; + exports[`app-config-schema > EMBED_SCHEMA > should throw an error if contentPageType is invalid 1`] = ` [ValidationError: { "slug": "test-embed", diff --git a/tests/unit/__snapshots__/app-config.test.ts.snap b/tests/unit/__snapshots__/app-config.test.ts.snap index f536c4c..dd05782 100644 --- a/tests/unit/__snapshots__/app-config.test.ts.snap +++ b/tests/unit/__snapshots__/app-config.test.ts.snap @@ -6,6 +6,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con "inputsTemplate": { "url": "https://example.com", }, + "isDirectExecutionAllowed": false, "operation": "test-operation", "slug": "test-action", }, @@ -18,6 +19,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con "inputsTemplate": { "url": "https://example.com", }, + "isDirectExecutionAllowed": false, "operation": "test-operation", "slug": "test-action-step-1", }, @@ -29,6 +31,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con ], "strategy": "sequential", }, + "isDirectExecutionAllowed": false, "operation": "workflow", "slug": "test-action", }, @@ -42,6 +45,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a basi "inputsTemplate": { "url": "https://example.com", }, + "isDirectExecutionAllowed": false, "operation": "webhook", "slug": "test-action", }, @@ -77,7 +81,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a basi "assetTemplates": [ { "count": 100, - "entityPath": "@truffle/tips/_Countable/sparks", + "entityPath": "@truffle/sparks/_Countable/sparks:@truffle", "metadata": {}, "receivers": [ { @@ -112,6 +116,29 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a basi } `; +exports[`app-config > convertAppConfigToMothertreeConfig > should convert a postInstallAction that is defined as an action object 1`] = ` +{ + "actions": [ + { + "inputsTemplate": { + "url": "https://example.com", + }, + "isDirectExecutionAllowed": false, + "operation": "webhook", + "slug": "post-install-action", + }, + ], + "cliVersion": "0.0.0", + "countables": [], + "embeds": [], + "name": "test-app", + "path": "@truffle/test-app", + "postInstallActionPath": "./_Action/post-install-action", + "productVariants": [], + "products": [], +} +`; + exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs > should convert a basic product 1`] = ` { "actions": [ @@ -129,7 +156,7 @@ exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs "assetTemplates": [ { "count": 100, - "entityPath": "@truffle/tips/_Countable/sparks", + "entityPath": "@truffle/sparks/_Countable/sparks:@truffle", "metadata": {}, "receivers": [ { @@ -178,6 +205,7 @@ exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs "inputsTemplate": { "url": "https://example.com", }, + "isDirectExecutionAllowed": false, "operation": "webhook", "slug": "test-product-test-variant-workflow-step-1", }, @@ -189,6 +217,7 @@ exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs ], "strategy": "sequential", }, + "isDirectExecutionAllowed": false, "operation": "workflow", "slug": "test-product-test-variant-workflow", }, @@ -199,7 +228,7 @@ exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs "assetTemplates": [ { "count": 100, - "entityPath": "@truffle/tips/_Countable/sparks", + "entityPath": "@truffle/sparks/_Countable/sparks:@truffle", "metadata": {}, "receivers": [ { diff --git a/tests/unit/app-config-schema.test.ts b/tests/unit/app-config-schema.test.ts index 252d058..0a7c82e 100644 --- a/tests/unit/app-config-schema.test.ts +++ b/tests/unit/app-config-schema.test.ts @@ -170,4 +170,46 @@ describe('app-config-schema', () => { expect(error).toMatchSnapshot(); }); }); + + describe('APP_CONFIG_SCHEMA', () => { + it('should allow a path to be defined for postInstallAction', () => { + const config = { + path: '@truffle/test-app', + name: 'test-app', + cliVersion: '1.0.0', + postInstallAction: '@truffle/app/_Action/test-action', + }; + + Joi.assert(config, schemas.APP_CONFIG_SCHEMA); + }); + + it('should allow an action to be defined for postInstallAction', () => { + const config = { + path: '@truffle/test-app', + name: 'test-app', + cliVersion: '1.0.0', + postInstallAction: { + operation: 'webhook', + url: 'https://example.com/webhook', + }, + }; + + Joi.assert(config, schemas.APP_CONFIG_SCHEMA); + }); + + it('should throw an error if postInstallAction is invalid', () => { + const config = { + path: '@truffle/test-app', + name: 'test-app', + cliVersion: '1.0.0', + postInstallAction: { + operation: 'webhook', + }, + }; + + expect(() => + Joi.assert(config, schemas.APP_CONFIG_SCHEMA), + ).toThrowErrorMatchingSnapshot(); + }); + }); }); diff --git a/tests/unit/app-config.test.ts b/tests/unit/app-config.test.ts index 79866b4..de76a66 100644 --- a/tests/unit/app-config.test.ts +++ b/tests/unit/app-config.test.ts @@ -5,6 +5,7 @@ import { convertProductConfigsToMothertreeProductAndVariantConfigs, validateAppConfig, } from '../../src/util/app-config'; +import { MothertreeAppConfig } from '../../src/types/mt-app-config'; describe('app-config', () => { describe('validateAppConfig', () => { @@ -209,5 +210,32 @@ describe('app-config', () => { expect(convertAppConfigToMothertreeConfig(appConfig)).toMatchSnapshot(); }); + + it('should define a postInstallActionPath for a postInstallAction that is defined as a path', () => { + const appConfig = { + path: '@truffle/test-app', + name: 'test-app', + cliVersion: '0.0.0', + postInstallAction: './_Action/post-install', + }; + + expect( + convertAppConfigToMothertreeConfig(appConfig).postInstallActionPath, + ).toBe(appConfig.postInstallAction); + }); + + it('should convert a postInstallAction that is defined as an action object', () => { + const appConfig = { + path: '@truffle/test-app', + name: 'test-app', + cliVersion: '0.0.0', + postInstallAction: { + operation: 'webhook', + url: 'https://example.com', + }, + }; + + expect(convertAppConfigToMothertreeConfig(appConfig)).toMatchSnapshot(); + }); }); }); From 5060704297415b34fa9f35ee802e9bd4bdc88494 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 18 Mar 2024 14:19:00 -0600 Subject: [PATCH 2/9] feat: add powerups and apply-powerup action --- src/constants/app-config-schema.ts | 54 ++++++--- src/types/mt-app-config.ts | 8 ++ src/util/app-config.ts | 108 ++++++++++++------ .../app-config-schema.test.ts.snap | 12 ++ .../__snapshots__/app-config.test.ts.snap | 64 ++++++++++- tests/unit/app-config-schema.test.ts | 41 +++++++ tests/unit/app-config.test.ts | 70 +++++++++++- 7 files changed, 304 insertions(+), 53 deletions(-) diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index 47aa60c..05b220d 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -19,9 +19,9 @@ export const OPERATION_TYPES = [ 'webhook', 'workflow', 'exchange', + 'apply-powerup', // TODO: support these // 'conditional', - // 'apply-powerup', ] as const; export type OperationType = (typeof OPERATION_TYPES)[number]; @@ -73,23 +73,60 @@ export const ASSET_SCHEMA = Joi.object({ quantity: Joi.number().required(), }); +const POWERUP_SCHEMA = Joi.object({ + slug: Joi.string().required(), + name: Joi.string().optional(), + data: Joi.object().optional(), + imageFileReference: Joi.object().optional(), +}); + export const ACTION_SCHEMA = Joi.object({ operation: Joi.string() .valid(...OPERATION_TYPES) .required(), + + // webhook inputs url: Joi.when('operation', { is: 'webhook', then: Joi.string().required(), }), + // workflow inputs + strategy: Joi.when('operation', { + is: 'workflow', + then: Joi.string().valid('sequential', 'parallel').required(), + }), + actions: Joi.when('operation', { is: 'workflow', then: Joi.array().items(Joi.link('#actionSchema')).required(), }), - strategy: Joi.when('operation', { - is: 'workflow', - then: Joi.string().valid('sequential', 'parallel').required(), + // exchange inputs + assets: Joi.when('operation', { + is: 'exchange', + then: Joi.array().items(ASSET_SCHEMA).required(), + }), + + // apply-pwerup inputs + powerup: Joi.when('operation', { + is: 'apply-powerup', + then: Joi.alternatives().try(Joi.string(), POWERUP_SCHEMA).required(), + }), + + targetType: Joi.when('operation', { + is: 'apply-powerup', + then: Joi.string().required(), + }), + + targetId: Joi.when('operation', { + is: 'apply-powerup', + then: Joi.string().required(), + }), + + ttlSeconds: Joi.when('operation', { + is: 'apply-powerup', + then: Joi.number().required(), }), inputsTemplate: Joi.object().optional(), @@ -103,21 +140,12 @@ const ACTION_WITH_SLUG_SCHEMA = ACTION_SCHEMA.concat( }), ); -const POWERUP_SCHEMA = Joi.object({ - slug: Joi.string().required(), - name: Joi.string().optional(), - data: Joi.object().optional(), - imageFileReference: Joi.object().optional(), -}); - export const PRODUCT_VARIANT_SCHEMA = Joi.object({ slug: Joi.string().required(), name: Joi.string().optional(), price: Joi.number().required(), description: Joi.string().optional(), - action: ACTION_SCHEMA.optional(), - assets: Joi.array().items(ASSET_SCHEMA).optional(), }); diff --git a/src/types/mt-app-config.ts b/src/types/mt-app-config.ts index 75774ad..0b20b04 100644 --- a/src/types/mt-app-config.ts +++ b/src/types/mt-app-config.ts @@ -9,6 +9,7 @@ export interface MothertreeAppConfig { countables: MothertreeCountableConfig[]; products: MothertreeProductConfig[]; productVariants: MothertreeProductVariantConfig[]; + powerups: MothertreePowerupConfig[]; postInstallActionPath?: string; } @@ -97,3 +98,10 @@ export interface MothertreeAssetParticipantTemplate { entityId: string | '{{USE_PROVIDED}}' | '{{USE_USER_ID}}' | '{{USE_ORG_ID}}'; share: number; } + +export interface MothertreePowerupConfig { + slug: string; + name?: string; + data?: object; + imageFileReference?: object; +} diff --git a/src/util/app-config.ts b/src/util/app-config.ts index 339d5cd..6d783a0 100644 --- a/src/util/app-config.ts +++ b/src/util/app-config.ts @@ -44,6 +44,10 @@ export function makeLocalActionPath(actionSlug: string) { return `./_Action/${actionSlug}`; } +export function makeLocalPowerupPath(powerupSlug: string) { + return `./_Powerup/${powerupSlug}`; +} + type ActionConfig = NonNullable[number]; type EmbeddedActionConfig = Omit; @@ -54,40 +58,70 @@ export function convertActionConfigsToMothertreeActionConfigs( mtAppConfig.actions.push( ...actionConfigs.map((actionConfig) => { let inputsTemplate: Record; - if (actionConfig.operation === 'workflow') { - inputsTemplate = { - // if the action is a workflow, we need to create a new action for each sub-action - // and add the sub-action paths to the inputsTemplate - actionPaths: actionConfig.actions.map( - // a sub action could either be an action path or an embedded action - (subAction: EmbeddedActionConfig | string, subActionIdx) => { - // if the sub-action is a string, it's an action path, so just return it - if (typeof subAction === 'string') { - return subAction; - } - - // if the sub-action is an embedded action, we need to create a new action - const slug = `${actionConfig.slug}-step-${subActionIdx}`; - - // add the sub-action to the actions array - convertActionConfigsToMothertreeActionConfigs( - [{ ...subAction, slug }], - mtAppConfig, - ); - - return makeLocalActionPath(slug); - }, - ), - - // pass along the strategy - strategy: actionConfig.strategy, - }; - } else { - // if the action is not a workflow, we can just fill in url and assets - inputsTemplate = { - ...actionConfig.inputsTemplate, - ..._.pick(actionConfig, ['url', 'assets']), - }; + switch (actionConfig.operation) { + case 'workflow': { + inputsTemplate = { + // if the action is a workflow, we need to create a new action for each sub-action + // and add the sub-action paths to the inputsTemplate + actionPaths: actionConfig.actions.map( + // a sub action could either be an action path or an embedded action + (subAction: EmbeddedActionConfig | string, subActionIdx) => { + // if the sub-action is a string, it's an action path, so just return it + if (typeof subAction === 'string') { + return subAction; + } + + // if the sub-action is an embedded action, we need to create a new action + const slug = `${actionConfig.slug}-step-${subActionIdx}`; + + // add the sub-action to the actions array + convertActionConfigsToMothertreeActionConfigs( + [{ ...subAction, slug }], + mtAppConfig, + ); + + return makeLocalActionPath(slug); + }, + ), + + // pass along the strategy + strategy: actionConfig.strategy, + }; + break; + } + + case 'apply-powerup': { + // actionConfig.powerup is either a string or an object containing a powerup config + let powerupPath: string = ''; + if (typeof actionConfig.powerup === 'string') { + powerupPath = actionConfig.powerup; + } else { + mtAppConfig.powerups.push(actionConfig.powerup); + powerupPath = makeLocalPowerupPath(actionConfig.powerup.slug); + } + + inputsTemplate = { + powerupPath: powerupPath, + targetType: actionConfig.targetType, + targetId: actionConfig.targetId, + ttlSeconds: actionConfig.ttlSeconds, + }; + break; + } + + case 'webhook': { + inputsTemplate = { + url: actionConfig.url, + }; + break; + } + + case 'exchange': { + inputsTemplate = { + assets: actionConfig.assets, + }; + break; + } } return { @@ -180,7 +214,8 @@ export function convertProductConfigsToMothertreeProductAndVariantConfigs( // tbh I don't know why we have to cast this } as MothertreeAssetParticipantTemplate, ], - // TODO: if we want, we can change this to do rev/share split with org, us, and dev + // TODO: if we want, we can change this to do rev/share split with org, us, and dev... + // this might not be the place to do that though receivers: [ { entityType: 'org', @@ -213,8 +248,9 @@ export function convertAppConfigToMothertreeConfig(appConfig: AppConfig) { name: appConfig.name, cliVersion: appConfig.cliVersion, embeds: appConfig.embeds ?? [], - actions: [], countables: appConfig.countables ?? [], + powerups: appConfig.powerups ?? [], + actions: [], products: [], productVariants: [], }; diff --git a/tests/unit/__snapshots__/app-config-schema.test.ts.snap b/tests/unit/__snapshots__/app-config-schema.test.ts.snap index 589bea3..12b6596 100644 --- a/tests/unit/__snapshots__/app-config-schema.test.ts.snap +++ b/tests/unit/__snapshots__/app-config-schema.test.ts.snap @@ -2,6 +2,18 @@ exports[`app-config-schema > ACTION_SCHEMA > should throw an error if a sub-action is invalid 1`] = `[ValidationError: "actions[0].url" is required]`; +exports[`app-config-schema > ACTION_SCHEMA > should throw an error if an apply-powerup action is invalid 1`] = ` +[ValidationError: { + "operation": "apply-powerup", + "targetType": "chat", + "targetId": "chatId", + "ttlSeconds": 60, + "powerup" [1]: -- missing -- +} + +[1] "powerup" is required] +`; + exports[`app-config-schema > APP_CONFIG_SCHEMA > should throw an error if postInstallAction is invalid 1`] = ` [ValidationError: { "path": "@truffle/test-app", diff --git a/tests/unit/__snapshots__/app-config.test.ts.snap b/tests/unit/__snapshots__/app-config.test.ts.snap index dd05782..b59fb18 100644 --- a/tests/unit/__snapshots__/app-config.test.ts.snap +++ b/tests/unit/__snapshots__/app-config.test.ts.snap @@ -1,5 +1,63 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should convert a basic apply-powerup action with a powerup object 1`] = ` +{ + "actions": [ + { + "inputsTemplate": { + "powerupPath": "./_Powerup/test-powerup", + "targetId": "test-target-id", + "targetType": "test-target-type", + "ttlSeconds": 60, + }, + "isDirectExecutionAllowed": false, + "operation": "apply-powerup", + "slug": "test-action", + }, + ], + "powerups": [ + { + "name": "Test Powerup", + "slug": "test-powerup", + }, + ], +} +`; + +exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should convert a basic apply-powerup action with a powerup path 1`] = ` +[ + { + "inputsTemplate": { + "powerupPath": "@truffle/app/_Powerup/test-powerup", + "targetId": "test-target-id", + "targetType": "test-target-type", + "ttlSeconds": 60, + }, + "isDirectExecutionAllowed": false, + "operation": "apply-powerup", + "slug": "test-action", + }, +] +`; + +exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should convert a basic exchange action 1`] = ` +[ + { + "inputsTemplate": { + "assets": [ + { + "path": "./_Asset/test-asset", + "quantity": 1, + }, + ], + }, + "isDirectExecutionAllowed": false, + "operation": "exchange", + "slug": "test-action", + }, +] +`; + exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should convert a basic webhook action 1`] = ` [ { @@ -7,7 +65,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con "url": "https://example.com", }, "isDirectExecutionAllowed": false, - "operation": "test-operation", + "operation": "webhook", "slug": "test-action", }, ] @@ -20,7 +78,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con "url": "https://example.com", }, "isDirectExecutionAllowed": false, - "operation": "test-operation", + "operation": "webhook", "slug": "test-action-step-1", }, { @@ -75,6 +133,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a basi ], "name": "test-app", "path": "@truffle/test-app", + "powerups": [], "productVariants": [ { "actionPath": "./_Action/default-exchange-action", @@ -134,6 +193,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a post "name": "test-app", "path": "@truffle/test-app", "postInstallActionPath": "./_Action/post-install-action", + "powerups": [], "productVariants": [], "products": [], } diff --git a/tests/unit/app-config-schema.test.ts b/tests/unit/app-config-schema.test.ts index 0a7c82e..262f858 100644 --- a/tests/unit/app-config-schema.test.ts +++ b/tests/unit/app-config-schema.test.ts @@ -155,6 +155,7 @@ describe('app-config-schema', () => { it('should throw an error if a sub-action is invalid', () => { const action = { operation: 'workflow', + strategy: 'sequential', actions: [ { operation: 'webhook', @@ -169,6 +170,46 @@ describe('app-config-schema', () => { const { error } = schemas.ACTION_SCHEMA.validate(action); expect(error).toMatchSnapshot(); }); + + it('validate an apply-powerup action with a path', () => { + const action = { + operation: 'apply-powerup', + powerup: '@truffle/test-app/_Powerup/test-powerup', + targetType: 'chat', + targetId: 'chatId', + ttlSeconds: 60, + }; + + Joi.assert(action, schemas.ACTION_SCHEMA); + }); + + it('should validate an apply powerup action with a powerup object', () => { + const action = { + operation: 'apply-powerup', + powerup: { + slug: 'test-powerup', + name: 'Test Powerup', + }, + targetType: 'chat', + targetId: 'chatId', + ttlSeconds: 60, + }; + + Joi.assert(action, schemas.ACTION_SCHEMA); + }); + + it('should throw an error if an apply-powerup action is invalid', () => { + const action = { + operation: 'apply-powerup', + targetType: 'chat', + targetId: 'chatId', + ttlSeconds: 60, + }; + + expect(() => + Joi.assert(action, schemas.ACTION_SCHEMA), + ).toThrowErrorMatchingSnapshot(); + }); }); describe('APP_CONFIG_SCHEMA', () => { diff --git a/tests/unit/app-config.test.ts b/tests/unit/app-config.test.ts index de76a66..0672683 100644 --- a/tests/unit/app-config.test.ts +++ b/tests/unit/app-config.test.ts @@ -63,7 +63,7 @@ describe('app-config', () => { const actionConfigs = [ { slug: 'test-action', - operation: 'test-operation', + operation: 'webhook', url: 'https://example.com', }, ]; @@ -86,7 +86,7 @@ describe('app-config', () => { './_Action/action-1', { slug: 'action-2', - operation: 'test-operation', + operation: 'webhook', url: 'https://example.com', }, ], @@ -100,6 +100,72 @@ describe('app-config', () => { expect(mtAppConfig.actions).toMatchSnapshot(); }); + + it('should convert a basic exchange action', () => { + const actionConfigs = [ + { + slug: 'test-action', + operation: 'exchange', + assets: [ + { + path: './_Asset/test-asset', + quantity: 1, + }, + ], + }, + ]; + const mtAppConfig = { + actions: [], + }; + + convertActionConfigsToMothertreeActionConfigs(actionConfigs, mtAppConfig); + + expect(mtAppConfig.actions).toMatchSnapshot(); + }); + + it('should convert a basic apply-powerup action with a powerup path', () => { + const actionConfigs = [ + { + slug: 'test-action', + operation: 'apply-powerup', + powerup: '@truffle/app/_Powerup/test-powerup', + targetType: 'test-target-type', + targetId: 'test-target-id', + ttlSeconds: 60, + }, + ]; + const mtAppConfig = { + actions: [], + }; + + convertActionConfigsToMothertreeActionConfigs(actionConfigs, mtAppConfig); + + expect(mtAppConfig.actions).toMatchSnapshot(); + }); + + it('should convert a basic apply-powerup action with a powerup object', () => { + const actionConfigs = [ + { + slug: 'test-action', + operation: 'apply-powerup', + powerup: { + slug: 'test-powerup', + name: 'Test Powerup', + }, + targetType: 'test-target-type', + targetId: 'test-target-id', + ttlSeconds: 60, + }, + ]; + const mtAppConfig = { + powerups: [], + actions: [], + }; + + convertActionConfigsToMothertreeActionConfigs(actionConfigs, mtAppConfig); + + expect(mtAppConfig).toMatchSnapshot(); + }); }); describe('convertProductConfigsToMothertreeProductAndVariantConfigs', () => { From ab2859908c1298981c77acb6b69f0e9ec50843b3 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 25 Mar 2024 10:01:20 -0600 Subject: [PATCH 3/9] feat: exchange action --- src/constants/app-config-schema.ts | 44 +++++++++++++++++++++++++++++- tests/unit/app-config.test.ts | 1 - 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index 05b220d..8306980 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -25,6 +25,15 @@ export const OPERATION_TYPES = [ ] as const; export type OperationType = (typeof OPERATION_TYPES)[number]; +export const ASSET_PARTICIPANT_ENTITY_TYPES = [ + 'user', + 'org-member', + 'org', + 'company', +] as const; +export type AssetParticipantEntityType = + (typeof ASSET_PARTICIPANT_ENTITY_TYPES)[number]; + const COUNTABLE_SCHEMA = Joi.object({ slug: Joi.string().required(), name: Joi.string().optional(), @@ -68,6 +77,7 @@ export const EMBED_SCHEMA = Joi.object({ .optional(), }); +// TODO: actually implement this in app-config.ts export const ASSET_SCHEMA = Joi.object({ path: Joi.string().required(), quantity: Joi.number().required(), @@ -80,6 +90,32 @@ const POWERUP_SCHEMA = Joi.object({ imageFileReference: Joi.object().optional(), }); +const ASSET_PARTICIPANT_TEMPLATE_SCHEMA = Joi.object({ + entityType: Joi.string() + .valid(...ASSET_PARTICIPANT_ENTITY_TYPES) + .required(), + entityId: Joi.string().required(), + share: Joi.number().required(), +}); + +const ASSET_TEMPLATE_SCHEMA = Joi.object({ + entityType: Joi.string().valid('countable', 'fiat'), + entityId: Joi.string(), + entityPath: Joi.string(), + count: Joi.alternatives() + .try( + Joi.number().required(), + Joi.string().valid('{{USE_PROVIDED}}').required(), + ) + .required(), + metadata: Joi.object().optional(), + senders: Joi.array().items(ASSET_PARTICIPANT_TEMPLATE_SCHEMA).required(), + receivers: Joi.array().items(ASSET_PARTICIPANT_TEMPLATE_SCHEMA).required(), +}) + // this makes it so that either entityId and entityType is required or entityPath is required + .with('entityId', 'entityType') + .xor('entityPath', 'entityId'); + export const ACTION_SCHEMA = Joi.object({ operation: Joi.string() .valid(...OPERATION_TYPES) @@ -105,7 +141,13 @@ export const ACTION_SCHEMA = Joi.object({ // exchange inputs assets: Joi.when('operation', { is: 'exchange', - then: Joi.array().items(ASSET_SCHEMA).required(), + then: Joi.alternatives() + .try( + Joi.string().valid('{{USE_SECURE_PROVIDED}}').required(), + Joi.array().items(ASSET_SCHEMA).required(), + Joi.array().items(ASSET_TEMPLATE_SCHEMA).required(), + ) + .required(), }), // apply-pwerup inputs diff --git a/tests/unit/app-config.test.ts b/tests/unit/app-config.test.ts index 0672683..a388d0b 100644 --- a/tests/unit/app-config.test.ts +++ b/tests/unit/app-config.test.ts @@ -5,7 +5,6 @@ import { convertProductConfigsToMothertreeProductAndVariantConfigs, validateAppConfig, } from '../../src/util/app-config'; -import { MothertreeAppConfig } from '../../src/types/mt-app-config'; describe('app-config', () => { describe('validateAppConfig', () => { From 4404a4726b9e676b96970eb217a64077a13818ab Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 25 Mar 2024 11:28:30 -0600 Subject: [PATCH 4/9] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4ba905..9f14bfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trufflehq/cli", - "version": "0.6.1", + "version": "0.6.3", "description": "The Truffle Developer Platform CLI", "main": "dist/cli.mjs", "bin": { From decc5d26e4e4e5af90ad6d5dc260953fa3b61874 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 25 Mar 2024 11:59:38 -0600 Subject: [PATCH 5/9] feat: data for actions --- src/constants/app-config-schema.ts | 7 ++++ src/util/app-config.ts | 1 + .../app-config-schema.test.ts.snap | 2 ++ .../__snapshots__/app-config.test.ts.snap | 21 +++++++++++ tests/unit/app-config-schema.test.ts | 35 +++++++++++++++++++ tests/unit/app-config.test.ts | 20 +++++++++++ 6 files changed, 86 insertions(+) diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index 8306980..39dc033 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -127,6 +127,13 @@ export const ACTION_SCHEMA = Joi.object({ then: Joi.string().required(), }), + data: Joi.when('operation', { + is: 'webhook', + then: Joi.alternatives() + .try(Joi.object(), Joi.string().valid('{{USE_PROVIDED}}')) + .optional(), + }), + // workflow inputs strategy: Joi.when('operation', { is: 'workflow', diff --git a/src/util/app-config.ts b/src/util/app-config.ts index 6d783a0..d413edd 100644 --- a/src/util/app-config.ts +++ b/src/util/app-config.ts @@ -112,6 +112,7 @@ export function convertActionConfigsToMothertreeActionConfigs( case 'webhook': { inputsTemplate = { url: actionConfig.url, + data: actionConfig.data, }; break; } diff --git a/tests/unit/__snapshots__/app-config-schema.test.ts.snap b/tests/unit/__snapshots__/app-config-schema.test.ts.snap index 12b6596..e052614 100644 --- a/tests/unit/__snapshots__/app-config-schema.test.ts.snap +++ b/tests/unit/__snapshots__/app-config-schema.test.ts.snap @@ -14,6 +14,8 @@ exports[`app-config-schema > ACTION_SCHEMA > should throw an error if an apply-p [1] "powerup" is required] `; +exports[`app-config-schema > ACTION_SCHEMA > should throw an error if data is an invalid string 1`] = `[ValidationError: "data" must be one of [object, {{USE_PROVIDED}}]]`; + exports[`app-config-schema > APP_CONFIG_SCHEMA > should throw an error if postInstallAction is invalid 1`] = ` [ValidationError: { "path": "@truffle/test-app", diff --git a/tests/unit/__snapshots__/app-config.test.ts.snap b/tests/unit/__snapshots__/app-config.test.ts.snap index b59fb18..f126565 100644 --- a/tests/unit/__snapshots__/app-config.test.ts.snap +++ b/tests/unit/__snapshots__/app-config.test.ts.snap @@ -62,6 +62,23 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con [ { "inputsTemplate": { + "data": undefined, + "url": "https://example.com", + }, + "isDirectExecutionAllowed": false, + "operation": "webhook", + "slug": "test-action", + }, +] +`; + +exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should convert a basic webhook action with data 1`] = ` +[ + { + "inputsTemplate": { + "data": { + "some": "data", + }, "url": "https://example.com", }, "isDirectExecutionAllowed": false, @@ -75,6 +92,7 @@ exports[`app-config > convertActionConfigsToMothertreeActionConfigs > should con [ { "inputsTemplate": { + "data": undefined, "url": "https://example.com", }, "isDirectExecutionAllowed": false, @@ -101,6 +119,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a basi "actions": [ { "inputsTemplate": { + "data": undefined, "url": "https://example.com", }, "isDirectExecutionAllowed": false, @@ -180,6 +199,7 @@ exports[`app-config > convertAppConfigToMothertreeConfig > should convert a post "actions": [ { "inputsTemplate": { + "data": undefined, "url": "https://example.com", }, "isDirectExecutionAllowed": false, @@ -263,6 +283,7 @@ exports[`app-config > convertProductConfigsToMothertreeProductAndVariantConfigs }, { "inputsTemplate": { + "data": undefined, "url": "https://example.com", }, "isDirectExecutionAllowed": false, diff --git a/tests/unit/app-config-schema.test.ts b/tests/unit/app-config-schema.test.ts index 262f858..2b41a10 100644 --- a/tests/unit/app-config-schema.test.ts +++ b/tests/unit/app-config-schema.test.ts @@ -132,6 +132,41 @@ describe('app-config-schema', () => { }); describe('ACTION_SCHEMA', () => { + it('should validate a webhook action with {{USE_PROVIDED}} data', () => { + const action = { + operation: 'webhook', + url: 'https://example.com/webhook', + data: '{{USE_PROVIDED}}', + }; + + const { error } = schemas.ACTION_SCHEMA.validate(action); + expect(error).toBeUndefined(); + }); + + it('should validate a webhook action with object data', () => { + const action = { + operation: 'webhook', + url: 'https://example.com/webhook', + data: { + some: 'data', + }, + }; + + const { error } = schemas.ACTION_SCHEMA.validate(action); + expect(error).toBeUndefined(); + }); + + it('should throw an error if data is an invalid string', () => { + const action = { + operation: 'webhook', + url: 'https://example.com/webhook', + data: 'invalid', + }; + + const { error } = schemas.ACTION_SCHEMA.validate(action); + expect(error).toMatchSnapshot(); + }); + it('should validate sub-actions in a workflow', () => { const action = { operation: 'workflow', diff --git a/tests/unit/app-config.test.ts b/tests/unit/app-config.test.ts index a388d0b..318e976 100644 --- a/tests/unit/app-config.test.ts +++ b/tests/unit/app-config.test.ts @@ -75,6 +75,26 @@ describe('app-config', () => { expect(mtAppConfig.actions).toMatchSnapshot(); }); + it('should convert a basic webhook action with data', () => { + const actionConfigs = [ + { + slug: 'test-action', + operation: 'webhook', + url: 'https://example.com', + data: { + some: 'data', + }, + }, + ]; + const mtAppConfig = { + actions: [], + }; + + convertActionConfigsToMothertreeActionConfigs(actionConfigs, mtAppConfig); + + expect(mtAppConfig.actions).toMatchSnapshot(); + }); + it('should convert a basic workflow action', () => { const actionConfigs = [ { From d444dc36c5eabea9e6139d98e15dde6586ed376d Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 25 Mar 2024 12:00:13 -0600 Subject: [PATCH 6/9] chore: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f14bfd..916d845 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trufflehq/cli", - "version": "0.6.3", + "version": "0.6.4", "description": "The Truffle Developer Platform CLI", "main": "dist/cli.mjs", "bin": { From 2427fc5357fa22bc02c75f57661e8c2602d06ed0 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Mon, 25 Mar 2024 15:07:02 -0600 Subject: [PATCH 7/9] fix: parentQuerySelector --- package.json | 2 +- src/constants/app-config-schema.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 916d845..00fabcc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trufflehq/cli", - "version": "0.6.4", + "version": "0.6.5", "description": "The Truffle Developer Platform CLI", "main": "dist/cli.mjs", "bin": { diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index 39dc033..b426a7e 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -72,6 +72,7 @@ export const EMBED_SCHEMA = Joi.object({ minTruffleVersion: Joi.string().optional(), maxTruffleVersion: Joi.string().optional(), deviceType: Joi.string().valid('desktop', 'mobile').optional(), + parentQuerySelector: Joi.string().optional(), status: Joi.string() .valid('published', 'experimental', 'disabled') .optional(), From 72dc751b7ffbce362705cf57442ebd1a8998ab29 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Wed, 24 Apr 2024 09:17:35 -0600 Subject: [PATCH 8/9] fix: add action name --- package.json | 2 +- src/constants/app-config-schema.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 00fabcc..b17c0d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@trufflehq/cli", - "version": "0.6.5", + "version": "0.6.7", "description": "The Truffle Developer Platform CLI", "main": "dist/cli.mjs", "bin": { diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index b426a7e..c8b7bf2 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -122,6 +122,8 @@ export const ACTION_SCHEMA = Joi.object({ .valid(...OPERATION_TYPES) .required(), + name: Joi.string().optional(), + // webhook inputs url: Joi.when('operation', { is: 'webhook', From 9664d1dbf4e5ccba490d2b30b74f99ee733c1376 Mon Sep 17 00:00:00 2001 From: Austin Fay Date: Wed, 24 Apr 2024 09:19:16 -0600 Subject: [PATCH 9/9] chore: remove unimplemented action schema --- src/constants/app-config-schema.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/constants/app-config-schema.ts b/src/constants/app-config-schema.ts index c8b7bf2..912a570 100644 --- a/src/constants/app-config-schema.ts +++ b/src/constants/app-config-schema.ts @@ -78,12 +78,6 @@ export const EMBED_SCHEMA = Joi.object({ .optional(), }); -// TODO: actually implement this in app-config.ts -export const ASSET_SCHEMA = Joi.object({ - path: Joi.string().required(), - quantity: Joi.number().required(), -}); - const POWERUP_SCHEMA = Joi.object({ slug: Joi.string().required(), name: Joi.string().optional(), @@ -154,7 +148,6 @@ export const ACTION_SCHEMA = Joi.object({ then: Joi.alternatives() .try( Joi.string().valid('{{USE_SECURE_PROVIDED}}').required(), - Joi.array().items(ASSET_SCHEMA).required(), Joi.array().items(ASSET_TEMPLATE_SCHEMA).required(), ) .required(),