Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ Add series to link related articles #1146

Closed
wants to merge 4 commits into from

Conversation

RangerCreaky
Copy link
Contributor

✨ Codu Pull Request 💻

Fixes #1081

Pull Request details

  • Added an extra input field in the more options section (While creating a new post) under the Canonical Url and Draft link
  • Added a new table in the database called "series"
  • Added a new field in the post table called seriesId referencing the id of the series table

Cases considered

  • CREATE
    When the user creates a new record will be created in the series table

  • EDIT

  1. If the user adds a series name to a post which didn't have a series then a new record will be created
  2. If the user changes the series name to a new series name (which is not present in the db), then a new record will be created in the series table. (The previous series will be removed from the series table, if no other post has that series name to maintain consistency).
  3. If the user removes the series name then the seriesId in the post table will be set to null. (The previous series will be removed from the series table, if no other post has that series name to maintain consistency)
  • DELETE
    If the user deletes the post which had a series name then the series will be removed from the series table, if no other post has that series.

Any Breaking changes

None

Note to Reviewers

Please let me know,

  • If there is any case missed
  • any change in coding style required
  • any error case not handled
  • any other change required

Associated Screenshots

UI
image

Working video
series codu

@RangerCreaky RangerCreaky requested a review from a team as a code owner October 18, 2024 03:48
Copy link

vercel bot commented Oct 18, 2024

@RangerCreaky is attempting to deploy a commit to the Codú Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

coderabbitai bot commented Oct 18, 2024

Walkthrough

The pull request introduces a "Series" feature to the application, allowing users to link related articles. It adds a new input field for seriesName in the Create component and modifies the database schema to include a new "Series" table. The SQL changes establish relationships between posts and series. The API is updated with a new router for series management and includes methods for updating series information. Validation schemas are also updated to accommodate the new series functionality, enhancing the overall content organization in the application.

Changes

File Path Change Summary
app/(app)/create/[[...paramsArr]]/_client.tsx Added seriesUpdate mutation, updated getFormData to include seriesName, and added input field for seriesName.
drizzle/0011_add_series_update_post.sql Created "Series" table and modified "Post" table to include seriesId with a foreign key constraint.
schema/post.ts Added optional seriesName field to SavePostSchema and ConfirmPostSchema.
schema/series.ts Introduced UpdateSeriesSchema for validating series updates.
server/api/router/index.ts Added seriesRouter to appRouter for series management.
server/api/router/post.ts Updated postRouter to handle series-related logic in delete and editDraft mutations.
server/api/router/series.ts Introduced seriesRouter with an update method for managing series updates.
server/db/schema.ts Defined new series table and updated post table to include seriesId with appropriate relations.

Assessment against linked issues

Objective Addressed Explanation
Allow users to group related articles into a series.
Add a series field to the article creation form and database.
Automatic Navigation for articles in a series. Navigation links are not implemented in this PR.
Show indication of the article’s place within the series. This feature is not addressed in the changes.
Articles index in the series should be chronological. Implementation details for chronological indexing are unclear.

Possibly related PRs

Suggested labels

hacktoberfest-accepted, hacktoberfest

Poem

In a world of posts, a series takes flight,
Linking tales together, oh what a sight!
With names to remember, and paths to explore,
Each article shines, like never before.
Hop along, dear reader, through stories entwined,
A journey of knowledge, beautifully aligned! 🐇✨


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>, please review it.
    • 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 gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @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 using 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.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

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: 8

🧹 Outside diff range and nitpick comments (11)
schema/series.ts (1)

3-6: LGTM: UpdateSeriesSchema is well-defined, with minor suggestions.

The UpdateSeriesSchema correctly defines the structure for updating series information, aligning with the PR objectives. The optional seriesName allows for removing a post from a series, and trimming prevents whitespace issues.

Consider the following suggestions for potential improvements:

  1. Add a minimum length constraint to postId to ensure it's not empty.
  2. Consider adding a maximum length constraint to seriesName to prevent excessively long series names.
  3. You might want to add a regex pattern to postId if it follows a specific format.

Example implementation:

export const UpdateSeriesSchema = z.object({
    postId: z.string().min(1, "Post ID cannot be empty"),
    seriesName: z.string().trim().max(100, "Series name cannot exceed 100 characters").optional()
});
drizzle/0011_add_series_update_post.sql (3)

