Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/web/app/(ee)/api/admin/partners/platforms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ const postSchema = z.object({
// POST /api/admin/partners/platforms
export const POST = withAdmin(
async ({ req }) => {
const { partnerId, platform, identifier: rawIdentifier, postUrl } = postSchema
.parse(await req.json());
const {
partnerId,
platform,
identifier: rawIdentifier,
postUrl,
} = postSchema.parse(await req.json());

const partner = await prisma.partner.findUnique({
where: { id: partnerId },
Expand Down
23 changes: 19 additions & 4 deletions apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,39 @@ import {
GroupSchema,
updateGroupSchema,
} from "@/lib/zod/schemas/groups";
import { booleanQuerySchema } from "@/lib/zod/schemas/misc";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK, constructURLFromUTMParams } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

const getGroupQuerySchema = z.object({
includeExpandedFields: booleanQuerySchema.optional(),
includeBounties: booleanQuerySchema.optional(),
});

// GET /api/groups/[groupIdOrSlug] - get information about a group
export const GET = withWorkspace(
async ({ params, workspace }) => {
async ({ params, workspace, searchParams }) => {
const { includeExpandedFields, includeBounties } =
getGroupQuerySchema.parse(searchParams);

const programId = getDefaultProgramIdOrThrow(workspace);

const schema =
includeExpandedFields || includeBounties
? GroupWithProgramSchema
: GroupSchema;

const group = await getGroupOrThrow({
programId,
groupId: params.groupIdOrSlug,
includeExpandedFields: true,
includeBounties: true,
includeExpandedFields,
includeBounties,
});

return NextResponse.json(GroupWithProgramSchema.parse(group));
return NextResponse.json(schema.parse(group));
},
{
requiredPermissions: ["groups.read"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function EmbedBountyDetail({
aria-label="Back to bounties"
title="Back to bounties"
onClick={onBack}
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
>
<Trophy className="size-4" />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function EmbedBountySubmissionDetail({
aria-label="Back to bounties"
title="Back to bounties"
onClick={onBackToRoot}
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
>
<Trophy className="size-4" />
</button>
Expand Down
31 changes: 31 additions & 0 deletions apps/web/lib/openapi/groups/create-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { openApiErrorResponses } from "@/lib/openapi/responses";
import { createGroupSchema, GroupSchema } from "@/lib/zod/schemas/groups";
import { ZodOpenApiOperationObject } from "zod-openapi";

export const createGroup: ZodOpenApiOperationObject = {
operationId: "createGroup",
"x-speakeasy-name-override": "create",
summary: "Create a group",
description:
"Create a group for the authenticated workspace's partner program.",
requestBody: {
content: {
"application/json": {
schema: createGroupSchema,
},
},
},
responses: {
"201": {
description: "The created group",
content: {
"application/json": {
schema: GroupSchema,
},
},
},
...openApiErrorResponses,
},
tags: ["Groups"],
security: [{ token: [] }],
};
34 changes: 34 additions & 0 deletions apps/web/lib/openapi/groups/get-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { openApiErrorResponses } from "@/lib/openapi/responses";
import { GroupSchema } from "@/lib/zod/schemas/groups";
import { ZodOpenApiOperationObject } from "zod-openapi";
import * as z from "zod/v4";

export const getGroup: ZodOpenApiOperationObject = {
operationId: "getGroup",
"x-speakeasy-name-override": "get",
summary: "Retrieve a group",
description:
"Retrieve a group by ID or slug for the authenticated workspace's partner program.",
requestParams: {
path: z.object({
groupId: z
.string()
.describe(
"The ID of the group to retrieve. You can also use the slug of the group.",
),
}),
},
responses: {
"200": {
description: "The group",
content: {
"application/json": {
schema: GroupSchema,
},
},
},
...openApiErrorResponses,
},
tags: ["Groups"],
security: [{ token: [] }],
};
14 changes: 14 additions & 0 deletions apps/web/lib/openapi/groups/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ZodOpenApiPathsObject } from "zod-openapi";
import { createGroup } from "./create-group";
import { getGroup } from "./get-group";
import { listGroups } from "./list-groups";

export const groupsPaths: ZodOpenApiPathsObject = {
"/groups": {
post: createGroup,
get: listGroups,
},
"/groups/{groupId}": {
get: getGroup,
},
};
31 changes: 31 additions & 0 deletions apps/web/lib/openapi/groups/list-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { openApiErrorResponses } from "@/lib/openapi/responses";
import {
getGroupsQuerySchema,
GroupSchemaExtended,
} from "@/lib/zod/schemas/groups";
import { ZodOpenApiOperationObject } from "zod-openapi";
import * as z from "zod/v4";

export const listGroups: ZodOpenApiOperationObject = {
operationId: "listGroups",
"x-speakeasy-name-override": "list",
summary: "Retrieve a list of groups",
description:
"Retrieve a list of groups for the authenticated workspace's partner program.",
requestParams: {
query: getGroupsQuerySchema,
},
responses: {
"200": {
description: "A list of groups",
content: {
"application/json": {
schema: z.array(GroupSchemaExtended),
},
},
},
...openApiErrorResponses,
},
tags: ["Groups"],
security: [{ token: [] }],
};
2 changes: 2 additions & 0 deletions apps/web/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { domainsPaths } from "./domains";
import { embedTokensPaths } from "./embed-tokens";
import { eventsPath } from "./events";
import { foldersPaths } from "./folders";
import { groupsPaths } from "./groups";
import { linksPaths } from "./links";
import { partnersPaths } from "./partners";
import { payoutsPaths } from "./payouts";
Expand Down Expand Up @@ -52,6 +53,7 @@ export const document = createDocument({
...domainsPaths,
...trackPaths,
...customersPaths,
...groupsPaths,
...partnersPaths,
...commissionsPaths,
...payoutsPaths,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/swr/use-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function useGroup<T = GroupProps>(
mutate: mutateGroup,
} = useSWR<T>(
workspaceId && groupIdOrSlug
? `/api/groups/${groupIdOrSlug}?${new URLSearchParams({ workspaceId, ...query }).toString()}`
? `/api/groups/${groupIdOrSlug}?${new URLSearchParams({ workspaceId, includeExpandedFields: "true", ...query }).toString()}`
: null,
fetcher,
{
Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/zod/schemas/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ export const additionalPartnerLinkSchema = z.object({
.refine((v) => isValidDomainFormatWithLocalhost(v), {
message: "Please enter a valid domain (eg: acme.com or localhost:3000).",
})
.transform((v) => v.toLowerCase()),
.overwrite((v) => v.toLowerCase()),
path: z
.string()
.transform((v) => v.toLowerCase())
.overwrite((v) => v.toLowerCase())
.optional()
.default(""),
validationMode: z.enum([
Expand Down
9 changes: 4 additions & 5 deletions apps/web/lib/zod/schemas/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,10 @@ export const rewardConditionsArraySchema = z
.array(rewardConditionsSchema)
.min(1);

const decimalToNumber = z
.any()
.transform((val) => (val != null && val !== "" ? Number(val) : null))
.nullable()
.optional();
const decimalToNumber = z.preprocess(
(val) => (val != null && val !== "" ? Number(val) : null),
z.number().nullable().optional(),
);

export const RewardSchema = z.object({
id: z.string(),
Expand Down
33 changes: 32 additions & 1 deletion apps/web/tests/partner-groups/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe.sequential("/groups/**", async () => {
// Fetch the default group to get its default values
const { data: defaultGroup } = await http.get<GroupWithProgramProps>({
path: `/groups/${DEFAULT_PARTNER_GROUP.slug}`,
query: { includeExpandedFields: "true" },
});

const groupName = generateRandomName();
Expand Down Expand Up @@ -79,9 +80,35 @@ describe.sequential("/groups/**", async () => {
group = data;
});

test("GET /groups/[groupId] - fetch single group", async () => {
test("GET /groups/[groupId] - fetch single group without extended params", async () => {
const { status, data } = await http.get<GroupProps>({
path: `/groups/${group.id}`,
});

expect(status).toEqual(200);
expect(() => GroupSchema.parse(data)).not.toThrow();
expect(data).not.toHaveProperty("program");
expect(data).not.toHaveProperty("applicationFormData");
expect(data).not.toHaveProperty("landerData");
expect(data).toMatchObject({
id: group.id,
name: group.name,
slug: group.slug,
color: group.color,
logo: group.logo,
wordmark: group.wordmark,
brandColor: group.brandColor,
holdingPeriodDays: group.holdingPeriodDays,
maxPartnerLinks: group.maxPartnerLinks,
linkStructure: group.linkStructure,
additionalLinks: group.additionalLinks,
});
});

test("GET /groups/[groupId] - fetch single group with includeExpandedFields", async () => {
const { status, data } = await http.get<GroupWithProgramProps>({
path: `/groups/${group.id}`,
query: { includeExpandedFields: "true" },
});

const {
Expand All @@ -95,6 +122,7 @@ describe.sequential("/groups/**", async () => {
} = data;

expect(status).toEqual(200);
expect(program).toBeDefined();
expect(fetchedGroup).toStrictEqual({
...group,
utmTemplate: null,
Expand Down Expand Up @@ -161,6 +189,7 @@ describe.sequential("/groups/**", async () => {
// Fetch the group to verify moveRules was persisted
const { data: fetchedGroup } = await http.get<GroupWithProgramProps>({
path: `/groups/${group.id}`,
query: { includeExpandedFields: "true" },
});

const {
Expand Down Expand Up @@ -206,6 +235,7 @@ describe.sequential("/groups/**", async () => {
// Fetch the group to verify moveRules was updated
const { data: fetchedGroup } = await http.get<GroupWithProgramProps>({
path: `/groups/${group.id}`,
query: { includeExpandedFields: "true" },
});

const {
Expand Down Expand Up @@ -238,6 +268,7 @@ describe.sequential("/groups/**", async () => {
// Fetch the group to verify moveRules was removed
const { data: fetchedGroup } = await http.get<GroupWithProgramProps>({
path: `/groups/${group.id}`,
query: { includeExpandedFields: "true" },
});

const {
Expand Down
4 changes: 1 addition & 3 deletions apps/web/ui/analytics/partner-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,7 @@ export function PartnerSection() {
filterParamKey ? onApplyFilterValues : undefined
}
onImmediateFilter={
filterParamKey
? (val) => onApplyFilterValues([val])
: undefined
filterParamKey ? (val) => onApplyFilterValues([val]) : undefined
}
{...(limit && { limit })}
/>
Expand Down
4 changes: 1 addition & 3 deletions apps/web/ui/analytics/top-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ export function TopLinks() {
filterParamKey ? onApplyFilterValues : undefined
}
onImmediateFilter={
filterParamKey
? (val) => onApplyFilterValues([val])
: undefined
filterParamKey ? (val) => onApplyFilterValues([val]) : undefined
}
{...(limit && { limit })}
/>
Expand Down
Loading