Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add webhook to link builder and link APIs #1340

Merged
merged 38 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
861cf54
add webhookIds to create and update link
devkiran Oct 8, 2024
fbcf855
add Copy Webhook ID button
devkiran Oct 8, 2024
5a08b73
add webook selector
devkiran Oct 8, 2024
ea1a2d4
return the webhooks with links info
devkiran Oct 8, 2024
d7899dc
fix build
devkiran Oct 8, 2024
19f1975
Merge branch 'main' into link-webhook
devkiran Oct 8, 2024
52410f5
fix the tests
devkiran Oct 8, 2024
f84d6cc
fix the webhook selector
devkiran Oct 8, 2024
511d0ed
update schema
devkiran Oct 8, 2024
4a893d0
fix webhook icon
steven-tey Oct 9, 2024
9bff1b3
Merge branch 'side-nav-layout' into link-webhook
steven-tey Oct 9, 2024
38c915c
Merge branch 'main' into link-webhook
steven-tey Oct 10, 2024
eca2419
Merge branch 'main' into link-webhook
steven-tey Oct 11, 2024
075b2dd
Merge branch 'main' into link-webhook
steven-tey Oct 11, 2024
ff73df9
Merge branch 'main' into link-webhook
steven-tey Oct 11, 2024
634d6ad
simplify & fixes
steven-tey Oct 12, 2024
21b0ffd
fix tests
steven-tey Oct 12, 2024
870efab
fix more tests
steven-tey Oct 12, 2024
38801a9
Merge branch 'main' into link-webhook
steven-tey Oct 12, 2024
3e92b25
fix more tests
steven-tey Oct 12, 2024
7831e9b
Merge branch 'link-webhook' of https://github.com/dubinc/dub into lin…
steven-tey Oct 12, 2024
c733938
fix tests
steven-tey Oct 12, 2024
c6a7cf6
fix tests final
steven-tey Oct 12, 2024
7c1b2b2
fix E2E_CUSTOMER_ID
steven-tey Oct 12, 2024
0f61946
Merge branch 'main' into link-webhook
steven-tey Oct 12, 2024
1e330db
Merge branch 'main' into link-webhook
steven-tey Oct 12, 2024
4a96988
Merge branch 'main' into link-webhook
steven-tey Oct 12, 2024
3c67205
improve tags & webhook checks for bulk link creation
steven-tey Oct 12, 2024
4585c62
fix formatRedisLink
steven-tey Oct 13, 2024
2ead4ed
Merge branch 'main' into link-webhook
steven-tey Oct 13, 2024
564d60b
fix DEFAULT_LINK_PROPS
steven-tey Oct 13, 2024
650b889
Merge branch 'main' into link-webhook
steven-tey Oct 14, 2024
3008f6c
add empty state for webhooks selector
devkiran Oct 14, 2024
f4444d0
small fix
devkiran Oct 14, 2024
3ae0cdd
Fix combobox empty state
TWilson023 Oct 14, 2024
9214f8a
Merge branch 'main' into link-webhook
steven-tey Oct 14, 2024
e7ef8a4
update
devkiran Oct 14, 2024
095e141
Merge branch 'main' into link-webhook
steven-tey Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 97 additions & 41 deletions apps/web/app/api/links/bulk/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { DubApiError, exceededLimitError } from "@/lib/api/errors";
import { bulkCreateLinks, combineTagIds, processLink } from "@/lib/api/links";
import {
bulkCreateLinks,
checkIfLinksHaveTags,
checkIfLinksHaveWebhooks,
combineTagIds,
processLink,
} from "@/lib/api/links";
import { bulkDeleteLinks } from "@/lib/api/links/bulk-delete-links";
import { bulkUpdateLinks } from "@/lib/api/links/bulk-update-links";
import { throwIfLinksUsageExceeded } from "@/lib/api/links/usage-checks";
Expand Down Expand Up @@ -86,49 +92,99 @@ export const POST = withWorkspace(
code,
}));

