Skip to content

Commit

Permalink
Merge pull request #146 from realmq/feature/admin-delete-resources
Browse files Browse the repository at this point in the history
Feature - admin delete resources
  • Loading branch information
alrik authored Jul 6, 2023
2 parents 73d3668 + fa0bcdc commit 2b0fe44
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Introduce new administrative channel management endpoints `/admin/v1/realms/{realmId}/channels`.
- Introduce new administrative subscription management endpoints `/admin/v1/realms/{realmId}/subscriptions`.
- Introduce new administrative endpoint for deleting auth tokens `DELETE /admin/v1/realms/{realmId}/tokens/{tokenId}`
- Introduce new administrative endpoint for deleting channels `DELETE /admin/v1/realms/{realmId}/channels/{channelId}`.
- Introduce new administrative endpoint for deleting subscriptions `DELETE /admin/v1/realms/{realmId}/subscriptions/{subscriptionsId}`.
- Introduce new administrative endpoint for deleting users `DELETE /admin/v1/realms/{realmId}/users/{userId}`.

### Changed
- Add support for upgraded verneMQ broker. This requires the acceptance of their [EULA](https://vernemq.com/end-user-license-agreement/),
Expand Down
22 changes: 22 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/channels/{id}.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @param {{admin: AdminTasks}} tasks Tasks
* @returns {{delete: delete}} Method handlers
*/
module.exports = tasks => ({
/**
* DELETE /realms/{realmId}/channels/{id}
* @param {object} request Request
* @param {object} response Response
*/
async delete(request, response) {
const {realmId, id} = request.params;

const {ok, error} = await tasks.admin.deleteChannel({realmId, id});

if (!ok) {
throw error;
}

response.status(204).send();
},
});
25 changes: 25 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/channels/{id}.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/realms/{realmId}/channels/{id}:
parameters:
- $ref: '#/parameters/realmIdInPath'
- name: id
description: The channel id.
in: path
required: true
type: string
pattern: ^[\w-]+$

delete:
summary: Delete the channel referenced by id.
description: |
Delete the channel and all sub resources like subscriptions and messages.
tags:
- Channels
responses:
204:
description: Channel has been deleted successfully.
400:
description: Request validation failed.
401:
description: The channel could not be deleted due to failed authorization.
404:
description: The requested channel does not exist.
22 changes: 22 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/subscriptions/{id}.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @param {{admin: AdminTasks}} tasks Tasks
* @returns {{delete: delete}} Method handlers
*/
module.exports = tasks => ({
/**
* DELETE /realms/{realmId}/subscriptions/{id}
* @param {object} request Request
* @param {object} response Response
*/
async delete(request, response) {
const {realmId, id} = request.params;

const {ok, error} = await tasks.admin.deleteSubscription({realmId, id});

if (!ok) {
throw error;
}

response.status(204).send();
},
});
25 changes: 25 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/subscriptions/{id}.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/realms/{realmId}/subscriptions/{id}:
parameters:
- $ref: '#/parameters/realmIdInPath'
- name: id
description: The subscription id.
in: path
required: true
type: string
pattern: ^[\w-]+$

delete:
summary: Delete the subscription referenced by id.
description: |
Delete the subscription and emit a subscription sync event.
tags:
- Subscriptions
responses:
204:
description: Subscription has been deleted successfully.
400:
description: Request validation failed.
401:
description: The subscription could not be deleted due to failed authorization.
404:
description: The requested subscription does not exist.
22 changes: 22 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/users/{id}.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @param {{admin: AdminTasks}} tasks Tasks
* @returns {{delete: delete}} Method handlers
*/
module.exports = tasks => ({
/**
* DELETE /realms/{realmId}/users/{id}
* @param {object} request Request
* @param {object} response Response
*/
async delete(request, response) {
const {realmId, id} = request.params;

const {ok, error} = await tasks.admin.deleteUser({realmId, id});

if (!ok) {
throw error;
}

response.status(204).send();
},
});
25 changes: 25 additions & 0 deletions src/api/admin/v1/routes/realms/{realmId}/users/{id}.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/realms/{realmId}/users/{id}:
parameters:
- $ref: '#/parameters/realmIdInPath'
- name: id
description: The user id.
in: path
required: true
type: string
pattern: ^[\w-]+$

delete:
summary: Delete the user referenced by id.
description: |
Delete the user and all sub resources like subscriptions, auth tokens and realtime connections.
tags:
- Users
responses:
204:
description: User has been deleted successfully.
400:
description: Request validation failed.
401:
description: The user could not be deleted due to failed authorization.
404:
description: The requested user does not exist.
1 change: 1 addition & 0 deletions src/bootstrap/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = ({
authTokenRules,
authRepository,
channelRepository,
messageRepository,
realmRepository,
subscriptionRepository,
userRepository,
Expand Down
14 changes: 14 additions & 0 deletions src/repositories/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,19 @@ module.exports = ({collection, createModel = createMessageModel}) => {

return multiRealmRepo.find({...query}, {limit, offset, sort});
},

/**
* Remove all messages from the given channel.
*
* @param {string} realmId Realm context
* @param {string} channelId Id of channel to remove messages for
*/
async deleteAllByChannelId({realmId, channelId}) {
if (!channelId) {
throw new Error('Missing channel id.');
}

await multiRealmRepo.deleteMany({realmId, channelId});
},
};
};
49 changes: 49 additions & 0 deletions src/tasks/admin/delete-channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const {success, failure} = require('../../lib/result');
const taskError = require('../../lib/error/task');
const createTaskError = require('../../lib/error/task');

/**
* Init delete channel task
* @param {RealmRepository} realmRepository Realm repository
* @param {ChannelRepository} channelRepository Channel repository
* @param {SubscriptionRepository} subscriptionRepository Subscription repository
* @param {MessageRepository} messageRepository Message repository
* @returns {Function} Task
*/
module.exports = ({
realmRepository,
channelRepository,
subscriptionRepository,
messageRepository,
}) =>
/**
* @function AdminTasks#deleteChannel
* @param {string} realmId Realm id
* @param {string} id Channel id
* @returns {Result<ChannelModel>}
*/
async ({realmId, id}) => {
const realm = await realmRepository.findOne({id: realmId});
if (!realm) {
return failure(createTaskError({
code: 'UnknownRealm',
message: 'Cannot lookup the given realm.',
}));
}

const channel = await channelRepository.findOne({realmId, id});
if (!channel) {
return failure(taskError({
code: 'UnknownChannel',
message: 'Channel does not exists.',
}));
}

await Promise.all([
channelRepository.deleteOne({realmId, id}),
subscriptionRepository.deleteAllByChannelId({realmId, channelId: id}),
messageRepository.deleteAllByChannelId({realmId, channelId: id}),
]);

return success(channel);
};
84 changes: 84 additions & 0 deletions src/tasks/admin/delete-channel.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const channelRepository = require('../../lib/test/mocks/repositories/channel');
const realmRepository = require('../../lib/test/mocks/repositories/realm');
const messageRepository = require('../../lib/test/mocks/repositories/message');
const subscriptionRepository = require('../../lib/test/mocks/repositories/subscription');
const initDeleteChannel = require('./delete-channel');

describe('The client deleteChannel task', () => {
let deleteChannel;

beforeEach(() => {
channelRepository.deleteOne = jest.fn();
messageRepository.deleteAllByChannelId = jest.fn();
subscriptionRepository.deleteAllByChannelId = jest.fn();
deleteChannel = initDeleteChannel({
realmRepository,
messageRepository,
channelRepository,
subscriptionRepository,
});
});

describe('when called with unknown realm id', () => {
it('should fail with appropriate error', async () => {
const {ok, error} = await deleteChannel({realmId: realmRepository.unknownRealmId, id: channelRepository.knownChannelId});

expect(ok).toBe(false);
expect(error).toBeDefined();
expect(error.code).toBe('UnknownRealm');
});
});

describe('when given an unknown channel id', () => {
it('should fail with appropriate error', async () => {
const {ok, error} = await deleteChannel({
realmId: realmRepository.knownRealmId,
id: channelRepository.unknownChannelId,
});

expect(ok).toBe(false);
expect(error).toBeDefined();
expect(error.code).toBe('UnknownChannel');
});
});

describe('when configured correctly', () => {
it('should delete the channel', async () => {
const {ok} = await deleteChannel({
realmId: realmRepository.knownRealmId,
id: channelRepository.knownChannelId,
});

expect(ok).toBe(true);
expect(channelRepository.deleteOne).toHaveBeenCalled();
});

it('should delete all subscriptions on a channel', async () => {
const subscription = subscriptionRepository.validSubscription;
await deleteChannel({
realmId: subscription.realmId,
id: subscription.channelId,
});

const deleteMethod = subscriptionRepository.deleteAllByChannelId;
expect(deleteMethod).toHaveBeenCalledWith({
realmId: subscription.realmId,
channelId: subscription.channelId,
});
});

it('should delete all messages on a channel', async () => {
const message = messageRepository.validMessage;
await deleteChannel({
realmId: message.realmId,
id: message.channelId,
});

const deleteMethod = messageRepository.deleteAllByChannelId;
expect(deleteMethod).toHaveBeenCalledWith({
realmId: message.realmId,
channelId: message.channelId,
});
});
});
});
44 changes: 44 additions & 0 deletions src/tasks/admin/delete-subscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const {success, failure} = require('../../lib/result');
const taskError = require('../../lib/error/task');
const createTaskError = require('../../lib/error/task');

/**
* Init delete subscription task
* @param {RealmRepository} realmRepository Realm repository
* @param {SubscriptionRepository} subscriptionRepository Subscription repository
* @param {CommonTasks#sendSubscriptionSyncMessage} sendSubscriptionSyncMessage Task for sending subscription sync
* @returns {AdminTasks#deleteSubscription} Task
*/
module.exports = ({
realmRepository,
subscriptionRepository,
sendSubscriptionSyncMessage,
}) =>
/**
* @function AdminTasks#deleteSubscription
* @param {string} realmId Realm id
* @param {string} id Subscription id
* @returns {Result<SubscriptionModel>}
*/
async ({realmId, id}) => {
const realm = await realmRepository.findOne({id: realmId});
if (!realm) {
return failure(createTaskError({
code: 'UnknownRealm',
message: 'Cannot lookup the given realm.',
}));
}

const subscription = await subscriptionRepository.findOne({realmId, id});
if (!subscription) {
return failure(taskError({
code: 'UnknownSubscription',
message: 'Subscription does not exists.',
}));
}

await subscriptionRepository.deleteOne({realmId, id});
sendSubscriptionSyncMessage({subscription, action: 'deleted'});

return success(subscription);
};
Loading

0 comments on commit 2b0fe44

Please sign in to comment.