Skip to content

Commit 1885cea

Browse files
fix(OUT-4208): collapse duplicate custom-fields requests on client pageload
The /client pageload fired two parallel GET /api/custom-fields/[entityType] requests (client + company), which Sentry's N+1 API Call detector groups and flags. Fetch all entity types in a single GET /api/custom-fields and split them client-side by entityType, removing the now-unused dynamic route. Fixes CLIENT-HOME-V3-1X Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4be6d96 commit 1885cea

4 files changed

Lines changed: 29 additions & 41 deletions

File tree

File renamed without changes.

‎src/app/routes.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const ROUTES = Object.freeze({
1313
users: '/api/users',
1414
clientContext: '/api/client-context',
1515
notificationCounts: '/api/users/:id/notification-counts',
16-
listCustomFields: '/api/custom-fields/:entityType',
16+
listCustomFields: '/api/custom-fields',
1717
customFieldOptionsMap: '/api/custom-fields/options-map',
1818
segments: '/api/segments',
1919
segment: '/api/segments/:segmentId',

‎src/features/custom-fields/hooks/useCustomFields.ts‎

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@assembly/types'
99
import type { IconType } from '@assembly-js/design-system'
1010
import { useQuery } from '@tanstack/react-query'
11+
import { useMemo } from 'react'
1112
import { api } from '@/lib/core/axios.instance'
1213

1314
const CUSTOM_FIELDS_QUERY_KEY = 'custom-fields'
@@ -35,43 +36,40 @@ export type CustomFieldItem = {
3536
/** Nested map: { [entityType]: { [fieldKey]: { [optionKey]: optionLabel } } } */
3637
export type CustomFieldOptionsMap = Record<string, Record<string, Record<string, string>>>
3738

38-
export function useCustomFields() {
39-
const { data: clientCustomFields, isLoading: clientIsLoading } = useQuery({
40-
queryKey: [CUSTOM_FIELDS_QUERY_KEY, CustomFieldEntityType.CLIENT],
41-
queryFn: async (): Promise<CustomFieldItem[]> => {
42-
const res = await api.get(`/api/custom-fields/${CustomFieldEntityType.CLIENT}`)
43-
const parsed = ListCustomFieldResponseSchema.parse(res.data)
44-
return parsed.data
45-
.sort((a, b) => a.order - b.order)
46-
.map(({ id, key, name, type, options }) => ({
47-
id,
48-
key,
49-
name,
50-
type,
51-
options,
52-
icon: CUSTOM_FIELD_TYPE_ICON[type],
53-
}))
54-
},
55-
})
39+
type CustomFieldItemWithEntity = CustomFieldItem & { entityType: CustomFieldEntityType }
5640

57-
const { data: companyCustomFields, isLoading: companyIsLoading } = useQuery({
58-
queryKey: [CUSTOM_FIELDS_QUERY_KEY, CustomFieldEntityType.COMPANY],
59-
queryFn: async (): Promise<CustomFieldItem[]> => {
60-
const res = await api.get(`/api/custom-fields/${CustomFieldEntityType.COMPANY}`)
41+
export const useCustomFields = () => {
42+
const { data: customFields, isLoading: fieldsIsLoading } = useQuery({
43+
queryKey: [CUSTOM_FIELDS_QUERY_KEY],
44+
queryFn: async (): Promise<CustomFieldItemWithEntity[]> => {
45+
const res = await api.get('/api/custom-fields')
6146
const parsed = ListCustomFieldResponseSchema.parse(res.data)
6247
return parsed.data
6348
.sort((a, b) => a.order - b.order)
64-
.map(({ id, key, name, type, options }) => ({
49+
.map(({ id, key, name, type, options, entityType }) => ({
6550
id,
6651
key,
6752
name,
6853
type,
6954
options,
55+
entityType,
7056
icon: CUSTOM_FIELD_TYPE_ICON[type],
7157
}))
7258
},
7359
})
7460

61+
// Split the single response by entity type, since field keys are only
62+
// unique within an entity type (client vs company).
63+
const { clientCustomFields, companyCustomFields } = useMemo(() => {
64+
const client: CustomFieldItem[] = []
65+
const company: CustomFieldItem[] = []
66+
for (const { entityType, ...field } of customFields ?? []) {
67+
if (entityType === CustomFieldEntityType.CLIENT) client.push(field)
68+
else if (entityType === CustomFieldEntityType.COMPANY) company.push(field)
69+
}
70+
return { clientCustomFields: client, companyCustomFields: company }
71+
}, [customFields])
72+
7573
const { data: optionsMap, isLoading: optionsMapIsLoading } = useQuery({
7674
queryKey: [CUSTOM_FIELD_OPTIONS_MAP_QUERY_KEY],
7775
queryFn: async (): Promise<CustomFieldOptionsMap> => {
@@ -81,9 +79,9 @@ export function useCustomFields() {
8179
})
8280

8381
return {
84-
clientCustomFields: clientCustomFields ?? [],
85-
companyCustomFields: companyCustomFields ?? [],
82+
clientCustomFields,
83+
companyCustomFields,
8684
optionsMap: optionsMap ?? {},
87-
isLoading: clientIsLoading || companyIsLoading || optionsMapIsLoading,
85+
isLoading: fieldsIsLoading || optionsMapIsLoading,
8886
}
8987
}

‎src/features/custom-fields/lib/custom-fields.controller.ts‎

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import AssemblyClient from '@assembly/assembly-client'
2-
import { CustomFieldEntityType } from '@assembly/types'
32
import { authenticateHeaders } from '@auth/lib/authenticate'
43
import { type NextRequest, NextResponse } from 'next/server'
5-
import z from 'zod'
64
import type { APIResponse } from '@/app/types'
7-
import { NotFoundError } from '@/errors/not-found.error'
85

9-
export const listCustomFields = async (
10-
req: NextRequest,
11-
{ params }: { params: Promise<{ entityType: string }> },
12-
): Promise<NextResponse<APIResponse>> => {
6+
// Returns custom fields for all entity types in a single call.
7+
// The client splits them by entityType, avoiding one request per type.
8+
export const listCustomFields = async (req: NextRequest): Promise<NextResponse<APIResponse>> => {
139
const user = authenticateHeaders(req.headers)
1410

1511
const assembly = new AssemblyClient(user.token)
1612

17-
const entityTypeResult = z.enum(CustomFieldEntityType).safeParse((await params).entityType)
18-
19-
if (entityTypeResult.error) {
20-
throw new NotFoundError()
21-
}
22-
23-
const response = await assembly.listCustomFields({ entityType: entityTypeResult.data })
13+
const response = await assembly.listCustomFields()
2414

2515
return NextResponse.json(response)
2616
}

0 commit comments

Comments
 (0)