Skip to content

Commit

Permalink
feat: issues
Browse files Browse the repository at this point in the history
  • Loading branch information
ActivePieces committed Apr 28, 2024
1 parent cadb76d commit eba8b0c
Show file tree
Hide file tree
Showing 23 changed files with 596 additions and 4 deletions.
3 changes: 2 additions & 1 deletion packages/ee/shared/src/index.ts
Expand Up @@ -17,4 +17,5 @@ export * from './lib/managed-authn'
export * from './lib/oauth-apps'
export * from './lib/otp'
export * from './lib/authn'
export * from './lib/activity'
export * from './lib/activity'
export * from './lib/issues'
2 changes: 2 additions & 0 deletions packages/ee/shared/src/lib/issues/index.ts
@@ -0,0 +1,2 @@
export * from './issues-requests'
export * from './issue-dto'
25 changes: 25 additions & 0 deletions packages/ee/shared/src/lib/issues/issue-dto.ts
@@ -0,0 +1,25 @@
import { Static, Type } from "@sinclair/typebox";
import { ApId, BaseModelSchema } from "@activepieces/shared";

export enum IssueStatus {
ONGOING = 'ONGOING',
RESOLEVED = 'RESOLEVED',
}

export const Issue = Type.Object({
...BaseModelSchema,
projectId: ApId,
flowId: ApId,
status: Type.Enum(IssueStatus),
count: Type.Number(),
lastSeen: Type.String(),
})

export type Issue = Static<typeof Issue>


export const PopulatedIssue = Type.Composite([Issue, Type.Object({
flowDisplayName: Type.String()
})])

export type PopulatedIssue = Static<typeof PopulatedIssue>
19 changes: 19 additions & 0 deletions packages/ee/shared/src/lib/issues/issues-requests.ts
@@ -0,0 +1,19 @@

import { Type, Static } from '@sinclair/typebox'
import { ApId } from '@activepieces/shared'
import { IssueStatus } from './issue-dto'

export const ListIssuesParams = Type.Object({
projectId: ApId,
cursor: Type.Optional(Type.String()),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
})
export type ListIssuesParams = Static<typeof ListIssuesParams>

export const UpdateIssueRequest = Type.Object({
projectId: ApId,
flowId: ApId,
status: Type.Enum(IssueStatus),
})

