Chapter 6 - Adding Comments to Schema - Complete

This commit is contained in:
Colin Diem
2022-04-26 01:24:39 -04:00
parent e180c93ada
commit ac15c86840
15 changed files with 307 additions and 11 deletions

View File

@@ -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
);

View File

@@ -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())
}

View File

@@ -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
}
`

View File

@@ -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 },
})
}

View File

@@ -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",
},
},
},
})

View File

@@ -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)
})
})

View File

@@ -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 }) => {
<div className="mt-2 text-gray-900 font-light">
{summary ? truncate(article.body, 100) : article.body}
</div>
{!summary && (
<div className="mt-12">
<CommentsCell />
</div>
)}
</article>
)
}

View File

@@ -10,7 +10,7 @@ export const full = () => {
return <Article article={ARTICLE} />
}
export const summart = () => {
export const summary = () => {
return <Article article={ARTICLE} summary={true} />
}

View File

@@ -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(<Article article={ARTICLE} />)
await waitFor(() =>
expect(screen.getByText(comment.body)).toBeInTheDocument()
)
})
it('renders a summary of a blog post', () => {
render(<Article article={ARTICLE} summary={true} />)
@@ -27,4 +37,13 @@ describe('Article', () => {
)
).toBeInTheDocument()
})
it('does not render comments when displaying a summary', async () => {
const comment = standard().comments[0]
render(<Article article={ARTICLE} summary={true} />)
await waitFor(() =>
expect(screen.queryByText(comment.body)).not.toBeInTheDocument()
)
})
})

View File

@@ -10,7 +10,7 @@ const Comment = ({ comment }) => {
<header className="flex justify-between">
<h2 className="font-semibold text-gray-700">{comment.name}</h2>
<time className="text-xs text-gray-500" dateTime={comment.createdAt}>
{comment.createdAt}
{formattedDate(comment.createdAt)}
</time>
</header>
<p className="text-sm mt-2">{comment.body}</p>

View File

@@ -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(<Comment />)
}).not.toThrow()
const comment = {
name: 'John Doe',
body: 'This is my comment',
createdAt: '2020-01-02T12:34:56Z',
}
render(<Comment comment={comment} />)
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)
})
})

View File

@@ -0,0 +1,32 @@
import Comment from 'src/components/Comment'
export const QUERY = gql`
query CommentsQuery {
comments {
id
name
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => (
<div className="text-center text-gray-500">No comments yet</div>
)
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ comments }) => {
return (
<div className="space-y-8">
{comments.map((comment) => (
// eslint-disable-next-line prettier/prettier
<Comment key={comment.id} comment={comment} />
))}
</div>
)
}

View File

@@ -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',
},
],
})

View File

@@ -0,0 +1,20 @@
import { Loading, Empty, Failure, Success } from './CommentsCell'
import { standard } from './CommentsCell.mock'
export const loading = () => {
return Loading ? <Loading /> : null
}
export const empty = () => {
return Empty ? <Empty /> : null
}
export const failure = () => {
return Failure ? <Failure error={new Error('Oh no')} /> : null
}
export const success = () => {
return Success ? <Success {...standard()} /> : null
}
export default { title: 'Cells/CommentsCell' }

View File

@@ -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(<Loading />)
}).not.toThrow()
})
it('renders Empty successfully', async () => {
render(<Empty />)
expect(screen.getByText('No comments yet')).toBeInTheDocument()
})
it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).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(<Success comments={comments} />)
comments.forEach((comment) => {
expect(screen.getByText(comment.body)).toBeInTheDocument()
})
})
})