Skip to content

Inbox Zero API#203

Merged
elie222 merged 10 commits intomainfrom
api
Jul 29, 2024
Merged

Inbox Zero API#203
elie222 merged 10 commits intomainfrom
api

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Jul 28, 2024

An API to access your Inbox Zero account

Summary by CodeRabbit

  • New Features

    • Introduced a user-friendly interface for managing API keys, allowing users to create and deactivate keys directly from the settings.
    • Added a new configuration parameter for API key salt to enhance security in API interactions.
    • Implemented a new API endpoint for retrieving user emails associated with specific groups.
    • Enhanced API validation and response structuring for group email functionalities.
  • Documentation

    • Enhanced the environment configuration documentation with details about the new API key salt parameter.
  • Tests

    • Updated test coverage to reflect new functionalities related to API key management and email retrieval.

@vercel
Copy link

vercel bot commented Jul 28, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
inbox-zero ✅ Ready (Inspect) Visit Preview Jul 29, 2024 9:05am

@elie222 elie222 changed the title Inbox Zero API WIP: Inbox Zero API Jul 28, 2024
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 28, 2024

Warning

Rate limit exceeded

@elie222 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 41 seconds before requesting another review.

How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Commits

Files that changed from the base of the PR and between 8714b35 and 1609058.

Walkthrough

This update enhances API key management by introducing user-friendly components for creating and deactivating API keys directly from the interface. Backend improvements ensure secure storage and retrieval, with validation schemas for data integrity. Overall, the changes streamline the management process, making it more intuitive and efficient for users while reinforcing security measures.

Changes

Files Change Summary
.../ApiKeysCreateForm.tsx, .../actions/api-key.ts, .../actions/validation.ts Introduced components and actions for managing API keys, including creation and deactivation functionalities. Added validation schemas for createApiKeyBody and deactivateApiKeyBody, enhancing type safety and clarity.
.../settings/ApiKeysSection.tsx Added a new component to manage API keys linked to user accounts, including displaying current keys and options to create or deactivate them.
.../api/user/group/[groupId]/examples/controller.ts, .../route.ts Introduced functionality for fetching example messages tied to user groups, streamlining the integration of user authentication and data retrieval processes.
.../env.ts Added an optional API_KEY_SALT configuration parameter, enhancing the environment setup for cryptographic purposes.
.../utils/actions/api-key.ts Introduced functions for securely generating and hashing API keys, supporting robust authentication practices.
.../migrations/20240728084326_api_key/migration.sql, .../schema.prisma Created a new ApiKey table in the database for managing API key data, ensuring relationships and indexing for integrity and performance.
.../DeleteSection.tsx Removed toast notification imports, reflecting a change in how user feedback is handled during account deletion.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ApiKeysSection
    participant ApiKeysCreateButtonModal
    participant Database

    User->>ApiKeysSection: Request to create API key
    ApiKeysSection->>ApiKeysCreateButtonModal: Open modal
    User->>ApiKeysCreateButtonModal: Submit API key details
    ApiKeysCreateButtonModal->>Database: Store hashed API key
    Database-->>ApiKeysCreateButtonModal: Confirm storage
    ApiKeysCreateButtonModal-->>ApiKeysSection: Show success message
    ApiKeysSection-->>User: Display new API key
Loading

🐰 In fields of bright green, new secrets we weave,
API keys born, oh what tricks they believe!
With a hop and a skip, we create and deactivate,
Our garden of keys, we happily cultivate.
Let’s dance in the sunshine, let our worries take flight,
In the realm of API wonders, everything feels right! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between c04e98d and 0676a33.

Files selected for processing (9)
  • apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (1 hunks)
  • apps/web/app/(app)/settings/ApiKeysSection.tsx (1 hunks)
  • apps/web/app/(app)/settings/DeleteSection.tsx (1 hunks)
  • apps/web/app/(app)/settings/page.tsx (2 hunks)
  • apps/web/components/CopyInput.tsx (1 hunks)
  • apps/web/prisma/migrations/20240728084326_api_key/migration.sql (1 hunks)
  • apps/web/prisma/schema.prisma (2 hunks)
  • apps/web/utils/actions/api-key.ts (1 hunks)
  • apps/web/utils/actions/validation.ts (2 hunks)
Files skipped from review due to trivial changes (1)
  • apps/web/app/(app)/settings/DeleteSection.tsx
Additional context used
Biome
apps/web/utils/actions/api-key.ts

[error] 3-3: A Node.js builtin module should be imported with the node: protocol.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.
Unsafe fix: Add the node: protocol.

(lint/style/useNodejsImportProtocol)


[error] 51-51: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)


[error] 5-11: Some named imports are only used as types.

This import is only used as a type.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

apps/web/app/(app)/settings/ApiKeysSection.tsx

[error] 47-47: JSX elements without children should be marked as self-closing. In JSX, it is valid for any element to be self-closing.

Unsafe fix: Use a SelfClosingElement instead

(lint/style/useSelfClosingElements)

apps/web/app/(app)/settings/ApiKeysCreateForm.tsx

[error] 17-18: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)


[error] 49-49: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

Additional comments not posted (17)
apps/web/prisma/migrations/20240728084326_api_key/migration.sql (3)

15-15: LGTM!

The unique index on hashedKey is a good practice.


18-18: LGTM!

The index on userId and isActive will optimize relevant queries.


21-21: LGTM!

The foreign key constraint with ON DELETE CASCADE and ON UPDATE CASCADE is appropriate.

apps/web/app/(app)/settings/page.tsx (2)

8-8: LGTM!

The import statement for ApiKeysSection is correct.


18-18: LGTM!

The ApiKeysSection component is properly integrated into the Settings function.

apps/web/components/CopyInput.tsx (3)

1-4: LGTM!

The import statements are necessary and correctly used in the component.


10-17: LGTM!

The input field properties and attributes are correctly set.


18-27: LGTM!

The button properties and attributes are correctly set and the logic is sound.

apps/web/utils/actions/api-key.ts (1)

44-46: LGTM!

The function generateSecureApiKey is well-implemented.

apps/web/app/(app)/settings/ApiKeysSection.tsx (4)

1-17: LGTM!

The imports are correct and necessary for the component.


23-30: LGTM!

The database query is correct and necessary for fetching the API keys.


39-63: LGTM!

The table rendering logic is correct and necessary for displaying the API keys.

Tools
Biome

[error] 47-47: JSX elements without children should be marked as self-closing. In JSX, it is valid for any element to be self-closing.

Unsafe fix: Use a SelfClosingElement instead

(lint/style/useSelfClosingElements)


65-65: LGTM!

The ApiKeysCreateButtonModal component is correctly used for creating new API keys.

apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (3)

28-46: LGTM!

The ApiKeysCreateButtonModal component is well-structured and uses appropriate UI components.


49-95: LGTM!

The ApiKeysForm component is well-structured and follows best practices for form handling and validation.

Tools
Biome

[error] 49-49: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)


97-109: LGTM!

The ApiKeysDeactivateButton component is well-structured and follows best practices for handling actions.

apps/web/prisma/schema.prisma (1)

315-327: LGTM!

The ApiKey model is well-defined and includes necessary fields for managing API keys. The index on userId and isActive is a good addition for optimizing queries.

Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid using '{}' as a type.

Prefer explicitly defining the object shape. '{}' means "any non-nullable value".

