Skip to content

Commit 665e1bd

Browse files
committed
feat(api): add referral_link field to /api/v1/me endpoint
- Add referral_code and referral_link to valid user fields - Compute referral_link from referral_code on backend - Update database contracts and tests - Allows CLI to fetch ready-to-use referral links
1 parent 5171b33 commit 665e1bd

File tree

5 files changed

+81
-9
lines changed

5 files changed

+81
-9
lines changed

common/src/testing/impl/agent-runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const TEST_AGENT_RUNTIME_IMPL = Object.freeze<
2727
id: 'test-user-id',
2828
email: 'test-email',
2929
discord_id: 'test-discord-id',
30+
referral_code: 'ref-test-code',
3031
}),
3132
fetchAgentFromDatabase: async () => null,
3233
startAgentRun: async () => 'test-agent-run-id',

common/src/types/contracts/database.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ type User = {
55
id: string
66
email: string
77
discord_id: string | null
8+
referral_code: string | null
89
}
9-
export const userColumns = ['id', 'email', 'discord_id'] as const
10+
export const userColumns = ['id', 'email', 'discord_id', 'referral_code'] as const
1011
export type UserColumn = keyof User
1112
export type GetUserInfoFromApiKeyInput<T extends UserColumn> = {
1213
apiKey: string

web/src/app/api/v1/me/__tests__/me.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ describe('/api/v1/me route', () => {
2222
id: 'user-123',
2323
email: 'test@example.com',
2424
discord_id: 'discord-123',
25+
referral_code: 'ref-user-123',
2526
},
2627
'test-api-key-456': {
2728
id: 'user-456',
2829
email: 'test2@example.com',
2930
discord_id: null,
31+
referral_code: 'ref-user-456',
3032
},
3133
}
3234

@@ -208,7 +210,7 @@ describe('/api/v1/me route', () => {
208210
const body = await response.json()
209211
expect(body.error).toContain('Invalid fields: invalid_field')
210212
expect(body.error).toContain(
211-
`Valid fields are: ${VALID_USER_INFO_FIELDS.join(', ')}`,
213+
'Valid fields are: id, email, discord_id, referral_code, referral_link',
212214
)
213215
})
214216

@@ -298,6 +300,23 @@ describe('/api/v1/me route', () => {
298300
})
299301
})
300302

