2091 extensibility twenty sdk add command twenty app function logs and twenty app function test (#17278)

add `function:logs` and `function:execute` cli commands
This commit is contained in:
martmull
2026-01-20 15:10:01 +01:00
committed by GitHub
parent f9c400cd78
commit 801878cb2b
18 changed files with 400 additions and 35 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ yarn app:dev
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -35,7 +35,8 @@ yarn app:dev # Start dev mode (sync + watch)
yarn app:sync # One-time sync
yarn entity:add # Add a new entity (function, object, role)
yarn app:generate # Generate typed Twenty client
yarn app:logs # Stream function logs
yarn function:logs # Stream function logs
yarn function:execute # Execute a function with JSON payload
yarn app:uninstall # Uninstall app from workspace
```
@@ -199,7 +199,8 @@ const createPackageJson = async ({
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'app:logs': 'twenty app:logs',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
@@ -53,7 +53,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -55,7 +55,10 @@ yarn app:generate
yarn app:sync
# Watch your application's functions logs
yarn app:logs
yarn function:logs
# Execute a function by name
yarn function:execute -n my-function -p '{"name": "test"}'
# Uninstall the application from the current workspace
yarn app:uninstall
@@ -4283,6 +4283,7 @@ export type ServerlessFunction = {
runtime: Scalars['String'];
timeoutSeconds: Scalars['Float'];
toolInputSchema?: Maybe<Scalars['JSON']>;
universalIdentifier?: Maybe<Scalars['UUID']>;
updatedAt: Scalars['DateTime'];
};
@@ -4127,6 +4127,7 @@ export type ServerlessFunction = {
runtime: Scalars['String'];
timeoutSeconds: Scalars['Float'];
toolInputSchema?: Maybe<Scalars['JSON']>;
universalIdentifier?: Maybe<Scalars['UUID']>;
updatedAt: Scalars['DateTime'];
};
+18 -3
View File
@@ -113,11 +113,17 @@ Application development commands.
- `twenty app:generate [appPath]` — Generate the typed Twenty client for your application.
- `twenty app:logs [appPath]` — Stream application function logs.
- `twenty function:logs [appPath]` — Stream application function logs.
- Options:
- `-u, --functionUniversalIdentifier <id>`: Only show logs for a specific function universal ID.
- `-n, --functionName <name>`: Only show logs for a specific function name.
- `twenty function:execute [appPath]` — Execute a serverless function with a JSON payload.
- Options:
- `-n, --functionName <name>`: Name of the function to execute (required if `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `-n` not provided).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
Examples:
```bash
@@ -140,10 +146,19 @@ twenty entity:add
twenty app:generate
# Watch all function logs
twenty app:logs
twenty function:logs
# Watch logs for a specific function by name
twenty app:logs -n my-function
twenty function:logs -n my-function
# Execute a function by name (with empty payload)
twenty function:execute -n my-function
# Execute a function with a JSON payload
twenty function:execute -n my-function -p '{"name": "test"}'
# Execute a function by universal identifier
twenty function:execute -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "value"}'
```
## Configuration
@@ -4,12 +4,13 @@ import type { Command } from 'commander';
import { AppBuildCommand } from './app/app-build';
import { AppDevCommand } from './app/app-dev';
import { AppGenerateCommand } from './app/app-generate';
import { AppLogsCommand } from './app/app-logs';
import { AppSyncCommand } from './app/app-sync';
import { AppUninstallCommand } from './app/app-uninstall';
import { AuthLoginCommand } from './auth/auth-login';
import { AuthLogoutCommand } from './auth/auth-logout';
import { AuthStatusCommand } from './auth/auth-status';
import { FunctionExecuteCommand } from './function/function-execute';
import { FunctionLogsCommand } from './function/function-logs';
import {
EntityAddCommand,
isSyncableEntity,
@@ -51,7 +52,8 @@ export const registerCommands = (program: Command): void => {
const uninstallCommand = new AppUninstallCommand();
const addCommand = new EntityAddCommand();
const generateCommand = new AppGenerateCommand();
const logsCommand = new AppLogsCommand();
const logsCommand = new FunctionLogsCommand();
const executeCommand = new FunctionExecuteCommand();
const buildCommand = new AppBuildCommand();
program
@@ -113,24 +115,6 @@ export const registerCommands = (program: Command): void => {
}
});
// Keeping to avoid breaking changes
program
.command('app:delete [appPath]', { hidden: true })
.description('Delete application from Twenty')
.action(async (appPath?: string) => {
try {
const result = await uninstallCommand.execute({
appPath: formatPath(appPath),
askForConfirmation: true,
});
if (!result.success) {
process.exit(1);
}
} catch {
process.exit(1);
}
});
program
.command('entity:add [entityType]')
.option('--path <path>', 'Path in which the entity should be created.')
@@ -156,8 +140,9 @@ export const registerCommands = (program: Command): void => {
await generateCommand.execute(formatPath(appPath));
});
// Function commands
program
.command('app:logs [appPath]')
.command('function:logs [appPath]')
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Only show logs for the function with this universal ID',
@@ -181,4 +166,45 @@ export const registerCommands = (program: Command): void => {
});
},
);
program
.command('function:execute [appPath]')
.option(
'-p, --payload <payload>',
'JSON payload to send to the function',
'{}',
)
.option(
'-u, --functionUniversalIdentifier <functionUniversalIdentifier>',
'Universal ID of the function to execute',
)
.option(
'-n, --functionName <functionName>',
'Name of the function to execute',
)
.description('Execute a serverless function with a JSON payload')
.action(
async (
appPath?: string,
options?: {
payload?: string;
functionUniversalIdentifier?: string;
functionName?: string;
},
) => {
if (!options?.functionUniversalIdentifier && !options?.functionName) {
console.error(
chalk.red(
'Error: Either --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
}
await executeCommand.execute({
...options,
payload: options?.payload ?? '{}',
appPath: formatPath(appPath),
});
},
);
};
@@ -0,0 +1,165 @@
import chalk from 'chalk';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { buildManifest } from '@/cli/utilities/manifest/utils/manifest-build';
import { type ApplicationManifest } from 'twenty-shared/application';
export class FunctionExecuteCommand {
private apiService = new ApiService();
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
functionUniversalIdentifier?: string;
functionName?: string;
payload?: string;
}): Promise<void> {
try {
let parsedPayload: Record<string, unknown>;
try {
parsedPayload = JSON.parse(payload);
} catch {
console.error(
chalk.red('Invalid JSON payload. Please provide valid JSON.'),
);
process.exit(1);
}
const { manifest } = await buildManifest(appPath);
const functionsResult = await this.apiService.findServerlessFunctions();
if (!functionsResult.success) {
console.error(
chalk.red('Failed to fetch functions:'),
functionsResult.error instanceof Error
? functionsResult.error.message
: functionsResult.error,
);
process.exit(1);
}
const appFunctions = functionsResult.data.filter(
(fn) =>
fn.universalIdentifier && this.belongsToApplication(fn, manifest),
);
const targetFunction = appFunctions.find((fn) => {
if (functionUniversalIdentifier) {
return fn.universalIdentifier === functionUniversalIdentifier;
}
if (functionName) {
return fn.name === functionName;
}
return false;
});
if (!targetFunction) {
const identifier = functionUniversalIdentifier || functionName;
console.error(
chalk.red(`Function "${identifier}" not found in application.`),
);
console.log('');
if (appFunctions.length > 0) {
console.log(chalk.cyan('Available functions:'));
appFunctions.forEach((fn) => {
console.log(
` - ${chalk.white(fn.name)} (${fn.universalIdentifier})`,
);
});
} else {
console.log(
chalk.yellow(
'No functions found for this application. Have you synced your app with `yarn app:sync`?',
),
);
}
process.exit(1);
}
console.log(
chalk.blue(`🚀 Executing function "${targetFunction.name}"...`),
);
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}`));
console.log('');
const result = await this.apiService.executeServerlessFunction({
functionId: targetFunction.id,
payload: parsedPayload,
version: 'draft',
});
if (!result.success) {
console.error(
chalk.red('Execution failed:'),
result.error instanceof Error ? result.error.message : result.error,
);
process.exit(1);
}
const executionResult = result.data!;
console.log(chalk.cyan('─'.repeat(60)));
console.log(chalk.cyan('Execution Result'));
console.log(chalk.cyan('─'.repeat(60)));
const statusColor =
executionResult.status === 'SUCCESS' ? chalk.green : chalk.red;
console.log(
`${chalk.bold('Status:')} ${statusColor(executionResult.status)}`,
);
console.log(`${chalk.bold('Duration:')} ${executionResult.duration}ms`);
if (executionResult.data !== undefined && executionResult.data !== null) {
console.log('');
console.log(chalk.bold('Data:'));
console.log(chalk.white(JSON.stringify(executionResult.data, null, 2)));
}
if (executionResult.error) {
console.log('');
console.log(chalk.bold.red('Error:'));
console.log(chalk.red(` Type: ${executionResult.error.errorType}`));
console.log(
chalk.red(` Message: ${executionResult.error.errorMessage}`),
);
if (executionResult.error.stackTrace) {
console.log('');
console.log(chalk.gray('Stack trace:'));
console.log(chalk.gray(executionResult.error.stackTrace));
}
}
if (executionResult.logs) {
console.log('');
console.log(chalk.bold('Logs:'));
console.log(chalk.gray(executionResult.logs));
}
console.log(chalk.cyan('─'.repeat(60)));
if (executionResult.status !== 'SUCCESS') {
process.exit(1);
}
} catch (error) {
console.error(
chalk.red('Execution failed:'),
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
private belongsToApplication(
fn: { universalIdentifier: string; applicationId: string | null },
manifest: ApplicationManifest,
): boolean {
return manifest.serverlessFunctions.some(
(manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier,
);
}
}
@@ -3,7 +3,7 @@ import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/cu
import { ApiService } from '@/cli/utilities/api/services/api.service';
import { buildManifest } from '@/cli/utilities/manifest/utils/manifest-build';
export class AppLogsCommand {
export class FunctionLogsCommand {
private apiService = new ApiService();
async execute({
@@ -237,6 +237,135 @@ export class ApiService {
}
}
async findServerlessFunctions(): Promise<
ApiResponse<
Array<{
id: string;
name: string;
universalIdentifier: string;
applicationId: string | null;
}>
>
> {
try {
const query = `
query FindManyServerlessFunctions {
findManyServerlessFunctions {
id
name
universalIdentifier
applicationId
}
}
`;
const response = await this.client.post(
'/metadata',
{ query },
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
if (response.data.errors) {
return {
success: false,
error: response.data.errors[0]?.message || 'Failed to fetch functions',
};
}
return {
success: true,
data: response.data.data.findManyServerlessFunctions,
};
} catch (error) {
return {
success: false,
error,
};
}
}
async executeServerlessFunction({
functionId,
payload,
version = 'latest',
}: {
functionId: string;
payload: Record<string, unknown>;
version?: string;
}): Promise<
ApiResponse<{
data: unknown;
logs: string;
duration: number;
status: string;
error?: {
errorType: string;
errorMessage: string;
stackTrace: string;
};
}>
> {
try {
const mutation = `
mutation ExecuteOneServerlessFunction($input: ExecuteServerlessFunctionInput!) {
executeOneServerlessFunction(input: $input) {
data
logs
duration
status
error
}
}
`;
const variables = {
input: {
id: functionId,
payload,
version,
},
};
const response = await this.client.post(
'/metadata',
{
query: mutation,
variables,
},
{
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
},
},
);
if (response.data.errors) {
return {
success: false,
error:
response.data.errors[0]?.message ||
'Failed to execute serverless function',
};
}
return {
success: true,
data: response.data.data.executeOneServerlessFunction,
};
} catch (error) {
return {
success: false,
error,
};
}
}
async subscribeToLogs({
applicationUniversalIdentifier,
functionUniversalIdentifier,
@@ -96,6 +96,11 @@ export class ServerlessFunctionDTO {
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
@HideField()
workspaceId: string;