Skip to content
Merged
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
28 changes: 16 additions & 12 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,23 @@ You are an expert in TypeScript, Node.js, Next.js App Router, React, Prisma, Pos
- This is the format of a server action. Example:

```typescript
export async function processHistoryAction(
unsafeData: ProcessHistoryOptions
): Promise<ServerActionResponse> {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
export const deactivateApiKeyAction = withActionInstrumentation(
"deactivateApiKey",
async (unsafeData: DeactivateApiKeyBody) => {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };

const data = processHistorySchema.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
const { data, success, error } =
deactivateApiKeyBody.safeParse(unsafeData);
if (!success) return { error: error.message };

// perform action
await processHistory();
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
Comment on lines +126 to +129
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle potential errors during the database update

The prisma.apiKey.update operation may throw an error if the update fails. To ensure robust error handling and proper communication back to the client, consider adding error handling for the database operation.

Apply this diff to handle potential database errors:

+       try {
          await prisma.apiKey.update({
            where: { id: data.id, userId },
            data: { isActive: false },
          });
+       } catch (error) {
+         return { error: error.message };
+       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
try {
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
} catch (error) {
return { error: error.message };
}


revalidatePath("/history");
}
revalidatePath("/settings");
}
);
Comment on lines +115 to +133
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Return a success response from the server action

Currently, the deactivateApiKeyAction server action does not return any response upon successful completion. To conform to the expected Promise<ServerActionResponse> type and to signal success to the caller, consider returning a success response.

Apply this diff to return a success response:

        revalidatePath("/settings");
+       return { success: true };
      }
    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const deactivateApiKeyAction = withActionInstrumentation(
"deactivateApiKey",
async (unsafeData: DeactivateApiKeyBody) => {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const data = processHistorySchema.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
const { data, success, error } =
deactivateApiKeyBody.safeParse(unsafeData);
if (!success) return { error: error.message };
// perform action
await processHistory();
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
revalidatePath("/history");
}
revalidatePath("/settings");
}
);
export const deactivateApiKeyAction = withActionInstrumentation(
"deactivateApiKey",
async (unsafeData: DeactivateApiKeyBody) => {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const { data, success, error } =
deactivateApiKeyBody.safeParse(unsafeData);
if (!success) return { error: error.message };
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
revalidatePath("/settings");
return { success: true };
}
);

```
2 changes: 1 addition & 1 deletion apps/web/app/(app)/ErrorMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function ErrorMessages() {
className="mt-2"
>
<Button type="submit" variant="red" size="sm">
Clear
I've fixed them
</Button>
</form>
</>
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/(app)/admin/AdminUserControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const AdminUserControls = () => {
onClick={async () => {
setIsProcessing(true);
const email = getValues("email");
const result = await adminProcessHistoryAction(email);
const result = await adminProcessHistoryAction({
emailAddress: email,
});
handleActionResult(result, `Processed history for ${email}`);
setIsProcessing(false);
}}
Expand Down
78 changes: 39 additions & 39 deletions apps/web/app/(app)/automation/RulesPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import Link from "next/link";
import { useForm } from "react-hook-form";
import useSWR from "swr";
import { zodResolver } from "@hookform/resolvers/zod";
Expand All @@ -30,6 +29,7 @@ import { SectionHeader } from "@/components/Typography";
import type { RulesPromptResponse } from "@/app/api/user/rules/prompt/route";
import { LoadingContent } from "@/components/LoadingContent";
import { Tooltip } from "@/components/Tooltip";
import { handleActionCall } from "@/utils/server-action";

const examplePrompts = [
'Label newsletters as "Newsletter" and archive them',
Expand Down Expand Up @@ -96,36 +96,37 @@ function RulesPromptForm({
setIsSubmitting(true);

const saveRulesPromise = async (data: SaveRulesPromptBody) => {
try {
const result = await saveRulesPromptAction(data);
console.log("saveRulesPromptAction completed", result);
setIsSubmitting(false);
if (isActionError(result)) {
throw new Error(result.error);
}

router.push("/automation?tab=rules");
mutate();
setIsSubmitting(false);
setIsSubmitting(true);
const result = await handleActionCall("saveRulesPromptAction", () =>
saveRulesPromptAction(data),
);

return result;
} catch (error) {
if (isActionError(result)) {
setIsSubmitting(false);
captureException(error);
throw error;
throw new Error(result.error);
}

router.push("/automation?tab=rules");
mutate();
setIsSubmitting(false);

return result;
};

toast.promise(() => saveRulesPromise(data), {
loading: "Saving rules... This may take a while to process...",
success: (result) => {
const { createdRules, editedRules, removedRules } = result || {};

return `Rules saved successfully! ${[
createdRules ? `${createdRules} rules created. ` : "",
editedRules ? `${editedRules} rules edited. ` : "",
removedRules ? `${removedRules} rules removed. ` : "",
].join("")}`;
const message = [
createdRules ? `${createdRules} rules created.` : "",
editedRules ? `${editedRules} rules edited.` : "",
removedRules ? `${removedRules} rules removed.` : "",
]
.filter(Boolean)
.join(" ");

return `Rules saved successfully! ${message}`;
},
error: (err) => {
return `Error saving rules: ${err.message}`;
Expand Down Expand Up @@ -198,27 +199,26 @@ Feel free to add as many as you want:
if (isSubmitting || isGenerating) return;
toast.promise(
async () => {
try {
setIsGenerating(true);
const result = await generateRulesPromptAction();
setIsGenerating(false);
if (isActionError(result))
throw new Error(result.error);
if (!result)
throw new Error("Unable to generate prompt");

const currentPrompt = getValues("rulesPrompt");
const updatedPrompt = currentPrompt
? `${currentPrompt}\n\n${result.rulesPrompt}`
: result.rulesPrompt;
setValue("rulesPrompt", updatedPrompt.trim());
setIsGenerating(true);
const result = await handleActionCall(
"generateRulesPromptAction",
generateRulesPromptAction,
);

return result;
} catch (error) {
if (isActionError(result)) {
setIsGenerating(false);
captureException(error);
throw error;
throw new Error(result.error);
}

const currentPrompt = getValues("rulesPrompt");
const updatedPrompt = currentPrompt
? `${currentPrompt}\n\n${result.rulesPrompt}`
: result.rulesPrompt;
setValue("rulesPrompt", updatedPrompt.trim());

setIsGenerating(false);

return result;
},
{
loading: "Generating prompt...",
Expand Down
7 changes: 4 additions & 3 deletions apps/web/app/(landing)/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ export default function Components() {
</div>
</div>

<TestErrorButton />

<TestActionButton />
<div className="flex gap-2">
<TestErrorButton />
<TestActionButton />
</div>
</div>
</Container>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/web/components/email-list/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ export function EmailList(props: {
if (isActionError(result)) {
setIsCategorizing((s) => ({ ...s, [thread.id]: false }));
throw new Error(`There was an error categorizing the email.`);
} else if (!result) {
throw new Error("The request did not complete");
} else {
// setCategory(res);
refetch();
Expand Down
49 changes: 28 additions & 21 deletions apps/web/utils/actions/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { processHistoryForUser } from "@/app/api/google/webhook/process-history";
import { isAdmin } from "@/utils/admin";
import type { ServerActionResponse } from "@/utils/error";
import { withActionInstrumentation } from "@/utils/actions/middleware";

export async function adminProcessHistoryAction(
emailAddress: string,
historyId?: number,
startHistoryId?: number,
): Promise<ServerActionResponse> {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
if (!isAdmin(session.user.email)) return { error: "Not admin" };
export const adminProcessHistoryAction = withActionInstrumentation(
"adminProcessHistory",
async ({
emailAddress,
historyId,
startHistoryId,
}: {
emailAddress: string;
historyId?: number;
startHistoryId?: number;
}) => {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
if (!isAdmin(session.user.email)) return { error: "Not admin" };

console.log(`Processing history for ${emailAddress}`);
console.log(`Processing history for ${emailAddress}`);

await processHistoryForUser(
{
emailAddress,
historyId: historyId ? historyId : 0,
},
{
startHistoryId: startHistoryId ? startHistoryId.toString() : undefined,
},
);
}
await processHistoryForUser(
{
emailAddress,
historyId: historyId ? historyId : 0,
},
{
startHistoryId: startHistoryId ? startHistoryId.toString() : undefined,
},
);
},
);
2 changes: 1 addition & 1 deletion apps/web/utils/actions/categorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ export const categorizeAction = withActionInstrumentation(
{ email: u.email! },
);

return res;
return { category: res?.category };
},
);
17 changes: 10 additions & 7 deletions apps/web/utils/actions/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import { revalidatePath } from "next/cache";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { ServerActionResponse } from "@/utils/error";
import { clearUserErrorMessages } from "@/utils/error-messages";
import { withActionInstrumentation } from "@/utils/actions/middleware";

export async function clearUserErrorMessagesAction(): Promise<ServerActionResponse> {
const session = await auth();
if (!session?.user) return { error: "Not logged in" };
await clearUserErrorMessages(session.user.id);
revalidatePath("/(app)", "layout");
}
export const clearUserErrorMessagesAction = withActionInstrumentation(
"clearUserErrorMessages",
async () => {
const session = await auth();
if (!session?.user) return { error: "Not logged in" };
await clearUserErrorMessages(session.user.id);
revalidatePath("/(app)", "layout");
},
);
Loading