Skip to content

Commit

Permalink
Merge pull request #1795 from automatisch/rest-get-user-apps
Browse files Browse the repository at this point in the history
feat: Implement users get apps API endpoint
  • Loading branch information
farukaydin committed Apr 7, 2024
2 parents c814737 + 3e3e481 commit 855ec53
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 3 deletions.
7 changes: 7 additions & 0 deletions packages/backend/src/controllers/api/v1/users/get-apps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { renderObject } from '../../../../helpers/renderer.js';

export default async (request, response) => {
const apps = await request.currentUser.getApps(request.query.name);

renderObject(response, apps, { serializer: 'App' });
};
210 changes: 210 additions & 0 deletions packages/backend/src/controllers/api/v1/users/get-apps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import getAppsMock from '../../../../../test/mocks/rest/api/v1/users/get-apps.js';

describe('GET /api/v1/users/:userId/apps', () => {
let currentUser, currentUserRole, token;

beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });

token = createAuthTokenByUserId(currentUser.id);
});

it('should return all apps of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});

await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});

const flowOne = await createFlow({ userId: currentUser.id });

await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});

const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});

await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});

const flowTwo = await createFlow({ userId: currentUser.id });

const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});

await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});

await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});

const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);

const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});

it('should return all apps of the another user', async () => {
const anotherUser = await createUser();

await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});

await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});

const flowOne = await createFlow({ userId: anotherUser.id });

await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});

const flowOneActionStepConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});

await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});

const flowTwo = await createFlow({ userId: anotherUser.id });

const flowTwoTriggerStepConnection = await createConnection({
userId: anotherUser.id,
key: 'github',
draft: false,
});

await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});

await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});

const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);

const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});

it('should return specified app of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});

await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});

const flowOne = await createFlow({ userId: currentUser.id });

await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});

const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});

await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});

const flowTwo = await createFlow({ userId: currentUser.id });

const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});

await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});

await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});

const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps?name=deepl`)
.set('Authorization', token)
.expect(200);

expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toEqual('deepl');
});
});
4 changes: 4 additions & 0 deletions packages/backend/src/helpers/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const authorizationList = {
action: 'read',
subject: 'User',
},
'GET /api/v1/users/:userId/apps': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/flows/:flowId': {
action: 'read',
subject: 'Flow',
Expand Down
51 changes: 51 additions & 0 deletions packages/backend/src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { hasValidLicense } from '../helpers/license.ee.js';
import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js';
import App from './app.js';
import Connection from './connection.js';
import Execution from './execution.js';
import Flow from './flow.js';
Expand Down Expand Up @@ -313,6 +314,56 @@ class User extends Base {
return invoices;
}

async getApps(name) {
const connections = await this.authorizedConnections
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');

const flows = await this.authorizedFlows
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');

const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);

const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];

let apps = await App.findAll(name);

apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);

app.connectionCount = connection?.count || 0;
app.flowCount = 0;

flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);

if (usedFlow) {
app.flowCount += 1;
}
});

return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));

return apps;
}

async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);

Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/routes/api/v1/users.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
import getAppsAction from '../../../controllers/api/v1/users/get-apps.js';
import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js';
import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js';
import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js';

const router = Router();

router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));

router.get(
'/:userId/apps',
authenticateUser,
authorizeUser,
asyncHandler(getAppsAction)
);

router.get(
'/invoices',
authenticateUser,
Expand Down
16 changes: 13 additions & 3 deletions packages/backend/src/serializers/app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
const appSerializer = (app) => {
return {
name: app.name,
let appData = {
key: app.key,
name: app.name,
iconUrl: app.iconUrl,
primaryColor: app.primaryColor,
authDocUrl: app.authDocUrl,
supportsConnections: app.supportsConnections,
primaryColor: app.primaryColor,
};

if (app.connectionCount) {
appData.connectionCount = app.connectionCount;
}

if (app.flowCount) {
appData.flowCount = app.flowCount;
}

return appData;
};

export default appSerializer;

0 comments on commit 855ec53

Please sign in to comment.