Skip to content
This repository has been archived by the owner on May 4, 2023. It is now read-only.

Commit

Permalink
feat: finish tutorial (#18)
Browse files Browse the repository at this point in the history
* chore: add some storybook

https://redwoodjs.com/docs/tutorial/chapter5/first-story

* fix: enable show code button

storybookjs/storybook#8104 (comment)

* fix: better layout

* chore: make boilerplate stories

TODO have framework generate these

* fix: account for missing id on netlify

> Error: Variable "$id" of required type "Int!" was not provided.

* chore: fixup some tests

https://redwoodjs.com/docs/tutorial/chapter5/first-test

* chore: add `Comment`

yarn rw g component Comment
https://redwoodjs.com/docs/tutorial/chapter6/the-redwood-way

* chore: add some boilerplate

* chore(Comment): improve and wire up some mock data

https://redwoodjs.com/docs/tutorial/chapter6/the-redwood-way#storybook

* chore: add styling

* chore: add CommentsCell

https://redwoodjs.com/docs/tutorial/chapter6/multiple-comments

* chore: add some boilerplate

* chore: add better mocks

* chore: update CommentsCell

* chore: add a bit more styling

* chore: add comments

TODO figure out loading part of this

* chore: fix

* chore: add Comments to the schema

https://redwoodjs.com/docs/tutorial/chapter6/comments-schema

* chore(prisma): generate Comment table migration

* chore(prisma): generate Comment table migration

* chore(ide): add recommended extension

* chore: remove unused routes in main app

* Revert "chore: remove unused routes in main app"

This reverts commit 53637f8.

* chore: disable contact/login forms

not used

* chore: remove unused import

* test(unit): add example async test

* test(unit): default summary to true

* chore: update empty comments cell

https://redwoodjs.com/docs/tutorial/chapter6/comments-schema

* chore(comments): allow/create

https://redwoodjs.com/docs/tutorial/chapter6/comments-schema

* chore(commentForm): run generator

`yarn rw g component CommentForm`
https://redwoodjs.com/docs/tutorial/chapter6/comment-form

* chore(commentForm): add some boilerplate

* chore(commentForm): simple form

* chore(commentForm): add submit

* chore(storybook): wire up mockGraphQLMutation

* chore(storybook): add interaction test

https://stackoverflow.com/a/63745654

* chore: fixup style

* test(unit): better test

* test: add loading snapshot

* chore(commentForm): use form

https://redwoodjs.com/docs/tutorial/chapter6/comment-form#adding-the-form-to-the-blog-post

* chore(commentForm): wire up correctly in Article

* chore(contactForm): refetch comments after create

https://redwoodjs.com/docs/tutorial/chapter6/comment-form#graphql-query-caching

* chore: add toast feedback for form

https://redwoodjs.com/docs/tutorial/chapter6/comment-form#graphql-query-caching

* chore: wire up comments to posts correctly

* chore: fixup prod/dev routes

* chore: get comment by post

* chore(rbac): add roles to user

https://redwoodjs.com/docs/tutorial/chapter7/rbac

* chore(rbac): gate admin page

* chore(rbac): add seed script

* chore: add default route for /admin

* chore(rbac): add delete button

also wire up test/story
https://redwoodjs.com/docs/tutorial/chapter7/rbac#mocking-currentuser-for-jest

* chore: gate delete call

* chore: allow admin to delete as well

* chore(rbac): wire up delete gating

* test(unit): simplify

* fix(storybook): add explicit imports for Netlify

* build(🧶): add reslutions for `@storybook/*`

transitive dependencies of redwood
  • Loading branch information
virtuoushub committed Apr 2, 2022
1 parent 4a1b79d commit 149289e
Show file tree
Hide file tree
Showing 55 changed files with 2,484 additions and 431 deletions.
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"pflannery.vscode-versionlens",
"editorconfig.editorconfig",
"prisma.prisma",
"graphql.vscode-graphql"
"graphql.vscode-graphql",
"johnpapa.vscode-cloak"
],
"unwantedRecommendations": []
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ WRONG
api/node_modules/.prisma

---

