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:
@@ -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'];
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+5
@@ -96,6 +96,11 @@ export class ServerlessFunctionDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
applicationId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
universalIdentifier?: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user