diff --git a/src/controllers/account.ts b/src/controllers/account.ts index e6449e3..09e43b7 100644 --- a/src/controllers/account.ts +++ b/src/controllers/account.ts @@ -45,10 +45,10 @@ class AccountController { public static async delete(uuid: string): Promise { try { const _account = await Account.get(uuid) - const { blocks } = _account + const _blocks = await _account.getBlocks() logger.info(`Deleting account: ${uuid}`) - for (const _blockName of blocks) { + for (const _blockName of _blocks) { logger.info(`Deleting block in account: ${uuid}`) const _block = await Block.get(uuid, _blockName) await _block.delete() diff --git a/src/controllers/block.ts b/src/controllers/block.ts index 4f8d323..02d8a22 100644 --- a/src/controllers/block.ts +++ b/src/controllers/block.ts @@ -23,9 +23,6 @@ class BlockController { const _block = new Block(accountUUID, name, payload) await _block.store() - logger.info(`Adding block to account: ${accountUUID}`) - await _account.addBlock(name) - return `Your Pantry was updated with basket: ${name}!` } catch (error) { logger.error(`Block creation failed: ${error.message}`) @@ -43,7 +40,6 @@ class BlockController { _block = await Block.get(accountUUID, name) _blockDetails = _block.sanitize() } catch (error) { - await _account.removeBlock(name) throw error } @@ -60,13 +56,10 @@ class BlockController { public static async delete(accountUUID: string, name: string): Promise { try { - const _account = await Account.get(accountUUID) - const _block = await Block.get(accountUUID, name) logger.info(`Removing block from account: ${accountUUID}`) await _block.delete() - await _account.removeBlock(name) return `${name} was removed from your Pantry!` } catch (error) { diff --git a/src/interfaces/account.ts b/src/interfaces/account.ts index 5f27b65..8ac8314 100644 --- a/src/interfaces/account.ts +++ b/src/interfaces/account.ts @@ -5,7 +5,6 @@ export interface IAccount { } export interface IAccountPrivate extends IAccount { - blocks: string[], maxNumberOfBlocks: number, notifications: boolean, uuid?: string, diff --git a/src/models/account.ts b/src/models/account.ts index e2582a5..fb27b65 100644 --- a/src/models/account.ts +++ b/src/models/account.ts @@ -1,6 +1,5 @@ // Extarnal Libs import { - IsArray, IsBoolean, IsEmail, IsNotEmpty, @@ -42,10 +41,6 @@ class Account { return `account:${uuid}` } - @IsNotEmpty() - @IsArray() - public blocks: string[] - @IsNotEmpty() @IsString() private name: string @@ -70,13 +65,12 @@ class Account { private readonly defaultMaxNumberOfBlocks = 50 constructor(params: any) { - const { name, description, contactEmail, notifications, blocks, uuid, maxNumberOfBlocks } = params + const { name, description, contactEmail, notifications, uuid, maxNumberOfBlocks } = params this.name = name this.description = description this.contactEmail = contactEmail this.notifications = notifications ?? false this.maxNumberOfBlocks = maxNumberOfBlocks ?? this.defaultMaxNumberOfBlocks - this.blocks = blocks ?? [] this.uuid = uuid ?? uuidv4() } @@ -94,34 +88,22 @@ class Account { return this.uuid } - public sanitize(): IAccountPublic { + public async sanitize(): Promise { + const _baskets = await this.getBlocks() + const _sanitizedItems: IAccountPublic = { name: this.name, description: this.description, contactEmail: this.contactEmail, - baskets: this.blocks, + baskets: _baskets, } return _sanitizedItems } - public async addBlock(blockName: string): Promise { - const _currentBlocks = this.blocks.filter((name) => name !== blockName) - const _updatedBlocks = [..._currentBlocks, blockName] - - this.blocks = _updatedBlocks - await this.store() - } - - public async removeBlock(blockName: string): Promise { - const _updatedBlocks = this.blocks.filter((name) => name !== blockName) - - this.blocks = _updatedBlocks - await this.store() - } - - public checkIfFull(): boolean { - const _isFull = this.blocks.length === this.maxNumberOfBlocks + public async checkIfFull(): Promise { + const _blocks = await this.getBlocks() + const _isFull = _blocks.length === this.maxNumberOfBlocks return _isFull } @@ -134,6 +116,17 @@ class Account { await this.store() } + public async getBlocks(): Promise { + const _accountKey = Account.generateRedisKey(this.uuid) + const _blocks = await dataStore.scan(`${_accountKey}::block:*`) + + const _blocksSanitized = _blocks.map((block) => { + return block.split(':')[4] + }) + + return _blocksSanitized + } + private generateRedisPayload(): string { const _accountDetails: IAccountPrivate = { name: this.name, @@ -141,7 +134,6 @@ class Account { contactEmail: this.contactEmail, notifications: this.notifications, maxNumberOfBlocks: this.maxNumberOfBlocks, - blocks: this.blocks, uuid: this.uuid, } return JSON.stringify(_accountDetails) diff --git a/src/models/block.ts b/src/models/block.ts index 2ba0aca..281520a 100644 --- a/src/models/block.ts +++ b/src/models/block.ts @@ -17,7 +17,6 @@ import { IBlock } from '../interfaces/block' class Block { public static async get(accountUUID: string, name: string): Promise { const _blockKey = Block.generateRedisKey(accountUUID, name) - const _stringifiedBlock = await dataStore.get(_blockKey) if (!_stringifiedBlock) { diff --git a/src/services/__mocks__/dataStore.ts b/src/services/__mocks__/dataStore.ts index f436d33..af47300 100644 --- a/src/services/__mocks__/dataStore.ts +++ b/src/services/__mocks__/dataStore.ts @@ -2,6 +2,7 @@ const dataStore = { get: jest.fn(async () => { return }), set: jest.fn(async () => { return }), delete: jest.fn(async () => { return }), + scan: jest.fn(async () => { return }), } export = dataStore diff --git a/src/services/dataStore.ts b/src/services/dataStore.ts index 16cb95e..5185abe 100644 --- a/src/services/dataStore.ts +++ b/src/services/dataStore.ts @@ -17,11 +17,11 @@ const _crypto = { } const dataStore = { - async get(uuid: string): Promise { + async get(key: string): Promise { try { const _redisClient = redis.createClient() const _get = promisify(_redisClient.get).bind(_redisClient) - const _storedPayloadString = await _get(uuid) + const _storedPayloadString = await _get(key) _redisClient.quit() if (_storedPayloadString) { @@ -45,7 +45,7 @@ const dataStore = { } }, - async set(uuid: string, payload: string, lifespan: number): Promise { + async set(key: string, payload: string, lifespan: number): Promise { try { const _cipher = crypto.createCipheriv( _crypto.algorithm, @@ -59,7 +59,7 @@ const dataStore = { const _redisClient = redis.createClient() const _set = promisify(_redisClient.set).bind(_redisClient) - await _set(uuid, _encryptedPayloadString, 'EX', lifespan) + await _set(key, _encryptedPayloadString, 'EX', lifespan) _redisClient.quit() } catch (error) { logger.error(`Error when setting key: ${error.message}`) @@ -67,17 +67,35 @@ const dataStore = { } }, - async delete(uuid: string): Promise { + async delete(key: string): Promise { try { const _redisClient = redis.createClient() const _delete = promisify(_redisClient.del).bind(_redisClient) - await _delete(uuid) + await _delete(key) _redisClient.quit() } catch (error) { logger.error(`Error when deleting a key: ${error.message}`) throw new Error('Pantry is having critical issues') } }, + + async scan(pattern: string): Promise { + try { + const _redisClient = redis.createClient() + const _scan = promisify(_redisClient.scan).bind(_redisClient) + const [ _cursor, _storedKeys ] = await _scan(0, 'MATCH', pattern) + _redisClient.quit() + + if (Number(_cursor) !== 0) { + throw new Error(`cursor returned invalid value: ${_cursor}`) + } + + return _storedKeys + } catch (error) { + logger.error(`Error when scanning keys: ${error.message}`) + throw new Error('Pantry is having critical issues') + } + }, } export = dataStore diff --git a/tests/controllers/account.test.ts b/tests/controllers/account.test.ts index 234550a..f89ff3a 100644 --- a/tests/controllers/account.test.ts +++ b/tests/controllers/account.test.ts @@ -60,7 +60,6 @@ describe('When retrieving an account', () => { name: 'Existing Account', description: 'Account made while testing', contactEmail: 'derp@flerp.com', - blocks: [], maxNumberOfBlocks: 50, notifications: true, uuid: '6dc70531-d0bf-4b3a-8265-b20f8a69e180', @@ -68,6 +67,7 @@ describe('When retrieving an account', () => { it ('returns the correct account attributes', async () => { mockedDataStore.get.mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve([])) const _accountBase: IAccount = await AccountController.get(_existingAccount.uuid) expect(_accountBase).toBeDefined() @@ -87,7 +87,6 @@ describe('When deleting an account', () => { name: 'Existing Account', description: 'Account made while testing', contactEmail: 'derp@flerp.com', - blocks: [], maxNumberOfBlocks: 50, notifications: true, uuid: '6dc70531-d0bf-4b3a-8265-b20f8a69e180', @@ -95,6 +94,18 @@ describe('When deleting an account', () => { it ('returns confirmation message', async () => { mockedDataStore.get.mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve([])) + + const _response = await AccountController.delete(_existingAccount.uuid) + expect(_response).toMatch(/Your Pantry has been deleted/) + }) + + it ('deletes all existing blocks', async () => { + mockedDataStore.get + .mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) + .mockReturnValueOnce(Promise.resolve(JSON.stringify({ derp: 'flerp' }))) + + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve(['existingBlock'])) const _response = await AccountController.delete(_existingAccount.uuid) expect(_response).toMatch(/Your Pantry has been deleted/) diff --git a/tests/controllers/block.test.ts b/tests/controllers/block.test.ts index cac1289..453be82 100644 --- a/tests/controllers/block.test.ts +++ b/tests/controllers/block.test.ts @@ -12,13 +12,13 @@ const _existingAccount: IAccountPrivate = { name: 'Existing Account', description: 'Account made while testing', contactEmail: 'derp@flerp.com', - blocks: [], maxNumberOfBlocks: 50, notifications: true, uuid: '6dc70531-d0bf-4b3a-8265-b20f8a69e180', } afterEach(() => { + mockedDataStore.get.mockReset() jest.clearAllMocks() }) @@ -27,24 +27,36 @@ describe('When creating a block', () => { const _accountUUID = '6dc70531-d0bf-4b3a-8265-b20f8a69e180' mockedDataStore.get.mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve([])) const _response = await BlockController.create(_accountUUID, 'NewBlock', { derp: 'flerp' }) expect(_response).toMatch(/Your Pantry was updated with basket: NewBlock/) }) + it ('throws an error if validation fails', async () => { + const _accountUUID = '6dc70531-d0bf-4b3a-8265-b20f8a69e180' + + mockedDataStore.get.mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve([])) + + await expect(BlockController.create(_accountUUID, 'NewBlock', {})) + .rejects + .toThrow('Validation failed:') + }) + it ('throws an error if account has reached max # of blocks', async () => { const _accountUUID = '6dc70531-d0bf-4b3a-8265-b20f8a69e180' const _maxedAccount: IAccountPrivate = { name: 'Maxed Existing Account', description: 'Account made while testing', contactEmail: 'derp@flerp.com', - blocks: ['blockName'], maxNumberOfBlocks: 1, notifications: true, uuid: '6dc70531-d0bf-4b3a-8265-b20f8a69e180', } mockedDataStore.get.mockReturnValueOnce(Promise.resolve(JSON.stringify(_maxedAccount))) + mockedDataStore.scan.mockReturnValueOnce(Promise.resolve(['oldBlock'])) await expect(BlockController.create(_accountUUID, 'NewBlock', { derp: 'flerp' })) .rejects @@ -98,9 +110,7 @@ describe('When deleting a block', () => { const _accountUUID = '6dc70531-d0bf-4b3a-8265-b20f8a69e180' const _blockName = 'NewBlock' - mockedDataStore.get - .mockReturnValueOnce(Promise.resolve(JSON.stringify(_existingAccount))) - .mockReturnValueOnce(Promise.resolve(null)) + mockedDataStore.get.mockReturnValueOnce(Promise.resolve(null)) await expect(BlockController.delete(_accountUUID, _blockName)) .rejects