// filter out tags that don't belong to the workspace
const workspaceTags = await prisma.tag.findMany({
where: {
projectId: workspace.id,
},
select: {
id: true,
name: true,
},
});
const workspaceTagIds = workspaceTags.map(({ id }) => id);
const workspaceTagNames = workspaceTags.map(({ name }) => name);
validLinks.forEach((link, index) => {
const combinedTagIds =
combineTagIds({
tagId: link.tagId,
tagIds: link.tagIds,
}) ?? [];
const invalidTagIds = combinedTagIds.filter(
(id) => !workspaceTagIds.includes(id),
);
if (invalidTagIds.length > 0) {
// remove link from validLinks and add error to errorLinks
validLinks = validLinks.filter((_, i) => i !== index);
errorLinks.push({
error: `Invalid tagIds detected: ${invalidTagIds.join(", ")}`,
code: "unprocessable_entity",
link,
});
}
if (checkIfLinksHaveTags(validLinks)) {
// filter out tags that don't belong to the workspace
const tagIds = validLinks
.map((link) =>
combineTagIds({ tagId: link.tagId, tagIds: link.tagIds }),
)
.flat()
.filter(Boolean) as string[];
const tagNames = validLinks
.map((link) => link.tagNames)
.flat()
.filter(Boolean) as string[];

const workspaceTags = await prisma.tag.findMany({
where: {
projectId: workspace.id,
...(tagIds.length > 0 ? { id: { in: tagIds } } : {}),
...(tagNames.length > 0 ? { name: { in: tagNames } } : {}),
},
select: {
id: true,
name: true,
},
});
const workspaceTagIds = workspaceTags.map(({ id }) => id);
const workspaceTagNames = workspaceTags.map(({ name }) => name);
validLinks.forEach((link, index) => {
const combinedTagIds =
combineTagIds({
tagId: link.tagId,
tagIds: link.tagIds,
}) ?? [];
const invalidTagIds = combinedTagIds.filter(
(id) => !workspaceTagIds.includes(id),
);
if (invalidTagIds.length > 0) {
// remove link from validLinks and add error to errorLinks
validLinks = validLinks.filter((_, i) => i !== index);
errorLinks.push({
error: `Invalid tagIds detected: ${invalidTagIds.join(", ")}`,
code: "unprocessable_entity",
link,
});
}

const invalidTagNames = link.tagNames?.filter(
(name) => !workspaceTagNames.includes(name),
);
if (invalidTagNames?.length) {
validLinks = validLinks.filter((_, i) => i !== index);
errorLinks.push({
error: `Invalid tagNames detected: ${invalidTagNames.join(", ")}`,
code: "unprocessable_entity",
link,
const invalidTagNames = link.tagNames?.filter(
(name) => !workspaceTagNames.includes(name),
);
if (invalidTagNames?.length) {
validLinks = validLinks.filter((_, i) => i !== index);
errorLinks.push({
error: `Invalid tagNames detected: ${invalidTagNames.join(", ")}`,
code: "unprocessable_entity",
link,
});
}
});
}

if (checkIfLinksHaveWebhooks(validLinks)) {
if (workspace.plan === "free" || workspace.plan === "pro") {
throw new DubApiError({
code: "forbidden",
message:
"You can only use webhooks on a Business plan and above. Upgrade to Business to use this feature.",
});
}
});

const webhookIds = validLinks
.map((link) => link.webhookIds)
.flat()
.filter((id): id is string => id !== null);

const webhooks = await prisma.webhook.findMany({
where: { projectId: workspace.id, id: { in: webhookIds } },
});

const workspaceWebhookIds = webhooks.map(({ id }) => id);

validLinks.forEach((link, index) => {
const invalidWebhookIds = link.webhookIds?.filter(
(id) => !workspaceWebhookIds.includes(id),
);
if (invalidWebhookIds && invalidWebhookIds.length > 0) {
validLinks = validLinks.filter((_, i) => i !== index);
errorLinks.push({
error: `Invalid webhookIds detected: ${invalidWebhookIds.join(", ")}`,
code: "unprocessable_entity",
link,
});
}
});
}

const validLinksResponse =
validLinks.length > 0 ? await bulkCreateLinks({ links: validLinks }) : [];
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/api/links/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const GET = withWorkspace(
showArchived,
withTags,
includeUser,
includeWebhooks,
linkIds,
} = getLinksQuerySchemaExtended.parse(searchParams);

Expand All @@ -52,6 +53,7 @@ export const GET = withWorkspace(
showArchived,
withTags,
includeUser,
includeWebhooks,
linkIds,
});

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/tags/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const GET = withWorkspace(
},
);

// POST /api/workspaces/[idOrSlug]/tags - create a tag for a workspace
// POST /api/tags - create a tag for a workspace
export const POST = withWorkspace(
async ({ req, workspace, headers }) => {
const tagsCount = await prisma.tag.count({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
"use client";

import { clientAccessCheck } from "@/lib/api/tokens/permissions";
import useWebhooks from "@/lib/swr/use-webhooks";
import useWorkspace from "@/lib/swr/use-workspace";
import { WebhookProps } from "@/lib/types";
import EmptyState from "@/ui/shared/empty-state";
import WebhookCard from "@/ui/webhooks/webhook-card";
import WebhookPlaceholder from "@/ui/webhooks/webhook-placeholder";
import { Button, TooltipContent } from "@dub/ui";
import { InfoTooltip } from "@dub/ui/src/tooltip";
import { fetcher } from "@dub/utils";
import { Webhook } from "lucide-react";
import { redirect, useRouter } from "next/navigation";
import useSWR from "swr";

export default function WebhooksPageClient() {
const router = useRouter();
const {
id: workspaceId,
slug,
plan,
role,
conversionEnabled,
flags,
} = useWorkspace();
const { slug, plan, role, conversionEnabled, flags } = useWorkspace();

if (!flags?.webhooks) {
redirect(`/${slug}/settings`);
}

const { data: webhooks, isLoading } = useSWR<WebhookProps[]>(
`/api/webhooks?workspaceId=${workspaceId}`,
fetcher,
);
const { webhooks, isLoading } = useWebhooks();

const { error: permissionsError } = clientAccessCheck({
action: "webhooks.write",
Expand Down
29 changes: 23 additions & 6 deletions apps/web/lib/api/links/bulk-create-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import {
import { waitUntil } from "@vercel/functions";
import { propagateBulkLinkChanges } from "./propagate-bulk-link-changes";
import { updateLinksUsage } from "./update-links-usage";
import { combineTagIds, LinkWithTags, transformLink } from "./utils";
import {
checkIfLinksHaveTags,
checkIfLinksHaveWebhooks,
combineTagIds,
LinkWithTags,
transformLink,
} from "./utils";

export async function bulkCreateLinks({
links,
Expand All @@ -18,19 +24,18 @@ export async function bulkCreateLinks({
}) {
if (links.length === 0) return [];

const hasTags = links.some(
(link) => link.tagNames?.length || link.tagIds?.length || link.tagId,
);
const hasTags = checkIfLinksHaveTags(links);
const hasWebhooks = checkIfLinksHaveWebhooks(links);

let createdLinks: LinkWithTags[] = [];

if (hasTags) {
if (hasTags || hasWebhooks) {
// create links via Promise.all (because createMany doesn't return the created links)
// @see https://github.com/prisma/prisma/issues/8131#issuecomment-997667070
// there is createManyAndReturn but it's not available for MySQL :(
// @see https://www.prisma.io/docs/orm/reference/prisma-client-reference#createmanyandreturn
createdLinks = await Promise.all(
links.map(({ tagId, tagIds, tagNames, ...link }) => {
links.map(({ tagId, tagIds, tagNames, webhookIds, ...link }) => {
const shortLink = linkConstructor({
domain: link.domain,
key: link.key,
Expand Down Expand Up @@ -82,6 +87,17 @@ export async function bulkCreateLinks({
},
},
}),

...(webhookIds &&
webhookIds.length > 0 && {
webhooks: {
createMany: {
data: webhookIds.map((webhookId) => ({
webhookId,
})),
},
},
}),
},
include: {
tags: {
Expand All @@ -95,6 +111,7 @@ export async function bulkCreateLinks({
},
},
},
webhooks: hasWebhooks,
},
});
}),
Expand Down
16 changes: 15 additions & 1 deletion apps/web/lib/api/links/create-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function createLink(link: ProcessedLinkProps) {
const { utm_source, utm_medium, utm_campaign, utm_term, utm_content } =
getParamsFromURL(url);

const { tagId, tagIds, tagNames, ...rest } = link;
const { tagId, tagIds, tagNames, webhookIds, ...rest } = link;

const response = await prisma.link.create({
data: {
Expand Down Expand Up @@ -69,6 +69,18 @@ export async function createLink(link: ProcessedLinkProps) {
},
},
}),

// Webhooks
...(webhookIds &&
webhookIds.length > 0 && {
webhooks: {
createMany: {
data: webhookIds.map((webhookId) => ({
webhookId,
})),
},
},
}),
},
include: {
tags: {
Expand All @@ -82,6 +94,7 @@ export async function createLink(link: ProcessedLinkProps) {
},
},
},
webhooks: webhookIds ? true : false,
},
});

Expand All @@ -93,6 +106,7 @@ export async function createLink(link: ProcessedLinkProps) {
redis.hset(link.domain.toLowerCase(), {
[link.key.toLowerCase()]: await formatRedisLink(response),
}),

// record link in Tinybird
recordLink({
link_id: response.id,
Expand Down
4 changes: 3 additions & 1 deletion apps/web/lib/api/links/get-links-for-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function getLinksForWorkspace({
showArchived,
withTags,
includeUser,
includeWebhooks,
linkIds,
}: z.infer<typeof getLinksQuerySchemaExtended> & {
workspaceId: string;
Expand Down Expand Up @@ -64,7 +65,6 @@ export async function getLinksForWorkspace({
...(linkIds && { id: { in: linkIds } }),
},
include: {
user: includeUser,
tags: {
include: {
tag: {
Expand All @@ -76,6 +76,8 @@ export async function getLinksForWorkspace({
},
},
},
user: includeUser,
webhooks: includeWebhooks,
},
orderBy: {
[sort]: "desc",
Expand Down
Loading
Loading