Skip to content

Commit

Permalink
Merge pull request #90 from richard-lopes-ifood/file-storage
Browse files Browse the repository at this point in the history
File storage: persisting changes over restarts
  • Loading branch information
rhberro authored Aug 9, 2021
2 parents 3249679 + 0bf170d commit 704a99e
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 22 deletions.
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"cz-conventional-changelog": "^3.3.0",
"husky": "^4.2.5",
"jest": "^26.6.3",
"memfs": "^3.2.2",
"mock-fs": "^4.12.0",
"prettier": "2.0.4",
"pretty-quick": "^2.0.1",
Expand Down
2 changes: 2 additions & 0 deletions source/interfaces/ServerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ProxyProperties from './ProxyProperties';
import Throttling from './Throttling';
import MethodOverride from './MethodOverride';
import { RequestHandler } from 'express';
import { FileStorageOptions } from '../storage';

export default interface ServerOptions {
basePath?: string;
Expand All @@ -13,4 +14,5 @@ export default interface ServerOptions {
pagination?: PaginationProperties;
proxies: Array<ProxyProperties>;
throttlings: Array<Throttling>;
fileStorageOptions?: FileStorageOptions;
}
79 changes: 69 additions & 10 deletions source/overrides.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getMockReq, getMockRes } from '@jest-mock/express';
import { mocked } from 'ts-jest/utils';
import { fs as inMemoryFileSystem } from 'memfs';

import { Response, RouteProperties } from './interfaces';

Expand All @@ -12,6 +13,7 @@ import {
import { OverrideManager } from './overrides';
import { RouteManager } from './routes';
import { Middleware } from './types';
import { FileStorage } from './storage';

jest.mock('../source/prompts');

Expand Down Expand Up @@ -222,16 +224,7 @@ describe('source/override.ts', () => {
describe('when selecting a method override', () => {
beforeEach(() => {
routeManager.setAll(routes);

mocked(promptRoutePath).mockImplementation(async () => ({
url: '/dogs',
}));
mocked(promptRouteMethodType).mockImplementation(async () => ({
type: 'GET',
}));
mocked(promptRouteMethodOverride).mockImplementation(async () => ({
name: 'Doggernaut',
}));
mockOverridePrompts('GET', '/dogs', 'Doggernaut');
});

it('prompts and changes a route method override', async () => {
Expand Down Expand Up @@ -357,5 +350,71 @@ describe('source/override.ts', () => {
});
});
});

describe('OverrideManager with FileStorage', () => {
const override = {
routePath: '/dogs',
methodType: 'get',
name: 'Doggernaut',
};

function createOverrideManager(fileStorage: FileStorage<'overrides'>) {
const overrideManager = new OverrideManager(routeManager, fileStorage);
overrideManager.applyExternalOverrides();
}

it('loads overrides from file storage', () => {
const fileStorage = createFileStorage();
fileStorage.setItem('overrides', [override]);
createOverrideManager(fileStorage);
expect(overrideManager.getAllSelected()).toContainEqual(override);
});

it('persists an override to file storage', async () => {
const fileStorage = createFileStorage();
mockOverridePrompts('GET', '/dogs', 'Doggernaut');
createOverrideManager(fileStorage);
await overrideManager.choose();
expect(fileStorage.getItem('overrides')).toContainEqual(override);
});

it('properly clears and apply external (file) overrides', async () => {
const fileStorage = createFileStorage();
fileStorage.setItem('overrides', [override]);
const overrideManager = new OverrideManager(routeManager, fileStorage);
overrideManager.applyExternalOverrides();
expect(fileStorage.getItem('overrides')).toContainEqual(override);

fileStorage.clear();
expect(fileStorage.isEmpty()).toBeTruthy();

overrideManager.applyExternalOverrides();
expect(fileStorage.getItem('overrides')).toContainEqual(override);
});
});
});
});

function mockOverridePrompts(
methodType: string,
routePath: string,
name: string
) {
mocked(promptRoutePath).mockImplementation(async () => ({
url: routePath,
}));
mocked(promptRouteMethodType).mockImplementation(async () => ({
type: methodType,
}));
mocked(promptRouteMethodOverride).mockImplementation(async () => ({
name: name,
}));
}

function createFileStorage() {
return new FileStorage<'overrides'>({
enabled: true,
path: '/.storage',
fs: inMemoryFileSystem as any,
});
}
37 changes: 33 additions & 4 deletions source/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
formatMethodType,
RouteManager,
} from './routes';
import { FileStorage } from './storage';
import { Middleware } from './types';

const OVERRIDE_DEFAULT_OPTION = 'Default';
Expand Down Expand Up @@ -62,16 +63,42 @@ function findSelectedMethodOverride(method: Method) {
return method.overrides?.find(propSatisfies(equals(true), 'selected'));
}