- function ApiKeysForm(props: {}) {
+ function ApiKeysForm(props: Record<string, never>) {
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
function ApiKeysForm(props: {}) {
function ApiKeysForm(props: Record<string, never>) {
Tools
Biome

[error] 49-49: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

Comment on lines +20 to +23
onClick={() => {
navigator.clipboard.writeText(value);
setCopied(true);
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling for clipboard operations.

Consider adding error handling for the clipboard write operation to improve robustness.

-  navigator.clipboard.writeText(value);
-  setCopied(true);
+  navigator.clipboard.writeText(value)
+    .then(() => setCopied(true))
+    .catch((err) => console.error('Failed to copy text: ', err));
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
onClick={() => {
navigator.clipboard.writeText(value);
setCopied(true);
}}
onClick={() => {
navigator.clipboard.writeText(value)
.then(() => setCopied(true))
.catch((err) => console.error('Failed to copy text: ', err));
}}

Comment on lines 15 to 42
export async function createApiKeyAction(
unsafeData: CreateApiKeyBody,
): Promise {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };

const data = createApiKeyBody.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };

console.log(`Creating API key for ${userId}`);

const secretKey = generateSecureApiKey();
const hashedKey = hashApiKey(secretKey);

await prisma.apiKey.create({
data: {
userId,
name: data.data.name || "Secret key",
hashedKey,
isActive: true,
},
});

revalidatePath("/settings");

return { secretKey };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Specify the return type and add error handling.

The function createApiKeyAction should specify a return type and add error handling for the database operation.

- export async function createApiKeyAction(unsafeData: CreateApiKeyBody): Promise {
+ export async function createApiKeyAction(unsafeData: CreateApiKeyBody): Promise<{ secretKey?: string; error?: string }> {
    const session = await auth();
    const userId = session?.user.id;
    if (!userId) return { error: "Not logged in" };

    const data = createApiKeyBody.safeParse(unsafeData);
    if (!data.success) return { error: "Invalid data" };

    console.log(`Creating API key for ${userId}`);

    const secretKey = generateSecureApiKey();
    const hashedKey = hashApiKey(secretKey);

    try {
        await prisma.apiKey.create({
            data: {
                userId,
                name: data.data.name || "Secret key",
                hashedKey,
                isActive: true,
            },
        });
    } catch (error) {
        console.error("Error creating API key:", error);
        return { error: "Failed to create API key" };
    }

    revalidatePath("/settings");

    return { secretKey };
}
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 async function createApiKeyAction(
unsafeData: CreateApiKeyBody,
): Promise {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const data = createApiKeyBody.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
console.log(`Creating API key for ${userId}`);
const secretKey = generateSecureApiKey();
const hashedKey = hashApiKey(secretKey);
await prisma.apiKey.create({
data: {
userId,
name: data.data.name || "Secret key",
hashedKey,
isActive: true,
},
});
revalidatePath("/settings");
return { secretKey };
}
export async function createApiKeyAction(unsafeData: CreateApiKeyBody): Promise<{ secretKey?: string; error?: string }> {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const data = createApiKeyBody.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
console.log(`Creating API key for ${userId}`);
const secretKey = generateSecureApiKey();
const hashedKey = hashApiKey(secretKey);
try {
await prisma.apiKey.create({
data: {
userId,
name: data.data.name || "Secret key",
hashedKey,
isActive: true,
},
});
} catch (error) {
console.error("Error creating API key:", error);
return { error: "Failed to create API key" };
}
revalidatePath("/settings");
return { secretKey };
}

Comment on lines 54 to 72
export async function deactivateApiKeyAction(
unsafeData: DeactivateApiKeyBody,
): Promise {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };

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

console.log(`Deactivating API key for ${userId}`);

await prisma.apiKey.update({
where: { id: data.data.id, userId },
data: { isActive: false },
});

revalidatePath("/settings");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Specify the return type and add error handling.

The function deactivateApiKeyAction should specify a return type and add error handling for the database operation.

- export async function deactivateApiKeyAction(unsafeData: DeactivateApiKeyBody): Promise {
+ export async function deactivateApiKeyAction(unsafeData: DeactivateApiKeyBody): Promise<{ error?: string }> {
    const session = await auth();
    const userId = session?.user.id;
    if (!userId) return { error: "Not logged in" };

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

    console.log(`Deactivating API key for ${userId}`);

    try {
        await prisma.apiKey.update({
            where: { id: data.data.id, userId },
            data: { isActive: false },
        });
    } catch (error) {
        console.error("Error deactivating API key:", error);
        return { error: "Failed to deactivate API key" };
    }

    revalidatePath("/settings");
}
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 async function deactivateApiKeyAction(
unsafeData: DeactivateApiKeyBody,
): Promise {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const data = deactivateApiKeyBody.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
console.log(`Deactivating API key for ${userId}`);
await prisma.apiKey.update({
where: { id: data.data.id, userId },
data: { isActive: false },
});
revalidatePath("/settings");
}
export async function deactivateApiKeyAction(unsafeData: DeactivateApiKeyBody): Promise<{ error?: string }> {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const data = deactivateApiKeyBody.safeParse(unsafeData);
if (!data.success) return { error: "Invalid data" };
console.log(`Deactivating API key for ${userId}`);
try {
await prisma.apiKey.update({
where: { id: data.data.id, userId },
data: { isActive: false },
});
} catch (error) {
console.error("Error deactivating API key:", error);
return { error: "Failed to deactivate API key" };
}
revalidatePath("/settings");
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Outside diff range, codebase verification and nitpick comments (1)
apps/web/utils/actions/validation.ts (1)

69-69: Potential issue with type inference for CreateRuleBody.

The type CreateRuleBody is now inferred from createGroupBody instead of createRuleBody. Given the structural differences between createGroupBody and createRuleBody, this change could introduce inconsistencies and bugs. Please re-evaluate this change to ensure it aligns with the intended functionality.

  • createGroupBody fields: name, prompt
  • createRuleBody fields: id, name, instructions, automate, runOnThreads

Consider reverting the change or updating the relevant code to handle the new structure correctly.

Analysis chain

Verify the change in type inference for CreateRuleBody.

The type CreateRuleBody is now inferred from createGroupBody instead of createRuleBody. Ensure that this change is intentional and does not introduce any bugs or inconsistencies.

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the change in type inference for `CreateRuleBody` is intentional and does not introduce any inconsistencies.

# Test: Search for the usage of `CreateRuleBody`. Expect: No inconsistencies in the usage.
rg --type js -A 5 $'CreateRuleBody'

Length of output: 34


Script:

#!/bin/bash
# Description: Verify the usage of `CreateRuleBody` in TypeScript files and find the definitions of `createGroupBody` and `createRuleBody`.

# Search for the usage of `CreateRuleBody` in TypeScript files.
rg --type ts -A 5 $'CreateRuleBody'

# Search for the definition of `createGroupBody`.
rg --type ts -A 5 $'createGroupBody'

# Search for the definition of `createRuleBody`.
rg --type ts -A 5 $'createRuleBody'

Length of output: 10725

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 0676a33 and 6ad5716.

Files selected for processing (3)
  • apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (1 hunks)
  • apps/web/utils/actions/api-key.ts (1 hunks)
  • apps/web/utils/actions/validation.ts (1 hunks)
Additional context used
Biome
apps/web/utils/actions/api-key.ts

[error] 3-3: A Node.js builtin module should be imported with the node: protocol.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.
Unsafe fix: Add the node: protocol.

(lint/style/useNodejsImportProtocol)


[error] 51-51: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)


[error] 5-11: Some named imports are only used as types.

This import is only used as a type.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)


[error] 11-12: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

apps/web/app/(app)/settings/ApiKeysCreateForm.tsx

[error] 17-18: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

Additional comments not posted (4)
apps/web/utils/actions/validation.ts (2)

78-79: LGTM!

The new Zod schema createApiKeyBody and its inferred type CreateApiKeyBody are correctly specified.


81-82: LGTM!

The new Zod schema deactivateApiKeyBody and its inferred type DeactivateApiKeyBody are correctly specified.

apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (2)

28-46: LGTM!

The component ApiKeysCreateButtonModal correctly renders a button to create a new API key and a modal form for the key creation process.


100-113: LGTM!

The component ApiKeysDeactivateButton correctly handles the button click event to deactivate the API key.

Comment on lines +49 to +98
function ApiKeysForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<CreateApiKeyBody>({
resolver: zodResolver(createApiKeyBody),
defaultValues: {},
});

const [secretKey, setSecretKey] = useState("");

const onSubmit: SubmitHandler<CreateApiKeyBody> = useCallback(
async (data) => {
const result = await createApiKeyAction(data);
handleActionResult(result, "API key created!");

if (!isActionError(result) && result?.secretKey) {
setSecretKey(result.secretKey);
} else {
toastError({ description: "Failed to create API key" });
}
},
[],
);

return !secretKey ? (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
type="text"
name="name"
label="Name (optional)"
placeholder="My secret key"
registerProps={register("name")}
error={errors.name}
/>

<Button type="submit" loading={isSubmitting}>
Create
</Button>
</form>
) : (
<div className="space-y-2">
<SectionDescription>
This will only be shown once. Please copy it. Your secret key is:
</SectionDescription>
<CopyInput value={secretKey} />
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add missing dependency in useCallback hook.

The useCallback hook is missing the handleActionResult and toastError dependencies.

-   [],
+   [handleActionResult, toastError],
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
function ApiKeysForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<CreateApiKeyBody>({
resolver: zodResolver(createApiKeyBody),
defaultValues: {},
});
const [secretKey, setSecretKey] = useState("");
const onSubmit: SubmitHandler<CreateApiKeyBody> = useCallback(
async (data) => {
const result = await createApiKeyAction(data);
handleActionResult(result, "API key created!");
if (!isActionError(result) && result?.secretKey) {
setSecretKey(result.secretKey);
} else {
toastError({ description: "Failed to create API key" });
}
},
[],
);
return !secretKey ? (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
type="text"
name="name"
label="Name (optional)"
placeholder="My secret key"
registerProps={register("name")}
error={errors.name}
/>
<Button type="submit" loading={isSubmitting}>
Create
</Button>
</form>
) : (
<div className="space-y-2">
<SectionDescription>
This will only be shown once. Please copy it. Your secret key is:
</SectionDescription>
<CopyInput value={secretKey} />
</div>
);
}
const onSubmit: SubmitHandler<CreateApiKeyBody> = useCallback(
async (data) => {
const result = await createApiKeyAction(data);
handleActionResult(result, "API key created!");
if (!isActionError(result) && result?.secretKey) {
setSecretKey(result.secretKey);
} else {
toastError({ description: "Failed to create API key" });
}
},
[handleActionResult, toastError],
);

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 6ad5716 and 04d0fba.

Files selected for processing (9)
  • apps/web/.env.example (1 hunks)
  • apps/web/app/(app)/automation/group/Groups.tsx (1 hunks)
  • apps/web/app/(app)/settings/ApiKeysSection.tsx (1 hunks)
  • apps/web/app/api/user/group/[groupId]/examples/controller.ts (1 hunks)
  • apps/web/app/api/user/group/[groupId]/examples/route.ts (2 hunks)
  • apps/web/app/api/v1/group/[groupId]/emails/route.ts (1 hunks)
  • apps/web/env.ts (1 hunks)
  • apps/web/utils/actions/api-key.ts (1 hunks)
  • apps/web/utils/api-key.ts (1 hunks)
Files skipped from review due to trivial changes (2)
  • apps/web/.env.example
  • apps/web/app/(app)/automation/group/Groups.tsx
Additional context used
Biome
apps/web/app/api/user/group/[groupId]/examples/controller.ts

[error] 2-3: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

apps/web/utils/actions/api-key.ts

[error] 12-13: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

apps/web/app/(app)/settings/ApiKeysSection.tsx

[error] 47-47: JSX elements without children should be marked as self-closing. In JSX, it is valid for any element to be self-closing.

Unsafe fix: Use a SelfClosingElement instead

(lint/style/useSelfClosingElements)

Additional comments not posted (8)
apps/web/utils/api-key.ts (2)

4-6: LGTM!

The generateSecureApiKey function correctly generates a secure API key using randomBytes and encodes it in base64.


8-12: LGTM!

The hashApiKey function securely hashes the API key using scryptSync with a salt from the environment variables. Error handling for missing salt is correctly implemented.

apps/web/app/api/user/group/[groupId]/examples/controller.ts (1)

7-25: LGTM!

The getExamples function correctly retrieves example messages for a group using Prisma and the Gmail API. Error handling for missing groups is implemented, and the function signature is clear and type-safe.

apps/web/app/api/user/group/[groupId]/examples/route.ts (2)

Line range hint 7-17:
LGTM!

The GET method correctly integrates the logic of getExamples and includes session validation, group ID validation, and Gmail client retrieval. The integration improves readability and reduces overhead.


4-4: Use import type for type-only imports.

The static analysis tool suggests using import type for type-only imports to ensure they are removed by the transpilers and avoid loading unnecessary modules.

- import { getExamples } from "@/app/api/user/group/[groupId]/examples/controller";
+ import type { getExamples } from "@/app/api/user/group/[groupId]/examples/controller";

Likely invalid or redundant comment.

apps/web/utils/actions/api-key.ts (1)

9-12: Use import type for type-only imports.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.

- import {
-   CreateApiKeyBody,
-   DeactivateApiKeyBody,
- } from "@/utils/actions/validation";
+ import type {
+   CreateApiKeyBody,
+   DeactivateApiKeyBody,
+ } from "@/utils/actions/validation";

Likely invalid or redundant comment.

apps/web/env.ts (2)

29-29: LGTM! The addition of API_KEY_SALT looks good.

The API_KEY_SALT parameter is correctly added as an optional string, following the pattern of other optional string parameters.


Line range hint 1-91:
The overall structure and correctness of the file are approved.

The file is well-structured, and the environment variable definitions are consistent and correct.

Comment on lines +18 to +69
export async function ApiKeysSection() {
const session = await auth();
const userId = session?.user.id;
if (!userId) throw new Error("Not authenticated");

const apiKeys = await prisma.apiKey.findMany({
where: { userId, isActive: true },
select: {
id: true,
name: true,
createdAt: true,
},
});

return (
<FormSection>
<FormSectionLeft
title="API keys"
description="Create an API key to access the Inbox Zero API. Do not share your API key with others, or expose it in the browser or other client-side code."
/>

<div className="col-span-2 space-y-4">
{apiKeys.length > 0 ? (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Created</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((apiKey) => (
<TableRow key={apiKey.id}>
<TableCell>{apiKey.name}</TableCell>
<TableCell>{apiKey.createdAt.toLocaleString()}</TableCell>
<TableCell>
<ApiKeysDeactivateButton id={apiKey.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
) : null}

<ApiKeysCreateButtonModal />
</div>
</FormSection>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Handle the null session case and use self-closing elements.

The function should handle the case where session is null, and the JSX element without children should be self-closing.

- export async function ApiKeysSection() {
+ export async function ApiKeysSection(): Promise<JSX.Element> {
    const session = await auth();
-   const userId = session?.user.id;
+   if (!session) throw new Error("Not authenticated");
+   const userId = session.user.id;
    if (!userId) throw new Error("Not authenticated");

    const apiKeys = await prisma.apiKey.findMany({
        where: { userId, isActive: true },
        select: {
            id: true,
            name: true,
            createdAt: true,
        },
    });

    return (
        <FormSection>
            <FormSectionLeft
                title="API keys"
                description="Create an API key to access the Inbox Zero API. Do not share your API key with others, or expose it in the browser or other client-side code."
            />
            <div className="col-span-2 space-y-4">
                {apiKeys.length > 0 ? (
                    <Card>
                        <Table>
                            <TableHeader>
                                <TableRow>
                                    <TableHead>Name</TableHead>
                                    <TableHead>Created</TableHead>
-                                   <TableHead></TableHead>
+                                   <TableHead />
                                </TableRow>
                            </TableHeader>
                            <TableBody>
                                {apiKeys.map((apiKey) => (
                                    <TableRow key={apiKey.id}>
                                        <TableCell>{apiKey.name}</TableCell>
                                        <TableCell>{apiKey.createdAt.toLocaleString()}</TableCell>
                                        <TableCell>
                                            <ApiKeysDeactivateButton id={apiKey.id} />
                                        </TableCell>
                                    </TableRow>
                                ))}
                            </TableBody>
                        </Table>
                    </Card>
                ) : null}
                <ApiKeysCreateButtonModal />
            </div>
        </FormSection>
    );
}
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 async function ApiKeysSection() {
const session = await auth();
const userId = session?.user.id;
if (!userId) throw new Error("Not authenticated");
const apiKeys = await prisma.apiKey.findMany({
where: { userId, isActive: true },
select: {
id: true,
name: true,
createdAt: true,
},
});
return (
<FormSection>
<FormSectionLeft
title="API keys"
description="Create an API key to access the Inbox Zero API. Do not share your API key with others, or expose it in the browser or other client-side code."
/>
<div className="col-span-2 space-y-4">
{apiKeys.length > 0 ? (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Created</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((apiKey) => (
<TableRow key={apiKey.id}>
<TableCell>{apiKey.name}</TableCell>
<TableCell>{apiKey.createdAt.toLocaleString()}</TableCell>
<TableCell>
<ApiKeysDeactivateButton id={apiKey.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
) : null}
<ApiKeysCreateButtonModal />
</div>
</FormSection>
);
}
export async function ApiKeysSection(): Promise<JSX.Element> {
const session = await auth();
if (!session) throw new Error("Not authenticated");
const userId = session.user.id;
if (!userId) throw new Error("Not authenticated");
const apiKeys = await prisma.apiKey.findMany({
where: { userId, isActive: true },
select: {
id: true,
name: true,
createdAt: true,
},
});
return (
<FormSection>
<FormSectionLeft
title="API keys"
description="Create an API key to access the Inbox Zero API. Do not share your API key with others, or expose it in the browser or other client-side code."
/>
<div className="col-span-2 space-y-4">
{apiKeys.length > 0 ? (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Created</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{apiKeys.map((apiKey) => (
<TableRow key={apiKey.id}>
<TableCell>{apiKey.name}</TableCell>
<TableCell>{apiKey.createdAt.toLocaleString()}</TableCell>
<TableCell>
<ApiKeysDeactivateButton id={apiKey.id} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
) : null}
<ApiKeysCreateButtonModal />
</div>
</FormSection>
);
}
Tools
Biome

[error] 47-47: JSX elements without children should be marked as self-closing. In JSX, it is valid for any element to be self-closing.

Unsafe fix: Use a SelfClosingElement instead

(lint/style/useSelfClosingElements)

Comment on lines 7 to 50
export async function GET(
request: Request,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;

const apiKey = request.headers.get("API-Key");

if (!apiKey)
return NextResponse.json({ error: "Missing API key" }, { status: 401 });

const user = await getUserFromApiKey(apiKey);

if (!user)
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });

const account = user.accounts[0];

if (!account)
return NextResponse.json({ error: "Missing account" }, { status: 401 });

if (!account.access_token || !account.refresh_token || !account.expires_at)
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 },
);

const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId,
);

const examples = await getExamples({
groupId,
userId: user.id,
gmail,
});

return NextResponse.json(examples);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The function GET should include error handling and logging for better debugging and user feedback.

  const { groupId } = params;

  const apiKey = request.headers.get("API-Key");

  if (!apiKey) {
    console.error("Missing API key");
    return NextResponse.json({ error: "Missing API key" }, { status: 401 });
  }

  const user = await getUserFromApiKey(apiKey);

  if (!user) {
    console.error("Invalid API key");
    return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
  }

  const account = user.accounts[0];

  if (!account) {
    console.error("Missing account");
    return NextResponse.json({ error: "Missing account" }, { status: 401 });
  }

  if (!account.access_token || !account.refresh_token || !account.expires_at) {
    console.error("Missing access token");
    return NextResponse.json(
      { error: "Missing access token" },
      { status: 401 }
    );
  }

  try {
    const gmail = await getGmailClientWithRefresh(
      {
        accessToken: account.access_token,
        refreshToken: account.refresh_token,
        expiryDate: account.expires_at,
      },
      account.providerAccountId
    );

    const examples = await getExamples({
      groupId,
      userId: user.id,
      gmail,
    });

    return NextResponse.json(examples);
  } catch (error) {
    console.error("Error retrieving emails:", error);
    return NextResponse.json({ error: "Failed to retrieve emails" }, { status: 500 });
  }
}
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 async function GET(
request: Request,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;
const apiKey = request.headers.get("API-Key");
if (!apiKey)
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
const user = await getUserFromApiKey(apiKey);
if (!user)
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
const account = user.accounts[0];
if (!account)
return NextResponse.json({ error: "Missing account" }, { status: 401 });
if (!account.access_token || !account.refresh_token || !account.expires_at)
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 },
);
const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId,
);
const examples = await getExamples({
groupId,
userId: user.id,
gmail,
});
return NextResponse.json(examples);
}
export async function GET(
request: Request,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;
const apiKey = request.headers.get("API-Key");
if (!apiKey) {
console.error("Missing API key");
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const user = await getUserFromApiKey(apiKey);
if (!user) {
console.error("Invalid API key");
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
const account = user.accounts[0];
if (!account) {
console.error("Missing account");
return NextResponse.json({ error: "Missing account" }, { status: 401 });
}
if (!account.access_token || !account.refresh_token || !account.expires_at) {
console.error("Missing access token");
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 }
);
}
try {
const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId
);
const examples = await getExamples({
groupId,
userId: user.id,
gmail,
});
return NextResponse.json(examples);
} catch (error) {
console.error("Error retrieving emails:", error);
return NextResponse.json({ error: "Failed to retrieve emails" }, { status: 500 });
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Outside diff range, codebase verification and nitpick comments (2)
apps/web/app/api/user/rules/[id]/example/controller.ts (2)

Line range hint 12-24:
Ensure proper error handling for fetching group example messages.

Consider adding error handling for the fetchPaginatedMessages function to handle potential failures gracefully.

-  return fetchPaginatedMessages({
-    groupItems: rule.group.items,
-    gmail,
-  });
+  try {
+    return fetchPaginatedMessages({
+      groupItems: rule.group.items,
+      gmail,
+    });
+  } catch (error) {
+    console.error("Failed to fetch group example messages:", error);
+    return [];
+  }

Line range hint 26-50:
Ensure proper error handling for fetching static example messages.

Consider adding error handling for the gmail.users.messages.list function to handle potential failures gracefully.

-  const response = await gmail.users.messages.list({
-    userId: "me",
-    maxResults: 50,
-    q,
-  });
+  let response;
+  try {
+    response = await gmail.users.messages.list({
+      userId: "me",
+      maxResults: 50,
+      q,
+    });
+  } catch (error) {
+    console.error("Failed to fetch static example messages:", error);
+    return [];
+  }
Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 04d0fba and b244bac.

Files selected for processing (5)
  • apps/web/app/(app)/automation/group/[groupId]/examples/page.tsx (2 hunks)
  • apps/web/app/api/user/group/[groupId]/messages/controller.ts (1 hunks)
  • apps/web/app/api/user/group/[groupId]/messages/route.ts (1 hunks)
  • apps/web/app/api/user/rules/[id]/example/controller.ts (3 hunks)
  • apps/web/app/api/v1/group/[groupId]/emails/route.ts (1 hunks)
Additional context used
Biome
apps/web/app/api/v1/group/[groupId]/emails/route.ts

[error] 1-1: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

apps/web/app/api/user/group/[groupId]/messages/controller.ts

[error] 3-3: A Node.js builtin module should be imported with the node: protocol.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.
Unsafe fix: Add the node: protocol.

(lint/style/useNodejsImportProtocol)


[error] 175-178: This else clause can be omitted because previous branches break early.

Unsafe fix: Omit the else clause.

(lint/style/noUselessElse)


[error] 226-226: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)


[error] 1-2: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)


[error] 8-9: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

Additional comments not posted (8)
apps/web/app/api/user/group/[groupId]/messages/route.ts (1)

1-5: Imports and middleware setup look good.

The imports and middleware setup are appropriate for the functionality provided in this file.

apps/web/app/(app)/automation/group/[groupId]/examples/page.tsx (2)

1-7: Imports and type definitions look good.

The imports and type definitions are appropriate for the functionality provided in this file.


Line range hint 21-40:
Component rendering logic looks good.

The component correctly renders the list of example emails grouped by sender.

apps/web/app/api/user/rules/[id]/example/controller.ts (1)

1-10: Imports and type definitions look good.

The imports and type definitions are appropriate for the functionality provided in this file.

apps/web/app/api/v1/group/[groupId]/emails/route.ts (1)

20-27: LGTM! Validation logic is correct.

The query parameters validation using zod is correctly implemented.

apps/web/app/api/user/group/[groupId]/messages/controller.ts (3)

114-121: LGTM!

The function createGroupItemsHash is correctly implemented.


206-241: LGTM!

The function fetchGroupMessages is correctly implemented.

Tools
Biome

[error] 226-226: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)


243-269: LGTM!

The function buildQuery is correctly implemented.

Comment on lines +12 to +13
const groupId = params.groupId;
if (!groupId) return NextResponse.json({ error: "Missing group id" });
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper error handling for missing groupId.

The groupId validation is good, but consider adding a status code to the response for better clarity.

-  if (!groupId) return NextResponse.json({ error: "Missing group id" });
+  if (!groupId) {
+    return NextResponse.json({ error: "Missing group id" }, { status: 400 });
+  }
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
const groupId = params.groupId;
if (!groupId) return NextResponse.json({ error: "Missing group id" });
const groupId = params.groupId;
if (!groupId) {
return NextResponse.json({ error: "Missing group id" }, { status: 400 });
}

Comment on lines +7 to +10
export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email)
return NextResponse.json({ error: "Not authenticated" });
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper error handling for authentication.

The authentication check is good, but consider adding a status code to the response for better clarity.

-  if (!session?.user.email)
-    return NextResponse.json({ error: "Not authenticated" });
+  if (!session?.user.email) {
+    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
+  }
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 GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email)
return NextResponse.json({ error: "Not authenticated" });
export const GET = withError(async (_request, { params }) => {
const session = await auth();
if (!session?.user.email) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

Comment on lines +15 to +26
const gmail = getGmailClient(session);

const { messages } = await getGroupEmails({
groupId,
userId: session.user.id,
gmail,
from: undefined,
to: undefined,
pageToken: "",
});

return NextResponse.json({ messages });
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure proper error handling for fetching emails.

Consider adding error handling for the getGroupEmails function to handle potential failures gracefully.

-  const { messages } = await getGroupEmails({
-    groupId,
-    userId: session.user.id,
-    gmail,
-    from: undefined,
-    to: undefined,
-    pageToken: "",
-  });
+  try {
+    const { messages } = await getGroupEmails({
+      groupId,
+      userId: session.user.id,
+      gmail,
+      from: undefined,
+      to: undefined,
+      pageToken: "",
+    });
+    return NextResponse.json({ messages });
+  } catch (error) {
+    return NextResponse.json({ error: "Failed to fetch group emails" }, { status: 500 });
+  }
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
const gmail = getGmailClient(session);
const { messages } = await getGroupEmails({
groupId,
userId: session.user.id,
gmail,
from: undefined,
to: undefined,
pageToken: "",
});
return NextResponse.json({ messages });
const gmail = getGmailClient(session);
try {
const { messages } = await getGroupEmails({
groupId,
userId: session.user.id,
gmail,
from: undefined,
to: undefined,
pageToken: "",
});
return NextResponse.json({ messages });
} catch (error) {
return NextResponse.json({ error: "Failed to fetch group emails" }, { status: 500 });
}

Comment on lines +175 to +178
} else {
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove redundant else clause.

The else clause can be omitted because previous branches break early.

      if (result.nextPageToken) {
        nextPaginationState = {
          type,
          chunkIndex: paginationState.chunkIndex,
          pageToken: result.nextPageToken,
          groupItemsHash: paginationState.groupItemsHash,
        };
        break;
      } else {
        paginationState.chunkIndex++;
        paginationState.pageToken = undefined;
      }
-    }
+    paginationState.chunkIndex++;
+    paginationState.pageToken = undefined;
    }
  };
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
} else {
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
if (result.nextPageToken) {
nextPaginationState = {
type,
chunkIndex: paginationState.chunkIndex,
pageToken: result.nextPageToken,
groupItemsHash: paginationState.groupItemsHash,
};
break;
}
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
Tools
Biome

[error] 175-178: This else clause can be omitted because previous branches break early.

Unsafe fix: Omit the else clause.

(lint/style/noUselessElse)

Comment on lines +23 to +53
export async function getGroupEmails({
groupId,
userId,
gmail,
from,
to,
pageToken,
}: {
groupId: string;
userId: string;
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const group = await prisma.group.findUnique({
where: { id: groupId, userId },
include: { items: true },
});

if (!group) throw new Error("Group not found");

const { messages, nextPageToken } = await fetchPaginatedMessages({
groupItems: group.items,
gmail,
from,
to,
pageToken,
});

return { messages, nextPageToken };
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The function getGroupEmails should include error handling and logging for better debugging and user feedback.

  const group = await prisma.group.findUnique({
    where: { id: groupId, userId },
    include: { items: true },
  });

  if (!group) throw new Error("Group not found");

  try {
    const { messages, nextPageToken } = await fetchPaginatedMessages({
      groupItems: group.items,
      gmail,
      from,
      to,
      pageToken,
    });

    return { messages, nextPageToken };
  } catch (error) {
    console.error("Error fetching group emails:", error);
    throw new Error("Failed to fetch group emails");
  }
}
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 async function getGroupEmails({
groupId,
userId,
gmail,
from,
to,
pageToken,
}: {
groupId: string;
userId: string;
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const group = await prisma.group.findUnique({
where: { id: groupId, userId },
include: { items: true },
});
if (!group) throw new Error("Group not found");
const { messages, nextPageToken } = await fetchPaginatedMessages({
groupItems: group.items,
gmail,
from,
to,
pageToken,
});
return { messages, nextPageToken };
export async function getGroupEmails({
groupId,
userId,
gmail,
from,
to,
pageToken,
}: {
groupId: string;
userId: string;
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const group = await prisma.group.findUnique({
where: { id: groupId, userId },
include: { items: true },
});
if (!group) throw new Error("Group not found");
try {
const { messages, nextPageToken } = await fetchPaginatedMessages({
groupItems: group.items,
gmail,
from,
to,
pageToken,
});
return { messages, nextPageToken };
} catch (error) {
console.error("Error fetching group emails:", error);
throw new Error("Failed to fetch group emails");
}
}

Comment on lines +1 to +10
import prisma from "@/utils/prisma";
import { gmail_v1 } from "googleapis";
import { createHash } from "crypto";
import groupBy from "lodash/groupBy";
import { getMessage } from "@/utils/gmail/message";
import { findMatchingGroupItem } from "@/utils/group/find-matching-group";
import { parseMessage } from "@/utils/mail";
import { extractEmailAddress } from "@/utils/email";
import { GroupItem, GroupItemType } from "@prisma/client";
import type { MessageWithGroupItem } from "@/app/(app)/automation/rule/[ruleId]/examples/types";
Copy link
Contributor

Choose a reason for hiding this comment

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

Use import type for type-only imports.

Some imports are only used as types. Using import type ensures they are removed by the transpilers and avoids loading unnecessary modules.

- import { gmail_v1 } from "googleapis";
- import { GroupItem, GroupItemType } from "@prisma/client";
+ import type { gmail_v1 } from "googleapis";
+ import type { GroupItem, GroupItemType } from "@prisma/client";
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
import prisma from "@/utils/prisma";
import { gmail_v1 } from "googleapis";
import { createHash } from "crypto";
import groupBy from "lodash/groupBy";
import { getMessage } from "@/utils/gmail/message";
import { findMatchingGroupItem } from "@/utils/group/find-matching-group";
import { parseMessage } from "@/utils/mail";
import { extractEmailAddress } from "@/utils/email";
import { GroupItem, GroupItemType } from "@prisma/client";
import type { MessageWithGroupItem } from "@/app/(app)/automation/rule/[ruleId]/examples/types";
import prisma from "@/utils/prisma";
import { createHash } from "crypto";
import groupBy from "lodash/groupBy";
import { getMessage } from "@/utils/gmail/message";
import { findMatchingGroupItem } from "@/utils/group/find-matching-group";
import { parseMessage } from "@/utils/mail";
import { extractEmailAddress } from "@/utils/email";
import type { gmail_v1 } from "googleapis";
import type { GroupItem, GroupItemType } from "@prisma/client";
import type { MessageWithGroupItem } from "@/app/(app)/automation/rule/[ruleId]/examples/types";
Tools
Biome

[error] 3-3: A Node.js builtin module should be imported with the node: protocol.

Using the node: protocol is more explicit and signals that the imported module belongs to Node.js.
Unsafe fix: Add the node: protocol.

(lint/style/useNodejsImportProtocol)


[error] 1-2: All these imports are only used as types.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)


[error] 8-9: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

Comment on lines +56 to +109
export async function fetchPaginatedMessages({
groupItems,
gmail,
from,
to,
pageToken,
}: {
groupItems: GroupItem[];
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const groupItemsHash = createGroupItemsHash(groupItems);
let paginationState: InternalPaginationState;

const defaultPaginationState = {
type: GroupItemType.FROM,
chunkIndex: 0,
groupItemsHash,
};

if (pageToken) {
try {
const decodedState = JSON.parse(
Buffer.from(pageToken, "base64").toString("utf-8"),
);
if (decodedState.groupItemsHash === groupItemsHash) {
paginationState = decodedState;
} else {
// Group items have changed, start from the beginning
paginationState = defaultPaginationState;
}
} catch (error) {
// Invalid pageToken, start from the beginning
paginationState = defaultPaginationState;
}
} else {
paginationState = defaultPaginationState;
}

const { messages, nextPaginationState } = await fetchPaginatedGroupMessages(
groupItems,
gmail,
from,
to,
paginationState,
);

const nextPageToken = nextPaginationState
? Buffer.from(JSON.stringify(nextPaginationState)).toString("base64")
: undefined;

return { messages, nextPageToken };
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The function fetchPaginatedMessages should include error handling and logging for better debugging and user feedback.

  const groupItemsHash = createGroupItemsHash(groupItems);
  let paginationState: InternalPaginationState;

  const defaultPaginationState = {
    type: GroupItemType.FROM,
    chunkIndex: 0,
    groupItemsHash,
  };

  if (pageToken) {
    try {
      const decodedState = JSON.parse(
        Buffer.from(pageToken, "base64").toString("utf-8"),
      );
      if (decodedState.groupItemsHash === groupItemsHash) {
        paginationState = decodedState;
      } else {
        // Group items have changed, start from the beginning
        paginationState = defaultPaginationState;
      }
    } catch (error) {
      console.error("Invalid pageToken:", error);
      // Invalid pageToken, start from the beginning
      paginationState = defaultPaginationState;
    }
  } else {
    paginationState = defaultPaginationState;
  }

  try {
    const { messages, nextPaginationState } = await fetchPaginatedGroupMessages(
      groupItems,
      gmail,
      from,
      to,
      paginationState,
    );

    const nextPageToken = nextPaginationState
      ? Buffer.from(JSON.stringify(nextPaginationState)).toString("base64")
      : undefined;

    return { messages, nextPageToken };
  } catch (error) {
    console.error("Error fetching paginated messages:", error);
    throw new Error("Failed to fetch paginated messages");
  }
}
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 async function fetchPaginatedMessages({
groupItems,
gmail,
from,
to,
pageToken,
}: {
groupItems: GroupItem[];
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const groupItemsHash = createGroupItemsHash(groupItems);
let paginationState: InternalPaginationState;
const defaultPaginationState = {
type: GroupItemType.FROM,
chunkIndex: 0,
groupItemsHash,
};
if (pageToken) {
try {
const decodedState = JSON.parse(
Buffer.from(pageToken, "base64").toString("utf-8"),
);
if (decodedState.groupItemsHash === groupItemsHash) {
paginationState = decodedState;
} else {
// Group items have changed, start from the beginning
paginationState = defaultPaginationState;
}
} catch (error) {
// Invalid pageToken, start from the beginning
paginationState = defaultPaginationState;
}
} else {
paginationState = defaultPaginationState;
}
const { messages, nextPaginationState } = await fetchPaginatedGroupMessages(
groupItems,
gmail,
from,
to,
paginationState,
);
const nextPageToken = nextPaginationState
? Buffer.from(JSON.stringify(nextPaginationState)).toString("base64")
: undefined;
return { messages, nextPageToken };
export async function fetchPaginatedMessages({
groupItems,
gmail,
from,
to,
pageToken,
}: {
groupItems: GroupItem[];
gmail: gmail_v1.Gmail;
from?: Date;
to?: Date;
pageToken?: string;
}) {
const groupItemsHash = createGroupItemsHash(groupItems);
let paginationState: InternalPaginationState;
const defaultPaginationState = {
type: GroupItemType.FROM,
chunkIndex: 0,
groupItemsHash,
};
if (pageToken) {
try {
const decodedState = JSON.parse(
Buffer.from(pageToken, "base64").toString("utf-8"),
);
if (decodedState.groupItemsHash === groupItemsHash) {
paginationState = decodedState;
} else {
// Group items have changed, start from the beginning
paginationState = defaultPaginationState;
}
} catch (error) {
console.error("Invalid pageToken:", error);
// Invalid pageToken, start from the beginning
paginationState = defaultPaginationState;
}
} else {
paginationState = defaultPaginationState;
}
try {
const { messages, nextPaginationState } = await fetchPaginatedGroupMessages(
groupItems,
gmail,
from,
to,
paginationState,
);
const nextPageToken = nextPaginationState
? Buffer.from(JSON.stringify(nextPaginationState)).toString("base64")
: undefined;
return { messages, nextPageToken };
} catch (error) {
console.error("Error fetching paginated messages:", error);
throw new Error("Failed to fetch paginated messages");
}
}

Comment on lines +126 to +204
async function fetchPaginatedGroupMessages(
groupItems: GroupItem[],
gmail: gmail_v1.Gmail,
from: Date | undefined,
to: Date | undefined,
paginationState: InternalPaginationState,
): Promise<{
messages: MessageWithGroupItem[];
nextPaginationState?: InternalPaginationState;
}> {
const CHUNK_SIZE = PAGE_SIZE;

const groupItemTypes: GroupItemType[] = [
GroupItemType.FROM,
GroupItemType.SUBJECT,
];
const groupItemsByType = groupBy(groupItems, (item) => item.type);

let messages: MessageWithGroupItem[] = [];
let nextPaginationState: InternalPaginationState | undefined;

const processChunk = async (type: GroupItemType) => {
const items = groupItemsByType[type] || [];
while (paginationState.type === type && messages.length < PAGE_SIZE) {
const chunk = items.slice(
paginationState.chunkIndex * CHUNK_SIZE,
(paginationState.chunkIndex + 1) * CHUNK_SIZE,
);
if (chunk.length === 0) break;

const result = await fetchGroupMessages(
type,
chunk,
gmail,
PAGE_SIZE - messages.length,
from,
to,
paginationState.pageToken,
);
messages = [...messages, ...result.messages];

if (result.nextPageToken) {
nextPaginationState = {
type,
chunkIndex: paginationState.chunkIndex,
pageToken: result.nextPageToken,
groupItemsHash: paginationState.groupItemsHash,
};
break;
} else {
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
}
};

for (const type of groupItemTypes) {
if (messages.length < PAGE_SIZE) {
await processChunk(type);
} else {
break;
}
}

// Handle transition to the next GroupItemType if current type is exhausted
// This ensures we paginate through all types in order
if (!nextPaginationState && messages.length < PAGE_SIZE) {
const nextTypeIndex = groupItemTypes.indexOf(paginationState.type) + 1;
if (nextTypeIndex < groupItemTypes.length) {
nextPaginationState = {
type: groupItemTypes[nextTypeIndex],
chunkIndex: 0,
groupItemsHash: paginationState.groupItemsHash,
};
}
}

return { messages, nextPaginationState };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The function fetchPaginatedGroupMessages should include error handling and logging for better debugging and user feedback.

  const CHUNK_SIZE = PAGE_SIZE;

  const groupItemTypes: GroupItemType[] = [
    GroupItemType.FROM,
    GroupItemType.SUBJECT,
  ];
  const groupItemsByType = groupBy(groupItems, (item) => item.type);

  let messages: MessageWithGroupItem[] = [];
  let nextPaginationState: InternalPaginationState | undefined;

  const processChunk = async (type: GroupItemType) => {
    const items = groupItemsByType[type] || [];
    while (paginationState.type === type && messages.length < PAGE_SIZE) {
      const chunk = items.slice(
        paginationState.chunkIndex * CHUNK_SIZE,
        (paginationState.chunkIndex + 1) * CHUNK_SIZE,
      );
      if (chunk.length === 0) break;

      try {
        const result = await fetchGroupMessages(
          type,
          chunk,
          gmail,
          PAGE_SIZE - messages.length,
          from,
          to,
          paginationState.pageToken,
        );
        messages = [...messages, ...result.messages];

        if (result.nextPageToken) {
          nextPaginationState = {
            type,
            chunkIndex: paginationState.chunkIndex,
            pageToken: result.nextPageToken,
            groupItemsHash: paginationState.groupItemsHash,
          };
          break;
        } else {
          paginationState.chunkIndex++;
          paginationState.pageToken = undefined;
        }
      } catch (error) {
        console.error("Error fetching group messages:", error);
        throw new Error("Failed to fetch group messages");
      }
    }
  };

  for (const type of groupItemTypes) {
    if (messages.length < PAGE_SIZE) {
      await processChunk(type);
    } else {
      break;
    }
  }

  // Handle transition to the next GroupItemType if current type is exhausted
  // This ensures we paginate through all types in order
  if (!nextPaginationState && messages.length < PAGE_SIZE) {
    const nextTypeIndex = groupItemTypes.indexOf(paginationState.type) + 1;
    if (nextTypeIndex < groupItemTypes.length) {
      nextPaginationState = {
        type: groupItemTypes[nextTypeIndex],
        chunkIndex: 0,
        groupItemsHash: paginationState.groupItemsHash,
      };
    }
  }

  return { messages, nextPaginationState };
}
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
async function fetchPaginatedGroupMessages(
groupItems: GroupItem[],
gmail: gmail_v1.Gmail,
from: Date | undefined,
to: Date | undefined,
paginationState: InternalPaginationState,
): Promise<{
messages: MessageWithGroupItem[];
nextPaginationState?: InternalPaginationState;
}> {
const CHUNK_SIZE = PAGE_SIZE;
const groupItemTypes: GroupItemType[] = [
GroupItemType.FROM,
GroupItemType.SUBJECT,
];
const groupItemsByType = groupBy(groupItems, (item) => item.type);
let messages: MessageWithGroupItem[] = [];
let nextPaginationState: InternalPaginationState | undefined;
const processChunk = async (type: GroupItemType) => {
const items = groupItemsByType[type] || [];
while (paginationState.type === type && messages.length < PAGE_SIZE) {
const chunk = items.slice(
paginationState.chunkIndex * CHUNK_SIZE,
(paginationState.chunkIndex + 1) * CHUNK_SIZE,
);
if (chunk.length === 0) break;
const result = await fetchGroupMessages(
type,
chunk,
gmail,
PAGE_SIZE - messages.length,
from,
to,
paginationState.pageToken,
);
messages = [...messages, ...result.messages];
if (result.nextPageToken) {
nextPaginationState = {
type,
chunkIndex: paginationState.chunkIndex,
pageToken: result.nextPageToken,
groupItemsHash: paginationState.groupItemsHash,
};
break;
} else {
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
}
};
for (const type of groupItemTypes) {
if (messages.length < PAGE_SIZE) {
await processChunk(type);
} else {
break;
}
}
// Handle transition to the next GroupItemType if current type is exhausted
// This ensures we paginate through all types in order
if (!nextPaginationState && messages.length < PAGE_SIZE) {
const nextTypeIndex = groupItemTypes.indexOf(paginationState.type) + 1;
if (nextTypeIndex < groupItemTypes.length) {
nextPaginationState = {
type: groupItemTypes[nextTypeIndex],
chunkIndex: 0,
groupItemsHash: paginationState.groupItemsHash,
};
}
}
return { messages, nextPaginationState };
}
async function fetchPaginatedGroupMessages(
groupItems: GroupItem[],
gmail: gmail_v1.Gmail,
from: Date | undefined,
to: Date | undefined,
paginationState: InternalPaginationState,
): Promise<{
messages: MessageWithGroupItem[];
nextPaginationState?: InternalPaginationState;
}> {
const CHUNK_SIZE = PAGE_SIZE;
const groupItemTypes: GroupItemType[] = [
GroupItemType.FROM,
GroupItemType.SUBJECT,
];
const groupItemsByType = groupBy(groupItems, (item) => item.type);
let messages: MessageWithGroupItem[] = [];
let nextPaginationState: InternalPaginationState | undefined;
const processChunk = async (type: GroupItemType) => {
const items = groupItemsByType[type] || [];
while (paginationState.type === type && messages.length < PAGE_SIZE) {
const chunk = items.slice(
paginationState.chunkIndex * CHUNK_SIZE,
(paginationState.chunkIndex + 1) * CHUNK_SIZE,
);
if (chunk.length === 0) break;
try {
const result = await fetchGroupMessages(
type,
chunk,
gmail,
PAGE_SIZE - messages.length,
from,
to,
paginationState.pageToken,
);
messages = [...messages, ...result.messages];
if (result.nextPageToken) {
nextPaginationState = {
type,
chunkIndex: paginationState.chunkIndex,
pageToken: result.nextPageToken,
groupItemsHash: paginationState.groupItemsHash,
};
break;
} else {
paginationState.chunkIndex++;
paginationState.pageToken = undefined;
}
} catch (error) {
console.error("Error fetching group messages:", error);
throw new Error("Failed to fetch group messages");
}
}
};
for (const type of groupItemTypes) {
if (messages.length < PAGE_SIZE) {
await processChunk(type);
} else {
break;
}
}
// Handle transition to the next GroupItemType if current type is exhausted
// This ensures we paginate through all types in order
if (!nextPaginationState && messages.length < PAGE_SIZE) {
const nextTypeIndex = groupItemTypes.indexOf(paginationState.type) + 1;
if (nextTypeIndex < groupItemTypes.length) {
nextPaginationState = {
type: groupItemTypes[nextTypeIndex],
chunkIndex: 0,
groupItemsHash: paginationState.groupItemsHash,
};
}
}
return { messages, nextPaginationState };
}
Tools
Biome

[error] 175-178: This else clause can be omitted because previous branches break early.

Unsafe fix: Omit the else clause.

(lint/style/noUselessElse)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between b244bac and eab8354.

Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
Files selected for processing (4)
  • apps/web/app/api/v1/group/[groupId]/emails/route.ts (1 hunks)
  • apps/web/app/api/v1/group/[groupId]/emails/validation.ts (1 hunks)
  • apps/web/app/api/v1/openapi/route.ts (1 hunks)
  • apps/web/package.json (3 hunks)
Additional context used
Biome
apps/web/app/api/v1/group/[groupId]/emails/route.ts

[error] 1-1: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)


[error] 5-9: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

Additional comments not posted (10)
apps/web/app/api/v1/group/[groupId]/emails/validation.ts (3)

4-8: Ensure optional query parameters are handled correctly.

The groupEmailsQuerySchema defines optional query parameters. Make sure the downstream code correctly handles cases where these parameters are not provided.


10-37: Validate response schema for completeness and correctness.

The groupEmailsResponseSchema defines the structure of the response object. Ensure all required fields are included and correctly typed. Specifically, verify the attachments and inline fields, which are defined as arrays of empty objects.


38-38: LGTM!

The type alias GroupEmailsResult correctly infers the type from groupEmailsResponseSchema.

apps/web/app/api/v1/openapi/route.ts (5)

9-11: Ensure schemas are correctly imported.

The groupEmailsQuerySchema and groupEmailsResponseSchema are imported from the validation file. Ensure these schemas are correctly defined and used in the OpenAPI registry.


13-13: LGTM!

The extendZodWithOpenApi function correctly extends Zod with OpenAPI capabilities.


15-21: Define security schemes correctly.

The OpenAPI registry defines an API key security scheme. Ensure this scheme is correctly referenced in the endpoint definitions.


23-42: Ensure endpoint definition is complete and correct.

The OpenAPI registry defines the /group/{groupId}/emails endpoint. Ensure the request parameters, query parameters, and response schema are correctly defined and referenced.


44-71: LGTM!

The GET function generates the OpenAPI document and returns it as a JSON response.

apps/web/package.json (2)

17-17: Approve the addition of @asteasolutions/zod-to-openapi.

The addition of @asteasolutions/zod-to-openapi enhances the capability to convert Zod schemas into OpenAPI specifications, which is beneficial for API documentation and validation.


111-111: Approve the addition of @inboxzero/eslint-config.

The addition of @inboxzero/eslint-config reinstates the ESLint configuration package, which is essential for maintaining code quality and consistency during development.

Comment on lines +1 to +2
import { GroupItemType } from "@prisma/client";
import { z } from "zod";
Copy link
Contributor

Choose a reason for hiding this comment

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

Use import type for type-only imports.

The import GroupItemType is only used as a type. Using import type ensures they are removed by the transpilers and avoids loading unnecessary modules.

- import { GroupItemType } from "@prisma/client";
+ import type { GroupItemType } from "@prisma/client";
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
import { GroupItemType } from "@prisma/client";
import { z } from "zod";
import type { GroupItemType } from "@prisma/client";
import { z } from "zod";

Comment on lines +1 to +2
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
Copy link
Contributor

Choose a reason for hiding this comment

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

Use import type for type-only imports.

The import NextRequest and NextResponse are only used as types. Using import type ensures they are removed by the transpilers and avoids loading unnecessary modules.

- import { type NextRequest, NextResponse } from "next/server";
+ import type { NextRequest, NextResponse } from "next/server";
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
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import type { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

Comment on lines +77 to +103
async function getUserFromApiKey(secretKey: string) {
const hashedKey = hashApiKey(secretKey);

const result = await prisma.apiKey.findUnique({
where: { hashedKey, isActive: true },
select: {
user: {
select: {
id: true,
accounts: {
select: {
access_token: true,
refresh_token: true,
expires_at: true,
providerAccountId: true,
},
where: { provider: "google" },
take: 1,
},
},
},
isActive: true,
},
});

return result?.user;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The function getUserFromApiKey should include error handling and logging for better debugging and user feedback.

  const hashedKey = hashApiKey(secretKey);

  try {
    const result = await prisma.apiKey.findUnique({
      where: { hashedKey, isActive: true },
      select: {
        user: {
          select: {
            id: true,
            accounts: {
              select: {
                access_token: true,
                refresh_token: true,
                expires_at: true,
                providerAccountId: true,
              },
              where: { provider: "google" },
              take: 1,
            },
          },
        },
        isActive: true,
      },
    });

    return result?.user;
  } catch (error) {
    console.error("Error retrieving user from API key:", error);
    return null;
  }
}
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
async function getUserFromApiKey(secretKey: string) {
const hashedKey = hashApiKey(secretKey);
const result = await prisma.apiKey.findUnique({
where: { hashedKey, isActive: true },
select: {
user: {
select: {
id: true,
accounts: {
select: {
access_token: true,
refresh_token: true,
expires_at: true,
providerAccountId: true,
},
where: { provider: "google" },
take: 1,
},
},
},
isActive: true,
},
});
return result?.user;
}
async function getUserFromApiKey(secretKey: string) {
const hashedKey = hashApiKey(secretKey);
try {
const result = await prisma.apiKey.findUnique({
where: { hashedKey, isActive: true },
select: {
user: {
select: {
id: true,
accounts: {
select: {
access_token: true,
refresh_token: true,
expires_at: true,
providerAccountId: true,
},
where: { provider: "google" },
take: 1,
},
},
},
isActive: true,
},
});
return result?.user;
} catch (error) {
console.error("Error retrieving user from API key:", error);
return null;
}
}

Comment on lines +5 to +9
import { hashApiKey } from "@/utils/api-key";
import {
groupEmailsQuerySchema,
GroupEmailsResult,
} from "@/app/api/v1/group/[groupId]/emails/validation";
Copy link
Contributor

Choose a reason for hiding this comment

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

Use import type for type-only imports.

The imports GroupEmailsResult are only used as types. Using import type ensures they are removed by the transpilers and avoids loading unnecessary modules.

- import { getGmailClientWithRefresh } from "@/utils/gmail/client";
- import { hashApiKey } from "@/utils/api-key";
- import { groupEmailsQuerySchema, GroupEmailsResult } from "@/app/api/v1/group/[groupId]/emails/validation";
+ import type { GroupEmailsResult } from "@/app/api/v1/group/[groupId]/emails/validation";

Committable suggestion was skipped due to low confidence.

Tools
Biome

[error] 5-9: Some named imports are only used as types.

This import is only used as a type.

Importing the types with import type ensures that they are removed by the transpilers and avoids loading unnecessary modules.
Safe fix: Use import type.

(lint/style/useImportType)

Comment on lines +11 to +75
export async function GET(
request: NextRequest,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;
const { searchParams } = new URL(request.url);
const queryResult = groupEmailsQuerySchema.safeParse(
Object.fromEntries(searchParams),
);

if (!queryResult.success) {
return NextResponse.json(
{ error: "Invalid query parameters" },
{ status: 400 },
);
}

const apiKey = request.headers.get("API-Key");

if (!apiKey)
return NextResponse.json({ error: "Missing API key" }, { status: 401 });

const user = await getUserFromApiKey(apiKey);

if (!user)
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });

const account = user.accounts[0];

if (!account)
return NextResponse.json({ error: "Missing account" }, { status: 401 });

if (!account.access_token || !account.refresh_token || !account.expires_at)
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 },
);

const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId,
);

const { pageToken, from, to } = queryResult.data;

const { messages, nextPageToken } = await getGroupEmails({
groupId,
userId: user.id,
gmail,
from: from ? new Date(from) : undefined,
to: to ? new Date(to) : undefined,
pageToken,
});

const result: GroupEmailsResult = {
messages,
nextPageToken,
};

return NextResponse.json(result);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Add error handling and logging.

The GET function should include error handling and logging for better debugging and user feedback.

  const { groupId } = params;
  const { searchParams } = new URL(request.url);
  const queryResult = groupEmailsQuerySchema.safeParse(Object.fromEntries(searchParams));

  if (!queryResult.success) {
    return NextResponse.json(
      { error: "Invalid query parameters" },
      { status: 400 },
    );
  }

  const apiKey = request.headers.get("API-Key");

  if (!apiKey) {
    console.error("Missing API key");
    return NextResponse.json({ error: "Missing API key" }, { status: 401 });
  }

  const user = await getUserFromApiKey(apiKey);

  if (!user) {
    console.error("Invalid API key");
    return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
  }

  const account = user.accounts[0];

  if (!account) {
    console.error("Missing account");
    return NextResponse.json({ error: "Missing account" }, { status: 401 });
  }

  if (!account.access_token || !account.refresh_token || !account.expires_at) {
    console.error("Missing access token");
    return NextResponse.json(
      { error: "Missing access token" },
      { status: 401 }
    );
  }

  try {
    const gmail = await getGmailClientWithRefresh(
      {
        accessToken: account.access_token,
        refreshToken: account.refresh_token,
        expiryDate: account.expires_at,
      },
      account.providerAccountId
    );

    const examples = await getExamples({
      groupId,
      userId: user.id,
      gmail,
    });

    return NextResponse.json(examples);
  } catch (error) {
    console.error("Error retrieving emails:", error);
    return NextResponse.json({ error: "Failed to retrieve emails" }, { status: 500 });
  }
}
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 async function GET(
request: NextRequest,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;
const { searchParams } = new URL(request.url);
const queryResult = groupEmailsQuerySchema.safeParse(
Object.fromEntries(searchParams),
);
if (!queryResult.success) {
return NextResponse.json(
{ error: "Invalid query parameters" },
{ status: 400 },
);
}
const apiKey = request.headers.get("API-Key");
if (!apiKey)
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
const user = await getUserFromApiKey(apiKey);
if (!user)
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
const account = user.accounts[0];
if (!account)
return NextResponse.json({ error: "Missing account" }, { status: 401 });
if (!account.access_token || !account.refresh_token || !account.expires_at)
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 },
);
const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId,
);
const { pageToken, from, to } = queryResult.data;
const { messages, nextPageToken } = await getGroupEmails({
groupId,
userId: user.id,
gmail,
from: from ? new Date(from) : undefined,
to: to ? new Date(to) : undefined,
pageToken,
});
const result: GroupEmailsResult = {
messages,
nextPageToken,
};
return NextResponse.json(result);
}
export async function GET(
request: NextRequest,
{ params }: { params: { groupId: string } },
) {
const { groupId } = params;
const { searchParams } = new URL(request.url);
const queryResult = groupEmailsQuerySchema.safeParse(
Object.fromEntries(searchParams),
);
if (!queryResult.success) {
return NextResponse.json(
{ error: "Invalid query parameters" },
{ status: 400 },
);
}
const apiKey = request.headers.get("API-Key");
if (!apiKey) {
console.error("Missing API key");
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const user = await getUserFromApiKey(apiKey);
if (!user) {
console.error("Invalid API key");
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
const account = user.accounts[0];
if (!account) {
console.error("Missing account");
return NextResponse.json({ error: "Missing account" }, { status: 401 });
}
if (!account.access_token || !account.refresh_token || !account.expires_at) {
console.error("Missing access token");
return NextResponse.json(
{ error: "Missing access token" },
{ status: 401 }
);
}
try {
const gmail = await getGmailClientWithRefresh(
{
accessToken: account.access_token,
refreshToken: account.refresh_token,
expiryDate: account.expires_at,
},
account.providerAccountId
);
const { pageToken, from, to } = queryResult.data;
const { messages, nextPageToken } = await getGroupEmails({
groupId,
userId: user.id,
gmail,
from: from ? new Date(from) : undefined,
to: to ? new Date(to) : undefined,
pageToken,
});
const result: GroupEmailsResult = {
messages,
nextPageToken,
};
return NextResponse.json(result);
} catch (error) {
console.error("Error retrieving emails:", error);
return NextResponse.json({ error: "Failed to retrieve emails" }, { status: 500 });
}
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between eab8354 and 8714b35.

Files selected for processing (1)
  • apps/web/utils/actions/validation.ts (1 hunks)
Files skipped from review as they are similar to previous changes (1)
  • apps/web/utils/actions/validation.ts

@elie222 elie222 changed the title WIP: Inbox Zero API Inbox Zero API Jul 29, 2024
@elie222 elie222 merged commit 3eca2ce into main Jul 29, 2024
@elie222 elie222 deleted the api branch July 29, 2024 09:24
@coderabbitai coderabbitai bot mentioned this pull request Nov 12, 2024
@coderabbitai coderabbitai bot mentioned this pull request Nov 27, 2024
This was referenced Dec 5, 2024
@coderabbitai coderabbitai bot mentioned this pull request Dec 30, 2024
@coderabbitai coderabbitai bot mentioned this pull request Jan 31, 2025
@coderabbitai coderabbitai bot mentioned this pull request Feb 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments