Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/weak-pets-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/store': minor
'@sap-ux/odata-service-inquirer': patch
---

migrate backend system file from .fioritools to .sapdevelopmenttools
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@sap-ux/btp-utils';
import { ERROR_TYPE } from '@sap-ux/inquirer-common';
import type { OdataVersion } from '@sap-ux/odata-service-writer';
import { type BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store';
import { type BackendSystemKey, type BackendSystem } from '@sap-ux/store';
import type { ListChoiceOptions } from 'inquirer';
import { t } from '../../../../i18n';
import type { ConnectedSystem, DestinationFilters } from '../../../../types';
Expand All @@ -19,6 +19,7 @@ import type { ConnectionValidator } from '../../../connectionValidator';
import LoggerHelper from '../../../logger-helper';
import type { ValidationResult } from '../../../types';
import { getBackendSystemDisplayName } from '@sap-ux/fiori-generator-shared';
import { getBackendSystemService } from '../../../../utils/store';

// New system choice value is a hard to guess string to avoid conflicts with existing system names or user named systems
// since it will be used as a new system value in the system selection prompt.
Expand Down Expand Up @@ -51,7 +52,9 @@ export async function connectWithBackendSystem(
// Create a new connection with the selected system
PromptState.resetConnectedSystem();
let connectValResult: ValidationResult = false;
const backendSystem = await new SystemService(LoggerHelper.logger).read(backendKey);

const backendService = await getBackendSystemService();
const backendSystem = await backendService.read(backendKey);

if (backendSystem) {
// Backend systems validation supports using a cached service provider to prevent re-authentication (e.g. re-opening a browser window)
Expand Down Expand Up @@ -238,7 +241,8 @@ export async function createSystemChoices(
};
}
} else {
const backendSystems = await new SystemService(LoggerHelper.logger).getAll({ includeSensitiveData: false });
const backendService = await getBackendSystemService();
const backendSystems = await backendService.getAll({ includeSensitiveData: false });
// Cache the backend systems
PromptState.backendSystemsCache = backendSystems;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { t } from '../../../i18n';
import { SystemService } from '@sap-ux/store';
import LoggerHelper from '../../logger-helper';
import type { ServiceInfo } from '@sap-ux/btp-utils';
import { readFileSync } from 'node:fs';
import { getBackendSystemService } from '../../../utils/store';

/**
* Check if the system name is already in use.
Expand All @@ -11,7 +10,10 @@ import { readFileSync } from 'node:fs';
* @returns true if the system name is already in use, otherwise false
*/
async function isSystemNameInUse(systemName: string): Promise<boolean> {
const backendSystems = await new SystemService(LoggerHelper.logger).getAll({ includeSensitiveData: false });
const backendService = await getBackendSystemService();
const backendSystems = await backendService.getAll({
includeSensitiveData: false
});
return !!backendSystems.find((system) => system.name === systemName);
}

Expand Down
13 changes: 13 additions & 0 deletions packages/odata-service-inquirer/src/utils/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getService, type BackendSystem, type BackendSystemKey, type Service } from '@sap-ux/store';

/**
* Get the backend system service instance.
*
* @returns the backend system service instance
*/
export async function getBackendSystemService(): Promise<Service<BackendSystem, BackendSystemKey>> {
const backendService = await getService<BackendSystem, BackendSystemKey>({
entityName: 'system'
});
return backendService;
}
8 changes: 6 additions & 2 deletions packages/odata-service-inquirer/test/unit/index-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as prompts from '../../src/prompts';
import * as systemSelection from '../../src/prompts/datasources/sap-system/system-selection';
import LoggerHelper from '../../src/prompts/logger-helper';
import { PromptState } from '../../src/utils';
import { Service } from '../../../fiori-mcp-server/src/tools/functionalities/page/service';

jest.mock('../../src/prompts', () => ({
__esModule: true, // Workaround for spyOn TypeError: Jest cannot redefine property
Expand All @@ -21,7 +22,7 @@ jest.mock('../../src/prompts/datasources/sap-system/system-selection', () => ({
jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValue([
{
name: 'storedSystem1',
Expand All @@ -42,6 +43,10 @@ describe('API tests', () => {
jest.restoreAllMocks();
});

afterEach(() => {
jest.clearAllMocks();
});

test('getPrompts', async () => {
jest.spyOn(prompts, 'getQuestions').mockResolvedValue([
{
Expand Down Expand Up @@ -102,7 +107,6 @@ describe('API tests', () => {

test('getPrompts, i18n is loaded', async () => {
const { prompts: questions } = await getPrompts(undefined, undefined, true, undefined, true);

expect(questions).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { getQuestions } from '../../../src/prompts';
import { DatasourceType } from '../../../src/types';
import * as utils from '../../../src/utils';
import { hostEnvironment } from '@sap-ux/fiori-generator-shared';
import { isFeatureEnabled } from '@sap-ux/feature-toggle';

/**
* Workaround to for spyOn TypeError: Jest cannot redefine property
Expand All @@ -19,7 +18,7 @@ jest.mock('@sap-ux/btp-utils', () => {
jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValue([
{
name: 'storedSystem1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ConnectionValidator } from '../../../../../src/prompts/connectionValida
jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValue([{ name: 'http://abap.on.prem:1234' }])
}))
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { suggestSystemName } from '../../../../src/prompts/datasources/sap-syste
jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValue([{ name: 'system1' }, { name: 'system2' }, { name: 'system2 (1)' }])
}))
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
findDefaultSystemSelectionIndex,
NewSystemChoice
} from '../../../../../src/prompts/datasources/sap-system/system-selection/prompt-helpers';
import type { AuthenticationType, BackendSystem } from '@sap-ux/store';
import type { BackendSystem } from '@sap-ux/store';
import type { Destination, Destinations } from '@sap-ux/btp-utils';
import type { AxiosError } from '@sap-ux/axios-extension';

Expand Down Expand Up @@ -42,7 +42,7 @@ jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
// Mock store access
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValueOnce(backendSystems),
partialUpdate: jest.fn().mockImplementation((system: BackendSystem) => {
return Promise.resolve(system);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import LoggerHelper from '../../../../../src/prompts/logger-helper';
import type { ConnectedSystem } from '../../../../../src/types';
import { promptNames } from '../../../../../src/types';
import { getPromptHostEnvironment, PromptState } from '../../../../../src/utils';
import { isFeatureEnabled } from '@sap-ux/feature-toggle';

jest.mock('../../../../../src/utils', () => ({
...jest.requireActual('../../../../../src/utils'),
Expand Down Expand Up @@ -74,7 +73,7 @@ jest.mock('@sap-ux/store', () => ({
__esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property
...jest.requireActual('@sap-ux/store'),
// Mock store access
SystemService: jest.fn().mockImplementation(() => systemServiceMock)
getService: jest.fn().mockImplementation(() => systemServiceMock)
}));

jest.mock('@sap-ux/btp-utils', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { BackendSystem } from '@sap-ux/store';
import type { ServiceInfo } from '@sap-ux/btp-utils';

jest.mock('@sap-ux/store', () => ({
SystemService: jest.fn().mockImplementation(() => ({
getService: jest.fn().mockImplementation(() => ({
getAll: jest.fn().mockResolvedValue([{ name: 'new system' } as BackendSystem])
}))
}));
Expand Down
17 changes: 1 addition & 16 deletions packages/store/src/data-access/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import path from 'node:path';
import type { FSWatcher } from 'node:fs';
import fs, { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { plural } from 'pluralize';
import type { DataAccess } from '.';
import type { Logger } from '@sap-ux/logger';
import { errorInstance, getFioriToolsDirectory } from '../utils';
import { errorInstance, getEntityFileName, getFioriToolsDirectory, toPersistenceName } from '../utils';
import type { ServiceOptions } from '../types';
import os from 'node:os';
import type { Entity } from '../constants';
Expand Down Expand Up @@ -220,20 +219,6 @@ class FilesystemStore<E extends object> implements DataAccess<E> {
}
}

/**
* Trims, lowercases and returns plural if a non-empty string
*
* @param s
*/
function toPersistenceName(s: string): string | undefined {
const t = s?.trim().toLowerCase();
return t && plural(t);
}

function getEntityFileName(entityName: string): string {
return toPersistenceName(entityName) + '.json';
}

/** Return an FSWatcher for a given entity name
* The client is responsible for disposing of the FSWatcher
*/
Expand Down
37 changes: 36 additions & 1 deletion packages/store/src/services/backend-system.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type { Logger } from '@sap-ux/logger';
import type { Service, ServiceRetrievalOptions } from '.';
import type { DataProvider } from '../data-provider';
import type { ServiceOptions } from '../types';
import { SystemDataProvider } from '../data-provider/backend-system';
import { BackendSystem, BackendSystemKey } from '../entities/backend-system';
import { text } from '../i18n';
import type { ServiceOptions } from '../types';
import { existsSync, copyFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { getFioriToolsDirectory, getSapDevToolsDirectory, getEntityFileName } from '../utils';
import { Entity } from '../constants';

/**
* Should not be used directly, use factory method `getService` instead.
* Data integrity cannot be guaranteed when using this class directly.
*/
export class SystemService implements Service<BackendSystem, BackendSystemKey> {
private readonly dataProvider: DataProvider<BackendSystem, BackendSystemKey>;
private readonly logger: Logger;
Expand All @@ -14,6 +22,7 @@ export class SystemService implements Service<BackendSystem, BackendSystemKey> {
this.logger = logger;
this.dataProvider = new SystemDataProvider(this.logger, options);
}

public async partialUpdate(
key: BackendSystemKey,
entity: Partial<BackendSystem>
Expand Down Expand Up @@ -81,5 +90,31 @@ export class SystemService implements Service<BackendSystem, BackendSystemKey> {
}

export function getInstance(logger: Logger, options: ServiceOptions = {}): SystemService {
if (!options.baseDirectory) {
ensureSettingsMigrated();
options.baseDirectory = getSapDevToolsDirectory();
}
return new SystemService(logger, options);
}

/**
* Ensure settings are migrated from the old fiori tools directory to the new sap development tools directory.
*/
function ensureSettingsMigrated(): void {
const sapDevToolsDir = getSapDevToolsDirectory();
const migrationFlag = join(sapDevToolsDir, '.migrated');

if (existsSync(migrationFlag)) {
return;
}

const systemFileName = getEntityFileName(Entity.BackendSystem);
const legacyPath = join(getFioriToolsDirectory(), systemFileName);
const newPath = join(sapDevToolsDir, systemFileName);

if (existsSync(legacyPath)) {
mkdirSync(dirname(newPath), { recursive: true });
copyFileSync(legacyPath, newPath);
writeFileSync(migrationFlag, new Date().toISOString());
}
}
23 changes: 23 additions & 0 deletions packages/store/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { homedir } from 'node:os';
import path from 'node:path';
import { plural } from 'pluralize';

/** Pick the properties listed and return a new object with a shallow-copy */
export const pick = <T>(target: T, ...props: Array<keyof T>): Partial<T> | undefined => {
Expand Down Expand Up @@ -34,9 +35,31 @@ export enum FioriToolsSettings {
dir = '.fioritools'
}

export enum SapDevTools {
dir = '.sapdevelopmenttools'
}

export const getFioriToolsDirectory = (): string => {
return path.join(homedir(), FioriToolsSettings.dir);
};

export const getSapDevToolsDirectory = (): string => {
return path.join(homedir(), SapDevTools.dir);
};

/**
* Trims, lowercases and returns plural if a non-empty string
*
* @param s
*/
export function toPersistenceName(s: string): string | undefined {
const t = s?.trim().toLowerCase();
return t && plural(t);
}

export function getEntityFileName(entityName: string): string {
return toPersistenceName(entityName) + '.json';
}

export * from './app-studio';
export * from './backend';
39 changes: 38 additions & 1 deletion packages/store/test/unit/services/backend-system.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
import { SystemService } from '../../../src/services/backend-system';
import { getInstance, SystemService } from '../../../src/services/backend-system';
import { BackendSystem, BackendSystemKey } from '../../../src';
import { SystemDataProvider } from '../../../src/data-provider/backend-system';
import { initI18n, text } from '../../../src/i18n';
import { ToolsLogger, NullTransport } from '@sap-ux/logger';
import * as nodeFs from 'node:fs';

jest.mock('../../../src/data-provider/backend-system');

jest.mock('node:fs', () => {
const originalFs = jest.requireActual('node:fs');
return {
...originalFs,
existsSync: jest.fn().mockReturnValue(false),
copyFileSync: jest.fn(),
writeFileSync: jest.fn(),
mkdirSync: jest.fn()
};
});

describe('BackendSystem service', () => {
beforeAll(async () => {
await initI18n();
});

const logger = new ToolsLogger({ transports: [new NullTransport()] });

describe('getInstance', () => {
it('creates an instance of SystemService', () => {
const service = getInstance(logger, { baseDirectory: 'some_directory' });
expect(service).toBeInstanceOf(SystemService);
});

it('should check and return for already existing .migrated file', () => {
const existsSyncSpy = jest.spyOn(nodeFs, 'existsSync').mockReturnValue(true);
const service = getInstance(logger);
expect(service).toBeInstanceOf(SystemService);
expect(existsSyncSpy).toHaveBeenCalledWith(expect.stringContaining('.migrated'));
});

it('should create .migrated file after migration', () => {
const existsSyncSpy = jest.spyOn(nodeFs, 'existsSync').mockReturnValueOnce(false).mockReturnValueOnce(true);
const writeFileSyncSpy = jest.spyOn(nodeFs, 'writeFileSync').mockImplementation(() => {});
getInstance(logger);
expect(existsSyncSpy).toHaveBeenCalledWith(expect.stringContaining('.migrated'));
expect(writeFileSyncSpy).toHaveBeenCalledWith(
expect.stringContaining('.migrated'),
expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
);
});
});

describe('delete', () => {
it('delegates to data provider', async () => {
const mockSystemDataProvider = jest.spyOn(SystemDataProvider.prototype, 'delete');
Expand Down
Loading