Skip to content

feat: postInstallAction and powerups #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@trufflehq/cli",
"version": "0.6.1",
"version": "0.6.7",
"description": "The Truffle Developer Platform CLI",
"main": "dist/cli.mjs",
"bin": {
Expand Down
102 changes: 93 additions & 9 deletions src/constants/app-config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,21 @@ export const OPERATION_TYPES = [
'webhook',
'workflow',
'exchange',
'apply-powerup',
// TODO: support these
// 'conditional',
// 'apply-powerup',
] 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(),
Expand Down Expand Up @@ -63,35 +72,108 @@ 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(),
});

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(),
data: Joi.object().optional(),
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)
.required(),

name: Joi.string().optional(),

// webhook inputs
url: Joi.when('operation', {
is: 'webhook',
then: Joi.string().required(),
}),

actions: Joi.when('operation', {
is: 'workflow',
then: Joi.array().items(Joi.link('#actionSchema')).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',
then: Joi.string().valid('sequential', 'parallel').required(),
}),

actions: Joi.when('operation', {
is: 'workflow',
then: Joi.array().items(Joi.link('#actionSchema')).required(),
}),

// exchange inputs
assets: Joi.when('operation', {
is: 'exchange',
then: Joi.alternatives()
.try(
Joi.string().valid('{{USE_SECURE_PROVIDED}}').required(),
Joi.array().items(ASSET_TEMPLATE_SCHEMA).required(),
)
.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(),

isDirectExecutionAllowed: Joi.boolean().optional(),
Expand All @@ -108,9 +190,7 @@ export const PRODUCT_VARIANT_SCHEMA = Joi.object({
name: Joi.string().optional(),
price: Joi.number().required(),
description: Joi.string().optional(),

action: ACTION_SCHEMA.optional(),

assets: Joi.array().items(ASSET_SCHEMA).optional(),
});

Expand All @@ -131,4 +211,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(),
});
9 changes: 9 additions & 0 deletions src/types/mt-app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface MothertreeAppConfig {
countables: MothertreeCountableConfig[];
products: MothertreeProductConfig[];
productVariants: MothertreeProductVariantConfig[];
powerups: MothertreePowerupConfig[];
postInstallActionPath?: string;
}

export interface MothertreeEmbedConfig {
Expand Down Expand Up @@ -96,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;
}
133 changes: 97 additions & 36 deletions src/util/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export function makeLocalActionPath(actionSlug: string) {
return `./_Action/${actionSlug}`;
}

export function makeLocalPowerupPath(powerupSlug: string) {
return `./_Powerup/${powerupSlug}`;
}

type ActionConfig = NonNullable<AppConfig['actions']>[number];
type EmbeddedActionConfig = Omit<ActionConfig, 'slug'>;

Expand All @@ -54,40 +58,71 @@ export function convertActionConfigsToMothertreeActionConfigs(
mtAppConfig.actions.push(
...actionConfigs.map((actionConfig) => {
let inputsTemplate: Record<string, unknown>;
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,
data: actionConfig.data,
};
break;
}

case 'exchange': {
inputsTemplate = {
assets: actionConfig.assets,
};
break;
}
}

return {
Expand Down Expand Up @@ -180,7 +215,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',
Expand Down Expand Up @@ -213,8 +249,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: [],
};
Expand All @@ -226,6 +263,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,
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/__snapshots__/app-config-schema.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

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 > 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",
"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",
Expand Down
Loading