From edce145a7fc2f2a049d094e9dd6c2f6a338c81e9 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 31 Dec 2024 21:14:37 -0800 Subject: [PATCH 1/9] Add support for tenantId --- apps/web/lib/zod/schemas/links.ts | 22 ++++++++++++++++++++-- packages/prisma/schema/link.prisma | 12 ++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/zod/schemas/links.ts b/apps/web/lib/zod/schemas/links.ts index ce8b0ac23c..6c971da502 100644 --- a/apps/web/lib/zod/schemas/links.ts +++ b/apps/web/lib/zod/schemas/links.ts @@ -57,6 +57,12 @@ const LinksQuerySchema = z.object({ "The search term to filter the links by. The search term will be matched against the short link slug and the destination url.", ), userId: z.string().optional().describe("The user ID to filter the links by."), + tenantId: z + .string() + .optional() + .describe( + "The ID of the tenant that created the link inside your system. If set, will only return links for the specified tenant.", + ), showArchived: booleanQuerySchema .optional() .default("false") @@ -160,9 +166,15 @@ export const createLinkBodySchema = z.object({ .transform((v) => (v?.startsWith("ext_") ? v.slice(4) : v)) .nullish() .describe( - "This is the ID of the link in your database. If set, it can be used to identify the link in the future. Must be prefixed with `ext_` when passed as a query parameter.", + "The ID of the link in your database. If set, it can be used to identify the link in future API requests (must be prefixed with 'ext_' when passed as a query parameter). This key is unique across your workspace.", ) .openapi({ example: "123456" }), + tenantId: z + .string() + .optional() + .describe( + "The ID of the tenant that created the link inside your system. If set, it can be used to fetch all links for a tenant.", + ), prefix: z .string() .optional() @@ -392,7 +404,13 @@ export const LinkSchema = z .string() .nullable() .describe( - "This is the ID of the link in your database that is unique across your workspace. If set, it can be used to identify the link in future API requests. Must be prefixed with 'ext_' when passed as a query parameter.", + "The ID of the link in your database. If set, it can be used to identify the link in future API requests (must be prefixed with 'ext_' when passed as a query parameter). This key is unique across your workspace.", + ), + tenantId: z + .string() + .nullable() + .describe( + "The ID of the tenant that created the link inside your system. If set, it can be used to fetch all links for a tenant.", ), archived: z .boolean() diff --git a/packages/prisma/schema/link.prisma b/packages/prisma/schema/link.prisma index ec27e44e2a..547d7a2b21 100644 --- a/packages/prisma/schema/link.prisma +++ b/packages/prisma/schema/link.prisma @@ -8,7 +8,6 @@ model Link { expiresAt DateTime? // when the link expires – stored on Redis via ttl expiredUrl String? @db.LongText // URL to redirect the user to when the link is expired password String? // password to access the link - externalId String? trackConversion Boolean @default(false) // whether to track conversions or not proxy Boolean @default(false) // Proxy to use custom OG tags (stored on redis) – if false, will use OG tags from target url @@ -44,6 +43,10 @@ model Link { // Relational reference to the project domain projectDomain Domain? @relation(fields: [domain], references: [slug], onUpdate: Cascade, onDelete: Cascade) + // External & tenant IDs (for API usage + multi-tenancy) + externalId String? + tenantId String? + publicStats Boolean @default(false) // whether to show public stats or not clicks Int @default(0) // number of clicks lastClicked DateTime? // when the link was last clicked @@ -71,7 +74,7 @@ model Link { customers Customer[] @@unique([domain, key]) - @@unique([projectId, externalId]) + @@fulltext([key, url]) @@index(trackConversion) @@index(proxy) @@index(password) @@ -79,7 +82,8 @@ model Link { @@index(createdAt(sort: Desc)) @@index(clicks(sort: Desc)) @@index(lastClicked) + @@index(programId) @@index(userId) - @@index([programId]) - @@fulltext([key, url]) + @@unique([projectId, externalId]) + @@index(tenantId) } From 6a62358fb444dc4f3a01a6d55aac6bc7bdf4070b Mon Sep 17 00:00:00 2001 From: Kiran K Date: Tue, 7 Jan 2025 23:49:19 +0530 Subject: [PATCH 2/9] filter the links by tenantId --- apps/web/app/api/links/route.ts | 2 ++ apps/web/lib/api/links/get-links-for-workspace.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/web/app/api/links/route.ts b/apps/web/app/api/links/route.ts index 167c73e670..6f635622d2 100644 --- a/apps/web/app/api/links/route.ts +++ b/apps/web/app/api/links/route.ts @@ -35,6 +35,7 @@ export const GET = withWorkspace( includeWebhooks, includeDashboard, linkIds, + tenantId, } = getLinksQuerySchemaExtended.parse(searchParams); if (domain) { @@ -57,6 +58,7 @@ export const GET = withWorkspace( includeWebhooks, includeDashboard, linkIds, + tenantId, }); return NextResponse.json(response, { diff --git a/apps/web/lib/api/links/get-links-for-workspace.ts b/apps/web/lib/api/links/get-links-for-workspace.ts index 43856a499c..a6d406c9f8 100644 --- a/apps/web/lib/api/links/get-links-for-workspace.ts +++ b/apps/web/lib/api/links/get-links-for-workspace.ts @@ -21,6 +21,7 @@ export async function getLinksForWorkspace({ includeWebhooks, includeDashboard, linkIds, + tenantId, }: z.infer & { workspaceId: string; }) { @@ -65,6 +66,7 @@ export async function getLinksForWorkspace({ : {}), ...(userId && { userId }), ...(linkIds && { id: { in: linkIds } }), + ...(tenantId && { tenantId }), }, include: { tags: { From 0e4d78e5eebbe7350498e684da0f964abf5cba28 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Wed, 8 Jan 2025 00:05:34 +0530 Subject: [PATCH 3/9] Update links.ts --- apps/web/lib/zod/schemas/links.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/zod/schemas/links.ts b/apps/web/lib/zod/schemas/links.ts index 6c971da502..1d530d354d 100644 --- a/apps/web/lib/zod/schemas/links.ts +++ b/apps/web/lib/zod/schemas/links.ts @@ -171,7 +171,8 @@ export const createLinkBodySchema = z.object({ .openapi({ example: "123456" }), tenantId: z .string() - .optional() + .max(255) + .nullish() .describe( "The ID of the tenant that created the link inside your system. If set, it can be used to fetch all links for a tenant.", ), From 80de2645be16ad51ad0c7bf59b044ffd864c106f Mon Sep 17 00:00:00 2001 From: Kiran K Date: Wed, 8 Jan 2025 00:09:06 +0530 Subject: [PATCH 4/9] fix tests --- apps/web/tests/utils/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/tests/utils/schema.ts b/apps/web/tests/utils/schema.ts index 95f8d8c046..1a7eb62b8b 100644 --- a/apps/web/tests/utils/schema.ts +++ b/apps/web/tests/utils/schema.ts @@ -52,6 +52,7 @@ export const expectedLink: Partial & { expiredUrl: null, externalId: null, programId: null, + tenantId: null, }; export const expectedTag: Partial = { From af157ee8edcca9d12c54cc2a0fae407a118aa626 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Jan 2025 14:04:59 -0800 Subject: [PATCH 5/9] improve recordLink + bulkDeleteLinks --- apps/web/app/api/cron/cleanup/route.ts | 10 +++--- apps/web/app/api/cron/domains/delete/route.ts | 20 ++++-------- .../app/api/cron/domains/transfer/route.ts | 11 +------ apps/web/app/api/cron/domains/update/route.ts | 19 ++++------- .../app/api/cron/workspaces/delete/route.ts | 10 +++--- .../app/api/links/[linkId]/transfer/route.ts | 12 +------ apps/web/app/api/links/bulk/route.ts | 12 +++---- .../webhook/customer-subscription-deleted.ts | 16 ++++------ apps/web/app/api/tags/[id]/route.ts | 32 ++++++++----------- .../lib/actions/partners/invite-partner.ts | 1 - apps/web/lib/api/links/bulk-delete-links.ts | 21 +++--------- apps/web/lib/api/links/create-link.ts | 11 +------ apps/web/lib/api/links/delete-link.ts | 19 +++++------ .../api/links/propagate-bulk-link-changes.ts | 13 +------- apps/web/lib/api/links/update-link.ts | 11 +------ apps/web/lib/api/partners/invite-partner.ts | 16 +++------- apps/web/lib/tinybird/record-link.ts | 29 ++++++++++++++++- apps/web/scripts/bulk-delete-links.ts | 28 ++++++---------- apps/web/scripts/sync-links-metadata.ts | 19 ++++------- apps/web/scripts/sync-tag-analytics.ts | 19 ++++------- 20 files changed, 122 insertions(+), 207 deletions(-) diff --git a/apps/web/app/api/cron/cleanup/route.ts b/apps/web/app/api/cron/cleanup/route.ts index aa71130cea..d90c37791d 100644 --- a/apps/web/app/api/cron/cleanup/route.ts +++ b/apps/web/app/api/cron/cleanup/route.ts @@ -31,7 +31,11 @@ export async function GET(req: Request) { }, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, take: 100, }), @@ -63,9 +67,7 @@ export async function GET(req: Request) { }); // Post delete cleanup - await bulkDeleteLinks({ - links, - }); + await bulkDeleteLinks(links); } // Delete the domains diff --git a/apps/web/app/api/cron/domains/delete/route.ts b/apps/web/app/api/cron/domains/delete/route.ts index f75c969080..a50720551c 100644 --- a/apps/web/app/api/cron/domains/delete/route.ts +++ b/apps/web/app/api/cron/domains/delete/route.ts @@ -39,7 +39,11 @@ export async function POST(req: Request) { domain, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, take: 100, // TODO: We can adjust this number based on the performance }); @@ -53,19 +57,7 @@ export async function POST(req: Request) { 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), - workspace_id: workspaceId, - program_id: link.programId ?? "", - created_at: link.createdAt, - deleted: true, - })), - ), + recordLink(links), // Remove image from R2 storage if it exists links diff --git a/apps/web/app/api/cron/domains/transfer/route.ts b/apps/web/app/api/cron/domains/transfer/route.ts index 00db26fbdd..c1d012c460 100644 --- a/apps/web/app/api/cron/domains/transfer/route.ts +++ b/apps/web/app/api/cron/domains/transfer/route.ts @@ -72,16 +72,7 @@ export async function POST(req: Request) { }), recordLink( - links.map((link) => ({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: [], - program_id: link.programId ?? "", - workspace_id: newWorkspaceId, - created_at: link.createdAt, - })), + links.map((link) => ({ ...link, projectId: newWorkspaceId })), ), ]); diff --git a/apps/web/app/api/cron/domains/update/route.ts b/apps/web/app/api/cron/domains/update/route.ts index 8fbad9d4ac..b4c038a396 100644 --- a/apps/web/app/api/cron/domains/update/route.ts +++ b/apps/web/app/api/cron/domains/update/route.ts @@ -41,7 +41,11 @@ export async function POST(req: Request) { domain: newDomain, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, skip: (page - 1) * pageSize, take: pageSize, @@ -59,18 +63,7 @@ export async function POST(req: Request) { }), // 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), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, - })), - ), + recordLink(links), ]); await queueDomainUpdate({ diff --git a/apps/web/app/api/cron/workspaces/delete/route.ts b/apps/web/app/api/cron/workspaces/delete/route.ts index a0bc8413a3..5715773230 100644 --- a/apps/web/app/api/cron/workspaces/delete/route.ts +++ b/apps/web/app/api/cron/workspaces/delete/route.ts @@ -37,7 +37,11 @@ export async function POST(req: Request) { projectId: workspace.id, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, take: 100, // TODO: We can adjust this number based on the performance }); @@ -52,9 +56,7 @@ export async function POST(req: Request) { }, }), - bulkDeleteLinks({ - links, - }), + bulkDeleteLinks(links), ]); console.log(res); diff --git a/apps/web/app/api/links/[linkId]/transfer/route.ts b/apps/web/app/api/links/[linkId]/transfer/route.ts index f9f44c6f91..d39c5a2534 100644 --- a/apps/web/app/api/links/[linkId]/transfer/route.ts +++ b/apps/web/app/api/links/[linkId]/transfer/route.ts @@ -80,17 +80,7 @@ export const POST = withWorkspace( waitUntil( Promise.all([ linkCache.set({ ...link, projectId: newWorkspaceId }), - - recordLink({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: [], - program_id: link.programId ?? "", - workspace_id: newWorkspaceId, - created_at: link.createdAt, - }), + recordLink({ ...link, projectId: newWorkspaceId }), // increment new workspace usage prisma.project.update({ where: { diff --git a/apps/web/app/api/links/bulk/route.ts b/apps/web/app/api/links/bulk/route.ts index db5a774082..27caa97c08 100644 --- a/apps/web/app/api/links/bulk/route.ts +++ b/apps/web/app/api/links/bulk/route.ts @@ -395,7 +395,11 @@ export const DELETE = withWorkspace( ], }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, }); @@ -406,11 +410,7 @@ export const DELETE = withWorkspace( }, }); - waitUntil( - bulkDeleteLinks({ - links, - }), - ); + waitUntil(bulkDeleteLinks(links)); return NextResponse.json( { diff --git a/apps/web/app/api/stripe/webhook/customer-subscription-deleted.ts b/apps/web/app/api/stripe/webhook/customer-subscription-deleted.ts index 93a41e13e5..deb2fe6448 100644 --- a/apps/web/app/api/stripe/webhook/customer-subscription-deleted.ts +++ b/apps/web/app/api/stripe/webhook/customer-subscription-deleted.ts @@ -26,7 +26,11 @@ export async function customerSubscriptionDeleted(event: Stripe.Event) { key: "_root", }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, }, users: { @@ -138,14 +142,8 @@ export async function customerSubscriptionDeleted(event: Stripe.Event) { // record root domain link for all domains from Tinybird recordLink( workspaceLinks.map((link) => ({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: link.tags.map((tag) => tag.tagId), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, + ...link, + url: "", })), ), log({ diff --git a/apps/web/app/api/tags/[id]/route.ts b/apps/web/app/api/tags/[id]/route.ts index c4c944136e..69ac1f4808 100644 --- a/apps/web/app/api/tags/[id]/route.ts +++ b/apps/web/app/api/tags/[id]/route.ts @@ -3,6 +3,7 @@ import { withWorkspace } from "@/lib/auth"; import { recordLink } from "@/lib/tinybird"; import { TagSchema, updateTagBodySchema } from "@/lib/zod/schemas/tags"; import { prisma } from "@dub/prisma"; +import { waitUntil } from "@vercel/functions"; import { NextResponse } from "next/server"; // PATCH /api/workspaces/[idOrSlug]/tags/[id] – update a tag for a workspace @@ -69,13 +70,12 @@ export const DELETE = withWorkspace( links: { select: { link: { - select: { - id: true, - domain: true, - key: true, - url: true, - programId: true, - createdAt: true, + include: { + tags: { + select: { + tag: true, + }, + }, }, }, }, @@ -91,17 +91,13 @@ export const DELETE = withWorkspace( } // update links metadata in tinybird after deleting a tag - await recordLink( - response.links.map(({ link }) => ({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: [], - program_id: link.programId ?? "", - workspace_id: workspace.id, - created_at: link.createdAt, - })), + waitUntil( + recordLink( + response.links.map(({ link }) => ({ + ...link, + tags: link.tags.filter(({ tag }) => tag.id !== id), + })), + ), ); return NextResponse.json({ id }); diff --git a/apps/web/lib/actions/partners/invite-partner.ts b/apps/web/lib/actions/partners/invite-partner.ts index 0374010cc8..6938342312 100644 --- a/apps/web/lib/actions/partners/invite-partner.ts +++ b/apps/web/lib/actions/partners/invite-partner.ts @@ -39,6 +39,5 @@ export const invitePartnerAction = authActionClient email, program, link, - workspace, }); }); diff --git a/apps/web/lib/api/links/bulk-delete-links.ts b/apps/web/lib/api/links/bulk-delete-links.ts index 8a294416a4..29c2747652 100644 --- a/apps/web/lib/api/links/bulk-delete-links.ts +++ b/apps/web/lib/api/links/bulk-delete-links.ts @@ -1,14 +1,10 @@ import { storage } from "@/lib/storage"; -import { recordLink } from "@/lib/tinybird"; +import { recordLinkTB, transformLinkTB } from "@/lib/tinybird"; import { R2_URL } from "@dub/utils"; -import { Link } from "@prisma/client"; import { linkCache } from "./cache"; +import { ExpandedLink } from "./utils"; -export async function bulkDeleteLinks({ - links, -}: { - links: (Link & { tags: { tagId: string }[] })[]; -}) { +export async function bulkDeleteLinks(links: ExpandedLink[]) { if (links.length === 0) { return; } @@ -18,16 +14,9 @@ export async function bulkDeleteLinks({ linkCache.deleteMany(links), // Record the links deletion in Tinybird - recordLink( + recordLinkTB( links.map((link) => ({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: link.tags.map(({ tagId }) => tagId), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, + ...transformLinkTB(link), deleted: true, })), ), diff --git a/apps/web/lib/api/links/create-link.ts b/apps/web/lib/api/links/create-link.ts index a89b907437..79a38f2767 100644 --- a/apps/web/lib/api/links/create-link.ts +++ b/apps/web/lib/api/links/create-link.ts @@ -137,16 +137,7 @@ export async function createLink(link: ProcessedLinkProps) { // cache link in Redis linkCache.set(response), // record link in Tinybird - recordLink({ - link_id: response.id, - domain: response.domain, - key: response.key, - url: response.url, - tag_ids: response.tags.map(({ tag }) => tag.id), - program_id: link.programId ?? "", - workspace_id: response.projectId, - created_at: response.createdAt, - }), + recordLink(response), // Upload image to R2 and update the link with the uploaded image URL when // proxy is enabled and image is set and not stored in R2 ...(proxy && image && !isStored(image) diff --git a/apps/web/lib/api/links/delete-link.ts b/apps/web/lib/api/links/delete-link.ts index 197ebef7a1..28b8876cad 100644 --- a/apps/web/lib/api/links/delete-link.ts +++ b/apps/web/lib/api/links/delete-link.ts @@ -1,5 +1,5 @@ import { storage } from "@/lib/storage"; -import { recordLink } from "@/lib/tinybird"; +import { recordLinkTB, transformLinkTB } from "@/lib/tinybird"; import { prisma } from "@dub/prisma"; import { R2_URL } from "@dub/utils"; import { waitUntil } from "@vercel/functions"; @@ -11,7 +11,11 @@ export async function deleteLink(linkId: string) { id: linkId, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, }); @@ -26,15 +30,8 @@ export async function deleteLink(linkId: string) { linkCache.delete(link), // Record link in the Tinybird - recordLink({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: link.tags.map((tag) => tag.tagId), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, + recordLinkTB({ + ...transformLinkTB(link), deleted: true, }), ]), diff --git a/apps/web/lib/api/links/propagate-bulk-link-changes.ts b/apps/web/lib/api/links/propagate-bulk-link-changes.ts index 0a0d6f94da..281450b0d7 100644 --- a/apps/web/lib/api/links/propagate-bulk-link-changes.ts +++ b/apps/web/lib/api/links/propagate-bulk-link-changes.ts @@ -7,17 +7,6 @@ export async function propagateBulkLinkChanges(links: ExpandedLink[]) { // update Redis cache linkCache.mset(links), // update 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) ?? [], - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, - })), - ), + recordLink(links), ]); } diff --git a/apps/web/lib/api/links/update-link.ts b/apps/web/lib/api/links/update-link.ts index c14a7fdc52..6931679999 100644 --- a/apps/web/lib/api/links/update-link.ts +++ b/apps/web/lib/api/links/update-link.ts @@ -164,16 +164,7 @@ export async function updateLink({ linkCache.set(response), // record link in Tinybird - recordLink({ - link_id: response.id, - domain: response.domain, - key: response.key, - url: response.url, - tag_ids: response.tags.map(({ tag }) => tag.id), - program_id: response.programId ?? "", - workspace_id: response.projectId, - created_at: response.createdAt, - }), + recordLink(response), // if key is changed: delete the old key in Redis (changedDomain || changedKey) && linkCache.delete(oldLink), diff --git a/apps/web/lib/api/partners/invite-partner.ts b/apps/web/lib/api/partners/invite-partner.ts index 92c74921af..0d83bf6aa0 100644 --- a/apps/web/lib/api/partners/invite-partner.ts +++ b/apps/web/lib/api/partners/invite-partner.ts @@ -2,7 +2,7 @@ import { updateConfig } from "@/lib/edge-config"; import { recordLink } from "@/lib/tinybird"; import { ProgramProps } from "@/lib/types"; import { prisma } from "@dub/prisma"; -import { Link, Project } from "@dub/prisma/client"; +import { Link } from "@dub/prisma/client"; import { sendEmail } from "emails"; import PartnerInvite from "emails/partner-invite"; import { createId } from "../utils"; @@ -10,12 +10,10 @@ import { createId } from "../utils"; export const invitePartner = async ({ email, program, - workspace, link, }: { email: string; program: ProgramProps; - workspace: Project; link: Link; }) => { const [programEnrollment, programInvite] = await Promise.all([ @@ -84,15 +82,9 @@ export const invitePartner = async ({ // record link update in tinybird recordLink({ - domain: link.domain, - key: link.key, - link_id: link.id, - created_at: link.createdAt, - url: link.url, - tag_ids: tags.map((t) => t.id) || [], - program_id: program.id, - workspace_id: workspace.id, - deleted: false, + ...link, + tags: tags.map((t) => ({ tag: t })), + programId: program.id, }), // TODO: Remove this once we open up partners.dub.co to everyone diff --git a/apps/web/lib/tinybird/record-link.ts b/apps/web/lib/tinybird/record-link.ts index 9fd4175ba8..97593cdfad 100644 --- a/apps/web/lib/tinybird/record-link.ts +++ b/apps/web/lib/tinybird/record-link.ts @@ -1,4 +1,5 @@ import z from "@/lib/zod"; +import { ExpandedLink } from "../api/links"; import { tb } from "./client"; export const dubLinksMetadataSchema = z.object({ @@ -7,6 +8,10 @@ export const dubLinksMetadataSchema = z.object({ key: z.string(), url: z.string().default(""), tag_ids: z.array(z.string()).default([]), + tenant_id: z + .string() + .nullable() + .transform((v) => (v === null ? "" : v)), program_id: z.string().default(""), workspace_id: z .string() @@ -28,8 +33,30 @@ export const dubLinksMetadataSchema = z.object({ .transform((v) => (v ? 1 : 0)), }); -export const recordLink = tb.buildIngestEndpoint({ +export const recordLinkTB = tb.buildIngestEndpoint({ datasource: "dub_links_metadata", event: dubLinksMetadataSchema, wait: true, }); + +export const transformLinkTB = (link: ExpandedLink) => { + return { + link_id: link.id, + domain: link.domain, + key: link.key, + url: link.url, + tag_ids: link.tags?.map(({ tag }) => tag.id), + tenant_id: link.tenantId ?? "", + program_id: link.programId ?? "", + workspace_id: link.projectId, + created_at: link.createdAt, + }; +}; + +export const recordLink = async (payload: ExpandedLink | ExpandedLink[]) => { + if (Array.isArray(payload)) { + return await recordLinkTB(payload.map(transformLinkTB)); + } else { + return await recordLinkTB(transformLinkTB(payload)); + } +}; diff --git a/apps/web/scripts/bulk-delete-links.ts b/apps/web/scripts/bulk-delete-links.ts index e7ab43ea17..8b77b5a146 100644 --- a/apps/web/scripts/bulk-delete-links.ts +++ b/apps/web/scripts/bulk-delete-links.ts @@ -1,4 +1,4 @@ -import { recordLink } from "@/lib/tinybird"; +import { recordLinkTB, transformLinkTB } from "@/lib/tinybird"; import { prisma } from "@dub/prisma"; import "dotenv-flow/config"; @@ -9,15 +9,12 @@ async function main() { where: { domain, }, - select: { - id: true, - domain: true, - key: true, - url: true, - projectId: true, - programId: true, - tags: true, - createdAt: true, + include: { + tags: { + select: { + tag: true, + }, + }, }, // take: 10000, }); @@ -31,16 +28,9 @@ async function main() { }, }), // redis.del(domain), - recordLink( + recordLinkTB( links.map((link) => ({ - link_id: link.id, - domain: link.domain, - key: link.key, - url: link.url, - tag_ids: link.tags.map((tag) => tag.tagId), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, + ...transformLinkTB(link), deleted: true, })), ), diff --git a/apps/web/scripts/sync-links-metadata.ts b/apps/web/scripts/sync-links-metadata.ts index 5293263507..26d7cf052f 100644 --- a/apps/web/scripts/sync-links-metadata.ts +++ b/apps/web/scripts/sync-links-metadata.ts @@ -8,7 +8,11 @@ const limit = 20000; async function main() { const links = await prisma.link.findMany({ include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, orderBy: [ { @@ -46,18 +50,7 @@ async function main() { // })); // }); - const res = await 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), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, - })), - ); + const res = await recordLink(links); console.log(res); } diff --git a/apps/web/scripts/sync-tag-analytics.ts b/apps/web/scripts/sync-tag-analytics.ts index 0ae3663a95..d97e7c14ba 100644 --- a/apps/web/scripts/sync-tag-analytics.ts +++ b/apps/web/scripts/sync-tag-analytics.ts @@ -10,7 +10,11 @@ async function main() { }, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, orderBy: { createdAt: "asc", @@ -19,18 +23,7 @@ async function main() { take: 1000, }); - const res = await 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), - program_id: link.programId ?? "", - workspace_id: link.projectId, - created_at: link.createdAt, - })), - ); + const res = await recordLink(links); console.log(res); } From c4c3517f7db36ab976d50d014f45f3d7e548e2f6 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Jan 2025 14:31:26 -0800 Subject: [PATCH 6/9] Update approve-partner.ts --- .../lib/actions/partners/approve-partner.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/web/lib/actions/partners/approve-partner.ts b/apps/web/lib/actions/partners/approve-partner.ts index 9f02cb00a4..aa66b26f32 100644 --- a/apps/web/lib/actions/partners/approve-partner.ts +++ b/apps/web/lib/actions/partners/approve-partner.ts @@ -61,24 +61,16 @@ export const approvePartnerAction = authActionClient programId, }, include: { - tags: true, + tags: { + select: { + tag: true, + }, + }, }, }), ]); - waitUntil( - recordLink({ - domain: updatedLink.domain, - key: updatedLink.key, - link_id: updatedLink.id, - created_at: updatedLink.createdAt, - url: updatedLink.url, - tag_ids: updatedLink.tags.map((t) => t.tagId), - program_id: program.id, - workspace_id: workspace.id, - deleted: false, - }), - ); + waitUntil(recordLink(updatedLink)); // TODO: [partners] Notify partner of approval? From 80d24bbd8780ee83e6d81064098acbe389789170 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Jan 2025 14:47:12 -0800 Subject: [PATCH 7/9] Update invite-partner.ts --- apps/web/lib/api/partners/invite-partner.ts | 56 ++++++++++----------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/apps/web/lib/api/partners/invite-partner.ts b/apps/web/lib/api/partners/invite-partner.ts index 0d83bf6aa0..4e007148bc 100644 --- a/apps/web/lib/api/partners/invite-partner.ts +++ b/apps/web/lib/api/partners/invite-partner.ts @@ -3,6 +3,7 @@ import { recordLink } from "@/lib/tinybird"; import { ProgramProps } from "@/lib/types"; import { prisma } from "@dub/prisma"; import { Link } from "@dub/prisma/client"; +import { waitUntil } from "@vercel/functions"; import { sendEmail } from "emails"; import PartnerInvite from "emails/partner-invite"; import { createId } from "../utils"; @@ -50,17 +51,7 @@ export const invitePartner = async ({ throw new Error(`Partner ${email} already invited to this program.`); } - const tags = await prisma.tag.findMany({ - where: { - links: { - some: { - linkId: link.id, - }, - }, - }, - }); - - const [programInvited] = await Promise.all([ + const [programInvited, updatedLink] = await Promise.all([ prisma.programInvite.create({ data: { id: createId({ prefix: "pgi_" }), @@ -78,13 +69,13 @@ export const invitePartner = async ({ data: { programId: program.id, }, - }), - - // record link update in tinybird - recordLink({ - ...link, - tags: tags.map((t) => ({ tag: t })), - programId: program.id, + include: { + tags: { + select: { + tag: true, + }, + }, + }, }), // TODO: Remove this once we open up partners.dub.co to everyone @@ -94,18 +85,23 @@ export const invitePartner = async ({ }), ]); - await sendEmail({ - subject: `${program.name} invited you to join Dub Partners`, - email, - react: PartnerInvite({ - email, - appName: `${process.env.NEXT_PUBLIC_APP_NAME}`, - program: { - name: program.name, - logo: program.logo, - }, - }), - }); + waitUntil( + Promise.all([ + recordLink(updatedLink), + sendEmail({ + subject: `${program.name} invited you to join Dub Partners`, + email, + react: PartnerInvite({ + email, + appName: `${process.env.NEXT_PUBLIC_APP_NAME}`, + program: { + name: program.name, + logo: program.logo, + }, + }), + }), + ]), + ); return programInvited; }; From 795441cd989110f348d932cf10dc1fe57b7194c0 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Jan 2025 14:52:25 -0800 Subject: [PATCH 8/9] promise.allSettled --- apps/web/lib/api/links/create-link.ts | 2 +- apps/web/lib/api/links/update-link.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/api/links/create-link.ts b/apps/web/lib/api/links/create-link.ts index 79a38f2767..d5afba7b1f 100644 --- a/apps/web/lib/api/links/create-link.ts +++ b/apps/web/lib/api/links/create-link.ts @@ -133,7 +133,7 @@ export async function createLink(link: ProcessedLinkProps) { const uploadedImageUrl = `${R2_URL}/images/${response.id}`; waitUntil( - Promise.all([ + Promise.allSettled([ // cache link in Redis linkCache.set(response), // record link in Tinybird diff --git a/apps/web/lib/api/links/update-link.ts b/apps/web/lib/api/links/update-link.ts index 6931679999..4ea3a2cc3f 100644 --- a/apps/web/lib/api/links/update-link.ts +++ b/apps/web/lib/api/links/update-link.ts @@ -159,7 +159,7 @@ export async function updateLink({ }); waitUntil( - Promise.all([ + Promise.allSettled([ // record link in Redis linkCache.set(response), From 703f85ab6c5ecb5dd7da3ce2adf83b1d34a2348c Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Jan 2025 15:17:13 -0800 Subject: [PATCH 9/9] add tenantId to advanced section --- .../lib/api/links/get-links-for-workspace.ts | 2 +- apps/web/tests/links/create-link.test.ts | 2 +- apps/web/ui/links/link-controls.tsx | 3 +- .../ui/modals/link-builder/advanced-modal.tsx | 39 ++++++++++++++++--- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/apps/web/lib/api/links/get-links-for-workspace.ts b/apps/web/lib/api/links/get-links-for-workspace.ts index a6d406c9f8..4deeebfdd8 100644 --- a/apps/web/lib/api/links/get-links-for-workspace.ts +++ b/apps/web/lib/api/links/get-links-for-workspace.ts @@ -64,9 +64,9 @@ export async function getLinksForWorkspace({ }, } : {}), + ...(tenantId && { tenantId }), ...(userId && { userId }), ...(linkIds && { id: { in: linkIds } }), - ...(tenantId && { tenantId }), }, include: { tags: { diff --git a/apps/web/tests/links/create-link.test.ts b/apps/web/tests/links/create-link.test.ts index 0c660d58f0..aeb3ebde4c 100644 --- a/apps/web/tests/links/create-link.test.ts +++ b/apps/web/tests/links/create-link.test.ts @@ -139,7 +139,7 @@ describe.sequential("POST /links", async () => { }); }); - test("utm builder", async (ctx) => { + test("utm builder", async () => { const longUrl = new URL(url); const utm = { utm_source: "facebook", diff --git a/apps/web/ui/links/link-controls.tsx b/apps/web/ui/links/link-controls.tsx index c6f4c233b0..ce62993f04 100644 --- a/apps/web/ui/links/link-controls.tsx +++ b/apps/web/ui/links/link-controls.tsx @@ -66,7 +66,8 @@ export function LinkControls({ link }: { link: ResponseLink }) { id: _, createdAt: __, updatedAt: ___, - userId: ____, + userId: ____, // don't duplicate userId since the current user can be different + externalId: _____, // don't duplicate externalId since it should be unique ...propsToDuplicate } = link; const { diff --git a/apps/web/ui/modals/link-builder/advanced-modal.tsx b/apps/web/ui/modals/link-builder/advanced-modal.tsx index 33d6567783..52a97e678c 100644 --- a/apps/web/ui/modals/link-builder/advanced-modal.tsx +++ b/apps/web/ui/modals/link-builder/advanced-modal.tsx @@ -1,4 +1,3 @@ -import useWorkspace from "@/lib/swr/use-workspace"; import { Button, InfoTooltip, @@ -37,21 +36,23 @@ function AdvancedModal({ register, handleSubmit, formState: { isDirty }, - } = useForm>({ + } = useForm>({ values: { externalId: getValuesParent("externalId"), + tenantId: getValuesParent("tenantId"), }, }); - const [externalIdParent] = watchParent(["externalId"]); + const [externalIdParent, tenantIdParent] = watchParent([ + "externalId", + "tenantId", + ]); useKeyboardShortcut("a", () => setShowAdvancedModal(true), { modal: true, }); - const parentEnabled = Boolean(externalIdParent); - - const { conversionEnabled } = useWorkspace(); + const parentEnabled = Boolean(externalIdParent || tenantIdParent); return ( + + {/* Tenant ID */} +
+
+ + +
+
+ +
+