TODO see about test not picking up transitive dependencies, e.g. ArticleCell -> Article -> CommentForm
13 changes: 13 additions & 0 deletions api/db/migrations/20220402112722_create_comment/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "Comment" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"body" TEXT NOT NULL,
"postId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "roles" TEXT NOT NULL DEFAULT E'moderator';
15 changes: 13 additions & 2 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,4 +32,14 @@ model User {
salt String
resetToken String?
resetTokenExpiresAt DateTime?
roles String @default("moderator")
}

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 changes: 31 additions & 0 deletions api/src/graphql/comments.sdl.js
Original file line number Diff line number Diff line change
@@ -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(postId: Int!): [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(roles: ["moderator", "admin"])
}
`
4 changes: 2 additions & 2 deletions api/src/lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { db } from './db'
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, email: true },
select: { id: true, email: true, roles: true },
})
}

Expand Down Expand Up @@ -57,7 +57,7 @@ export const hasRole = ({ roles }) => {
if (Array.isArray(context.currentUser.roles)) {
return context.currentUser.roles?.some((r) => roles.includes(r))
} else {
roles.some((r) => context.currentUser.roles?.includes(r))
return roles.some((r) => context.currentUser.roles?.includes(r))
}
}

Expand Down
30 changes: 30 additions & 0 deletions api/src/services/comments/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'

export const comments = ({ postId }) => {
return db.comment.findMany({ where: { postId } })
}

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 }) => {
requireAuth({ roles: ['moderator', 'admin'] })
return db.comment.delete({
where: { id },
})
}
39 changes: 39 additions & 0 deletions api/src/services/comments/comments.scenarios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const standard = defineScenario({
comment: {
jane: {
data: {
name: 'Jane Doe',
body: 'I like trees',
post: {
create: {
title: 'Redwood Leaves',
body: 'The quick brown fox jumped over the lazy dog.',
},
},
},
},
john: {
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",
},
},
},
})
78 changes: 78 additions & 0 deletions api/src/services/comments/comments.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { comments, createComment, deleteComment } from './comments'
import { db } from 'src/lib/db'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

// 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 for a single post from the database',
async (scenario) => {
const result = await comments({ postId: scenario.comment.jane.postId })
const post = await db.post.findUnique({
where: { id: scenario.comment.jane.postId },
include: { comments: true },
})
expect(result.length).toEqual(post.comments.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)
})
scenario(
'allows admins and moderators to delete a comment',
async (scenario) => {
mockCurrentUser({ roles: ['admin', 'moderator'] })

const comment = await deleteComment({
id: scenario.comment.jane.id,
})
expect(comment.id).toEqual(scenario.comment.jane.id)

const result = await comments({ postId: scenario.comment.jane.id })
expect(result.length).toEqual(0)
}
)

scenario(
'does not allow a non-moderator to delete a comment',
async (scenario) => {
mockCurrentUser({ roles: 'user' })

expect(() =>
deleteComment({
id: scenario.comment.jane.id,
})
).toThrow(ForbiddenError)
}
)

scenario(
'does not allow a logged out user to delete a comment',
async (scenario) => {
mockCurrentUser(null)

expect(() =>
deleteComment({
id: scenario.comment.jane.id,
})
).toThrow(AuthenticationError)
}
)
})
2 changes: 0 additions & 2 deletions api/src/services/contacts/contacts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
deleteContact,
} from './contacts'

import { UserInputError } from '@redwoodjs/graphql-server'

// 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:
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@
"prisma": {
"seed": "yarn rw exec seed"
},
"resolutions": {
"@storybook/addon-a11y": "6.4.20",
"@storybook/builder-webpack5": "6.4.20",
"@storybook/manager-webpack5": "6.4.20",
"@storybook/react": "6.4.20"
},
"packageManager": "yarn@3.2.0"
}
15 changes: 15 additions & 0 deletions scripts/create-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/node

import CryptoJS from 'crypto-js'
import { db } from 'api/src/lib/db'

const user = 'moderator@moderator.com'
const password = 'password'
const salt = CryptoJS.lib.WordArray.random(128 / 8).toString()
const hashedPassword = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32,
}).toString()
// TODO add flag for admin
db.user.create({
data: { email: user, hashedPassword, salt },
})
47 changes: 26 additions & 21 deletions scripts/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,33 @@ export default async () => {

console.log(` Seeded "${post.title}"`)
}

// TODO: provide way for user to add their hashed password/salt
// create an admin user
// await db.user.upsert({
// where: { id: 1 },
// create: {
// id: 1,
// email: 'admin@admin.com',
// hashedPassword:
// 'ad9563042fe9f154419361eeeb775d8a12f3975a3722953fd8e326dd108d5645',
// salt: '1c99de412b219e9abf4665293211adce',
// },
// update: {},
// })

// console.info('')
// console.info(' Seeded admin user:')
// console.info('')
// console.info(' Email: admin@admin.com')
// console.info(' Password: admin')
// console.info('')
// console.info(` (Please don't use this login in a production environment)`)
// console.info('')
await db.user.upsert({
where: { id: 1 },
create: {
id: 1,
email: 'admin@admin.com',
hashedPassword:
'8bfb40af37a60874fd038eb3ffb16882ce93f5d84c42534d5dc4d24b1c9bcd39',
salt: '18def4c24d52742cb16f2d18312883c6',
roles: 'admin',
},
update: {},
})
// create a moderator user
await db.user.upsert({
where: { id: 2 },
create: {
id: 2,
email: 'moderator@moderator.com',
hashedPassword:
'2fbc110bd445dbb4f588c67c7a88f324444fd84716af3ad35ced0b0ebeef4e47',
salt: 'f0a8a4a7ccbe92563bffa4ca5fc40c3f',
roles: 'moderator',
},
update: {},
})
} catch (error) {
console.warn('Please define your seed data.')
console.error(error)
Expand Down
5 changes: 1 addition & 4 deletions web/config/storybook.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,5 @@ module.exports = {
features: {
interactionsDebugger: true,
},
addons: [
'@storybook/addon-actions/register',
'@storybook/addon-interactions',
],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
}
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"react-dom": "17.0.2"
},
"devDependencies": {
"@storybook/addon-actions": "6.4.20",
"@storybook/addon-essentials": "6.4.20",
"@storybook/addon-interactions": "6.4.20",
"@storybook/jest": "0.0.10",
"@storybook/test-runner": "0.0.4",
Expand Down
Loading

0 comments on commit 149289e

Please sign in to comment.