diff --git a/README.md b/README.md
index ba7df6e3..c6bd0a5b 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,31 @@
-# AREA
+# Area
-## Team
+Area is an automation platform, available as a web app and a mobile app, that allows you to
+combine Actions and REactions [from a large range of integrations]((https://rezarahemtola.notion.site/Area-Integrations-f382af642ddb49d4b5a52e3fc307bfd9)) to fit your needs.
-### Frontend
+You can learn more about the project [in our documentation](https://docs.area.rezar.fr) 😄
-| [
Tom BARITEAU--PETER](https://github.com/Tomi-Tom) | [
Axel DENIS](https://github.com/axel-denis)
-|:---:| :---: |
+## Getting started 🚀
-### Backend
+To start using our amazing platform, go to [`https://area.rezar.fr`](https://area.rezar.fr) and setup your account to start creating workflows!
-| [
Dorian MOY](https://github.com/Croos3r) | [
Florian LAUCH](https://github.com/EdenComp)
-|:---:| :---: |
+If you want a more detailed guide of the different options to use Area (including self-hosted 💻), [check this page](https://docs.area.rezar.fr/#/general/getting-started).
-### Full-stack & Project manager
-| [
Reza Rahemtola](https://github.com/RezaRahemtola)
-| :---: |
+## How it works 🤔
+
+If you're here for the technical details, we got you covered too!
+You can dive into the architecture documentation explaining how we are using [microservices](https://docs.area.rezar.fr/#/architecture/workers)
+and a [supervisor](https://docs.area.rezar.fr/#/architecture/supervisor) to create Docker containers on-demand.
+
+> 💡 Some pretty standard parts may not be covered, but don't hesitate to reach out by creating an issue on [our repository](https://github.com/RezaRahemtola/Area), we'll be happy to help you!
+
+## Contribute 🤝
+
+If you want to go a step forward and participate in the development of features to enhance Area, we'd be glad to onboard you 🥇
+
+Reach out to [`cramptarea@gmail.com`](mailto:cramptarea@gmail.com) and we'll get back to you in a few business days!
+
+## Team ❤️
+
+| [
Dorian MOY](https://github.com/Croos3r)
Backend | [
Florian LAUCH](https://github.com/EdenComp)
Microservices | [
Tom BARITEAU-PETER](https://github.com/Tomi-Tom)
Mobile | [
Axel DENIS](https://github.com/axel-denis)
Mobile | [
Reza RAHEMTOLA](https://github.com/RezaRahemtola)
Web & DevOps
+|:---:| :---: | :---: | :---: | :---: |
diff --git a/backend/back/src/connections/connections.module.ts b/backend/back/src/connections/connections.module.ts
index 8c8dc9e1..b91439b8 100644
--- a/backend/back/src/connections/connections.module.ts
+++ b/backend/back/src/connections/connections.module.ts
@@ -10,6 +10,7 @@ import { OauthController } from "./oauth.controller";
import { OauthService } from "./oauth.service";
import { UsersModule } from "../users/users.module";
import { AuthModule } from "../auth/auth.module";
+import { GrpcModule } from "../grpc/grpc.module";
@Module({
imports: [
@@ -17,6 +18,7 @@ import { AuthModule } from "../auth/auth.module";
forwardRef(() => ServicesModule),
HttpModule,
UsersModule,
+ forwardRef(() => GrpcModule),
forwardRef(() => AuthModule),
],
controllers: [ConnectionsController, OauthController],
diff --git a/backend/back/src/connections/oauth.controller.ts b/backend/back/src/connections/oauth.controller.ts
index 5a858b06..317f6f6c 100644
--- a/backend/back/src/connections/oauth.controller.ts
+++ b/backend/back/src/connections/oauth.controller.ts
@@ -1,7 +1,9 @@
import {
Controller,
ForbiddenException,
+ forwardRef,
Get,
+ Inject,
InternalServerErrorException,
Logger,
Param,
@@ -17,6 +19,7 @@ import { ServiceIdParamDto } from "../param-validators.dto";
import { ConnectionsService } from "./connections.service";
import { AuthService } from "../auth/auth.service";
import { UsersService } from "../users/users.service";
+import { GrpcService } from "../grpc/grpc.service";
@ApiTags("OAuth Callbacks")
@Controller("connections/oauth")
@@ -29,6 +32,8 @@ export class OauthController {
private readonly connectionService: ConnectionsService,
private readonly authService: AuthService,
private readonly usersService: UsersService,
+ @Inject(forwardRef(() => GrpcService))
+ private readonly grpcService: GrpcService,
) {}
@Get("/:serviceId/callback")
@@ -66,6 +71,13 @@ export class OauthController {
const connection = await this.connectionService.createUserConnection(userId, serviceId, scopes, data);
if (!connection) throw new InternalServerErrorException("Failed to create connection");
this.logger.log(`Connection created for user ${userId}, redirecting to frontend...`);
+ await this.grpcService.onAction({
+ name: "area-on-account-connect",
+ identifier: `area-on-account-connect-${userId}`,
+ params: {
+ service: serviceId,
+ },
+ });
}
return response.redirect(
`${this.configService.getOrThrow("FRONT_OAUTH_REDIRECTION_URL")}?${queryParams.toString()}`,
diff --git a/backend/back/src/connections/oauth.service.ts b/backend/back/src/connections/oauth.service.ts
index 57c17c59..700bf43c 100644
--- a/backend/back/src/connections/oauth.service.ts
+++ b/backend/back/src/connections/oauth.service.ts
@@ -604,5 +604,13 @@ export class OauthService {
throw new Error("Cannot create OAuth connection for riot service");
},
},
+ area: {
+ urlFactory: () => {
+ throw new Error("Cannot create OAuth URL for area service");
+ },
+ connectionFactory: () => {
+ throw new Error("Cannot create OAuth connection for area service");
+ },
+ },
};
}
diff --git a/backend/back/src/database.ts b/backend/back/src/database.ts
index f0a5b902..c73afb80 100644
--- a/backend/back/src/database.ts
+++ b/backend/back/src/database.ts
@@ -75,6 +75,20 @@ import ActivityLog from "./activity/entities/activity-log.entity";
import { AddActivityLogEntity1698986590525 } from "./migrations/1698986590525-AddActivityLogEntity";
import { CreateRiotGamesService1698964479750 } from "./services/seed/1698964479750-CreateRiotGamesService";
import { CreateRiotGamesActions1698964479850 } from "./workflows/seed/1698964479850-CreateRiotGamesActions";
+import { CreateTodoistCreateTaskReaction1699101589099 } from "./workflows/seed/1699101589099-CreateTodoistCreateTaskReaction";
+import { CreateAirtableDeleteRecordReaction1699107944021 } from "./workflows/seed/1699107944021-CreateAirtableDeleteRecordReaction";
+import { CreateTwitterCreateTweetReaction1699115775099 } from "./workflows/seed/1699115775099-CreateTwitterCreateTweetReaction";
+import { ChangeRiotLogoToSvg1699116526789 } from "./services/seed/1699116526789-ChangeRiotLogoToSvg";
+import { CreateOutlookSendEmailReaction1699126617355 } from "./workflows/seed/1699126617355-CreateOutlookSendEmailReaction";
+import { CreateSlackCreateMessageReaction1699131357051 } from "./workflows/seed/1699131357051-CreateSlackCreateMessageReaction";
+import { CreateDiscordOnGuildJoinAction1699140593736 } from "./workflows/seed/1699140593736-CreateDiscordOnGuildJoinAction";
+import { CreateTodoistTaskReactions1699140054686 } from "./workflows/seed/1699140054686-CreateTodoistTaskReactions";
+import { CreateAreaService1699143731594 } from "./services/seed/1699143731594-CreateAreaService";
+import { CreateAreaOnActionAction1699145276143 } from "./workflows/seed/1699145276143-CreateAreaOnActionAction";
+import { CreateDiscordNewActions1699165196600 } from "./workflows/seed/1699165196600-CreateDiscordNewActions";
+import { CreateAreaAreas1699152935342 } from "./workflows/seed/1699152935342-CreateAreaAreas";
+import { FixAddScopeToLinearOnIssueCreate1699190007820 } from "./workflows/seed/1699190007820-FixAddScopeToLinearOnIssueCreate";
+import { CreateLinearProjectCommentAREAs1699202040386 } from "./workflows/seed/1699202040386-CreateLinearProjectCommentAREAs";
dotenv.config();
@@ -153,6 +167,20 @@ export const DATA_SOURCE_OPTIONS: DataSourceOptions = {
AddActivityLogEntity1698986590525,
CreateRiotGamesService1698964479750,
CreateRiotGamesActions1698964479850,
+ CreateTodoistCreateTaskReaction1699101589099,
+ CreateAirtableDeleteRecordReaction1699107944021,
+ CreateTwitterCreateTweetReaction1699115775099,
+ ChangeRiotLogoToSvg1699116526789,
+ CreateOutlookSendEmailReaction1699126617355,
+ CreateSlackCreateMessageReaction1699131357051,
+ CreateDiscordOnGuildJoinAction1699140593736,
+ CreateTodoistTaskReactions1699140054686,
+ CreateAreaService1699143731594,
+ CreateAreaOnActionAction1699145276143,
+ CreateDiscordNewActions1699165196600,
+ CreateAreaAreas1699152935342,
+ FixAddScopeToLinearOnIssueCreate1699190007820,
+ CreateLinearProjectCommentAREAs1699202040386,
],
synchronize: process.env.NODE_ENV === "development",
};
diff --git a/backend/back/src/grpc/grpc.controller.ts b/backend/back/src/grpc/grpc.controller.ts
index 69cda3d6..1ca27a45 100644
--- a/backend/back/src/grpc/grpc.controller.ts
+++ b/backend/back/src/grpc/grpc.controller.ts
@@ -1,7 +1,7 @@
import { Controller } from "@nestjs/common";
import { GrpcMethod } from "@nestjs/microservices";
import { ApiExcludeController } from "@nestjs/swagger";
-import { JobData } from "./grpc.dto";
+import { JobData, JobError } from "./grpc.dto";
import { GrpcService } from "./grpc.service";
@ApiExcludeController()
@@ -20,7 +20,7 @@ export class GrpcController {
}
@GrpcMethod("AreaBackService", "OnError")
- async onError(data: JobData): Promise {
+ async onError(data: JobError): Promise {
await this.grpcService.onError(data);
}
}
diff --git a/backend/back/src/grpc/grpc.service.ts b/backend/back/src/grpc/grpc.service.ts
index c03266bf..19f9644c 100644
--- a/backend/back/src/grpc/grpc.service.ts
+++ b/backend/back/src/grpc/grpc.service.ts
@@ -1,6 +1,6 @@
import { forwardRef, Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { ClientGrpc } from "@nestjs/microservices";
-import { AuthenticatedJobData, GrpcResponse, JobData, JobId, JobList } from "./grpc.dto";
+import { AuthenticatedJobData, GrpcResponse, JobData, JobError, JobId, JobList } from "./grpc.dto";
import { firstValueFrom, Observable } from "rxjs";
import { JobsParams, JobsType } from "../types/jobs";
import { JobsIdentifiers } from "../types/jobIds";
@@ -35,11 +35,7 @@ export class GrpcService implements OnModuleInit {
await this.jobsService.synchronizeJobs();
}
- launchJob(
- name: TJob,
- params: TParams,
- auth: unknown,
- ): Promise {
+ launchJob(name: TJob, params: JobsParams[TJob], auth: unknown): Promise {
const identifier = JobsIdentifiers[name](params);
this.logger.log(
`Launching job ${identifier} with name ${name} and params ${JSON.stringify({
@@ -79,6 +75,18 @@ export class GrpcService implements OnModuleInit {
this.logger.log(`Received action job data ${JSON.stringify(data, undefined, 2)}`);
await this.activityService.createActivityLogsForJobIdentifier("ran", data.identifier);
await this.jobsService.launchNextJob(data);
+
+ if (data.name === "area-on-action") return;
+ const owners = await this.jobsService.getWorkflowOwnersForAction(data.identifier);
+ for (const owner of owners) {
+ await this.onAction({
+ name: "area-on-action",
+ identifier: `area-on-action-${owner}`,
+ params: {
+ name: data.name,
+ },
+ });
+ }
}
async onReaction(data: JobData) {
@@ -87,7 +95,7 @@ export class GrpcService implements OnModuleInit {
await this.jobsService.launchNextJob(data);
}
- async onError(data: JobData) {
+ async onError(data: JobError) {
this.logger.error(`Received error job data ${JSON.stringify(data, undefined, 2)}`);
await this.activityService.createActivityLogsForJobIdentifier("error", data.identifier);
}
diff --git a/backend/back/src/jobs/back-jobs.service.ts b/backend/back/src/jobs/back-jobs.service.ts
new file mode 100644
index 00000000..5370fdd6
--- /dev/null
+++ b/backend/back/src/jobs/back-jobs.service.ts
@@ -0,0 +1,52 @@
+import { forwardRef, Inject, Injectable } from "@nestjs/common";
+import { JobData } from "../grpc/grpc.dto";
+import { JobsType } from "../types/jobs";
+import { JobsService } from "./jobs.service";
+import { WorkflowToggleParams } from "../types/jobParams";
+import { WorkflowsService } from "../workflows/workflows.service";
+import { GrpcService } from "../grpc/grpc.service";
+import { JobsIdentifiers } from "../types/jobIds";
+
+@Injectable()
+export class BackJobsService {
+ constructor(
+ @Inject(forwardRef(() => GrpcService)) private readonly grpcService: GrpcService,
+ @Inject(forwardRef(() => JobsService)) private readonly jobsService: JobsService,
+ private readonly workflowsService: WorkflowsService,
+ ) {}
+
+ async toggleWorkflow(params: WorkflowToggleParams, newState: boolean) {
+ const jobName = `area-${newState ? "enable" : "disable"}-workflow`;
+ const workflow = await this.workflowsService.getWorkflowByNameAndOwner(params.workflowName, params.ownerId);
+
+ if (workflow.active === newState) return;
+ try {
+ await this.workflowsService.toggleWorkflow(workflow.id, newState, params.ownerId);
+ await this.grpcService.onAction({
+ name: jobName,
+ identifier: `area-${newState ? "enable" : "disable"}-workflow-${params.ownerId}`,
+ params: {},
+ });
+ } catch (e) {
+ await this.grpcService.onError({
+ identifier: JobsIdentifiers[jobName](params),
+ error: e.message,
+ isAuthError: false,
+ });
+ }
+ }
+
+ async executeBackJob(job: JobData) {
+ const jobType: JobsType = job.name as JobsType;
+ const params = await this.jobsService.convertParams(jobType, job.params);
+
+ switch (jobType) {
+ case "area-disable-workflow":
+ await this.toggleWorkflow(params as WorkflowToggleParams, false);
+ break;
+ case "area-enable-workflow":
+ await this.toggleWorkflow(params as WorkflowToggleParams, true);
+ break;
+ }
+ }
+}
diff --git a/backend/back/src/jobs/jobs.dto.ts b/backend/back/src/jobs/jobs.dto.ts
new file mode 100644
index 00000000..0e6c11bb
--- /dev/null
+++ b/backend/back/src/jobs/jobs.dto.ts
@@ -0,0 +1,16 @@
+import { JobsType } from "../types/jobs";
+
+export const AREAS_WITHOUT_GRPC: JobsType[] = [
+ "area-disable-workflow",
+ "area-enable-workflow",
+ "area-on-account-connect",
+ "area-on-action",
+ "area-on-workflow-create",
+ "area-on-workflow-toggle",
+ "facebook-on-status-create",
+ "linear-on-comment-create",
+ "linear-on-issue-create",
+ "linear-on-issue-update",
+ "linear-on-project-create",
+ "linear-on-project-update",
+];
diff --git a/backend/back/src/jobs/jobs.module.ts b/backend/back/src/jobs/jobs.module.ts
index f2661253..a6dc9029 100644
--- a/backend/back/src/jobs/jobs.module.ts
+++ b/backend/back/src/jobs/jobs.module.ts
@@ -5,6 +5,7 @@ import { WorkflowsModule } from "../workflows/workflows.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import WorkflowArea from "../workflows/entities/workflow-area.entity";
import { ConnectionsModule } from "../connections/connections.module";
+import { BackJobsService } from "./back-jobs.service";
@Module({
imports: [
@@ -14,7 +15,7 @@ import { ConnectionsModule } from "../connections/connections.module";
forwardRef(() => GrpcModule),
],
controllers: [],
- providers: [JobsService],
+ providers: [BackJobsService, JobsService],
exports: [JobsService],
})
export class JobsModule {}
diff --git a/backend/back/src/jobs/jobs.service.ts b/backend/back/src/jobs/jobs.service.ts
index 3518b516..9cb79fe0 100644
--- a/backend/back/src/jobs/jobs.service.ts
+++ b/backend/back/src/jobs/jobs.service.ts
@@ -10,7 +10,10 @@ import { Repository } from "typeorm";
import { AuthenticatedJobData, JobData } from "../grpc/grpc.dto";
import { RuntimeException } from "@nestjs/core/errors/exceptions";
import { ConnectionsService } from "../connections/connections.service";
-import { uniqBy } from "lodash";
+import { partition, uniq, uniqBy } from "lodash";
+import { AREAS_WITHOUT_GRPC } from "./jobs.dto";
+import { BackJobsService } from "./back-jobs.service";
+import Workflow from "../workflows/entities/workflow.entity";
@Injectable()
export class JobsService {
@@ -21,6 +24,7 @@ export class JobsService {
@Inject(forwardRef(() => GrpcService)) private readonly grpcService: GrpcService,
@Inject(forwardRef(() => WorkflowsService)) private readonly workflowsService: WorkflowsService,
@InjectRepository(WorkflowArea) private readonly workflowAreaRepository: Repository,
+ private readonly backJobsService: BackJobsService,
) {}
getJobName(areaServiceId: string, areaId: string): JobsType {
@@ -61,9 +65,13 @@ export class JobsService {
);
}
- async getReactionsForJob(jobId: string): Promise {
+ async getReactionsForJob(jobId: string, active?: boolean): Promise {
+ if (active !== undefined) this.logger.log(`The workflows need to be ${active ? "active" : "inactive"}`);
const jobs = await this.workflowAreaRepository.find({
- where: { jobId },
+ where: [
+ { jobId, actionOfWorkflow: { active } },
+ { jobId, workflow: { active } },
+ ],
relations: { nextWorkflowReactions: { area: true }, workflow: true, actionOfWorkflow: true },
});
const nextJobs = await Promise.all(
@@ -89,7 +97,16 @@ export class JobsService {
return reactionJobs;
}
- async convertParams(job: JobsType, params: unknown): Promise {
+ async getWorkflowOwnersForAction(jobId: string, active?: boolean): Promise {
+ const jobs = await this.workflowAreaRepository.find({
+ where: { jobId, actionOfWorkflow: { active } },
+ relations: { actionOfWorkflow: true },
+ });
+ const owners = jobs.map((job) => job.actionOfWorkflow.ownerId);
+ return uniq(owners);
+ }
+
+ async convertParams(job: JobsType, params: unknown): Promise {
const data = plainToInstance