Skip to content

Commit 704a99e

Browse files
authored
Merge pull request #90 from richard-lopes-ifood/file-storage
File storage: persisting changes over restarts
2 parents 3249679 + 0bf170d commit 704a99e

9 files changed

Lines changed: 313 additions & 22 deletions

File tree

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"cz-conventional-changelog": "^3.3.0",
7070
"husky": "^4.2.5",
7171
"jest": "^26.6.3",
72+
"memfs": "^3.2.2",
7273
"mock-fs": "^4.12.0",
7374
"prettier": "2.0.4",
7475
"pretty-quick": "^2.0.1",

source/interfaces/ServerOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ProxyProperties from './ProxyProperties';
33
import Throttling from './Throttling';
44
import MethodOverride from './MethodOverride';
55
import { RequestHandler } from 'express';
6+
import { FileStorageOptions } from '../storage';
67

78
export default interface ServerOptions {
89
basePath?: string;
@@ -13,4 +14,5 @@ export default interface ServerOptions {
1314
pagination?: PaginationProperties;
1415
proxies: Array<ProxyProperties>;
1516
throttlings: Array<Throttling>;
17+
fileStorageOptions?: FileStorageOptions;
1618
}

source/overrides.spec.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getMockReq, getMockRes } from '@jest-mock/express';
22
import { mocked } from 'ts-jest/utils';
3+
import { fs as inMemoryFileSystem } from 'memfs';
34

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

@@ -12,6 +13,7 @@ import {
1213
import { OverrideManager } from './overrides';
1314
import { RouteManager } from './routes';
1415
import { Middleware } from './types';
16+
import { FileStorage } from './storage';
1517

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

@@ -222,16 +224,7 @@ describe('source/override.ts', () => {
222224
describe('when selecting a method override', () => {
223225
beforeEach(() => {
224226
routeManager.setAll(routes);
225-
226-
mocked(promptRoutePath).mockImplementation(async () => ({
227-
url: '/dogs',
228-
}));
229-
mocked(promptRouteMethodType).mockImplementation(async () => ({
230-
type: 'GET',
231-
}));
232-
mocked(promptRouteMethodOverride).mockImplementation(async () => ({
233-
name: 'Doggernaut',
234-
}));
227+
mockOverridePrompts('GET', '/dogs', 'Doggernaut');
235228
});
236229

237230
it('prompts and changes a route method override', async () => {
@@ -357,5 +350,71 @@ describe('source/override.ts', () => {
357350
});
358351
});
359352
});
353+
354+
describe('OverrideManager with FileStorage', () => {
355+
const override = {
356+
routePath: '/dogs',
357+
methodType: 'get',
358+
name: 'Doggernaut',
359+
};
360+
361+
function createOverrideManager(fileStorage: FileStorage<'overrides'>) {
362+
const overrideManager = new OverrideManager(routeManager, fileStorage);
363+
overrideManager.applyExternalOverrides();
364+
}
365+
366+
it('loads overrides from file storage', () => {
367+
const fileStorage = createFileStorage();
368+
fileStorage.setItem('overrides', [override]);
369+
createOverrideManager(fileStorage);
370+
expect(overrideManager.getAllSelected()).toContainEqual(override);
371+
});
372+
373+
it('persists an override to file storage', async () => {
374+
const fileStorage = createFileStorage();
375+
mockOverridePrompts('GET', '/dogs', 'Doggernaut');
376+
createOverrideManager(fileStorage);
377+
await overrideManager.choose();
378+
expect(fileStorage.getItem('overrides')).toContainEqual(override);
379+
});
380+
381+
it('properly clears and apply external (file) overrides', async () => {
382+
const fileStorage = createFileStorage();
383+
fileStorage.setItem('overrides', [override]);
384+
const overrideManager = new OverrideManager(routeManager, fileStorage);
385+
overrideManager.applyExternalOverrides();
386+
expect(fileStorage.getItem('overrides')).toContainEqual(override);
387+
388+
fileStorage.clear();
389+
expect(fileStorage.isEmpty()).toBeTruthy();
390+
391+
overrideManager.applyExternalOverrides();
392+
expect(fileStorage.getItem('overrides')).toContainEqual(override);
393+
});
394+
});
360395
});
361396
});
397+
398+
function mockOverridePrompts(
399+
methodType: string,
400+
routePath: string,
401+
name: string
402+
) {
403+
mocked(promptRoutePath).mockImplementation(async () => ({
404+
url: routePath,
405+
}));
406+
mocked(promptRouteMethodType).mockImplementation(async () => ({
407+
type: methodType,
408+
}));
409+
mocked(promptRouteMethodOverride).mockImplementation(async () => ({
410+
name: name,
411+
}));
412+
}
413+
414+
function createFileStorage() {
415+
return new FileStorage<'overrides'>({
416+
enabled: true,
417+
path: '/.storage',
418+
fs: inMemoryFileSystem as any,
419+
});
420+
}

source/overrides.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
formatMethodType,
2323
RouteManager,
2424
} from './routes';
25+
import { FileStorage } from './storage';
2526
import { Middleware } from './types';
2627

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

65-
export class OverrideManager {
66-
private routeManager: RouteManager;
66+
const FILE_STORAGE_KEY = 'overrides';
6767

68+
export class OverrideManager {
6869
/**
6970
* Creates a new override manager.
7071
*
7172
* @param routeManager An instance of route manager
7273
*/
73-
constructor(routeManager: RouteManager) {
74-
this.routeManager = routeManager;
74+
constructor(
75+
private routeManager: RouteManager,
76+
private fileStorage?: FileStorage<typeof FILE_STORAGE_KEY>
77+
) {}
78+
79+
applyExternalOverrides() {
80+
if (!this.fileStorage?.options.enabled || !this.fileStorage.isInitialized())
81+
return;
82+
83+
if (this.fileStorage.isEmpty()) {
84+
this.fileStorage?.setItem(FILE_STORAGE_KEY, this.getAllSelected());
85+
} else {
86+
const persistedOverrides = this.fileStorage.getItem<Override[]>(
87+
FILE_STORAGE_KEY
88+
);
89+
90+
persistedOverrides?.forEach((override) => {
91+
const overridableRoutes = this.getAll();
92+
const url = override.routePath;
93+
const route = findRouteByUrl(overridableRoutes, url);
94+
const type = override.methodType;
95+
const overrides = getMethodOverridesByType(route, type.toLowerCase());
96+
const name = override.name;
97+
overrides.forEach((override) => {
98+
override.selected = override.name === name;
99+
});
100+
});
101+
}
75102
}
76103

77104
/**
@@ -128,6 +155,8 @@ export class OverrideManager {
128155
override.selected = override.name === name;
129156
});
130157

158+
this.fileStorage?.setItem('overrides', this.getAllSelected());
159+
131160
return { routePath: url, methodType: type, name };
132161
}
133162

source/server.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ThrottlingManager } from './throttling';
1919
import { UIManager } from './ui';
2020
import { GraphQLManager } from './graphql';
2121
import { join } from 'path';
22+
import { FileStorage } from './storage';
2223

2324
export function createServer(options = {} as ServerOptions): Server {
2425
const {
@@ -29,16 +30,19 @@ export function createServer(options = {} as ServerOptions): Server {
2930
overrides,
3031
proxies,
3132
throttlings,
33+
fileStorageOptions,
3234
} = options;
3335

36+
const fileStorage = new FileStorage(fileStorageOptions);
3437
const routeManager = new RouteManager(overrides);
35-
const overrideManager = new OverrideManager(routeManager);
38+
const overrideManager = new OverrideManager(routeManager, fileStorage);
3639
const proxyManager = new ProxyManager(routeManager, proxies, basePath);
3740
const throttlingManager = new ThrottlingManager(throttlings);
3841
const uiManager = new UIManager(
3942
proxyManager,
4043
throttlingManager,
41-
overrideManager
44+
overrideManager,
45+
fileStorage
4246
);
4347

4448
const expressServer: express.Application = express();
@@ -121,6 +125,7 @@ export function createServer(options = {} as ServerOptions): Server {
121125
routeManager.setAll(routes);
122126
routeManager.addDocsRoute(basePath, docsRoute);
123127
routeManager.getAll().forEach(createRoute);
128+
overrideManager.applyExternalOverrides();
124129
},
125130

126131
/**
@@ -162,11 +167,20 @@ export function createServer(options = {} as ServerOptions): Server {
162167
);
163168
}
164169

170+
async function onResetFileStorage() {
171+
fileStorage.clear();
172+
uiManager.drawDashboard();
173+
}
174+
165175
inputManager.addListener('c', onConnection);
166176
inputManager.addListener('t', onThrottling);
167177
inputManager.addListener('o', onOverride);
168178
inputManager.addListener('p', onRouteProxy);
169179

180+
if (fileStorage.options.enabled) {
181+
inputManager.addListener('x', onResetFileStorage);
182+
}
183+
170184
expressServer.listen(port);
171185
},
172186
};

source/storage.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { FileStorage } from './storage';
2+
import { fs as inMemoryFileSystem, vol } from 'memfs';
3+
4+
describe('FileStorage', () => {
5+
beforeEach(() => {
6+
vol.reset();
7+
});
8+
9+
it('throws an error when file is invalid', () => {
10+
inMemoryFileSystem.writeFileSync('/.storage', 'abc');
11+
createFileStorage();
12+
expect(() => {
13+
createFileStorage().getItem('foo');
14+
}).toThrow('Invalid file storage content');
15+
});
16+
17+
it('throws an error when path is missing', () => {
18+
expect(() => {
19+
new FileStorage({ enabled: true });
20+
}).toThrow('FileStorage path option is missing');
21+
});
22+
23+
it('does not initializes store if disabled', () => {
24+
expect(() => {
25+
new FileStorage({ enabled: false, path: '/.storage' });
26+
inMemoryFileSystem.readFileSync('/.storage', 'utf-8');
27+
}).toThrowError("ENOENT: no such file or directory, open '/.storage'");
28+
});
29+
30+
it('properly creates an empty storage when the storage file does not exist', () => {
31+
createFileStorage();
32+
expect(inMemoryFileSystem.readFileSync('/.storage', 'utf-8')).toEqual('{}');
33+
});
34+
35+
it('properly creates a storage with the content of a existing file', () => {
36+
inMemoryFileSystem.writeFileSync(
37+
'/.storage',
38+
'{ "foo": { "key": "value" } }'
39+
);
40+
createFileStorage();
41+
expect(createFileStorage().getItem('foo')).toEqual({ key: 'value' });
42+
});
43+
44+
it('sets and gets item from storage', () => {
45+
expect(() => {
46+
createFileStorage().setItem('foo', { key: 'value' });
47+
}).not.toThrow();
48+
49+
expect(createFileStorage().getItem('foo')).toEqual({ key: 'value' });
50+
});
51+
52+
it('gets an unexisting item from storage', () => {
53+
expect(createFileStorage().getItem('foo')).toEqual(undefined);
54+
});
55+
});
56+
57+
function createFileStorage() {
58+
return new FileStorage<'foo'>({
59+
enabled: true,
60+
path: '/.storage',
61+
fs: inMemoryFileSystem as any,
62+
});
63+
}

0 commit comments

Comments
 (0)