303+
test('returns referral_link when requested', async () => {
304+
const req = new NextRequest(
305+
'http://localhost:3000/api/v1/me?fields=referral_link',
306+
{
307+
headers: { Authorization: 'Bearer test-api-key-123' },
308+
},
309+
)
310+
311+
const response = await getMe({
312+
...agentRuntimeImpl,
313+
req,
314+
})
315+
expect(response.status).toBe(200)
316+
const body = await response.json()
317+
expect(typeof body.referral_link).toBe('string')
318+
})
319+
301320
test('handles null discord_id correctly', async () => {
302321
const req = new NextRequest(
303322
'http://localhost:3000/api/v1/me?fields=id,discord_id',

web/src/app/api/v1/me/_get.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
2+
import { getReferralLink } from '@codebuff/common/util/referral'
23
import { NextResponse } from 'next/server'
34

45
import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics'
@@ -9,7 +10,16 @@ import type { NextRequest } from 'next/server'
910
import { VALID_USER_INFO_FIELDS } from '@/db/user'
1011
import { extractApiKeyFromHeader } from '@/util/auth'
1112

12-
type ValidField = (typeof VALID_USER_INFO_FIELDS)[number]
13+
const DERIVED_USER_INFO_FIELDS = ['referral_link'] as const
14+
15+
type DerivedField = (typeof DERIVED_USER_INFO_FIELDS)[number]
16+
type ValidDbField = (typeof VALID_USER_INFO_FIELDS)[number]
17+
type ValidField = ValidDbField | DerivedField
18+
19+
const ALL_USER_INFO_FIELDS = [
20+
...VALID_USER_INFO_FIELDS,
21+
...DERIVED_USER_INFO_FIELDS,
22+
] as const
1323

1424
export async function getMe(params: {
1525
req: NextRequest
@@ -41,15 +51,15 @@ export async function getMe(params: {
4151
if (requestedFields.length === 0) {
4252
return NextResponse.json(
4353
{
44-
error: `Invalid fields: empty. Valid fields are: ${VALID_USER_INFO_FIELDS.join(', ')}`,
54+
error: `Invalid fields: empty. Valid fields are: ${ALL_USER_INFO_FIELDS.join(', ')}`,
4555
},
4656
{ status: 400 },
4757
)
4858
}
4959

5060
// Validate that all requested fields are valid
5161
const invalidFields = requestedFields.filter(
52-
(f) => !VALID_USER_INFO_FIELDS.includes(f as ValidField),
62+
(f) => !ALL_USER_INFO_FIELDS.includes(f as ValidField),
5363
)
5464
if (invalidFields.length > 0) {
5565
trackEvent({
@@ -63,7 +73,7 @@ export async function getMe(params: {
6373
})
6474
return NextResponse.json(
6575
{
66-
error: `Invalid fields: ${invalidFields.join(', ')}. Valid fields are: ${VALID_USER_INFO_FIELDS.join(', ')}`,
76+
error: `Invalid fields: ${invalidFields.join(', ')}. Valid fields are: ${ALL_USER_INFO_FIELDS.join(', ')}`,
6777
},
6878
{ status: 400 },
6979
)
@@ -74,10 +84,29 @@ export async function getMe(params: {
7484
fields = ['id']
7585
}
7686

87+
// Build database field selection (exclude derived fields, always include id)
88+
const dbFieldsSet = new Set<ValidDbField>()
89+
90+
for (const field of fields) {
91+
if (VALID_USER_INFO_FIELDS.includes(field as ValidDbField)) {
92+
dbFieldsSet.add(field as ValidDbField)
93+
}
94+
}
95+
96+
// Always include id for tracking
97+
dbFieldsSet.add('id')
98+
99+
// If referral_link is requested, ensure we also fetch referral_code
100+
if (fields.includes('referral_link') && !dbFieldsSet.has('referral_code')) {
101+
dbFieldsSet.add('referral_code')
102+
}
103+
104+
const dbFields = Array.from(dbFieldsSet)
105+
77106
// Get user info
78107
const userInfo = await getUserInfoFromApiKey({
79108
apiKey,
80-
fields,
109+
fields: dbFields,
81110
logger,
82111
})
83112

@@ -98,5 +127,22 @@ export async function getMe(params: {
98127
logger,
99128
})
100129

101-
return NextResponse.json(userInfo)
130+
// Build response including derived fields
131+
const userInfoRecord = userInfo as Partial<Record<ValidDbField, string | null>>
132+
133+
const responseBody: Record<string, unknown> = {}
134+
135+
for (const field of fields) {
136+
if (field === 'referral_link') {
137+
const referralCode = userInfoRecord.referral_code ?? null
138+
responseBody.referral_link =
139+
typeof referralCode === 'string' && referralCode.length > 0
140+
? getReferralLink(referralCode)
141+
: null
142+
} else {
143+
responseBody[field] = userInfoRecord[field as ValidDbField] ?? null
144+
}
145+
}
146+
147+
return NextResponse.json(responseBody)
102148
}

web/src/db/user.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import type {
77
GetUserInfoFromApiKeyOutput,
88
} from '@codebuff/common/types/contracts/database'
99

10-
export const VALID_USER_INFO_FIELDS = ['id', 'email', 'discord_id'] as const
10+
export const VALID_USER_INFO_FIELDS = [
11+
'id',
12+
'email',
13+
'discord_id',
14+
'referral_code',
15+
] as const
1116

1217
export async function getUserInfoFromApiKey<
1318
T extends (typeof VALID_USER_INFO_FIELDS)[number],

0 commit comments

Comments
 (0)