Compare commits

..

1 Commits

Author SHA1 Message Date
Ali BARIN
7dee26c16d chore: allow custom seed user creds 2024-03-07 08:41:30 +00:00
392 changed files with 5226 additions and 7445 deletions

View File

@@ -1,32 +0,0 @@
name: Automatisch Docs Change
on:
pull_request:
paths:
- 'packages/docs/**'
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Label PR
uses: actions/github-script@v6
with:
script: |
const { pull_request } = context.payload;
const label = 'documentation-change';
const hasLabel = pull_request.labels.some(({ name }) => name === label);
if (!hasLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull_request.number,
labels: [label],
});
console.log(`Label "${label}" added to PR #${pull_request.number}`);
} else {
console.log(`Label "${label}" already exists on PR #${pull_request.number}`);
}

View File

@@ -18,8 +18,8 @@ async function fetchAdminRole() {
}
export async function createUser(
email = 'user@automatisch.io',
password = 'sample'
email = appConfig.seedUserEmail,
password = appConfig.seedUserPassword
) {
const UNIQUE_VIOLATION_CODE = '23505';

View File

@@ -1,116 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create Cloud Firestore document',
key: 'createCloudFirestoreDocument',
description: 'Creates a new document within a Cloud Firestore collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
{
label: 'Convert Numerics',
key: 'convertNumerics',
type: 'dropdown',
required: false,
description:
"If any value represents a valid numerical value, whether it's an integer or a floating-point number, this field directs the database to store it as a numeric data type instead of a string.",
variables: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
{
label: 'Document ID',
key: 'documentId',
type: 'string',
required: false,
description:
'The document ID to use for this document. If not specified, an ID will be assigned.',
variables: true,
},
{
label: 'Document Data',
key: 'documentData',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: false,
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
const projectId = $.auth.data.projectId;
const { collectionId, documentId, documentData, convertNumerics } =
$.step.parameters;
const documentDataObject = documentData.reduce((result, entry) => {
const key = entry.key?.toLowerCase();
const value = entry.value;
const isNumber = !isNaN(parseFloat(value));
if (key && value) {
const formattedValue =
convertNumerics && isNumber
? { integerValue: parseFloat(value) }
: { stringValue: value };
return {
...result,
[key]: formattedValue,
};
}
return result;
}, {});
const body = {
fields: documentDataObject,
};
const { data } = await $.http.post(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}?documentId=${documentId}`,
body,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,100 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create Firebase Realtime Database Record',
key: 'createFirebaseRealtimeDatabaseRecord',
description: 'Creates a child object within your Firebase Realtime Database.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here'.",
variables: true,
},
{
label: 'Convert Numerics',
key: 'convertNumerics',
type: 'dropdown',
required: false,
description:
"If any value represents a valid numerical value, whether it's an integer or a floating-point number, this field directs the database to store it as a numeric data type instead of a string.",
variables: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
{
label: 'New ID',
key: 'newId',
type: 'string',
required: false,
description:
'The key to use for this object, or leave it blank for Firebase to create one automatically.',
variables: true,
},
{
label: 'Data',
key: 'childData',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: false,
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
let path = $.step.parameters.path;
const { convertNumerics, newId, childData } = $.step.parameters;
if (newId) {
path = `${path}/${newId}.json`;
} else {
path = `${path}.json`;
}
const formattedChildObjectData = childData.reduce((result, entry) => {
const key = entry?.key;
const value = entry?.value;
const isNumber = !isNaN(parseFloat(value));
if (isNumber && convertNumerics) {
result[key] = parseFloat(value);
} else {
result[key] = value;
}
return result;
}, {});
const body = formattedChildObjectData;
const { data } = await $.http.post(path, body, {
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,53 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find Cloud Firestore document',
key: 'findCloudFirestoreDocument',
description: 'Finds a document within a collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
{
label: 'Document ID',
key: 'documentId',
type: 'string',
required: true,
description: '',
variables: true,
},
],
async run($) {
const projectId = $.auth.data.projectId;
const { collectionId, documentId } = $.step.parameters;
const { data } = await $.http.get(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}/${documentId}`,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,32 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find Firebase Realtime Database Record',
key: 'findFirebaseRealtimeDatabaseRecord',
description: 'Finds a child object in Firebase Realtime Database.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here'.",
variables: true,
},
],
async run($) {
const { path } = $.step.parameters;
const { data } = await $.http.get(`${path}.json`, {
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,11 +0,0 @@
import createCloudFirestoreDocument from './create-cloud-firestore-document/index.js';
import createFirebaseRealtimeDatabaseRecord from './create-firebase-realtime-database-record/index.js';
import findCloudFirestoreDocument from './find-cloud-firestore-document/index.js';
import findFirebaseRealtimeDatabaseRecord from './find-firebase-realtime-database-record/index.js';
export default [
createCloudFirestoreDocument,
createFirebaseRealtimeDatabaseRecord,
findCloudFirestoreDocument,
findFirebaseRealtimeDatabaseRecord,
];

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,23 +0,0 @@
import { URLSearchParams } from 'url';
import authScope from '../common/auth-scope.js';
export default async function generateAuthUrl($) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const searchParams = new URLSearchParams({
client_id: $.auth.data.clientId,
redirect_uri: redirectUri,
prompt: 'select_account',
scope: authScope.join(' '),
response_type: 'code',
access_type: 'offline',
});
const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`;
await $.auth.set({
url,
});
}

View File

@@ -1,71 +0,0 @@
import generateAuthUrl from './generate-auth-url.js';
import verifyCredentials from './verify-credentials.js';
import refreshToken from './refresh-token.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/firebase/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in Google Cloud, enter the URL above.',
clickToCopy: true,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'projectId',
label: 'Project ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'The project id of your Firebase project',
clickToCopy: false,
},
{
key: 'realtimeDatabaseId',
label: 'Realtime Database Domain',
type: 'string',
required: false,
readOnly: false,
value: null,
placeholder: null,
description:
'If you want to use Realtime Database, please provide the domain of your Realtime Database (https://{{domain}}.firebaseio.com)',
clickToCopy: false,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -1,8 +0,0 @@
import getCurrentUser from '../common/get-current-user.js';
const isStillVerified = async ($) => {
const currentUser = await getCurrentUser($);
return !!currentUser.resourceName;
};
export default isStillVerified;

View File

@@ -1,31 +0,0 @@
import { URLSearchParams } from 'node:url';
import authScope from '../common/auth-scope.js';
const refreshToken = async ($) => {
const params = new URLSearchParams({
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
grant_type: 'refresh_token',
refresh_token: $.auth.data.refreshToken,
});
const { data } = await $.http.post(
'https://oauth2.googleapis.com/token',
params.toString(),
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessToken: data.access_token,
expiresIn: data.expires_in,
scope: authScope.join(' '),
tokenType: data.token_type,
});
};
export default refreshToken;

View File

@@ -1,50 +0,0 @@
import getCurrentUser from '../common/get-current-user.js';
const verifyCredentials = async ($) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const { data } = await $.http.post(
`https://oauth2.googleapis.com/token`,
{
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
code: $.auth.data.code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
},
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessToken: data.access_token,
tokenType: data.token_type,
});
const currentUser = await getCurrentUser($);
const { displayName } = currentUser.names.find(
(name) => name.metadata.primary
);
const { value: email } = currentUser.emailAddresses.find(
(emailAddress) => emailAddress.metadata.primary
);
await $.auth.set({
clientId: $.auth.data.clientId,
clientSecret: $.auth.data.clientSecret,
scope: $.auth.data.scope,
idToken: data.id_token,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
resourceName: currentUser.resourceName,
screenName: `${displayName} - ${email}`,
});
};
export default verifyCredentials;

View File

@@ -1,9 +0,0 @@
const addAuthHeader = ($, requestConfig) => {
if ($.auth.data?.accessToken) {
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -1,9 +0,0 @@
const authScope = [
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/firebase.database',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
];
export default authScope;

View File

@@ -1,13 +0,0 @@
const getCurrentUser = async ($) => {
const { data: currentUser } = await $.http.get(
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses',
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
return currentUser;
};
export default getCurrentUser;

View File

@@ -1,16 +0,0 @@
const setBaseUrl = ($, requestConfig) => {
const realtimeDatabaseId = $.auth.data.realtimeDatabaseId;
if (requestConfig.additionalProperties?.skipAddingAuthHeader)
return requestConfig;
if (requestConfig.additionalProperties?.setFirestoreBaseUrl) {
requestConfig.baseURL = 'https://firestore.googleapis.com';
} else {
requestConfig.baseURL = `https://${realtimeDatabaseId}.firebaseio.com`;
}
return requestConfig;
};
export default setBaseUrl;

View File

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

View File

@@ -1,32 +0,0 @@
export default {
name: 'List firestore collections',
key: 'listFirestoreCollections',
async run($) {
const firestoreCollections = {
data: [],
};
const projectId = $.auth.data.projectId;
const { data } = await $.http.post(
`/v1/projects/${projectId}/databases/(default)/documents:listCollectionIds`,
null,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
if (data.collectionIds?.length) {
for (const collectionId of data.collectionIds) {
firestoreCollections.data.push({
value: collectionId,
name: collectionId,
});
}
}
return firestoreCollections;
},
};

View File

@@ -1,23 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import setBaseUrl from './common/set-base-url.js';
import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Firebase',
key: 'firebase',
baseUrl: 'https://firebase.google.com',
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/firebase/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/firebase/connection',
primaryColor: 'FFA000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,
dynamicData,
actions,
});

View File

@@ -1,7 +0,0 @@
import newChildObjectInFirebaseRealtimeDatabase from './new-child-object-in-a-firebase-realtime-database/index.js';
import newDocumentsWithinFirestoreCollection from './new-documents-within-firestore-collection/index.js';
export default [
newChildObjectInFirebaseRealtimeDatabase,
newDocumentsWithinFirestoreCollection,
];

View File

@@ -1,75 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New child object in a firebase realtime database',
key: 'newChildObjectInFirebaseRealtimeDatabase',
pollInterval: 15,
description:
'Triggers when a new child object is generated within a specific path.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here.json'.",
variables: true,
},
{
label: 'Order',
key: 'order',
type: 'string',
required: false,
description:
'The key or path of the child that should be utilized for comparing objects/records. If unspecified, the order of $key is used.',
variables: true,
},
{
label: 'Location of newest objects',
key: 'locationOfNewestObjects',
type: 'dropdown',
required: false,
description:
'Specifies whether the new 100 records are positioned at the "top" or the "bottom" of the ordering. If left unspecified, the assumption is that the bottom/last result represents the "newest objects" (limitToLast).',
variables: true,
options: [
{ label: 'Top of results', value: 'limitToFirst' },
{ label: 'Bottom of results', value: 'limitToLast' },
],
},
],
async run($) {
const { path, order, locationOfNewestObjects } = $.step.parameters;
const params = {};
if (order) {
params.orderBy = `"${order}"`;
}
if (locationOfNewestObjects) {
params[`${locationOfNewestObjects}`] = 100;
}
const { data } = await $.http.get(path, {
params,
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
if (!data) {
return;
}
$.pushTriggerItem({
raw: data,
meta: {
internalId: Crypto.randomUUID(),
},
});
},
});

View File

@@ -1,66 +0,0 @@
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New documents within a firestore collection',
key: 'newDocumentsWithinFirestoreCollection',
pollInterval: 15,
description:
'Triggers when a new document is added within a Cloud Firestore collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
],
async run($) {
const projectId = $.auth.data.projectId;
const collectionId = $.step.parameters.collectionId;
const params = {
pageSize: 100,
pageToken: undefined,
};
do {
const { data } = await $.http.get(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}`,
{
params,
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
params.pageToken = data.nextPageToken;
if (!data?.documents?.length) {
return;
}
for (const document of data.documents) {
const nameParts = document.name.split('/');
const id = nameParts[nameParts.length - 1];
$.pushTriggerItem({
raw: document,
meta: {
internalId: id,
},
});
}
} while (params.pageToken);
},
});

View File

@@ -90,7 +90,7 @@ export default defineAction({
async run($) {
const method = $.step.parameters.method;
const data = $.step.parameters.data || null;
const data = $.step.parameters.data;
const url = $.step.parameters.url;
const headers = $.step.parameters.headers;
@@ -108,17 +108,14 @@ export default defineAction({
return result;
}, {});
let expectedResponseContentType = headersObject.accept;
let contentType = headersObject['content-type'];
// in case HEAD request is not supported by the URL
try {
const metadataResponse = await $.http.head(url, {
headers: headersObject,
});
if (!expectedResponseContentType) {
expectedResponseContentType = metadataResponse.headers['content-type'];
}
contentType = metadataResponse.headers['content-type'];
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
// eslint-disable-next-line no-empty
@@ -131,7 +128,7 @@ export default defineAction({
headers: headersObject,
};
if (!isPossiblyTextBased(expectedResponseContentType)) {
if (!isPossiblyTextBased(contentType)) {
requestData.responseType = 'arraybuffer';
}
@@ -141,7 +138,7 @@ export default defineAction({
let responseData = response.data;
if (!isPossiblyTextBased(expectedResponseContentType)) {
if (!isPossiblyTextBased(contentType)) {
responseData = Buffer.from(responseData).toString('base64');
}

View File

@@ -64,17 +64,32 @@ export default defineAction({
value: '1',
description:
'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listStages',
},
],
},
options: [
{
label: 'Qualified (Pipeline)',
value: 1,
},
{
label: 'Contact Made (Pipeline)',
value: 2,
},
{
label: 'Prospect Qualified (Pipeline)',
value: 3,
},
{
label: 'Needs Defined (Pipeline)',
value: 4,
},
{
label: 'Proposal Made (Pipeline)',
value: 5,
},
{
label: 'Negotiations Started (Pipeline)',
value: 6,
},
],
},
{
label: 'Owner',

View File

@@ -1,25 +1,23 @@
import listActivityTypes from './list-activity-types/index.js';
import listCurrencies from './list-currencies/index.js';
import listDeals from './list-deals/index.js';
import listLeadLabels from './list-lead-labels/index.js';
import listLeads from './list-leads/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js';
import listLeadLabels from './list-lead-labels/index.js';
import listOrganizations from './list-organizations/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js';
import listPersonLabelField from './list-person-label-field/index.js';
import listPersons from './list-persons/index.js';
import listStages from './list-stages/index.js';
import listUsers from './list-users/index.js';
export default [
listActivityTypes,
listCurrencies,
listDeals,
listLeadLabels,
listLeads,
listOrganizationLabelField,
listLeadLabels,
listOrganizations,
listOrganizationLabelField,
listPersonLabelField,
listPersons,
listStages,
listUsers,
];

View File

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

View File

@@ -1,3 +0,0 @@
import respondWith from './respond-with/index.js';
export default [respondWith];

View File

@@ -1,69 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Respond with',
key: 'respondWith',
description: 'Respond with defined JSON body.',
arguments: [
{
label: 'Status code',
key: 'statusCode',
type: 'string',
required: true,
variables: true,
value: '200',
},
{
label: 'Headers',
key: 'headers',
type: 'dynamic',
required: false,
description: 'Add or remove headers as needed',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: true,
description: 'Header key',
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: true,
description: 'Header value',
variables: true,
},
],
},
{
label: 'Body',
key: 'body',
type: 'string',
required: true,
description: 'The content of the response body.',
variables: true,
},
],
async run($) {
const statusCode = parseInt($.step.parameters.statusCode, 10);
const body = $.step.parameters.body;
const headers = $.step.parameters.headers.reduce((result, entry) => {
return {
...result,
[entry.key]: entry.value,
};
}, {});
$.setActionItem({
raw: {
headers,
body,
statusCode,
},
});
},
});

View File

@@ -1,5 +1,4 @@
import defineApp from '../../helpers/define-app.js';
import actions from './actions/index.js';
import triggers from './triggers/index.js';
export default defineApp({
@@ -11,6 +10,5 @@ export default defineApp({
baseUrl: '',
apiBaseUrl: '',
primaryColor: '0059F7',
actions,
triggers,
});

View File

@@ -7,20 +7,7 @@ export default defineTrigger({
key: 'catchRawWebhook',
type: 'webhook',
showWebhookUrl: true,
description:
'Triggers (immediately if configured) when the webhook receives a request.',
arguments: [
{
label: 'Wait until flow is done',
key: 'workSynchronously',
type: 'dropdown',
required: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
],
description: 'Triggers when the webhook receives a request.',
async run($) {
const dataItem = {

View File

@@ -94,6 +94,8 @@ const appConfig = {
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
seedUserEmail: process.env.SEED_USER_EMAIL || 'user@automatisch.io',
seedUserPassword: process.env.SEED_USER_PASSWORD || 'sample',
};
if (!appConfig.encryptionKey) {

View File

@@ -1,13 +0,0 @@
import User from '../../../../models/user.js';
import { renderObject, renderError } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const { email, password } = request.body;
const token = await User.authenticate(email, password);
if (token) {
return renderObject(response, { token });
}
renderError(response, [{ general: ['Incorrect email or password.'] }]);
};

View File

@@ -1,39 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import { createUser } from '../../../../../test/factories/user';
describe('POST /api/v1/access-tokens', () => {
beforeEach(async () => {
await createUser({
email: 'user@automatisch.io',
password: 'password',
});
});
it('should return the token data with correct credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'user@automatisch.io',
password: 'password',
})
.expect(200);
expect(response.body.data.token.length).toBeGreaterThan(0);
});
it('should return error with incorrect credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'incorrect@email.com',
password: 'incorrectpassword',
})
.expect(422);
expect(response.body.errors.general).toEqual([
'Incorrect email or password.',
]);
});
});

View File

@@ -4,7 +4,6 @@ import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ app_key: request.params.appKey })
.throwIfNotFound();
renderObject(response, appAuthClient);

View File

@@ -0,0 +1,52 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients/:appAuthClientId', () => {
let currentUser, currentUserRole, currentAppAuthClient, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/admin/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client UUID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});
});

View File

@@ -1,55 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, adminRole, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,10 +0,0 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -1,44 +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 { createRole } from '../../../../../../test/factories/role.js';
import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
let currentUser, adminRole, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,14 +0,0 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProvider = await SamlAuthProvider.query()
.findById(request.params.samlAuthProviderId)
.throwIfNotFound();
const roleMappings = await samlAuthProvider
.$relatedQuery('samlAuthProvidersRoleMappings')
.orderBy('remote_role_name', 'asc');
renderObject(response, roleMappings);
};

View File

@@ -1,51 +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 { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
import getRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappings', () => {
let roleMappingOne, roleMappingTwo, samlAuthProvider, currentUser, token;
beforeEach(async () => {
const role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
samlAuthProvider = await createSamlAuthProvider();
roleMappingOne = await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Admin',
});
roleMappingTwo = await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return role mappings', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get(
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getRoleMappingsMock([
roleMappingOne,
roleMappingTwo,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -6,7 +6,5 @@ export default async (request, response) => {
.findById(request.params.samlAuthProviderId)
.throwIfNotFound();
renderObject(response, samlAuthProvider, {
serializer: 'AdminSamlAuthProvider',
});
renderObject(response, samlAuthProvider);
};

View File

@@ -7,7 +7,5 @@ export default async (request, response) => {
'desc'
);
renderObject(response, samlAuthProviders, {
serializer: 'AdminSamlAuthProvider',
});
renderObject(response, samlAuthProviders);
};

View File

@@ -4,7 +4,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ app_key: request.params.appKey, active: true })
.where({ active: true })
.throwIfNotFound();
renderObject(response, appAuthClient);

View File

@@ -4,27 +4,25 @@ import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
describe('GET /api/v1/app-auth-clients/:id', () => {
let currentUser, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.get(`/api/v1/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
@@ -36,14 +34,14 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`)
.get(`/api/v1/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.get('/api/v1/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});

View File

@@ -1,10 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey, active: true })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -1,42 +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 getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/auth-clients', () => {
let currentUser, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,15 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import AppConfig from '../../../../models/app-config.js';
export default async (request, response) => {
const appConfig = await AppConfig.query()
.withGraphFetched({
appAuthClients: true,
})
.findOne({
key: request.params.appKey,
})
.throwIfNotFound();
renderObject(response, appConfig);
};

View File

@@ -1,44 +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 getAppConfigMock from '../../../../../test/mocks/rest/api/v1/apps/get-config.js';
import { createAppConfig } from '../../../../../test/factories/app-config.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/config', () => {
let currentUser, appConfig, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
appConfig = await createAppConfig({
key: 'deepl',
allowCustomConnection: true,
shared: true,
disabled: false,
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app config info', async () => {
const response = await request(app)
.get(`/api/v1/apps/${appConfig.key}/config`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppConfigMock(appConfig);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app key', async () => {
await request(app)
.get('/api/v1/apps/not-existing-app-key/config')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -1,24 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import App from '../../../../models/app.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
const connections = await request.currentUser.authorizedConnections
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': app.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
renderObject(response, connections);
};

View File

@@ -1,101 +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.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js';
describe('GET /api/v1/apps/:appKey/connections', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the connections data of specified app for current user', async () => {
const currentUserConnectionOne = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
const currentUserConnectionTwo = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
currentUserConnectionTwo,
currentUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return the connections data of specified app for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnectionOne = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
const anotherUserConnectionTwo = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
anotherUserConnectionTwo,
anotherUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid connection UUID', async () => {
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -1,23 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import App from '../../../../models/app.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
const flowsQuery = request.currentUser.authorizedFlows
.clone()
.joinRelated({
steps: true,
})
.withGraphFetched({
steps: true,
})
.where('steps.app_key', app.key)
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc');
const flows = await paginateRest(flowsQuery, request.query.page);
renderObject(response, flows);
};

View File

@@ -1,129 +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.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js';
describe('GET /api/v1/apps/:appKey/flows', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of specified app for current user', async () => {
const currentUserFlowOne = await createFlow({ userId: currentUser.id });
const triggerStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'trigger',
appKey: 'webhook',
});
const actionStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'action',
});
const currentUserFlowTwo = await createFlow({ userId: currentUser.id });
await createStep({
flowId: currentUserFlowTwo.id,
type: 'trigger',
appKey: 'github',
});
await createStep({
flowId: currentUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/apps/webhook/flows')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[currentUserFlowOne],
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the flows data of specified app for another user', async () => {
const anotherUser = await createUser();
const anotherUserFlowOne = await createFlow({ userId: anotherUser.id });
const triggerStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'trigger',
appKey: 'webhook',
});
const actionStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'action',
});
const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id });
await createStep({
flowId: anotherUserFlowTwo.id,
type: 'trigger',
appKey: 'github',
});
await createStep({
flowId: anotherUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/apps/webhook/flows')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[anotherUserFlowOne],
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/apps/invalid-app-key/flows')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -1,24 +0,0 @@
import appConfig from '../../../../config/app.js';
import Config from '../../../../models/config.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
let config = await Config.query().orderBy('key', 'asc');
config = config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
renderObject(response, config);
};

View File

@@ -1,51 +0,0 @@
import { vi, expect, describe, it } from 'vitest';
import request from 'supertest';
import { createConfig } from '../../../../../test/factories/config.js';
import app from '../../../../app.js';
import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/automatisch/config', () => {
it('should return Automatisch config', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const logoConfig = await createConfig({
key: 'logo.svgData',
value: { data: '<svg>Sample</svg>' },
});
const primaryDarkConfig = await createConfig({
key: 'palette.primary.dark',
value: { data: '#001F52' },
});
const primaryLightConfig = await createConfig({
key: 'palette.primary.light',
value: { data: '#4286FF' },
});
const primaryMainConfig = await createConfig({
key: 'palette.primary.main',
value: { data: '#0059F7' },
});
const titleConfig = await createConfig({
key: 'title',
value: { data: 'Sample Title' },
});
const response = await request(app)
.get('/api/v1/automatisch/config')
.expect(200);
const expectedPayload = configMock(
logoConfig,
primaryDarkConfig,
primaryLightConfig,
primaryMainConfig,
titleConfig
);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,14 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let connection = await request.currentUser.authorizedConnections
.clone()
.findOne({
id: request.params.connectionId,
})
.throwIfNotFound();
connection = await connection.testAndUpdateConnection();
renderObject(response, connection);
};

View File

@@ -1,123 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
describe('POST /api/v1/connections/:connectionId/test', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should update the connection as not verified for current user', async () => {
const currentUserConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/connections/${currentUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should update the connection as not verified for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/connections/${anotherUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should return not found response for not existing connection UUID', async () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post(`/api/v1/connections/${notExistingConnectionUUID}/test`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post('/api/v1/connections/invalidConnectionUUID/test')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,20 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const flowsQuery = request.currentUser.authorizedFlows
.clone()
.joinRelated({
steps: true,
})
.withGraphFetched({
steps: true,
})
.where('steps.connection_id', request.params.connectionId)
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc');
const flows = await paginateRest(flowsQuery, request.query.page);
renderObject(response, flows);
};

View File

@@ -1,128 +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.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js';
describe('GET /api/v1/connections/:connectionId/flows', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of specified connection for current user', async () => {
const currentUserFlowOne = await createFlow({ userId: currentUser.id });
const currentUserConnection = await createConnection({
userId: currentUser.id,
key: 'webhook',
});
const triggerStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'trigger',
appKey: 'webhook',
connectionId: currentUserConnection.id,
});
const actionStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'action',
});
const currentUserFlowTwo = await createFlow({ userId: currentUser.id });
await createStep({
flowId: currentUserFlowTwo.id,
type: 'trigger',
appKey: 'github',
});
await createStep({
flowId: currentUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/connections/${currentUserConnection.id}/flows`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[currentUserFlowOne],
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the flows data of specified connection for another user', async () => {
const anotherUser = await createUser();
const anotherUserFlowOne = await createFlow({ userId: anotherUser.id });
const anotherUserConnection = await createConnection({
userId: anotherUser.id,
key: 'webhook',
});
const triggerStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'trigger',
appKey: 'webhook',
connectionId: anotherUserConnection.id,
});
const actionStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'action',
});
const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id });
await createStep({
flowId: anotherUserFlowTwo.id,
type: 'trigger',
appKey: 'github',
});
await createStep({
flowId: anotherUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/connections/${anotherUserConnection.id}/flows`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[anotherUserFlowOne],
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -2,7 +2,6 @@ import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const execution = await request.currentUser.authorizedExecutions
.clone()
.withGraphFetched({
flow: {
steps: true,

View File

@@ -3,7 +3,6 @@ import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const executionsQuery = request.currentUser.authorizedExecutions
.clone()
.withSoftDeleted()
.orderBy('created_at', 'desc')
.withGraphFetched({

View File

@@ -42,12 +42,9 @@ describe('GET /api/v1/executions', () => {
const currentUserExecutionTwo = await createExecution({
flowId: currentUserFlow.id,
deletedAt: new Date().toISOString(),
});
await currentUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({
action: 'read',
subject: 'Execution',
@@ -90,12 +87,9 @@ describe('GET /api/v1/executions', () => {
const anotherUserExecutionTwo = await createExecution({
flowId: anotherUserFlow.id,
deletedAt: new Date().toISOString(),
});
await anotherUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({
action: 'read',
subject: 'Execution',

View File

@@ -2,7 +2,6 @@ import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const flow = await request.currentUser.authorizedFlows
.clone()
.withGraphJoined({ steps: true })
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': request.params.flowId })

View File

@@ -1,21 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const flowsQuery = request.currentUser.authorizedFlows
.clone()
.withGraphFetched({
steps: true,
})
.where((builder) => {
if (request.query.name) {
builder.where('flows.name', 'ilike', `%${request.query.name}%`);
}
})
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc');
const flows = await paginateRest(flowsQuery, request.query.page);
renderObject(response, flows);
};

View File

@@ -1,118 +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 { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js';
describe('GET /api/v1/flows', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of current user', async () => {
const currentUserFlowOne = await createFlow({ userId: currentUser.id });
const triggerStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'trigger',
});
const actionStepFlowOne = await createStep({
flowId: currentUserFlowOne.id,
type: 'action',
});
const currentUserFlowTwo = await createFlow({ userId: currentUser.id });
const triggerStepFlowTwo = await createStep({
flowId: currentUserFlowTwo.id,
type: 'trigger',
});
const actionStepFlowTwo = await createStep({
flowId: currentUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/flows')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[currentUserFlowTwo, currentUserFlowOne],
[
triggerStepFlowOne,
actionStepFlowOne,
triggerStepFlowTwo,
actionStepFlowTwo,
]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the flows data of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlowOne = await createFlow({ userId: anotherUser.id });
const triggerStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'trigger',
});
const actionStepFlowOne = await createStep({
flowId: anotherUserFlowOne.id,
type: 'action',
});
const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id });
const triggerStepFlowTwo = await createStep({
flowId: anotherUserFlowTwo.id,
type: 'trigger',
});
const actionStepFlowTwo = await createStep({
flowId: anotherUserFlowTwo.id,
type: 'action',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/flows')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowsMock(
[anotherUserFlowTwo, anotherUserFlowOne],
[
triggerStepFlowOne,
actionStepFlowOne,
triggerStepFlowTwo,
actionStepFlowTwo,
]
);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,12 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProviders = await SamlAuthProvider.query()
.where({
active: true,
})
.orderBy('created_at', 'desc');
renderObject(response, samlAuthProviders);
};

View File

@@ -1,30 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import { createSamlAuthProvider } from '../../../../../test/factories/saml-auth-provider.ee.js';
import getSamlAuthProvidersMock from '../../../../../test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/saml-auth-providers', () => {
let samlAuthProviderOne, samlAuthProviderTwo;
beforeEach(async () => {
samlAuthProviderOne = await createSamlAuthProvider();
samlAuthProviderTwo = await createSamlAuthProvider();
});
it('should return saml auth providers', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/saml-auth-providers')
.expect(200);
const expectedPayload = await getSamlAuthProvidersMock([
samlAuthProviderTwo,
samlAuthProviderOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,18 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.whereNotNull('steps.connection_id')
.first()
.throwIfNotFound();
const dynamicData = await step.createDynamicData(
request.body.dynamicDataKey,
request.body.parameters
);
renderObject(response, dynamicData);
};

View File

@@ -1,244 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createConnection } from '../../../../../test/factories/connection';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js';
import HttpError from '../../../../errors/http.js';
describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
describe('should return dynamically created data', () => {
let repositories;
beforeEach(async () => {
repositories = [
{
value: 'automatisch/automatisch',
name: 'automatisch/automatisch',
},
{
value: 'automatisch/sample',
name: 'automatisch/sample',
},
];
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
return {
data: repositories,
};
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
it('of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const connection = await createConnection({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
});
describe('should return error for dynamically created data', () => {
let errors;
beforeEach(async () => {
errors = {
message: 'Not Found',
documentation_url:
'https://docs.github.com/rest/users/users#get-a-user',
};
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
throw new HttpError({ message: errors });
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.errors).toEqual(errors);
});
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,17 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.first()
.throwIfNotFound();
const dynamicFields = await step.createDynamicFields(
request.body.dynamicFieldsKey,
request.body.parameters
);
renderObject(response, dynamicFields);
};

View File

@@ -1,169 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields';
describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return dynamically created fields of the current users step', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return dynamically created fields of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserflow = await createFlow({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,11 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.findById(request.params.stepId)
.throwIfNotFound();
const connection = await step.$relatedQuery('connection').throwIfNotFound();
renderObject(response, connection);
};

View File

@@ -1,121 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createConnection } from '../../../../../test/factories/connection';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import getConnectionMock from '../../../../../test/mocks/rest/api/v1/steps/get-connection';
describe('GET /api/v1/steps/:stepId/connection', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the current user connection data of specified step', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const currentUserConnection = await createConnection();
const triggerStep = await createStep({
flowId: currentUserflow.id,
connectionId: currentUserConnection.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/steps/${triggerStep.id}/connection`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionMock(currentUserConnection);
expect(response.body).toEqual(expectedPayload);
});
it('should return the current user connection data of specified step', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const anotherUserConnection = await createConnection();
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
connectionId: anotherUserConnection.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/steps/${triggerStep.id}/connection`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionMock(anotherUserConnection);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing step without connection', async () => {
const stepWithoutConnection = await createStep();
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get(`/api/v1/steps/${stepWithoutConnection.id}/connection`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingFlowUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingFlowUUID}/connection`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/steps/invalidFlowUUID/connection')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,27 +0,0 @@
import { ref } from 'objection';
import ExecutionStep from '../../../../models/execution-step.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.findOne({ 'steps.id': request.params.stepId })
.throwIfNotFound();
const previousSteps = await request.currentUser.authorizedSteps
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
renderObject(response, previousSteps);
};

View File

@@ -1,173 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
import { createPermission } from '../../../../../test/factories/permission';
import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps';
describe('GET /api/v1/steps/:stepId/previous-steps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the previous steps of the specified step of the current user', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const triggerStep = await createStep({
flowId: currentUserflow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the previous steps of the specified step of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingFlowUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/steps/invalidFlowUUID/previous-steps')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,7 +0,0 @@
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' });
};

View File

@@ -1,210 +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 { 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');
});
});

View File

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

View File

@@ -1,68 +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 { createSubscription } from '../../../../../test/factories/subscription.js';
import { createUsageData } from '../../../../../test/factories/usage-data.js';
import appConfig from '../../../../config/app.js';
import { DateTime } from 'luxon';
describe('GET /api/v1/users/:userId/plan-and-usage', () => {
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);
});
it('should return free trial plan and usage data', async () => {
const response = await request(app)
.get(`/api/v1/users/${user.id}/plan-and-usage`)
.set('Authorization', token)
.expect(200);
const expectedResponseData = {
plan: {
id: null,
limit: null,
name: 'Free Trial',
},
usage: {
task: 0,
},
};
expect(response.body.data).toEqual(expectedResponseData);
});
it('should return current plan and usage data', async () => {
await createSubscription({ userId: user.id });
await createUsageData({
userId: user.id,
consumedTaskCount: 1234,
});
const response = await request(app)
.get(`/api/v1/users/${user.id}/plan-and-usage`)
.set('Authorization', token)
.expect(200);
const expectedResponseData = {
plan: {
id: '47384',
limit: '10,000',
name: '10k - monthly',
},
usage: {
task: 1234,
},
};
expect(response.body.data).toEqual(expectedResponseData);
});
});

View File

@@ -1,9 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const subscription = await request.currentUser
.$relatedQuery('currentSubscription')
.throwIfNotFound();
renderObject(response, subscription);
};

View File

@@ -1,51 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import appConfig from '../../../../config/app.js';
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 { createSubscription } from '../../../../../test/factories/subscription.js';
import getSubscriptionMock from '../../../../../test/mocks/rest/api/v1/users/get-subscription.js';
describe('GET /api/v1/users/:userId/subscription', () => {
let currentUser, role, subscription, token;
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
role = await createRole();
currentUser = await createUser({
roleId: role.id,
});
subscription = await createSubscription({ userId: currentUser.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return subscription info of the current user', async () => {
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/subscription`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getSubscriptionMock(subscription);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response if there is no current subscription', async () => {
const userWithoutSubscription = await createUser({
roleId: role.id,
});
const token = createAuthTokenByUserId(userWithoutSubscription.id);
await request(app)
.get(`/api/v1/users/${userWithoutSubscription.id}/subscription`)
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -1,29 +0,0 @@
import Flow from '../../models/flow.js';
import logger from '../../helpers/logger.js';
import handlerSync from '../../helpers/webhook-handler-sync.js';
export default async (request, response) => {
const computedRequestPayload = {
headers: request.headers,
body: request.body,
query: request.query,
params: request.params,
};
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
const flowId = request.params.flowId;
const flow = await Flow.query().findById(flowId).throwIfNotFound();
const triggerStep = await flow.getTriggerStep();
if (triggerStep.appKey !== 'webhook') {
const connection = await triggerStep.$relatedQuery('connection');
if (!(await connection.verifyWebhook(request))) {
return response.sendStatus(401);
}
}
await handlerSync(flowId, request, response);
};

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_key');
});
}

View File

@@ -1,17 +0,0 @@
export async function up(knex) {
const appAuthClients = await knex('app_auth_clients').select('*');
for (const appAuthClient of appAuthClients) {
const appConfig = await knex('app_configs')
.where('id', appAuthClient.app_config_id)
.first();
await knex('app_auth_clients')
.where('id', appAuthClient.id)
.update({ app_key: appConfig.key });
}
}
export async function down() {
// void
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_config_id');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.uuid('app_config_id').references('id').inTable('app_configs');
});
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').notNullable().alter();
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').nullable().alter();
});
}

View File

@@ -1,4 +1,4 @@
import Step from '../../models/step.js';
import Step from '../../models/flow.js';
const deleteStep = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');

View File

@@ -0,0 +1,24 @@
import AppAuthClient from '../../models/app-auth-client.js';
const getAppAuthClient = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appAuthClient = AppAuthClient.query()
.findById(params.id)
.throwIfNotFound();
if (!canSeeAllClients) {
appAuthClient.where({ active: true });
}
return await appAuthClient;
};
export default getAppAuthClient;

View File

@@ -0,0 +1,33 @@
import AppConfig from '../../models/app-config.js';
const getAppAuthClients = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appConfig = await AppConfig.query()
.findOne({
key: params.appKey,
})
.throwIfNotFound();
const appAuthClients = appConfig
.$relatedQuery('appAuthClients')
.where({ active: params.active })
.skipUndefined();
if (!canSeeAllClients) {
appAuthClients.where({
active: true,
});
}
return await appAuthClients;
};
export default getAppAuthClients;

View File

@@ -0,0 +1,17 @@
import AppConfig from '../../models/app-config.js';
const getAppConfig = async (_parent, params, context) => {
context.currentUser.can('create', 'Connection');
const appConfig = await AppConfig.query()
.withGraphFetched({
appAuthClients: true,
})
.findOne({
key: params.key,
});
return appConfig;
};
export default getAppConfig;

View File

@@ -0,0 +1,41 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
const getApp = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await connectionBaseQuery
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
return {
...app,
connections,
};
}
return app;
};
export default getApp;

View File

@@ -0,0 +1,17 @@
import App from '../../models/app.js';
const getApps = async (_parent, params) => {
const apps = await App.findAll(params.name);
if (params.onlyWithTriggers) {
return apps.filter((app) => app.triggers?.length);
}
if (params.onlyWithActions) {
return apps.filter((app) => app.actions?.length);
}
return apps;
};
export default getApps;

View File

@@ -0,0 +1,101 @@
import { DateTime } from 'luxon';
import Billing from '../../helpers/billing/index.ee.js';
import ExecutionStep from '../../models/execution-step.js';
const getBillingAndUsage = async (_parent, _params, context) => {
const persistedSubscription = await context.currentUser.$relatedQuery(
'currentSubscription'
);
const subscription = persistedSubscription
? paidSubscription(persistedSubscription)
: freeTrialSubscription();
return {
subscription,
usage: {
task: executionStepCount(context),
},
};
};
const paidSubscription = (subscription) => {
const currentPlan = Billing.paddlePlans.find(
(plan) => plan.productId === subscription.paddlePlanId
);
return {
status: subscription.status,
monthlyQuota: {
title: currentPlan.limit,
action: {
type: 'link',
text: 'Cancel plan',
src: subscription.cancelUrl,
},
},
nextBillAmount: {
title: subscription.nextBillAmount
? '€' + subscription.nextBillAmount
: '---',
action: {
type: 'link',
text: 'Update payment method',
src: subscription.updateUrl,
},
},
nextBillDate: {
title: subscription.nextBillDate ? subscription.nextBillDate : '---',
action: {
type: 'text',
text: '(monthly payment)',
},
},
};
};
const freeTrialSubscription = () => {
return {
status: null,
monthlyQuota: {
title: 'Free Trial',
action: {
type: 'link',
text: 'Upgrade plan',
src: '/settings/billing/upgrade',
},
},
nextBillAmount: {
title: '---',
action: null,
},
nextBillDate: {
title: '---',
action: null,
},
};
};
const executionIds = async (context) => {
return (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution) => execution.id);
};
const executionStepCount = async (context) => {
const executionStepCount = await ExecutionStep.query()
.whereIn('execution_id', await executionIds(context))
.andWhere(
'created_at',
'>=',
DateTime.now().minus({ days: 30 }).toISODate()
)
.count()
.first();
return executionStepCount.count;
};
export default getBillingAndUsage;

View File

@@ -0,0 +1,32 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
configQuery.whereIn('key', params.keys);
}
const config = await configQuery.orderBy('key', 'asc');
return config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
};
export default getConfig;

View File

@@ -0,0 +1,140 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
let configOne, configTwo, configThree, query;
beforeEach(async () => {
configOne = await createConfig({ key: 'configOne' });
configTwo = await createConfig({ key: 'configTwo' });
configThree = await createConfig({ key: 'configThree' });
query = `
query {
getConfig
}
`;
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and correct permissions', () => {
it('should return empty config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = { data: { getConfig: {} } };
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
describe('and without providing specific keys', () => {
it('should return all config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with providing specific keys', () => {
it('should return all config data', async () => {
query = `
query {
getConfig(keys: ["configOne", "configTwo"])
}
`;
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -0,0 +1,67 @@
import App from '../../models/app.js';
import Flow from '../../models/flow.js';
import Connection from '../../models/connection.js';
const getConnectedApps = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await flowBaseQuery
.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])];
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;
};
export default getConnectedApps;

View File

@@ -0,0 +1,5 @@
const getCurrentUser = async (_parent, _params, context) => {
return context.currentUser;
};
export default getCurrentUser;

View File

@@ -0,0 +1,79 @@
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 { createUser } from '../../../test/factories/user';
describe('graphQL getCurrentUser query', () => {
let role, currentUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return user data', async () => {
const query = `
query {
getCurrentUser {
id
email
fullName
email
createdAt
updatedAt
role {
id
name
}
}
}
`;
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getCurrentUser: {
createdAt: currentUser.createdAt.getTime().toString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: { id: role.id, name: role.name },
updatedAt: currentUser.updatedAt.getTime().toString(),
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return user password', async () => {
const query = `
query {
getCurrentUser {
id
email
password
}
}
`;
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
});

View File

@@ -0,0 +1,65 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import ExecutionStep from '../../models/execution-step.js';
import globalVariable from '../../helpers/global-variable.js';
import computeParameters from '../../helpers/compute-parameters.js';
const getDynamicData = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!connection || !step.appKey) return null;
const flow = step.flow;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow, step });
const command = app.dynamicData.find((data) => data.key === params.key);
// apply run-time parameters that're not persisted yet
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
// compute variables in parameters
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const fetchedData = await command.run($);
if (fetchedData.error) {
throw new Error(JSON.stringify(fetchedData.error));
}
return fetchedData.data;
};
export default getDynamicData;

View File

@@ -0,0 +1,40 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import globalVariable from '../../helpers/global-variable.js';
const getDynamicFields = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!step.appKey) return null;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow: step.flow, step });
const command = app.dynamicFields.find((data) => data.key === params.key);
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const additionalFields = (await command.run($)) || [];
return additionalFields;
};
export default getDynamicFields;

Some files were not shown because too many files have changed in this diff Show More