Compare commits

..

2 Commits

Author SHA1 Message Date
Rıdvan Akca
c762f0562f feat(airbrake): add new errors trigger 2024-02-13 15:02:57 +03:00
Rıdvan Akca
98274c3d71 feat(airbrake): add airbrake integration 2024-02-13 12:57:10 +03:00
57 changed files with 369 additions and 637 deletions

View File

@@ -33,6 +33,7 @@
"axios": "1.6.0",
"bcrypt": "^5.0.1",
"bullmq": "^3.0.0",
"copyfiles": "^2.4.1",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
@@ -44,6 +45,7 @@
"graphql-middleware": "^6.1.15",
"graphql-shield": "^7.5.0",
"graphql-tools": "^8.2.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
@@ -57,7 +59,7 @@
"morgan": "^1.10.0",
"multer": "1.4.5-lts.1",
"node-html-markdown": "^1.3.0",
"nodemailer": "6.9.9",
"nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6",
"objection": "^3.0.0",
"passport": "^0.6.0",
@@ -66,6 +68,7 @@
"pluralize": "^8.0.0",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"stripe": "^11.13.0",
"winston": "^3.7.1",
"xmlrpc": "^1.3.2"
},

View File

@@ -0,0 +1 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,44 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'screenName',
label: 'Screen Name',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description:
'Screen name of your connection to be used on Automatisch UI.',
clickToCopy: false,
},
{
key: 'instanceUrl',
label: 'Instance URL',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Your subdomain as https://{yoursubdomain}.airbrake.io',
clickToCopy: false,
},
{
key: 'authToken',
label: 'Auth Token',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Airbrake Auth Token of your account.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,8 @@
import verifyCredentials from './verify-credentials.js';
const isStillVerified = async ($) => {
await verifyCredentials($);
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,14 @@
const verifyCredentials = async ($) => {
await $.http.get(`/api/v4/projects?key=${$.auth.data.authToken}`, {
additionalProperties: {
skipAddingAuthToken: true,
},
});
await $.auth.set({
screenName: $.auth.data.screenName,
authToken: $.auth.data.authToken,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,10 @@
const addAuthToken = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthToken)
return requestConfig;
requestConfig.url = requestConfig.url + `?key=${$.auth.data.authToken}`;
return requestConfig;
};
export default addAuthToken;

View File

@@ -0,0 +1,11 @@
const setBaseUrl = ($, requestConfig) => {
const subdomain = $.auth.data.instanceUrl;
if (subdomain) {
requestConfig.baseURL = `https://${subdomain}.airbrake.io`;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -0,0 +1,3 @@
import listProjects from './list-projects/index.js';
export default [listProjects];

View File

@@ -0,0 +1,23 @@
export default {
name: 'List projects',
key: 'listProjects',
async run($) {
const projects = {
data: [],
};
const { data } = await $.http.get('/api/v4/projects');
if (data.projects.length) {
for (const project of data.projects) {
projects.data.push({
value: project.id,
name: project.name,
});
}
}
return projects;
},
};

View File

@@ -0,0 +1,21 @@
import defineApp from '../../helpers/define-app.js';
import setBaseUrl from './common/set-base-url.js';
import auth from './auth/index.js';
import addAuthToken from './common/add-auth-token.js';
import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js';
export default defineApp({
name: 'Airbrake',
key: 'airbrake',
iconUrl: '{BASE_URL}/apps/airbrake/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/airbrake/connection',
supportsConnections: true,
baseUrl: 'https://www.airbrake.io',
apiBaseUrl: '',
primaryColor: 'f58c54',
beforeRequest: [setBaseUrl, addAuthToken],
auth,
triggers,
dynamicData,
});

View File

@@ -0,0 +1,3 @@
import newErrors from './new-errors/index.js';
export default [newErrors];

View File

@@ -0,0 +1,66 @@
//import { URLSearchParams } from 'node:url';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New errors',
key: 'newErrors',
pollInterval: 15,
description: 'Triggers when a new error occurs.',
arguments: [
{
label: 'Project',
key: 'projectId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listProjects',
},
],
},
},
],
async run($) {
const projectId = $.step.parameters.projectId;
const params = {
limit: 100,
page: 1,
};
let next = false;
do {
const { data } = await $.http.get(
`/api/v4/projects/${projectId}/groups`,
{ params }
);
if (data.count > params.limit) {
params.page = params.page + 1;
next = true;
} else {
next = false;
}
if (!data?.groups?.length) {
return;
}
for (const group of data.groups) {
$.pushTriggerItem({
raw: group,
meta: {
internalId: group.id,
},
});
}
} while (next);
},
});

