From 77e29050c846224bd0acd1688c1c1528bdcb9168 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Sun, 9 Apr 2023 16:23:34 +0000 Subject: [PATCH 1/2] feat(flow): add virtual paused/published/draft status --- packages/backend/src/graphql/schema.graphql | 7 +++++ packages/backend/src/models/flow.ts | 30 ++++++++++++++++++- packages/backend/src/models/usage-data.ee.ts | 4 +++ packages/types/index.d.ts | 1 + packages/web/src/components/FlowRow/index.tsx | 24 +++++++++++++-- packages/web/src/graphql/queries/get-flow.ts | 1 + packages/web/src/graphql/queries/get-flows.ts | 1 + packages/web/src/locales/en.json | 1 + 8 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 2ffe0fcdc6..614ca958b5 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -250,6 +250,12 @@ type FlowEdge { node: Flow } +enum FlowStatus { + paused + published + draft +} + type Flow { id: String name: String @@ -257,6 +263,7 @@ type Flow { steps: [Step] createdAt: String updatedAt: String + status: FlowStatus } type Execution { diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index ed57c1f3ee..f9e1b1917a 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -1,5 +1,5 @@ import { ValidationError } from 'objection'; -import type { ModelOptions, QueryContext } from 'objection'; +import type { ModelOptions, QueryContext, StaticHookArguments } from 'objection'; import appConfig from '../config/app'; import ExtendedQueryBuilder from './query-builder'; import Base from './base'; @@ -14,6 +14,7 @@ class Flow extends Base { name!: string; userId!: string; active: boolean; + status: 'paused' | 'published' | 'draft'; steps: Step[]; published_at: string; remoteWebhookId: string; @@ -65,6 +66,26 @@ class Flow extends Base { }, }); + static async afterFind(args: StaticHookArguments): Promise { + const { result } = args; + + const referenceFlow = result[0]; + + if (referenceFlow) { + const shouldBePaused = await referenceFlow.isPaused(); + + for (const flow of result) { + if (!flow.active) { + flow.status = 'draft'; + } else if (flow.active && shouldBePaused) { + flow.status = 'paused'; + } else { + flow.status = 'published'; + } + } + } + } + async lastInternalId() { const lastExecution = await this.$relatedQuery('executions') .orderBy('created_at', 'desc') @@ -132,6 +153,13 @@ class Flow extends Base { }); } + async isPaused() { + const user = await this.$relatedQuery('user'); + const currentUsageData = await user.$relatedQuery('currentUsageData'); + + return await currentUsageData.checkIfLimitExceeded(); + } + async checkIfQuotaExceeded() { if (!appConfig.isCloud) return; diff --git a/packages/backend/src/models/usage-data.ee.ts b/packages/backend/src/models/usage-data.ee.ts index 047d0bf172..6ad1309399 100644 --- a/packages/backend/src/models/usage-data.ee.ts +++ b/packages/backend/src/models/usage-data.ee.ts @@ -56,6 +56,10 @@ class UsageData extends Base { const subscription = await this.$relatedQuery('subscription'); + if (!subscription) { + return true; + } + if (!subscription.isActive) { return true; } diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index a8a7e48f40..1dfcd3c044 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -73,6 +73,7 @@ export interface IFlow { name: string; userId: string; active: boolean; + status: 'paused' | 'published' | 'draft'; steps: IStep[]; createdAt: string; updatedAt: string; diff --git a/packages/web/src/components/FlowRow/index.tsx b/packages/web/src/components/FlowRow/index.tsx index 97b3d858dd..6fdafe9103 100644 --- a/packages/web/src/components/FlowRow/index.tsx +++ b/packages/web/src/components/FlowRow/index.tsx @@ -18,6 +18,26 @@ type FlowRowProps = { flow: IFlow; }; +function getFlowStatusTranslationKey(status: IFlow["status"]): string { + if (status === 'published') { + return 'flow.published'; + } else if (status === 'paused') { + return 'flow.paused'; + } + + return 'flow.draft'; +} + +function getFlowStatusColor(status: IFlow["status"]): 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' { + if (status === 'published') { + return 'success'; + } else if (status === 'paused') { + return 'error'; + } + + return 'info'; +} + export default function FlowRow(props: FlowRowProps): React.ReactElement { const formatMessage = useFormatMessage(); const contextButtonRef = React.useRef(null); @@ -76,10 +96,10 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement { diff --git a/packages/web/src/graphql/queries/get-flow.ts b/packages/web/src/graphql/queries/get-flow.ts index f3e864462f..521dcdab7d 100644 --- a/packages/web/src/graphql/queries/get-flow.ts +++ b/packages/web/src/graphql/queries/get-flow.ts @@ -6,6 +6,7 @@ export const GET_FLOW = gql` id name active + status steps { id type diff --git a/packages/web/src/graphql/queries/get-flows.ts b/packages/web/src/graphql/queries/get-flows.ts index ab1040f8be..d2e5d3216b 100644 --- a/packages/web/src/graphql/queries/get-flows.ts +++ b/packages/web/src/graphql/queries/get-flows.ts @@ -26,6 +26,7 @@ export const GET_FLOWS = gql` createdAt updatedAt active + status steps { iconUrl } diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b4b7437e00..f2217d9abd 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -43,6 +43,7 @@ "flow.active": "ON", "flow.inactive": "OFF", "flow.published": "Published", + "flow.paused": "Paused", "flow.draft": "Draft", "flow.successfullyDeleted": "The flow and associated executions have been deleted.", "flowEditor.publish": "PUBLISH", From 9cb4607f69fd189414cfa351ee700ff6f3e767d1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 10 Apr 2023 17:21:19 +0200 Subject: [PATCH 2/2] refactor: User permission and limits to run flows --- packages/backend/src/config/app.ts | 2 + .../src/controllers/webhooks/handler.ts | 19 ++++++--- packages/backend/src/models/flow.ts | 39 +++---------------- packages/backend/src/models/usage-data.ee.ts | 23 ----------- packages/backend/src/models/user.ts | 36 ++++++++++++++--- packages/backend/src/workers/flow.ts | 6 +-- 6 files changed, 54 insertions(+), 71 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index ab7fd430a4..032b1e539d 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -39,6 +39,7 @@ type AppConfig = { smtpPassword: string; fromEmail: string; isCloud: boolean; + isSelfHosted: boolean; paddleVendorId: number; paddleVendorAuthCode: string; paddlePublicKey: string; @@ -110,6 +111,7 @@ const appConfig: AppConfig = { smtpPassword: process.env.SMTP_PASSWORD, fromEmail: process.env.FROM_EMAIL, isCloud: process.env.AUTOMATISCH_CLOUD === 'true', + isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true', paddleVendorId: Number(process.env.PADDLE_VENDOR_ID), paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE, paddlePublicKey: process.env.PADDLE_PUBLIC_KEY, diff --git a/packages/backend/src/controllers/webhooks/handler.ts b/packages/backend/src/controllers/webhooks/handler.ts index 5ac8a160cb..6ef0414fcf 100644 --- a/packages/backend/src/controllers/webhooks/handler.ts +++ b/packages/backend/src/controllers/webhooks/handler.ts @@ -6,17 +6,24 @@ import Flow from '../../models/flow'; import { processTrigger } from '../../services/trigger'; import actionQueue from '../../queues/action'; import globalVariable from '../../helpers/global-variable'; -import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration'; +import QuotaExceededError from '../../errors/quote-exceeded'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../../helpers/remove-job-configuration'; export default async (request: IRequest, response: Response) => { const flow = await Flow.query() .findById(request.params.flowId) .throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + const testRun = !flow.active; + const quotaExceeded = !testRun && !(await user.isAllowedToRunFlows()); - if (!testRun) { - await flow.throwIfQuotaExceeded(); + if (quotaExceeded) { + throw new QuotaExceededError(); } const triggerStep = await flow.getTriggerStep(); @@ -58,7 +65,7 @@ export default async (request: IRequest, response: Response) => { headers: request.headers, body: request.body, query: request.query, - } + }; rawInternalId = JSON.stringify(payload); } @@ -74,7 +81,7 @@ export default async (request: IRequest, response: Response) => { flowId: flow.id, stepId: triggerStep.id, triggerItem, - testRun + testRun, }); if (testRun) { @@ -93,7 +100,7 @@ export default async (request: IRequest, response: Response) => { const jobOptions = { removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, - } + }; await actionQueue.add(jobName, jobPayload, jobOptions); diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index f9e1b1917a..2c8a06b3e0 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -1,13 +1,15 @@ import { ValidationError } from 'objection'; -import type { ModelOptions, QueryContext, StaticHookArguments } from 'objection'; -import appConfig from '../config/app'; +import type { + ModelOptions, + QueryContext, + StaticHookArguments, +} from 'objection'; import ExtendedQueryBuilder from './query-builder'; import Base from './base'; import Step from './step'; import User from './user'; import Execution from './execution'; import Telemetry from '../helpers/telemetry'; -import QuotaExceededError from '../errors/quote-exceeded'; class Flow extends Base { id!: string; @@ -155,36 +157,7 @@ class Flow extends Base { async isPaused() { const user = await this.$relatedQuery('user'); - const currentUsageData = await user.$relatedQuery('currentUsageData'); - - return await currentUsageData.checkIfLimitExceeded(); - } - - async checkIfQuotaExceeded() { - if (!appConfig.isCloud) return; - - const user = await this.$relatedQuery('user'); - const usageData = await user.$relatedQuery('currentUsageData'); - - const hasExceeded = await usageData.checkIfLimitExceeded(); - - if (hasExceeded) { - return true; - } - - return false; - } - - async throwIfQuotaExceeded() { - if (!appConfig.isCloud) return; - - const hasExceeded = await this.checkIfQuotaExceeded(); - - if (hasExceeded) { - throw new QuotaExceededError(); - } - - return this; + return await user.isAllowedToRunFlows(); } } diff --git a/packages/backend/src/models/usage-data.ee.ts b/packages/backend/src/models/usage-data.ee.ts index 6ad1309399..6cae3e5635 100644 --- a/packages/backend/src/models/usage-data.ee.ts +++ b/packages/backend/src/models/usage-data.ee.ts @@ -2,7 +2,6 @@ import { raw } from 'objection'; import Base from './base'; import User from './user'; import Subscription from './subscription.ee'; -import { getPlanById } from '../helpers/billing/plans.ee'; class UsageData extends Base { id!: string; @@ -47,28 +46,6 @@ class UsageData extends Base { }, }); - async checkIfLimitExceeded() { - const user = await this.$relatedQuery('user'); - - if (await user.inTrial()) { - return false; - } - - const subscription = await this.$relatedQuery('subscription'); - - if (!subscription) { - return true; - } - - if (!subscription.isActive) { - return true; - } - - const plan = subscription.plan; - - return this.consumedTaskCount >= plan.quota; - } - async increaseConsumedTaskCountByOne() { return await this.$query().patch({ consumedTaskCount: raw('consumed_task_count + 1'), diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index daba8d74e7..2e17203e46 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -165,18 +165,24 @@ class User extends Base { this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); } - async hasActiveSubscription() { - if (!appConfig.isCloud) { - return false; + async isAllowedToRunFlows() { + if (appConfig.isSelfHosted) { + return true; } - const subscription = await this.$relatedQuery('currentSubscription'); + if (await this.inTrial()) { + return true; + } - return subscription?.isActive; + if ((await this.hasActiveSubscription()) && (await this.withinLimits())) { + return true; + } + + return false; } async inTrial() { - if (!appConfig.isCloud) { + if (appConfig.isSelfHosted) { return false; } @@ -196,6 +202,24 @@ class User extends Base { return now < expiryDate; } + async hasActiveSubscription() { + if (!appConfig.isCloud) { + return false; + } + + const subscription = await this.$relatedQuery('currentSubscription'); + + return subscription?.isActive; + } + + async withinLimits() { + const currentSubscription = await this.$relatedQuery('currentSubscription'); + const plan = currentSubscription.plan; + const currentUsageData = await this.$relatedQuery('currentUsageData'); + + return currentUsageData.consumedTaskCount >= plan.quota; + } + async $beforeInsert(queryContext: QueryContext) { await super.$beforeInsert(queryContext); await this.generateHash(); diff --git a/packages/backend/src/workers/flow.ts b/packages/backend/src/workers/flow.ts index 884be7d8cf..49ced1f88b 100644 --- a/packages/backend/src/workers/flow.ts +++ b/packages/backend/src/workers/flow.ts @@ -17,10 +17,10 @@ export const worker = new Worker( const { flowId } = job.data; const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + const allowedToRunFlows = await user.isAllowedToRunFlows(); - const quotaExceeded = await flow.checkIfQuotaExceeded(); - - if (quotaExceeded) { + if (!allowedToRunFlows) { return; }