Skip to content

Commit

Permalink
Merge branch 'link-folders' of https://github.com/dubinc/dub into lin…
Browse files Browse the repository at this point in the history
…k-folders
  • Loading branch information
devkiran committed Dec 23, 2024
2 parents 5b64be1 + a05d2c0 commit c522e6e
Show file tree
Hide file tree
Showing 92 changed files with 2,044 additions and 871 deletions.
11 changes: 4 additions & 7 deletions apps/web/app/api/admin/links/ban/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { linkCache } from "@/lib/api/links/cache";
import { withAdmin } from "@/lib/auth";
import { updateConfig } from "@/lib/edge-config";
import { formatRedisLink, redis } from "@/lib/upstash";
import { domainKeySchema } from "@/lib/zod/schemas/links";
import { prisma } from "@dub/prisma";
import {
Expand Down Expand Up @@ -34,12 +34,9 @@ export const DELETE = withAdmin(async ({ searchParams }) => {
projectId: LEGAL_WORKSPACE_ID,
},
}),
redis.hset(link.domain.toLowerCase(), {
[link.key.toLowerCase()]: {
...(await formatRedisLink(link)),
projectId: LEGAL_WORKSPACE_ID,
},
}),

linkCache.set({ ...link, projectId: LEGAL_WORKSPACE_ID }),