1-9: LGTM! Consider adding a trigger for "updatedAt".

The "Series" table structure is well-designed and appropriate for the feature. Good use of constraints and data types.

Consider adding a trigger to automatically update the "updatedAt" column when a row is modified. This ensures the column is always up-to-date without relying on application logic. Here's an example:

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
   NEW."updatedAt" = NOW();
   RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_series_updated_at
BEFORE UPDATE ON "Series"
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

10-16: LGTM! Consider adding an index on "seriesId".

The alteration to the "Post" table is well-structured and correctly implements the relationship between posts and series.

Consider adding an index on the "seriesId" column to improve query performance when filtering or joining posts by series. Here's an example:

CREATE INDEX idx_post_series_id ON "Post" ("seriesId");

This index will be particularly useful if you frequently query posts by their series or join the "Post" and "Series" tables.


1-16: Overall implementation aligns well with PR objectives.

The SQL script successfully implements the database schema changes required for the series feature. It creates a new "Series" table and modifies the "Post" table to establish the relationship between posts and series. This implementation supports the CREATE, EDIT, and DELETE cases described in the PR objectives.

The changes align with the requirements outlined in the linked issue #1081, providing the necessary database structure to:

  1. Allow specifying a series for articles
  2. Enable navigation between related articles
  3. Support indexing articles within a series

As the feature develops, consider the following:

  1. Implement appropriate indexing strategies as the data grows.
  2. Ensure that application logic handles the ordering of articles within a series, possibly by adding an "order" column to the "Post" table in the future.
  3. Plan for efficient querying of series and their associated posts to support the navigation features described in the issue.
server/api/router/index.ts (1)

Line range hint 11-21: Summary: Series feature successfully integrated into the main router.

The changes in this file effectively integrate the new series feature into the application's routing system. The addition of the seriesRouter import and its inclusion in the appRouter are done consistently with other features, minimizing the risk of integration issues. These changes provide the necessary foundation for the series functionality to be accessible throughout the application, aligning well with the PR objectives.

A few points to consider:

  1. Ensure that the series.ts file is properly implemented and tested.
  2. Verify that any necessary middleware or authentication for the series routes are correctly set up in the seriesRouter.
  3. Update any relevant documentation to reflect the new series feature and its routing.
schema/post.ts (2)

28-28: LGTM! Consider adding a maximum length constraint.

The addition of the seriesName field to SavePostSchema is well-implemented and aligns with the PR objectives. It's correctly defined as an optional trimmed string.

Consider adding a maximum length constraint to prevent excessively long series names:

-  seriesName: z.string().trim().optional()
+  seriesName: z.string().trim().max(100, "Max series name length is 100 characters.").optional()

54-54: LGTM! Consider adding a maximum length constraint.

The addition of the seriesName field to ConfirmPostSchema is consistent with the change in SavePostSchema and appropriate for the post confirmation process.

For consistency with the suggested improvement in SavePostSchema, consider adding a maximum length constraint:

-  seriesName: z.string().trim().optional()
+  seriesName: z.string().trim().max(100, "Max series name length is 100 characters.").optional()
server/api/router/series.ts (1)

54-58: Include createdAt Field When Inserting a New Series

When creating a new series, setting the createdAt timestamp ensures accurate record-keeping and consistency.

Apply this diff to add the createdAt field:

 await tx.insert(series).values({
   title: seriesTitle, 
   userId: ctx.session.user.id,
+  createdAt: new Date(),
   updatedAt: new Date()  
 }).returning();
server/api/router/post.ts (1)

190-210: Rename inner 'deletedPost' variable to avoid shadowing and improve clarity

Inside the transaction, the variable deletedPost is redeclared. This shadows the outer deletedPost variable and can lead to confusion. Consider renaming the inner variable to something like deletedPostData to enhance readability.

server/db/schema.ts (1)

40-42: Add a unique constraint on 'title' per user

To prevent users from creating multiple series with the same title, consider adding a unique index on (userId, title). This ensures each user has uniquely titled series, enhancing data integrity.

