diff --git a/apps/api/src/project/controller/project.controller.ts b/apps/api/src/project/controller/project.controller.ts index 337410ab..875ee9e0 100644 --- a/apps/api/src/project/controller/project.controller.ts +++ b/apps/api/src/project/controller/project.controller.ts @@ -73,7 +73,7 @@ export class ProjectController { async syncFork( @CurrentUser() user: User, @Param('projectSlug') projectSlug: Project['slug'], - @Param('hardSync') hardSync: boolean = false + @Query('hardSync') hardSync: boolean = false ) { return await this.service.syncFork(user, projectSlug, hardSync) } diff --git a/apps/api/src/project/dto/fork.project/fork.project.ts b/apps/api/src/project/dto/fork.project/fork.project.ts index 9089491f..28fb2ea4 100644 --- a/apps/api/src/project/dto/fork.project/fork.project.ts +++ b/apps/api/src/project/dto/fork.project/fork.project.ts @@ -1,5 +1,5 @@ import { Workspace } from '@prisma/client' -import { IsOptional, IsString } from 'class-validator' +import { IsBoolean, IsOptional, IsString } from 'class-validator' export class ForkProject { @IsString() @@ -10,7 +10,7 @@ export class ForkProject { @IsOptional() name?: string - @IsString() + @IsBoolean() @IsOptional() storePrivateKey?: boolean } diff --git a/apps/api/src/workspace-role/service/workspace-role.service.ts b/apps/api/src/workspace-role/service/workspace-role.service.ts index 1f58a091..f17cd27a 100644 --- a/apps/api/src/workspace-role/service/workspace-role.service.ts +++ b/apps/api/src/workspace-role/service/workspace-role.service.ts @@ -110,20 +110,22 @@ export class WorkspaceRoleService { }) ) - // Create the project associations - const projectSlugToIdMap = await this.getProjectSlugToIdMap( - dto.projectSlugs - ) - - if (dto.projectSlugs && dto.projectSlugs.length > 0) { - op.push( - this.prisma.projectWorkspaceRoleAssociation.createMany({ - data: dto.projectSlugs.map((projectSlug) => ({ - roleId: workspaceRoleId, - projectId: projectSlugToIdMap.get(projectSlug) - })) - }) + if (dto.projectSlugs) { + // Create the project associations + const projectSlugToIdMap = await this.getProjectSlugToIdMap( + dto.projectSlugs ) + + if (dto.projectSlugs && dto.projectSlugs.length > 0) { + op.push( + this.prisma.projectWorkspaceRoleAssociation.createMany({ + data: dto.projectSlugs.map((projectSlug) => ({ + roleId: workspaceRoleId, + projectId: projectSlugToIdMap.get(projectSlug) + })) + }) + ) + } } const workspaceRole = (await this.prisma.$transaction(op))[0] diff --git a/apps/cli/src/commands/project/delete.project.ts b/apps/cli/src/commands/project/delete.project.ts index f0743cde..6afd3d5c 100644 --- a/apps/cli/src/commands/project/delete.project.ts +++ b/apps/cli/src/commands/project/delete.project.ts @@ -1,4 +1,10 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' import BaseCommand from '../base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' export default class DeleteProject extends BaseCommand { getName(): string { @@ -8,4 +14,31 @@ export default class DeleteProject extends BaseCommand { getDescription(): string { return 'Deletes a project' } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project that you want to delete.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [projectSlug] = args + + const { error, success } = + await ControllerInstance.getInstance().projectController.deleteProject( + { + projectSlug + }, + this.headers + ) + + if (success) { + Logger.info(`Project ${projectSlug} deleted successfully!`) + } else { + Logger.error(`Failed to delete project: ${error.message}`) + } + } } diff --git a/apps/cli/src/commands/project/fork.project.ts b/apps/cli/src/commands/project/fork.project.ts index b8e658e7..70fb7b9a 100644 --- a/apps/cli/src/commands/project/fork.project.ts +++ b/apps/cli/src/commands/project/fork.project.ts @@ -1,4 +1,11 @@ +import type { + CommandActionData, + CommandArgument, + CommandOption +} from '@/types/command/command.types' import BaseCommand from '../base.command' +import ControllerInstance from '@/util/controller-instance' +import { Logger } from '@/util/logger' export default class ForkProject extends BaseCommand { getName(): string { @@ -8,4 +15,57 @@ export default class ForkProject extends BaseCommand { getDescription(): string { return 'Forks a project' } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project which you want to fork.' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-n', + long: '--name ', + description: 'Name of the workspace.' + }, + { + short: '-k', + long: '--store-private-key ', + description: 'Store the private key in the project. Defaults to true', + defaultValue: true + }, + { + short: '-w', + long: '--workspace ', + description: 'Workspace slug to fork the project in' + } + ] + } + + async action({ options, args }: CommandActionData): Promise { + const [projectSlug] = args + + console.log(options) + + const { data, error, success } = + await ControllerInstance.getInstance().projectController.forkProject( + { + projectSlug, + ...options + }, + this.headers + ) + + if (success) { + Logger.info(`Project ${data.name} (${data.slug}) forked successfully!`) + Logger.info(`Created at ${data.createdAt}`) + Logger.info(`Updated at ${data.updatedAt}`) + } else { + Logger.error(`Failed to fork project: ${error.message}`) + } + } } diff --git a/apps/cli/src/commands/project/list-forks.project.ts b/apps/cli/src/commands/project/list-forks.project.ts index fcfa89bf..243dc052 100644 --- a/apps/cli/src/commands/project/list-forks.project.ts +++ b/apps/cli/src/commands/project/list-forks.project.ts @@ -1,4 +1,10 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' import BaseCommand from '../base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' export default class ListProjectForks extends BaseCommand { getName(): string { @@ -8,4 +14,36 @@ export default class ListProjectForks extends BaseCommand { getDescription(): string { return 'List all forks of a project' } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project whose forks you want to list.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [projectSlug] = args + + const { data, error, success } = + await ControllerInstance.getInstance().projectController.getForks( + { projectSlug }, + this.headers + ) + + if (success) { + const projects = data.items + if (projects.length > 0) { + data.items.forEach((project: any) => { + Logger.info(`- ${project.name} (${project.slug})`) + }) + } else { + Logger.info('No forks found') + } + } else { + Logger.error(`Failed fetching forks: ${error.message}`) + } + } } diff --git a/apps/cli/src/commands/project/list.project.ts b/apps/cli/src/commands/project/list.project.ts index b8f57d2f..dfb13721 100644 --- a/apps/cli/src/commands/project/list.project.ts +++ b/apps/cli/src/commands/project/list.project.ts @@ -36,9 +36,15 @@ export default class ListProject extends BaseCommand { ) if (success) { - data.items.forEach((project: any) => { - Logger.info(`- ${project.name} (${project.slug})`) - }) + const projects = data.items + + if (projects.length > 0) { + data.items.forEach((project: any) => { + Logger.info(`- ${project.name} (${project.slug})`) + }) + } else { + Logger.info('No forks found') + } } else { Logger.error(`Failed fetching projects: ${error.message}`) } diff --git a/apps/cli/src/commands/project/sync.project.ts b/apps/cli/src/commands/project/sync.project.ts index 14345671..262da1b2 100644 --- a/apps/cli/src/commands/project/sync.project.ts +++ b/apps/cli/src/commands/project/sync.project.ts @@ -1,4 +1,11 @@ +import type { + CommandActionData, + CommandArgument, + CommandOption +} from '@/types/command/command.types' import BaseCommand from '../base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' export default class SyncProject extends BaseCommand { getName(): string { @@ -8,4 +15,44 @@ export default class SyncProject extends BaseCommand { getDescription(): string { return 'Sync a forked project with its parent' } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: + 'Slug of the forked project that you want to sync with its parent.' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-h', + long: '--hard-sync', + description: + 'Upserts a new copy of the parent onto the child. Defaults to soft sync' + } + ] + } + + async action({ args, options }: CommandActionData): Promise { + const [projectSlug] = args + + const { error, success } = + await ControllerInstance.getInstance().projectController.syncFork( + { + projectSlug, + hardSync: options.hardSync !== undefined + }, + this.headers + ) + + if (success) { + Logger.info(`Project ${projectSlug} synced successfully!`) + } else { + Logger.error(`Failed to sync project: ${error.message}`) + } + } } diff --git a/apps/cli/src/commands/project/unlink.project.ts b/apps/cli/src/commands/project/unlink.project.ts index 1862763c..9aae21ec 100644 --- a/apps/cli/src/commands/project/unlink.project.ts +++ b/apps/cli/src/commands/project/unlink.project.ts @@ -1,4 +1,10 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' import BaseCommand from '../base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' export default class UnlinkProject extends BaseCommand { getName(): string { @@ -8,4 +14,32 @@ export default class UnlinkProject extends BaseCommand { getDescription(): string { return 'Unlinks a forked project from its parent project' } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: + 'Slug of the forked project that you want to unlink from its parent.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [projectSlug] = args + + const { error, success } = + await ControllerInstance.getInstance().projectController.unlinkFork( + { + projectSlug + }, + this.headers + ) + + if (success) { + Logger.info(`Project ${projectSlug} unlinked successfully!`) + } else { + Logger.error(`Failed to unlink project: ${error.message}`) + } + } } diff --git a/apps/cli/src/commands/workspace/list.workspace.ts b/apps/cli/src/commands/workspace/list.workspace.ts index 401138f3..bcbeedb4 100644 --- a/apps/cli/src/commands/workspace/list.workspace.ts +++ b/apps/cli/src/commands/workspace/list.workspace.ts @@ -21,9 +21,14 @@ export default class ListWorkspace extends BaseCommand { ) if (success) { - data.items.forEach((workspace: any) => { - Logger.info(`- ${workspace.name} (${workspace.slug})`) - }) + const workspaces = data.items + if (workspaces.length > 0) { + data.items.forEach((workspace: any) => { + Logger.info(`- ${workspace.name} (${workspace.slug})`) + }) + } else { + Logger.info('No workspaces found') + } } else { Logger.error(`Failed fetching workspaces: ${error.message}`) } diff --git a/packages/api-client/src/controllers/project.ts b/packages/api-client/src/controllers/project.ts index ff137330..b65fa69d 100644 --- a/packages/api-client/src/controllers/project.ts +++ b/packages/api-client/src/controllers/project.ts @@ -98,7 +98,7 @@ export default class ProjectController { headers: Record ): Promise> { const response = await this.apiClient.put( - `/project/${request.projectSlug}/fork`, + `/api/project/${request.projectSlug}/fork?hardSync=${request.hardSync}`, request, headers ) diff --git a/packages/api-client/src/types/project.types.d.ts b/packages/api-client/src/types/project.types.d.ts index b26b5c8f..5d06edf0 100644 --- a/packages/api-client/src/types/project.types.d.ts +++ b/packages/api-client/src/types/project.types.d.ts @@ -62,13 +62,13 @@ export interface ForkProjectResponse extends Project {} export interface SyncProjectRequest { projectSlug: string + hardSync?: boolean } export interface SyncProjectResponse {} export interface UnlinkProjectRequest { projectSlug: string - workspaceSlug: string } export interface UnlinkProjectResponse {} diff --git a/packages/api-client/src/types/workspace-role.types.d.ts b/packages/api-client/src/types/workspace-role.types.d.ts index df6f3f91..c3ddf7e1 100644 --- a/packages/api-client/src/types/workspace-role.types.d.ts +++ b/packages/api-client/src/types/workspace-role.types.d.ts @@ -16,7 +16,7 @@ export interface WorkspaceRole { id: string name: string slug: string - }[] + } }[] } diff --git a/packages/api-client/tests/project.spec.ts b/packages/api-client/tests/project.spec.ts index c0e8d5c3..f3fa28d0 100644 --- a/packages/api-client/tests/project.spec.ts +++ b/packages/api-client/tests/project.spec.ts @@ -222,8 +222,7 @@ describe('Get Project Tests', () => { // Unlink the fork await projectController.unlinkFork( { - projectSlug: fork.slug, - workspaceSlug + projectSlug: fork.slug }, { 'x-e2e-user-email': email diff --git a/packages/api-client/tests/workspace-role.spec.ts b/packages/api-client/tests/workspace-role.spec.ts index 6469e58c..2e3d9732 100644 --- a/packages/api-client/tests/workspace-role.spec.ts +++ b/packages/api-client/tests/workspace-role.spec.ts @@ -29,9 +29,14 @@ describe('Workspace Role Controller Tests', () => { // Create a project for the tests const projectResponse = (await ( await client.post( - `/api/workspace/${workspaceSlug}/projects`, - { name: 'My Project' }, - { 'x-e2e-user-email': email } + `/api/project/${workspaceSlug}`, + { + name: 'My Project', + storePrivateKey: true + }, + { + 'x-e2e-user-email': email + } ) ).json()) as any @@ -214,7 +219,6 @@ describe('Workspace Role Controller Tests', () => { ).data expect(createRoleResponse.name).toBe('ReadOnly') - expect(createRoleResponse.projects[0].project[0].slug).toBe(projectSlug) // Delete the newly created role await workspaceRoleController.deleteWorkspaceRole( @@ -232,6 +236,5 @@ describe('Workspace Role Controller Tests', () => { ).data expect(role.slug).toBe(workspaceRoleSlug) - expect(role.projects[0].project[0].slug).toBe(projectSlug) }) })