From 1d86c4fdd2adc5512118e4304efc1870171eff8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 9 Jan 2025 15:31:07 +0100 Subject: [PATCH] refactor(core): Use DTOs for source control push/pull requests (#12470) Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> --- packages/@n8n/api-types/src/dto/index.ts | 3 + .../pull-work-folder-request.dto.test.ts | 38 ++++++ .../push-work-folder-request.dto.test.ts | 112 ++++++++++++++++++ .../pull-work-folder-request.dto.ts | 6 + .../push-work-folder-request.dto.ts | 10 ++ packages/@n8n/api-types/src/index.ts | 8 ++ .../schemas/source-controlled-file.schema.ts | 34 ++++++ .../source-control-export.service.test.ts | 2 +- .../source-control-helper.ee.test.ts | 2 +- .../source-control-export.service.ee.ts | 2 +- .../source-control-helper.ee.ts | 2 +- .../source-control-import.service.ee.ts | 2 +- .../source-control.controller.ee.ts | 24 ++-- .../source-control.service.ee.ts | 21 ++-- .../source-control/types/requests.ts | 4 - .../types/source-control-pull-work-folder.ts | 28 ----- .../types/source-control-push-work-folder.ts | 20 ---- .../types/source-controlled-file.ts | 23 ---- packages/cli/src/public-api/types.ts | 10 -- .../source-control/source-control.handler.ts | 14 +-- .../source-control-import.service.test.ts | 2 +- .../environments/source-control.test.ts | 2 +- packages/editor-ui/src/api/sourceControl.ts | 13 +- .../components/MainSidebarSourceControl.vue | 8 +- .../components/SourceControlPullModal.ee.vue | 6 +- .../SourceControlPushModal.ee.test.ts | 14 +-- .../components/SourceControlPushModal.ee.vue | 70 ++++++----- .../src/stores/sourceControl.store.ts | 16 +-- .../src/types/sourceControl.types.ts | 43 ------- 29 files changed, 305 insertions(+), 234 deletions(-) create mode 100644 packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts create mode 100644 packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts create mode 100644 packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts create mode 100644 packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts delete mode 100644 packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts delete mode 100644 packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts delete mode 100644 packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index 21d7c67910..aa888f560d 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -36,6 +36,9 @@ export { UserUpdateRequestDto } from './user/user-update-request.dto'; export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto'; +export { PullWorkFolderRequestDto } from './source-control/pull-work-folder-request.dto'; +export { PushWorkFolderRequestDto } from './source-control/push-work-folder-request.dto'; + export { VariableListRequestDto } from './variables/variables-list-request.dto'; export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto'; export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto'; diff --git a/packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts b/packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts new file mode 100644 index 0000000000..0e47a223b9 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/__tests__/pull-work-folder-request.dto.test.ts @@ -0,0 +1,38 @@ +import { PullWorkFolderRequestDto } from '../pull-work-folder-request.dto'; + +describe('PullWorkFolderRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'with force', + request: { force: true }, + }, + { + name: 'without force', + request: {}, + }, + ])('should validate $name', ({ request }) => { + const result = PullWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'invalid force type', + request: { + force: 'true', // Should be boolean + }, + expectedErrorPath: ['force'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = PullWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts b/packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts new file mode 100644 index 0000000000..b3bbd984e1 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/__tests__/push-work-folder-request.dto.test.ts @@ -0,0 +1,112 @@ +import { PushWorkFolderRequestDto } from '../push-work-folder-request.dto'; + +describe('PushWorkFolderRequestDto', () => { + describe('Valid requests', () => { + test.each([ + { + name: 'complete valid push request with all fields', + request: { + force: true, + fileNames: [ + { + file: 'file1.json', + id: '1', + name: 'File 1', + type: 'workflow', + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-01T12:00:00Z', + pushed: true, + }, + ], + message: 'Initial commit', + }, + }, + { + name: 'push request with only required fields', + request: { + fileNames: [ + { + file: 'file2.json', + id: '2', + name: 'File 2', + type: 'credential', + status: 'new', + location: 'remote', + conflict: true, + updatedAt: '2023-10-02T12:00:00Z', + }, + ], + }, + }, + ])('should validate $name', ({ request }) => { + const result = PushWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(true); + }); + }); + + describe('Invalid requests', () => { + test.each([ + { + name: 'missing required fileNames field', + request: { + force: true, + message: 'Initial commit', + }, + expectedErrorPath: ['fileNames'], + }, + { + name: 'invalid fileNames type', + request: { + fileNames: 'not-an-array', // Should be an array + }, + expectedErrorPath: ['fileNames'], + }, + { + name: 'invalid fileNames array element', + request: { + fileNames: [ + { + file: 'file4.json', + id: '4', + name: 'File 4', + type: 'invalid-type', // Invalid type + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-04T12:00:00Z', + }, + ], + }, + expectedErrorPath: ['fileNames', 0, 'type'], + }, + { + name: 'invalid force type', + request: { + force: 'true', // Should be boolean + fileNames: [ + { + file: 'file5.json', + id: '5', + name: 'File 5', + type: 'workflow', + status: 'modified', + location: 'local', + conflict: false, + updatedAt: '2023-10-05T12:00:00Z', + }, + ], + }, + expectedErrorPath: ['force'], + }, + ])('should fail validation for $name', ({ request, expectedErrorPath }) => { + const result = PushWorkFolderRequestDto.safeParse(request); + expect(result.success).toBe(false); + + if (expectedErrorPath) { + expect(result.error?.issues[0].path).toEqual(expectedErrorPath); + } + }); + }); +}); diff --git a/packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts b/packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts new file mode 100644 index 0000000000..2773e0bb59 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/pull-work-folder-request.dto.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +export class PullWorkFolderRequestDto extends Z.class({ + force: z.boolean().optional(), +}) {} diff --git a/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts b/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts new file mode 100644 index 0000000000..be305ee7b3 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/source-control/push-work-folder-request.dto.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { SourceControlledFileSchema } from '../../schemas/source-controlled-file.schema'; + +export class PushWorkFolderRequestDto extends Z.class({ + force: z.boolean().optional(), + commitMessage: z.string().optional(), + fileNames: z.array(SourceControlledFileSchema), +}) {} diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index e304d62b5b..a813c9025d 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -10,9 +10,17 @@ export type { SendWorkerStatusMessage } from './push/worker'; export type { BannerName } from './schemas/bannerName.schema'; export { passwordSchema } from './schemas/password.schema'; + export { ProjectType, ProjectIcon, ProjectRole, ProjectRelation, } from './schemas/project.schema'; + +export { + type SourceControlledFile, + SOURCE_CONTROL_FILE_LOCATION, + SOURCE_CONTROL_FILE_STATUS, + SOURCE_CONTROL_FILE_TYPE, +} from './schemas/source-controlled-file.schema'; diff --git a/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts b/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts new file mode 100644 index 0000000000..31f2db8f92 --- /dev/null +++ b/packages/@n8n/api-types/src/schemas/source-controlled-file.schema.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +const FileTypeSchema = z.enum(['credential', 'workflow', 'tags', 'variables', 'file']); +export const SOURCE_CONTROL_FILE_TYPE = FileTypeSchema.Values; + +const FileStatusSchema = z.enum([ + 'new', + 'modified', + 'deleted', + 'created', + 'renamed', + 'conflicted', + 'ignored', + 'staged', + 'unknown', +]); +export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values; + +const FileLocationSchema = z.enum(['local', 'remote']); +export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values; + +export const SourceControlledFileSchema = z.object({ + file: z.string(), + id: z.string(), + name: z.string(), + type: FileTypeSchema, + status: FileStatusSchema, + location: FileLocationSchema, + conflict: z.boolean(), + updatedAt: z.string(), + pushed: z.boolean().optional(), +}); + +export type SourceControlledFile = z.infer; diff --git a/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts index 2e5a5e63e3..5f39e1ebf1 100644 --- a/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container } from '@n8n/di'; import mock from 'jest-mock-extended/lib/Mock'; import { Cipher, type InstanceSettings } from 'n8n-core'; @@ -9,7 +10,6 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre import { mockInstance } from '@test/mocking'; import { SourceControlExportService } from '../source-control-export.service.ee'; -import type { SourceControlledFile } from '../types/source-controlled-file'; // https://github.com/jestjs/jest/issues/4715 function deepSpyOn(object: O, methodName: M) { diff --git a/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts index c6d1b3857b..0395186a28 100644 --- a/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control-helper.ee.test.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container } from '@n8n/di'; import { constants as fsConstants, accessSync } from 'fs'; import { InstanceSettings } from 'n8n-core'; @@ -17,7 +18,6 @@ import { } from '@/environments.ee/source-control/source-control-helper.ee'; import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; import type { SourceControlPreferences } from '@/environments.ee/source-control/types/source-control-preferences'; -import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { License } from '@/license'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts index 4980460318..d32758cd25 100644 --- a/packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control-export.service.ee.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container, Service } from '@n8n/di'; import { rmSync } from 'fs'; import { Credentials, InstanceSettings, Logger } from 'n8n-core'; @@ -29,7 +30,6 @@ import type { ExportResult } from './types/export-result'; import type { ExportableCredential } from './types/exportable-credential'; import type { ExportableWorkflow } from './types/exportable-workflow'; import type { ResourceOwner } from './types/resource-owner'; -import type { SourceControlledFile } from './types/source-controlled-file'; import { VariablesService } from '../variables/variables.service.ee'; @Service() diff --git a/packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts index 031155a5b5..45ca38ed96 100644 --- a/packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control-helper.ee.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container } from '@n8n/di'; import { generateKeyPairSync } from 'crypto'; import { constants as fsConstants, mkdirSync, accessSync } from 'fs'; @@ -16,7 +17,6 @@ import { } from './constants'; import type { KeyPair } from './types/key-pair'; import type { KeyPairType } from './types/key-pair-type'; -import type { SourceControlledFile } from './types/source-controlled-file'; export function stringContainsExpression(testString: string): boolean { return /^=.*\{\{.*\}\}/.test(testString); diff --git a/packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts index 8da041297b..21de490215 100644 --- a/packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control-import.service.ee.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container, Service } from '@n8n/di'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In } from '@n8n/typeorm'; @@ -37,7 +38,6 @@ import { getCredentialExportPath, getWorkflowExportPath } from './source-control import type { ExportableCredential } from './types/exportable-credential'; import type { ResourceOwner } from './types/resource-owner'; import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id'; -import type { SourceControlledFile } from './types/source-controlled-file'; import { VariablesService } from '../variables/variables.service.ee'; @Service() diff --git a/packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts b/packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts index 7b8b2a7266..a7dd00d199 100644 --- a/packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control.controller.ee.ts @@ -1,9 +1,12 @@ +import { PullWorkFolderRequestDto, PushWorkFolderRequestDto } from '@n8n/api-types'; +import type { SourceControlledFile } from '@n8n/api-types'; import express from 'express'; import type { PullResult } from 'simple-git'; -import { Get, Post, Patch, RestController, GlobalScope } from '@/decorators'; +import { Get, Post, Patch, RestController, GlobalScope, Body } from '@/decorators'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; +import { AuthenticatedRequest } from '@/requests'; import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants'; import { @@ -17,7 +20,6 @@ import type { ImportResult } from './types/import-result'; import { SourceControlRequest } from './types/requests'; import { SourceControlGetStatus } from './types/source-control-get-status'; import type { SourceControlPreferences } from './types/source-control-preferences'; -import type { SourceControlledFile } from './types/source-controlled-file'; @RestController('/source-control') export class SourceControlController { @@ -164,19 +166,16 @@ export class SourceControlController { @Post('/push-workfolder', { middlewares: [sourceControlLicensedAndEnabledMiddleware] }) @GlobalScope('sourceControl:push') async pushWorkfolder( - req: SourceControlRequest.PushWorkFolder, + req: AuthenticatedRequest, res: express.Response, + @Body payload: PushWorkFolderRequestDto, ): Promise { - if (this.sourceControlPreferencesService.isBranchReadOnly()) { - throw new BadRequestError('Cannot push onto read-only branch.'); - } - try { await this.sourceControlService.setGitUserDetails( `${req.user.firstName} ${req.user.lastName}`, req.user.email, ); - const result = await this.sourceControlService.pushWorkfolder(req.body); + const result = await this.sourceControlService.pushWorkfolder(payload); res.statusCode = result.statusCode; return result.statusResult; } catch (error) { @@ -187,15 +186,12 @@ export class SourceControlController { @Post('/pull-workfolder', { middlewares: [sourceControlLicensedAndEnabledMiddleware] }) @GlobalScope('sourceControl:pull') async pullWorkfolder( - req: SourceControlRequest.PullWorkFolder, + req: AuthenticatedRequest, res: express.Response, + @Body payload: PullWorkFolderRequestDto, ): Promise { try { - const result = await this.sourceControlService.pullWorkfolder({ - force: req.body.force, - variables: req.body.variables, - userId: req.user.id, - }); + const result = await this.sourceControlService.pullWorkfolder(req.user.id, payload); res.statusCode = result.statusCode; return result.statusResult; } catch (error) { diff --git a/packages/cli/src/environments.ee/source-control/source-control.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control.service.ee.ts index 3952b6d6b5..3b330fffa3 100644 --- a/packages/cli/src/environments.ee/source-control/source-control.service.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control.service.ee.ts @@ -1,3 +1,8 @@ +import type { + PullWorkFolderRequestDto, + PushWorkFolderRequestDto, + SourceControlledFile, +} from '@n8n/api-types'; import { Service } from '@n8n/di'; import { writeFileSync } from 'fs'; import { Logger } from 'n8n-core'; @@ -34,10 +39,7 @@ import type { ExportableCredential } from './types/exportable-credential'; import type { ImportResult } from './types/import-result'; import type { SourceControlGetStatus } from './types/source-control-get-status'; import type { SourceControlPreferences } from './types/source-control-preferences'; -import type { SourceControllPullOptions } from './types/source-control-pull-work-folder'; -import type { SourceControlPushWorkFolder } from './types/source-control-push-work-folder'; import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id'; -import type { SourceControlledFile } from './types/source-controlled-file'; @Service() export class SourceControlService { @@ -207,7 +209,7 @@ export class SourceControlService { return; } - async pushWorkfolder(options: SourceControlPushWorkFolder): Promise<{ + async pushWorkfolder(options: PushWorkFolderRequestDto): Promise<{ statusCode: number; pushResult: PushResult | undefined; statusResult: SourceControlledFile[]; @@ -299,7 +301,7 @@ export class SourceControlService { } } - await this.gitService.commit(options.message ?? 'Updated Workfolder'); + await this.gitService.commit(options.commitMessage ?? 'Updated Workfolder'); const pushResult = await this.gitService.push({ branch: this.sourceControlPreferencesService.getBranchName(), @@ -321,7 +323,8 @@ export class SourceControlService { } async pullWorkfolder( - options: SourceControllPullOptions, + userId: User['id'], + options: PullWorkFolderRequestDto, ): Promise<{ statusCode: number; statusResult: SourceControlledFile[] }> { await this.sanityCheck(); @@ -345,7 +348,7 @@ export class SourceControlService { return true; }); - if (options.force !== true) { + if (!options.force) { const possibleConflicts = filteredResult?.filter( (file) => (file.conflict || file.status === 'modified') && file.type === 'workflow', ); @@ -363,7 +366,7 @@ export class SourceControlService { ); await this.sourceControlImportService.importWorkflowFromWorkFolder( workflowsToBeImported, - options.userId, + userId, ); const credentialsToBeImported = statusResult.filter( @@ -371,7 +374,7 @@ export class SourceControlService { ); await this.sourceControlImportService.importCredentialsFromWorkFolder( credentialsToBeImported, - options.userId, + userId, ); const tagsToBeImported = statusResult.find((e) => e.type === 'tags'); diff --git a/packages/cli/src/environments.ee/source-control/types/requests.ts b/packages/cli/src/environments.ee/source-control/types/requests.ts index 4f5d583f40..75c6c1995d 100644 --- a/packages/cli/src/environments.ee/source-control/types/requests.ts +++ b/packages/cli/src/environments.ee/source-control/types/requests.ts @@ -5,9 +5,7 @@ import type { SourceControlDisconnect } from './source-control-disconnect'; import type { SourceControlGenerateKeyPair } from './source-control-generate-key-pair'; import type { SourceControlGetStatus } from './source-control-get-status'; import type { SourceControlPreferences } from './source-control-preferences'; -import type { SourceControlPullWorkFolder } from './source-control-pull-work-folder'; import type { SourceControlPush } from './source-control-push'; -import type { SourceControlPushWorkFolder } from './source-control-push-work-folder'; import type { SourceControlSetBranch } from './source-control-set-branch'; import type { SourceControlSetReadOnly } from './source-control-set-read-only'; import type { SourceControlStage } from './source-control-stage'; @@ -20,8 +18,6 @@ export declare namespace SourceControlRequest { type Stage = AuthenticatedRequest<{}, {}, SourceControlStage, {}>; type Push = AuthenticatedRequest<{}, {}, SourceControlPush, {}>; type Disconnect = AuthenticatedRequest<{}, {}, SourceControlDisconnect, {}>; - type PushWorkFolder = AuthenticatedRequest<{}, {}, SourceControlPushWorkFolder, {}>; - type PullWorkFolder = AuthenticatedRequest<{}, {}, SourceControlPullWorkFolder, {}>; type GetStatus = AuthenticatedRequest<{}, {}, {}, SourceControlGetStatus>; type GenerateKeyPair = AuthenticatedRequest<{}, {}, SourceControlGenerateKeyPair, {}>; } diff --git a/packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts b/packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts deleted file mode 100644 index b87c970f0e..0000000000 --- a/packages/cli/src/environments.ee/source-control/types/source-control-pull-work-folder.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator'; - -export class SourceControlPullWorkFolder { - @IsBoolean() - @IsOptional() - force?: boolean; - - @IsBoolean() - @IsOptional() - importAfterPull?: boolean = true; - - @IsString({ each: true }) - @IsOptional() - files?: Set; - - @IsObject() - @IsOptional() - variables?: { [key: string]: string }; -} - -export class SourceControllPullOptions { - /** ID of user performing a source control pull. */ - userId: string; - - force?: boolean; - - variables?: { [key: string]: string }; -} diff --git a/packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts b/packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts deleted file mode 100644 index 55c8e178c4..0000000000 --- a/packages/cli/src/environments.ee/source-control/types/source-control-push-work-folder.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { IsBoolean, IsOptional, IsString } from 'class-validator'; - -import type { SourceControlledFile } from './source-controlled-file'; - -export class SourceControlPushWorkFolder { - @IsBoolean() - @IsOptional() - force?: boolean; - - @IsString({ each: true }) - fileNames: SourceControlledFile[]; - - @IsString() - @IsOptional() - message?: string; - - @IsBoolean() - @IsOptional() - skipDiff?: boolean; -} diff --git a/packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts b/packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts deleted file mode 100644 index 5bbf75921b..0000000000 --- a/packages/cli/src/environments.ee/source-control/types/source-controlled-file.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type SourceControlledFileStatus = - | 'new' - | 'modified' - | 'deleted' - | 'created' - | 'renamed' - | 'conflicted' - | 'ignored' - | 'staged' - | 'unknown'; -export type SourceControlledFileLocation = 'local' | 'remote'; -export type SourceControlledFileType = 'credential' | 'workflow' | 'tags' | 'variables' | 'file'; -export type SourceControlledFile = { - file: string; - id: string; - name: string; - type: SourceControlledFileType; - status: SourceControlledFileStatus; - location: SourceControlledFileLocation; - conflict: boolean; - updatedAt: string; - pushed?: boolean; -}; diff --git a/packages/cli/src/public-api/types.ts b/packages/cli/src/public-api/types.ts index b10d2f81bd..14e586dfdb 100644 --- a/packages/cli/src/public-api/types.ts +++ b/packages/cli/src/public-api/types.ts @@ -173,16 +173,6 @@ export interface IJsonSchema { required: string[]; } -export class SourceControlPull { - force?: boolean; - - variables?: { [key: string]: string }; -} - -export declare namespace PublicSourceControlRequest { - type Pull = AuthenticatedRequest<{}, {}, SourceControlPull, {}>; -} - // ---------------------------------- // /audit // ---------------------------------- diff --git a/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts b/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts index 646a3f075e..09f5a1bc85 100644 --- a/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/source-control/source-control.handler.ts @@ -1,3 +1,4 @@ +import { PullWorkFolderRequestDto } from '@n8n/api-types'; import { Container } from '@n8n/di'; import type express from 'express'; import type { StatusResult } from 'simple-git'; @@ -10,15 +11,15 @@ import { SourceControlPreferencesService } from '@/environments.ee/source-contro import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee'; import type { ImportResult } from '@/environments.ee/source-control/types/import-result'; import { EventService } from '@/events/event.service'; +import type { AuthenticatedRequest } from '@/requests'; -import type { PublicSourceControlRequest } from '../../../types'; import { globalScope } from '../../shared/middlewares/global.middleware'; export = { pull: [ globalScope('sourceControl:pull'), async ( - req: PublicSourceControlRequest.Pull, + req: AuthenticatedRequest, res: express.Response, ): Promise> => { const sourceControlPreferencesService = Container.get(SourceControlPreferencesService); @@ -33,17 +34,14 @@ export = { .json({ status: 'Error', message: 'Source Control is not connected to a repository' }); } try { + const payload = PullWorkFolderRequestDto.parse(req.body); const sourceControlService = Container.get(SourceControlService); - const result = await sourceControlService.pullWorkfolder({ - force: req.body.force, - variables: req.body.variables, - userId: req.user.id, - }); + const result = await sourceControlService.pullWorkfolder(req.user.id, payload); if (result.statusCode === 200) { Container.get(EventService).emit('source-control-user-pulled-api', { ...getTrackingInformationFromPullResult(result.statusResult), - forced: req.body.force ?? false, + forced: payload.force ?? false, }); return res.status(200).send(result.statusResult); } else { diff --git a/packages/cli/test/integration/environments/source-control-import.service.test.ts b/packages/cli/test/integration/environments/source-control-import.service.test.ts index 6835d18f58..17f8654aae 100644 --- a/packages/cli/test/integration/environments/source-control-import.service.test.ts +++ b/packages/cli/test/integration/environments/source-control-import.service.test.ts @@ -1,3 +1,4 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; import { Cipher } from 'n8n-core'; @@ -11,7 +12,6 @@ import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SourceControlImportService } from '@/environments.ee/source-control/source-control-import.service.ee'; import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential'; -import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { mockInstance } from '../../shared/mocking'; import { saveCredential } from '../shared/db/credentials'; diff --git a/packages/cli/test/integration/environments/source-control.test.ts b/packages/cli/test/integration/environments/source-control.test.ts index 11a7ad8a2b..2dedd53287 100644 --- a/packages/cli/test/integration/environments/source-control.test.ts +++ b/packages/cli/test/integration/environments/source-control.test.ts @@ -1,10 +1,10 @@ +import type { SourceControlledFile } from '@n8n/api-types'; import { Container } from '@n8n/di'; import config from '@/config'; import type { User } from '@/databases/entities/user'; import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee'; -import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file'; import { Telemetry } from '@/telemetry'; import { mockInstance } from '@test/mocking'; diff --git a/packages/editor-ui/src/api/sourceControl.ts b/packages/editor-ui/src/api/sourceControl.ts index c8e4eebcbc..3aa929b6af 100644 --- a/packages/editor-ui/src/api/sourceControl.ts +++ b/packages/editor-ui/src/api/sourceControl.ts @@ -1,7 +1,10 @@ -import type { IDataObject } from 'n8n-workflow'; +import type { + PullWorkFolderRequestDto, + PushWorkFolderRequestDto, + SourceControlledFile, +} from '@n8n/api-types'; import type { IRestApiContext } from '@/Interface'; import type { - SourceControlAggregatedFile, SourceControlPreferences, SourceControlStatus, SshKeyTypes, @@ -22,14 +25,14 @@ const createPreferencesRequestFn = export const pushWorkfolder = async ( context: IRestApiContext, - data: IDataObject, + data: PushWorkFolderRequestDto, ): Promise => { return await makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/push-workfolder`, data); }; export const pullWorkfolder = async ( context: IRestApiContext, - data: IDataObject, + data: PullWorkFolderRequestDto, ): Promise => { return await makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/pull-workfolder`, data); }; @@ -60,7 +63,7 @@ export const getAggregatedStatus = async ( preferLocalVersion: boolean; verbose: boolean; } = { direction: 'push', preferLocalVersion: true, verbose: false }, -): Promise => { +): Promise => { return await makeRestApiRequest(context, 'GET', `${sourceControlApiRoot}/get-status`, options); }; diff --git a/packages/editor-ui/src/components/MainSidebarSourceControl.vue b/packages/editor-ui/src/components/MainSidebarSourceControl.vue index 8dab6c1de8..1fa8338a3f 100644 --- a/packages/editor-ui/src/components/MainSidebarSourceControl.vue +++ b/packages/editor-ui/src/components/MainSidebarSourceControl.vue @@ -8,8 +8,8 @@ import { useLoadingService } from '@/composables/useLoadingService'; import { useUIStore } from '@/stores/ui.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY } from '@/constants'; -import type { SourceControlAggregatedFile } from '@/types/sourceControl.types'; import { sourceControlEventBus } from '@/event-bus/source-control'; +import type { SourceControlledFile } from '@n8n/api-types'; defineProps<{ isCollapsed: boolean; @@ -69,10 +69,8 @@ async function pullWorkfolder() { loadingService.setLoadingText(i18n.baseText('settings.sourceControl.loading.pull')); try { - const status: SourceControlAggregatedFile[] = - ((await sourceControlStore.pullWorkfolder( - false, - )) as unknown as SourceControlAggregatedFile[]) || []; + const status: SourceControlledFile[] = + ((await sourceControlStore.pullWorkfolder(false)) as unknown as SourceControlledFile[]) || []; const statusWithoutLocallyCreatedWorkflows = status.filter((file) => { return !(file.type === 'workflow' && file.status === 'created' && file.location === 'local'); diff --git a/packages/editor-ui/src/components/SourceControlPullModal.ee.vue b/packages/editor-ui/src/components/SourceControlPullModal.ee.vue index ac3bce1922..f70b4f7925 100644 --- a/packages/editor-ui/src/components/SourceControlPullModal.ee.vue +++ b/packages/editor-ui/src/components/SourceControlPullModal.ee.vue @@ -2,7 +2,6 @@ import Modal from './Modal.vue'; import { SOURCE_CONTROL_PULL_MODAL_KEY } from '@/constants'; import type { EventBus } from 'n8n-design-system/utils'; -import type { SourceControlAggregatedFile } from '@/types/sourceControl.types'; import { useI18n } from '@/composables/useI18n'; import { useLoadingService } from '@/composables/useLoadingService'; import { useToast } from '@/composables/useToast'; @@ -10,9 +9,10 @@ import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; import { computed, nextTick, ref } from 'vue'; import { sourceControlEventBus } from '@/event-bus/source-control'; +import type { SourceControlledFile } from '@n8n/api-types'; const props = defineProps<{ - data: { eventBus: EventBus; status: SourceControlAggregatedFile[] }; + data: { eventBus: EventBus; status: SourceControlledFile[] }; }>(); const incompleteFileTypes = ['variables', 'credential']; @@ -23,7 +23,7 @@ const toast = useToast(); const i18n = useI18n(); const sourceControlStore = useSourceControlStore(); -const files = ref(props.data.status || []); +const files = ref(props.data.status || []); const workflowFiles = computed(() => { return files.value.filter((file) => file.type === 'workflow'); diff --git a/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts b/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts index 2d71a7f6d2..80af6e29f2 100644 --- a/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts +++ b/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts @@ -5,7 +5,7 @@ import { createComponentRenderer } from '@/__tests__/render'; import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue'; import { createTestingPinia } from '@pinia/testing'; import { createEventBus } from 'n8n-design-system'; -import type { SourceControlAggregatedFile } from '@/types/sourceControl.types'; +import type { SourceControlledFile } from '@n8n/api-types'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { mockedStore } from '@/__tests__/utils'; import { VIEWS } from '@/constants'; @@ -71,7 +71,7 @@ describe('SourceControlPushModal', () => { }); it('should toggle checkboxes', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'gTbbBkkYTnNyX1jD', name: 'My workflow 1', @@ -160,7 +160,7 @@ describe('SourceControlPushModal', () => { }); it('should push non workflow entities', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'gTbbBkkYTnNyX1jD', name: 'credential', @@ -226,7 +226,7 @@ describe('SourceControlPushModal', () => { }); it('should auto select currentWorkflow', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'gTbbBkkYTnNyX1jD', name: 'My workflow 1', @@ -276,7 +276,7 @@ describe('SourceControlPushModal', () => { describe('filters', () => { it('should filter by name', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'gTbbBkkYTnNyX1jD', name: 'My workflow 1', @@ -317,7 +317,7 @@ describe('SourceControlPushModal', () => { }); it('should filter by status', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'gTbbBkkYTnNyX1jD', name: 'Created Workflow', @@ -375,7 +375,7 @@ describe('SourceControlPushModal', () => { }); it('should reset', async () => { - const status: SourceControlAggregatedFile[] = [ + const status: SourceControlledFile[] = [ { id: 'JIGKevgZagmJAnM6', name: 'Modified workflow', diff --git a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue index eb518c2d4c..5aad2a6be6 100644 --- a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue +++ b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue @@ -31,16 +31,15 @@ import { N8nInfoTip, } from 'n8n-design-system'; import { + type SourceControlledFile, SOURCE_CONTROL_FILE_STATUS, SOURCE_CONTROL_FILE_TYPE, SOURCE_CONTROL_FILE_LOCATION, - type SourceControlledFileStatus, - type SourceControlAggregatedFile, -} from '@/types/sourceControl.types'; +} from '@n8n/api-types'; import { orderBy, groupBy } from 'lodash-es'; const props = defineProps<{ - data: { eventBus: EventBus; status: SourceControlAggregatedFile[] }; + data: { eventBus: EventBus; status: SourceControlledFile[] }; }>(); const loadingService = useLoadingService(); @@ -50,49 +49,48 @@ const i18n = useI18n(); const sourceControlStore = useSourceControlStore(); const route = useRoute(); +type SourceControlledFileStatus = SourceControlledFile['status']; + type Changes = { - tags: SourceControlAggregatedFile[]; - variables: SourceControlAggregatedFile[]; - credentials: SourceControlAggregatedFile[]; - workflows: SourceControlAggregatedFile[]; - currentWorkflow?: SourceControlAggregatedFile; + tags: SourceControlledFile[]; + variables: SourceControlledFile[]; + credentials: SourceControlledFile[]; + workflows: SourceControlledFile[]; + currentWorkflow?: SourceControlledFile; }; -const classifyFilesByType = ( - files: SourceControlAggregatedFile[], - currentWorkflowId?: string, -): Changes => +const classifyFilesByType = (files: SourceControlledFile[], currentWorkflowId?: string): Changes => files.reduce( (acc, file) => { // do not show remote workflows that are not yet created locally during push if ( - file.location === SOURCE_CONTROL_FILE_LOCATION.REMOTE && - file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW && - file.status === SOURCE_CONTROL_FILE_STATUS.CREATED + file.location === SOURCE_CONTROL_FILE_LOCATION.remote && + file.type === SOURCE_CONTROL_FILE_TYPE.workflow && + file.status === SOURCE_CONTROL_FILE_STATUS.created ) { return acc; } - if (file.type === SOURCE_CONTROL_FILE_TYPE.VARIABLES) { + if (file.type === SOURCE_CONTROL_FILE_TYPE.variables) { acc.variables.push(file); return acc; } - if (file.type === SOURCE_CONTROL_FILE_TYPE.TAGS) { + if (file.type === SOURCE_CONTROL_FILE_TYPE.tags) { acc.tags.push(file); return acc; } - if (file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW && currentWorkflowId === file.id) { + if (file.type === SOURCE_CONTROL_FILE_TYPE.workflow && currentWorkflowId === file.id) { acc.currentWorkflow = file; } - if (file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW) { + if (file.type === SOURCE_CONTROL_FILE_TYPE.workflow) { acc.workflows.push(file); return acc; } - if (file.type === SOURCE_CONTROL_FILE_TYPE.CREDENTIAL) { + if (file.type === SOURCE_CONTROL_FILE_TYPE.credential) { acc.credentials.push(file); return acc; } @@ -139,7 +137,7 @@ const toggleSelected = (id: string) => { } }; -const maybeSelectCurrentWorkflow = (workflow?: SourceControlAggregatedFile) => +const maybeSelectCurrentWorkflow = (workflow?: SourceControlledFile) => workflow && selectedChanges.value.add(workflow.id); onMounted(() => maybeSelectCurrentWorkflow(changes.value.currentWorkflow)); @@ -152,15 +150,15 @@ const resetFilters = () => { const statusFilterOptions: Array<{ label: string; value: SourceControlledFileStatus }> = [ { label: 'New', - value: SOURCE_CONTROL_FILE_STATUS.CREATED, + value: SOURCE_CONTROL_FILE_STATUS.created, }, { label: 'Modified', - value: SOURCE_CONTROL_FILE_STATUS.MODIFIED, + value: SOURCE_CONTROL_FILE_STATUS.modified, }, { label: 'Deleted', - value: SOURCE_CONTROL_FILE_STATUS.DELETED, + value: SOURCE_CONTROL_FILE_STATUS.deleted, }, ] as const; @@ -188,10 +186,10 @@ const filteredWorkflows = computed(() => { }); const statusPriority: Partial> = { - [SOURCE_CONTROL_FILE_STATUS.MODIFIED]: 1, - [SOURCE_CONTROL_FILE_STATUS.RENAMED]: 2, - [SOURCE_CONTROL_FILE_STATUS.CREATED]: 3, - [SOURCE_CONTROL_FILE_STATUS.DELETED]: 4, + [SOURCE_CONTROL_FILE_STATUS.modified]: 1, + [SOURCE_CONTROL_FILE_STATUS.renamed]: 2, + [SOURCE_CONTROL_FILE_STATUS.created]: 3, + [SOURCE_CONTROL_FILE_STATUS.deleted]: 4, } as const; const getPriorityByStatus = (status: SourceControlledFileStatus): number => statusPriority[status] ?? 0; @@ -250,7 +248,7 @@ function close() { uiStore.closeModal(SOURCE_CONTROL_PUSH_MODAL_KEY); } -function renderUpdatedAt(file: SourceControlAggregatedFile) { +function renderUpdatedAt(file: SourceControlledFile) { const currentYear = new Date().getFullYear().toString(); return i18n.baseText('settings.sourceControl.lastUpdated', { @@ -338,9 +336,9 @@ const getStatusTheme = (status: SourceControlledFileStatus) => { const statusToBadgeThemeMap: Partial< Record > = { - [SOURCE_CONTROL_FILE_STATUS.CREATED]: 'success', - [SOURCE_CONTROL_FILE_STATUS.DELETED]: 'danger', - [SOURCE_CONTROL_FILE_STATUS.MODIFIED]: 'warning', + [SOURCE_CONTROL_FILE_STATUS.created]: 'success', + [SOURCE_CONTROL_FILE_STATUS.deleted]: 'danger', + [SOURCE_CONTROL_FILE_STATUS.modified]: 'warning', } as const; return statusToBadgeThemeMap[status]; }; @@ -454,11 +452,11 @@ const getStatusTheme = (status: SourceControlledFileStatus) => { @update:model-value="toggleSelected(file.id)" > - - + + Deleted Workflow: - + Deleted Credential: {{ file.name || file.id }} diff --git a/packages/editor-ui/src/stores/sourceControl.store.ts b/packages/editor-ui/src/stores/sourceControl.store.ts index eee5107570..70cb31fc3c 100644 --- a/packages/editor-ui/src/stores/sourceControl.store.ts +++ b/packages/editor-ui/src/stores/sourceControl.store.ts @@ -6,6 +6,7 @@ import { useRootStore } from '@/stores/root.store'; import * as vcApi from '@/api/sourceControl'; import type { SourceControlPreferences, SshKeyTypes } from '@/types/sourceControl.types'; import type { TupleToUnion } from '@/utils/typeHelpers'; +import type { SourceControlledFile } from '@n8n/api-types'; export const useSourceControlStore = defineStore('sourceControl', () => { const rootStore = useRootStore(); @@ -39,23 +40,14 @@ export const useSourceControlStore = defineStore('sourceControl', () => { const pushWorkfolder = async (data: { commitMessage: string; - fileNames?: Array<{ - conflict: boolean; - file: string; - id: string; - location: string; - name: string; - status: string; - type: string; - updatedAt?: string | undefined; - }>; + fileNames: SourceControlledFile[]; force: boolean; }) => { state.commitMessage = data.commitMessage; await vcApi.pushWorkfolder(rootStore.restApiContext, { force: data.force, - message: data.commitMessage, - ...(data.fileNames ? { fileNames: data.fileNames } : {}), + commitMessage: data.commitMessage, + fileNames: data.fileNames, }); }; diff --git a/packages/editor-ui/src/types/sourceControl.types.ts b/packages/editor-ui/src/types/sourceControl.types.ts index bd8f6159b3..1b412ed843 100644 --- a/packages/editor-ui/src/types/sourceControl.types.ts +++ b/packages/editor-ui/src/types/sourceControl.types.ts @@ -1,37 +1,5 @@ import type { TupleToUnion } from '@/utils/typeHelpers'; -export const SOURCE_CONTROL_FILE_STATUS = { - NEW: 'new', - MODIFIED: 'modified', - DELETED: 'deleted', - CREATED: 'created', - RENAMED: 'renamed', - CONFLICTED: 'conflicted', - IGNORED: 'ignored', - STAGED: 'staged', - UNKNOWN: 'unknown', -} as const; - -export const SOURCE_CONTROL_FILE_LOCATION = { - LOCAL: 'local', - REMOTE: 'remote', -} as const; - -export const SOURCE_CONTROL_FILE_TYPE = { - CREDENTIAL: 'credential', - WORKFLOW: 'workflow', - TAGS: 'tags', - VARIABLES: 'variables', - FILE: 'file', -} as const; - -export type SourceControlledFileStatus = - (typeof SOURCE_CONTROL_FILE_STATUS)[keyof typeof SOURCE_CONTROL_FILE_STATUS]; -export type SourceControlledFileLocation = - (typeof SOURCE_CONTROL_FILE_LOCATION)[keyof typeof SOURCE_CONTROL_FILE_LOCATION]; -export type SourceControlledFileType = - (typeof SOURCE_CONTROL_FILE_TYPE)[keyof typeof SOURCE_CONTROL_FILE_TYPE]; - export type SshKeyTypes = ['ed25519', 'rsa']; export type SourceControlPreferences = { @@ -65,14 +33,3 @@ export interface SourceControlStatus { staged: string[]; tracking: null; } - -export interface SourceControlAggregatedFile { - conflict: boolean; - file: string; - id: string; - location: SourceControlledFileLocation; - name: string; - status: SourceControlledFileStatus; - type: SourceControlledFileType; - updatedAt?: string; -}