-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Move link settings under partner groups #2774
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
Changes from 235 commits
42f116e
ce3596b
c1e992a
70fefe9
0e194d0
e3c48fe
787404d
08411be
4e6b3e0
503bf58
bc5ed9a
f536412
f6a3b52
7f7b21c
6e8c608
2974d4c
71e75f3
430f14b
eada429
098c6ea
7dda601
500da52
571d88a
8ead9cb
1bfc9bc
6fb39cd
94427f3
0d4fb20
17b33c8
2511df9
4317004
b6e4f16
5d0b15a
c905bbb
90e16d3
b619d77
0e6dedb
0cf64ee
ac04ae4
3dc59e7
e18386f
50c4494
4ca27f0
20395a6
ab265e1
951241a
dbc8909
b8b0051
5e4691e
89864dd
05dde31
a4fdb00
2d67ed6
4e7ad65
4e0d4f4
0c68fef
23845a1
b1a2713
e60527c
e11f204
3ac1ae1
930b249
8895110
79e876e
89b6d15
b4f8bdb
37b02c0
d0a7fec
71558ae
680931d
5757e15
4ddaafd
84f8d4d
47f80ee
efae227
5466475
1ed8c9d
8e3b08c
f06063a
69fcaf1
11e456c
8e76386
efd4e7b
67924a2
b172ea7
513f84e
a3a4b4d
dec789b
f91d3e4
a50dd44
8687c05
f56326f
ec936b1
5ec9bc8
e7cf35c
e77d7be
3d54817
b2b9b11
05c88f5
871ff71
93bb974
c9113e0
bf709e3
8849d6b
1146095
45c58e5
f2fbb9d
0100861
ff3eed7
783403f
fceb828
410dfc5
3b7ee5e
93dfaa9
70afee3
6d8c384
031fd57
a0c2e8c
4ae078e
9ca78c1
93951ae
473925a
554a1a4
a44f96e
c748000
68e8a73
0fa2d7d
10c4611
14f3098
c204ef9
14e7ee6
adc3d5f
0551172
8bc4a6a
2aac3ed
76e4b78
84155d8
7bb6cb7
34004dd
5db029d
ccac033
1cd61a6
2a8a002
31bf7e5
3b1d29e
d1d27d5
18bbe4a
0598d44
4d6b093
36a6e2c
469007a
aed935c
8d7c3cb
2901ac9
37a6254
a524570
d74107d
53d6df4
2371786
7963f3c
6e3e9c6
b9784ee
142f7f2
ef1c816
5b7050d
7d0e90e
4f490a7
8b63f76
1d03009
9507acd
5412d5d
8d8aece
bdd0752
4fd63c3
d595100
4d37bc2
aa2a228
445d0f2
8e5d061
446558b
b88259f
00d150c
80d0161
2ef7ab8
5ff6b34
5b39c83
21ef882
bf556de
e883f3e
b85845c
08bce46
6b10669
fa3e405
f464029
5357f9b
d9365a5
9fbf7ab
ff429cc
2254060
1205b31
470a85d
56fa1dd
fa30c81
afefae5
76faea6
c73d73f
99784c8
f1eda26
bb23650
35f2a58
6584b99
1a269d8
56e449e
f6a3225
9e413ff
8c93f3e
27a681c
300a50c
3c938f7
9dba500
d60d2c6
bd49fa3
c55edab
f594e95
5e1f430
5b2033b
8c76623
1452ac7
42c766d
c44099d
fc09df3
9c4c9aa
b341e5e
ef6696f
141c23e
a066e6c
74f1200
27a72bf
982b261
2f80a8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import { handleAndReturnErrorResponse } from "@/lib/api/errors"; | ||
import { bulkCreateLinks } from "@/lib/api/links"; | ||
import { generatePartnerLink } from "@/lib/api/partners/generate-partner-link"; | ||
import { extractUtmParams } from "@/lib/api/utm/extract-utm-params"; | ||
import { qstash } from "@/lib/cron"; | ||
import { verifyQstashSignature } from "@/lib/cron/verify-qstash"; | ||
import { WorkspaceProps } from "@/lib/types"; | ||
import { prisma } from "@dub/prisma"; | ||
import { | ||
APP_DOMAIN_WITH_NGROK, | ||
constructURLFromUTMParams, | ||
isFulfilled, | ||
log, | ||
} from "@dub/utils"; | ||
import { z } from "zod"; | ||
import { logAndRespond } from "../../utils"; | ||
export const dynamic = "force-dynamic"; | ||
|
||
const PAGE_SIZE = 100; | ||
const MAX_BATCH = 10; | ||
|
||
const schema = z.object({ | ||
defaultLinkId: z.string(), | ||
userId: z.string(), | ||
cursor: z.string().optional(), | ||
}); | ||
|
||
/** | ||
* Cron job to create default partner links for all approved partners in a group. | ||
* | ||
* For each approved partner in the group, it creates a link based on | ||
* the group's default link configuration (domain, URL, etc.). | ||
* | ||
* It processes up to MAX_BATCH * PAGE_SIZE partners per execution | ||
* and schedules additional jobs if needed. | ||
*/ | ||
|
||
// POST /api/cron/groups/create-default-links | ||
export async function POST(req: Request) { | ||
try { | ||
const rawBody = await req.text(); | ||
await verifyQstashSignature({ req, rawBody }); | ||
|
||
const { defaultLinkId, userId, cursor } = schema.parse(JSON.parse(rawBody)); | ||
|
||
// Find the default link | ||
const defaultLink = await prisma.partnerGroupDefaultLink.findUnique({ | ||
where: { | ||
id: defaultLinkId, | ||
}, | ||
include: { | ||
partnerGroup: { | ||
include: { | ||
utmTemplate: true, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
if (!defaultLink) { | ||
return logAndRespond( | ||
`Default link ${defaultLinkId} not found. Skipping...`, | ||
{ | ||
logLevel: "error", | ||
}, | ||
); | ||
} | ||
|
||
const group = defaultLink.partnerGroup; | ||
if (!group) { | ||
return logAndRespond( | ||
`Group ${defaultLink.groupId} not found. Skipping...`, | ||
{ | ||
logLevel: "error", | ||
}, | ||
); | ||
} | ||
|
||
console.info( | ||
`Creating default links for the partners (defaultLinkId=${defaultLink.id}, groupId=${group.id}).`, | ||
); | ||
|
||
// Find the workspace & program | ||
const { workspace, ...program } = await prisma.program.findUniqueOrThrow({ | ||
where: { | ||
id: group.programId, | ||
}, | ||
include: { | ||
workspace: true, | ||
}, | ||
}); | ||
|
||
const { utmTemplate } = group; | ||
|
||
let hasMore = true; | ||
let currentCursor = cursor; | ||
let processedBatches = 0; | ||
|
||
while (processedBatches < MAX_BATCH) { | ||
// Find partners in the group | ||
const programEnrollments = await prisma.programEnrollment.findMany({ | ||
where: { | ||
...(currentCursor && { | ||
id: { | ||
gt: currentCursor, | ||
}, | ||
}), | ||
groupId: group.id, | ||
status: "approved", | ||
}, | ||
include: { | ||
partner: true, | ||
}, | ||
take: PAGE_SIZE, | ||
orderBy: { | ||
id: "asc", | ||
}, | ||
}); | ||
|
||
if (programEnrollments.length === 0) { | ||
hasMore = false; | ||
break; | ||
} | ||
|
||
// Create a new defaultLink for each partner in the group | ||
const processedLinks = ( | ||
await Promise.allSettled( | ||
programEnrollments.map(({ partner, ...programEnrollment }) => | ||
generatePartnerLink({ | ||
workspace: { | ||
id: workspace.id, | ||
plan: workspace.plan as WorkspaceProps["plan"], | ||
}, | ||
program: { | ||
id: program.id, | ||
defaultFolderId: program.defaultFolderId, | ||
}, | ||
partner: { | ||
id: partner.id, | ||
name: partner.name, | ||
email: partner.email!, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code uses View Details📝 Patch Detailsdiff --git a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
index c454e8a6b..4932ff061 100644
--- a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
+++ b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
@@ -103,6 +103,11 @@ export async function POST(req: Request) {
}),
groupId: group.id,
status: "approved",
+ partner: {
+ email: {
+ not: null,
+ },
+ },
},
include: {
partner: true,
diff --git a/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts b/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
index 739e4bd7d..4d67524cb 100644
--- a/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
+++ b/apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
@@ -57,6 +57,11 @@ export const POST = withWorkspace(
in: partnerIds,
},
programId,
+ partner: {
+ email: {
+ not: null,
+ },
+ },
},
include: {
partner: true,
AnalysisRuntime error in generatePartnerLink() when partner email is nullWhat fails: The How to reproduce: # Create a partner with null email and trigger default link creation
# The cron job queries partners without filtering null emails, then passes null to generatePartnerLink Result: TypeError: Cannot read properties of null (reading 'split') when Expected: Should filter out partners with null emails in the Prisma query, following the same pattern as |
||
tenantId: programEnrollment.tenantId ?? undefined, | ||
}, | ||
Comment on lines
+138
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The partner object passed to View Details📝 Patch Detailsdiff --git a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
index c454e8a6b..e070be3fd 100644
--- a/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
+++ b/apps/web/app/(ee)/api/cron/groups/create-default-links/route.ts
@@ -135,6 +135,7 @@ export async function POST(req: Request) {
id: partner.id,
name: partner.name,
email: partner.email!,
+ username: partner.username,
tenantId: programEnrollment.tenantId ?? undefined,
},
link: {
AnalysisIn the cron job that creates default partner links, the partner object being passed to The The partner object should include the
|
||
link: { | ||
domain: defaultLink.domain, | ||
url: constructURLFromUTMParams( | ||
defaultLink.url, | ||
extractUtmParams(group.utmTemplate), | ||
), | ||
...extractUtmParams(group.utmTemplate, { excludeRef: true }), | ||
tenantId: programEnrollment.tenantId ?? undefined, | ||
partnerGroupDefaultLinkId: defaultLink.id, | ||
}, | ||
userId, | ||
}), | ||
), | ||
) | ||
) | ||
.filter(isFulfilled) | ||
.map(({ value }) => value); | ||
|
||
const createdLinks = await bulkCreateLinks({ | ||
links: processedLinks, | ||
}); | ||
|
||
console.log( | ||
`Created ${createdLinks.length} default links for the partners in the group ${group.id}.`, | ||
); | ||
|
||
// Update cursor to the last processed record | ||
currentCursor = programEnrollments[programEnrollments.length - 1].id; | ||
devkiran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
processedBatches++; | ||
} | ||
|
||
if (hasMore) { | ||
await qstash.publishJSON({ | ||
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/groups/create-default-links`, | ||
method: "POST", | ||
body: { | ||
defaultLinkId, | ||
userId, | ||
cursor: currentCursor, | ||
}, | ||
}); | ||
} | ||
|
||
return logAndRespond(`Finished creating default links for the partners.`); | ||
} catch (error) { | ||
await log({ | ||
message: `Error creating default links for the partners: ${error.message}.`, | ||
type: "errors", | ||
}); | ||
|
||
console.error(error); | ||
|
||
return handleAndReturnErrorResponse(error); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Use cursor pagination instead of
id: { gt: cursor }
to avoid ordering issues.If
ProgramEnrollment.id
is non-monotonic (e.g., CUID),gt
is unsafe. Use Prismacursor
+skip: 1
withorderBy
.To confirm model types, run:
🏁 Script executed:
Length of output: 334
🏁 Script executed:
Length of output: 12402
Use Prisma cursor pagination for ProgramEnrollment
ProgramEnrollment.id
is defined asString @id @default(cuid())
, so filtering withgt
on CUIDs can skip or duplicate records. Replace withcursor
+skip: 1
:📝 Committable suggestion
🤖 Prompt for AI Agents