diff --git a/api/db/migrations/20220426045928_added_comments/migration.sql b/api/db/migrations/20220426045928_added_comments/migration.sql new file mode 100644 index 0000000..93f7f07 --- /dev/null +++ b/api/db/migrations/20220426045928_added_comments/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Comment" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "body" TEXT NOT NULL, + "postId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 24e6494..3735bff 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -9,10 +9,11 @@ generator client { } model Post { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) title String body String - createdAt DateTime @default(now()) + comments Comment[] + createdAt DateTime @default(now()) } model Contact { @@ -24,11 +25,20 @@ model Contact { } model User { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String? - email String @unique + email String @unique hashedPassword String salt String resetToken String? resetTokenExpiresAt DateTime? } + +model Comment { + id Int @id @default(autoincrement()) + name String + body String + post Post @relation(fields: [postId], references: [id]) + postId Int + createdAt DateTime @default(now()) +} diff --git a/api/src/graphql/comments.sdl.js b/api/src/graphql/comments.sdl.js new file mode 100644 index 0000000..da485f8 --- /dev/null +++ b/api/src/graphql/comments.sdl.js @@ -0,0 +1,31 @@ +export const schema = gql` + type Comment { + id: Int! + name: String! + body: String! + post: Post! + postId: Int! + createdAt: DateTime! + } + + type Query { + comments: [Comment!]! @skipAuth + } + + input CreateCommentInput { + name: String! + body: String! + postId: Int! + } + + input UpdateCommentInput { + name: String + body: String + postId: Int + } + + type Mutation { + createComment(input: CreateCommentInput!): Comment! @skipAuth + deleteComment(id: Int!): Comment! @requireAuth + } +` diff --git a/api/src/services/comments/comments.js b/api/src/services/comments/comments.js new file mode 100644 index 0000000..c55034e --- /dev/null +++ b/api/src/services/comments/comments.js @@ -0,0 +1,28 @@ +import { db } from 'src/lib/db' + +export const comments = () => { + return db.comment.findMany() +} + +export const comment = ({ id }) => { + return db.comment.findUnique({ + where: { id }, + }) +} + +export const Comment = { + post: (_obj, { root }) => + db.comment.findUnique({ where: { id: root.id } }).post(), +} + +export const createComment = ({ input }) => { + return db.comment.create({ + data: input, + }) +} + +export const deleteComment = ({ id }) => { + return db.comment.delete({ + where: { id }, + }) +} diff --git a/api/src/services/comments/comments.scenarios.js b/api/src/services/comments/comments.scenarios.js new file mode 100644 index 0000000..2f15b38 --- /dev/null +++ b/api/src/services/comments/comments.scenarios.js @@ -0,0 +1,42 @@ +import { defineScenario } from '@redwoodjs/testing/dist/api' + +export const standard = defineScenario({ + comment: { + one: { + data: { + name: 'Jane Doe', + body: 'I like trees.', + post: { + create: { + title: 'Redwood Leaves', + body: 'The quick brown fox jumped over the lazy dog.', + }, + }, + }, + }, + + two: { + data: { + name: 'John Doe', + body: 'Hug a tree today', + post: { + create: { + title: 'Root Systems', + body: 'The five boxing wizards jump quickly', + }, + }, + }, + }, + }, +}) + +export const postOnly = defineScenario({ + post: { + bark: { + data: { + title: 'Bark', + body: "A tree's bark is worse than its bite", + }, + }, + }, +}) diff --git a/api/src/services/comments/comments.test.js b/api/src/services/comments/comments.test.js new file mode 100644 index 0000000..8bd5f0a --- /dev/null +++ b/api/src/services/comments/comments.test.js @@ -0,0 +1,30 @@ +import { comments, createComment } from './comments' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('comments', () => { + scenario('returns all comments', async (scenario) => { + const result = await comments() + + expect(result.length).toEqual(Object.keys(scenario.comment).length) + }) + + scenario('postOnly', 'creates a new comment', async (scenario) => { + const comment = await createComment({ + input: { + name: 'Billy Bob', + body: 'What is your favorite tree bark?', + postId: scenario.post.bark.id, + }, + }) + + expect(comment.name).toEqual('Billy Bob') + expect(comment.body).toEqual('What is your favorite tree bark?') + expect(comment.postId).toEqual(scenario.post.bark.id) + expect(comment.createdAt).not.toEqual(null) + }) +}) diff --git a/web/src/components/Article/Article.js b/web/src/components/Article/Article.js index 092f957..43e2c23 100644 --- a/web/src/components/Article/Article.js +++ b/web/src/components/Article/Article.js @@ -1,4 +1,5 @@ import { Link, routes } from '@redwoodjs/router' +import CommentsCell from 'src/components/CommentsCell' const truncate = (text, length) => { return text.substring(0, length) + '...' @@ -15,6 +16,11 @@ const Article = ({ article, summary = false }) => {
{summary ? truncate(article.body, 100) : article.body}
+ {!summary && ( +
+ +
+ )} ) } diff --git a/web/src/components/Article/Article.stories.js b/web/src/components/Article/Article.stories.js index 9bbd1e0..bcaad1f 100644 --- a/web/src/components/Article/Article.stories.js +++ b/web/src/components/Article/Article.stories.js @@ -10,7 +10,7 @@ export const full = () => { return
} -export const summart = () => { +export const summary = () => { return
} diff --git a/web/src/components/Article/Article.test.js b/web/src/components/Article/Article.test.js index 9f2f339..2a116d1 100644 --- a/web/src/components/Article/Article.test.js +++ b/web/src/components/Article/Article.test.js @@ -1,4 +1,5 @@ -import { render, screen } from '@redwoodjs/testing' +import { render, screen, waitFor } from '@redwoodjs/testing' +import { standard } from 'src/components/CommentsCell/CommentsCell.mock' import Article from './Article' @@ -17,6 +18,15 @@ describe('Article', () => { expect(screen.getByText(ARTICLE.body)).toBeInTheDocument() }) + it('renders comments when displaying a full blog post', async () => { + const comment = standard().comments[0] + render(
) + + await waitFor(() => + expect(screen.getByText(comment.body)).toBeInTheDocument() + ) + }) + it('renders a summary of a blog post', () => { render(
) @@ -27,4 +37,13 @@ describe('Article', () => { ) ).toBeInTheDocument() }) + + it('does not render comments when displaying a summary', async () => { + const comment = standard().comments[0] + render(
) + + await waitFor(() => + expect(screen.queryByText(comment.body)).not.toBeInTheDocument() + ) + }) }) diff --git a/web/src/components/Comment/Comment.js b/web/src/components/Comment/Comment.js index 3552dea..8e16827 100644 --- a/web/src/components/Comment/Comment.js +++ b/web/src/components/Comment/Comment.js @@ -10,7 +10,7 @@ const Comment = ({ comment }) => {

{comment.name}

{comment.body}

diff --git a/web/src/components/Comment/Comment.test.js b/web/src/components/Comment/Comment.test.js index a1bc6f8..f6069f4 100644 --- a/web/src/components/Comment/Comment.test.js +++ b/web/src/components/Comment/Comment.test.js @@ -1,4 +1,4 @@ -import { render } from '@redwoodjs/testing/web' +import { render, screen } from '@redwoodjs/testing/web' import Comment from './Comment' @@ -7,8 +7,17 @@ import Comment from './Comment' describe('Comment', () => { it('renders successfully', () => { - expect(() => { - render() - }).not.toThrow() + const comment = { + name: 'John Doe', + body: 'This is my comment', + createdAt: '2020-01-02T12:34:56Z', + } + render() + expect(screen.getByText(comment.name)).toBeInTheDocument() + expect(screen.getByText(comment.body)).toBeInTheDocument() + const dateExpect = screen.getByText('2 January 2020') + expect(dateExpect).toBeInTheDocument() + expect(dateExpect.nodeName).toEqual('TIME') + expect(dateExpect).toHaveAttribute('datetime', comment.createdAt) }) }) diff --git a/web/src/components/CommentsCell/CommentsCell.js b/web/src/components/CommentsCell/CommentsCell.js new file mode 100644 index 0000000..3ec236d --- /dev/null +++ b/web/src/components/CommentsCell/CommentsCell.js @@ -0,0 +1,32 @@ +import Comment from 'src/components/Comment' +export const QUERY = gql` + query CommentsQuery { + comments { + id + name + body + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => ( +
No comments yet
+) + +export const Failure = ({ error }) => ( +
Error: {error.message}
+) + +export const Success = ({ comments }) => { + return ( +
+ {comments.map((comment) => ( + // eslint-disable-next-line prettier/prettier + + ))} +
+ ) +} diff --git a/web/src/components/CommentsCell/CommentsCell.mock.js b/web/src/components/CommentsCell/CommentsCell.mock.js new file mode 100644 index 0000000..8f01494 --- /dev/null +++ b/web/src/components/CommentsCell/CommentsCell.mock.js @@ -0,0 +1,17 @@ +// Define your own mock data here: +export const standard = () => ({ + comments: [ + { + id: 1, + name: 'Rob Cameron', + body: 'First comment', + createdAt: '2020-01-02T12:34:56Z', + }, + { + id: 2, + name: 'David Price', + body: 'Second comment', + createdAt: '2020-02-03T23:00:00Z', + }, + ], +}) diff --git a/web/src/components/CommentsCell/CommentsCell.stories.js b/web/src/components/CommentsCell/CommentsCell.stories.js new file mode 100644 index 0000000..bcfe10a --- /dev/null +++ b/web/src/components/CommentsCell/CommentsCell.stories.js @@ -0,0 +1,20 @@ +import { Loading, Empty, Failure, Success } from './CommentsCell' +import { standard } from './CommentsCell.mock' + +export const loading = () => { + return Loading ? : null +} + +export const empty = () => { + return Empty ? : null +} + +export const failure = () => { + return Failure ? : null +} + +export const success = () => { + return Success ? : null +} + +export default { title: 'Cells/CommentsCell' } diff --git a/web/src/components/CommentsCell/CommentsCell.test.js b/web/src/components/CommentsCell/CommentsCell.test.js new file mode 100644 index 0000000..66db3e1 --- /dev/null +++ b/web/src/components/CommentsCell/CommentsCell.test.js @@ -0,0 +1,43 @@ +import { render, screen } from '@redwoodjs/testing/web' +import { Loading, Empty, Failure, Success } from './CommentsCell' +import { standard } from './CommentsCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('CommentsCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + render() + expect(screen.getByText('No comments yet')).toBeInTheDocument() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@redwoodjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + const comments = standard().comments + render() + + comments.forEach((comment) => { + expect(screen.getByText(comment.body)).toBeInTheDocument() + }) + }) +})