@@ -43,6 +43,7 @@
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": "^24.5.0",
|
||||
"yarn": "^4.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name singular for the serverless function",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$",
|
||||
"description": "Name singular for the serverless function (eg: my-serverless-function)",
|
||||
"pattern": "^[a-z0-9-]+$",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import inquirer from 'inquirer';
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { loadManifest } from '../utils/app-manifest-loader';
|
||||
|
||||
export class AppDeleteCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
const appPath = CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('🚀 Deleting Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
if (!(await this.confirmationPrompt())) {
|
||||
console.error(chalk.red('⛔️ Aborting deletion'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { packageJson } = await loadManifest(appPath);
|
||||
|
||||
const result = await this.apiService.deleteApplication(packageJson);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Deletion failed:'), result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ Application deleted successfully'));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Deletion failed:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmationPrompt(): Promise<boolean> {
|
||||
const { confirmation } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirmation',
|
||||
message: 'Are you sure you want to delete this application?',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
return confirmation;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import chalk from 'chalk';
|
||||
import * as chokidar from 'chokidar';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { loadManifest } from '../utils/app-manifest-loader';
|
||||
|
||||
export class AppDevCommand {
|
||||
private apiService = new ApiService();
|
||||
@@ -15,7 +15,13 @@ export class AppDevCommand {
|
||||
|
||||
this.logStartupInfo(appPath, debounceMs);
|
||||
|
||||
await syncApp(appPath, this.apiService);
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
const watcher = this.setupFileWatcher(appPath, debounceMs);
|
||||
|
||||
@@ -54,7 +60,15 @@ export class AppDevCommand {
|
||||
|
||||
timeout = setTimeout(async () => {
|
||||
console.log(chalk.blue('🔄 Changes detected, syncing...'));
|
||||
await syncApp(appPath, this.apiService);
|
||||
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.gray('👀 Watching for changes... (Press Ctrl+C to stop)'),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { syncApp } from '../utils/app-sync';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '../constants/current-execution-directory';
|
||||
import { loadManifest } from '../utils/app-manifest-loader';
|
||||
|
||||
export class AppSyncCommand {
|
||||
private apiService = new ApiService();
|
||||
@@ -14,7 +14,13 @@ export class AppSyncCommand {
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await syncApp(appPath, this.apiService);
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
const result = await this.apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Sync failed:'), result.error);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
||||
import { AppSyncCommand } from './app-sync.command';
|
||||
import { AppDevCommand } from './app-dev.command';
|
||||
import { AppInitCommand } from './app-init.command';
|
||||
import { AppDeleteCommand } from './app-delete.command';
|
||||
import {
|
||||
AppAddCommand,
|
||||
isSyncableEntity,
|
||||
@@ -12,6 +13,7 @@ import chalk from 'chalk';
|
||||
export class AppCommand {
|
||||
private devCommand = new AppDevCommand();
|
||||
private syncCommand = new AppSyncCommand();
|
||||
private deleteCommand = new AppDeleteCommand();
|
||||
private initCommand = new AppInitCommand();
|
||||
private addCommand = new AppAddCommand();
|
||||
|
||||
@@ -34,6 +36,13 @@ export class AppCommand {
|
||||
await this.syncCommand.execute();
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('delete')
|
||||
.description('Delete application from Twenty')
|
||||
.action(async () => {
|
||||
await this.deleteCommand.execute();
|
||||
});
|
||||
|
||||
appCommand
|
||||
.command('init [directory]')
|
||||
.description('Initialize a new Twenty application')
|
||||
|
||||
@@ -72,7 +72,6 @@ export class ApiService {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*',
|
||||
'x-schema-version': '6',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -115,7 +114,6 @@ export class ApiService {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*',
|
||||
'x-schema-version': '6',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -143,4 +141,52 @@ export class ApiService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteApplication(packageJson: PackageJson): Promise<ApiResponse> {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation DeleteApplication($packageJson: JSON!) {
|
||||
deleteApplication(packageJson: $packageJson)
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = { packageJson };
|
||||
|
||||
const response: AxiosResponse = 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 delete application',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.deleteApplication,
|
||||
message: `Successfully deleted application: ${packageJson.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response.data?.errors?.[0]?.message || error.message,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { loadManifest } from './app-manifest-loader';
|
||||
|
||||
export const syncApp = async (
|
||||
appPath: string,
|
||||
apiService: ApiService,
|
||||
): Promise<any> => {
|
||||
const { manifest, packageJson, yarnLock } = await loadManifest(appPath);
|
||||
|
||||
try {
|
||||
const result = await apiService.syncApplication({
|
||||
manifest,
|
||||
packageJson,
|
||||
yarnLock,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(chalk.green('✅ Application synced successfully'));
|
||||
} else {
|
||||
console.error(chalk.red('❌ Sync failed:'), result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Sync error:'),
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1694,6 +1694,7 @@ export type Mutation = {
|
||||
createWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
createWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApplication: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCoreView: Scalars['Boolean'];
|
||||
deleteCoreViewField: CoreViewField;
|
||||
@@ -2057,6 +2058,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApplicationArgs = {
|
||||
packageJson: Scalars['JSON'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
@@ -1650,6 +1650,7 @@ export type Mutation = {
|
||||
createWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
createWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApplication: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCoreView: Scalars['Boolean'];
|
||||
deleteCoreViewField: CoreViewField;
|
||||
@@ -1993,6 +1994,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApplicationArgs = {
|
||||
packageJson: Scalars['JSON'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
@@ -6,12 +6,15 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
|
||||
import { DeleteApplicationInput } from 'src/engine/core-modules/application/dtos/deleteApplication.input';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@Resolver()
|
||||
export class ApplicationResolver {
|
||||
constructor(
|
||||
private readonly applicationSyncService: ApplicationSyncService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@@ -28,4 +31,17 @@ export class ApplicationResolver {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteApplication(
|
||||
@Args() { packageJson }: DeleteApplicationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
await this.applicationService.delete(
|
||||
packageJson.universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
|
||||
@@ -70,4 +71,20 @@ export class ApplicationService {
|
||||
|
||||
return updatedApplication;
|
||||
}
|
||||
|
||||
async delete(universalIdentifier: string, workspaceId: string) {
|
||||
const application = await this.findByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
throw new Error(`Application does not exist`);
|
||||
}
|
||||
|
||||
await this.applicationRepository.delete({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
|
||||
|
||||
@ArgsType()
|
||||
export class DeleteApplicationInput {
|
||||
@Field(() => GraphQLJSON, { nullable: false })
|
||||
packageJson: PackageJson;
|
||||
}
|
||||
@@ -21784,7 +21784,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20.0.0, @types/node@npm:^20.3.1":
|
||||
"@types/node@npm:^20.0.0":
|
||||
version: 20.19.19
|
||||
resolution: "@types/node@npm:20.19.19"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c0/488589d6d1d562ad4e8ed9bb36703a8f3bb7feed206bba867358eda0965e781a408f14ed7ff1ca334ae6f6823dcfe2eb2536ffe48e2ab02d47b4c3c5395d274a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^20.3.1":
|
||||
version: 20.19.13
|
||||
resolution: "@types/node@npm:20.19.13"
|
||||
dependencies:
|
||||
@@ -25015,7 +25024,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.6.0, axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3":
|
||||
"axios@npm:^1.6.0":
|
||||
version: 1.12.2
|
||||
resolution: "axios@npm:1.12.2"
|
||||
dependencies:
|
||||
follow-redirects: "npm:^1.15.6"
|
||||
form-data: "npm:^4.0.4"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
checksum: 10c0/80b063e318cf05cd33a4d991cea0162f3573481946f9129efb7766f38fde4c061c34f41a93a9f9521f02b7c9565ccbc197c099b0186543ac84a24580017adfed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3":
|
||||
version: 1.11.0
|
||||
resolution: "axios@npm:1.11.0"
|
||||
dependencies:
|
||||
@@ -52246,7 +52266,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsx@npm:^4.17.0, tsx@npm:^4.19.3, tsx@npm:^4.7.0":
|
||||
"tsx@npm:^4.17.0, tsx@npm:^4.19.3":
|
||||
version: 4.20.5
|
||||
resolution: "tsx@npm:4.20.5"
|
||||
dependencies:
|
||||
@@ -52262,6 +52282,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsx@npm:^4.7.0":
|
||||
version: 4.20.6
|
||||
resolution: "tsx@npm:4.20.6"
|
||||
dependencies:
|
||||
esbuild: "npm:~0.25.0"
|
||||
fsevents: "npm:~2.3.3"
|
||||
get-tsconfig: "npm:^4.7.5"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
bin:
|
||||
tsx: dist/cli.mjs
|
||||
checksum: 10c0/07757a9bf62c271e0a00869b2008c5f2d6e648766536e4faf27d9d8027b7cde1ac8e4871f4bb570c99388bcee0018e6869dad98c07df809b8052f9c549cd216f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tty-browserify@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "tty-browserify@npm:0.0.1"
|
||||
|
||||
Reference in New Issue
Block a user