Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
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 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. WalkthroughThis 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
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
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? TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Additionally, you can add CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 18
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
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
hashedKeyis a good practice.
18-18: LGTM!The index on
userIdandisActivewill optimize relevant queries.
21-21: LGTM!The foreign key constraint with
ON DELETE CASCADEandON UPDATE CASCADEis appropriate.apps/web/app/(app)/settings/page.tsx (2)
8-8: LGTM!The import statement for
ApiKeysSectionis correct.
18-18: LGTM!The
ApiKeysSectioncomponent is properly integrated into theSettingsfunction.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
generateSecureApiKeyis 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
ApiKeysCreateButtonModalcomponent is correctly used for creating new API keys.apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (3)
28-46: LGTM!The
ApiKeysCreateButtonModalcomponent is well-structured and uses appropriate UI components.
49-95: LGTM!The
ApiKeysFormcomponent 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
ApiKeysDeactivateButtoncomponent is well-structured and follows best practices for handling actions.apps/web/prisma/schema.prisma (1)
315-327: LGTM!The
ApiKeymodel is well-defined and includes necessary fields for managing API keys. The index onuserIdandisActiveis a good addition for optimizing queries.
There was a problem hiding this comment.
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.
| 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)
| onClick={() => { | ||
| navigator.clipboard.writeText(value); | ||
| setCopied(true); | ||
| }} |
There was a problem hiding this comment.
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.
| onClick={() => { | |
| navigator.clipboard.writeText(value); | |
| setCopied(true); | |
| }} | |
| onClick={() => { | |
| navigator.clipboard.writeText(value) | |
| .then(() => setCopied(true)) | |
| .catch((err) => console.error('Failed to copy text: ', err)); | |
| }} |
| 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 }; | ||
| } |
There was a problem hiding this comment.
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.
| 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 }; | |
| } |
| 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"); | ||
| } |
There was a problem hiding this comment.
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.
| 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"); | |
| } |
There was a problem hiding this comment.
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 forCreateRuleBody.The type
CreateRuleBodyis now inferred fromcreateGroupBodyinstead ofcreateRuleBody. Given the structural differences betweencreateGroupBodyandcreateRuleBody, this change could introduce inconsistencies and bugs. Please re-evaluate this change to ensure it aligns with the intended functionality.
createGroupBodyfields:name,promptcreateRuleBodyfields:id,name,instructions,automate,runOnThreadsConsider 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
CreateRuleBodyis now inferred fromcreateGroupBodyinstead ofcreateRuleBody. 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
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
createApiKeyBodyand its inferred typeCreateApiKeyBodyare correctly specified.
81-82: LGTM!The new Zod schema
deactivateApiKeyBodyand its inferred typeDeactivateApiKeyBodyare correctly specified.apps/web/app/(app)/settings/ApiKeysCreateForm.tsx (2)
28-46: LGTM!The component
ApiKeysCreateButtonModalcorrectly renders a button to create a new API key and a modal form for the key creation process.
100-113: LGTM!The component
ApiKeysDeactivateButtoncorrectly handles the button click event to deactivate the API key.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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>
There was a problem hiding this comment.
Actionable comments posted: 5
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
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
generateSecureApiKeyfunction correctly generates a secure API key usingrandomBytesand encodes it in base64.
8-12: LGTM!The
hashApiKeyfunction securely hashes the API key usingscryptSyncwith 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
getExamplesfunction 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
GETmethod correctly integrates the logic ofgetExamplesand includes session validation, group ID validation, and Gmail client retrieval. The integration improves readability and reduces overhead.
4-4: Useimport typefor type-only imports.The static analysis tool suggests using
import typefor 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: Useimport typefor type-only imports.Importing the types with
import typeensures 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 ofAPI_KEY_SALTlooks good.The
API_KEY_SALTparameter 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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)
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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 }); | |
| } | |
| } |
There was a problem hiding this comment.
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
fetchPaginatedMessagesfunction 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.listfunction 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
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
zodis correctly implemented.apps/web/app/api/user/group/[groupId]/messages/controller.ts (3)
114-121: LGTM!The function
createGroupItemsHashis correctly implemented.
206-241: LGTM!The function
fetchGroupMessagesis correctly implemented.Tools
Biome
[error] 226-226: Forbidden non-null assertion.
(lint/style/noNonNullAssertion)
243-269: LGTM!The function
buildQueryis correctly implemented.
| const groupId = params.groupId; | ||
| if (!groupId) return NextResponse.json({ error: "Missing group id" }); |
There was a problem hiding this comment.
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.
| 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 }); | |
| } |
| export const GET = withError(async (_request, { params }) => { | ||
| const session = await auth(); | ||
| if (!session?.user.email) | ||
| return NextResponse.json({ error: "Not authenticated" }); |
There was a problem hiding this comment.
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.
| 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 }); | |
| } |
| const gmail = getGmailClient(session); | ||
|
|
||
| const { messages } = await getGroupEmails({ | ||
| groupId, | ||
| userId: session.user.id, | ||
| gmail, | ||
| from: undefined, | ||
| to: undefined, | ||
| pageToken: "", | ||
| }); | ||
|
|
||
| return NextResponse.json({ messages }); |
There was a problem hiding this comment.
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.
| 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 }); | |
| } |
| } else { | ||
| paginationState.chunkIndex++; | ||
| paginationState.pageToken = undefined; | ||
| } |
There was a problem hiding this comment.
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.
| } 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)
| 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 }; |
There was a problem hiding this comment.
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.
| 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"); | |
| } | |
| } |
| 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"; |
There was a problem hiding this comment.
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.
| 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)
| 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 }; |
There was a problem hiding this comment.
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.
| 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"); | |
| } | |
| } |
| 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 }; | ||
| } |
There was a problem hiding this comment.
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.
| 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)
There was a problem hiding this comment.
Actionable comments posted: 6
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Files ignored due to path filters (1)
pnpm-lock.yamlis 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
groupEmailsQuerySchemadefines 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
groupEmailsResponseSchemadefines the structure of the response object. Ensure all required fields are included and correctly typed. Specifically, verify theattachmentsandinlinefields, which are defined as arrays of empty objects.
38-38: LGTM!The type alias
GroupEmailsResultcorrectly infers the type fromgroupEmailsResponseSchema.apps/web/app/api/v1/openapi/route.ts (5)
9-11: Ensure schemas are correctly imported.The
groupEmailsQuerySchemaandgroupEmailsResponseSchemaare imported from the validation file. Ensure these schemas are correctly defined and used in the OpenAPI registry.
13-13: LGTM!The
extendZodWithOpenApifunction 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}/emailsendpoint. Ensure the request parameters, query parameters, and response schema are correctly defined and referenced.
44-71: LGTM!The
GETfunction 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-openapienhances 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-configreinstates the ESLint configuration package, which is essential for maintaining code quality and consistency during development.
| import { GroupItemType } from "@prisma/client"; | ||
| import { z } from "zod"; |
There was a problem hiding this comment.
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.
| import { GroupItemType } from "@prisma/client"; | |
| import { z } from "zod"; | |
| import type { GroupItemType } from "@prisma/client"; | |
| import { z } from "zod"; |
| import { type NextRequest, NextResponse } from "next/server"; | ||
| import { z } from "zod"; |
There was a problem hiding this comment.
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.
| import { type NextRequest, NextResponse } from "next/server"; | |
| import { z } from "zod"; | |
| import type { NextRequest, NextResponse } from "next/server"; | |
| import { z } from "zod"; |
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } | |
| } |
| import { hashApiKey } from "@/utils/api-key"; | ||
| import { | ||
| groupEmailsQuerySchema, | ||
| GroupEmailsResult, | ||
| } from "@/app/api/v1/group/[groupId]/emails/validation"; |
There was a problem hiding this comment.
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)
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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 }); | |
| } | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 0
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
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
An API to access your Inbox Zero account
Summary by CodeRabbit
New Features
Documentation
Tests