Skip to content

Commit f681340

Browse files
Fix: Change local secure storage implementation
- Change usage of lowdb to using native nodejs capabilities - Adjust tests - Adjust usage in secure-storage.local.ts ##monday.com item id https://monday.monday.com/boards/3670992828/views/83352826/pulses/3875667859
1 parent 890894c commit f681340

File tree

9 files changed

+1464
-686
lines changed

9 files changed

+1464
-686
lines changed

jest.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function makeModuleNameMapper(srcPath, tsconfigPath) {
99
// Iterate over paths and convert them into moduleNameMapper format
1010
Object.keys(paths).forEach(item => {
1111
const key = `^${item.replace('/*', '/(.*)')}$`;
12-
const path = paths[item][0].replace('./lib/', '').replace('/*', '/$1');
12+
const path = paths[item][0].replace('./lib/', '').replace('*', '$1');
1313
aliases[key] = srcPath + '/' + path;
1414
});
1515
return aliases;
@@ -22,4 +22,10 @@ module.exports = {
2222
moduleNameMapper: makeModuleNameMapper(SRC_PATH, TS_CONFIG_PATH),
2323
preset: 'ts-jest',
2424
testEnvironment: 'node',
25+
clearMocks: true,
26+
globals: {
27+
'ts-jest': {
28+
tsconfig: '<rootDir>/tsconfig.general.json',
29+
},
30+
},
2531
};

lib/lowdb.d.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */
2-
31
import { BadRequestError, NotFoundError } from 'errors/apps-sdk-error';
42
import { isDefined } from 'types/guards';
53
import { ISecureStorageInstance } from 'types/secure-storage';
4+
import { ILocalStorageInstance } from 'types/secure-storage.local';
65
import { decrypt, encrypt } from 'utils/cipher';
76
import { initDb } from 'utils/local-db';
87

@@ -13,31 +12,25 @@ const validateKey = (key: string) => {
1312
};
1413

1514
export class LocalSecureStorage implements ISecureStorageInstance {
16-
private db;
15+
private db: ILocalStorageInstance;
1716

18-
private async init() {
19-
if (!isDefined(this.db)) {
20-
this.db = await initDb('local-secure-storage.db');
21-
}
17+
constructor() {
18+
this.db = initDb('local-secure-storage.db');
2219
}
2320

2421
async delete(key: string) {
2522
validateKey(key);
26-
await this.init();
27-
delete this.db.data[key];
28-
await this.db.write();
23+
await this.db.delete(key);
2924
return true;
3025
}
3126

3227
async get<T>(key: string) {
3328
validateKey(key);
34-
await this.init();
35-
const encryptedValue = this.db.data[key] as string;
29+
const encryptedValue = await this.db.get<string>(key);
3630
if (!isDefined(encryptedValue)) {
3731
throw new NotFoundError(`No data found for ${key}`);
3832
}
3933

40-
console.log(encryptedValue);
4134
const stringifiedValue = decrypt(encryptedValue);
4235
return JSON.parse(stringifiedValue) as T;
4336
}
@@ -48,11 +41,9 @@ export class LocalSecureStorage implements ISecureStorageInstance {
4841
}
4942

5043
validateKey(key);
51-
await this.init();
5244
const stringifiedValue = JSON.stringify(value);
5345
const encryptedValue = encrypt(stringifiedValue);
54-
this.db.data[key] = encryptedValue;
55-
this.db.write();
46+
await this.db.set(key, encryptedValue);
5647
return true;
5748
}
5849
}

lib/types/secure-storage.local.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type ILocalStorageInstance = {
2+
set: <T>(key: string, value: T) => Promise<boolean>;
3+
get: <T>(key: string) => Promise<T>;
4+
delete: (key: string) => Promise<boolean>;
5+
}

lib/utils/local-db.ts

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
import { existsSync, unlinkSync } from 'node:fs';
1+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
2+
import {
3+
accessSync,
4+
existsSync,
5+
constants as fsConstants,
6+
readFileSync,
7+
unlinkSync,
8+
writeFileSync
9+
} from 'node:fs';
210
import { join } from 'node:path';
311

412
import appRoot from 'app-root-path';
5-
import { Low } from 'lowdb';
6-
import { JSONFile } from 'lowdb/node';
713

14+
import { InternalServerError, NotFoundError } from 'errors/apps-sdk-error';
15+
import { isDefined } from 'types/guards';
16+
import { ILocalStorageInstance } from 'types/secure-storage.local';
817
import { Logger } from 'utils/logger';
918

1019
const DEFAULT_DB_NAME = 'storage';
@@ -13,24 +22,108 @@ const logger = new Logger('LocalDb', { passThrough: true });
1322

1423
const getDbPath = (dbName: string) => join(appRoot.toString(), `${dbName}.json`);
1524

25+
const hasDiskWriteAccess = () => {
26+
const rootDir = appRoot.toString();
27+
try {
28+
accessSync(rootDir, fsConstants.W_OK);
29+
return true;
30+
} catch (_err) {
31+
return false;
32+
}
33+
};
34+
1635
export const deleteDb = (dbName = DEFAULT_DB_NAME): void => {
17-
const file = getDbPath(dbName);
18-
if (existsSync(file)) {
19-
unlinkSync(file);
36+
if (hasDiskWriteAccess()) {
37+
const file = getDbPath(dbName);
38+
if (existsSync(file)) {
39+
unlinkSync(file);
40+
}
2041
}
2142
};
2243

23-
export const initDb = async (dbName = DEFAULT_DB_NAME) => {
24-
const file = getDbPath(dbName);
25-
logger.info(`Initializing local db in ${file}`);
44+
const inMemoryData: Record<string, any> = {};
45+
46+
export class LocalMemoryDb implements ILocalStorageInstance {
47+
async set<T>(key: string, value: T): Promise<boolean> {
48+
inMemoryData[key] = value;
49+
return Promise.resolve(true);
50+
}
51+
52+
async delete(key: string): Promise<boolean> {
53+
delete inMemoryData[key];
54+
return Promise.resolve(true);
55+
}
56+
57+
async get<T>(key: string): Promise<T> {
58+
if (key in inMemoryData) {
59+
return Promise.resolve(inMemoryData[key] as T);
60+
}
61+
62+
throw new NotFoundError(`Data not found for '${key}'`);
63+
}
64+
}
65+
66+
export class LocalDb implements ILocalStorageInstance {
67+
private readonly dbFilePath: string;
68+
private memoryData: Record<string, any>;
2669

27-
const adapter = new JSONFile(file);
28-
const db = new Low(adapter);
70+
constructor(dbFileName: string = DEFAULT_DB_NAME) {
71+
if (!hasDiskWriteAccess()) {
72+
throw new InternalServerError('Missing write permissions');
73+
}
74+
75+
this.dbFilePath = getDbPath(dbFileName);
76+
if (!existsSync(this.dbFilePath)) {
77+
this.memoryData = {};
78+
writeFileSync(this.dbFilePath, JSON.stringify(this.memoryData), { encoding: 'utf8', flag: 'wx' });
79+
return;
80+
}
81+
82+
const stringifiedDbData = readFileSync(this.dbFilePath, 'utf-8');
83+
if (isDefined(stringifiedDbData)) {
84+
this.memoryData = JSON.parse(stringifiedDbData);
85+
return;
86+
}
87+
88+
this.memoryData = {};
89+
}
2990

30-
await db.read();
31-
db.data ||= {};
32-
await db.write();
33-
logger.info('initialized local db');
91+
async set<T>(key: string, value: T): Promise<boolean> {
92+
this.memoryData[key] = value;
93+
94+
writeFileSync(this.dbFilePath, JSON.stringify(this.memoryData));
95+
return Promise.resolve(true);
96+
}
97+
98+
async delete(key: string): Promise<boolean> {
99+
delete this.memoryData[key];
100+
101+
writeFileSync(this.dbFilePath, JSON.stringify(this.memoryData));
102+
return Promise.resolve(true);
103+
}
104+
105+
async get<T>(key: string): Promise<T> {
106+
if (key in this.memoryData) {
107+
return Promise.resolve(this.memoryData[key] as T);
108+
}
109+
110+
const data = readFileSync(this.dbFilePath, 'utf-8');
111+
const parsedData: Record<string, any> = JSON.parse(data);
112+
this.memoryData = parsedData;
113+
if (key in this.memoryData) {
114+
return Promise.resolve(this.memoryData[key] as T);
115+
}
116+
117+
throw new NotFoundError(`Data not found for '${key}'`);
118+
}
119+
}
120+
121+
export const initDb = (dbName = DEFAULT_DB_NAME) => {
122+
if (hasDiskWriteAccess()) {
123+
logger.info('Initializing local db');
124+
return new LocalDb(dbName);
125+
}
34126

35-
return db;
127+
logger.info('No disk access, initializing in memory db');
128+
return new LocalMemoryDb();
36129
};

package.json

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
11
{
22
"name": "@mondaycom/apps-sdk",
3-
"version": "1.0.0",
3+
"version": "0.0.1",
44
"description": "monday apps SDK for NodeJS",
5-
"main": "./dist/index.js",
6-
"types": "./dist/index.d.ts",
5+
"main": "./dist/cjs/index.js",
6+
"module": "./dist/esm/index.js",
7+
"types": "./dist/types/index.d.ts",
8+
"exports": {
9+
"import": "./dist/esm/index.js",
10+
"require": "./dist/cjs/index.js",
11+
"default": "./dist/cjs/index.js"
12+
},
713
"scripts": {
814
"test": "jest --forceExit",
915
"test:coverage": "npm run test -- --coverage",
1016
"lint": "eslint --ext .js,.ts lib",
1117
"lint-fix": "npm run lint -- --fix",
1218
"prettier:fix": "prettier --write .",
1319
"prettier:check": "prettier --check .",
14-
"build": "tsc --project ./tsconfig.json && tsc-alias -f -p tsconfig.json",
20+
"build:esm": "tsc -p ./tsconfig.esm.json && tsc-alias -f -p tsconfig.esm.json",
21+
"build:cjs": "tsc -p ./tsconfig.cjs.json && tsc-alias -f -p ./tsconfig.cjs.json",
22+
"build": "rm -rf dist/ && yarn prettier:fix && yarn lint-fix && yarn build:esm && yarn build:cjs",
1523
"prepublish-and-build": "yarn install --frozen-lockfile && yarn build"
1624
},
25+
"files": [
26+
"dist",
27+
"LICENSE",
28+
"README.md"
29+
],
1730
"repository": {
1831
"type": "git",
1932
"url": "git+https://github.com/mondaycom/apps-sdk.git"
@@ -37,17 +50,16 @@
3750
"@types/app-root-path": "^1.2.4",
3851
"@types/jest": "^29.5.0",
3952
"@types/jsonwebtoken": "^9.0.1",
40-
"@types/lowdb": "^1.0.11",
4153
"@typescript-eslint/eslint-plugin": "^5.48.2",
4254
"@typescript-eslint/parser": "^5.48.2",
4355
"eslint": "^8.32.0",
4456
"eslint-config-prettier": "^8.7.0",
4557
"eslint-import-resolver-typescript": "^3.5.3",
4658
"eslint-plugin-import": "^2.27.5",
4759
"eslint-plugin-n": "^15.6.1",
48-
"jest": "^29.3.1",
60+
"jest": "^27.3.1",
4961
"prettier": "^2.8.4",
50-
"ts-jest": "^29.0.5",
62+
"ts-jest": "^27.0.5",
5163
"tsc-alias": "^1.8.4",
5264
"tsconfig-paths": "^4.1.2",
5365
"typescript": "^4.9.4"
@@ -57,7 +69,6 @@
5769
"google-auth-library": "^8.7.0",
5870
"http-status-codes": "^2.2.0",
5971
"jsonwebtoken": "^9.0.0",
60-
"lowdb": "^5.1.0",
6172
"node-fetch": "^3.3.1"
6273
}
6374
}

tests/secure-storage/secure-storage.local.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
import { LocalSecureStorage } from '../../lib/secure-storage/secure-storage.local';
2-
import { encrypt } from '../../lib/utils/cipher';
1+
/* eslint-disable @typescript-eslint/ban-ts-comment */
2+
import { LocalSecureStorage } from 'lib/secure-storage/secure-storage.local';
3+
import { encrypt } from 'utils/cipher';
34

45
const MOCK_KEY = 'mock';
56
const MOCK_VALUE = { mock: 'mock' };
67
const MOCK_ENCRYPTED_VALUE = encrypt(JSON.stringify(MOCK_VALUE));
78
const MOCK_STORED_DATA = { [`${MOCK_KEY}`]: MOCK_ENCRYPTED_VALUE };
89

9-
let mockRead = jest.fn();
10-
let mockWrite = jest.fn();
10+
const mockGet = jest.fn();
11+
const mockSet = jest.fn();
12+
const mockDelete = jest.fn();
13+
1114
jest.mock('../../lib/utils/local-db', () => ({
1215
initDb() {
1316
return {
14-
read: mockRead,
15-
data: MOCK_STORED_DATA,
16-
write: mockWrite,
17+
get: mockGet,
18+
set: mockSet,
19+
delete: mockDelete,
1720
};
1821
},
1922
}));
@@ -43,7 +46,7 @@ describe('LocalSecureStorage', () => {
4346

4447
it('should set data when all is valid', async () => {
4548
await localSecureStorage.set(MOCK_KEY, MOCK_VALUE);
46-
expect(mockWrite).toBeCalled();
49+
expect(mockSet).toBeCalled();
4750
});
4851
});
4952

@@ -55,12 +58,11 @@ describe('LocalSecureStorage', () => {
5558

5659
it('should fail for key without value', async () => {
5760
const badKey = 'badKey';
58-
// @ts-ignore
5961
await expect(localSecureStorage.get(badKey)).rejects.toThrow();
6062
});
6163

6264
it('should succeed for key with value', async () => {
63-
// @ts-ignore
65+
mockGet.mockImplementation(() => MOCK_STORED_DATA[MOCK_KEY]);
6466
const result = await localSecureStorage.get(MOCK_KEY);
6567
expect(result).toEqual(MOCK_VALUE);
6668
});
@@ -74,14 +76,12 @@ describe('LocalSecureStorage', () => {
7476

7577
it('should delete for valid key', async () => {
7678
const mockKeyToDelete = 'deleteMe';
77-
MOCK_STORED_DATA[mockKeyToDelete] = MOCK_ENCRYPTED_VALUE;
7879
await localSecureStorage.delete(mockKeyToDelete);
79-
expect(MOCK_STORED_DATA[mockKeyToDelete]).toBeUndefined();
80+
expect(mockDelete).toBeCalledWith(mockKeyToDelete);
8081
});
8182

8283
it('should not fail for multiple deletions of the same key', async () => {
8384
const mockKeyToDelete = 'deleteMe';
84-
MOCK_STORED_DATA[mockKeyToDelete] = MOCK_ENCRYPTED_VALUE;
8585
await localSecureStorage.delete(mockKeyToDelete);
8686
await expect(localSecureStorage.delete(mockKeyToDelete)).resolves.toBeTruthy();
8787
});

0 commit comments

Comments
 (0)