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

feat: finish tutorial #18

Merged
merged 56 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
96473fb
chore: add some storybook
virtuoushub Mar 30, 2022
a404666
fix: enable show code button
virtuoushub Apr 1, 2022
6c15adf
fix: better layout
virtuoushub Apr 1, 2022
54b46eb
chore: make boilerplate stories
virtuoushub Apr 1, 2022
c8cbea2
fix: account for missing id on netlify
virtuoushub Apr 1, 2022
08cfb1f
chore: fixup some tests
virtuoushub Apr 1, 2022
8043f5a
chore: add `Comment`
virtuoushub Apr 1, 2022
ff94e53
chore: add some boilerplate
virtuoushub Apr 1, 2022
cba0e50
chore(Comment): improve and wire up some mock data
virtuoushub Apr 1, 2022
e20c845
chore: add styling
virtuoushub Apr 1, 2022
974d90d
chore: add CommentsCell
virtuoushub Apr 1, 2022
842ce03
chore: add some boilerplate
virtuoushub Apr 1, 2022
98b023a
chore: add better mocks
virtuoushub Apr 1, 2022
d7e2fd1
chore: update CommentsCell
virtuoushub Apr 1, 2022
edaf655
chore: add a bit more styling
virtuoushub Apr 1, 2022
27cdc9b
chore: add comments
virtuoushub Apr 1, 2022
dff5b0d
chore: fix
virtuoushub Apr 1, 2022
3e54617
chore: add Comments to the schema
virtuoushub Apr 1, 2022
95d4435
chore(prisma): generate Comment table migration
virtuoushub Apr 2, 2022
6aae1de
chore(prisma): generate Comment table migration
virtuoushub Apr 2, 2022
db6c6bb
chore(ide): add recommended extension
virtuoushub Apr 1, 2022
c2e8465
chore: remove unused routes in main app
virtuoushub Apr 2, 2022
f348655
Revert "chore: remove unused routes in main app"
virtuoushub Apr 2, 2022
ce5966d
chore: disable contact/login forms
virtuoushub Apr 2, 2022
d2fd501
chore: remove unused import
virtuoushub Apr 2, 2022
a57418d
test(unit): add example async test
virtuoushub Apr 2, 2022
4d5ea73
test(unit): default summary to true
virtuoushub Apr 2, 2022
2c78d1d
chore: update empty comments cell
virtuoushub Apr 2, 2022
d8eb508
chore(comments): allow/create
virtuoushub Apr 2, 2022
266dbfb
chore(commentForm): run generator
virtuoushub Apr 2, 2022
b134721
chore(commentForm): add some boilerplate
virtuoushub Apr 2, 2022
6dbcd60
chore(commentForm): simple form
virtuoushub Apr 2, 2022
836aed0
chore(commentForm): add submit
virtuoushub Apr 2, 2022
a66e22b
chore(storybook): wire up mockGraphQLMutation
virtuoushub Apr 2, 2022
1dff79c
chore(storybook): add interaction test
virtuoushub Apr 2, 2022
a8f722d
chore: fixup style
virtuoushub Apr 2, 2022
d32e863
test(unit): better test
virtuoushub Apr 2, 2022
0203671
test: add loading snapshot
virtuoushub Apr 2, 2022
595b7be
chore(commentForm): use form
virtuoushub Apr 2, 2022
60aef02
chore(commentForm): wire up correctly in Article
virtuoushub Apr 2, 2022
929bbab
chore(contactForm): refetch comments after create
virtuoushub Apr 2, 2022
922638e
chore: add toast feedback for form
virtuoushub Apr 2, 2022
97e0a8e
chore: wire up comments to posts correctly
virtuoushub Apr 2, 2022
3d7bc13
chore: fixup prod/dev routes
virtuoushub Apr 2, 2022
bb41f32
chore: get comment by post
virtuoushub Apr 2, 2022
974f3c1
chore(rbac): add roles to user
virtuoushub Apr 2, 2022
6568698
chore(rbac): gate admin page
virtuoushub Apr 2, 2022
50682f5
chore(rbac): add seed script
virtuoushub Apr 2, 2022
398dd90
chore: add default route for /admin
virtuoushub Apr 2, 2022
2402799
chore(rbac): add delete button
virtuoushub Apr 2, 2022
5e6b778
chore: gate delete call
virtuoushub Apr 2, 2022
e3b1cee
chore: allow admin to delete as well
virtuoushub Apr 2, 2022
b385b93
chore(rbac): wire up delete gating
virtuoushub Apr 2, 2022
405525c
test(unit): simplify
virtuoushub Apr 2, 2022
16e678c
fix(storybook): add explicit imports for Netlify
virtuoushub Apr 2, 2022
64d1237
build(🧶): add reslutions for `@storybook/*`
virtuoushub Apr 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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