feat: Implement users get apps API endpoint
This commit is contained in:
@@ -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
packages/backend/src/controllers/api/v1/users/get-apps.test.js
Normal file
210
packages/backend/src/controllers/api/v1/users/get-apps.test.js
Normal file
@@ -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');
|
||||
});
|
||||
});
|
@@ -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',
|
||||
|
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
|
@@ -1,9 +1,11 @@
|
||||
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';
|
||||
@@ -11,6 +13,14 @@ import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-an
|
||||
const router = Router();
|
||||
|
||||
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
|
||||
|
||||
router.get(
|
||||
'/:userId/apps',
|
||||
authenticateUser,
|
||||
authorizeUser,
|
||||
asyncHandler(getAppsAction)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/invoices',
|
||||
authenticateUser,
|
||||
|
@@ -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;
|
||||
|
55
packages/backend/test/mocks/rest/api/v1/users/get-apps.js
Normal file
55
packages/backend/test/mocks/rest/api/v1/users/get-apps.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const getAppsMock = () => {
|
||||
const appsData = [
|
||||
{
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/deepl/connection',
|
||||
connectionCount: 1,
|
||||
flowCount: 1,
|
||||
iconUrl: 'http://localhost:3000/apps/deepl/assets/favicon.svg',
|
||||
key: 'deepl',
|
||||
name: 'DeepL',
|
||||
primaryColor: '0d2d45',
|
||||
supportsConnections: true,
|
||||
},
|
||||
{
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/github/connection',
|
||||
connectionCount: 1,
|
||||
flowCount: 1,
|
||||
iconUrl: 'http://localhost:3000/apps/github/assets/favicon.svg',
|
||||
key: 'github',
|
||||
name: 'GitHub',
|
||||
primaryColor: '000000',
|
||||
supportsConnections: true,
|
||||
},
|
||||
{
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/slack/connection',
|
||||
flowCount: 1,
|
||||
iconUrl: 'http://localhost:3000/apps/slack/assets/favicon.svg',
|
||||
key: 'slack',
|
||||
name: 'Slack',
|
||||
primaryColor: '4a154b',
|
||||
supportsConnections: true,
|
||||
},
|
||||
{
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection',
|
||||
flowCount: 1,
|
||||
iconUrl: 'http://localhost:3000/apps/webhook/assets/favicon.svg',
|
||||
key: 'webhook',
|
||||
name: 'Webhook',
|
||||
primaryColor: '0059F7',
|
||||
supportsConnections: false,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
data: appsData,
|
||||
meta: {
|
||||
count: appsData.length,
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'Object',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getAppsMock;
|
Reference in New Issue
Block a user