Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#206): Support switching and creating Collections #268

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions src/main/environment/service/environment-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { VariableMap, VariableObject } from 'shim/objects/variables';
import { PersistenceService } from 'main/persistence/service/persistence-service';
import { randomUUID } from 'node:crypto';
import { getSystemVariables } from './system-variable';
import { SettingsObject, SettingsService } from '../../persistence/service/settings-service';

const environmentService = EnvironmentService.instance;

Expand Down Expand Up @@ -123,4 +124,23 @@ describe('EnvironmentService', () => {
// Assert
expect(result).toEqual(systemVariable);
});

it('should load the configured collection from SettingsService', async () => {
// Arrange
const getSettingsSpy = vi.spyOn(SettingsService.instance, 'settings', 'get');
const collectionPath = '/path/to/collection';
const settings: SettingsObject = { collections: [collectionPath], currentCollectionIndex: 0 };
getSettingsSpy.mockReturnValueOnce(settings);
const loadCollectionSpy = vi
.spyOn(PersistenceService.instance, 'loadCollection')
.mockResolvedValueOnce(collection as Collection);

// Act
await environmentService.init();

// Assert
expect(getSettingsSpy).toHaveBeenCalled();
expect(loadCollectionSpy).toHaveBeenCalledWith(collectionPath);
expect(environmentService.currentCollection).toEqual(collection);
});
});
29 changes: 24 additions & 5 deletions src/main/environment/service/environment-service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Readable } from 'node:stream';
import { normalize } from 'node:path';
import { TemplateReplaceStream } from 'template-replace-stream';
import { Initializable } from 'main/shared/initializable';
import { PersistenceService } from 'main/persistence/service/persistence-service';
import { Collection } from 'shim/objects/collection';
import { VariableMap } from 'shim/objects/variables';
import { getSystemVariable, getSystemVariables } from './system-variable';
import { SettingsService } from 'main/persistence/service/settings-service';

const persistenceService = PersistenceService.instance;
const settingsService = SettingsService.instance;

/**
* The environment service is responsible for managing the current collection and
Expand All @@ -32,8 +35,11 @@ export class EnvironmentService implements Initializable {
* Initializes the environment service by loading the last used collection.
*/
public async init() {
// TODO: load the last used collection from state instead
this.currentCollection = await persistenceService.loadDefaultCollection();
const { settings } = settingsService;
await persistenceService.createDefaultCollectionIfNotExists();

const collectionDir = settings.collections[settings.currentCollectionIndex];
this.currentCollection = await persistenceService.loadCollection(collectionDir);
}

/**
Expand All @@ -55,12 +61,25 @@ export class EnvironmentService implements Initializable {
}

/**
* Changes the current collection to the one at the specified path.
* Loads the collection at the specified path and sets it as the current collection.
*
* @param path The path of the collection to load and set as the current collection.
* @param path The path of the collection
*/
public async changeCollection(path: string) {
return (this.currentCollection = await persistenceService.loadCollection(path));
path = normalize(path);
const settings = settingsService.modifiableSettings;

// open the collection if it is not already open
const collectionIndex = settings.collections.indexOf(path);
if (collectionIndex === -1) {
settings.currentCollectionIndex = settings.collections.push(path) - 1;
} else {
settings.currentCollectionIndex = collectionIndex;
}

this.currentCollection = await persistenceService.loadCollection(path);
await settingsService.setSettings(settings);
return this.currentCollection;
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/event/main-event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,12 @@ export class MainEventService implements IEventService {
async selectEnvironment(key: string) {
environmentService.currentEnvironmentKey = key;
}

async openCollection(dirPath: string) {
return await persistenceService.loadCollection(dirPath);
}

async createCollection(dirPath: string, title: string) {
return await persistenceService.createCollection(dirPath, title);
}
}
4 changes: 3 additions & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EnvironmentService } from 'main/environment/service/environment-service
import 'main/event/main-event-service';
import path from 'node:path';
import quit from 'electron-squirrel-startup';
import { SettingsService } from './persistence/service/settings-service';

declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
declare const MAIN_WINDOW_VITE_NAME: string;
Expand All @@ -13,7 +14,8 @@ if (quit) {
}

const createWindow = async () => {
// initialize services
// initialize services in correct order
await SettingsService.instance.init();
await EnvironmentService.instance.init();

// Create the browser window.
Expand Down
1 change: 1 addition & 0 deletions src/main/persistence/service/default-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function generateDefaultCollection(dirPath: string): Collection {
},
].map((variable) => [variable.key, variable])
),
environments: {},
children: [
{
id: exampleRequestId,
Expand Down
5 changes: 3 additions & 2 deletions src/main/persistence/service/info-files/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export function fromFolderInfoFile(

export function fromRequestInfoFile(
infoFile: RequestInfoFile,
parentId: TrufosRequest['parentId']
parentId: TrufosRequest['parentId'],
draft: boolean
): TrufosRequest {
return Object.assign(infoFile, { type: 'request' as const, parentId });
return Object.assign(infoFile, { type: 'request' as const, parentId, draft });
}
21 changes: 10 additions & 11 deletions src/main/persistence/service/persistence-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,24 @@ describe('PersistenceService', () => {
await mkdir(collection.dirPath, { recursive: true });
});

it('loadDefaultCollection() should return the existing default collection if it exists', async () => {
it('createDefaultCollectionIfNotExists() should not create if it already exists', async () => {
// Arrange
const defaultCollection = {} as Collection;
const loadCollectionSpy = vi
.spyOn(persistenceService, 'loadCollection')
.mockResolvedValueOnce(defaultCollection);
const defaultCollectionImport = await import('./default-collection');
const generateDefaultCollectionSpy = vi.spyOn(
defaultCollectionImport,
'generateDefaultCollection'
);
await mkdir(collectionDirPath);
await writeFile(path.join(collectionDirPath, 'collection.json'), '');

// Act
const result = await persistenceService.loadDefaultCollection();
await persistenceService.createDefaultCollectionIfNotExists();

// Assert
expect(result).toBe(defaultCollection);
expect(loadCollectionSpy).toHaveBeenCalledWith(collectionDirPath);
expect(generateDefaultCollectionSpy).not.toHaveBeenCalled();
});

it('loadDefaultCollection() should create a new default collection if does not exist', async () => {
it('createDefaultCollectionIfNotExists() should create a new default collection if does not exist', async () => {
// Arrange
const defaultCollectionImport = await import('./default-collection');
const defaultCollection = {} as Collection;
Expand All @@ -101,10 +101,9 @@ describe('PersistenceService', () => {
.mockResolvedValueOnce();

// Act
const result = await persistenceService.loadDefaultCollection();
await persistenceService.createDefaultCollectionIfNotExists();

// Assert
expect(result).toBe(defaultCollection);
expect(generateDefaultCollection).toHaveBeenCalledWith(collectionDirPath);
expect(saveCollectionRecursiveSpy).toHaveBeenCalledWith(defaultCollection);
});
Expand Down
50 changes: 35 additions & 15 deletions src/main/persistence/service/persistence-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,33 @@ import {
TEXT_BODY_FILE_NAME,
TextBody,
} from 'shim/objects/request';
import { exists, USER_DATA_DIR } from 'main/util/fs-util';
import { exists } from 'main/util/fs-util';
import { isCollection, isFolder, isRequest, TrufosObject } from 'shim/objects';
import { generateDefaultCollection } from './default-collection';
import { randomUUID } from 'node:crypto';
import { migrateInfoFile } from './info-files/migrators';
import { SemVer } from 'main/util/semver';
import { SettingsService } from './settings-service';

/**
* This service is responsible for persisting and loading collections, folders, and requests
* to and from the file system.
*/
export class PersistenceService {
private static readonly DEFAULT_COLLECTION_DIR = path.join(USER_DATA_DIR, 'default-collection');

public static readonly instance = new PersistenceService();

private readonly idToPathMap: Map<string, string> = new Map();

/**
* Loads the default collection into memory.
* @returns the default collection
* Creates the default collection if it does not exist.
*/
public async loadDefaultCollection() {
const dirPath = PersistenceService.DEFAULT_COLLECTION_DIR;
if (await exists(path.join(dirPath, 'collection.json'))) {
return await this.loadCollection(dirPath);
public async createDefaultCollectionIfNotExists() {
const dirPath = SettingsService.DEFAULT_COLLECTION_DIR;
if (!(await exists(path.join(dirPath, 'collection.json')))) {
console.info('Creating default collection at', dirPath);
const collection = generateDefaultCollection(dirPath);
await this.saveCollectionRecursive(collection);
}

console.info('Creating default collection at', dirPath);
const collection = generateDefaultCollection(dirPath);
await this.saveCollectionRecursive(collection);
return collection;
}

/**
Expand Down Expand Up @@ -283,12 +278,37 @@ export class PersistenceService {
}
}

/**
* Creates a new collection at the specified directory path.
* @param dirPath the directory path where the collection should be created
* @param title the title of the collection
*/
public async createCollection(dirPath: string, title: string): Promise<Collection> {
console.info('Creating new collection at', dirPath);
if ((await fs.readdir(dirPath)).length !== 0) {
throw new Error('Directory is not empty');
}

const collection: Collection = {
id: randomUUID(),
title: title,
type: 'collection',
dirPath,
variables: {},
environments: {},
children: [],
};
await this.saveCollection(collection);
return collection;
}

/**
* Loads a collection and all of its children from the file system.
* @param dirPath the directory path where the collection is located
* @returns the loaded collection
*/
public async loadCollection(dirPath: string): Promise<Collection> {
console.info('Loading collection at', dirPath);
const type = 'collection' as const;
const info = await this.readInfoFile(dirPath, type);
this.idToPathMap.set(info.id, dirPath);
Expand All @@ -303,7 +323,7 @@ export class PersistenceService {
const info = await this.readInfoFile(dirPath, type, draft);
this.idToPathMap.set(info.id, dirPath);

return fromRequestInfoFile(info, parentId);
return fromRequestInfoFile(info, parentId, draft);
}

private async loadFolder(parentId: string, dirPath: string): Promise<Folder> {
Expand Down
51 changes: 51 additions & 0 deletions src/main/persistence/service/settings-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { readFile } from 'node:fs/promises';
import { SettingsService } from './settings-service';
import { exists } from 'main/util/fs-util';

const settingsService = SettingsService.instance;

describe('SettingsService', async () => {
it('should create a new settings if none exists', async () => {
// Assert
expect(await exists(SettingsService.SETTINGS_FILE)).toBe(false);

// Act
await settingsService.init();

// Assert
expect(await exists(SettingsService.SETTINGS_FILE)).toBe(true);
expect(settingsService.settings.currentCollectionIndex).toBe(0);
});

it('should provide a deep clone of settings at modifiedSettings', () => {
// Act
const { modifiableSettings } = settingsService;

// Assert
expect(modifiableSettings).not.toBe(settingsService.settings);
expect(modifiableSettings).toEqual(settingsService.settings);
});

it('should persist settings when set', async () => {
// Arrange
await settingsService.init();
const newSettings: import('./settings-service').SettingsObject = {
currentCollectionIndex: 1,
collections: ['path/to/collection', 'some/where/else'],
};

// Assert
expect(settingsService.settings).not.toEqual(newSettings);
expect(JSON.parse(await readFile(SettingsService.SETTINGS_FILE, 'utf8'))).toEqual(
settingsService.settings
);

// Act
await settingsService.setSettings(newSettings);

// Assert
expect(settingsService.settings).toEqual(newSettings);
expect(JSON.parse(await readFile(SettingsService.SETTINGS_FILE, 'utf8'))).toEqual(newSettings);
});
});
Loading