diff --git a/packages/adapter-api/src/adapter.ts b/packages/adapter-api/src/adapter.ts index 57aed2b06ad..b6d2bec9e11 100644 --- a/packages/adapter-api/src/adapter.ts +++ b/packages/adapter-api/src/adapter.ts @@ -186,6 +186,23 @@ export type ConfigCreator = { getConfig: (options?: InstanceElement) => Promise } +export type IsInitializedFolderArgs = { + baseDir: string +} + +export type IsInitializedFolderResult = { + result: boolean + errors: ReadonlyArray +} + +export type InitFolderArgs = { + baseDir: string +} + +export type InitFolderResult = { + errors: ReadonlyArray +} + export type LoadElementsFromFolderArgs = { baseDir: string } & AdapterBaseContext @@ -235,6 +252,13 @@ export type ReferenceInfo = { export type GetCustomReferencesFunc = (elements: Element[], adapterConfig?: InstanceElement) => Promise +export type AdapterFormat = { + isInitializedFolder?: (args: IsInitializedFolderArgs) => Promise + initFolder?: (args: InitFolderArgs) => Promise + loadElementsFromFolder?: (args: LoadElementsFromFolderArgs) => Promise + dumpElementsToFolder?: (args: DumpElementsToFolderArgs) => Promise +} + export type Adapter = { operations: (context: AdapterOperationsContext) => AdapterOperations validateCredentials: (config: Readonly) => Promise @@ -242,9 +266,8 @@ export type Adapter = { configType?: ObjectType configCreator?: ConfigCreator install?: () => Promise - loadElementsFromFolder?: (args: LoadElementsFromFolderArgs) => Promise - dumpElementsToFolder?: (args: DumpElementsToFolderArgs) => Promise getAdditionalReferences?: GetAdditionalReferencesFunc + adapterFormat?: AdapterFormat getCustomReferences?: GetCustomReferencesFunc } diff --git a/packages/cli/src/commands/adapter_format.ts b/packages/cli/src/commands/adapter_format.ts index 30f99425d69..11ca4f1ae2c 100644 --- a/packages/cli/src/commands/adapter_format.ts +++ b/packages/cli/src/commands/adapter_format.ts @@ -9,13 +9,14 @@ import _ from 'lodash' import { logger } from '@salto-io/logging' import { Workspace } from '@salto-io/workspace' import { collections } from '@salto-io/lowerdash' -import { calculatePatch, syncWorkspaceToFolder } from '@salto-io/core' +import { calculatePatch, syncWorkspaceToFolder, initFolder, isInitializedFolder } from '@salto-io/core' import { WorkspaceCommandAction, createWorkspaceCommand, createCommandGroupDef } from '../command_builder' import { outputLine, errorOutputLine } from '../outputer' import { validateWorkspace, formatWorkspaceErrors } from '../workspace/workspace' import { CliExitCode, CliOutput } from '../types' import { UpdateModeArg, UPDATE_MODE_OPTION } from './common/update_mode' import { formatFetchWarnings, formatSyncToWorkspaceErrors } from '../formatter' +import { getUserBooleanInput } from '../callbacks' const log = logger(module) const { awu } = collections.asynciterable @@ -161,13 +162,34 @@ const applyPatchCmd = createWorkspaceCommand({ type SyncWorkspaceToFolderArgs = { toDir: string accountName: 'salesforce' + force: boolean } export const syncWorkspaceToFolderAction: WorkspaceCommandAction = async ({ workspace, input, output, }) => { - const { accountName, toDir } = input + const { accountName, toDir, force } = input + + const initializedResult = await isInitializedFolder({ workspace, accountName, baseDir: toDir }) + if (initializedResult.errors.length > 0) { + outputLine(formatSyncToWorkspaceErrors(initializedResult.errors), output) + return CliExitCode.AppError + } + + if (!initializedResult.result) { + if (force || (await getUserBooleanInput('The folder is no initialized for the adapter format, initialize?'))) { + outputLine(`Initializing adapter format folder at ${toDir}`, output) + const initResult = await initFolder({ workspace, accountName, baseDir: toDir }) + if (initResult.errors.length > 0) { + outputLine(formatSyncToWorkspaceErrors(initResult.errors), output) + return CliExitCode.AppError + } + } else { + outputLine('Folder not initialized for adapter format, aborting', output) + return CliExitCode.UserInputError + } + } outputLine(`Synchronizing content of workspace to folder at ${toDir}`, output) const result = await syncWorkspaceToFolder({ workspace, accountName, baseDir: toDir }) @@ -199,6 +221,13 @@ const syncToWorkspaceCmd = createWorkspaceCommand({ choices: ['salesforce'], default: 'salesforce', }, + { + name: 'force', + type: 'boolean', + alias: 'f', + description: 'Initialize the folder for adapter format if needed', + default: false, + }, ], }, action: syncWorkspaceToFolderAction, diff --git a/packages/cli/test/commands/adapter_format.test.ts b/packages/cli/test/commands/adapter_format.test.ts index e14e804fc1f..6dfeddf7e20 100644 --- a/packages/cli/test/commands/adapter_format.test.ts +++ b/packages/cli/test/commands/adapter_format.test.ts @@ -7,7 +7,7 @@ */ import { Element, InstanceElement, ObjectType, toChange } from '@salto-io/adapter-api' import { detailedCompare } from '@salto-io/adapter-utils' -import { calculatePatch, syncWorkspaceToFolder } from '@salto-io/core' +import { calculatePatch, initFolder, isInitializedFolder, syncWorkspaceToFolder } from '@salto-io/core' import { merger, updateElementsWithAlternativeAccount } from '@salto-io/workspace' import * as mocks from '../mocks' import { applyPatchAction, syncWorkspaceToFolderAction } from '../../src/commands/adapter_format' @@ -19,11 +19,15 @@ jest.mock('@salto-io/core', () => { ...actual, calculatePatch: jest.fn().mockImplementation(actual.calculatePatch), syncWorkspaceToFolder: jest.fn().mockImplementation(actual.syncWorkspaceToFolder), + isInitializedFolder: jest.fn().mockImplementation(actual.isInitializedFolder), + initFolder: jest.fn().mockImplementation(actual.initFolder), } }) const mockCalculatePatch = calculatePatch as jest.MockedFunction const mockSyncWorkspaceToFolder = syncWorkspaceToFolder as jest.MockedFunction +const mockIsInitializedFolder = isInitializedFolder as jest.MockedFunction +const mockInitFolder = initFolder as jest.MockedFunction describe('apply-patch command', () => { const commandName = 'apply-patch' @@ -238,16 +242,83 @@ describe('sync-to-workspace command', () => { }) }) - describe('when core sync returns without errors', () => { + describe('when isFolderInitialized returns errors', () => { + let result: CliExitCode + beforeEach(async () => { + mockIsInitializedFolder.mockResolvedValueOnce({ + result: false, + errors: [{ severity: 'Error', message: 'Not supported', detailedMessage: 'detailed Not Supported' }], + }) + result = await syncWorkspaceToFolderAction({ + ...cliCommandArgs, + workspace, + input: { + accountName: 'salesforce', + toDir: 'someDir', + force: true, + }, + }) + }) + it('should return non-success exit code', () => { + expect(result).toEqual(CliExitCode.AppError) + }) + }) + + describe('when folder is not initialized and initFolder returns errors', () => { + let result: CliExitCode + beforeEach(async () => { + mockIsInitializedFolder.mockResolvedValueOnce({ result: false, errors: [] }) + mockInitFolder.mockResolvedValueOnce({ + errors: [{ severity: 'Error', message: 'Not supported', detailedMessage: 'detailed Not Supported' }], + }) + result = await syncWorkspaceToFolderAction({ + ...cliCommandArgs, + workspace, + input: { + accountName: 'salesforce', + toDir: 'someDir', + force: true, + }, + }) + }) + it('should return non-success exit code', () => { + expect(result).toEqual(CliExitCode.AppError) + }) + }) + + describe('when folder is initialized and core sync returns without errors', () => { + let result: CliExitCode + beforeEach(async () => { + mockSyncWorkspaceToFolder.mockResolvedValueOnce({ errors: [] }) + mockIsInitializedFolder.mockResolvedValueOnce({ result: true, errors: [] }) + result = await syncWorkspaceToFolderAction({ + ...cliCommandArgs, + workspace, + input: { + accountName: 'salesforce', + toDir: 'someDir', + force: true, + }, + }) + }) + it('should return success exit code', () => { + expect(result).toEqual(CliExitCode.Success) + }) + }) + + describe('when folder is not initialized and folder init and core sync returns without errors', () => { let result: CliExitCode beforeEach(async () => { mockSyncWorkspaceToFolder.mockResolvedValueOnce({ errors: [] }) + mockIsInitializedFolder.mockResolvedValueOnce({ result: false, errors: [] }) + mockInitFolder.mockResolvedValueOnce({ errors: [] }) result = await syncWorkspaceToFolderAction({ ...cliCommandArgs, workspace, input: { accountName: 'salesforce', toDir: 'someDir', + force: true, }, }) }) @@ -256,18 +327,20 @@ describe('sync-to-workspace command', () => { }) }) - describe('when core sync returns with errors', () => { + describe('when folder is initialized and core sync returns with errors', () => { let result: CliExitCode beforeEach(async () => { mockSyncWorkspaceToFolder.mockResolvedValueOnce({ errors: [{ severity: 'Error', message: 'Not supported', detailedMessage: 'detailed Not Supported' }], }) + mockIsInitializedFolder.mockResolvedValueOnce({ result: true, errors: [] }) result = await syncWorkspaceToFolderAction({ ...cliCommandArgs, workspace, input: { accountName: 'salesforce', toDir: 'someDir', + force: true, }, }) }) diff --git a/packages/core/index.ts b/packages/core/index.ts index 382221ec9fc..4f20ca3b3f2 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -70,4 +70,10 @@ export { } from './src/local-workspace/remote_map' export { NoWorkspaceConfig } from './src/local-workspace/errors' export * from './src/types' -export { calculatePatch, syncWorkspaceToFolder, updateElementFolder } from './src/core/adapter_format' +export { + calculatePatch, + syncWorkspaceToFolder, + updateElementFolder, + isInitializedFolder, + initFolder, +} from './src/core/adapter_format' diff --git a/packages/core/src/core/adapter_format.ts b/packages/core/src/core/adapter_format.ts index 1b80c2434f9..6593199b3b2 100644 --- a/packages/core/src/core/adapter_format.ts +++ b/packages/core/src/core/adapter_format.ts @@ -6,7 +6,7 @@ * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES */ import _ from 'lodash' -import { Adapter, AdapterOperationsContext, Change, Element, SaltoError } from '@salto-io/adapter-api' +import { Adapter, AdapterFormat, AdapterOperationsContext, Change, Element, SaltoError } from '@salto-io/adapter-api' import { logger } from '@salto-io/logging' import { collections } from '@salto-io/lowerdash' import { merger, Workspace, ElementSelector, expressions, elementSource } from '@salto-io/workspace' @@ -19,12 +19,37 @@ const log = logger(module) const { awu } = collections.asynciterable const { makeArray } = collections.array -type GetAdapterAndContextArgs = { +type GetAdapterArgs = { workspace: Workspace accountName: string +} + +const getAdapter = ({ + workspace, + accountName, +}: GetAdapterArgs): + | { adapter: Adapter; adapterName: string; error: undefined } + | { adapter: undefined; adapterName: undefined; error: SaltoError } => { + const adapterName = workspace.getServiceFromAccountName(accountName) + if (adapterName !== accountName) { + return { + adapter: undefined, + adapterName: undefined, + error: { + severity: 'Error', + message: 'Account name that is different from the adapter name is not supported', + detailedMessage: '', + }, + } + } + + return { adapter: adapterCreators[adapterName], adapterName, error: undefined } +} + +type GetAdapterAndContextArgs = { ignoreStateElemIdMapping?: boolean ignoreStateElemIdMappingForSelectors?: ElementSelector[] -} +} & GetAdapterArgs type GetAdapterAndContextResult = { adapter: Adapter @@ -38,10 +63,11 @@ const getAdapterAndContext = async ({ ignoreStateElemIdMapping, ignoreStateElemIdMappingForSelectors, }: GetAdapterAndContextArgs): Promise => { - const adapterName = workspace.getServiceFromAccountName(accountName) - if (adapterName !== accountName) { - throw new Error('Account name that is different from the adapter name is not supported') + const { adapter, adapterName, error } = getAdapter({ workspace, accountName }) + if (error !== undefined) { + throw new Error(error.message) } + const workspaceElements = await workspace.elements() const resolvedElements = await expressions.resolve( await awu(await workspaceElements.getAll()).toArray(), @@ -56,13 +82,80 @@ const getAdapterAndContext = async ({ ignoreStateElemIdMappingForSelectors, }) const adapterContext = adaptersCreatorConfigs[accountName] - const adapter = adapterCreators[adapterName] return { adapter, adapterContext, resolvedElements } } +type IsInitializedFolderArgs = { + baseDir: string +} & GetAdapterArgs + +export type IsInitializedFolderResult = { + result: boolean + errors: ReadonlyArray +} + +export const isInitializedFolder = async ({ + baseDir, + workspace, + accountName, +}: IsInitializedFolderArgs): Promise => { + const { adapter, error } = getAdapter({ workspace, accountName }) + if (error !== undefined) { + return { + result: false, + errors: [error], + } + } + + if (adapter.adapterFormat?.isInitializedFolder === undefined) { + return { + result: false, + errors: [ + { + severity: 'Error' as const, + message: 'Format not supported', + detailedMessage: `Account ${accountName}'s adapter does not support checking a non-nacl format folder`, + }, + ], + } + } + + return adapter.adapterFormat.isInitializedFolder({ baseDir }) +} + +type InitFolderArgs = { + baseDir: string +} & GetAdapterArgs + +export type InitFolderResult = { + errors: ReadonlyArray +} + +export const initFolder = async ({ baseDir, workspace, accountName }: InitFolderArgs): Promise => { + const { adapter, error } = getAdapter({ workspace, accountName }) + if (error !== undefined) { + return { errors: [error] } + } + + const adapterInitFolder = adapter.adapterFormat?.initFolder + if (adapterInitFolder === undefined) { + return { + errors: [ + { + severity: 'Error' as const, + message: 'Format not supported', + detailedMessage: `Account ${accountName}'s adapter does not support initializing a non-nacl format folder`, + }, + ], + } + } + + return adapterInitFolder({ baseDir }) +} + const loadElementsAndMerge = ( dir: string, - loadElementsFromFolder: NonNullable, + loadElementsFromFolder: NonNullable, adapterContext: AdapterOperationsContext, ): Promise<{ elements: Element[] @@ -104,7 +197,7 @@ export const calculatePatch = async ({ ignoreStateElemIdMapping, ignoreStateElemIdMappingForSelectors, }) - const { loadElementsFromFolder } = adapter + const loadElementsFromFolder = adapter.adapterFormat?.loadElementsFromFolder if (loadElementsFromFolder === undefined) { throw new Error(`Account ${accountName}'s adapter does not support loading a non-nacl format`) } @@ -162,6 +255,7 @@ type SyncWorkspaceToFolderArgs = { export type SyncWorkspaceToFolderResult = { errors: ReadonlyArray } + export const syncWorkspaceToFolder = ({ workspace, accountName, @@ -181,7 +275,8 @@ export const syncWorkspaceToFolder = ({ ignoreStateElemIdMapping, ignoreStateElemIdMappingForSelectors, }) - const { loadElementsFromFolder, dumpElementsToFolder } = adapter + const loadElementsFromFolder = adapter.adapterFormat?.loadElementsFromFolder + const dumpElementsToFolder = adapter.adapterFormat?.dumpElementsToFolder if (loadElementsFromFolder === undefined) { return { errors: [ @@ -265,7 +360,7 @@ export const updateElementFolder = ({ workspace, accountName, }) - const { dumpElementsToFolder } = adapter + const dumpElementsToFolder = adapter.adapterFormat?.dumpElementsToFolder if (dumpElementsToFolder === undefined) { return { errors: [ diff --git a/packages/core/test/common/helpers.ts b/packages/core/test/common/helpers.ts index d3b4fec6c14..b3f9f2657c8 100644 --- a/packages/core/test/common/helpers.ts +++ b/packages/core/test/common/helpers.ts @@ -5,7 +5,15 @@ * * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES */ -import { Adapter, AdapterOperations, Element, ElemID, InstanceElement, ObjectType } from '@salto-io/adapter-api' +import { + Adapter, + AdapterFormat, + AdapterOperations, + Element, + ElemID, + InstanceElement, + ObjectType, +} from '@salto-io/adapter-api' import { mockFunction } from '@salto-io/test-utils' import { elementSource, remoteMap } from '@salto-io/workspace' @@ -23,7 +31,9 @@ export const inMemRemoteMapCreator = (): remoteMap.RemoteMapCreator => { } } -export const createMockAdapter = (adapterName: string): jest.Mocked> => { +export const createMockAdapter = ( + adapterName: string, +): jest.Mocked> & { adapterFormat: jest.Mocked> } => { const configType = new ObjectType({ elemID: new ElemID(adapterName) }) const credentialsType = new ObjectType({ elemID: new ElemID(adapterName) }) const optionsType = new ObjectType({ elemID: new ElemID(adapterName) }) @@ -44,13 +54,20 @@ export const createMockAdapter = (adapterName: string): jest.Mocked['install']>().mockResolvedValue({ success: true, installedVersion: '1' }), - loadElementsFromFolder: mockFunction['loadElementsFromFolder']>().mockResolvedValue({ - elements: [], - }), - dumpElementsToFolder: mockFunction['dumpElementsToFolder']>().mockResolvedValue({ - errors: [], - unappliedChanges: [], - }), + adapterFormat: { + isInitializedFolder: mockFunction>().mockResolvedValue({ + result: false, + errors: [], + }), + initFolder: mockFunction>().mockResolvedValue({ errors: [] }), + loadElementsFromFolder: mockFunction>().mockResolvedValue({ + elements: [], + }), + dumpElementsToFolder: mockFunction>().mockResolvedValue({ + errors: [], + unappliedChanges: [], + }), + }, getAdditionalReferences: mockFunction['getAdditionalReferences']>().mockResolvedValue([]), getCustomReferences: mockFunction['getCustomReferences']>().mockResolvedValue([]), } diff --git a/packages/core/test/core/adapter_format.test.ts b/packages/core/test/core/adapter_format.test.ts index aed6a2f2680..fbaf77023ba 100644 --- a/packages/core/test/core/adapter_format.test.ts +++ b/packages/core/test/core/adapter_format.test.ts @@ -23,6 +23,8 @@ import { mockWorkspace } from '../common/workspace' import { createMockAdapter } from '../common/helpers' import { calculatePatch, + initFolder, + isInitializedFolder, syncWorkspaceToFolder, SyncWorkspaceToFolderResult, updateElementFolder, @@ -31,6 +33,167 @@ import { const { awu } = collections.asynciterable +describe('isInitializedFolder', () => { + const mockAdapterName = 'mock' + + let mockAdapter: ReturnType + let workspace: Workspace + beforeEach(() => { + mockAdapter = createMockAdapter(mockAdapterName) + adapterCreators[mockAdapterName] = mockAdapter + + workspace = mockWorkspace({ + name: 'workspace', + accounts: [mockAdapterName], + accountToServiceName: { [mockAdapterName]: mockAdapterName }, + }) + }) + afterEach(() => { + delete adapterCreators[mockAdapterName] + }) + + describe('when adapter returns false', () => { + beforeEach(() => { + mockAdapter.adapterFormat.isInitializedFolder.mockResolvedValueOnce({ result: false, errors: [] }) + }) + + it('should return false', async () => { + const res = await isInitializedFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.result).toBeFalse() + expect(res.errors).toBeEmpty() + }) + }) + + describe('when adapter returns true', () => { + beforeEach(() => { + mockAdapter.adapterFormat.isInitializedFolder.mockResolvedValueOnce({ result: true, errors: [] }) + }) + + it('should return true', async () => { + const res = await isInitializedFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.result).toBeTrue() + expect(res.errors).toBeEmpty() + }) + }) + + describe('when adapter returns an error', () => { + beforeEach(() => { + mockAdapter.adapterFormat.isInitializedFolder.mockResolvedValueOnce({ + result: false, + errors: [ + { + severity: 'Error', + message: 'Error message', + detailedMessage: 'Detailed message', + }, + ], + }) + }) + + it('should return an error', async () => { + const res = await isInitializedFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.result).toBeFalse() + expect(res.errors).toEqual([ + { + severity: 'Error', + message: 'Error message', + detailedMessage: 'Detailed message', + }, + ]) + }) + }) + + describe('when used with an account that does not support isInitializedFolder', () => { + beforeEach(() => { + delete (mockAdapter as Adapter).adapterFormat + }) + + it('should return an error', async () => { + const res = await isInitializedFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.errors).toEqual([ + { + severity: 'Error', + message: 'Format not supported', + detailedMessage: `Account ${mockAdapterName}'s adapter does not support checking a non-nacl format folder`, + }, + ]) + }) + }) +}) + +describe('initFolder', () => { + const mockAdapterName = 'mock' + + let mockAdapter: ReturnType + let workspace: Workspace + beforeEach(() => { + mockAdapter = createMockAdapter(mockAdapterName) + adapterCreators[mockAdapterName] = mockAdapter + + workspace = mockWorkspace({ + name: 'workspace', + accounts: [mockAdapterName], + accountToServiceName: { [mockAdapterName]: mockAdapterName }, + }) + }) + afterEach(() => { + delete adapterCreators[mockAdapterName] + }) + + describe('when adapter returns no errors', () => { + beforeEach(() => { + mockAdapter.adapterFormat.initFolder.mockResolvedValueOnce({ errors: [] }) + }) + + it('should return no errors', async () => { + const res = await initFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.errors).toBeEmpty() + }) + }) + + describe('when adapter returns errors', () => { + beforeEach(() => { + mockAdapter.adapterFormat.initFolder.mockResolvedValueOnce({ + errors: [ + { + severity: 'Error', + message: 'Failed initializing adapter format folder', + detailedMessage: 'Details on the error', + }, + ], + }) + }) + + it('should return errors', async () => { + const res = await initFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.errors).toEqual([ + { + severity: 'Error', + message: 'Failed initializing adapter format folder', + detailedMessage: 'Details on the error', + }, + ]) + }) + }) + + describe('when used with an account that does not support initFolder', () => { + beforeEach(() => { + delete (mockAdapter as Adapter).adapterFormat + }) + + it('should return an error', async () => { + const res = await initFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) + expect(res.errors).toEqual([ + { + severity: 'Error', + message: 'Format not supported', + detailedMessage: `Account ${mockAdapterName}'s adapter does not support initializing a non-nacl format folder`, + }, + ]) + }) + }) +}) + describe('calculatePatch', () => { const mockAdapterName = 'mock' const type = new ObjectType({ @@ -71,7 +234,7 @@ describe('calculatePatch', () => { const afterNewInstance = new InstanceElement('instance2', type, { f: 'v' }) const beforeElements = [instance] const afterElements = [afterModifyInstance, afterNewInstance] - mockAdapter.loadElementsFromFolder + mockAdapter.adapterFormat.loadElementsFromFolder .mockResolvedValueOnce({ elements: beforeElements }) .mockResolvedValueOnce({ elements: afterElements }) const res = await calculatePatch({ @@ -93,7 +256,7 @@ describe('calculatePatch', () => { afterModifyInstance.value.f = 'v3' const beforeElements: Element[] = [] const afterElements = [afterModifyInstance] - mockAdapter.loadElementsFromFolder + mockAdapter.adapterFormat.loadElementsFromFolder .mockResolvedValueOnce({ elements: beforeElements }) .mockResolvedValueOnce({ elements: afterElements }) const res = await calculatePatch({ @@ -114,7 +277,7 @@ describe('calculatePatch', () => { it('should return with no changes and no errors', async () => { const beforeElements = [instance] const afterElements = [instance] - mockAdapter.loadElementsFromFolder + mockAdapter.adapterFormat.loadElementsFromFolder .mockResolvedValueOnce({ elements: beforeElements }) .mockResolvedValueOnce({ elements: afterElements }) const res = await calculatePatch({ @@ -133,7 +296,7 @@ describe('calculatePatch', () => { describe('when there is a merge error', () => { it('should return with merge error and success false', async () => { const beforeElements = [instance, instance] - mockAdapter.loadElementsFromFolder.mockResolvedValueOnce({ elements: beforeElements }) + mockAdapter.adapterFormat.loadElementsFromFolder.mockResolvedValueOnce({ elements: beforeElements }) const res = await calculatePatch({ workspace, fromDir: 'before', @@ -153,16 +316,18 @@ describe('calculatePatch', () => { afterModifyInstance.value.f = 'v3' const beforeElements = [instance] const afterElements = [afterModifyInstance] - mockAdapter.loadElementsFromFolder.mockResolvedValueOnce({ elements: beforeElements }).mockResolvedValueOnce({ - elements: afterElements, - errors: [ - { - message: 'err', - severity: 'Warning', - detailedMessage: 'err', - }, - ], - }) + mockAdapter.adapterFormat.loadElementsFromFolder + .mockResolvedValueOnce({ elements: beforeElements }) + .mockResolvedValueOnce({ + elements: afterElements, + errors: [ + { + message: 'err', + severity: 'Warning', + detailedMessage: 'err', + }, + ], + }) const res = await calculatePatch({ workspace, fromDir: 'before', @@ -184,7 +349,7 @@ describe('calculatePatch', () => { afterConflictInstance.value.f = 'v4' const beforeElements = [beforeConflictInstance] const afterElements = [afterConflictInstance] - mockAdapter.loadElementsFromFolder + mockAdapter.adapterFormat.loadElementsFromFolder .mockResolvedValueOnce({ elements: beforeElements }) .mockResolvedValueOnce({ elements: afterElements }) const res = await calculatePatch({ @@ -204,7 +369,7 @@ describe('calculatePatch', () => { describe('when used with an account that does not support loadElementsFromFolder', () => { it('Should throw an error', async () => { - delete (mockAdapter as Adapter).loadElementsFromFolder + delete (mockAdapter as Adapter).adapterFormat await expect( calculatePatch({ workspace, fromDir: 'before', toDir: 'after', accountName: mockAdapterName }), ).rejects.toThrow() @@ -247,7 +412,7 @@ describe('syncWorkspaceToFolder', () => { accounts: [mockAdapterName], accountToServiceName: { [mockAdapterName]: mockAdapterName }, }) - mockAdapter.loadElementsFromFolder.mockResolvedValue({ elements: folderElements }) + mockAdapter.adapterFormat.loadElementsFromFolder.mockResolvedValue({ elements: folderElements }) }) describe('when adapter supports all required actions', () => { let result: SyncWorkspaceToFolderResult @@ -258,7 +423,7 @@ describe('syncWorkspaceToFolder', () => { expect(result.errors).toBeEmpty() }) it('should apply deletion changes for elements that exist in the folder and not the workspace', () => { - expect(mockAdapter.dumpElementsToFolder).toHaveBeenCalledWith({ + expect(mockAdapter.adapterFormat.dumpElementsToFolder).toHaveBeenCalledWith({ baseDir: 'dir', changes: expect.arrayContaining([expect.objectContaining(toChange({ before: separateInstanceInFolder }))]), elementsSource: expect.anything(), @@ -266,7 +431,7 @@ describe('syncWorkspaceToFolder', () => { }) it('should apply modification changes for elements that exist in both the workspace and the folder', () => { // Note - we currently do not expect the function to filter out changes for elements that are identical - expect(mockAdapter.dumpElementsToFolder).toHaveBeenCalledWith({ + expect(mockAdapter.adapterFormat.dumpElementsToFolder).toHaveBeenCalledWith({ baseDir: 'dir', changes: expect.arrayContaining([ expect.objectContaining(toChange({ before: sameInstanceInFolder, after: sameInstanceInWorkspace })), @@ -275,7 +440,7 @@ describe('syncWorkspaceToFolder', () => { }) }) it('should apply addition changes for elements that exist in the workspace and not the folder', () => { - expect(mockAdapter.dumpElementsToFolder).toHaveBeenCalledWith({ + expect(mockAdapter.adapterFormat.dumpElementsToFolder).toHaveBeenCalledWith({ baseDir: 'dir', changes: expect.arrayContaining([expect.objectContaining(toChange({ after: separateInstanceInWorkspace }))]), elementsSource: expect.anything(), @@ -286,7 +451,7 @@ describe('syncWorkspaceToFolder', () => { describe('when adapter does not support loading from folder', () => { let result: SyncWorkspaceToFolderResult beforeEach(async () => { - delete (mockAdapter as Adapter).loadElementsFromFolder + delete (mockAdapter as Adapter).adapterFormat result = await syncWorkspaceToFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return an error', () => { @@ -298,7 +463,7 @@ describe('syncWorkspaceToFolder', () => { describe('when adapter does not support dumping to folder', () => { let result: SyncWorkspaceToFolderResult beforeEach(async () => { - delete (mockAdapter as Adapter).dumpElementsToFolder + delete (mockAdapter as Adapter).adapterFormat result = await syncWorkspaceToFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return an error', () => { @@ -313,26 +478,26 @@ describe('syncWorkspaceToFolder', () => { let errors: SaltoError[] beforeEach(async () => { errors = [{ severity: 'Error', message: 'something failed', detailedMessage: 'something failed' }] - mockAdapter.loadElementsFromFolder.mockResolvedValue({ elements: [], errors }) + mockAdapter.adapterFormat.loadElementsFromFolder.mockResolvedValue({ elements: [], errors }) result = await syncWorkspaceToFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return the error without trying to update the folder', () => { expect(result.errors).toEqual(errors) - expect(mockAdapter.dumpElementsToFolder).not.toHaveBeenCalled() + expect(mockAdapter.adapterFormat.dumpElementsToFolder).not.toHaveBeenCalled() }) }) describe('when loading elements from folder returns elements that cannot be merged', () => { let result: SyncWorkspaceToFolderResult beforeEach(async () => { - mockAdapter.loadElementsFromFolder.mockResolvedValue({ + mockAdapter.adapterFormat.loadElementsFromFolder.mockResolvedValue({ elements: [separateInstanceInFolder, separateInstanceInFolder], }) result = await syncWorkspaceToFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return error without trying to update the folder', () => { expect(result.errors).not.toHaveLength(0) - expect(mockAdapter.dumpElementsToFolder).not.toHaveBeenCalled() + expect(mockAdapter.adapterFormat.dumpElementsToFolder).not.toHaveBeenCalled() }) }) @@ -347,7 +512,7 @@ describe('syncWorkspaceToFolder', () => { detailedMessage: 'something failed', }, ] - mockAdapter.dumpElementsToFolder.mockResolvedValue({ errors, unappliedChanges: [] }) + mockAdapter.adapterFormat.dumpElementsToFolder.mockResolvedValue({ errors, unappliedChanges: [] }) result = await syncWorkspaceToFolder({ workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return the error in the result', () => { @@ -378,9 +543,11 @@ describe('updateElementFolder', () => { let mockAdapter: ReturnType let workspace: Workspace let changes: ReadonlyArray + beforeEach(() => { mockAdapter = createMockAdapter(mockAdapterName) adapterCreators[mockAdapterName] = mockAdapter + const type = new ObjectType({ elemID: new ElemID(mockAdapterName, 'type'), fields: { @@ -404,7 +571,7 @@ describe('updateElementFolder', () => { await updateElementFolder({ changes, workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should call dumpElementsToFolder with the correct parameters', async () => { - expect(mockAdapter.dumpElementsToFolder).toHaveBeenCalledWith({ + expect(mockAdapter.adapterFormat.dumpElementsToFolder).toHaveBeenCalledWith({ baseDir: 'dir', changes, elementsSource: expect.anything(), @@ -423,7 +590,7 @@ describe('updateElementFolder', () => { detailedMessage: 'something failed', }, ] - mockAdapter.dumpElementsToFolder.mockResolvedValue({ errors, unappliedChanges: [] }) + mockAdapter.adapterFormat.dumpElementsToFolder.mockResolvedValue({ errors, unappliedChanges: [] }) result = await updateElementFolder({ changes, workspace, accountName: mockAdapterName, baseDir: 'dir' }) }) it('should return the error in the result', () => { @@ -433,7 +600,7 @@ describe('updateElementFolder', () => { describe('when used with an account that does not support dumpElementsToFolder', () => { let result: UpdateElementFolderResult it('should return an error', async () => { - delete (mockAdapter as Adapter).dumpElementsToFolder + delete (mockAdapter as Adapter).adapterFormat result = await updateElementFolder({ changes, workspace, accountName: mockAdapterName, baseDir: 'dir' }) expect(result).toEqual({ errors: [ diff --git a/packages/dummy-adapter/src/adapter_creator.ts b/packages/dummy-adapter/src/adapter_creator.ts index 5daba699de8..6df54ba328a 100644 --- a/packages/dummy-adapter/src/adapter_creator.ts +++ b/packages/dummy-adapter/src/adapter_creator.ts @@ -15,6 +15,7 @@ import { GetCustomReferencesFunc, ConfigCreator, createRestriction, + AdapterFormat, } from '@salto-io/adapter-api' import { createDefaultInstanceFromType, inspectValue } from '@salto-io/adapter-utils' import { logger } from '@salto-io/logging' @@ -64,7 +65,7 @@ const getCustomReferences: GetCustomReferencesFunc = async elements => ] : [] -const loadElementsFromFolder: Adapter['loadElementsFromFolder'] = async ({ baseDir }) => ({ +const loadElementsFromFolder: AdapterFormat['loadElementsFromFolder'] = async ({ baseDir }) => ({ elements: await generateExtraElementsFromPaths([baseDir]), }) @@ -209,5 +210,7 @@ export const adapter: Adapter = { configType, getCustomReferences, configCreator, - loadElementsFromFolder, + adapterFormat: { + loadElementsFromFolder, + }, } diff --git a/packages/dummy-adapter/test/adapter_creator.test.ts b/packages/dummy-adapter/test/adapter_creator.test.ts index 9c3b64f2264..e6ee101ec5c 100644 --- a/packages/dummy-adapter/test/adapter_creator.test.ts +++ b/packages/dummy-adapter/test/adapter_creator.test.ts @@ -86,7 +86,7 @@ describe('adapter creator', () => { }, ]), ) - loadedElements = await adapter.loadElementsFromFolder?.({ + loadedElements = await adapter.adapterFormat?.loadElementsFromFolder?.({ baseDir: 'some_path', elementsSource: buildElementsSourceFromElements([]), }) diff --git a/packages/netsuite-adapter/e2e_test/adapter.test.ts b/packages/netsuite-adapter/e2e_test/adapter.test.ts index 943433e04d1..5e3cb335390 100644 --- a/packages/netsuite-adapter/e2e_test/adapter.test.ts +++ b/packages/netsuite-adapter/e2e_test/adapter.test.ts @@ -939,7 +939,7 @@ describe('Netsuite adapter E2E with real account', () => { ]) await createAdditionalFiles(projectPath, [ADDITINAL_NEW_FILE, ADDITINAL_EXISTING_FILE]) logMessage('loading elements from SDF project') - const res = await adapterCreator.loadElementsFromFolder?.({ + const res = await adapterCreator.adapterFormat?.loadElementsFromFolder?.({ baseDir: projectPath, elementsSource: buildElementsSourceFromElements(existingFileCabinetInstances), config: new InstanceElement(ElemID.CONFIG_NAME, configType, { diff --git a/packages/netsuite-adapter/src/adapter_creator.ts b/packages/netsuite-adapter/src/adapter_creator.ts index 11b889be8e6..f6336c99887 100644 --- a/packages/netsuite-adapter/src/adapter_creator.ts +++ b/packages/netsuite-adapter/src/adapter_creator.ts @@ -175,7 +175,9 @@ export const adapter: Adapter = { return { success: false, errors: [err.message ?? err] } } }, - loadElementsFromFolder, + adapterFormat: { + loadElementsFromFolder, + }, getCustomReferences: combineCustomReferenceGetters( _.mapValues(customReferenceHandlers, handler => handler.findWeakReferences), ), diff --git a/packages/salesforce-adapter/package.json b/packages/salesforce-adapter/package.json index a9c064a7aaf..542d66237f4 100644 --- a/packages/salesforce-adapter/package.json +++ b/packages/salesforce-adapter/package.json @@ -33,6 +33,7 @@ "dependencies": { "@salesforce/core": "^8.1.1", "@salesforce/source-deploy-retrieve": "^12.1.4", + "@salesforce/templates": "^61.4.11", "@salto-io/adapter-api": "0.4.3", "@salto-io/adapter-components": "0.4.3", "@salto-io/adapter-utils": "0.4.3", diff --git a/packages/salesforce-adapter/src/adapter_creator.ts b/packages/salesforce-adapter/src/adapter_creator.ts index 17dc5afa216..978ae2a48a9 100644 --- a/packages/salesforce-adapter/src/adapter_creator.ts +++ b/packages/salesforce-adapter/src/adapter_creator.ts @@ -49,6 +49,7 @@ import { ConfigChange } from './config_change' import { configCreator } from './config_creator' import { loadElementsFromFolder } from './sfdx_parser/sfdx_parser' import { dumpElementsToFolder } from './sfdx_parser/sfdx_dump' +import { isProjectFolder, createProject } from './sfdx_parser/project' import { getAdditionalReferences } from './additional_references' import { getCustomReferences } from './custom_references/handlers' import { dependencyChanger } from './dependency_changer' @@ -383,8 +384,12 @@ export const adapter: Adapter = { }, configType, configCreator, - loadElementsFromFolder, - dumpElementsToFolder, + adapterFormat: { + isInitializedFolder: isProjectFolder, + initFolder: createProject, + loadElementsFromFolder, + dumpElementsToFolder, + }, getAdditionalReferences, getCustomReferences, } diff --git a/packages/salesforce-adapter/src/sfdx_parser/project.ts b/packages/salesforce-adapter/src/sfdx_parser/project.ts new file mode 100644 index 00000000000..fb89dd82ae2 --- /dev/null +++ b/packages/salesforce-adapter/src/sfdx_parser/project.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Salto Labs Ltd. + * Licensed under the Salto Terms of Use (the "License"); + * You may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.salto.io/terms-of-use + * + * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES + */ + +import path from 'path' +import { logger } from '@salto-io/logging' +import { AdapterFormat } from '@salto-io/adapter-api' +import { API_VERSION } from '../client/client' +import { SfProject, SfError, TemplateService, TemplateType, ProjectOptions } from './salesforce_imports' + +const log = logger(module) + +type IsInitializedFolderFunc = NonNullable +export const isProjectFolder: IsInitializedFolderFunc = async ({ baseDir }) => { + try { + await SfProject.resolve(baseDir) + return { + result: true, + errors: [], + } + } catch (error) { + if (error instanceof SfError && error.name === 'InvalidProjectWorkspaceError') { + return { + result: false, + errors: [], + } + } + + log.error(error) + return { + result: false, + errors: [ + { + severity: 'Error', + message: 'Failed checking if folder contains an SFDX project', + detailedMessage: error.message, + }, + ], + } + } +} + +type InitFolderFunc = NonNullable +export const createProject: InitFolderFunc = async ({ baseDir }) => { + const templateService = TemplateService.getInstance() + const opts: ProjectOptions = { + projectname: path.basename(baseDir), + outputdir: path.dirname(baseDir), + manifest: false, + loginurl: 'https://login.salesforce.com', + template: 'standard', + ns: '', + defaultpackagedir: 'force-app', + apiversion: API_VERSION, + } + + try { + await templateService.create(TemplateType.Project, opts) + return { errors: [] } + } catch (error) { + log.error(error) + return { + errors: [ + { + severity: 'Error', + message: 'Failed initializing SFDX project', + detailedMessage: error.message, + }, + ], + } + } +} diff --git a/packages/salesforce-adapter/src/sfdx_parser/salesforce_imports.ts b/packages/salesforce-adapter/src/sfdx_parser/salesforce_imports.ts index 02c89074549..98bfba264ed 100644 --- a/packages/salesforce-adapter/src/sfdx_parser/salesforce_imports.ts +++ b/packages/salesforce-adapter/src/sfdx_parser/salesforce_imports.ts @@ -9,7 +9,7 @@ // We have to have this import before any import from a @salesforce package import './salesforce_imports_fix' -export { SfProject } from '@salesforce/core' +export { SfProject, SfError } from '@salesforce/core' export { ComponentSet, ZipTreeContainer, @@ -19,3 +19,4 @@ export { SourceComponent, SourcePath, } from '@salesforce/source-deploy-retrieve' +export { TemplateService, TemplateType, ProjectOptions } from '@salesforce/templates' diff --git a/packages/salesforce-adapter/src/sfdx_parser/sfdx_dump.ts b/packages/salesforce-adapter/src/sfdx_parser/sfdx_dump.ts index 03d813f6a85..c9cef3acf6e 100644 --- a/packages/salesforce-adapter/src/sfdx_parser/sfdx_dump.ts +++ b/packages/salesforce-adapter/src/sfdx_parser/sfdx_dump.ts @@ -9,7 +9,7 @@ import _ from 'lodash' import path from 'path' import { isSubDirectory, rm } from '@salto-io/file' import { logger } from '@salto-io/logging' -import { Adapter, getChangeData, isField, isObjectType } from '@salto-io/adapter-api' +import { AdapterFormat, getChangeData, isField, isObjectType } from '@salto-io/adapter-api' import { resolveChangeElement } from '@salto-io/adapter-components' import { filter } from '@salto-io/adapter-utils' import { collections, objects, promises, values } from '@salto-io/lowerdash' @@ -100,7 +100,7 @@ const compactPathList = (paths: string[]): string[] => { .filter(values.isDefined) } -type DumpElementsToFolderFunc = NonNullable +type DumpElementsToFolderFunc = NonNullable export const dumpElementsToFolder: DumpElementsToFolderFunc = async ({ baseDir, changes, elementsSource }) => { const [customObjectInstanceChanges, metadataAndTypeChanges] = _.partition(changes, isInstanceOfCustomObjectChangeSync) const [metadataChanges, typeChanges] = _.partition(metadataAndTypeChanges, change => { diff --git a/packages/salesforce-adapter/src/sfdx_parser/sfdx_parser.ts b/packages/salesforce-adapter/src/sfdx_parser/sfdx_parser.ts index 81b7faab0b8..0cf381dd5c6 100644 --- a/packages/salesforce-adapter/src/sfdx_parser/sfdx_parser.ts +++ b/packages/salesforce-adapter/src/sfdx_parser/sfdx_parser.ts @@ -12,7 +12,7 @@ import { filter } from '@salto-io/adapter-utils' import type { FileProperties } from '@salto-io/jsforce-types' import { logger } from '@salto-io/logging' import { collections } from '@salto-io/lowerdash' -import { BuiltinTypes, FetchResult, LoadElementsFromFolderArgs } from '@salto-io/adapter-api' +import { AdapterFormat, BuiltinTypes } from '@salto-io/adapter-api' import { fromRetrieveResult, isComplexType, METADATA_XML_SUFFIX } from '../transformers/xml_transformer' import { createInstanceElement, @@ -88,10 +88,8 @@ const getXmlDestination = (component: SourceComponent): string | undefined => { return xmlDestination } -export const loadElementsFromFolder = async ({ - baseDir, - elementsSource, -}: LoadElementsFromFolderArgs): Promise => { +type LoadElementsFromFolderFunc = NonNullable +export const loadElementsFromFolder: LoadElementsFromFolderFunc = async ({ baseDir, elementsSource }) => { try { // Load current SFDX project // SFDX code has some issues when working with relative paths (some custom object files may get the wrong path) diff --git a/packages/salesforce-adapter/test/sfdx_parser/project.test.ts b/packages/salesforce-adapter/test/sfdx_parser/project.test.ts new file mode 100644 index 00000000000..28c8cfed378 --- /dev/null +++ b/packages/salesforce-adapter/test/sfdx_parser/project.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2024 Salto Labs Ltd. + * Licensed under the Salto Terms of Use (the "License"); + * You may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.salto.io/terms-of-use + * + * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES + */ +import fs from 'fs' +import path from 'path' +import { InitFolderResult } from '@salto-io/adapter-api' +import { setupTmpDir } from '@salto-io/test-utils' +import { isProjectFolder, createProject } from '../../src/sfdx_parser/project' +import { setupTmpProject } from './utils' + +describe('isProjectFolder', () => { + describe('when there is a project in the directory', () => { + const project = setupTmpProject() + + it('should return true', async () => { + const result = await isProjectFolder({ baseDir: project.name() }) + expect(result).toEqual({ + result: true, + errors: [], + }) + }) + }) + + describe('when there is no project in the directory', () => { + const project = setupTmpDir('all') + + it('should return false', async () => { + const result = await isProjectFolder({ baseDir: project.name() }) + expect(result).toEqual({ + result: false, + errors: [], + }) + }) + }) +}) + +describe('createProject', () => { + describe('when given a valid directory', () => { + const project = setupTmpDir('all') + let res: InitFolderResult + + beforeEach(async () => { + res = await createProject({ baseDir: project.name() }) + }) + + it('should create a project', () => { + expect(res.errors).toBeEmpty() + expect(fs.existsSync(path.join(project.name(), 'sfdx-project.json'))).toBeTrue() + }) + }) + + describe('when given an invalid directory', () => { + let res: InitFolderResult + + beforeEach(async () => { + res = await createProject({ baseDir: '/dev/null' }) + }) + + it('should return an error', () => { + expect(res.errors).toEqual([ + expect.objectContaining({ + severity: 'Error', + message: 'Failed initializing SFDX project', + }), + ]) + }) + }) +}) diff --git a/packages/salesforce-adapter/test/sfdx_parser/sfdx_dump.test.ts b/packages/salesforce-adapter/test/sfdx_parser/sfdx_dump.test.ts index c0e5102b0b5..72ca58333ba 100644 --- a/packages/salesforce-adapter/test/sfdx_parser/sfdx_dump.test.ts +++ b/packages/salesforce-adapter/test/sfdx_parser/sfdx_dump.test.ts @@ -5,11 +5,9 @@ * * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES */ -import fs from 'fs' import path from 'path' import { collections } from '@salto-io/lowerdash' import { exists, readTextFile } from '@salto-io/file' -import { setupTmpDir } from '@salto-io/test-utils' import { CORE_ANNOTATIONS, DumpElementsResult, @@ -26,16 +24,9 @@ import { mockTypes, mockDefaultValues } from '../mock_elements' import { createInstanceElement, Types } from '../../src/transformers/transformer' import { createCustomObjectType } from '../utils' import { xmlToValues } from '../../src/transformers/xml_transformer' +import { setupTmpProject } from './utils' describe('dumpElementsToFolder', () => { - const setupTmpProject = (): ReturnType => { - const tmpDir = setupTmpDir('all') - beforeAll(async () => { - await fs.promises.cp(path.join(__dirname, 'test_sfdx_project'), tmpDir.name(), { recursive: true }) - }) - return tmpDir - } - const getExistingCustomObject = (): ObjectType => createCustomObjectType('Test__c', { fields: { @@ -76,6 +67,7 @@ describe('dumpElementsToFolder', () => { `) }) }) + describe('when deleting an existing instance', () => { const project = setupTmpProject() let dumpResult: DumpElementsResult @@ -182,6 +174,7 @@ describe('dumpElementsToFolder', () => { }) }) }) + describe('when modifying an existing nested instance', () => { const project = setupTmpProject() let dumpResult: DumpElementsResult diff --git a/packages/salesforce-adapter/test/sfdx_parser/utils.ts b/packages/salesforce-adapter/test/sfdx_parser/utils.ts new file mode 100644 index 00000000000..482ba0f533a --- /dev/null +++ b/packages/salesforce-adapter/test/sfdx_parser/utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Salto Labs Ltd. + * Licensed under the Salto Terms of Use (the "License"); + * You may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.salto.io/terms-of-use + * + * CERTAIN THIRD PARTY SOFTWARE MAY BE CONTAINED IN PORTIONS OF THE SOFTWARE. See NOTICE FILE AT https://github.com/salto-io/salto/blob/main/NOTICES + */ +import fs from 'fs' +import path from 'path' +import { setupTmpDir } from '@salto-io/test-utils' + +export const setupTmpProject = (): ReturnType => { + const tmpDir = setupTmpDir('all') + beforeAll(async () => { + await fs.promises.cp(path.join(__dirname, 'test_sfdx_project'), tmpDir.name(), { recursive: true }) + }) + return tmpDir +} diff --git a/yarn.lock b/yarn.lock index e1a8595f1e4..c5251b733cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3863,6 +3863,15 @@ __metadata: languageName: node linkType: hard +"@salesforce/kit@npm:^3.2.3": + version: 3.2.3 + resolution: "@salesforce/kit@npm:3.2.3" + dependencies: + "@salesforce/ts-types": ^2.0.12 + checksum: 5294020bc9f14bea019bc7f1c823f51f7bb6d9672e407524dd461ba0a9cf751327c2dfba926e02bba8b700d93e5d040d1baf615b9474d650ced805a97e87263f + languageName: node + linkType: hard + "@salesforce/schemas@npm:^1.9.0": version: 1.9.0 resolution: "@salesforce/schemas@npm:1.9.0" @@ -3890,6 +3899,22 @@ __metadata: languageName: node linkType: hard +"@salesforce/templates@npm:^61.4.11": + version: 61.4.12 + resolution: "@salesforce/templates@npm:61.4.12" + dependencies: + "@salesforce/kit": ^3.2.3 + ejs: ^3.1.10 + got: ^11.8.2 + hpagent: ^1.2.0 + mime-types: ^2.1.35 + proxy-from-env: ^1.1.0 + tar: ^6.2.1 + tslib: ^2.6.2 + checksum: f87697ed2c902658da827cd6e219c8f5a48a70d0d5e3073eff48d6867ada36f4b002c71af443dbf118639785af2fa5418b2b046f8fd565bf9d5037d224fa75e2 + languageName: node + linkType: hard + "@salesforce/ts-types@npm:^2.0.10": version: 2.0.10 resolution: "@salesforce/ts-types@npm:2.0.10" @@ -3897,6 +3922,13 @@ __metadata: languageName: node linkType: hard +"@salesforce/ts-types@npm:^2.0.12": + version: 2.0.12 + resolution: "@salesforce/ts-types@npm:2.0.12" + checksum: 36ebe057da8fcb1e1f1cd44932a93898a316edbf4a4fd66807b96e69c0a8106fa8c3f6bb511787ead80566fb328da230787f94399f4742e473c5e0b343a7c2ba + languageName: node + linkType: hard + "@salto-io/adapter-api@0.4.3, @salto-io/adapter-api@workspace:packages/adapter-api": version: 0.0.0-use.local resolution: "@salto-io/adapter-api@workspace:packages/adapter-api" @@ -5210,6 +5242,7 @@ __metadata: "@eslint/js": ^9.6.0 "@salesforce/core": ^8.1.1 "@salesforce/source-deploy-retrieve": ^12.1.4 + "@salesforce/templates": ^61.4.11 "@salto-io/adapter-api": 0.4.3 "@salto-io/adapter-components": 0.4.3 "@salto-io/adapter-utils": 0.4.3 @@ -9971,7 +10004,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.0.0": +"ejs@npm:^3.0.0, ejs@npm:^3.1.10": version: 3.1.10 resolution: "ejs@npm:3.1.10" dependencies: @@ -12385,6 +12418,13 @@ __metadata: languageName: node linkType: hard +"hpagent@npm:^1.2.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: b029da695edae438cee4da2a437386f9db4ac27b3ceb7306d02e1b586c9c194741ed2e943c8a222e0cfefaf27ee3f863aca7ba1721b0950a2a19bf25bc0d85e2 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -15242,7 +15282,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -15477,6 +15517,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + "minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" @@ -19695,6 +19742,20 @@ __metadata: languageName: node linkType: hard +"tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: ^2.0.0 + fs-minipass: ^2.0.0 + minipass: ^5.0.0 + minizlib: ^2.1.1 + mkdirp: ^1.0.3 + yallist: ^4.0.0 + checksum: f1322768c9741a25356c11373bce918483f40fa9a25c69c59410c8a1247632487edef5fe76c5f12ac51a6356d2f1829e96d2bc34098668a2fc34d76050ac2b6c + languageName: node + linkType: hard + "temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -20074,6 +20135,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.2": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + "tunnel-agent@npm:*, tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0"