Chapter 6 - Adding Comments to Schema - Complete
This commit is contained in:
@@ -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
|
||||
);
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
31
api/src/graphql/comments.sdl.js
Normal file
31
api/src/graphql/comments.sdl.js
Normal 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
|
||||
}
|
||||
`
|
||||
28
api/src/services/comments/comments.js
Normal file
28
api/src/services/comments/comments.js
Normal 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 },
|
||||
})
|
||||
}
|
||||
42
api/src/services/comments/comments.scenarios.js
Normal file
42
api/src/services/comments/comments.scenarios.js
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
30
api/src/services/comments/comments.test.js
Normal file
30
api/src/services/comments/comments.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const full = () => {
|
||||
return <Article article={ARTICLE} />
|
||||
}
|
||||
|
||||
export const summart = () => {
|
||||
export const summary = () => {
|
||||
return <Article article={ARTICLE} summary={true} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
32
web/src/components/CommentsCell/CommentsCell.js
Normal file
32
web/src/components/CommentsCell/CommentsCell.js
Normal 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>
|
||||
)
|
||||
}
|
||||
17
web/src/components/CommentsCell/CommentsCell.mock.js
Normal file
17
web/src/components/CommentsCell/CommentsCell.mock.js
Normal 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',
|
||||
},
|
||||
],
|
||||
})
|
||||
20
web/src/components/CommentsCell/CommentsCell.stories.js
Normal file
20
web/src/components/CommentsCell/CommentsCell.stories.js
Normal 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' }
|
||||
43
web/src/components/CommentsCell/CommentsCell.test.js
Normal file
43
web/src/components/CommentsCell/CommentsCell.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user