export class OverrideManager {
private routeManager: RouteManager;
const FILE_STORAGE_KEY = 'overrides';

export class OverrideManager {
/**
* Creates a new override manager.
*
* @param routeManager An instance of route manager
*/
constructor(routeManager: RouteManager) {
this.routeManager = routeManager;
constructor(
private routeManager: RouteManager,
private fileStorage?: FileStorage<typeof FILE_STORAGE_KEY>
) {}

applyExternalOverrides() {
if (!this.fileStorage?.options.enabled || !this.fileStorage.isInitialized())
return;

if (this.fileStorage.isEmpty()) {
this.fileStorage?.setItem(FILE_STORAGE_KEY, this.getAllSelected());
} else {
const persistedOverrides = this.fileStorage.getItem<Override[]>(
FILE_STORAGE_KEY
);

persistedOverrides?.forEach((override) => {
const overridableRoutes = this.getAll();
const url = override.routePath;
const route = findRouteByUrl(overridableRoutes, url);
const type = override.methodType;
const overrides = getMethodOverridesByType(route, type.toLowerCase());
const name = override.name;
overrides.forEach((override) => {
override.selected = override.name === name;
});
});
}
}

/**
Expand Down Expand Up @@ -128,6 +155,8 @@ export class OverrideManager {
override.selected = override.name === name;
});

this.fileStorage?.setItem('overrides', this.getAllSelected());

return { routePath: url, methodType: type, name };
}

Expand Down
18 changes: 16 additions & 2 deletions source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ThrottlingManager } from './throttling';
import { UIManager } from './ui';
import { GraphQLManager } from './graphql';
import { join } from 'path';
import { FileStorage } from './storage';

export function createServer(options = {} as ServerOptions): Server {
const {
Expand All @@ -29,16 +30,19 @@ export function createServer(options = {} as ServerOptions): Server {
overrides,
proxies,
throttlings,
fileStorageOptions,
} = options;

const fileStorage = new FileStorage(fileStorageOptions);
const routeManager = new RouteManager(overrides);
const overrideManager = new OverrideManager(routeManager);
const overrideManager = new OverrideManager(routeManager, fileStorage);
const proxyManager = new ProxyManager(routeManager, proxies, basePath);
const throttlingManager = new ThrottlingManager(throttlings);
const uiManager = new UIManager(
proxyManager,
throttlingManager,
overrideManager
overrideManager,
fileStorage
);

const expressServer: express.Application = express();
Expand Down Expand Up @@ -121,6 +125,7 @@ export function createServer(options = {} as ServerOptions): Server {
routeManager.setAll(routes);
routeManager.addDocsRoute(basePath, docsRoute);
routeManager.getAll().forEach(createRoute);
overrideManager.applyExternalOverrides();
},

/**
Expand Down Expand Up @@ -162,11 +167,20 @@ export function createServer(options = {} as ServerOptions): Server {
);
}

async function onResetFileStorage() {
fileStorage.clear();
uiManager.drawDashboard();
}

inputManager.addListener('c', onConnection);
inputManager.addListener('t', onThrottling);
inputManager.addListener('o', onOverride);
inputManager.addListener('p', onRouteProxy);

if (fileStorage.options.enabled) {
inputManager.addListener('x', onResetFileStorage);
}

expressServer.listen(port);
},
};
Expand Down
63 changes: 63 additions & 0 deletions source/storage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FileStorage } from './storage';
import { fs as inMemoryFileSystem, vol } from 'memfs';

describe('FileStorage', () => {
beforeEach(() => {
vol.reset();
});

it('throws an error when file is invalid', () => {
inMemoryFileSystem.writeFileSync('/.storage', 'abc');
createFileStorage();
expect(() => {
createFileStorage().getItem('foo');
}).toThrow('Invalid file storage content');
});

it('throws an error when path is missing', () => {
expect(() => {
new FileStorage({ enabled: true });
}).toThrow('FileStorage path option is missing');
});

it('does not initializes store if disabled', () => {
expect(() => {
new FileStorage({ enabled: false, path: '/.storage' });
inMemoryFileSystem.readFileSync('/.storage', 'utf-8');
}).toThrowError("ENOENT: no such file or directory, open '/.storage'");
});

it('properly creates an empty storage when the storage file does not exist', () => {
createFileStorage();
expect(inMemoryFileSystem.readFileSync('/.storage', 'utf-8')).toEqual('{}');
});

it('properly creates a storage with the content of a existing file', () => {
inMemoryFileSystem.writeFileSync(
'/.storage',
'{ "foo": { "key": "value" } }'
);
createFileStorage();
expect(createFileStorage().getItem('foo')).toEqual({ key: 'value' });
});

it('sets and gets item from storage', () => {
expect(() => {
createFileStorage().setItem('foo', { key: 'value' });
}).not.toThrow();

expect(createFileStorage().getItem('foo')).toEqual({ key: 'value' });
});

it('gets an unexisting item from storage', () => {
expect(createFileStorage().getItem('foo')).toEqual(undefined);
});
});

function createFileStorage() {
return new FileStorage<'foo'>({
enabled: true,
path: '/.storage',
fs: inMemoryFileSystem as any,
});
}
Loading

0 comments on commit 704a99e

Please sign in to comment.