Skip to content

Commit

Permalink
Merge pull request dubinc#771 from dubinc/eng-249
Browse files Browse the repository at this point in the history
Add Zod to `/domains` endpoints and document in Mintlify
  • Loading branch information
steven-tey authored Apr 17, 2024
2 parents 4bcb12a + ff9e60b commit ba88e5e
Show file tree
Hide file tree
Showing 30 changed files with 452 additions and 112 deletions.
4 changes: 4 additions & 0 deletions apps/docs/api-reference/endpoint/add-a-domain.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: post /domains
og:title: "Add a domain to the workspace with the Dub.co API - API Reference"
---
4 changes: 4 additions & 0 deletions apps/docs/api-reference/endpoint/delete-a-domain.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: delete /domains/{slug}
og:title: "Delete a domain from the workspace with the Dub.co API - API Reference"
---
4 changes: 4 additions & 0 deletions apps/docs/api-reference/endpoint/edit-a-domain.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: patch /domains/{slug}
og:title: "Edit a domain with the Dub.co API - API Reference"
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: get /domains
og:title: "Get list of domains associated with the workspace with the Dub.co API - API Reference"
---
4 changes: 4 additions & 0 deletions apps/docs/api-reference/endpoint/set-a-primary-domain.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: post /domains/{slug}/primary
og:title: "Set a domain as primary for a workspace with the Dub.co API - API Reference"
---
4 changes: 4 additions & 0 deletions apps/docs/api-reference/endpoint/transfer-a-domain.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
openapi: post /domains/{slug}/transfer
og:title: "Transfer a domain to the another workspace with the Dub.co API - API Reference"
---
11 changes: 11 additions & 0 deletions apps/docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@
"api-reference/endpoint/retrieve-a-list-of-tags"
]
},
{
"group": "Domains",
"pages": [
"api-reference/endpoint/add-a-domain",
"api-reference/endpoint/edit-a-domain",
"api-reference/endpoint/delete-a-domain",
"api-reference/endpoint/retrieve-a-list-of-domains",
"api-reference/endpoint/set-a-primary-domain",
"api-reference/endpoint/transfer-a-domain"
]
},
{
"group": "Metatags",
"pages": ["api-reference/endpoint/retrieve-metatags"]
Expand Down
27 changes: 0 additions & 27 deletions apps/web/app/api/domains/[domain]/archive/route.ts

This file was deleted.

10 changes: 7 additions & 3 deletions apps/web/app/api/domains/[domain]/primary/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { withAuth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { DomainSchema } from "@/lib/zod/schemas";
import { NextResponse } from "next/server";

// POST /api/domains/[domain]/primary – set a domain as primary
export const POST = withAuth(
async ({ headers, workspace, domain }) => {
const response = await Promise.all([
const responses = await Promise.all([
prisma.domain.update({
where: {
slug: domain,
Expand All @@ -19,14 +20,17 @@ export const POST = withAuth(
where: {
projectId: workspace.id,
primary: true,
slug: {
not: domain,
},
},
data: {
primary: false,
},
}),
]);
console.log({ response });
return NextResponse.json(response, { headers });

return NextResponse.json(DomainSchema.parse(responses[0]), { headers });
},
{
domainChecks: true,
Expand Down
46 changes: 29 additions & 17 deletions apps/web/app/api/domains/[domain]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {
setRootDomain,
validateDomain,
} from "@/lib/api/domains";
import { DubApiError } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { withAuth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { DomainSchema, updateDomainBodySchema } from "@/lib/zod/schemas";
import { NextResponse } from "next/server";

// GET /api/domains/[domain] – get a workspace's domain
Expand All @@ -28,7 +31,10 @@ export const GET = withAuth(
},
});
if (!data) {
return new Response("Domain not found", { status: 404 });
throw new DubApiError({
code: "not_found",
message: "Domain not found",
});
}
return NextResponse.json({
...data,
Expand All @@ -41,24 +47,32 @@ export const GET = withAuth(
);

// PUT /api/domains/[domain] – edit a workspace's domain
export const PUT = withAuth(
export const PATCH = withAuth(
async ({ req, workspace, domain }) => {
const body = await parseRequestBody(req);
const {
slug: newDomain,
target,
type,
placeholder,
expiredUrl,
} = await req.json();
archived,
} = updateDomainBodySchema.parse(body);

if (newDomain !== domain) {
if (newDomain && newDomain !== domain) {
const validDomain = await validateDomain(newDomain);
if (validDomain !== true) {
return new Response(validDomain, { status: 422 });
throw new DubApiError({
code: "unprocessable_entity",
message: validDomain,
});
}
const vercelResponse = await addDomainToVercel(newDomain);
if (vercelResponse.error) {
return new Response(vercelResponse.error.message, { status: 422 });
throw new DubApiError({
code: "unprocessable_entity",
message: vercelResponse.error.message,
});
}
}

Expand All @@ -68,15 +82,13 @@ export const PUT = withAuth(
slug: domain,
},
data: {
...(newDomain !== domain && {
slug: newDomain,
}),
slug: newDomain,
type,
placeholder,
// only set target and expiredUrl if the workspace is not free
archived,
...(placeholder && { placeholder }),
...(workspace.plan !== "free" && {
target: target || null,
expiredUrl: expiredUrl || null,
target,
expiredUrl,
}),
},
}),
Expand All @@ -88,7 +100,7 @@ export const PUT = withAuth(
id: response[0].id,
domain,
...(workspace.plan !== "free" && {
url: target,
url: target || undefined,
}),
rewrite: type === "rewrite",
...(newDomain !== domain && {
Expand All @@ -97,7 +109,7 @@ export const PUT = withAuth(
projectId: workspace.id,
});

return NextResponse.json(response);
return NextResponse.json(DomainSchema.parse(response[0]));
},
{
domainChecks: true,
Expand All @@ -108,8 +120,8 @@ export const PUT = withAuth(
// DELETE /api/domains/[domain] - delete a workspace's domain
export const DELETE = withAuth(
async ({ domain }) => {
const response = await deleteDomainAndLinks(domain);
return NextResponse.json(response);
await deleteDomainAndLinks(domain);
return NextResponse.json({ slug: domain });
},
{
domainChecks: true,
Expand Down
11 changes: 2 additions & 9 deletions apps/web/app/api/domains/[domain]/transfer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,10 @@ import { DubApiError } from "@/lib/api/errors";
import { withAuth } from "@/lib/auth";
import { qstash } from "@/lib/cron";
import prisma from "@/lib/prisma";
import z from "@/lib/zod";
import { DomainSchema, transferDomainBodySchema } from "@/lib/zod/schemas";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
import { NextResponse } from "next/server";

const transferDomainBodySchema = z.object({
newWorkspaceId: z
.string()
.min(1, "Missing new workspace ID.")
.transform((v) => v.replace("ws_", "")),
});

// POST /api/domains/[domain]/transfer – transfer a domain to another workspace
export const POST = withAuth(
async ({ req, headers, session, params, workspace }) => {
Expand Down Expand Up @@ -156,7 +149,7 @@ export const POST = withAuth(
},
});

return NextResponse.json(domainResponse, { headers });
return NextResponse.json(DomainSchema.parse(domainResponse), { headers });
},
{ requiredRole: ["owner"] },
);
11 changes: 7 additions & 4 deletions apps/web/app/api/domains/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
validateDomain,
} from "@/lib/api/domains";
import { exceededLimitError } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { withAuth } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { DomainSchema, addDomainBodySchema } from "@/lib/zod/schemas";
import { NextResponse } from "next/server";

// GET /api/domains – get all domains for a workspace
Expand All @@ -31,13 +33,14 @@ export const GET = withAuth(async ({ workspace }) => {

// POST /api/domains - add a domain
export const POST = withAuth(async ({ req, workspace }) => {
const body = await parseRequestBody(req);
const {
slug: domain,
target,
type,
expiredUrl,
placeholder,
} = await req.json();
} = addDomainBodySchema.parse(body);

if (workspace.domains.length >= workspace.domainsLimit) {
return new Response(
Expand Down Expand Up @@ -75,7 +78,7 @@ export const POST = withAuth(async ({ req, workspace }) => {
type,
projectId: workspace.id,
primary: workspace.domains.length === 0,
placeholder,
...(placeholder && { placeholder }),
...(workspace.plan !== "free" && {
target,
expiredUrl,
Expand All @@ -88,10 +91,10 @@ export const POST = withAuth(async ({ req, workspace }) => {
domain,
projectId: workspace.id,
...(workspace.plan !== "free" && {
url: target,
url: target || undefined,
}),
rewrite: type === "rewrite",
});

return NextResponse.json(response);
return NextResponse.json(DomainSchema.parse(response), { status: 201 });
});
17 changes: 0 additions & 17 deletions apps/web/lib/api/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,23 +207,6 @@ export async function setRootDomain({
]);
}

export async function archiveDomain({
domain,
archived,
}: {
domain: string;
archived: boolean;
}) {
return await prisma.domain.update({
where: {
slug: domain,
},
data: {
archived,
},
});
}

/* Delete a domain and all links & images associated with it */
export async function deleteDomainAndLinks(
domain: string,
Expand Down
6 changes: 5 additions & 1 deletion apps/web/lib/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export function fromZodError(error: ZodError): ErrorResponse {
}

export function handleApiError(error: any): ErrorResponse & { status: number } {
console.error("API error occurred", error);

// Zod errors
if (error instanceof ZodError) {
return {
Expand All @@ -139,10 +141,12 @@ export function handleApiError(error: any): ErrorResponse & { status: number } {
}

// Fallback
// Unhandled errors are not user-facing, so we don't expose the actual error
return {
error: {
code: "internal_server_error",
message: error instanceof Error ? error.message : "Internal Server Error",
message:
"An internal server error occurred. Please contact our support if the problem persists.",
doc_url: `${docErrorUrl}#internal_server_error`,
},
status: 500,
Expand Down
15 changes: 15 additions & 0 deletions apps/web/lib/api/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DubApiError } from "./errors";

// TODO:
// Move this to a proper place
export const parseRequestBody = async (req: Request) => {
try {
return await req.json();
} catch (e) {
throw new DubApiError({
code: "bad_request",
message:
"Invalid JSON format in request body. Please ensure the request body is a valid JSON object.",
});
}
};
34 changes: 34 additions & 0 deletions apps/web/lib/openapi/domains/add-domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { openApiErrorResponses } from "@/lib/openapi/responses";
import { DomainSchema, addDomainBodySchema } from "@/lib/zod/schemas";
import { ZodOpenApiOperationObject } from "zod-openapi";
import { workspaceParamsSchema } from "../request";

export const addDomain: ZodOpenApiOperationObject = {
operationId: "addDomain",
"x-speakeasy-name-override": "add",
summary: "Add a domain",
description: "Add a domain to the authenticated workspace.",
requestParams: {
query: workspaceParamsSchema,
},
requestBody: {
content: {
"application/json": {
schema: addDomainBodySchema,
},
},
},
responses: {
"200": {
description: "The domain was added.",
content: {
"application/json": {
schema: DomainSchema,
},
},
},
...openApiErrorResponses,
},
tags: ["Domains"],
security: [{ token: [] }],
};
Loading

0 comments on commit ba88e5e

Please sign in to comment.