From fe5e61b1074a74a6f9fbabaedb2abd270e927f54 Mon Sep 17 00:00:00 2001 From: Manu Artero Anguita Date: Thu, 18 Mar 2021 15:31:34 +0100 Subject: [PATCH] feat(server): unsubscribe from topic WIP, to be completed with API overhaul --- src/server/handlers.js | 16 +++++ src/server/restify-server.js | 104 +++++++++++++++++++----------- src/server/restify-server.test.js | 28 +++++++- src/storage/mysql.js | 34 +++++++++- types.d.ts | 14 +++- 5 files changed, 154 insertions(+), 42 deletions(-) diff --git a/src/server/handlers.js b/src/server/handlers.js index ad4cf2b..7d0e8b9 100644 --- a/src/server/handlers.js +++ b/src/server/handlers.js @@ -81,10 +81,26 @@ const createHandlers = (adapter, storage, bot) => { return getTopics() }, + removeTopic: async topic => { + const formerSubscribers = await storage.getSubscribers(topic) + const cancelSubscriptionTasks = formerSubscribers.map(user => { + return () => storage.cancelSubscription(user, topic) + }) + await Promise.all(cancelSubscriptionTasks) + await storage.removeTopic(topic) + return getTopics() + }, + forceSubscription: async (user, topic) => { await storage.subscribe(user, topic) const subscribers = await storage.getSubscribers(topic) return subscribers + }, + + cancelSubscription: async (user, topic) => { + await storage.cancelSubscription(user, topic) + const currentSubscribers = await storage.getSubscribers(topic) + return currentSubscribers } } } diff --git a/src/server/restify-server.js b/src/server/restify-server.js index 8008435..4883c76 100644 --- a/src/server/restify-server.js +++ b/src/server/restify-server.js @@ -17,7 +17,7 @@ const ensureTopic = req => /** * restify server in charge of: - * - routing to handler functions + * - routing to handler functions (delegating all business-logic) * - logging requests and responses * - extracting input from request (parsing) * - handle HTTP status codes @@ -31,7 +31,9 @@ const createRestifyServer = ({ getUsers, getTopics, createTopic, - forceSubscription + removeTopic, + forceSubscription, + cancelSubscription }) => { const server = restify.createServer({ log }) server.use(restify.plugins.queryParser()) @@ -54,41 +56,6 @@ const createRestifyServer = ({ next() }) - server.get('/api/v1/users', async (_, res, next) => { - const users = await getUsers() - res.send(200, users) - next() - }) - - server.get('/api/v1/topics', async (_, res, next) => { - const topics = await getTopics() - res.send(200, topics) - next() - }) - - server.post('/api/v1/topics', async (req, res, next) => { - const topic = req.body ? req.body.name : undefined - if (!topic) { - const err = new BadRequestError("required: 'name'") - return next(err) - } - const topics = await createTopic(topic) - res.send(200, topics) - next() - }) - - server.put('/api/v1/topics/:topic', async (req, res, next) => { - const user = req.body ? req.body.user : undefined - if (!user) { - const err = new BadRequestError("required: 'user'") - return next(err) - } - const topic = req.params.topic - const subscribers = await forceSubscription(user, topic) - res.send(200, { subscribers }) - next() - }) - /** Resolves 202: Accepted */ server.post('/api/v1/messages', async (req, res, next) => { await processMessage(req, res) @@ -136,6 +103,69 @@ const createRestifyServer = ({ } }) + server.get('/api/v1/admin', (_, res, next) => { + const { routes } = server.getDebugInfo() + res.send(200, routes) + }) + + server.get('/api/v1/admin/users', async (_, res, next) => { + const users = await getUsers() + res.send(200, users) + next() + }) + + server.get('/api/v1/admin/topics', async (_, res, next) => { + const topics = await getTopics() + res.send(200, topics) + next() + }) + + server.post('/api/v1/admin/topics', async (req, res, next) => { + const topic = req.body ? req.body.name : undefined + if (!topic) { + const err = new BadRequestError("required: 'name'") + return next(err) + } + const topics = await createTopic(topic) + res.send(200, topics) + next() + }) + + server.del('/api/v1/topics/:topic', async (req, res, next) => { + const topic = req.body ? req.body.name : undefined + try { + const topics = await removeTopic(topic) + res.send(200, topics) + next() + } catch (err) { + next(err) + } + }) + + server.put('/api/v1/topics/:topic/subscribers', async (req, res, next) => { + const user = req.body ? req.body.user : undefined + if (!user) { + const err = new BadRequestError("required: 'user'") + return next(err) + } + const topic = req.params.topic + const subscribers = await forceSubscription(user, topic) + res.send(200, { subscribers }) + next() + }) + + // server.del('/api/v1/topics/:topic', async (req, res, next) => { + // const user = req.body ? req.body.user : undefined + // if (!user) { + // const err = new BadRequestError("required: 'user'") + // return next(err) + // } + // const topic = req.params.topic + // const subscribers = await cancelSubscription(user, topic) + // res.send(200, { subscribers }) + // next() + // }) + return { /** * @param {object=} param0 diff --git a/src/server/restify-server.test.js b/src/server/restify-server.test.js index cf288ec..4aea7f5 100644 --- a/src/server/restify-server.test.js +++ b/src/server/restify-server.test.js @@ -20,9 +20,14 @@ const mockedHandlers = { orange: ['jane.doe@megacoorp.com', 'jhon.smith@contractor.com'], tangerine: [] }), + removeTopic: jest.fn().mockResolvedValue({ + banana: ['jane.doe@megacoorp.com'], + tangerine: [] + }), forceSubscription: jest .fn() - .mockResolvedValue(['jane.doe@megacoorp.com', 'jhon.smith@contractor.com']) + .mockResolvedValue(['jane.doe@megacoorp.com', 'jhon.smith@contractor.com']), + cancelSubscription: jest.fn().mockResolvedValue(['jhon.smith@contractor.com']) } describe('createRestifyServer()', () => { @@ -107,6 +112,21 @@ describe('createRestifyServer()', () => { }) }) + describe('[DELETE] /api/v1/topics', () => { + it('[200] routes to removeTopic()', done => { + // @ts-ignore + client.del('/api/v1/topics', (_, __, res, data) => { + expect(res.statusCode).toEqual(200) + expect(data).toEqual({ + banana: ['jane.doe@megacoorp.com'], + tangerine: [] + }) + expect(mockedHandlers.removeTopic).toHaveBeenCalledWith('') + done() + }) + }) + }) + describe('[PUT] /api/v1/topics/{topic}', () => { it("[400] requires 'user'", done => { client.put( @@ -255,7 +275,11 @@ describe('createRestifyServer()', () => { it('[202] routes to broadcast() (considering topic creation)', done => { client.post( '/api/v1/broadcast', - { topic: 'orange', message: 'orange event', createTopicIfNotExists: true }, + { + topic: 'orange', + message: 'orange event', + createTopicIfNotExists: true + }, // @ts-ignore (_, __, res, data) => { expect(mockedHandlers.broadcast).toHaveBeenCalledWith( diff --git a/src/storage/mysql.js b/src/storage/mysql.js index 8690573..315b814 100644 --- a/src/storage/mysql.js +++ b/src/storage/mysql.js @@ -99,9 +99,30 @@ const ensureTopic = async topic => { const registerTopic = async topic => ensureTopic(topic).then(instance => !!instance) +/** + * @param {string} topic + * @return {Promise} + */ +const removeTopic = async topic => { + log.debug(`[db] removing topic: "${topic}"`) + return Topics.destroy({ where: { name: topic } }) + .then(affectedRows => { + if (affectedRows === 1) { + log.debug(`[db] removed topic "${topic}"`) + return true + } + log.warn(`[db] unexpected affected rows removing topic "${topic}"`) + return false + }) + .catch(err => { + log.error(`[db] unable to remove topic "${topic}"`, err) + return false + }) +} + /** * @param {string} user - * @return {Promise<{userInstance: any, topics: string[]|null}>} + * @return {Promise<{userInstance: any, topics: string[]}|null>} */ const getAllUserInfo = async user => { log.debug(`[db] reading subscriptions for user "${user}"`) @@ -157,6 +178,15 @@ const subscribe = async (user, topic) => { }) } +/** + * @param {string} user + * @param {string} topic + * @return {Promise} + */ +const cancelSubscription = async (user, topic) => { + +} + /** * @param {string} topic * @return {Promise} @@ -268,7 +298,9 @@ const storage = { saveConversation, getConversation, registerTopic, + removeTopic, subscribe, + cancelSubscription, getSubscribedTopics, getSubscribers, listUsers, diff --git a/types.d.ts b/types.d.ts index 3d95f5a..574a89c 100644 --- a/types.d.ts +++ b/types.d.ts @@ -22,9 +22,13 @@ declare namespace Types { /** @return success flag (independently of actual operation - e.g. already existing entry) */ registerTopic: (topic: string) => Promise; + removeTopic: (topic: string) => Promise; + /** @return success flag (independently of actual operation - e.g. already existing entry) */ subscribe: (user: string, topic: string) => Promise; + cancelSubscription: (user: string, topic: string) => Promise; + /** @return null if err */ getSubscribedTopics: (user: string) => Promise; @@ -50,6 +54,8 @@ declare namespace Types { ensureTopic?: boolean; } + type TopicsDictionary = { [topic: string]: string[] }; + interface Handlers { /* bot-SDK entry point */ processMessage: ( @@ -76,14 +82,18 @@ declare namespace Types { /* debugging */ getUsers: () => Promise; - getTopics: () => Promise<{ [topic: string]: string[] }>; + getTopics: () => Promise; /* ops */ /** @return topics */ - createTopic: (topic: string) => Promise<{ [topic: string]: string[] }>; + createTopic: (topic: string) => Promise; + /** @return topics */ + removeTopic: (topic: string) => Promise; /** @return subscribers */ forceSubscription: (user: string, topic: string) => Promise; + /** @return subscribers */ + cancelSubscription: (user: string, topic: string) => Promise; } interface ICard {