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',
|
action: 'read',
|
||||||
subject: 'User',
|
subject: 'User',
|
||||||
},
|
},
|
||||||
|
'GET /api/v1/users/:userId/apps': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Connection',
|
||||||
|
},
|
||||||
'GET /api/v1/flows/:flowId': {
|
'GET /api/v1/flows/:flowId': {
|
||||||
action: 'read',
|
action: 'read',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
|
@@ -7,6 +7,7 @@ import { hasValidLicense } from '../helpers/license.ee.js';
|
|||||||
import userAbility from '../helpers/user-ability.js';
|
import userAbility from '../helpers/user-ability.js';
|
||||||
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
|
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
|
import App from './app.js';
|
||||||
import Connection from './connection.js';
|
import Connection from './connection.js';
|
||||||
import Execution from './execution.js';
|
import Execution from './execution.js';
|
||||||
import Flow from './flow.js';
|
import Flow from './flow.js';
|
||||||
@@ -313,6 +314,56 @@ class User extends Base {
|
|||||||
return invoices;
|
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) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { authenticateUser } from '../../../helpers/authentication.js';
|
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||||
|
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||||
import checkIsCloud from '../../../helpers/check-is-cloud.js';
|
import checkIsCloud from '../../../helpers/check-is-cloud.js';
|
||||||
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.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 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 getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js';
|
||||||
import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.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';
|
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();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
|
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:userId/apps',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
asyncHandler(getAppsAction)
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/invoices',
|
'/invoices',
|
||||||
authenticateUser,
|
authenticateUser,
|
||||||
|
@@ -1,12 +1,22 @@
|
|||||||
const appSerializer = (app) => {
|
const appSerializer = (app) => {
|
||||||
return {
|
let appData = {
|
||||||
name: app.name,
|
|
||||||
key: app.key,
|
key: app.key,
|
||||||
|
name: app.name,
|
||||||
iconUrl: app.iconUrl,
|
iconUrl: app.iconUrl,
|
||||||
|
primaryColor: app.primaryColor,
|
||||||
authDocUrl: app.authDocUrl,
|
authDocUrl: app.authDocUrl,
|
||||||
supportsConnections: app.supportsConnections,
|
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;
|
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