export type UpdateIssueRequest = Static<typeof UpdateIssueRequest>
3 changes: 3 additions & 0 deletions packages/server/api/src/app/app.ts
Expand Up @@ -19,6 +19,7 @@ import { copilotModule } from './copilot/copilot.module'
import { rateLimitModule } from './core/security/rate-limit'
import { securityHandlerChain } from './core/security/security-handler-chain'
import { activityModule } from './ee/activity/activity-module'
import { issuesModule } from './ee/issues/issues-module'
import { analyticsModule } from './ee/analytics/analytics-module'
import { apiKeyModule } from './ee/api-keys/api-key-module'
import { cloudAppConnectionsHooks } from './ee/app-connections/cloud-app-connection-service'
Expand Down Expand Up @@ -252,6 +253,8 @@ export const setupApp = async (): Promise<FastifyInstance> => {
await pieceSyncService.setup()
await app.register(flowWorkerModule)
await app.register(platformUserModule)
await app.register(issuesModule)

await setupBullMQBoard(app)

app.get(
Expand Down
2 changes: 2 additions & 0 deletions packages/server/api/src/app/database/database-connection.ts
Expand Up @@ -7,6 +7,7 @@ import {
import { AppConnectionEntity } from '../app-connection/app-connection.entity'
import { AppEventRoutingEntity } from '../app-event-routing/app-event-routing.entity'
import { ActivityEntity } from '../ee/activity/activity-entity'
import {IssueEntity } from '../ee/issues/issues-entity'
import { ApiKeyEntity } from '../ee/api-keys/api-key-entity'
import { AppCredentialEntity } from '../ee/app-credentials/app-credentials.entity'
import { AuditEventEntity } from '../ee/audit-logs/audit-event-entity'
Expand Down Expand Up @@ -68,6 +69,7 @@ function getEntities(): EntitySchema<unknown>[] {
PlatformEntity,
TagEntity,
PieceTagEntity,
IssueEntity,
]

switch (edition) {
Expand Down
@@ -0,0 +1,44 @@
import { logger } from "@activepieces/server-shared";
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddIssueEntitySqlite1714255928781 implements MigrationInterface {
name = 'AddIssueEntitySqlite1714255928781'

public async up(queryRunner: QueryRunner): Promise<void> {
logger.info({ name: 'AddIssueEntitySqlite1714255928781' }, 'up');
await queryRunner.query(`
CREATE TABLE "issue" (
"id" varchar(21) PRIMARY KEY NOT NULL,
"created" datetime NOT NULL DEFAULT (datetime('now')),
"updated" datetime NOT NULL DEFAULT (datetime('now')),
"projectId" varchar(21) NOT NULL,
"flowId" varchar(21) NOT NULL,
"status" varchar CHECK("status" IN ('ONGOING', 'RESOLEVED')) NOT NULL,
"count" integer NOT NULL,
"lastSeen" datetime NOT NULL,
CONSTRAINT "REL_6c7309a7ac3112d264f5d7b49f" UNIQUE ("flowId"),
CONSTRAINT "fk_issue_flow_id" FOREIGN KEY ("flowId") REFERENCES "flow" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
CONSTRAINT "fk_issue_project_id" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE ON UPDATE RESTRICT
)
`);
await queryRunner.query(`
CREATE UNIQUE INDEX "idx_issue_flow_id" ON "issue" ("flowId")
`);
await queryRunner.query(`
CREATE INDEX "idx_issue_project_id_flow_id" ON "issue" ("projectId", "flowId")
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "idx_issue_project_id_flow_id"
`);
await queryRunner.query(`
DROP INDEX "idx_issue_flow_id"
`);
await queryRunner.query(`
DROP TABLE "issue"
`);
}

}
2 changes: 2 additions & 0 deletions packages/server/api/src/app/database/sqlite-connection.ts
Expand Up @@ -44,6 +44,7 @@ import { AddUniqueNameToFolderSqlite1713645171373 } from './migration/sqlite/171
import { AddFeatureFlagsToPlatform1714137103728 } from './migration/sqlite/1714137103728-AddFeatureFlagsToPlatform'
import { system, SystemProp } from '@activepieces/server-shared'
import { ApEdition, ApEnvironment } from '@activepieces/shared'
import { AddIssueEntitySqlite1714255928781 } from './migration/sqlite/1714255928781-AddIssueEntitySqlite'


const getSqliteDatabaseFilePath = (): string => {
Expand Down Expand Up @@ -106,6 +107,7 @@ const getMigrations = (): (new () => MigrationInterface)[] => {
AddPlatformRole1713271221154,
AddUniqueNameToFolderSqlite1713645171373,
AddFeatureFlagsToPlatform1714137103728,
AddIssueEntitySqlite1714255928781,
]
const edition = getEdition()
if (edition !== ApEdition.COMMUNITY) {
Expand Down
27 changes: 27 additions & 0 deletions packages/server/api/src/app/ee/issues/issues-controller.ts
@@ -0,0 +1,27 @@
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { issuesService } from './issues-service'
import { ListIssuesParams } from '@activepieces/ee-shared'
import { Permission, PrincipalType } from '@activepieces/shared'

export const issuesController: FastifyPluginAsyncTypebox = async (app) => {
app.get('/', ListIssuesRequest, async (req) => {
return issuesService.list({
projectId: req.query.projectId,
cursor: req.query.cursor,
limit: req.query.limit ?? 10,
})
})

}

const ListIssuesRequest = {
config: {
allowedPrincipals: [
PrincipalType.USER,
],
permission: Permission.READ_ACTIVITY,
},
schema: {
querystring: ListIssuesParams,
},
}
78 changes: 78 additions & 0 deletions packages/server/api/src/app/ee/issues/issues-entity.ts
@@ -0,0 +1,78 @@
import { EntitySchema } from 'typeorm'
import {
ApIdSchema,
BaseColumnSchemaPart,
TIMESTAMP_COLUMN_TYPE,
} from '../../database/database-common'
import {
IssueStatus,
Issue,
} from '@activepieces/ee-shared'
import { Flow, Project } from '@activepieces/shared'


type IssueSchema = Issue & {
project: Project
flow: Flow
}

export const IssueEntity = new EntitySchema<IssueSchema>({
name: 'issue',
columns: {
...BaseColumnSchemaPart,
projectId: {
...ApIdSchema,
},
flowId: {
...ApIdSchema,
},
status: {
type: String,
enum: IssueStatus,
},
count: {
type: Number,
},
lastSeen: {
type: TIMESTAMP_COLUMN_TYPE,
}
},
indices: [
{
name: 'idx_issue_flow_id',
unique: true,
columns: ['flowId'],
},
{
name: 'idx_issue_project_id_flow_id',
unique: false,
columns: ['projectId', 'flowId'],
},
],
relations: {
flow: {
type: 'one-to-one',
target: 'flow',
cascade: true,
onDelete: 'CASCADE',
joinColumn: {
name: 'flowId',
referencedColumnName: 'id',
foreignKeyConstraintName: 'fk_issue_flow_id',
},
},
project: {
type: 'many-to-one',
target: 'project',
cascade: true,
onUpdate: 'RESTRICT',
onDelete: 'CASCADE',
joinColumn: {
name: 'projectId',
foreignKeyConstraintName: 'fk_issue_project_id',
},
},
},
})


6 changes: 6 additions & 0 deletions packages/server/api/src/app/ee/issues/issues-module.ts
@@ -0,0 +1,6 @@
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { issuesController } from './issues-controller'

export const issuesModule: FastifyPluginAsyncTypebox = async (app) => {
await app.register(issuesController, { prefix: '/v1/issues' })
}
101 changes: 101 additions & 0 deletions packages/server/api/src/app/ee/issues/issues-service.ts
@@ -0,0 +1,101 @@
import { databaseConnection } from '../../database/database-connection'
import { buildPaginator } from '../../helper/pagination/build-paginator'
import { paginationHelper } from '../../helper/pagination/pagination-utils'
import { Issue, ListIssuesParams, UpdateIssueRequest, IssueStatus, PopulatedIssue } from '@activepieces/ee-shared'
import { apId, SeekPage, isNil, ActivepiecesError, ErrorCode, spreadIfDefined } from '@activepieces/shared'
import { IssueEntity } from './issues-entity'
import dayjs from 'dayjs'
import { flowService } from '../../flows/flow/flow.service'
import { flowVersionService } from '../../flows/flow-version/flow-version.service'
const repo = databaseConnection.getRepository(IssueEntity)

export const issuesService = {
async add({ projectId, flowId }: { flowId: string, projectId: string }): Promise<void> {
await repo.createQueryBuilder()
.insert()
.into(IssueEntity)
.values({
projectId,
flowId,
id: apId(),
lastSeen: dayjs().toISOString(),
count: 0,
status: IssueStatus.ONGOING,
created: dayjs().toISOString(),
updated: dayjs().toISOString(),
})
.orIgnore()
.execute();

await this.update({
projectId: projectId,
flowId: flowId,
status: IssueStatus.ONGOING,
})
},
async get(projectId: string, flowId: string): Promise<Issue | null> {
return repo.findOneBy({
projectId: projectId,
flowId: flowId,
})
},

async getOrThrow(projectId: string, flowId: string): Promise<Issue> {
const issue = await repo.findOneBy({
projectId: projectId,
flowId: flowId,
})
if (isNil(issue)) {
throw new ActivepiecesError({
code: ErrorCode.ENTITY_NOT_FOUND,
params: {
message: `issue not found`,
},
})
}
return issue
},
async list({ projectId, cursor, limit }: ListIssuesParams): Promise<SeekPage<PopulatedIssue>> {
const decodedCursor = paginationHelper.decodeCursor(cursor ?? null)
const paginator = buildPaginator({
entity: IssueEntity,
query: {
limit,
order: 'ASC',
afterCursor: decodedCursor.nextCursor,
beforeCursor: decodedCursor.previousCursor,
},
})

const query = repo.createQueryBuilder(IssueEntity.options.name).where({
projectId,
})

const { data, cursor: newCursor } = await paginator.paginate(query)

const populatedIssues = await Promise.all(data.map(async issue => {
const flowVersion = await flowVersionService.getLatestLockedVersionOrThrow(issue.flowId);
return {
...issue,
flowDisplayName: flowVersion.displayName,
}
}))
return paginationHelper.createPage<PopulatedIssue>(populatedIssues, newCursor)
},

// Updates the status of the issue and updates the coloumns `count` and `lastSeen` accordingly.
async update({ projectId, flowId, status }: UpdateIssueRequest): Promise<void> {
if (status != IssueStatus.RESOLEVED) {
await repo.increment({ projectId, flowId }, 'count', 1);
}
await repo.update({
projectId,
flowId,
}, {
...spreadIfDefined('lastSeen', status !== IssueStatus.RESOLEVED ? dayjs().toISOString() : undefined),
...spreadIfDefined('count', status === IssueStatus.RESOLEVED ? 0 : undefined),
status,
updated: new Date().toISOString(),
})
},
}

0 comments on commit eba8b0c

Please sign in to comment.