Apply this change:

 export const series = pgTable("Series", {
   id: serial("id").primaryKey(),
   title: text("title").notNull(),
   description: text("description"),
   userId: text("userId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }),
+}, (table) => {
+  return {
+    userTitleUniqueIndex: uniqueIndex("Series_userId_title_unique").on(table.userId, table.title),
+  };
+});
app/(app)/create/[[...paramsArr]]/_client.tsx (1)

581-586: Add validation for series name input

To prevent users from entering series names that are too long or contain invalid characters, consider adding validation to the seriesName input field.

Apply this diff to add a maximum length attribute and basic pattern validation:

           <input
             id="seriesName"
             type="text"
             placeholder="The name of my series"
+            maxLength={100}
+            pattern="^[\w\s\-]+$"
             defaultValue={data?.series?.title || ""}
             {...register("seriesName")}
           />

This adds a maximum length of 100 characters and restricts input to letters, numbers, spaces, underscores, and hyphens.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 48708f0 and ab67084.

📒 Files selected for processing (8)
  • app/(app)/create/[[...paramsArr]]/_client.tsx (4 hunks)
  • drizzle/0011_add_series_update_post.sql (1 hunks)
  • schema/post.ts (2 hunks)
  • schema/series.ts (1 hunks)
  • server/api/router/index.ts (2 hunks)
  • server/api/router/post.ts (3 hunks)
  • server/api/router/series.ts (1 hunks)
  • server/db/schema.ts (4 hunks)
🧰 Additional context used
🔇 Additional comments (11)
schema/series.ts (2)

1-1: LGTM: Zod import is correct.

The import statement correctly imports the Zod library, which is necessary for defining the schema in this file.


1-6: Overall assessment: Well-implemented schema for series updates.

The schema/series.ts file successfully introduces the UpdateSeriesSchema, which aligns well with the PR objectives for implementing a "Series" feature. The schema correctly handles the core requirements:

  1. It allows updating a post's series information.
  2. It supports removing a post from a series (by making seriesName optional).
  3. It prevents whitespace issues in series names.

The implementation provides a solid foundation for validating series update operations. Consider the minor suggestions provided earlier to further enhance the robustness of the schema.

server/api/router/index.ts (2)

21-21: LGTM! Series router successfully integrated.

The seriesRouter is correctly added to the appRouter object, following the established pattern in this file. This integration makes the new series functionality available through the main application router, which aligns with the PR objectives.


11-11: LGTM! Verify the existence of the series.ts file.

The import statement for seriesRouter is correctly added and follows the established pattern in this file. This change aligns with the PR objectives of implementing a new series feature.

To ensure the series.ts file exists, run the following script:

✅ Verification successful

Verification Successful!

The series.ts file exists in the server/api/router/ directory as required. The import statement for seriesRouter is correct and aligns with the PR objectives of implementing the new series feature.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence of the series.ts file in the same directory

# Test: Check if the series.ts file exists
if fd -p "server/api/router/series.ts"; then
    echo "series.ts file found"
else
    echo "series.ts file not found"
fi

Length of output: 114

schema/post.ts (1)

28-28: Verify the impact of seriesName addition on related components and API endpoints.

The addition of the seriesName field to SavePostSchema and ConfirmPostSchema is well-implemented and consistent. To ensure full integration of this new feature:

Please run the following script to check for any components or API endpoints that might need updating to handle the new seriesName field:

Also applies to: 54-54

server/api/router/series.ts (1)

1-129: Overall Code Structure and Implementation

The implementation of the seriesRouter and the update method effectively handles series management for posts. The use of transactions ensures data integrity during database operations.

server/api/router/post.ts (3)

17-17: Importing 'series' model is necessary for series functionality

The addition of series to the import statement ensures that the series model is available in this file, enabling series-related operations.


448-448: Including 'series' in the query fetches associated series data

Adding series: true in the with clause of the editDraft query ensures that the series data associated with the post is fetched. This is essential for editing drafts that are part of a series.


190-210: ⚠️ Potential issue

Ensure correct usage of transaction methods

Within the transaction, verify that using tx.query.post.findFirst is appropriate according to your ORM's API. Depending on the ORM, the transaction object tx might not have a query property, and you may need to use tx.select, tx.query, or another method.

To confirm the correct methods available on the transaction object, run the following script:

server/db/schema.ts (2)

172-172: Verify 'onDelete' behavior for 'seriesId' foreign key in 'post' table

The foreign key for seriesId in the post table uses onDelete: "set null". This means that when a series is deleted, the seriesId in associated posts will be set to null. Ensure this aligns with the desired application behavior. If you prefer posts to be deleted when their associated series is deleted, consider changing onDelete to "cascade".


298-304: LGTM!

The seriesRelations are correctly defined, establishing the appropriate relationships between series, posts, and user.

Comment on lines 89 to 90
await tx.delete(series).where(eq(series.id, seriesId));
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure Only the User's Series is Deleted

When deleting a series, the current code removes any series matching the seriesId, regardless of ownership. This could inadvertently delete another user's series with the same ID.

Modify the where clause to include userId, ensuring only the current user's series is deleted:

- await tx.delete(series).where(eq(series.id, seriesId));
+ await tx.delete(series).where(
+   and(
+     eq(series.id, seriesId),
+     eq(series.userId, ctx.session.user.id)
+   )
+ );
📝 Committable suggestion

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

Suggested change
await tx.delete(series).where(eq(series.id, seriesId));
}
await tx.delete(series).where(
and(
eq(series.id, seriesId),
eq(series.userId, ctx.session.user.id)
)
);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the seriesId is being picked from the currentPost , so this comparison is not necessary in my opinion. Had the comparison been done based on series title, then comparing the user id would have been important.