View File

@@ -1,6 +0,0 @@
import appConfig from '../../../../config/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, { version: appConfig.version });
};

View File

@@ -1,26 +0,0 @@
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
describe('GET /api/v1/automatisch/version', () => {
it('should return Automatisch version', async () => {
const response = await request(app)
.get('/api/v1/automatisch/version')
.expect(200);
const expectedPayload = {
data: {
version: '0.10.0',
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,5 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, request.currentUser);
};

View File

@@ -1,26 +0,0 @@
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 { createUser } from '../../../../../test/factories/user';
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
describe('GET /api/v1/users/me', () => {
let role, currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
role = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return current user info', async () => {
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', token)
.expect(200);
const expectedPayload = getCurrentUserMock(currentUser, role);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,12 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const inTrial = await request.currentUser.inTrial();
const trialInfo = {
inTrial,
expireAt: request.currentUser.trialExpiryDate,
};
renderObject(response, trialInfo);
};

View File

@@ -1,38 +0,0 @@
import { vi, 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.js';
import { createUser } from '../../../../../test/factories/user.js';
import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js';
import appConfig from '../../../../config/app.js';
import { DateTime } from 'luxon';
import User from '../../../../models/user.js';
describe('GET /api/v1/users/:userId/trial', () => {
let user, token;
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
describe('should return in trial, active subscription and expire at info', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true);
});
it('should return null', async () => {
const response = await request(app)
.get(`/api/v1/users/${user.id}/trial`)
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

@@ -1,16 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
export default async (request, response) => {
const user = await User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.findById(request.params.userId)
.throwIfNotFound();
renderObject(response, user);
};

View File

@@ -1,36 +0,0 @@
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 { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission';
import getUserMock from '../../../../../test/mocks/rest/api/v1/users/get-user';
describe('GET /api/v1/users/:userId', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
anotherUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUserRole = await anotherUser.$relatedQuery('role');
await createPermission({
roleId: currentUserRole.id,
action: 'read',
subject: 'User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified user info', async () => {
const response = await request(app)
.get(`/api/v1/users/${anotherUser.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,18 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const usersQuery = User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
const users = await paginateRest(usersQuery, request.query.page);
renderObject(response, users);
};

View File

@@ -1,56 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createPermission } from '../../../../../test/factories/permission';
import { createUser } from '../../../../../test/factories/user';
import getUsersMock from '../../../../../test/mocks/rest/api/v1/users/get-users';
describe('GET /api/v1/users', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole({
key: 'currentUser',
name: 'Current user role',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: currentUserRole.id,
});
currentUser = await createUser({
roleId: currentUserRole.id,
fullName: 'Current User',
});
anotherUserRole = await createRole({
key: 'anotherUser',
name: 'Another user role',
});
anotherUser = await createUser({
roleId: anotherUserRole.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return users data', async () => {
const response = await request(app)
.get('/api/v1/users')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUsersMock(
[anotherUser, currentUser],
[anotherUserRole, currentUserRole]
);
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -1,3 +0,0 @@
export default async (request, response) => {
response.status(200).end();
};

View File

@@ -1,9 +0,0 @@
import { describe, it } from 'vitest';
import request from 'supertest';
import app from '../../app.js';
describe('GET /healthcheck', () => {
it('should return 200 response with version data', async () => {
await request(app).get('/healthcheck').expect(200);
});
});

View File

@@ -20,8 +20,7 @@ export const isAuthenticated = async (_parent, _args, req) => {
.withGraphFetched({
role: true,
permissions: true,
})
.throwIfNotFound();
});
return true;
} catch (error) {
@@ -29,14 +28,6 @@ export const isAuthenticated = async (_parent, _args, req) => {
}
};
export const authenticateUser = async (request, response, next) => {
if (await isAuthenticated(null, null, request)) {
next();
} else {
return response.status(401).end();
}
};
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {

View File

@@ -1,8 +1,11 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { allow } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user.js';
import { isAuthenticated, authenticationRules } from './authentication.js';
import { createUser } from '../../test/factories/user.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
vi.mock('jsonwebtoken');
vi.mock('../models/user.js');
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
@@ -11,25 +14,28 @@ describe('isAuthenticated', () => {
});
it('should return false if token is invalid', async () => {
jwt.verify.mockImplementation(() => {
throw new Error('invalid token');
});
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid and there is a user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
it('should return true if token is valid', async () => {
jwt.verify.mockReturnValue({ userId: '123' });
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(true);
User.query.mockReturnValue({
findById: vi.fn().mockReturnValue({
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi
.fn()
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
}),
});
it('should return false if token is valid and but there is no user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
await user.$query().delete();
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(false);
const req = { headers: { authorization: 'validToken' } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
});

View File

@@ -1,22 +0,0 @@
const authorizationList = {
'/api/v1/users/:userId': {
action: 'read',
subject: 'User',
},
'/api/v1/users/': {
action: 'read',
subject: 'User',
},
};
export const authorizeUser = async (request, response, next) => {
const currentRoute = request.baseUrl + request.route.path;
const currentRouteRule = authorizationList[currentRoute];
try {
request.currentUser.can(currentRouteRule.action, currentRouteRule.subject);
next();
} catch (error) {
return response.status(403).end();
}
};

View File

@@ -1,11 +0,0 @@
import appConfig from '../config/app.js';
export const checkIsCloud = async (request, response, next) => {
if (appConfig.isCloud) {
next();
} else {
return response.status(404).end();
}
};
export default checkIsCloud;

View File

@@ -1,25 +0,0 @@
const paginateRest = async (query, page) => {
const pageSize = 10;
page = parseInt(page, 10);
if (isNaN(page) || page < 1) {
page = 1;
}
const [records, count] = await Promise.all([
query.limit(pageSize).offset((page - 1) * pageSize),
query.resultSize(),
]);
return {
pageInfo: {
currentPage: page,
totalPages: Math.ceil(count / pageSize),
},
totalCount: count,
records,
};
};
export default paginateRest;

View File

@@ -1,42 +0,0 @@
import serializers from '../serializers/index.js';
const isPaginated = (object) =>
object?.pageInfo &&
object?.totalCount !== undefined &&
Array.isArray(object?.records);
const isArray = (object) =>
Array.isArray(object) || Array.isArray(object?.records);
const totalCount = (object) =>
isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1;
const renderObject = (response, object) => {
let data = isPaginated(object) ? object.records : object;
const type = isPaginated(object)
? object.records[0].constructor.name
: object.constructor.name;
const serializer = serializers[type];
if (serializer) {
data = Array.isArray(data)
? data.map((item) => serializer(item))
: serializer(data);
}
const computedPayload = {
data,
meta: {
type,
count: totalCount(object),
isArray: isArray(object),
currentPage: isPaginated(object) ? object.pageInfo.currentPage : null,
totalPages: isPaginated(object) ? object.pageInfo.totalPages : null,
},
};
return response.json(computedPayload);
};
export { renderObject };

View File

@@ -1,8 +0,0 @@
import { Router } from 'express';
import versionAction from '../../../controllers/api/v1/automatisch/version.js';
const router = Router();
router.get('/version', versionAction);
export default router;

View File

@@ -1,22 +0,0 @@
import { Router } from 'express';
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 getUserAction from '../../../controllers/api/v1/users/get-user.js';
import getUsersAction from '../../../controllers/api/v1/users/get-users.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
const router = Router();
router.get('/', authenticateUser, authorizeUser, getUsersAction);
router.get('/me', authenticateUser, getCurrentUserAction);
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
router.get(
'/:userId/trial',
authenticateUser,
checkIsCloud,
getUserTrialAction
);
export default router;

View File

@@ -1,8 +0,0 @@
import { Router } from 'express';
import indexAction from '../controllers/healthcheck/index.js';
const router = Router();
router.get('/', indexAction);
export default router;

View File

@@ -2,17 +2,11 @@ import { Router } from 'express';
import graphQLInstance from '../helpers/graphql-instance.js';
import webhooksRouter from './webhooks.js';
import paddleRouter from './paddle.ee.js';
import healthcheckRouter from './healthcheck.js';
import automatischRouter from './api/v1/automatisch.js';
import usersRouter from './api/v1/users.js';
const router = Router();
router.use('/graphql', graphQLInstance);
router.use('/webhooks', webhooksRouter);
router.use('/paddle', paddleRouter);
router.use('/healthcheck', healthcheckRouter);
router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/users', usersRouter);
export default router;

View File

@@ -1,11 +0,0 @@
import userSerializer from './user.js';
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
const serializers = {
User: userSerializer,
Role: roleSerializer,
Permission: permissionSerializer,
};
export default serializers;

View File

@@ -1,13 +0,0 @@
const permissionSerializer = (permission) => {
return {
id: permission.id,
roleId: permission.roleId,
action: permission.action,
subject: permission.subject,
conditions: permission.conditions,
createdAt: permission.createdAt,
updatedAt: permission.updatedAt,
};
};
export default permissionSerializer;

View File

@@ -1,13 +0,0 @@
const roleSerializer = (role) => {
return {
id: role.id,
name: role.name,
key: role.key,
description: role.description,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isAdmin: role.isAdmin,
};
};
export default roleSerializer;

View File

@@ -1,32 +0,0 @@
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
import appConfig from '../config/app.js';
const userSerializer = (user) => {
let userData = {
id: user.id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
fullName: user.fullName,
roleId: user.roleId,
};
if (user.role) {
userData.role = roleSerializer(user.role);
}
if (user.permissions) {
userData.permissions = user.permissions.map((permission) =>
permissionSerializer(permission)
);
}
if (appConfig.isCloud && user.trialExpiryDate) {
userData.trialExpiryDate = user.trialExpiryDate;
}
return userData;
};
export default userSerializer;

View File

@@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker';
import Config from '../../src/models/config';
export const createConfig = async (params = {}) => {
const configData = {
@@ -7,7 +6,10 @@ export const createConfig = async (params = {}) => {
value: params?.value || { data: 'sampleConfig' },
};
const config = await Config.query().insert(configData).returning('*');
const [config] = await global.knex
.table('config')
.insert(configData)
.returning('*');
return config;
};

View File

@@ -1,6 +1,5 @@
import appConfig from '../../src/config/app';
import { AES } from 'crypto-js';
import Connection from '../../src/models/connection';
export const createConnection = async (params = {}) => {
params.key = params?.key || 'deepl';
@@ -17,7 +16,10 @@ export const createConnection = async (params = {}) => {
appConfig.encryptionKey
).toString();
const connection = await Connection.query().insert(params).returning('*');
const [connection] = await global.knex
.table('connections')
.insert(params)
.returning('*');
return connection;
};

View File

@@ -1,4 +1,3 @@
import ExecutionStep from '../../src/models/execution-step';
import { createExecution } from './execution';
import { createStep } from './step';
@@ -9,7 +8,8 @@ export const createExecutionStep = async (params = {}) => {
params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
const executionStep = await ExecutionStep.query()
const [executionStep] = await global.knex
.table('executionSteps')
.insert(params)
.returning('*');

View File

@@ -1,4 +1,3 @@
import Execution from '../../src/models/execution';
import { createFlow } from './flow';
export const createExecution = async (params = {}) => {
@@ -7,7 +6,10 @@ export const createExecution = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const execution = await Execution.query().insert(params).returning('*');
const [execution] = await global.knex
.table('executions')
.insert(params)
.returning('*');
return execution;
};

View File

@@ -1,4 +1,3 @@
import Flow from '../../src/models/flow';
import { createUser } from './user';
export const createFlow = async (params = {}) => {
@@ -7,7 +6,7 @@ export const createFlow = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const flow = await Flow.query().insert(params).returning('*');
const [flow] = await global.knex.table('flows').insert(params).returning('*');
return flow;
};

View File

@@ -1,4 +1,3 @@
import Permission from '../../src/models/permission';
import { createRole } from './role';
export const createPermission = async (params = {}) => {
@@ -7,7 +6,10 @@ export const createPermission = async (params = {}) => {
params.subject = params?.subject || 'User';
params.conditions = params?.conditions || ['isCreator'];
const permission = await Permission.query().insert(params).returning('*');
const [permission] = await global.knex
.table('permissions')
.insert(params)
.returning('*');
return permission;
};

View File

@@ -1,10 +1,8 @@
import Role from '../../src/models/role';
export const createRole = async (params = {}) => {
params.name = params?.name || 'Viewer';
params.key = params?.key || 'viewer';
const role = await Role.query().insert(params).returning('*');
const [role] = await global.knex.table('roles').insert(params).returning('*');
return role;
};

View File

@@ -1,4 +1,3 @@
import Step from '../../src/models/step';
import { createFlow } from './flow';
export const createStep = async (params = {}) => {
@@ -17,7 +16,7 @@ export const createStep = async (params = {}) => {
params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
const step = await Step.query().insert(params).returning('*');
const [step] = await global.knex.table('steps').insert(params).returning('*');
return step;
};

View File

@@ -1,6 +1,5 @@
import { createRole } from './role';
import { faker } from '@faker-js/faker';
import User from '../../src/models/user';
export const createUser = async (params = {}) => {
params.roleId = params?.roleId || (await createRole()).id;
@@ -8,7 +7,7 @@ export const createUser = async (params = {}) => {
params.email = params?.email || faker.internet.email();
params.password = params?.password || faker.internet.password();
const user = await User.query().insert(params).returning('*');
const [user] = await global.knex.table('users').insert(params).returning('*');
return user;
};

View File

@@ -1,32 +0,0 @@
const getCurrentUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
permissions: [],
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getCurrentUserMock;

View File

@@ -1,17 +0,0 @@
const getUserTrialMock = async (currentUser) => {
return {
data: {
inTrial: await currentUser.inTrial(),
expireAt: currentUser.trialExpiryDate.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
};
export default getUserTrialMock;

View File

@@ -1,31 +0,0 @@
const getUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getUserMock;

View File

@@ -1,38 +0,0 @@
const getUsersMock = async (users, roles) => {
const data = users.map((user) => {
const role = roles.find((r) => r.id === user.roleId);
return {
createdAt: user.createdAt.toISOString(),
email: user.email,
fullName: user.fullName,
id: user.id,
role: role
? {
createdAt: role.createdAt.toISOString(),
description: role.description,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
}
: null, // Fallback to null if role not found
roleId: user.roleId,
trialExpiryDate: user.trialExpiryDate.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
});
return {
data: data,
meta: {
count: data.length,
currentPage: 1,
isArray: true,
totalPages: 1,
type: 'User',
},
};
};
export default getUsersMock;

View File

@@ -32,6 +32,15 @@ export default defineConfig({
],
sidebar: {
'/apps/': [
{
text: 'Airbrake',
collapsible: true,
collapsed: true,
items: [
{ text: 'Triggers', link: '/apps/airbrake/triggers' },
{ text: 'Connection', link: '/apps/airbrake/connection' },
],
},
{
text: 'Carbone',
collapsible: true,
@@ -305,7 +314,7 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/removebg/actions' },
{ text: 'Connection', link: '/apps/removebg/connection' }
{ text: 'Connection', link: '/apps/removebg/connection' },
],
},
{

View File

@@ -0,0 +1,13 @@
# Airbrake
:::info
This page explains the steps you need to follow to set up the Airbrake
connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Login to your Airbrake account: [https://www.airbrake.io/](https://www.airbrake.io/).
2. Go to your profile & notifications page.
3. Copy `Auth Token` from the page to the `Auth Token` field on Automatisch.
4. Fill the instance URL field with your subdomain. (https://{yoursubdomain}.airbrake.io)
5. Write any screen name to be displayed in Automatisch.
6. Now, you can start using the Airbrake connection with Automatisch.

View File

@@ -0,0 +1,12 @@
---
favicon: /favicons/airbrake.svg
items:
- name: New errors
desc: Triggers when a new error occurs.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -3,6 +3,7 @@
The following integrations are currently supported by Automatisch.
- [Carbone](/apps/carbone/actions)
- [Airbrake](/apps/airbrake/triggers)
- [DeepL](/apps/deepl/actions)
- [Delay](/apps/delay/actions)
- [Discord](/apps/discord/actions)

View File

@@ -0,0 +1 @@
<svg height="255" preserveAspectRatio="xMidYMid" viewBox="0 0 256 255" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m128.636514 155.746615v-155.23361889h-3.522242v.06873152l-124.60824865 64.03287157v60.8642488h.00597665v3.234366h-.00597665v60.868233l124.60824865 64.747082h3.842989v-98.581914z" fill="#ff8e4a"/><path d="m129.941416 254.328529 125.568498-64.747082v-124.9668478l-125.887253-64.10160309h-2.243237v253.81055289h2.243237" fill="#f48746"/><path d="m109.097837 87.2551595h36.19561v59.2077195h-36.19561z" fill="#ff8e4a"/><path d="m66.1735097 188.397074h14.8639378c9.4102412 0 12.6087471-2.238257 15.6189883-9.988981l8.2796572-21.353587h45.159596l8.280653 21.353587c3.011238 7.750724 6.396016 9.988981 15.805261 9.988981h14.677665v-19.114335h-3.011237c-3.19751 0-4.704622-.689307-5.831222-3.790194l-39.516638-99.3658524h-25.779299l-39.703907 99.3658524c-1.1285915 3.100887-2.632716 3.790194-5.833214 3.790194h-3.0102413zm44.4075333-49.939922 11.478163-30.655253c2.445448-6.714771 5.269417-18.2556889 5.269417-18.2556889h.375533s2.822972 11.5409179 5.269416 18.2556889l11.478163 30.655253z" fill="#fff"/><path d="m231.204856 150.082739v-51.8086223c.235082 4.5233303 2.970397 16.8432063 24.305058 27.8512063v11.653479zm0-53.1623343v1.353712c-.029883-.5926848-.01793-1.0479066 0-1.353712zm.041837-.4392841s-.022911.1534008-.041837.4392841v-.4392841z" fill="#d4763c"/><path d="m231.155051 94.3016342c-.013946.9931207.05877 1.8945993.049805 2.0460078-.01793.2480312-2.220327 16.094132 24.305058 29.777681v-60.863253c-23.325883 12.0349884-24.449494 25.7414475-24.354863 29.0395642" fill="#ff8e4a"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -3952,6 +3952,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz"
integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==
"@types/node@>=8.1.0":
version "18.14.5"
resolved "https://registry.npmjs.org/@types/node/-/node-18.14.5.tgz"
integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw==
"@types/node@^12.0.0":
version "12.20.42"
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.42.tgz"
@@ -6367,6 +6372,19 @@ cookiejar@^2.1.4:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
copyfiles@^2.4.1:
version "2.4.1"
resolved "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz"
integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==
dependencies:
glob "^7.0.5"
minimatch "^3.0.3"
mkdirp "^1.0.4"
noms "0.0.0"
through2 "^2.0.1"
untildify "^4.0.0"
yargs "^16.1.0"
core-js-compat@^3.20.0, core-js-compat@^3.20.2:
version "3.20.3"
resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz"
@@ -8637,7 +8655,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.0"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -8773,6 +8791,11 @@ graphql-tools@^8.2.0:
optionalDependencies:
"@apollo/client" "~3.2.5 || ~3.3.0 || ~3.4.0"
graphql-type-json@^0.3.2:
version "0.3.2"
resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz"
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
graphql@^15.6.0:
version "15.8.0"
resolved "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz"
@@ -9226,7 +9249,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -9648,6 +9671,11 @@ is-yarn-global@^0.3.0:
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
@@ -11032,6 +11060,13 @@ minimatch@3.0.4, minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimatch@^3.0.3:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.0"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz"
@@ -11458,10 +11493,10 @@ node-releases@^2.0.1:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz"
integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
nodemailer@6.9.9:
version "6.9.9"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.9.tgz#4549bfbf710cc6addec5064dd0f19874d24248d9"
integrity sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==
nodemailer@6.7.0:
version "6.7.0"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.0.tgz"
integrity sha512-AtiTVUFHLiiDnMQ43zi0YgkzHOEWUkhDgPlBXrsDzJiJvB29Alo4OKxHQ0ugF3gRqRQIneCLtZU3yiUo7pItZw==
nodemon@^2.0.13:
version "2.0.15"
@@ -11479,6 +11514,14 @@ nodemon@^2.0.13:
undefsafe "^2.0.5"
update-notifier "^5.1.0"
noms@0.0.0:
version "0.0.0"
resolved "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz"
integrity sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=
dependencies:
inherits "^2.0.1"
readable-stream "~1.0.31"
nopt@^4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz"
@@ -13592,6 +13635,16 @@ readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@~1.0.31:
version "1.0.34"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readdir-scoped-modules@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz"
@@ -14718,6 +14771,11 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
@@ -14804,6 +14862,14 @@ strip-literal@^1.3.0:
dependencies:
acorn "^8.10.0"
stripe@^11.13.0:
version "11.13.0"
resolved "https://registry.npmjs.org/stripe/-/stripe-11.13.0.tgz"
integrity sha512-Jx0nDbdvRsTtDSX5OFQ+4rLmYIftoiOE9HAXWIgyhAz1QjRFI3UIiJ/kCyhkdJBoHu019O5Ya6EmQ5Zf635XDw==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.11.0"
strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz"
@@ -15101,7 +15167,7 @@ throat@^6.0.1:
resolved "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz"
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
through2@^2.0.0:
through2@^2.0.0, through2@^2.0.1:
version "2.0.5"
resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz"
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
@@ -15506,6 +15572,11 @@ unquote@~1.1.1:
resolved "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz"
integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
untildify@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
upath@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz"
@@ -16425,7 +16496,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3:
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs@^16.2.0:
yargs@^16.1.0, yargs@^16.2.0:
version "16.2.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz"
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==