feature(dashboard): add integrations and notifications

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-10-02 22:12:05 +02:00
parent d920f6951c
commit f65a633403
94 changed files with 3692 additions and 127 deletions

View File

@@ -0,0 +1,137 @@
import { z } from 'zod';
import { BASE_INTEGRATIONS, db } from '@openpanel/db';
import { getSlackInstallUrl } from '@openpanel/integrations/src/slack';
import {
type ISlackConfig,
zCreateDiscordIntegration,
zCreateSlackIntegration,
zCreateWebhookIntegration,
} from '@openpanel/validation';
import { getOrganizationAccessCached } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const integrationRouter = createTRPCRouter({
get: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
const integration = await db.integration.findUniqueOrThrow({
where: {
id: input.id,
},
});
const access = await getOrganizationAccessCached({
userId: ctx.session.userId,
organizationId: integration.organizationId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return integration;
}),
list: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.query(async ({ input }) => {
const integrations = await db.integration.findMany({
where: {
organizationId: input.organizationId,
},
});
return [...BASE_INTEGRATIONS, ...integrations];
}),
createOrUpdateSlack: protectedProcedure
.input(zCreateSlackIntegration)
.mutation(async ({ input }) => {
if (input.id) {
const res = await db.integration.update({
where: {
id: input.id,
organizationId: input.organizationId,
},
data: {
name: input.name,
// This is empty and will be filled by the webhook
config: {} as ISlackConfig,
},
});
return {
...res,
slackInstallUrl: await getSlackInstallUrl({
integrationId: res.id,
organizationId: input.organizationId,
}),
};
}
const res = await db.integration.create({
data: {
name: input.name,
organizationId: input.organizationId,
// This is empty and will be filled by the webhook
config: {} as ISlackConfig,
},
});
return {
...res,
slackInstallUrl: await getSlackInstallUrl({
integrationId: res.id,
organizationId: input.organizationId,
}),
};
}),
createOrUpdate: protectedProcedure
.input(z.union([zCreateDiscordIntegration, zCreateWebhookIntegration]))
.mutation(async ({ input }) => {
if (input.id) {
return db.integration.update({
where: {
id: input.id,
organizationId: input.organizationId,
},
data: {
name: input.name,
config: input.config,
},
});
}
return db.integration.create({
data: {
name: input.name,
organizationId: input.organizationId,
config: input.config,
},
});
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input: { id }, ctx }) => {
const integration = await db.integration.findUniqueOrThrow({
where: {
id,
},
});
const access = await getOrganizationAccessCached({
userId: ctx.session.userId,
organizationId: integration.organizationId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.integration.delete({
where: {
id,
},
});
}),
});

View File

@@ -0,0 +1,150 @@
import { z } from 'zod';
import {
APP_NOTIFICATION_INTEGRATION_ID,
BASE_INTEGRATIONS,
EMAIL_NOTIFICATION_INTEGRATION_ID,
db,
getNotificationRulesByProjectId,
isBaseIntegration,
} from '@openpanel/db';
import { zCreateNotificationRule } from '@openpanel/validation';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const notificationRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input }) => {
return db.notification.findMany({
where: {
projectId: input.projectId,
sendToApp: true,
},
orderBy: {
createdAt: 'desc',
},
include: {
integration: {
include: {
notificationRules: {
select: {
name: true,
},
},
},
},
},
take: 100,
});
}),
rules: protectedProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input }) => {
return db.notificationRule
.findMany({
where: {
projectId: input.projectId,
},
include: {
integrations: true,
},
orderBy: {
createdAt: 'desc',
},
})
.then((rules) => {
return rules.map((rule) => {
return {
...rule,
integrations: [
...BASE_INTEGRATIONS.filter((integration) => {
return (
(integration.id === APP_NOTIFICATION_INTEGRATION_ID &&
rule.sendToApp) ||
(integration.id === EMAIL_NOTIFICATION_INTEGRATION_ID &&
rule.sendToEmail)
);
}),
...rule.integrations,
],
};
});
});
}),
createOrUpdateRule: protectedProcedure
.input(zCreateNotificationRule)
.mutation(async ({ input }) => {
// Clear the cache for the project
await getNotificationRulesByProjectId.clear(input.projectId);
if (input.id) {
return db.notificationRule.update({
where: {
id: input.id,
},
data: {
name: input.name,
projectId: input.projectId,
sendToApp: !!input.integrations.find(
(id) => id === APP_NOTIFICATION_INTEGRATION_ID,
),
sendToEmail: !!input.integrations.find(
(id) => id === EMAIL_NOTIFICATION_INTEGRATION_ID,
),
integrations: {
set: input.integrations
.filter((id) => !isBaseIntegration(id))
.map((id) => ({ id })),
},
config: input.config,
},
});
}
return db.notificationRule.create({
data: {
name: input.name,
projectId: input.projectId,
sendToApp: !!input.integrations.find(
(id) => id === APP_NOTIFICATION_INTEGRATION_ID,
),
sendToEmail: !!input.integrations.find(
(id) => id === EMAIL_NOTIFICATION_INTEGRATION_ID,
),
integrations: {
connect: input.integrations
.filter((id) => !isBaseIntegration(id))
.map((id) => ({ id })),
},
config: input.config,
},
});
}),
deleteRule: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input: { id }, ctx }) => {
const rule = await db.notificationRule.findUniqueOrThrow({
where: {
id,
},
});
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: rule.projectId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.notificationRule.delete({
where: {
id,
},
});
}),
});

View File

@@ -1,6 +1,11 @@
import { z } from 'zod';
import { db, getId, getProjectsByOrganizationSlug } from '@openpanel/db';
import {
db,
getId,
getProjectByIdCached,
getProjectsByOrganizationSlug,
} from '@openpanel/db';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
@@ -34,8 +39,7 @@ export const projectRouter = createTRPCRouter({
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.project.update({
const res = await db.project.update({
where: {
id: input.id,
},
@@ -43,6 +47,8 @@ export const projectRouter = createTRPCRouter({
name: input.name,
},
});
await getProjectByIdCached.clear(input.id);
return res;
}),
create: protectedProcedure
.input(