However, this check is alse added in the latest commit.

Copy link
Contributor

Choose a reason for hiding this comment

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

@RangerCreaky, thank you for addressing the issue and adding the check in your latest commit. I appreciate your attention to detail.


✏️ Learnings added
Learnt from: RangerCreaky
PR: codu-code/codu#1146
File: server/api/router/series.ts:0-0
Timestamp: 2024-10-18T04:13:45.764Z
Learning: In the `seriesRouter` in `server/api/router/series.ts`, when deleting a series using the `seriesId` from `currentPost`, additional `userId` checks may not be necessary because unauthorized access to `currentPost` is already prevented.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 80 to 84
where: (post, { eq, and, ne }) =>
and (
ne(post.id, currentPost.id),
eq(post.seriesId, currentPost.seriesId!)
)
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Incorrect Reference to currentPost.seriesId in unlinkSeries Function

In the unlinkSeries function, the condition uses currentPost.seriesId!, which may not align with the seriesId parameter passed to the function. This could lead to unexpected behavior if currentPost.seriesId has changed.

Apply this diff to use the seriesId parameter for accurate matching:

- eq(post.seriesId, currentPost.seriesId!)
+ eq(post.seriesId, seriesId)
📝 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
where: (post, { eq, and, ne }) =>
and (
ne(post.id, currentPost.id),
eq(post.seriesId, currentPost.seriesId!)
)
where: (post, { eq, and, ne }) =>
and (
ne(post.id, currentPost.id),
eq(post.seriesId, seriesId)
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in the latest commit

Copy link
Contributor

Choose a reason for hiding this comment

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

@RangerCreaky, thank you for fixing the issue! 😊

(_/)
(•_•)
/ 🐰


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines 46 to 51
const currSeries = await tx.query.series.findFirst({
columns: {
id: true
},
where: (series, { eq }) => eq(series.title, seriesTitle),
})
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent Cross-User Series Linking by Including userId in Series Query

Currently, when checking for an existing series, the query filters only by series.title. This could allow a user to link their post to a series owned by another user if the series titles match.

Apply this diff to include userId in the where clause to ensure the series belongs to the current user:

- where: (series, { eq }) => eq(series.title, seriesTitle),
+ where: (series, { eq, and }) => and(
+   eq(series.title, seriesTitle),
+   eq(series.userId, ctx.session.user.id)
+ ),
📝 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 currSeries = await tx.query.series.findFirst({
columns: {
id: true
},
where: (series, { eq }) => eq(series.title, seriesTitle),
})
const currSeries = await tx.query.series.findFirst({
columns: {
id: true
},
where: (series, { eq, and }) => and(
eq(series.title, seriesTitle),
eq(series.userId, ctx.session.user.id)
),
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in the latest commit

Copy link
Contributor

Choose a reason for hiding this comment

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

@RangerCreaky, glad to hear that the issue has been resolved! 🎉

(^o^/)
(='.'=)
(")_(")


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Comment on lines +101 to +127
if(seriesName){
// check if the current post is already linked to a series
if(currentPost?.seriesId){
// check if the series title is same as the current series name
// then we do nothing
if(currentPost?.series?.title !== seriesName){
// then the user has updated the series name in this particular edit
// Check if there is another post with the same title, else delete the series
// and create a new post with the new series name
// and update that new series id in the post
await unlinkSeries(currentPost.seriesId);
await createNewSeries(seriesName);
}
}
else{
// the current post is not yet linked to a seriesId
// so create a new series and put that Id in the post
await createNewSeries(seriesName);
}
}
else{
// either the user has not added the series Name (We do nothing)
// or while editing the post, the user has removed the series name
if(currentPost.seriesId !== null){
await unlinkSeries(currentPost.seriesId);
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify Conditional Logic for Better Readability

The nested if-else statements can be refactored for clarity and maintainability. Simplifying the conditions will make the code easier to understand and reduce potential errors.

Consider restructuring the logic as follows:

 if (seriesName) {
   if (currentPost.seriesId) {
     if (currentPost.series.title !== seriesName) {
       await unlinkSeries(currentPost.seriesId);
       await createNewSeries(seriesName);
     }
     // No action needed if titles match
   } else {
     await createNewSeries(seriesName);
   }
 } else if (currentPost.seriesId !== null) {
   await unlinkSeries(currentPost.seriesId);
 }
 // No action needed if seriesName is not provided and no seriesId is linked
📝 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
if(seriesName){
// check if the current post is already linked to a series
if(currentPost?.seriesId){
// check if the series title is same as the current series name
// then we do nothing
if(currentPost?.series?.title !== seriesName){
// then the user has updated the series name in this particular edit
// Check if there is another post with the same title, else delete the series
// and create a new post with the new series name
// and update that new series id in the post
await unlinkSeries(currentPost.seriesId);
await createNewSeries(seriesName);
}
}
else{
// the current post is not yet linked to a seriesId
// so create a new series and put that Id in the post
await createNewSeries(seriesName);
}
}
else{
// either the user has not added the series Name (We do nothing)
// or while editing the post, the user has removed the series name
if(currentPost.seriesId !== null){
await unlinkSeries(currentPost.seriesId);
}
}
if (seriesName) {
if (currentPost.seriesId) {
if (currentPost.series.title !== seriesName) {
await unlinkSeries(currentPost.seriesId);
await createNewSeries(seriesName);
}
// No action needed if titles match
} else {
await createNewSeries(seriesName);
}
} else if (currentPost.seriesId !== null) {
await unlinkSeries(currentPost.seriesId);
}
// No action needed if seriesName is not provided and no seriesId is linked

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The changes suggested are to remove the comments
However, keeping it for now, for the ease of the reviewers

Copy link
Contributor

Choose a reason for hiding this comment

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

@RangerCreaky, understood. Keeping the comments for the ease of reviewers makes sense.

(_/)
(•︵•)
('')('')


✏️ Learnings added
Learnt from: RangerCreaky
PR: codu-code/codu#1146
File: server/api/router/series.ts:109-135
Timestamp: 2024-10-18T04:18:28.426Z
Learning: In the TypeScript file `server/api/router/series.ts`, when comments are included to aid reviewers, avoid suggesting their removal.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +16 to +31
const currentPost = await ctx.db.query.post.findFirst({
columns: {
id: true,
seriesId: true,
userId: true
},
with: {
series: {
columns: {
id: true,
title: true
},
},
},
where: (post, { eq }) => eq(post.id, postId),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Enhance postId Validation with Specific Constraints

Currently, postId is validated as a string. To further mitigate security risks, please implement more specific validation rules (e.g., ensure postId follows a UUID format if applicable).

🔗 Analysis chain

Validate postId and Handle Potential Security Risks

While fetching currentPost, ensure that postId is validated to prevent SQL injection or unauthorized access. Although TRPC and the ORM handle parameterization, additional validation enhances security.

Run the following script to check the validation of postId:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that `UpdateSeriesSchema` properly validates `postId`

# Test: Check the schema definition for `postId` validation
rg --type typescript "UpdateSeriesSchema"

Length of output: 76


Script:

#!/bin/bash
# Description: Verify that `UpdateSeriesSchema` properly validates `postId`

# Test: Check the schema definition for `postId` validation without specifying file type
rg "UpdateSeriesSchema"

# Alternatively, restrict the search to TypeScript files using a glob pattern
rg "UpdateSeriesSchema" --glob "*.ts" --glob "*.tsx"

Length of output: 490


Script:

#!/bin/bash
# Description: Extract the definition of `UpdateSeriesSchema` to verify `postId` validation

# Extract lines surrounding the `UpdateSeriesSchema` definition in schema/series.ts
rg "export const UpdateSeriesSchema" schema/series.ts -A 10 -B 5

Length of output: 206

Comment on lines +50 to +55
updatedAt: timestamp("updatedAt", {
precision: 3,
withTimezone: true
}).notNull()
.$onUpdate(() => new Date())
.default(sql`CURRENT_TIMESTAMP`),
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure consistency in 'updatedAt' timestamp configuration

In the series table, the updatedAt field is missing the mode: "string" option and uses .$onUpdate(() => new Date()), whereas other tables like post use mode: "string" and .$onUpdate(() => new Date().toISOString()). For consistency and to prevent potential type mismatches, consider adding mode: "string" and using .$onUpdate(() => new Date().toISOString()).

Apply this diff to ensure consistency:

 updatedAt: timestamp("updatedAt", {
   precision: 3,
+  mode: "string",
   withTimezone: true
 }).notNull()
-  .$onUpdate(() => new Date())
+  .$onUpdate(() => new Date().toISOString())
   .default(sql`CURRENT_TIMESTAMP`),
📝 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
updatedAt: timestamp("updatedAt", {
precision: 3,
withTimezone: true
}).notNull()
.$onUpdate(() => new Date())
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: timestamp("updatedAt", {
precision: 3,
mode: "string",
withTimezone: true
}).notNull()
.$onUpdate(() => new Date().toISOString())
.default(sql`CURRENT_TIMESTAMP`),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keeping it as in the date time format, as mentioned in the issue description

Copy link
Contributor

Choose a reason for hiding this comment

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

@RangerCreaky, understood. We'll keep the datetime format as per the issue description.

(_/)
( •_•)
/


✏️ Learnings added
Learnt from: RangerCreaky
PR: codu-code/codu#1146
File: server/db/schema.ts:50-55
Timestamp: 2024-10-18T04:16:22.358Z
Learning: In `server/db/schema.ts`, for the `series` table, the `updatedAt` field uses `.$onUpdate(() => new Date())` without `mode: "string"`, as per the issue description. This is intentional, and we should not suggest changing it for consistency with other tables.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +237 to +260
let saveSuccess = false;
try {
await save({ ...formData, id: postId });
saveSuccess = true;
} catch (error) {
toast.error("Error saving post.");
Sentry.captureException(error);
}

let seriesUpdateSuccess = false;
try {
if(formData?.seriesName){
await seriesUpdate({ postId, seriesName: formData.seriesName });
}
seriesUpdateSuccess = true;
} catch (error) {
toast.error("Error updating series.");
Sentry.captureException(error);
}

if(saveSuccess && seriesUpdateSuccess){
toast.success("Saved");
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix logical error in series update success handling

There is a logical issue in the way seriesUpdateSuccess is initialized and evaluated. If formData.seriesName is undefined, the seriesUpdate function is not called, but seriesUpdateSuccess is still set to true. This may prevent the success toast from displaying even when the post is saved successfully.

To ensure that the success toast appears appropriately, initialize seriesUpdateSuccess to true before the try block, and only set it to false if an error occurs during the series update. This way, if seriesUpdate is not called due to the absence of seriesName, seriesUpdateSuccess remains true.

Apply this diff to fix the issue:

-          let seriesUpdateSuccess = false;
+          let seriesUpdateSuccess = true;
           try {
             if(formData?.seriesName){
               await seriesUpdate({ postId, seriesName: formData.seriesName });
             }
-            seriesUpdateSuccess = true;
           } catch (error) {
             toast.error("Error updating series.");
             Sentry.captureException(error);
+            seriesUpdateSuccess = false;
           }
📝 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
let saveSuccess = false;
try {
await save({ ...formData, id: postId });
saveSuccess = true;
} catch (error) {
toast.error("Error saving post.");
Sentry.captureException(error);
}
let seriesUpdateSuccess = false;
try {
if(formData?.seriesName){
await seriesUpdate({ postId, seriesName: formData.seriesName });
}
seriesUpdateSuccess = true;
} catch (error) {
toast.error("Error updating series.");
Sentry.captureException(error);
}
if(saveSuccess && seriesUpdateSuccess){
toast.success("Saved");
}
let saveSuccess = false;
try {
await save({ ...formData, id: postId });
saveSuccess = true;
} catch (error) {
toast.error("Error saving post.");
Sentry.captureException(error);
}
let seriesUpdateSuccess = true;
try {
if(formData?.seriesName){
await seriesUpdate({ postId, seriesName: formData.seriesName });
}
} catch (error) {
toast.error("Error updating series.");
Sentry.captureException(error);
seriesUpdateSuccess = false;
}
if(saveSuccess && seriesUpdateSuccess){
toast.success("Saved");
}

Comment on lines +572 to +589
<p className="mt-2 mb-2 text-sm text-neutral-600 dark:text-neutral-400">
Share this link with others to preview your
draft. Anyone with the link can view your draft.
</p>

<label htmlFor="seriesName">
Series Name
</label>
<input
id="seriesName"
type="text"
placeholder="The name of my series"
defaultValue={data?.series?.title || ""}
{...register("seriesName")}
/>
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
This text is case-sensitive so make sure you type it exactly as you did in previous articles to ensure they are connected
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Normalize series names to be case-insensitive

The series name field is currently case-sensitive, which may lead to user errors if the series name is not typed exactly the same in each article. This could result in related articles not being linked correctly.

Consider normalizing series names by converting them to lowercase (or a consistent case) when storing and comparing them in the database. This will enhance the user experience by ensuring that articles are correctly linked in a series, regardless of the case used when entering the series name.

Apply this diff to handle case normalization on the client side:

           const formData = {
             ...data,
             tags,
             canonicalUrl: data.canonicalUrl || undefined,
             excerpt: data.excerpt || removeMarkdown(data.body, {}).substring(0, 155),
-            seriesName: data.seriesName || undefined
+            seriesName: data.seriesName ? data.seriesName.toLowerCase() : undefined
           };

Ensure that the server-side logic also handles series names in a case-insensitive manner.

Committable suggestion was skipped due to low confidence.

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

🧹 Outside diff range and nitpick comments (2)
server/api/router/series.ts (2)

36-36: Remove unnecessary optional chaining

Since currentPost has been confirmed to exist earlier in the code, the optional chaining in currentPost?.userId is unnecessary. Access currentPost.userId directly for clarity and consistency.

Apply this diff:

- if (currentPost?.userId !== ctx.session.user.id) {
+ if (currentPost.userId !== ctx.session.user.id) {

57-61: Include 'createdAt' timestamp when creating a new series

In the createNewSeries function, only the updatedAt field is set when inserting a new series. If your series table includes a createdAt field, it's advisable to set it during insertion to maintain accurate timestamps.

Apply this diff:

 await tx.insert(series).values({
     title: seriesTitle, 
     userId: ctx.session.user.id,
+    createdAt: new Date(),
     updatedAt: new Date()  
 }).returning();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between ab67084 and b83902e.

📒 Files selected for processing (1)
  • server/api/router/series.ts (1 hunks)
🧰 Additional context used
🔇 Additional comments (1)
server/api/router/series.ts (1)

83-88: ⚠️ Potential issue

Ensure 'unlinkSeries' considers only the current user's posts

The query in the unlinkSeries function checks for other posts linked to the same series but does not filter by userId. This could lead to incorrect behavior if other users have posts linked to the same series. Include userId in the query to ensure that only the current user's posts are considered.

Apply this diff to modify the query:

 const anotherPostInThisSeries = await tx.query.post.findFirst({
     where: (post, { eq, and, ne }) => 
         and (
             ne(post.id, currentPost.id),
             eq(post.seriesId, seriesId),
+            eq(post.userId, ctx.session.user.id)
         )
 })
⛔ Skipped due to learnings
Learnt from: RangerCreaky
PR: codu-code/codu#1146
File: server/api/router/series.ts:0-0
Timestamp: 2024-10-18T04:13:45.950Z
Learning: In the `seriesRouter` in `server/api/router/series.ts`, when deleting a series using the `seriesId` from `currentPost`, additional `userId` checks may not be necessary because unauthorized access to `currentPost` is already prevented.

@RangerCreaky
Copy link
Contributor Author

@NiallJoeMaher please review and let me know the changes I need to make

@NiallJoeMaher
Copy link
Contributor

I'm out of the office at the moment so will try get to this soon. Need to do some testing on it. 🦾

@RangerCreaky
Copy link
Contributor Author

Sure, I am up for it

@@ -25,6 +25,7 @@ export const SavePostSchema = z.object({
canonicalUrl: z.optional(z.string().trim().url()),
tags: z.string().array().max(5).optional(),
published: z.string().datetime().optional(),
seriesName: z.string().trim().optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be seriesId ?

In the drizzle migration you added a FK constraint for seriesId but I only see seriesName on the post schema

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @JohnAllenTech , Sorry I was away for the past 2 days, so couldn't reply

You are right, the foreign key is seriesId. However, the as of my knowledge, this schema (savePostSchema) is the input for the update Procedure in the post router

` update: protectedProcedure
.input(SavePostSchema)
.mutation(async ({ input, ctx }) => {
const { id, body, title, excerpt, canonicalUrl, tags = [] } = input;

  const currentPost = await ctx.db.query.post.findFirst({
    where: (posts, { eq }) => eq(posts.id, id),
  });

`
This method takes the seriesName as input, and creates a new record in the series. and updates the seriesId in the posts table.

The code below has the schema for the post table which has the seriesId
(db/schema.ts)
.references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }), showComments: boolean("showComments").default(true).notNull(), likes: integer("likes").default(0).notNull(), seriesId: integer("seriesId").references(() => series.id, { onDelete: "set null", onUpdate: "cascade" }),

Copy link
Contributor

Choose a reason for hiding this comment

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

I also had the same question as @JohnAllenTech since the series name may not be unique and we could have collisions while trying to find the relevant series. I think this needs to be seriesId.

@RangerCreaky
Copy link
Contributor Author

@NiallJoeMaher @JohnAllenTech any update on this?

Copy link
Contributor

@NiallJoeMaher NiallJoeMaher left a comment

Choose a reason for hiding this comment

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

A few comments

@@ -25,6 +25,7 @@ export const SavePostSchema = z.object({
canonicalUrl: z.optional(z.string().trim().url()),
tags: z.string().array().max(5).optional(),
published: z.string().datetime().optional(),
seriesName: z.string().trim().optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

I also had the same question as @JohnAllenTech since the series name may not be unique and we could have collisions while trying to find the relevant series. I think this needs to be seriesId.

-- Create Series table
CREATE TABLE IF NOT EXISTS "Series" (
"id" SERIAL PRIMARY KEY,
"title" TEXT NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we change this to "name"


export const UpdateSeriesSchema = z.object({
postId: z.string(),
seriesName: z.string().trim().optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't unique so could cause issues when we join.

@@ -50,6 +51,7 @@ export const ConfirmPostSchema = z.object({
.optional(),
canonicalUrl: z.string().trim().url().optional(),
tags: z.string().array().max(5).optional(),
seriesName: z.string().trim().optional()
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, seriesId

CREATE TABLE IF NOT EXISTS "Series" (
"id" SERIAL PRIMARY KEY,
"title" TEXT NOT NULL,
"description" TEXT,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is used yet so we can delete it for the moment until we have a UI to update this

"id" SERIAL PRIMARY KEY,
"title" TEXT NOT NULL,
"description" TEXT,
"userId" TEXT NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be inferred from the post itself

@NiallJoeMaher
Copy link
Contributor

Let me know if this is still WIP so I can pick it up if you don't have the time.

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.

Feature/ Add "series" to link related articles
3 participants