├── scripts ├── .keep └── seed.ts ├── .nvmrc ├── web ├── src │ ├── index.css │ ├── layouts │ │ ├── .keep │ │ └── UsersLayout │ │ │ └── UsersLayout.tsx │ ├── components │ │ ├── .keep │ │ ├── User │ │ │ ├── UserCell │ │ │ │ └── UserCell.tsx │ │ │ ├── UsersCell │ │ │ │ └── UsersCell.tsx │ │ │ ├── NewUser │ │ │ │ └── NewUser.tsx │ │ │ ├── User │ │ │ │ └── User.tsx │ │ │ ├── UserForm │ │ │ │ └── UserForm.tsx │ │ │ ├── EditUserCell │ │ │ │ └── EditUserCell.tsx │ │ │ └── Users │ │ │ │ └── Users.tsx │ │ └── DeleteButton.tsx │ ├── pages │ │ ├── User │ │ │ ├── UsersPage │ │ │ │ └── UsersPage.tsx │ │ │ ├── NewUserPage │ │ │ │ └── NewUserPage.tsx │ │ │ ├── UserPage │ │ │ │ └── UserPage.tsx │ │ │ └── EditUserPage │ │ │ │ └── EditUserPage.tsx │ │ ├── NotFoundPage │ │ │ └── NotFoundPage.tsx │ │ └── FatalErrorPage │ │ │ └── FatalErrorPage.tsx │ ├── index.html │ ├── App.tsx │ ├── Routes.tsx │ └── scaffold.css ├── public │ ├── robots.txt │ ├── favicon.png │ └── README.md ├── jest.config.js ├── package.json └── tsconfig.json ├── api ├── src │ ├── graphql │ │ ├── .keep │ │ ├── objectIdentification.sdl.ts │ │ └── users.sdl.ts │ ├── services │ │ ├── .keep │ │ ├── users │ │ │ ├── users.scenarios.ts │ │ │ ├── users.ts │ │ │ └── users.test.ts │ │ └── objectIdentification.ts │ ├── directives │ │ ├── skipAuth │ │ │ ├── skipAuth.test.ts │ │ │ └── skipAuth.ts │ │ └── requireAuth │ │ │ ├── requireAuth.ts │ │ │ └── requireAuth.test.ts │ ├── lib │ │ ├── db.ts │ │ ├── logger.ts │ │ └── auth.ts │ └── functions │ │ └── graphql.ts ├── jest.config.js ├── db │ ├── migrations │ │ ├── migration_lock.toml │ │ └── 20211211163440_init │ │ │ └── migration.sql │ └── schema.prisma ├── package.json └── tsconfig.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .env.example ├── graphql.config.js ├── .editorconfig ├── .gitignore ├── prettier.config.js ├── package.json ├── redwood.toml ├── LICENSE ├── .env.defaults └── README.md /scripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/src/graphql/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/src/services/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/layouts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/components/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@redwoodjs/testing/config/jest/api') 2 | -------------------------------------------------------------------------------- /web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@redwoodjs/testing/config/jest/web') 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | ], 4 | "unwantedRecommendations": [] 5 | } 6 | -------------------------------------------------------------------------------- /web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orta/redwood-object-identification/HEAD/web/public/favicon.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # DATABASE_URL=file:./dev.db 2 | # TEST_DATABASE_URL=file:./.redwood/test.db 3 | # PRISMA_HIDE_UPDATE_MESSAGE=true 4 | # LOG_LEVEL=trace 5 | -------------------------------------------------------------------------------- /graphql.config.js: -------------------------------------------------------------------------------- 1 | const { getPaths } = require('@redwoodjs/internal') 2 | 3 | module.exports = { 4 | schema: getPaths().generated.schema, 5 | } 6 | -------------------------------------------------------------------------------- /api/db/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /web/src/pages/User/UsersPage/UsersPage.tsx: -------------------------------------------------------------------------------- 1 | import UsersCell from "src/components/User/UsersCell" 2 | 3 | const UsersPage = () => { 4 | return 5 | } 6 | 7 | export default UsersPage 8 | -------------------------------------------------------------------------------- /web/src/pages/User/NewUserPage/NewUserPage.tsx: -------------------------------------------------------------------------------- 1 | import NewUser from "src/components/User/NewUser" 2 | 3 | const NewUserPage = () => { 4 | return 5 | } 6 | 7 | export default NewUserPage 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .env 4 | .netlify 5 | .redwood 6 | dev.db* 7 | dist 8 | dist-babel 9 | node_modules 10 | yarn-error.log 11 | web/public/mockServiceWorker.js 12 | web/types/graphql.d.ts 13 | api/types/graphql.d.ts 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "yarn redwood dev", 6 | "name": "launch development", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "files.trimTrailingWhitespace": true, 4 | "editor.formatOnSave": false, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "[prisma]": { 9 | "editor.formatOnSave": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/src/pages/User/UserPage/UserPage.tsx: -------------------------------------------------------------------------------- 1 | import UserCell from "src/components/User/UserCell" 2 | 3 | type UserPageProps = { 4 | slug: string 5 | } 6 | 7 | const UserPage = ({ slug }: UserPageProps) => { 8 | return 9 | } 10 | 11 | export default UserPage 12 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@redwoodjs/api": "0.39.4", 7 | "@redwoodjs/graphql-server": "0.39.4" 8 | }, 9 | "devDependencies": { 10 | "cuid": "^2.1.8", 11 | "guid": "^0.0.12" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/src/pages/User/EditUserPage/EditUserPage.tsx: -------------------------------------------------------------------------------- 1 | import EditUserCell from "src/components/User/EditUserCell" 2 | 3 | type UserPageProps = { 4 | slug: string 5 | } 6 | 7 | const EditUserPage = ({ slug }: UserPageProps) => { 8 | return 9 | } 10 | 11 | export default EditUserPage 12 | -------------------------------------------------------------------------------- /api/src/graphql/objectIdentification.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | scalar ID 3 | 4 | # An object with a Globally Unique ID 5 | interface Node { 6 | id: ID! 7 | } 8 | 9 | type Query { 10 | node(id: ID): Node! @skipAuth 11 | } 12 | 13 | type Mutation { 14 | deleteNode(id: ID): Node! @requireAuth 15 | } 16 | ` 17 | -------------------------------------------------------------------------------- /api/src/services/users/users.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@prisma/client" 2 | 3 | export const standard = defineScenario({ 4 | user: { 5 | one: { data: { email: "String7803398" } }, 6 | two: { data: { email: "String9118803" } }, 7 | }, 8 | }) 9 | 10 | export type StandardScenario = typeof standard 11 | -------------------------------------------------------------------------------- /api/db/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = "native" 9 | } 10 | 11 | model User { 12 | id String @id @unique 13 | slug String @unique 14 | email String @unique 15 | name String? 16 | } 17 | -------------------------------------------------------------------------------- /api/src/directives/skipAuth/skipAuth.test.ts: -------------------------------------------------------------------------------- 1 | import { getDirectiveName } from "@redwoodjs/testing/api" 2 | 3 | import skipAuth from "./skipAuth" 4 | 5 | describe("skipAuth directive", () => { 6 | it("declares the directive sdl as schema, with the correct name", () => { 7 | expect(skipAuth.schema).toBeTruthy() 8 | expect(getDirectiveName(skipAuth.schema)).toBe("skipAuth") 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // https://prettier.io/docs/en/options.html 2 | /** @type {import('prettier').RequiredOptions} */ 3 | module.exports = { 4 | trailingComma: 'es5', 5 | bracketSpacing: true, 6 | tabWidth: 2, 7 | semi: false, 8 | singleQuote: true, 9 | arrowParens: 'always', 10 | overrides: [ 11 | { 12 | files: 'Routes.*', 13 | options: { 14 | printWidth: 999, 15 | }, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /api/src/directives/skipAuth/skipAuth.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | import { createValidatorDirective } from "@redwoodjs/graphql-server" 4 | 5 | export const schema = gql` 6 | """ 7 | Use to skip authentication checks and allow public access. 8 | """ 9 | directive @skipAuth on FIELD_DEFINITION 10 | ` 11 | 12 | const skipAuth = createValidatorDirective(schema, () => { 13 | return 14 | }) 15 | 16 | export default skipAuth 17 | -------------------------------------------------------------------------------- /web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | <%= prerenderPlaceholder %> 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /api/db/migrations/20211211163440_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "slug" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "name" TEXT 7 | ); 8 | 9 | -- CreateIndex 10 | CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "User_slug_key" ON "User"("slug"); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 17 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "browserslist": { 6 | "development": [ 7 | "last 1 version" 8 | ], 9 | "production": [ 10 | "defaults", 11 | "not IE 11", 12 | "not IE_Mob 11" 13 | ] 14 | }, 15 | "dependencies": { 16 | "@redwoodjs/forms": "0.39.4", 17 | "@redwoodjs/router": "0.39.4", 18 | "@redwoodjs/web": "0.39.4", 19 | "prop-types": "15.7.2", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | // See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor 2 | // for options. 3 | 4 | import { PrismaClient } from "@prisma/client" 5 | 6 | import { emitLogLevels, handlePrismaLogging } from "@redwoodjs/api/logger" 7 | 8 | import { logger } from "./logger" 9 | 10 | /* 11 | * Instance of the Prisma Client 12 | */ 13 | export const db = new PrismaClient({ 14 | log: emitLogLevels(["info", "warn", "error"]), 15 | }) 16 | 17 | handlePrismaLogging({ 18 | db, 19 | logger, 20 | logLevels: ["info", "warn", "error"], 21 | }) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": { 4 | "packages": [ 5 | "api", 6 | "web", 7 | "packages/*" 8 | ] 9 | }, 10 | "devDependencies": { 11 | "@redwoodjs/core": "0.39.4" 12 | }, 13 | "eslintConfig": { 14 | "extends": "@redwoodjs/eslint-config", 15 | "root": true 16 | }, 17 | "engines": { 18 | "node": ">=14.17 <=16.x", 19 | "yarn": ">=1.15 <2" 20 | }, 21 | "prisma": { 22 | "seed": "yarn rw exec seed" 23 | }, 24 | "prettier": { 25 | "printWidth": 140, 26 | "semi": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FatalErrorBoundary, RedwoodProvider } from "@redwoodjs/web" 2 | import { RedwoodApolloProvider } from "@redwoodjs/web/apollo" 3 | 4 | import FatalErrorPage from "src/pages/FatalErrorPage" 5 | import Routes from "src/Routes" 6 | 7 | import "./scaffold.css" 8 | import "./index.css" 9 | 10 | const App = () => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | 20 | export default App 21 | -------------------------------------------------------------------------------- /api/src/graphql/users.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type User implements Node { 3 | id: ID! 4 | slug: String! 5 | email: String! 6 | name: String 7 | } 8 | 9 | type Query { 10 | users: [User!]! @requireAuth 11 | user(id: String!): User @requireAuth 12 | } 13 | 14 | input CreateUserInput { 15 | email: String! 16 | name: String 17 | } 18 | 19 | input UpdateUserInput { 20 | email: String 21 | name: String 22 | } 23 | 24 | type Mutation { 25 | createUser(input: CreateUserInput!): User! @requireAuth 26 | updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth 27 | deleteUser(id: String!): User! @requireAuth 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /api/src/directives/requireAuth/requireAuth.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag" 2 | 3 | import { createValidatorDirective } from "@redwoodjs/graphql-server" 4 | 5 | import { requireAuth as applicationRequireAuth } from "src/lib/auth" 6 | 7 | export const schema = gql` 8 | """ 9 | Use to check whether or not a user is authenticated and is associated 10 | with an optional set of roles. 11 | """ 12 | directive @requireAuth(roles: [String]) on FIELD_DEFINITION 13 | ` 14 | 15 | const validate = ({ directiveArgs }) => { 16 | const { roles } = directiveArgs 17 | applicationRequireAuth({ roles }) 18 | } 19 | 20 | const requireAuth = createValidatorDirective(schema, validate) 21 | 22 | export default requireAuth 23 | -------------------------------------------------------------------------------- /api/src/functions/graphql.ts: -------------------------------------------------------------------------------- 1 | import { createGraphQLHandler } from "@redwoodjs/graphql-server" 2 | 3 | import directives from "src/directives/**/*.{js,ts}" 4 | import sdls from "src/graphql/**/*.sdl.{js,ts}" 5 | import services from "src/services/**/*.{js,ts}" 6 | 7 | import { db } from "src/lib/db" 8 | import { logger } from "src/lib/logger" 9 | 10 | import { createNodeResolveEnvelopPlugin } from "src/services/objectIdentification" 11 | 12 | export const handler = createGraphQLHandler({ 13 | loggerConfig: { logger, options: {} }, 14 | directives, 15 | sdls, 16 | services, 17 | extraPlugins: [createNodeResolveEnvelopPlugin()], 18 | onException: () => { 19 | // Disconnect from your database with an unhandled exception. 20 | db.$disconnect() 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /web/src/components/User/UserCell/UserCell.tsx: -------------------------------------------------------------------------------- 1 | import type { FindUserById } from "types/graphql" 2 | import type { CellSuccessProps, CellFailureProps } from "@redwoodjs/web" 3 | 4 | import User from "src/components/User/User" 5 | 6 | export const QUERY = gql` 7 | query FindUserById($slug: String!) { 8 | user: user(id: $slug) { 9 | id 10 | slug 11 | email 12 | name 13 | } 14 | } 15 | ` 16 | 17 | export const Loading = () =>
Loading...
18 | 19 | export const Empty = () =>
User not found
20 | 21 | export const Failure = ({ error }: CellFailureProps) =>
{error.message}
22 | 23 | export const Success = ({ user }: CellSuccessProps) => { 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /redwood.toml: -------------------------------------------------------------------------------- 1 | # This file contains the configuration settings for your Redwood app. 2 | # This file is also what makes your Redwood app a Redwood app. 3 | # If you remove it and try to run `yarn rw dev`, you'll get an error. 4 | # 5 | # For the full list of options, see the "App Configuration: redwood.toml" doc: 6 | # https://redwoodjs.com/docs/app-configuration-redwood-toml 7 | 8 | [web] 9 | title = "Redwood App" 10 | port = 8910 11 | apiUrl = "/.redwood/functions" # you can customise graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths 12 | includeEnvironmentVariables = [] # any ENV vars that should be available to the web side, see https://redwoodjs.com/docs/environment-variables#web 13 | [api] 14 | port = 8911 15 | [browser] 16 | open = true 17 | -------------------------------------------------------------------------------- /api/src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from "@redwoodjs/api/logger" 2 | 3 | /** 4 | * Creates a logger with RedwoodLoggerOptions 5 | * 6 | * These extend and override default LoggerOptions, 7 | * can define a destination like a file or other supported pino log transport stream, 8 | * and sets whether or not to show the logger configuration settings (defaults to false) 9 | * 10 | * @param RedwoodLoggerOptions 11 | * 12 | * RedwoodLoggerOptions have 13 | * @param {options} LoggerOptions - defines how to log, such as pretty printing, redaction, and format 14 | * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file 15 | * @param {boolean} showConfig - whether to display logger configuration on initialization 16 | */ 17 | export const logger = createLogger({}) 18 | -------------------------------------------------------------------------------- /api/src/directives/requireAuth/requireAuth.test.ts: -------------------------------------------------------------------------------- 1 | import { mockRedwoodDirective, getDirectiveName } from "@redwoodjs/testing/api" 2 | 3 | import requireAuth from "./requireAuth" 4 | 5 | describe("requireAuth directive", () => { 6 | it("declares the directive sdl as schema, with the correct name", () => { 7 | expect(requireAuth.schema).toBeTruthy() 8 | expect(getDirectiveName(requireAuth.schema)).toBe("requireAuth") 9 | }) 10 | 11 | it("requireAuth has stub implementation. Should not throw when current user", () => { 12 | // If you want to set values in context, pass it through e.g. 13 | // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }}) 14 | const mockExecution = mockRedwoodDirective(requireAuth, { context: {} }) 15 | 16 | expect(mockExecution).not.toThrowError() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "baseUrl": "./", 10 | "rootDirs": [ 11 | "./src", 12 | "../.redwood/types/mirror/web/src" 13 | ], 14 | "paths": { 15 | "src/*": [ 16 | "./src/*", 17 | "../.redwood/types/mirror/web/src/*" 18 | ], 19 | "types/*": ["./types/*"], 20 | "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/web"] 21 | }, 22 | "typeRoots": ["../node_modules/@types", "./node_modules/@types"], 23 | "types": ["jest"], 24 | "jsx": "preserve", 25 | }, 26 | "include": [ 27 | "src", 28 | "../.redwood/types/includes/all-*", 29 | "../.redwood/types/includes/web-*", 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "baseUrl": "./", 10 | "rootDirs": [ 11 | "./src", 12 | "../.redwood/types/mirror/api/src" 13 | ], 14 | "paths": { 15 | "src/*": [ 16 | "./src/*", 17 | "../.redwood/types/mirror/api/src/*" 18 | ], 19 | "types/*": ["./types/*"], 20 | "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/api"] 21 | }, 22 | "typeRoots": [ 23 | "../node_modules/@types", 24 | "./node_modules/@types" 25 | ], 26 | "types": ["jest"], 27 | }, 28 | "include": [ 29 | "src", 30 | "../.redwood/types/includes/all-*", 31 | "../.redwood/types/includes/api-*", 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /api/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Once you are ready to add authentication to your application 3 | * you'll build out requireAuth() with real functionality. For 4 | * now we just return `true` so that the calls in services 5 | * have something to check against, simulating a logged 6 | * in user that is allowed to access that service. 7 | * 8 | * See https://redwoodjs.com/docs/authentication for more info. 9 | */ 10 | export const isAuthenticated = () => { 11 | return true 12 | } 13 | 14 | export const hasRole = ({ roles }) => { 15 | return roles !== undefined 16 | } 17 | 18 | // This is used by the redwood directive 19 | // in ./api/src/directives/requireAuth 20 | 21 | // Roles are passed in by the requireAuth directive if you have auth setup 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | export const requireAuth = ({ roles }) => { 24 | return isAuthenticated() 25 | } 26 | -------------------------------------------------------------------------------- /scripts/seed.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@prisma/client" 2 | import { db } from "api/src/lib/db" 3 | import cuid from "cuid" 4 | 5 | export default async () => { 6 | type Data = Omit 7 | 8 | const data: Data[] = [ 9 | { name: "alice", email: "alice@example.com" }, 10 | { name: "bob", email: "bob@example.com" }, 11 | { 12 | name: "charlie", 13 | email: "charlie@example.com", 14 | }, 15 | { 16 | name: "danielle", 17 | email: "dani@example.com", 18 | }, 19 | { name: "eli", email: "eli@example.com" }, 20 | ] 21 | 22 | Promise.all( 23 | data.map(async (userExample) => { 24 | const record = await db.user.create({ 25 | data: { 26 | id: cuid() + ":user", 27 | slug: cuid.slug(), 28 | ...userExample, 29 | }, 30 | }) 31 | console.log(record) 32 | }) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /web/src/layouts/UsersLayout/UsersLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Link, routes } from "@redwoodjs/router" 2 | import { Toaster } from "@redwoodjs/web/toast" 3 | 4 | type UserLayoutProps = { 5 | children: React.ReactNode 6 | } 7 | 8 | const UsersLayout = ({ children }: UserLayoutProps) => { 9 | return ( 10 |
11 | 12 |
13 |

14 | 15 | Users 16 | 17 |

18 | 19 |
+
New User 20 | 21 |
22 |
{children}
23 |
24 | ) 25 | } 26 | 27 | export default UsersLayout 28 | -------------------------------------------------------------------------------- /web/src/components/User/UsersCell/UsersCell.tsx: -------------------------------------------------------------------------------- 1 | import type { FindUsers } from "types/graphql" 2 | import type { CellSuccessProps, CellFailureProps } from "@redwoodjs/web" 3 | 4 | import { Link, routes } from "@redwoodjs/router" 5 | 6 | import Users from "src/components/User/Users" 7 | 8 | export const QUERY = gql` 9 | query FindUsers { 10 | users { 11 | id 12 | slug 13 | email 14 | name 15 | } 16 | } 17 | ` 18 | 19 | export const Loading = () =>
Loading...
20 | 21 | export const Empty = () => { 22 | return ( 23 |
24 | {"No users yet. "} 25 | 26 | {"Create one?"} 27 | 28 |
29 | ) 30 | } 31 | 32 | export const Failure = ({ error }: CellFailureProps) =>
{error.message}
33 | 34 | export const Success = ({ users }: CellSuccessProps) => { 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /web/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | // In this file, all Page components from 'src/pages` are auto-imported. Nested 2 | // directories are supported, and should be uppercase. Each subdirectory will be 3 | // prepended onto the component name. 4 | // 5 | // Examples: 6 | // 7 | // 'src/pages/HomePage/HomePage.js' -> HomePage 8 | // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage 9 | 10 | import { Set, Router, Route } from "@redwoodjs/router" 11 | import UsersLayout from "src/layouts/UsersLayout" 12 | 13 | const Routes = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Routes 28 | -------------------------------------------------------------------------------- /web/src/components/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import { navigate, routes } from "@redwoodjs/router" 2 | import { useMutation } from "@redwoodjs/web" 3 | import { toast } from "@redwoodjs/web/dist/toast" 4 | 5 | const DELETE_NODE_MUTATION = gql` 6 | mutation DeleteNodeMutation($id: ID!) { 7 | deleteNode(id: $id) { 8 | id 9 | } 10 | } 11 | ` 12 | 13 | export const DeleteButton = (props: { id: string; displayName: string }) => { 14 | const [deleteUser] = useMutation(DELETE_NODE_MUTATION, { 15 | onCompleted: () => { 16 | toast.success(`${props.displayName} deleted`) 17 | navigate(routes.users()) 18 | }, 19 | onError: (error) => { 20 | toast.error(error.message) 21 | }, 22 | }) 23 | 24 | const onDeleteClick = () => { 25 | if (confirm(`Are you sure you want to delete ${props.displayName}?`)) { 26 | deleteUser({ variables: { id: props.id } }) 27 | } 28 | } 29 | return ( 30 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redwood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/src/services/users/users.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@prisma/client" 2 | import cuid from "cuid" 3 | import { db } from "src/lib/db" 4 | 5 | export const users = () => { 6 | return db.user.findMany() 7 | } 8 | 9 | export const user = async (args: { id: string }) => { 10 | // Allow looking up with the same function with either slug or id 11 | const query = args.id.length > 10 ? { id: args.id } : { slug: args.id } 12 | const user = await db.user.findUnique({ where: query }) 13 | 14 | return user 15 | } 16 | 17 | interface CreateUserArgs { 18 | input: Prisma.UserCreateInput 19 | } 20 | 21 | export const createUser = ({ input }: CreateUserArgs) => { 22 | input.id = cuid() + ":user" 23 | input.slug = cuid.slug() 24 | 25 | return db.user.create({ 26 | data: input, 27 | }) 28 | } 29 | 30 | export const updateUser = (args: { id: string; input: Prisma.UserUpdateInput }) => { 31 | return db.user.update({ 32 | data: args.input, 33 | where: { id: args.id }, 34 | }) 35 | } 36 | 37 | export const deleteUser = ({ id }: { id: string }) => { 38 | return db.user.delete({ 39 | where: { id }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /.env.defaults: -------------------------------------------------------------------------------- 1 | # These environment variables will be used by default if you do not create any 2 | # yourself in .env. This file should be safe to check into your version control 3 | # system. Any custom values should go in .env and .env should *not* be checked 4 | # into version control. 5 | 6 | # schema.prisma defaults 7 | DATABASE_URL=file:./dev.db 8 | 9 | # location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set) 10 | # TEST_DATABASE_URL=file:./.redwood/test.db 11 | 12 | # disables Prisma CLI update notifier 13 | PRISMA_HIDE_UPDATE_MESSAGE=true 14 | 15 | 16 | # Option to override the current environment's default api-side log level 17 | # See: https://redwoodjs.com/docs/logger for level options: 18 | # trace | info | debug | warn | error | silent 19 | # LOG_LEVEL=debug 20 | 21 | # Sets an app-specific secret used to sign and verify your own app's webhooks. 22 | # For example if you schedule a cron job with a signed payload that later will 23 | # then invoke your api-side webhook function you will use this secret to sign and the verify. 24 | # Important: Please change this default to a strong password or other secret 25 | WEBHOOK_SECRET=THIS_IS_NOT_SECRET_PLEASE_CHANGE 26 | -------------------------------------------------------------------------------- /web/src/components/User/NewUser/NewUser.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@redwoodjs/web" 2 | import { toast } from "@redwoodjs/web/toast" 3 | import { navigate, routes } from "@redwoodjs/router" 4 | import UserForm from "src/components/User/UserForm" 5 | 6 | const CREATE_USER_MUTATION = gql` 7 | mutation CreateUserMutation($input: CreateUserInput!) { 8 | createUser(input: $input) { 9 | id 10 | } 11 | } 12 | ` 13 | 14 | const NewUser = () => { 15 | const [createUser, { loading, error }] = useMutation(CREATE_USER_MUTATION, { 16 | onCompleted: () => { 17 | toast.success("User created") 18 | navigate(routes.users()) 19 | }, 20 | onError: (error) => { 21 | toast.error(error.message) 22 | }, 23 | }) 24 | 25 | const onSave = (input) => { 26 | createUser({ variables: { input } }) 27 | } 28 | 29 | return ( 30 |
31 |
32 |

New User

33 |
34 |
35 | 36 |
37 |
38 | ) 39 | } 40 | 41 | export default NewUser 42 | -------------------------------------------------------------------------------- /web/src/components/User/User/User.tsx: -------------------------------------------------------------------------------- 1 | import { Link, routes } from "@redwoodjs/router" 2 | import { DeleteButton } from "src/components/DeleteButton" 3 | 4 | const User = ({ user }) => { 5 | return ( 6 | <> 7 |
8 |
9 |

User {user.id} Detail

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
id{user.id}
slug{user.slug}
Email{user.email}
Name{user.name}
31 |
32 | 38 | 39 | ) 40 | } 41 | 42 | export default User 43 | -------------------------------------------------------------------------------- /api/src/services/objectIdentification.ts: -------------------------------------------------------------------------------- 1 | import { deleteUser, user } from "./users/users" 2 | import type { Plugin } from "@envelop/types" 3 | 4 | const nodeTypes = { 5 | ":user": { 6 | type: "User", 7 | get: user, 8 | delete: deleteUser, 9 | }, 10 | } 11 | 12 | const keys = Object.keys(nodeTypes) 13 | 14 | export const node = (args: { id: string }) => { 15 | for (const key of keys) { 16 | if (args.id.endsWith(key)) { 17 | return nodeTypes[key].get({ id: args.id }) 18 | } 19 | } 20 | 21 | throw new Error(`Did not find a resolver for node with ${args.id}`) 22 | } 23 | 24 | export const deleteNode = (args) => { 25 | for (const key of keys) { 26 | if (args.id.endsWith(key)) { 27 | return nodeTypes[key].delete({ id: args.id }) 28 | } 29 | } 30 | throw new Error(`Did not find a resolver for deleteNode with ${args.id}`) 31 | } 32 | 33 | export const createNodeResolveEnvelopPlugin = (): Plugin => { 34 | return { 35 | onSchemaChange({ schema }) { 36 | const node: { resolveType?: (obj: { id: string }) => string } = schema.getType("Node") as unknown 37 | node.resolveType = (obj) => { 38 | for (const key of keys) { 39 | if (obj.id.endsWith(key)) { 40 | return nodeTypes[key].type 41 | } 42 | } 43 | throw new Error(`Did not find a resolver for deleteNode with ${args.id}`) 44 | } 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /api/src/services/users/users.test.ts: -------------------------------------------------------------------------------- 1 | import { users, user, createUser, updateUser, deleteUser } from "./users" 2 | import type { StandardScenario } from "./users.scenarios" 3 | 4 | describe("users", () => { 5 | scenario("returns all users", async (scenario: StandardScenario) => { 6 | const result = await users() 7 | 8 | expect(result.length).toEqual(Object.keys(scenario.user).length) 9 | }) 10 | 11 | scenario("returns a single user", async (scenario: StandardScenario) => { 12 | const result = await user({ id: scenario.user.one.id }) 13 | 14 | expect(result).toEqual(scenario.user.one) 15 | }) 16 | 17 | scenario("creates a user", async () => { 18 | const result = await createUser({ 19 | input: { email: "String7075649" }, 20 | }) 21 | 22 | expect(result.email).toEqual("String7075649") 23 | }) 24 | 25 | scenario("updates a user", async (scenario: StandardScenario) => { 26 | const original = await user({ id: scenario.user.one.id }) 27 | const result = await updateUser({ 28 | id: original.id, 29 | input: { email: "String81914542" }, 30 | }) 31 | 32 | expect(result.email).toEqual("String81914542") 33 | }) 34 | 35 | scenario("deletes a user", async (scenario: StandardScenario) => { 36 | const original = await deleteUser({ id: scenario.user.one.id }) 37 | const result = await user({ id: original.id }) 38 | 39 | expect(result).toEqual(null) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /web/src/pages/NotFoundPage/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
3 |