urlDomain &&
updateConfig({
key: "domains",
Expand Down
13 changes: 10 additions & 3 deletions apps/web/app/api/admin/links/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ export const GET = withAdmin(async ({ searchParams }) => {

const response = await prisma.link.findMany({
where: {
userId: {
not: LEGAL_USER_ID,
},
OR: [
{
userId: {
not: LEGAL_USER_ID,
},
},
{
userId: null,
},
],
...(domain
? { domain }
: {
Expand Down
9 changes: 7 additions & 2 deletions apps/web/app/api/cron/cleanup/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteDomainAndLinks } from "@/lib/api/domains";
import { markDomainAsDeleted } from "@/lib/api/domains";
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { bulkDeleteLinks } from "@/lib/api/links/bulk-delete-links";
import { verifyVercelSignature } from "@/lib/cron/verify-vercel";
Expand Down Expand Up @@ -71,7 +71,12 @@ export async function GET(req: Request) {
// Delete the domains
if (domains.length > 0) {
await Promise.all(
domains.map((domain) => deleteDomainAndLinks(domain.slug)),
domains.map(({ slug }) =>
markDomainAsDeleted({
domain: slug,
workspaceId: E2E_WORKSPACE_ID,
}),
),
);
}

Expand Down
132 changes: 132 additions & 0 deletions apps/web/app/api/cron/domains/delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { queueDomainDeletion } from "@/lib/api/domains/queue";
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { linkCache } from "@/lib/api/links/cache";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { storage } from "@/lib/storage";
import { recordLink } from "@/lib/tinybird/record-link";
import { prisma } from "@dub/prisma";
import { R2_URL } from "@dub/utils";
import { z } from "zod";

export const dynamic = "force-dynamic";

const schema = z.object({
domain: z.string(),
workspaceId: z.string(),
});

// POST /api/cron/domains/delete
export async function POST(req: Request) {
try {
const body = await req.json();

await verifyQstashSignature(req, body);

const { domain, workspaceId } = schema.parse(body);

const domainRecord = await prisma.domain.findUnique({
where: {
slug: domain,
},
});

if (!domainRecord) {
return new Response(`Domain ${domain} not found. Skipping...`);
}

const links = await prisma.link.findMany({
where: {
domain,
},
include: {
tags: true,
},
take: 100, // TODO: We can adjust this number based on the performance
});

if (links.length === 0) {
return new Response("No more links to delete. Exiting...");
}

const response = await Promise.allSettled([
// Remove the link from Redis
linkCache.deleteMany(links),

// Record link in the Tinybird
recordLink(
links.map((link) => ({
link_id: link.id,
domain: link.domain,
key: link.key,
url: link.url,
tag_ids: link.tags.map((tag) => tag.id),
folder_id: link.folderId,
program_id: link.programId ?? "",
workspace_id: workspaceId,
created_at: link.createdAt,
deleted: true,
})),
),

// Remove image from R2 storage if it exists
links
.filter((link) => link.image?.startsWith(`${R2_URL}/images/${link.id}`))
.map((link) => storage.delete(link.image!.replace(`${R2_URL}/`, ""))),

// Remove the link from MySQL
prisma.link.deleteMany({
where: {
id: { in: links.map((link) => link.id) },
},
}),
]);

console.log(response);

response.forEach((promise) => {
if (promise.status === "rejected") {
console.error("deleteDomainAndLinks", {
reason: promise.reason,
domain,
workspaceId,
});
}
});

const remainingLinks = await prisma.link.count({
where: {
domain,
},
});

console.log("remainingLinks", remainingLinks);

if (remainingLinks > 0) {
await queueDomainDeletion({
workspaceId,
domain,
delay: 2,
});
return new Response(
`Deleted ${links.length} links, ${remainingLinks} remaining. Starting next batch...`,
);
}

// After all links are deleted, delete the domain and image
await Promise.all([
prisma.domain.delete({
where: {
slug: domain,
},
}),
domainRecord.logo &&
storage.delete(domainRecord.logo.replace(`${R2_URL}/`, "")),
]);

return new Response(
`Deleted ${links.length} links, no more links remaining. Domain deleted.`,
);
} catch (error) {
return handleAndReturnErrorResponse(error);
}
}
8 changes: 6 additions & 2 deletions apps/web/app/api/cron/domains/transfer/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { linkCache } from "@/lib/api/links/cache";
import { qstash } from "@/lib/cron";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { recordLink } from "@/lib/tinybird";
import z from "@/lib/zod";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK, log } from "@dub/utils";
import { NextResponse } from "next/server";
import { sendDomainTransferredEmail, updateLinksInRedis } from "./utils";
import { sendDomainTransferredEmail } from "./utils";

const schema = z.object({
currentWorkspaceId: z.string(),
Expand Down Expand Up @@ -60,7 +61,10 @@ export async function POST(req: Request) {
where: { linkId: { in: linkIds } },
}),

updateLinksInRedis({ links, newWorkspaceId, domain }),
// Update links in redis
linkCache.mset(
links.map((link) => ({ ...link, projectId: newWorkspaceId })),
),

// Remove the webhooks associated with the links
prisma.linkWebhook.deleteMany({
Expand Down
35 changes: 0 additions & 35 deletions apps/web/app/api/cron/domains/transfer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,7 @@
import { formatRedisLink, redis } from "@/lib/upstash";
import { prisma } from "@dub/prisma";
import { Link } from "@dub/prisma/client";
import { sendEmail } from "emails";
import DomainTransferred from "emails/domain-transferred";

// Update links in redis
export const updateLinksInRedis = async ({
newWorkspaceId,
domain,
links,
}: {
newWorkspaceId: string;
domain: string;
links: Link[];
}) => {
const pipeline = redis.pipeline();

const formatedLinks = await Promise.all(
links.map(async (link) => {
return {
...(await formatRedisLink(link)),
projectId: newWorkspaceId,
key: link.key.toLowerCase(),
};
}),
);

formatedLinks.map((formatedLink) => {
const { key, ...rest } = formatedLink;

pipeline.hset(domain.toLowerCase(), {
[formatedLink.key]: rest,
});
});

await pipeline.exec();
};

// Send email to the owner after the domain transfer is completed
export const sendDomainTransferredEmail = async ({
domain,
Expand Down
89 changes: 89 additions & 0 deletions apps/web/app/api/cron/domains/update/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { queueDomainUpdate } from "@/lib/api/domains/queue";
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { linkCache } from "@/lib/api/links/cache";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { recordLink } from "@/lib/tinybird";
import { prisma } from "@dub/prisma";
import { z } from "zod";

export const dynamic = "force-dynamic";

const schema = z.object({
newDomain: z.string(),
oldDomain: z.string(),
workspaceId: z.string(),
page: z.number(),
});

const pageSize = 100;

// POST /api/cron/domains/update
export async function POST(req: Request) {
try {
const body = await req.json();

await verifyQstashSignature(req, body);

const { newDomain, oldDomain, workspaceId, page } = schema.parse(body);

const newDomainRecord = await prisma.domain.findUnique({
where: {
slug: newDomain,
},
});

if (!newDomainRecord) {
return new Response(`Domain ${newDomain} not found. Skipping update...`);
}

const links = await prisma.link.findMany({
where: {
domain: newDomain,
},
include: {
tags: true,
},
skip: (page - 1) * pageSize,
take: pageSize,
});

if (links.length === 0) {
return new Response("No more links to update. Exiting...");
}

await Promise.all([
// rename redis keys
linkCache.rename({
links,
oldDomain,
}),

// update links in Tinybird
recordLink(
links.map((link) => ({
link_id: link.id,
domain: link.domain,
key: link.key,
url: link.url,
tag_ids: link.tags.map((tag) => tag.tagId),
folder_id: link.folderId,
program_id: link.programId ?? "",
workspace_id: link.projectId,
created_at: link.createdAt,
})),
),
]);

await queueDomainUpdate({
workspaceId,
oldDomain,
newDomain,
page: page + 1,
delay: 2,
});

return new Response("Domain's links updated.");
} catch (error) {
return handleAndReturnErrorResponse(error);
}
}
8 changes: 6 additions & 2 deletions apps/web/app/api/cron/domains/verify/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteDomainAndLinks } from "@/lib/api/domains";
import { markDomainAsDeleted } from "@/lib/api/domains";
import { limiter } from "@/lib/cron/limiter";
import { prisma } from "@dub/prisma";
import { log } from "@dub/utils";
Expand Down Expand Up @@ -99,9 +99,13 @@ export const handleDomainUpdates = async ({
type: "cron",
});
}

// else, delete the domain
return await Promise.allSettled([
deleteDomainAndLinks(domain).then(async () => {
markDomainAsDeleted({
domain,
workspaceId: workspace.id,
}).then(async () => {
// if the deleted domain was primary, make another domain primary
if (primary) {
const anotherDomain = await prisma.domain.findFirst({
Expand Down
Loading

0 comments on commit c522e6e

Please sign in to comment.