Add simple user model
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
# Migration `20201018233330-add-simple-user-model`
|
||||||
|
|
||||||
|
This migration has been generated by Kurt Hutten at 10/19/2020, 10:33:30 AM.
|
||||||
|
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||||
|
|
||||||
|
## Database Steps
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"userName" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"issuer" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"image" TEXT,
|
||||||
|
"bio" TEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "User.userName_unique" ON "User"("userName")
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email")
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "User.issuer_unique" ON "User"("issuer")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git schema.prisma schema.prisma
|
||||||
|
migration 20201011095227-create-contact..20201018233330-add-simple-user-model
|
||||||
|
--- datamodel.dml
|
||||||
|
+++ datamodel.dml
|
||||||
|
@@ -1,9 +1,7 @@
|
||||||
|
datasource DS {
|
||||||
|
- // optionally set multiple providers
|
||||||
|
- // example: provider = ["sqlite", "postgresql"]
|
||||||
|
- provider = "sqlite"
|
||||||
|
- url = "***"
|
||||||
|
+ provider = ["sqlite", "postgresql"]
|
||||||
|
+ url = "***"
|
||||||
|
}
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
@@ -34,4 +32,17 @@
|
||||||
|
email String
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+model User {
|
||||||
|
+ id Int @id @default(autoincrement())
|
||||||
|
+ userName String @unique
|
||||||
|
+ email String @unique
|
||||||
|
+ issuer String @unique
|
||||||
|
+
|
||||||
|
+ createdAt DateTime @default(now())
|
||||||
|
+ updatedAt DateTime @updatedAt
|
||||||
|
+
|
||||||
|
+ image String? // url maybe id or file storage service? cloudinary?
|
||||||
|
+ bio String? //mark down
|
||||||
|
+}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
datasource DS {
|
||||||
|
provider = ["sqlite", "postgresql"]
|
||||||
|
url = "***"
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
binaryTargets = "native"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
body String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Part {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
description String // markdown string
|
||||||
|
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||||
|
mainImage String // link to cloudinary
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
// userId
|
||||||
|
//likes, comments, reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
model Contact {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userName String @unique
|
||||||
|
email String @unique
|
||||||
|
issuer String @unique
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
image String? // url maybe id or file storage service? cloudinary?
|
||||||
|
bio String? //mark down
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"version": "0.3.14-fixed",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"tag": "UpdateArgument",
|
||||||
|
"location": {
|
||||||
|
"tag": "Source",
|
||||||
|
"source": "DS"
|
||||||
|
},
|
||||||
|
"argument": "provider",
|
||||||
|
"newValue": "[\"sqlite\", \"postgresql\"]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateModel",
|
||||||
|
"model": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "id",
|
||||||
|
"type": "Int",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "id"
|
||||||
|
},
|
||||||
|
"directive": "id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "id"
|
||||||
|
},
|
||||||
|
"directive": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateArgument",
|
||||||
|
"location": {
|
||||||
|
"tag": "Directive",
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "id"
|
||||||
|
},
|
||||||
|
"directive": "default"
|
||||||
|
},
|
||||||
|
"argument": "",
|
||||||
|
"value": "autoincrement()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "userName",
|
||||||
|
"type": "String",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "userName"
|
||||||
|
},
|
||||||
|
"directive": "unique"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "email",
|
||||||
|
"type": "String",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "email"
|
||||||
|
},
|
||||||
|
"directive": "unique"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "issuer",
|
||||||
|
"type": "String",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "issuer"
|
||||||
|
},
|
||||||
|
"directive": "unique"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "createdAt",
|
||||||
|
"type": "DateTime",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "createdAt"
|
||||||
|
},
|
||||||
|
"directive": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateArgument",
|
||||||
|
"location": {
|
||||||
|
"tag": "Directive",
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "createdAt"
|
||||||
|
},
|
||||||
|
"directive": "default"
|
||||||
|
},
|
||||||
|
"argument": "",
|
||||||
|
"value": "now()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "updatedAt",
|
||||||
|
"type": "DateTime",
|
||||||
|
"arity": "Required"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateDirective",
|
||||||
|
"location": {
|
||||||
|
"path": {
|
||||||
|
"tag": "Field",
|
||||||
|
"model": "User",
|
||||||
|
"field": "updatedAt"
|
||||||
|
},
|
||||||
|
"directive": "updatedAt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "image",
|
||||||
|
"type": "String",
|
||||||
|
"arity": "Optional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "CreateField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "bio",
|
||||||
|
"type": "String",
|
||||||
|
"arity": "Optional"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Migration `20201019072122-add-simplify-user-model`
|
||||||
|
|
||||||
|
This migration has been generated by Kurt Hutten at 10/19/2020, 6:21:22 PM.
|
||||||
|
You can check out the [state of the schema](./schema.prisma) after the migration.
|
||||||
|
|
||||||
|
## Database Steps
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DROP INDEX "User.issuer_unique"
|
||||||
|
|
||||||
|
DROP INDEX "User.userName_unique"
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"image" TEXT,
|
||||||
|
"bio" TEXT
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("id", "email", "createdAt", "updatedAt", "image", "bio") SELECT "id", "email", "createdAt", "updatedAt", "image", "bio" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git schema.prisma schema.prisma
|
||||||
|
migration 20201018233330-add-simple-user-model..20201019072122-add-simplify-user-model
|
||||||
|
--- datamodel.dml
|
||||||
|
+++ datamodel.dml
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
datasource DS {
|
||||||
|
provider = ["sqlite", "postgresql"]
|
||||||
|
- url = "***"
|
||||||
|
+ url = "***"
|
||||||
|
}
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
@@ -34,12 +34,12 @@
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
model User {
|
||||||
|
- id Int @id @default(autoincrement())
|
||||||
|
- userName String @unique
|
||||||
|
- email String @unique
|
||||||
|
- issuer String @unique
|
||||||
|
+ id Int @id @default(autoincrement())
|
||||||
|
+ email String @unique
|
||||||
|
+ // userName String @unique
|
||||||
|
+ // issuer String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
datasource DS {
|
||||||
|
provider = ["sqlite", "postgresql"]
|
||||||
|
url = "***"
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
binaryTargets = "native"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
body String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Part {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
description String // markdown string
|
||||||
|
code String @default("// Welcome to Cascade Studio! Here are some useful functions:\n// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()\n// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()\n// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),\n// FilletEdges(), ChamferEdges(),\n// Slider(), Button(), Checkbox()\nlet holeRadius = Slider(\"Radius\", 30 , 20 , 40);\nlet sphere = Sphere(50);\nlet cylinderZ = Cylinder(holeRadius, 200, true);\nlet cylinderY = Rotate([0,1,0], 90, Cylinder(holeRadius, 200, true));\nlet cylinderX = Rotate([1,0,0], 90, Cylinder(holeRadius, 200, true));\nTranslate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));\n\nTranslate([-25, 0, 40], Text3D(\"Hi!\"));\n// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!")
|
||||||
|
mainImage String // link to cloudinary
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
// userId
|
||||||
|
//likes, comments, reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
model Contact {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
// userName String @unique
|
||||||
|
// issuer String @unique
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
image String? // url maybe id or file storage service? cloudinary?
|
||||||
|
bio String? //mark down
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "0.3.14-fixed",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"tag": "DeleteField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "userName"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "DeleteField",
|
||||||
|
"model": "User",
|
||||||
|
"field": "issuer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,3 +5,5 @@
|
|||||||
20201011052155-add-code-to-part
|
20201011052155-add-code-to-part
|
||||||
20201011082558-add-code-not-needed-upon-create
|
20201011082558-add-code-not-needed-upon-create
|
||||||
20201011095227-create-contact
|
20201011095227-create-contact
|
||||||
|
20201018233330-add-simple-user-model
|
||||||
|
20201019072122-add-simplify-user-model
|
||||||
@@ -33,3 +33,16 @@ model Contact {
|
|||||||
message String
|
message String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
// userName String @unique
|
||||||
|
// issuer String @unique
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
image String? // url maybe id or file storage service? cloudinary?
|
||||||
|
bio String? //mark down
|
||||||
|
}
|
||||||
|
|||||||
35
api/src/functions/identity-signup.js
Normal file
35
api/src/functions/identity-signup.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createUser } from 'src/services/users/users.js'
|
||||||
|
|
||||||
|
export const handler = async (req, _context) => {
|
||||||
|
const body = JSON.parse(req.body)
|
||||||
|
console.log(body)
|
||||||
|
console.log(_context)
|
||||||
|
|
||||||
|
const eventType = body.event
|
||||||
|
const user = body.user
|
||||||
|
const email = user.email
|
||||||
|
|
||||||
|
let roles = []
|
||||||
|
|
||||||
|
if (eventType === 'signup') {
|
||||||
|
roles.push('user')
|
||||||
|
const hi = {
|
||||||
|
email: 'kurt.hutten@gmail.com',
|
||||||
|
image: '',
|
||||||
|
bio: ''
|
||||||
|
}
|
||||||
|
const input = {
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
createUser({input})
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
body: JSON.stringify({ app_metadata: { roles: roles } }),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
api/src/graphql/users.sdl.js
Normal file
35
api/src/graphql/users.sdl.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const schema = gql`
|
||||||
|
type User {
|
||||||
|
id: Int!
|
||||||
|
email: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
image: String
|
||||||
|
bio: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
users: [User!]!
|
||||||
|
user(id: Int!): User
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateUserInput {
|
||||||
|
email: String!
|
||||||
|
issuer: String!
|
||||||
|
image: String
|
||||||
|
bio: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInput {
|
||||||
|
email: String
|
||||||
|
issuer: String
|
||||||
|
image: String
|
||||||
|
bio: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): User!
|
||||||
|
updateUser(id: Int!, input: UpdateUserInput!): User!
|
||||||
|
deleteUser(id: Int!): User!
|
||||||
|
}
|
||||||
|
`
|
||||||
38
api/src/services/users/users.js
Normal file
38
api/src/services/users/users.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { db } from 'src/lib/db'
|
||||||
|
import { requireAuth } from 'src/lib/auth'
|
||||||
|
|
||||||
|
export const users = () => {
|
||||||
|
requireAuth({ role: 'admin' })
|
||||||
|
return db.user.findMany()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const user = ({ id }) => {
|
||||||
|
requireAuth()
|
||||||
|
return db.user.findOne({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUser = ({ input }) => {
|
||||||
|
console.log(input)
|
||||||
|
console.log(JSON.stringify(input))
|
||||||
|
requireAuth({ role: 'admin' })
|
||||||
|
return db.user.create({
|
||||||
|
data: input,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUser = ({ id, input }) => {
|
||||||
|
requireAuth()
|
||||||
|
return db.user.update({
|
||||||
|
data: input,
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteUser = ({ id }) => {
|
||||||
|
requireAuth({ role: 'admin' })
|
||||||
|
return db.user.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
9
api/src/services/users/users.test.js
Normal file
9
api/src/services/users/users.test.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
import { users } from './users'
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('users', () => {
|
||||||
|
it('returns true', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,6 +12,15 @@ import { Router, Route, Private } from '@redwoodjs/router'
|
|||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
{/* TODO add add min role to users and users/new */}
|
||||||
|
<Private unauthenticated="home" role="admin">
|
||||||
|
<Route path="/users" page={UsersPage} name="users" />
|
||||||
|
<Route path="/users/new" page={NewUserPage} name="newUser" />
|
||||||
|
</Private>
|
||||||
|
<Private unauthenticated="home">
|
||||||
|
<Route path="/users/{id:Int}/edit" page={EditUserPage} name="editUser" />
|
||||||
|
</Private>
|
||||||
|
<Route path="/users/{id:Int}" page={UserPage} name="user" />
|
||||||
<Route path="/contact" page={ContactPage} name="contact" />
|
<Route path="/contact" page={ContactPage} name="contact" />
|
||||||
<Route path="/parts/new" page={NewPartPage} name="newPart" />
|
<Route path="/parts/new" page={NewPartPage} name="newPart" />
|
||||||
<Route path="/parts/{id:Int}/edit" page={EditPartPage} name="editPart" />
|
<Route path="/parts/{id:Int}/edit" page={EditPartPage} name="editPart" />
|
||||||
|
|||||||
50
web/src/components/EditUserCell/EditUserCell.js
Normal file
50
web/src/components/EditUserCell/EditUserCell.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
|
import UserForm from 'src/components/UserForm'
|
||||||
|
|
||||||
|
export const QUERY = gql`
|
||||||
|
query FIND_USER_BY_ID($id: Int!) {
|
||||||
|
user: user(id: $id) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
image
|
||||||
|
bio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const UPDATE_USER_MUTATION = gql`
|
||||||
|
mutation UpdateUserMutation($id: Int!, $input: UpdateUserInput!) {
|
||||||
|
updateUser(id: $id, input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Loading = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Success = ({ user }) => {
|
||||||
|
const { addMessage } = useFlash()
|
||||||
|
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
|
||||||
|
onCompleted: () => {
|
||||||
|
navigate(routes.users())
|
||||||
|
addMessage('User updated.', { classes: 'rw-flash-success' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSave = (input, id) => {
|
||||||
|
updateUser({ variables: { id, input } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">Edit User {user.id}</h2>
|
||||||
|
</header>
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<UserForm user={user} onSave={onSave} error={error} loading={loading} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
web/src/components/NewUser/NewUser.js
Normal file
38
web/src/components/NewUser/NewUser.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
|
import UserForm from 'src/components/UserForm'
|
||||||
|
|
||||||
|
const CREATE_USER_MUTATION = gql`
|
||||||
|
mutation CreateUserMutation($input: CreateUserInput!) {
|
||||||
|
createUser(input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const NewUser = () => {
|
||||||
|
const { addMessage } = useFlash()
|
||||||
|
const [createUser, { loading, error }] = useMutation(CREATE_USER_MUTATION, {
|
||||||
|
onCompleted: () => {
|
||||||
|
navigate(routes.users())
|
||||||
|
addMessage('User created.', { classes: 'rw-flash-success' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSave = (input) => {
|
||||||
|
createUser({ variables: { input } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">New User</h2>
|
||||||
|
</header>
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<UserForm onSave={onSave} loading={loading} error={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewUser
|
||||||
103
web/src/components/User/User.js
Normal file
103
web/src/components/User/User.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
|
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
const DELETE_USER_MUTATION = gql`
|
||||||
|
mutation DeleteUserMutation($id: Int!) {
|
||||||
|
deleteUser(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const jsonDisplay = (obj) => {
|
||||||
|
return (
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(obj, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeTag = (datetime) => {
|
||||||
|
return (
|
||||||
|
<time dateTime={datetime} title={datetime}>
|
||||||
|
{new Date(datetime).toUTCString()}
|
||||||
|
</time>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxInputTag = (checked) => {
|
||||||
|
return <input type="checkbox" checked={checked} disabled />
|
||||||
|
}
|
||||||
|
|
||||||
|
const User = ({ user }) => {
|
||||||
|
const { addMessage } = useFlash()
|
||||||
|
const [deleteUser] = useMutation(DELETE_USER_MUTATION, {
|
||||||
|
onCompleted: () => {
|
||||||
|
navigate(routes.users())
|
||||||
|
addMessage('User deleted.', { classes: 'rw-flash-success' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDeleteClick = (id) => {
|
||||||
|
if (confirm('Are you sure you want to delete user ' + id + '?')) {
|
||||||
|
deleteUser({ variables: { id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">
|
||||||
|
User {user.id} Detail
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<table className="rw-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<td>{user.id}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Created at</th>
|
||||||
|
<td>{timeTag(user.createdAt)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Updated at</th>
|
||||||
|
<td>{timeTag(user.updatedAt)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Image</th>
|
||||||
|
<td>{user.image}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Bio</th>
|
||||||
|
<td>{user.bio}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav className="rw-button-group">
|
||||||
|
<Link
|
||||||
|
to={routes.editUser({ id: user.id })}
|
||||||
|
className="rw-button rw-button-blue"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="rw-button rw-button-red"
|
||||||
|
onClick={() => onDeleteClick(user.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default User
|
||||||
22
web/src/components/UserCell/UserCell.js
Normal file
22
web/src/components/UserCell/UserCell.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import User from 'src/components/User'
|
||||||
|
|
||||||
|
export const QUERY = gql`
|
||||||
|
query FIND_USER_BY_ID($id: Int!) {
|
||||||
|
user: user(id: $id) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
image
|
||||||
|
bio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Loading = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Empty = () => <div>User not found</div>
|
||||||
|
|
||||||
|
export const Success = ({ user }) => {
|
||||||
|
return <User user={user} />
|
||||||
|
}
|
||||||
81
web/src/components/UserForm/UserForm.js
Normal file
81
web/src/components/UserForm/UserForm.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormError,
|
||||||
|
FieldError,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
Submit,
|
||||||
|
} from '@redwoodjs/forms'
|
||||||
|
|
||||||
|
const UserForm = (props) => {
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
props.onSave(data, props?.user?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} error={props.error}>
|
||||||
|
<FormError
|
||||||
|
error={props.error}
|
||||||
|
wrapperClassName="rw-form-error-wrapper"
|
||||||
|
titleClassName="rw-form-error-title"
|
||||||
|
listClassName="rw-form-error-list"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="email"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="email"
|
||||||
|
defaultValue={props.user?.email}
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
validation={{ required: true }}
|
||||||
|
/>
|
||||||
|
<FieldError name="email" className="rw-field-error" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="image"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Image
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="image"
|
||||||
|
defaultValue={props.user?.image}
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
/>
|
||||||
|
<FieldError name="image" className="rw-field-error" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="bio"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Bio
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="bio"
|
||||||
|
defaultValue={props.user?.bio}
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
/>
|
||||||
|
<FieldError name="bio" className="rw-field-error" />
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||||
|
Save
|
||||||
|
</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserForm
|
||||||
109
web/src/components/Users/Users.js
Normal file
109
web/src/components/Users/Users.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useMutation, useFlash } from '@redwoodjs/web'
|
||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
const DELETE_USER_MUTATION = gql`
|
||||||
|
mutation DeleteUserMutation($id: Int!) {
|
||||||
|
deleteUser(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MAX_STRING_LENGTH = 150
|
||||||
|
|
||||||
|
const truncate = (text) => {
|
||||||
|
let output = text
|
||||||
|
if (text && text.length > MAX_STRING_LENGTH) {
|
||||||
|
output = output.substring(0, MAX_STRING_LENGTH) + '...'
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonTruncate = (obj) => {
|
||||||
|
return truncate(JSON.stringify(obj, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeTag = (datetime) => {
|
||||||
|
return (
|
||||||
|
<time dateTime={datetime} title={datetime}>
|
||||||
|
{new Date(datetime).toUTCString()}
|
||||||
|
</time>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxInputTag = (checked) => {
|
||||||
|
return <input type="checkbox" checked={checked} disabled />
|
||||||
|
}
|
||||||
|
|
||||||
|
const UsersList = ({ users }) => {
|
||||||
|
const { addMessage } = useFlash()
|
||||||
|
const [deleteUser] = useMutation(DELETE_USER_MUTATION, {
|
||||||
|
onCompleted: () => {
|
||||||
|
addMessage('User deleted.', { classes: 'rw-flash-success' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDeleteClick = (id) => {
|
||||||
|
if (confirm('Are you sure you want to delete user ' + id + '?')) {
|
||||||
|
deleteUser({ variables: { id }, refetchQueries: ['USERS'] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rw-segment rw-table-wrapper-responsive">
|
||||||
|
<table className="rw-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Created at</th>
|
||||||
|
<th>Updated at</th>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>Bio</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id}>
|
||||||
|
<td>{truncate(user.id)}</td>
|
||||||
|
<td>{truncate(user.email)}</td>
|
||||||
|
<td>{timeTag(user.createdAt)}</td>
|
||||||
|
<td>{timeTag(user.updatedAt)}</td>
|
||||||
|
<td>{truncate(user.image)}</td>
|
||||||
|
<td>{truncate(user.bio)}</td>
|
||||||
|
<td>
|
||||||
|
<nav className="rw-table-actions">
|
||||||
|
<Link
|
||||||
|
to={routes.user({ id: user.id })}
|
||||||
|
title={'Show user ' + user.id + ' detail'}
|
||||||
|
className="rw-button rw-button-small"
|
||||||
|
>
|
||||||
|
Show
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={routes.editUser({ id: user.id })}
|
||||||
|
title={'Edit user ' + user.id}
|
||||||
|
className="rw-button rw-button-small rw-button-blue"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
title={'Delete user ' + user.id}
|
||||||
|
className="rw-button rw-button-small rw-button-red"
|
||||||
|
onClick={() => onDeleteClick(user.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersList
|
||||||
33
web/src/components/UsersCell/UsersCell.js
Normal file
33
web/src/components/UsersCell/UsersCell.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
import Users from 'src/components/Users'
|
||||||
|
|
||||||
|
export const QUERY = gql`
|
||||||
|
query USERS {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
image
|
||||||
|
bio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Loading = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Empty = () => {
|
||||||
|
return (
|
||||||
|
<div className="rw-text-center">
|
||||||
|
{'No users yet. '}
|
||||||
|
<Link to={routes.newUser()} className="rw-link">
|
||||||
|
{'Create one?'}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Success = ({ users }) => {
|
||||||
|
return <Users users={users} />
|
||||||
|
}
|
||||||
@@ -35,7 +35,12 @@ const MainLayout = ({ children }) => {
|
|||||||
<Svg name="plus" className="text-indigo-300" />
|
<Svg name="plus" className="text-indigo-300" />
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="mr-12 p-px border-2 rounded-full border-indigo-300"><img src={avatar} className="rounded-full h-10 w-10" /></li>
|
<li className="mr-12 p-px border-2 rounded-full border-indigo-300 text-indigo-200">
|
||||||
|
<a href="#" onClick={isAuthenticated ? logOut : logIn}>
|
||||||
|
{isAuthenticated ? 'Log Out' : 'Log In'}
|
||||||
|
<img src={avatar} className="rounded-full h-10 w-10" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
12
web/src/pages/EditUserPage/EditUserPage.js
Normal file
12
web/src/pages/EditUserPage/EditUserPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
|
import EditUserCell from 'src/components/EditUserCell'
|
||||||
|
|
||||||
|
const EditUserPage = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<EditUserCell id={id} />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditUserPage
|
||||||
12
web/src/pages/NewUserPage/NewUserPage.js
Normal file
12
web/src/pages/NewUserPage/NewUserPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
|
import NewUser from 'src/components/NewUser'
|
||||||
|
|
||||||
|
const NewUserPage = () => {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<NewUser />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewUserPage
|
||||||
12
web/src/pages/UserPage/UserPage.js
Normal file
12
web/src/pages/UserPage/UserPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
|
import UserCell from 'src/components/UserCell'
|
||||||
|
|
||||||
|
const UserPage = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<UserCell id={id} />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPage
|
||||||
12
web/src/pages/UsersPage/UsersPage.js
Normal file
12
web/src/pages/UsersPage/UsersPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
|
import UsersCell from 'src/components/UsersCell'
|
||||||
|
|
||||||
|
const UsersPage = () => {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<UsersCell />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersPage
|
||||||
Reference in New Issue
Block a user