├── apps ├── .gitkeep ├── api │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.ts │ │ │ └── environment.prod.ts │ │ ├── app │ │ │ ├── resources │ │ │ │ ├── authentication │ │ │ │ │ ├── dto │ │ │ │ │ │ └── login.input.ts │ │ │ │ │ ├── authentication.module.ts │ │ │ │ │ ├── authentication.resolver.ts │ │ │ │ │ └── authentication.service.ts │ │ │ │ └── user │ │ │ │ │ ├── user.module.ts │ │ │ │ │ ├── user.service.ts │ │ │ │ │ └── user.resolver.ts │ │ │ ├── guards │ │ │ │ └── auth-guards │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── check-auth.guard.ts │ │ │ │ │ ├── strategy │ │ │ │ │ ├── local.strategy.ts │ │ │ │ │ └── jwt.strategy.ts │ │ │ │ │ └── set-auth.guard.ts │ │ │ └── app.module.ts │ │ └── main.ts │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tsconfig.app.json │ ├── .eslintrc.json │ ├── jest.config.ts │ └── project.json ├── web │ ├── public │ │ └── .gitkeep │ ├── pages │ │ ├── index.module.css │ │ ├── _app.tsx │ │ ├── users │ │ │ └── index.tsx │ │ ├── sign-up │ │ │ └── index.tsx │ │ ├── login │ │ │ └── index.tsx │ │ ├── styles.css │ │ └── index.tsx │ ├── index.d.ts │ ├── api │ │ ├── shared.ts │ │ ├── user │ │ │ ├── user.gql.ts │ │ │ └── user.gql.gen.ts │ │ ├── auth │ │ │ ├── auth.gql.ts │ │ │ └── auth.gql.gen.ts │ │ ├── client-api.tsx │ │ └── server-api.ts │ ├── next-env.d.ts │ ├── specs │ │ └── index.spec.tsx │ ├── jest.config.ts │ ├── tsconfig.spec.json │ ├── next.config.js │ ├── tsconfig.json │ ├── .eslintrc.json │ └── project.json └── web-e2e │ ├── src │ ├── support │ │ ├── app.po.ts │ │ ├── index.ts │ │ └── commands.ts │ ├── fixtures │ │ └── example.json │ └── integration │ │ └── app.spec.ts │ ├── tsconfig.json │ ├── .eslintrc.json │ ├── cypress.json │ └── project.json ├── libs ├── .gitkeep ├── api │ ├── data-access-db │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ │ ├── migrations │ │ │ │ ├── migration_lock.toml │ │ │ │ ├── 20220528104909_user_uptates │ │ │ │ │ └── migration.sql │ │ │ │ └── 20220528104245_init │ │ │ │ │ └── migration.sql │ │ │ │ ├── db-service.ts │ │ │ │ └── schema.prisma │ │ ├── package.json │ │ ├── tsconfig.spec.json │ │ ├── README.md │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.json │ │ ├── .eslintrc.json │ │ ├── jest.config.ts │ │ └── project.json │ └── generated │ │ └── db-types │ │ ├── .babelrc │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── README.md │ │ ├── .eslintrc.json │ │ ├── tsconfig.spec.json │ │ ├── jest.config.ts │ │ ├── project.json │ │ └── src │ │ └── index.ts └── client │ └── generated │ └── graphql-types │ ├── src │ ├── index.ts │ └── lib │ │ └── types.ts │ ├── .babelrc │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── README.md │ ├── .eslintrc.json │ ├── tsconfig.spec.json │ ├── jest.config.ts │ └── project.json ├── tools ├── generators │ └── .gitkeep ├── readme │ └── schema.png ├── tsconfig.tools.json ├── docker-env │ └── dev.docker-compose.yml └── gql-codegen │ └── gql-codegen.yml ├── babel.config.json ├── .env ├── .prettierignore ├── jest.preset.js ├── .prettierrc ├── jest.config.ts ├── .editorconfig ├── workspace.json ├── .gitignore ├── tsconfig.base.json ├── nx.json ├── .eslintrc.json ├── README.md └── package.json /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/api/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | } 3 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/db-service' 2 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/types' 2 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1') 2 | -------------------------------------------------------------------------------- /tools/readme/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoqua/full-stack/HEAD/tools/readme/schema.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5433/postgres?schema=public 2 | 3 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | } 4 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nrwl/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /libs/api/data-access-db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@full-stack/api/data-access-db", 3 | "version": "0.0.1", 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/lib/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 = "postgresql" -------------------------------------------------------------------------------- /apps/web/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any 4 | export const ReactComponent: any 5 | export default content 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/api/shared.ts: -------------------------------------------------------------------------------- 1 | import { CombinedError } from 'urql' 2 | 3 | export const isAuthError = (error?: CombinedError) => { 4 | return error?.graphQLErrors.some((error) => error.message === 'Unauthorized') 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/authentication/dto/login.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, PickType } from '@nestjs/graphql' 2 | import { UserCreateInput } from '@full-stack/api/generated/db-types' 3 | 4 | @InputType() 5 | export class LoginInput extends PickType(UserCreateInput, ['email', 'password']) {} 6 | -------------------------------------------------------------------------------- /apps/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/web-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /libs/api/data-access-db/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/lib/migrations/20220528104909_user_uptates/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /libs/api/data-access-db/README.md: -------------------------------------------------------------------------------- 1 | # api-data-access-db 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build api-data-access-db` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test api-data-access-db` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /libs/api/data-access-db/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [], 7 | "target": "es6" 8 | }, 9 | "include": ["**/*.ts"], 10 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/app/guards/auth-guards/types.ts: -------------------------------------------------------------------------------- 1 | import { FastifyRequest, FastifyReply } from 'fastify' 2 | import { User } from '@full-stack/api/generated/db-types' 3 | 4 | export interface IUserContext { 5 | reply: FastifyReply 6 | request: FastifyRequest 7 | user: User 8 | } 9 | 10 | export type UserJwtPayload = false | { id: string } 11 | -------------------------------------------------------------------------------- /apps/web/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | 4 | import Index from '../pages/index' 5 | 6 | describe('Index', () => { 7 | it('should render successfully', () => { 8 | const { baseElement } = render() 9 | expect(baseElement).toBeTruthy() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/lib/migrations/20220528104245_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "name" TEXT, 6 | 7 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 12 | -------------------------------------------------------------------------------- /libs/api/data-access-db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tools/docker-env/dev.docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | db: 4 | image: postgres 5 | ports: 6 | - '5433:5432' 7 | volumes: 8 | - full-stack-db:/var/lib/postgresql/data 9 | environment: 10 | POSTGRES_PASSWORD: mysecretpassword 11 | PGDATA: /var/lib/postgresql/data/data 12 | 13 | volumes: 14 | full-stack-db: 15 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"], 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/api/user/user.gql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'urql' 2 | 3 | const GET_USER = gql` 4 | query GetUser($args: UserWhereUniqueInput!) { 5 | user(where: $args) { 6 | name 7 | email 8 | } 9 | } 10 | ` 11 | 12 | const GET_USERS = gql` 13 | query GetUsers { 14 | users { 15 | id 16 | name 17 | email 18 | } 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/README.md: -------------------------------------------------------------------------------- 1 | # api-generated-db-types 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test api-generated-db-types` to execute the unit tests via [Jest](https://jestjs.io). 8 | 9 | ## Running lint 10 | 11 | Run `nx lint api-generated-db-types` to execute the lint via [ESLint](https://eslint.org/). 12 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { UserService } from './user.service' 3 | import { UserResolver } from './user.resolver' 4 | import { DbService } from '@full-stack/api/data-access-db' 5 | 6 | @Module({ 7 | providers: [UserResolver, UserService, DbService], 8 | exports: [UserService] 9 | }) 10 | export class UserModule {} 11 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/README.md: -------------------------------------------------------------------------------- 1 | # client-generated-graphql-types 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test client-generated-graphql-types` to execute the unit tests via [Jest](https://jestjs.io). 8 | 9 | ## Running lint 10 | 11 | Run `nx lint client-generated-graphql-types` to execute the lint via [ESLint](https://eslint.org/). 12 | -------------------------------------------------------------------------------- /apps/web/api/auth/auth.gql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'urql' 2 | 3 | const SIGN_UP = gql` 4 | mutation SignUp($args: LoginInput!) { 5 | signUp(signUpInput: $args) { 6 | id 7 | name 8 | email 9 | } 10 | } 11 | ` 12 | 13 | const LOGIN = gql` 14 | mutation Login($args: LoginInput!) { 15 | login(loginInput: $args) { 16 | id 17 | name 18 | email 19 | } 20 | } 21 | ` 22 | -------------------------------------------------------------------------------- /libs/api/data-access-db/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'web', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../coverage/apps/web', 11 | }; 12 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/workspace-schema.json", 3 | "version": 2, 4 | "projects": { 5 | "api": "apps/api", 6 | "api-data-access-db": "libs/api/data-access-db", 7 | "api-generated-db-types": "libs/api/generated/db-types", 8 | "client-generated-graphql-types": "libs/client/generated/graphql-types", 9 | "web": "apps/web", 10 | "web-e2e": "apps/web-e2e" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'api', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]s$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'html'], 15 | coverageDirectory: '../../coverage/apps/api', 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app' 2 | import Head from 'next/head' 3 | import './styles.css' 4 | 5 | function CustomApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | Welcome to web! 10 | 11 |
12 | 13 |
14 | 15 | ) 16 | } 17 | 18 | export default CustomApp 19 | -------------------------------------------------------------------------------- /apps/web-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/web-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "supportFile": "./src/support/index.ts", 7 | "pluginsFile": false, 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/web-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/web-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/web-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po' 2 | 3 | describe('web', () => { 4 | beforeEach(() => cy.visit('/')) 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword') 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome web') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /libs/api/data-access-db/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'api-data-access-db', 4 | preset: '../../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]s$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'html'], 15 | coverageDirectory: '../../../coverage/libs/api/data-access-db', 16 | }; 17 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/lib/db-service.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common' 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | @Injectable() 5 | export class DbService extends PrismaClient implements OnModuleInit { 6 | async onModuleInit() { 7 | await this.$connect() 8 | } 9 | 10 | async enableShutdownHooks(app: INestApplication) { 11 | this.$on('beforeExit', async () => { 12 | await app.close() 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "**/*.test.ts", 12 | "**/*.spec.ts", 13 | "**/*.test.tsx", 14 | "**/*.spec.tsx", 15 | "**/*.test.js", 16 | "**/*.spec.js", 17 | "**/*.test.jsx", 18 | "**/*.spec.jsx", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'api-generated-db-types', 4 | preset: '../../../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]sx?$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 15 | coverageDirectory: '../../../../coverage/libs/api/generated/db-types', 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withNx = require('@nrwl/next/plugins/with-nx') 3 | 4 | /** 5 | * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions} 6 | **/ 7 | const nextConfig = { 8 | env: { 9 | API_HOST: process.env.API_HOST 10 | }, 11 | nx: { 12 | // Set this to true if you would like to to use SVGR 13 | // See: https://github.com/gregberge/svgr 14 | svgr: false 15 | }, 16 | swcMinify: true 17 | } 18 | 19 | module.exports = withNx(nextConfig) 20 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'client-generated-graphql-types', 4 | preset: '../../../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | transform: { 12 | '^.+\\.[tj]sx?$': 'ts-jest', 13 | }, 14 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 15 | coverageDirectory: '../../../../coverage/libs/client/generated/graphql-types', 16 | }; 17 | -------------------------------------------------------------------------------- /tools/gql-codegen/gql-codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'dist/apps/api/autogenerated-schema.gql' 3 | 4 | documents: 5 | - 'apps/**/*gql.ts' 6 | - 'libs/**/*gql.ts' 7 | 8 | generates: 9 | libs/client/generated/graphql-types/src/lib/types.ts: 10 | - typescript 11 | libs/: 12 | preset: near-operation-file 13 | presetConfig: 14 | extension: .gen.ts 15 | baseTypesPath: '~@full-stack/client/generated/graphql-types' 16 | plugins: 17 | - typescript-operations 18 | - typescript-urql 19 | config: 20 | withHooks: true 21 | gqlImport: 'urql#gql' 22 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "types": ["jest", "node"] 15 | }, 16 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], 17 | "exclude": ["node_modules", "jest.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nrwl/nx/react-typescript", 4 | "../../.eslintrc.json", 5 | "next", 6 | "next/core-web-vitals" 7 | ], 8 | "ignorePatterns": ["!**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": ["error", "apps/web/pages"] 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["*.js", "*.jsx"], 22 | "rules": {} 23 | } 24 | ], 25 | "env": { 26 | "jest": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .*.env 42 | -------------------------------------------------------------------------------- /apps/api/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import * as path from 'node:path' 3 | import { GraphQLModule } from '@nestjs/graphql' 4 | import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius' 5 | import { UserModule } from './resources/user/user.module' 6 | import { AuthenticationModule } from './resources/authentication/authentication.module' 7 | 8 | @Module({ 9 | imports: [ 10 | GraphQLModule.forRoot({ 11 | driver: MercuriusDriver, 12 | autoSchemaFile: path.join(__dirname, './autogenerated-schema.gql'), 13 | graphiql: true 14 | }), 15 | UserModule, 16 | AuthenticationModule 17 | ], 18 | controllers: [], 19 | providers: [] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "libs/api/generated/db-types/src", 4 | "projectType": "library", 5 | "targets": { 6 | "lint": { 7 | "executor": "@nrwl/linter:eslint", 8 | "outputs": ["{options.outputFile}"], 9 | "options": { 10 | "lintFilePatterns": ["libs/api/generated/db-types/**/*.ts"] 11 | } 12 | }, 13 | "test": { 14 | "executor": "@nrwl/jest:jest", 15 | "outputs": ["coverage/libs/api/generated/db-types"], 16 | "options": { 17 | "jestConfig": "libs/api/generated/db-types/jest.config.ts", 18 | "passWithNoTests": true 19 | } 20 | } 21 | }, 22 | "tags": ["scope:api"] 23 | } 24 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "libs/client/generated/graphql-types/src", 4 | "projectType": "library", 5 | "targets": { 6 | "lint": { 7 | "executor": "@nrwl/linter:eslint", 8 | "outputs": ["{options.outputFile}"], 9 | "options": { 10 | "lintFilePatterns": ["libs/client/generated/graphql-types/**/*.ts"] 11 | } 12 | }, 13 | "test": { 14 | "executor": "@nrwl/jest:jest", 15 | "outputs": ["coverage/libs/client/generated/graphql-types"], 16 | "options": { 17 | "jestConfig": "libs/client/generated/graphql-types/jest.config.ts", 18 | "passWithNoTests": true 19 | } 20 | } 21 | }, 22 | "tags": ["scope:client"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "apps/web-e2e/src", 4 | "projectType": "application", 5 | "targets": { 6 | "e2e": { 7 | "executor": "@nrwl/cypress:cypress", 8 | "options": { 9 | "cypressConfig": "apps/web-e2e/cypress.json", 10 | "devServerTarget": "web:serve:development" 11 | }, 12 | "configurations": { 13 | "production": { 14 | "devServerTarget": "web:serve:production" 15 | } 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nrwl/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["apps/web-e2e/**/*.{js,ts}"] 23 | } 24 | } 25 | }, 26 | "tags": [], 27 | "implicitDependencies": ["web"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/authentication/authentication.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AuthenticationService } from './authentication.service' 3 | import { AuthenticationResolver } from './authentication.resolver' 4 | import { UserModule } from '../user/user.module' 5 | import { LocalStrategy } from '../../guards/auth-guards/strategy/local.strategy' 6 | import { JwtModule } from '@nestjs/jwt' 7 | import { JwtStrategy } from '../../guards/auth-guards/strategy/jwt.strategy' 8 | 9 | @Module({ 10 | imports: [ 11 | UserModule, 12 | JwtModule.register({ 13 | secret: process.env.JWT_SECRET, 14 | signOptions: { expiresIn: Number(process.env.JWT_EXPIRES_SECONDS) } 15 | }) 16 | ], 17 | providers: [AuthenticationResolver, AuthenticationService, LocalStrategy, JwtStrategy] 18 | }) 19 | export class AuthenticationModule {} 20 | -------------------------------------------------------------------------------- /apps/api/src/app/guards/auth-guards/check-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport' 2 | import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common' 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | 5 | @Injectable() 6 | export class CheckAuthGuard extends AuthGuard('jwt') { 7 | getRequest(context: ExecutionContext) { 8 | const context_ = GqlExecutionContext.create(context) 9 | return context_.getContext().req 10 | } 11 | 12 | handleRequest(error, user, info, context) { 13 | if (!user || info || error) { 14 | const context_ = GqlExecutionContext.create(context) 15 | const reply = context_.getContext().reply 16 | 17 | reply.setCookie('token', '') 18 | reply.setCookie('token-expires', '') 19 | 20 | throw error || new UnauthorizedException() 21 | } 22 | 23 | return user 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/api/src/app/guards/auth-guards/strategy/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport' 2 | import { Strategy } from 'passport-local' 3 | import { AuthenticationService } from '../../../resources/authentication/authentication.service' 4 | import { User } from '@full-stack/api/generated/db-types' 5 | import { Injectable, UnauthorizedException } from '@nestjs/common' 6 | 7 | @Injectable() 8 | export class LocalStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly authService: AuthenticationService) { 10 | super({ usernameField: 'email', passwordField: 'password' }) // email will be passed to validate function 11 | } 12 | 13 | async validate(email: string, password: string): Promise { 14 | const user = this.authService.validateUser(email, password) 15 | 16 | if (!user) { 17 | throw new UnauthorizedException() 18 | } 19 | 20 | return user 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { DbService } from '@full-stack/api/data-access-db' 3 | import { 4 | CreateOneUserArgs, 5 | FindUniqueUserArgs, 6 | UpdateOneUserArgs 7 | } from '@full-stack/api/generated/db-types' 8 | 9 | @Injectable() 10 | export class UserService { 11 | constructor(private database: DbService) {} 12 | 13 | findOne(findUserArguments: FindUniqueUserArgs) { 14 | return this.database.user.findUnique(findUserArguments) 15 | } 16 | 17 | findAll() { 18 | return this.database.user.findMany() 19 | } 20 | 21 | create(userCreateArguments: CreateOneUserArgs) { 22 | return this.database.user.create(userCreateArguments) 23 | } 24 | 25 | update(userUpdateInput: UpdateOneUserArgs) { 26 | return this.database.user.update(userUpdateInput) 27 | } 28 | 29 | remove(removeUserArguments: FindUniqueUserArgs) { 30 | return this.database.user.delete(removeUserArguments) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@full-stack/api/data-access-db": ["libs/api/data-access-db/src/index.ts"], 19 | "@full-stack/api/generated/db-types": ["libs/api/generated/db-types/src/index.ts"], 20 | "@full-stack/client/data-access/graphql-types": [ 21 | "libs/client/data-access/graphql-types/src/index.ts" 22 | ], 23 | "@full-stack/client/generated/graphql-types": [ 24 | "libs/client/generated/graphql-types/src/index.ts" 25 | ] 26 | } 27 | }, 28 | "exclude": ["node_modules", "tmp"] 29 | } 30 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/authentication/authentication.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Context, Mutation, Resolver } from '@nestjs/graphql' 2 | import { AuthenticationService } from './authentication.service' 3 | import { User } from '@full-stack/api/generated/db-types' 4 | import { LoginInput } from './dto/login.input' 5 | import { SetAuthGuard } from '../../guards/auth-guards/set-auth.guard' 6 | import { UseGuards } from '@nestjs/common' 7 | import { IUserContext } from '../../guards/auth-guards/types' 8 | 9 | @Resolver(() => User) 10 | export class AuthenticationResolver { 11 | constructor(private readonly authenticationService: AuthenticationService) {} 12 | 13 | @UseGuards(SetAuthGuard) 14 | @Mutation(() => User) 15 | login(@Args('loginInput') loginInput: LoginInput, @Context() context: IUserContext) { 16 | const { user } = context 17 | 18 | return this.authenticationService.login(user) 19 | } 20 | 21 | @Mutation(() => User) 22 | signUp(@Args('signUpInput') signUpInput: LoginInput) { 23 | return this.authenticationService.signUp(signUpInput) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "npmScope": "full-stack", 4 | "affected": { 5 | "defaultBase": "main" 6 | }, 7 | "cli": { 8 | "defaultCollection": "@nrwl/nest" 9 | }, 10 | "implicitDependencies": { 11 | "package.json": { 12 | "dependencies": "*", 13 | "devDependencies": "*" 14 | }, 15 | ".eslintrc.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "nx/tasks-runners/default", 20 | "options": { 21 | "cacheableOperations": ["build", "lint", "test", "e2e"] 22 | } 23 | } 24 | }, 25 | "targetDependencies": { 26 | "build": [ 27 | { 28 | "target": "build", 29 | "projects": "dependencies" 30 | } 31 | ] 32 | }, 33 | "defaultProject": "api", 34 | "generators": { 35 | "@nrwl/react": { 36 | "application": { 37 | "babel": true 38 | } 39 | }, 40 | "@nrwl/next": { 41 | "application": { 42 | "style": "css", 43 | "linter": "eslint" 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/src/app/resources/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { UserService } from '../user/user.service' 3 | import { LoginInput } from './dto/login.input' 4 | 5 | import * as bcrypt from 'bcrypt' 6 | import { User } from '@full-stack/api/generated/db-types' 7 | 8 | @Injectable() 9 | export class AuthenticationService { 10 | constructor(private readonly userService: UserService) {} 11 | 12 | async validateUser(email: string, password: string): Promise { 13 | const user = await this.userService.findOne({ where: { email } }) 14 | if (!user) return null 15 | 16 | const isMatch = await bcrypt.compare(password, user.password) 17 | if (!isMatch) return null 18 | 19 | return user 20 | } 21 | 22 | login(user: User) { 23 | return user 24 | } 25 | 26 | async signUp(signUpInput: LoginInput) { 27 | const { email, password: plainPassword } = signUpInput 28 | const password = await bcrypt.hash(plainPassword, 10) 29 | 30 | return this.userService.create({ data: { email, password } }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | import { GetUsersDocument, useGetUsersQuery } from '../../api/user/user.gql.gen' 2 | import { withApi } from '../../api/client-api' 3 | import { GetServerSidePropsContext } from 'next' 4 | import { serverQuery } from '../../api/server-api' 5 | 6 | export const getServerSideProps = (context: GetServerSidePropsContext) => { 7 | return serverQuery(GetUsersDocument, {}, context) 8 | } 9 | 10 | export const UsersPage = () => { 11 | const [data] = useGetUsersQuery({ variables: {} }) 12 | 13 | return ( 14 |
23 |
24 |

Users:

25 |
26 | {data?.data?.users.map((user) => ( 27 |
28 |

Name: {user.name}

29 |

Email: {user.email}

30 |

ID: {user.id}

31 |
32 |
33 | ))} 34 |
35 |
36 | ) 37 | } 38 | 39 | export default withApi(UsersPage) 40 | -------------------------------------------------------------------------------- /libs/api/data-access-db/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "libs/api/data-access-db/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/libs/api/data-access-db", 11 | "tsConfig": "libs/api/data-access-db/tsconfig.lib.json", 12 | "packageJson": "libs/api/data-access-db/package.json", 13 | "main": "libs/api/data-access-db/src/index.ts", 14 | "assets": ["libs/api/data-access-db/*.md"] 15 | } 16 | }, 17 | "lint": { 18 | "executor": "@nrwl/linter:eslint", 19 | "outputs": ["{options.outputFile}"], 20 | "options": { 21 | "lintFilePatterns": ["libs/api/data-access-db/**/*.ts"] 22 | } 23 | }, 24 | "test": { 25 | "executor": "@nrwl/jest:jest", 26 | "outputs": ["coverage/libs/api/data-access-db"], 27 | "options": { 28 | "jestConfig": "libs/api/data-access-db/jest.config.ts", 29 | "passWithNoTests": true 30 | } 31 | } 32 | }, 33 | "tags": ["scope:api"] 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/api/client-api.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { 3 | cacheExchange, 4 | createClient, 5 | errorExchange, 6 | fetchExchange, 7 | Provider, 8 | ssrExchange 9 | } from 'urql' 10 | import { isAuthError } from './shared' 11 | const isClient = typeof window !== 'undefined' 12 | 13 | export const ssrCache = ssrExchange({ 14 | isClient, 15 | initialState: isClient ? window['__URQL_DATA__'] : undefined 16 | }) 17 | 18 | export const clientApi = createClient({ 19 | url: `http://${process.env.API_HOST}:3333/graphql`, 20 | fetchOptions: { credentials: 'include' }, 21 | exchanges: [ 22 | cacheExchange, 23 | ssrCache, 24 | errorExchange({ 25 | onError: (error) => { 26 | if (isAuthError(error)) { 27 | console.log('//TODO: log off') 28 | } 29 | } 30 | }), 31 | fetchExchange 32 | ] 33 | }) 34 | 35 | export const withApi = (Component: FC) => { 36 | return function ApiWrappedComponent({ ...properties }) { 37 | if (properties.urqlState) { 38 | ssrCache.restoreData(properties.urqlState) 39 | } 40 | 41 | return ( 42 | 43 | 44 | 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/src/app/guards/auth-guards/strategy/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-jwt' 2 | import { PassportStrategy } from '@nestjs/passport' 3 | import { Injectable } from '@nestjs/common' 4 | import { FastifyRequest } from 'fastify' 5 | import { UserJwtPayload } from '../types' 6 | 7 | // some jwt best practices https://www.rfc-editor.org/rfc/rfc8725.html 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor() { 12 | super({ 13 | jwtFromRequest: cookieExtractor, 14 | ignoreExpiration: false, 15 | secretOrKey: process.env.JWT_SECRET, 16 | jsonWebTokenOptions: { algorithms: ['HS256'] } 17 | }) 18 | } 19 | 20 | async validate(payload: { sub?: string }): Promise { 21 | if (!payload.sub) return false 22 | 23 | return { id: payload.sub } 24 | } 25 | } 26 | 27 | const cookieExtractor = (request: FastifyRequest): string | null => { 28 | const isCookieTokenExist = !!request?.cookies?.token 29 | if (!isCookieTokenExist) { 30 | console.log('Cookie not passed') // TODO: log 31 | return null 32 | } 33 | 34 | const unsignedCookieToken = request.unsignCookie(request.cookies.token) 35 | return unsignedCookieToken?.value || null 36 | } 37 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password) 22 | }) 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /apps/web/api/auth/auth.gql.gen.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '@full-stack/client/generated/graphql-types'; 2 | 3 | import { gql } from 'urql'; 4 | import * as Urql from 'urql'; 5 | export type Omit = Pick>; 6 | export type SignUpMutationVariables = Types.Exact<{ 7 | args: Types.LoginInput; 8 | }>; 9 | 10 | 11 | export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'User', id: string, name?: string | null, email: string } }; 12 | 13 | export type LoginMutationVariables = Types.Exact<{ 14 | args: Types.LoginInput; 15 | }>; 16 | 17 | 18 | export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'User', id: string, name?: string | null, email: string } }; 19 | 20 | 21 | export const SignUpDocument = gql` 22 | mutation SignUp($args: LoginInput!) { 23 | signUp(signUpInput: $args) { 24 | id 25 | name 26 | email 27 | } 28 | } 29 | `; 30 | 31 | export function useSignUpMutation() { 32 | return Urql.useMutation(SignUpDocument); 33 | }; 34 | export const LoginDocument = gql` 35 | mutation Login($args: LoginInput!) { 36 | login(loginInput: $args) { 37 | id 38 | name 39 | email 40 | } 41 | } 42 | `; 43 | 44 | export function useLoginMutation() { 45 | return Urql.useMutation(LoginDocument); 46 | }; -------------------------------------------------------------------------------- /apps/web/api/user/user.gql.gen.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '@full-stack/client/generated/graphql-types'; 2 | 3 | import { gql } from 'urql'; 4 | import * as Urql from 'urql'; 5 | export type Omit = Pick>; 6 | export type GetUserQueryVariables = Types.Exact<{ 7 | args: Types.UserWhereUniqueInput; 8 | }>; 9 | 10 | 11 | export type GetUserQuery = { __typename?: 'Query', user: { __typename?: 'User', name?: string | null, email: string } }; 12 | 13 | export type GetUsersQueryVariables = Types.Exact<{ [key: string]: never; }>; 14 | 15 | 16 | export type GetUsersQuery = { __typename?: 'Query', users: Array<{ __typename?: 'User', id: string, name?: string | null, email: string }> }; 17 | 18 | 19 | export const GetUserDocument = gql` 20 | query GetUser($args: UserWhereUniqueInput!) { 21 | user(where: $args) { 22 | name 23 | email 24 | } 25 | } 26 | `; 27 | 28 | export function useGetUserQuery(options: Omit, 'query'>) { 29 | return Urql.useQuery({ query: GetUserDocument, ...options }); 30 | }; 31 | export const GetUsersDocument = gql` 32 | query GetUsers { 33 | users { 34 | id 35 | name 36 | email 37 | } 38 | } 39 | `; 40 | 41 | export function useGetUsersQuery(options?: Omit, 'query'>) { 42 | return Urql.useQuery({ query: GetUsersDocument, ...options }); 43 | }; -------------------------------------------------------------------------------- /apps/api/src/app/resources/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' 2 | import { UserService } from './user.service' 3 | import { 4 | CreateOneUserArgs, 5 | FindUniqueUserArgs, 6 | UpdateOneUserArgs, 7 | User 8 | } from '@full-stack/api/generated/db-types' 9 | import { UseGuards } from '@nestjs/common' 10 | import { CheckAuthGuard } from '../../guards/auth-guards/check-auth.guard' 11 | 12 | @Resolver(() => User) 13 | export class UserResolver { 14 | constructor(private readonly userService: UserService) {} 15 | 16 | @UseGuards(CheckAuthGuard) 17 | @Query(() => User) 18 | user(@Args() findUserArguments: FindUniqueUserArgs) { 19 | return this.userService.findOne(findUserArguments) 20 | } 21 | 22 | @UseGuards(CheckAuthGuard) 23 | @Query(() => [User]) 24 | users() { 25 | return this.userService.findAll() 26 | } 27 | 28 | @UseGuards(CheckAuthGuard) 29 | @Mutation(() => User) 30 | createUser(@Args() userCreateArguments: CreateOneUserArgs) { 31 | return this.userService.create(userCreateArguments) 32 | } 33 | 34 | @UseGuards(CheckAuthGuard) 35 | @Mutation(() => User) 36 | updateUser(@Args() userUpdateInput: UpdateOneUserArgs) { 37 | return this.userService.update(userUpdateInput) 38 | } 39 | 40 | @UseGuards(CheckAuthGuard) 41 | @Mutation(() => User) 42 | removeUser(@Args() removeUserArguments: FindUniqueUserArgs) { 43 | return this.userService.remove(removeUserArguments) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /libs/api/data-access-db/src/lib/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | generator nestgraphql { 11 | provider = "node node_modules/prisma-nestjs-graphql" 12 | output = "../../../generated/db-types/src" 13 | purgeOutput = false 14 | combineScalarFilters = true 15 | emitSingle = true 16 | noAtomicOperations = true 17 | // field validator 18 | fields_Validator_input = true 19 | fields_Validator_output = true 20 | fields_Validator_model = true 21 | fields_Validator_from = "class-validator" 22 | // Args where|data nested validator 23 | decorate_1_type = "*Args" 24 | decorate_1_field = "@(data|where)" 25 | decorate_1_name = ValidateNested 26 | decorate_1_from = "class-validator" 27 | decorate_1_arguments = "['{ each: true }']" 28 | } 29 | 30 | model User { 31 | id Int @id @default(autoincrement()) 32 | 33 | /// @Validator.IsEmail() 34 | email String @unique 35 | 36 | /// @Validator.IsString() 37 | /// @Validator.MaxLength(100) 38 | /// @Validator.MinLength(3) 39 | name String? 40 | 41 | /// @Validator.IsString() 42 | /// @Validator.MaxLength(100) 43 | /// @Validator.MinLength(8) 44 | /// @HideField() 45 | password String 46 | } 47 | -------------------------------------------------------------------------------- /apps/api/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "apps/api/src", 4 | "projectType": "application", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/node:webpack", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/apps/api", 11 | "main": "apps/api/src/main.ts", 12 | "tsConfig": "apps/api/tsconfig.app.json", 13 | "assets": ["apps/api/src/assets"] 14 | }, 15 | "configurations": { 16 | "production": { 17 | "optimization": true, 18 | "extractLicenses": true, 19 | "inspect": false, 20 | "fileReplacements": [ 21 | { 22 | "replace": "apps/api/src/environments/environment.ts", 23 | "with": "apps/api/src/environments/environment.prod.ts" 24 | } 25 | ] 26 | } 27 | } 28 | }, 29 | "serve": { 30 | "executor": "@nrwl/node:node", 31 | "options": { 32 | "buildTarget": "api:build" 33 | } 34 | }, 35 | "lint": { 36 | "executor": "@nrwl/linter:eslint", 37 | "outputs": ["{options.outputFile}"], 38 | "options": { 39 | "lintFilePatterns": ["apps/api/**/*.ts"] 40 | } 41 | }, 42 | "test": { 43 | "executor": "@nrwl/jest:jest", 44 | "outputs": ["coverage/apps/api"], 45 | "options": { 46 | "jestConfig": "apps/api/jest.config.ts", 47 | "passWithNoTests": true 48 | } 49 | } 50 | }, 51 | "tags": ["scope:api"] 52 | } 53 | -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger, ValidationPipe } from '@nestjs/common' 2 | import { NestFactory } from '@nestjs/core' 3 | import { AppModule } from './app/app.module' 4 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify' 5 | import helmet from 'helmet' 6 | import fastifyCookie from '@fastify/cookie' 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule, new FastifyAdapter()) 10 | const globalPrefix = 'graphql' 11 | const port = process.env.PORT || 3333 12 | const isProduction = process.env.NODE_ENV === 'production' 13 | 14 | const developmentContentSecurityPolicy = { 15 | directives: { 16 | scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", 'https://unpkg.com/'] 17 | } 18 | } 19 | 20 | await app.register(fastifyCookie, { secret: process.env.COOKIE_SECRET }) 21 | 22 | app.use( 23 | helmet({ 24 | contentSecurityPolicy: isProduction ? undefined : developmentContentSecurityPolicy 25 | }) 26 | ) 27 | 28 | app.enableCors({ 29 | origin: true, 30 | credentials: true 31 | }) 32 | 33 | app.useGlobalPipes( 34 | new ValidationPipe({ 35 | skipMissingProperties: true, 36 | //whitelist: true, 37 | transform: true, 38 | transformOptions: { enableImplicitConversion: true } 39 | }) 40 | ) 41 | await app.listen(port) 42 | 43 | Logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`) 44 | Logger.log(`🚀 Application is running on: http://localhost:${port}/graphiql`) 45 | } 46 | 47 | bootstrap() 48 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx", "unicorn", "prettier"], 5 | "overrides": [ 6 | { 7 | "extends": ["plugin:unicorn/recommended", "plugin:prettier/recommended"], 8 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 9 | "rules": { 10 | "prettier/prettier": ["error"], 11 | "unicorn/prevent-abbreviations": "warn", 12 | "unicorn/no-null": "off", 13 | "unicorn/prefer-module": ["warn"], 14 | "unicorn/no-abusive-eslint-disable": ["warn"], 15 | "@nrwl/nx/enforce-module-boundaries": [ 16 | "error", 17 | { 18 | "allow": [], 19 | // update depConstraints based on your tags 20 | "depConstraints": [ 21 | { 22 | "sourceTag": "scope:shared", 23 | "onlyDependOnLibsWithTags": ["scope:shared"] 24 | }, 25 | { 26 | "sourceTag": "scope:api", 27 | "onlyDependOnLibsWithTags": ["scope:shared", "scope:api"] 28 | }, 29 | { 30 | "sourceTag": "scope:client", 31 | "onlyDependOnLibsWithTags": ["scope:shared", "scope:client"] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | }, 38 | { 39 | "files": ["*.ts", "*.tsx"], 40 | "extends": ["plugin:@nrwl/nx/typescript"], 41 | "rules": {} 42 | }, 43 | { 44 | "files": ["*.js", "*.jsx"], 45 | "extends": ["plugin:@nrwl/nx/javascript"], 46 | "rules": {} 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/pages/sign-up/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useSignUpMutation } from '../../api/auth/auth.gql.gen' 3 | import { withApi } from '../../api/client-api' 4 | 5 | export const SignUp = () => { 6 | const [email, setEmail] = useState('') 7 | const [password, setPassword] = useState('') 8 | const [, signUp] = useSignUpMutation() 9 | 10 | const login = async (event) => { 11 | event.preventDefault() 12 | await signUp({ args: { email, password } }) 13 | } 14 | return ( 15 |
25 |
30 | 31 | setEmail(event.target.value)} 36 | /> 37 | 38 |
39 | 40 | 41 | setPassword(event.target.value)} 46 | /> 47 | 48 |
49 | 50 | 53 |
54 |
55 | ) 56 | } 57 | 58 | export default withApi(SignUp) 59 | -------------------------------------------------------------------------------- /apps/web/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useLoginMutation } from '../../api/auth/auth.gql.gen' 3 | import { withApi } from '../../api/client-api' 4 | 5 | export const LoginPage = () => { 6 | const [email, setEmail] = useState('') 7 | const [password, setPassword] = useState('') 8 | const [, login] = useLoginMutation() 9 | 10 | const submitLogin = async (event) => { 11 | event.preventDefault() 12 | await login({ args: { email, password } }) 13 | } 14 | return ( 15 |
25 |
30 | 31 | setEmail(event.target.value)} 36 | /> 37 | 38 |
39 | 40 | 41 | setPassword(event.target.value)} 46 | /> 47 | 48 |
49 | 50 | 53 |
54 |
55 | ) 56 | } 57 | 58 | export default withApi(LoginPage) 59 | -------------------------------------------------------------------------------- /apps/web/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "apps/web", 4 | "projectType": "application", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/next:build", 8 | "outputs": ["{options.outputPath}"], 9 | "defaultConfiguration": "production", 10 | "options": { 11 | "root": "apps/web", 12 | "outputPath": "dist/apps/web" 13 | }, 14 | "configurations": { 15 | "development": {}, 16 | "production": {} 17 | } 18 | }, 19 | "serve": { 20 | "executor": "@nrwl/next:server", 21 | "defaultConfiguration": "development", 22 | "options": { 23 | "buildTarget": "web:build", 24 | "dev": true 25 | }, 26 | "configurations": { 27 | "development": { 28 | "buildTarget": "web:build:development", 29 | "dev": true 30 | }, 31 | "production": { 32 | "buildTarget": "web:build:production", 33 | "dev": false 34 | } 35 | } 36 | }, 37 | "export": { 38 | "executor": "@nrwl/next:export", 39 | "options": { 40 | "buildTarget": "web:build:production" 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/apps/web"], 46 | "options": { 47 | "jestConfig": "apps/web/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | }, 51 | "lint": { 52 | "executor": "@nrwl/linter:eslint", 53 | "outputs": ["{options.outputFile}"], 54 | "options": { 55 | "lintFilePatterns": ["apps/web/**/*.{ts,tsx,js,jsx}"] 56 | } 57 | } 58 | }, 59 | "tags": ["scope:client"] 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/api/server-api.ts: -------------------------------------------------------------------------------- 1 | import { createClient, fetchExchange, ssrExchange, TypedDocumentNode } from 'urql' 2 | import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next' 3 | import { DocumentNode } from 'graphql' 4 | import { SSRData } from '@urql/core/dist/types/exchanges/ssr' 5 | import { isAuthError } from './shared' 6 | 7 | type SsrResult = GetServerSidePropsResult<{ urqlState?: SSRData }> 8 | type SsrQuery = DocumentNode | TypedDocumentNode | string 9 | type SsrContext = GetServerSidePropsContext 10 | 11 | export async function serverQuery< 12 | QueryResult = { [key: string]: unknown }, 13 | Variables = { [key: string]: unknown } 14 | >( 15 | query: SsrQuery, 16 | variables?: Variables, 17 | context?: SsrContext 18 | ): Promise { 19 | const ssrCache = ssrExchange({ isClient: false }) 20 | const cookie = context.req.headers.cookie 21 | const serverClient = createClient({ 22 | url: `http://${process.env.API_HOST}:3333/graphql`, 23 | fetchOptions: { headers: { cookie } }, 24 | exchanges: [ssrCache, fetchExchange] 25 | }) 26 | 27 | try { 28 | const { error } = await serverClient 29 | .query, Variables>(query, variables) 30 | .toPromise() 31 | 32 | if (!error) return { props: { urqlState: ssrCache.extractData() } } 33 | 34 | if (isAuthError(error)) { 35 | context.res.setHeader('set-cookie', ['token=']) 36 | context.res.setHeader('set-cookie', ['token-expires=']) 37 | return { redirect: { permanent: false, destination: '/login' } } 38 | } 39 | } catch (error) { 40 | console.log('server side query unexpected error', error) 41 | } 42 | 43 | return { redirect: { permanent: false, destination: '/error' } } 44 | } 45 | -------------------------------------------------------------------------------- /apps/api/src/app/guards/auth-guards/set-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CookieSerializeOptions } from '@fastify/cookie' 2 | import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common' 3 | import { AuthGuard } from '@nestjs/passport' 4 | import { JwtService } from '@nestjs/jwt' 5 | import { GqlExecutionContext } from '@nestjs/graphql' 6 | 7 | const domain = process.env.WEB_APP_HOST 8 | const jwtExpiresSecond = process.env.JWT_EXPIRES_SECONDS 9 | 10 | const HTTP_ONLY_COOKIE: CookieSerializeOptions = { 11 | maxAge: Number(jwtExpiresSecond), // cookie lives same amount of time as jwt 12 | httpOnly: true, 13 | signed: true, 14 | domain 15 | } 16 | 17 | const USERS_COOKIE: CookieSerializeOptions = { 18 | maxAge: Number(jwtExpiresSecond), // cookie lives same amount of time as jwt 19 | domain 20 | } 21 | 22 | @Injectable() 23 | export class SetAuthGuard extends AuthGuard('local') { 24 | constructor(private jwtService: JwtService) { 25 | super() 26 | } 27 | 28 | getRequest(context: ExecutionContext) { 29 | const context_ = GqlExecutionContext.create(context) 30 | const request = context_.getContext() 31 | // should be the same name as args 32 | request.body = context_.getArgs().loginInput 33 | return request 34 | } 35 | 36 | handleRequest(error, user, info, context) { 37 | if (error || !user || info) throw error || new UnauthorizedException() 38 | 39 | const authContext = GqlExecutionContext.create(context) 40 | const { reply } = authContext.getContext() 41 | 42 | const jwtExpiresMs = Number(jwtExpiresSecond) * 1000 43 | const tokenExpires = Date.now() + jwtExpiresMs 44 | const accessToken = this.jwtService.sign({ sub: user.id }) 45 | 46 | reply.setCookie('token', accessToken, HTTP_ONLY_COOKIE) 47 | reply.setCookie('token-expires', tokenExpires.toString(), USERS_COOKIE) 48 | 49 | return user 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full-Stack Example App 2 | ## Nx, NestJS(Fastify), GraphQL(Mercurius), Prisma, NestJS, 3 | 4 | ![alt text](https://github.com/hoqua/full-stack/blob/part-1-backend/tools/readme/schema.png?raw=true) 5 | 6 | ### How to run: 7 | - Install docker and nx globally 8 | - `npm i` 9 | - Create `.local.env` from `.env` 10 | - `npm start:dev` 11 | - Go to GQL Playground. Create User : 12 | ``` 13 | mutation { 14 | signUp(signUpInput:{ 15 | email: "youtube-test@test.com", 16 | password:"testtest" 17 | }){ 18 | id 19 | } 20 | } 21 | ``` 22 | 23 | ### Core parts and useful tips 24 | - [Nx](https://nx.dev/getting-started/intro) 25 | - Create workspace `npx create-nx-workspace full-stack —preset=nest` 26 | - Add nest app `nx g @nrwl/nest:app my-nest-app --tags "scope:api"` 27 | - [Nest](https://nestjs.com) 28 | - Add nest package `npm install --save-dev @nrwl/nest` 29 | - Useful packages `npm i @nestjs/platform-fastify @nestjs/graphql @nestjs/mercurius graphql mercurius` 30 | - Generate nest resource `nx g @nrwl/nest:resource -p api --directory="app/resources" --type="graphql-code-first" --crud --name {name}` 31 | - Generate nest:lib with api scope `nx g @nrwl/nest:lib api/data-access-db --buildable --tags "scope:api"` 32 | - **!** Validation pipes works together with gql default validation, skipMissingProperties essential. 33 | - [Prisma](https://www.prisma.io/docs/) 34 | - Add prisma field under package.json `"schema": "libs/api/data-access-db/src/lib/schema.prisma"` 35 | - Add docker compose.yml to spin up database 36 | - **!** Check out `prisma-nestjs-graphql` docs if types/validation support needed 37 | - **!** Create `.local.env` with needed variables 38 | - [NextJS](https://nextjs.org/docs/getting-started) 39 | - Add next package `npm install --save-dev @nrwl/next` 40 | - Create next app `nx g @nrwl/next:app web` 41 | - Turn on `swcMinify: true` to make build faster 42 | - [GraphQL Code Generator](https://www.graphql-code-generator.com/docs/getting-started) 43 | - Add needed packages `npm i -D graphql-codegen @graphql-codegen/cli @graphql-codegen/near-operation-file-preset @graphql-codegen/typed-document-node @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-urql` 44 | - Run generate if config in place `"gen:gql": "graphql-codegen --config tools/gql-codegen/gql-codegen.yml --watch"` 45 | -------------------------------------------------------------------------------- /libs/client/generated/graphql-types/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { [K in keyof T]: T[K] }; 4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 6 | /** All built-in and custom scalars, mapped to their actual values */ 7 | export type Scalars = { 8 | ID: string; 9 | String: string; 10 | Boolean: boolean; 11 | Int: number; 12 | Float: number; 13 | }; 14 | 15 | export type LoginInput = { 16 | email: Scalars['String']; 17 | password: Scalars['String']; 18 | }; 19 | 20 | export type Mutation = { 21 | __typename?: 'Mutation'; 22 | createUser: User; 23 | login: User; 24 | removeUser: User; 25 | signUp: User; 26 | updateUser: User; 27 | }; 28 | 29 | 30 | export type MutationCreateUserArgs = { 31 | data: UserCreateInput; 32 | }; 33 | 34 | 35 | export type MutationLoginArgs = { 36 | loginInput: LoginInput; 37 | }; 38 | 39 | 40 | export type MutationRemoveUserArgs = { 41 | where: UserWhereUniqueInput; 42 | }; 43 | 44 | 45 | export type MutationSignUpArgs = { 46 | signUpInput: LoginInput; 47 | }; 48 | 49 | 50 | export type MutationUpdateUserArgs = { 51 | data: UserUpdateInput; 52 | where: UserWhereUniqueInput; 53 | }; 54 | 55 | export type Query = { 56 | __typename?: 'Query'; 57 | user: User; 58 | users: Array; 59 | }; 60 | 61 | 62 | export type QueryUserArgs = { 63 | where: UserWhereUniqueInput; 64 | }; 65 | 66 | export type User = { 67 | __typename?: 'User'; 68 | email: Scalars['String']; 69 | id: Scalars['ID']; 70 | name?: Maybe; 71 | }; 72 | 73 | export type UserAvgAggregate = { 74 | __typename?: 'UserAvgAggregate'; 75 | id?: Maybe; 76 | }; 77 | 78 | export type UserCountAggregate = { 79 | __typename?: 'UserCountAggregate'; 80 | _all: Scalars['Int']; 81 | email: Scalars['Int']; 82 | id: Scalars['Int']; 83 | name: Scalars['Int']; 84 | }; 85 | 86 | export type UserCreateInput = { 87 | email: Scalars['String']; 88 | name?: InputMaybe; 89 | password: Scalars['String']; 90 | }; 91 | 92 | export type UserMaxAggregate = { 93 | __typename?: 'UserMaxAggregate'; 94 | email?: Maybe; 95 | id?: Maybe; 96 | name?: Maybe; 97 | }; 98 | 99 | export type UserMinAggregate = { 100 | __typename?: 'UserMinAggregate'; 101 | email?: Maybe; 102 | id?: Maybe; 103 | name?: Maybe; 104 | }; 105 | 106 | export type UserSumAggregate = { 107 | __typename?: 'UserSumAggregate'; 108 | id?: Maybe; 109 | }; 110 | 111 | export type UserUpdateInput = { 112 | email?: InputMaybe; 113 | name?: InputMaybe; 114 | password?: InputMaybe; 115 | }; 116 | 117 | export type UserWhereUniqueInput = { 118 | email?: InputMaybe; 119 | id?: InputMaybe; 120 | }; 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "full-stack", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start:dev": "env-cmd -f .local.env concurrently --kill-others \"npm:db:up\" \"npm:dev:api\" \"npm:dev:web\" \"npm:gen:gql\"", 7 | "dev:api": "nx serve api", 8 | "dev:web": "nx serve web", 9 | "DATABASE": "-----------------------------------------------------------------------------------------------", 10 | "db:up": "npm run db:docker && npm run db:dev-migrate && npm run db:create-client -- --watch", 11 | "db:docker": "docker-compose -f tools/docker-env/dev.docker-compose.yml up -d --no-recreate --remove-orphans", 12 | "db:create-client": "prisma generate", 13 | "db:dev-migrate": "env-cmd --no-override -f .local.env npx prisma migrate dev", 14 | "db:studio": "env-cmd --no-override -f .local.env npx prisma studio", 15 | "UTILS": "---------------------------------------------------------------------------------------------------", 16 | "gen:gql": "graphql-codegen --config tools/gql-codegen/gql-codegen.yml --watch" 17 | }, 18 | "prisma": { 19 | "schema": "libs/api/data-access-db/src/lib/schema.prisma" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@fastify/cookie": "^6.0.0", 24 | "@nestjs/common": "^8.0.0", 25 | "@nestjs/core": "^8.0.0", 26 | "@nestjs/graphql": "^10.0.13", 27 | "@nestjs/jwt": "^8.0.1", 28 | "@nestjs/mercurius": "^10.0.13", 29 | "@nestjs/passport": "^8.2.2", 30 | "@nestjs/platform-express": "^8.0.0", 31 | "@nestjs/platform-fastify": "^8.4.5", 32 | "@nrwl/next": "14.1.9", 33 | "@prisma/client": "^3.14.0", 34 | "bcrypt": "^5.0.1", 35 | "class-transformer": "^0.5.1", 36 | "class-validator": "^0.13.2", 37 | "core-js": "^3.6.5", 38 | "graphql": "^16.5.0", 39 | "helmet": "^5.1.0", 40 | "mercurius": "^9.6.0", 41 | "next": "12.1.5", 42 | "passport-jwt": "^4.0.0", 43 | "passport-local": "^1.0.0", 44 | "react": "18.1.0", 45 | "react-dom": "18.1.0", 46 | "reflect-metadata": "^0.1.13", 47 | "regenerator-runtime": "0.13.7", 48 | "rxjs": "^7.0.0", 49 | "tslib": "^2.3.0", 50 | "urql": "^2.2.1" 51 | }, 52 | "devDependencies": { 53 | "@graphql-codegen/cli": "^2.6.2", 54 | "@graphql-codegen/near-operation-file-preset": "^2.2.12", 55 | "@graphql-codegen/typed-document-node": "^2.2.11", 56 | "@graphql-codegen/typescript": "^2.4.11", 57 | "@graphql-codegen/typescript-operations": "^2.4.0", 58 | "@graphql-codegen/typescript-urql": "^3.5.10", 59 | "@nestjs/schematics": "^8.0.0", 60 | "@nestjs/testing": "^8.0.0", 61 | "@nrwl/cli": "14.1.9", 62 | "@nrwl/cypress": "14.1.9", 63 | "@nrwl/eslint-plugin-nx": "14.1.9", 64 | "@nrwl/jest": "14.1.9", 65 | "@nrwl/js": "14.1.9", 66 | "@nrwl/linter": "14.1.9", 67 | "@nrwl/nest": "14.1.9", 68 | "@nrwl/next": "^14.1.9", 69 | "@nrwl/node": "14.1.9", 70 | "@nrwl/react": "14.1.9", 71 | "@nrwl/web": "14.1.9", 72 | "@nrwl/workspace": "14.1.9", 73 | "@testing-library/react": "13.1.1", 74 | "@types/bcrypt": "^5.0.0", 75 | "@types/jest": "27.4.1", 76 | "@types/node": "16.11.7", 77 | "@types/passport-jwt": "^3.0.6", 78 | "@types/passport-local": "^1.0.34", 79 | "@types/react": "18.0.8", 80 | "@types/react-dom": "18.0.3", 81 | "@typescript-eslint/eslint-plugin": "~5.18.0", 82 | "@typescript-eslint/parser": "~5.18.0", 83 | "babel-jest": "27.5.1", 84 | "concurrently": "^7.2.1", 85 | "cypress": "^9.1.0", 86 | "env-cmd": "^10.1.0", 87 | "eslint": "~8.12.0", 88 | "eslint-config-next": "12.1.5", 89 | "eslint-config-prettier": "8.1.0", 90 | "eslint-plugin-cypress": "^2.10.3", 91 | "eslint-plugin-import": "2.26.0", 92 | "eslint-plugin-jsx-a11y": "6.5.1", 93 | "eslint-plugin-prettier": "^4.1.0", 94 | "eslint-plugin-react": "7.29.4", 95 | "eslint-plugin-react-hooks": "4.5.0", 96 | "eslint-plugin-unicorn": "^42.0.0", 97 | "graphql-codegen": "^0.4.0", 98 | "jest": "27.5.1", 99 | "nx": "14.1.9", 100 | "prettier": "^2.5.1", 101 | "prisma": "^3.14.0", 102 | "prisma-nestjs-graphql": "^16.0.1", 103 | "react-test-renderer": "18.1.0", 104 | "ts-jest": "27.1.4", 105 | "ts-node": "9.1.1", 106 | "typescript": "~4.6.2" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /apps/web/pages/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-text-size-adjust: 100%; 3 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 4 | Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, 5 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 6 | line-height: 1.5; 7 | tab-size: 4; 8 | scroll-behavior: smooth; 9 | } 10 | body { 11 | font-family: inherit; 12 | line-height: inherit; 13 | margin: 0; 14 | } 15 | h1, 16 | h2, 17 | p, 18 | pre { 19 | margin: 0; 20 | } 21 | *, 22 | ::before, 23 | ::after { 24 | box-sizing: border-box; 25 | border-width: 0; 26 | border-style: solid; 27 | border-color: currentColor; 28 | } 29 | h1, 30 | h2 { 31 | font-size: inherit; 32 | font-weight: inherit; 33 | } 34 | a { 35 | color: inherit; 36 | text-decoration: inherit; 37 | } 38 | pre { 39 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 40 | Liberation Mono, Courier New, monospace; 41 | } 42 | svg { 43 | display: block; 44 | vertical-align: middle; 45 | shape-rendering: auto; 46 | text-rendering: optimizeLegibility; 47 | } 48 | pre { 49 | background-color: rgba(55, 65, 81, 1); 50 | border-radius: 0.25rem; 51 | color: rgba(229, 231, 235, 1); 52 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 53 | Liberation Mono, Courier New, monospace; 54 | overflow: scroll; 55 | padding: 0.5rem 0.75rem; 56 | } 57 | 58 | .shadow { 59 | box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 60 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 61 | } 62 | .rounded { 63 | border-radius: 1.5rem; 64 | } 65 | .wrapper { 66 | width: 100%; 67 | } 68 | .container { 69 | margin-left: auto; 70 | margin-right: auto; 71 | max-width: 768px; 72 | padding-bottom: 3rem; 73 | padding-left: 1rem; 74 | padding-right: 1rem; 75 | color: rgba(55, 65, 81, 1); 76 | width: 100%; 77 | } 78 | #welcome { 79 | margin-top: 2.5rem; 80 | } 81 | #welcome h1 { 82 | font-size: 3rem; 83 | font-weight: 500; 84 | letter-spacing: -0.025em; 85 | line-height: 1; 86 | } 87 | #welcome span { 88 | display: block; 89 | font-size: 1.875rem; 90 | font-weight: 300; 91 | line-height: 2.25rem; 92 | margin-bottom: 0.5rem; 93 | } 94 | #hero { 95 | align-items: center; 96 | background-color: hsla(214, 62%, 21%, 1); 97 | border: none; 98 | box-sizing: border-box; 99 | color: rgba(55, 65, 81, 1); 100 | display: grid; 101 | grid-template-columns: 1fr; 102 | margin-top: 3.5rem; 103 | } 104 | #hero .text-container { 105 | color: rgba(255, 255, 255, 1); 106 | padding: 3rem 2rem; 107 | } 108 | #hero .text-container h2 { 109 | font-size: 1.5rem; 110 | line-height: 2rem; 111 | position: relative; 112 | } 113 | #hero .text-container h2 svg { 114 | color: hsla(162, 47%, 50%, 1); 115 | height: 2rem; 116 | left: -0.25rem; 117 | position: absolute; 118 | top: 0; 119 | width: 2rem; 120 | } 121 | #hero .text-container h2 span { 122 | margin-left: 2.5rem; 123 | } 124 | #hero .text-container a { 125 | background-color: rgba(255, 255, 255, 1); 126 | border-radius: 0.75rem; 127 | color: rgba(55, 65, 81, 1); 128 | display: inline-block; 129 | margin-top: 1.5rem; 130 | padding: 1rem 2rem; 131 | text-decoration: inherit; 132 | } 133 | #hero .logo-container { 134 | display: none; 135 | justify-content: center; 136 | padding-left: 2rem; 137 | padding-right: 2rem; 138 | } 139 | #hero .logo-container svg { 140 | color: rgba(255, 255, 255, 1); 141 | width: 66.666667%; 142 | } 143 | #middle-content { 144 | align-items: flex-start; 145 | display: grid; 146 | gap: 4rem; 147 | grid-template-columns: 1fr; 148 | margin-top: 3.5rem; 149 | } 150 | #learning-materials { 151 | padding: 2.5rem 2rem; 152 | } 153 | #learning-materials h2 { 154 | font-weight: 500; 155 | font-size: 1.25rem; 156 | letter-spacing: -0.025em; 157 | line-height: 1.75rem; 158 | padding-left: 1rem; 159 | padding-right: 1rem; 160 | } 161 | .list-item-link { 162 | align-items: center; 163 | border-radius: 0.75rem; 164 | display: flex; 165 | margin-top: 1rem; 166 | padding: 1rem; 167 | transition-property: background-color, border-color, color, fill, stroke, 168 | opacity, box-shadow, transform, filter, backdrop-filter, 169 | -webkit-backdrop-filter; 170 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 171 | transition-duration: 150ms; 172 | width: 100%; 173 | } 174 | .list-item-link svg:first-child { 175 | margin-right: 1rem; 176 | height: 1.5rem; 177 | transition-property: background-color, border-color, color, fill, stroke, 178 | opacity, box-shadow, transform, filter, backdrop-filter, 179 | -webkit-backdrop-filter; 180 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 181 | transition-duration: 150ms; 182 | width: 1.5rem; 183 | } 184 | .list-item-link > span { 185 | flex-grow: 1; 186 | font-weight: 400; 187 | transition-property: background-color, border-color, color, fill, stroke, 188 | opacity, box-shadow, transform, filter, backdrop-filter, 189 | -webkit-backdrop-filter; 190 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 191 | transition-duration: 150ms; 192 | } 193 | .list-item-link > span > span { 194 | color: rgba(107, 114, 128, 1); 195 | display: block; 196 | flex-grow: 1; 197 | font-size: 0.75rem; 198 | font-weight: 300; 199 | line-height: 1rem; 200 | transition-property: background-color, border-color, color, fill, stroke, 201 | opacity, box-shadow, transform, filter, backdrop-filter, 202 | -webkit-backdrop-filter; 203 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 204 | transition-duration: 150ms; 205 | } 206 | .list-item-link svg:last-child { 207 | height: 1rem; 208 | transition-property: all; 209 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 210 | transition-duration: 150ms; 211 | width: 1rem; 212 | } 213 | .list-item-link:hover { 214 | color: rgba(255, 255, 255, 1); 215 | background-color: hsla(162, 47%, 50%, 1); 216 | } 217 | .list-item-link:hover > span { 218 | } 219 | .list-item-link:hover > span > span { 220 | color: rgba(243, 244, 246, 1); 221 | } 222 | .list-item-link:hover svg:last-child { 223 | transform: translateX(0.25rem); 224 | } 225 | #other-links { 226 | } 227 | .button-pill { 228 | padding: 1.5rem 2rem; 229 | transition-duration: 300ms; 230 | transition-property: background-color, border-color, color, fill, stroke, 231 | opacity, box-shadow, transform, filter, backdrop-filter, 232 | -webkit-backdrop-filter; 233 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 234 | align-items: center; 235 | display: flex; 236 | } 237 | .button-pill svg { 238 | transition-property: background-color, border-color, color, fill, stroke, 239 | opacity, box-shadow, transform, filter, backdrop-filter, 240 | -webkit-backdrop-filter; 241 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 242 | transition-duration: 150ms; 243 | flex-shrink: 0; 244 | width: 3rem; 245 | } 246 | .button-pill > span { 247 | letter-spacing: -0.025em; 248 | font-weight: 400; 249 | font-size: 1.125rem; 250 | line-height: 1.75rem; 251 | padding-left: 1rem; 252 | padding-right: 1rem; 253 | } 254 | .button-pill span span { 255 | display: block; 256 | font-size: 0.875rem; 257 | font-weight: 300; 258 | line-height: 1.25rem; 259 | } 260 | .button-pill:hover svg, 261 | .button-pill:hover { 262 | color: rgba(255, 255, 255, 1) !important; 263 | } 264 | #nx-console:hover { 265 | background-color: rgba(0, 122, 204, 1); 266 | } 267 | #nx-console svg { 268 | color: rgba(0, 122, 204, 1); 269 | } 270 | #nx-repo:hover { 271 | background-color: rgba(24, 23, 23, 1); 272 | } 273 | #nx-repo svg { 274 | color: rgba(24, 23, 23, 1); 275 | } 276 | #nx-cloud { 277 | margin-bottom: 2rem; 278 | margin-top: 2rem; 279 | padding: 2.5rem 2rem; 280 | } 281 | #nx-cloud > div { 282 | align-items: center; 283 | display: flex; 284 | } 285 | #nx-cloud > div svg { 286 | border-radius: 0.375rem; 287 | flex-shrink: 0; 288 | width: 3rem; 289 | } 290 | #nx-cloud > div h2 { 291 | font-size: 1.125rem; 292 | font-weight: 400; 293 | letter-spacing: -0.025em; 294 | line-height: 1.75rem; 295 | padding-left: 1rem; 296 | padding-right: 1rem; 297 | } 298 | #nx-cloud > div h2 span { 299 | display: block; 300 | font-size: 0.875rem; 301 | font-weight: 300; 302 | line-height: 1.25rem; 303 | } 304 | #nx-cloud p { 305 | font-size: 1rem; 306 | line-height: 1.5rem; 307 | margin-top: 1rem; 308 | } 309 | #nx-cloud pre { 310 | margin-top: 1rem; 311 | } 312 | #nx-cloud a { 313 | color: rgba(107, 114, 128, 1); 314 | display: block; 315 | font-size: 0.875rem; 316 | line-height: 1.25rem; 317 | margin-top: 1.5rem; 318 | text-align: right; 319 | } 320 | #nx-cloud a:hover { 321 | text-decoration: underline; 322 | } 323 | #commands { 324 | padding: 2.5rem 2rem; 325 | margin-top: 3.5rem; 326 | } 327 | #commands h2 { 328 | font-size: 1.25rem; 329 | font-weight: 400; 330 | letter-spacing: -0.025em; 331 | line-height: 1.75rem; 332 | padding-left: 1rem; 333 | padding-right: 1rem; 334 | } 335 | #commands p { 336 | font-size: 1rem; 337 | font-weight: 300; 338 | line-height: 1.5rem; 339 | margin-top: 1rem; 340 | padding-left: 1rem; 341 | padding-right: 1rem; 342 | } 343 | details { 344 | align-items: center; 345 | display: flex; 346 | margin-top: 1rem; 347 | padding-left: 1rem; 348 | padding-right: 1rem; 349 | width: 100%; 350 | } 351 | details pre > span { 352 | color: rgba(181, 181, 181, 1); 353 | display: block; 354 | } 355 | summary { 356 | border-radius: 0.5rem; 357 | display: flex; 358 | font-weight: 400; 359 | padding: 0.5rem; 360 | cursor: pointer; 361 | transition-property: background-color, border-color, color, fill, stroke, 362 | opacity, box-shadow, transform, filter, backdrop-filter, 363 | -webkit-backdrop-filter; 364 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 365 | transition-duration: 150ms; 366 | } 367 | summary:hover { 368 | background-color: rgba(243, 244, 246, 1); 369 | } 370 | summary svg { 371 | height: 1.5rem; 372 | margin-right: 1rem; 373 | width: 1.5rem; 374 | } 375 | #love { 376 | color: rgba(107, 114, 128, 1); 377 | font-size: 0.875rem; 378 | line-height: 1.25rem; 379 | margin-top: 3.5rem; 380 | opacity: 0.6; 381 | text-align: center; 382 | } 383 | #love svg { 384 | color: rgba(252, 165, 165, 1); 385 | width: 1.25rem; 386 | height: 1.25rem; 387 | display: inline; 388 | margin-top: -0.25rem; 389 | } 390 | @media screen and (min-width: 768px) { 391 | #hero { 392 | grid-template-columns: repeat(2, minmax(0, 1fr)); 393 | } 394 | #hero .logo-container { 395 | display: flex; 396 | } 397 | #middle-content { 398 | grid-template-columns: repeat(2, minmax(0, 1fr)); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /apps/web/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './index.module.css' 2 | import { useGetUserQuery } from '../api/user/user.gql.gen' 3 | import { withApi } from '../api/client-api' 4 | 5 | export function Index() { 6 | const [{ data, fetching }] = useGetUserQuery({ 7 | variables: { args: { id: 1 } } 8 | }) 9 | 10 | return ( 11 |
12 |
13 |
14 |
15 |

16 | Hello {fetching ? 'there' : data?.user?.name}, 17 | Welcome to Full-Stack web 👋 18 |

19 |
20 | 21 |
22 |
23 |

24 | 30 | 36 | 37 | You're up and running 38 |

39 | What's next? 40 |
41 |
42 | 48 | 49 | 50 |
51 |
52 | 53 | 308 | 309 |
310 |

Next steps

311 |

Here are some things you can do with Nx:

312 |
313 | 314 | 320 | 326 | 327 | Add UI library 328 | 329 |
330 |                 # Generate UI lib
331 |                 nx g @nrwl/angular:lib ui
332 |                 # Add a component
333 |                 nx g @nrwl/angular:component button --project ui
334 |               
335 |
336 |
337 | 338 | 344 | 350 | 351 | View interactive project graph 352 | 353 |
nx graph
354 |
355 |
356 | 357 | 363 | 369 | 370 | Run affected commands 371 | 372 |
373 |                 # see what's been affected by changes
374 |                 nx affected:graph
375 |                 # run tests for current changes
376 |                 nx affected:test
377 |                 # run e2e tests for current changes
378 |                 nx affected:e2e
379 |               
380 |
381 |
382 | 383 |

384 | Carefully crafted with 385 | 391 | 397 | 398 |

399 |
400 |
401 |
402 | ) 403 | } 404 | 405 | export default withApi(Index) 406 | -------------------------------------------------------------------------------- /libs/api/generated/db-types/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Field } from '@nestjs/graphql' 2 | import { ObjectType } from '@nestjs/graphql' 3 | import { Int } from '@nestjs/graphql' 4 | import { InputType } from '@nestjs/graphql' 5 | import { Float } from '@nestjs/graphql' 6 | import { registerEnumType } from '@nestjs/graphql' 7 | import { ArgsType } from '@nestjs/graphql' 8 | import { Type } from 'class-transformer' 9 | import { ValidateNested } from 'class-validator' 10 | import { HideField } from '@nestjs/graphql' 11 | import * as Validator from 'class-validator' 12 | import { ID } from '@nestjs/graphql' 13 | 14 | export enum UserScalarFieldEnum { 15 | id = 'id', 16 | email = 'email', 17 | name = 'name', 18 | password = 'password' 19 | } 20 | 21 | export enum SortOrder { 22 | asc = 'asc', 23 | desc = 'desc' 24 | } 25 | 26 | export enum QueryMode { 27 | 'default' = 'default', 28 | insensitive = 'insensitive' 29 | } 30 | 31 | registerEnumType(QueryMode, { name: 'QueryMode', description: undefined }) 32 | registerEnumType(SortOrder, { name: 'SortOrder', description: undefined }) 33 | registerEnumType(UserScalarFieldEnum, { name: 'UserScalarFieldEnum', description: undefined }) 34 | 35 | @ObjectType() 36 | export class AffectedRows { 37 | @Field(() => Int, { nullable: false }) 38 | count!: number 39 | } 40 | 41 | @InputType() 42 | export class FloatFilter { 43 | @Field(() => Float, { nullable: true }) 44 | equals?: number; 45 | @Field(() => [Float], { nullable: true }) 46 | in?: Array 47 | @Field(() => [Float], { nullable: true }) 48 | notIn?: Array 49 | @Field(() => Float, { nullable: true }) 50 | lt?: number 51 | @Field(() => Float, { nullable: true }) 52 | lte?: number 53 | @Field(() => Float, { nullable: true }) 54 | gt?: number 55 | @Field(() => Float, { nullable: true }) 56 | gte?: number 57 | @Field(() => FloatFilter, { nullable: true }) 58 | not?: InstanceType 59 | } 60 | 61 | @InputType() 62 | export class IntFilter { 63 | @Field(() => Int, { nullable: true }) 64 | equals?: number; 65 | @Field(() => [Int], { nullable: true }) 66 | in?: Array 67 | @Field(() => [Int], { nullable: true }) 68 | notIn?: Array 69 | @Field(() => Int, { nullable: true }) 70 | lt?: number 71 | @Field(() => Int, { nullable: true }) 72 | lte?: number 73 | @Field(() => Int, { nullable: true }) 74 | gt?: number 75 | @Field(() => Int, { nullable: true }) 76 | gte?: number 77 | @Field(() => IntFilter, { nullable: true }) 78 | not?: InstanceType 79 | } 80 | 81 | @InputType() 82 | export class IntWithAggregatesFilter { 83 | @Field(() => Int, { nullable: true }) 84 | equals?: number; 85 | @Field(() => [Int], { nullable: true }) 86 | in?: Array 87 | @Field(() => [Int], { nullable: true }) 88 | notIn?: Array 89 | @Field(() => Int, { nullable: true }) 90 | lt?: number 91 | @Field(() => Int, { nullable: true }) 92 | lte?: number 93 | @Field(() => Int, { nullable: true }) 94 | gt?: number 95 | @Field(() => Int, { nullable: true }) 96 | gte?: number 97 | @Field(() => IntWithAggregatesFilter, { nullable: true }) 98 | not?: InstanceType 99 | @Field(() => IntFilter, { nullable: true }) 100 | _count?: InstanceType 101 | @Field(() => FloatFilter, { nullable: true }) 102 | _avg?: InstanceType 103 | @Field(() => IntFilter, { nullable: true }) 104 | _sum?: InstanceType 105 | @Field(() => IntFilter, { nullable: true }) 106 | _min?: InstanceType 107 | @Field(() => IntFilter, { nullable: true }) 108 | _max?: InstanceType 109 | } 110 | 111 | @InputType() 112 | export class StringFilter { 113 | @Field(() => String, { nullable: true }) 114 | equals?: string; 115 | @Field(() => [String], { nullable: true }) 116 | in?: Array 117 | @Field(() => [String], { nullable: true }) 118 | notIn?: Array 119 | @Field(() => String, { nullable: true }) 120 | lt?: string 121 | @Field(() => String, { nullable: true }) 122 | lte?: string 123 | @Field(() => String, { nullable: true }) 124 | gt?: string 125 | @Field(() => String, { nullable: true }) 126 | gte?: string 127 | @Field(() => String, { nullable: true }) 128 | contains?: string 129 | @Field(() => String, { nullable: true }) 130 | startsWith?: string 131 | @Field(() => String, { nullable: true }) 132 | endsWith?: string 133 | @Field(() => StringFilter, { nullable: true }) 134 | not?: InstanceType 135 | } 136 | 137 | @InputType() 138 | export class StringWithAggregatesFilter { 139 | @Field(() => String, { nullable: true }) 140 | equals?: string; 141 | @Field(() => [String], { nullable: true }) 142 | in?: Array 143 | @Field(() => [String], { nullable: true }) 144 | notIn?: Array 145 | @Field(() => String, { nullable: true }) 146 | lt?: string 147 | @Field(() => String, { nullable: true }) 148 | lte?: string 149 | @Field(() => String, { nullable: true }) 150 | gt?: string 151 | @Field(() => String, { nullable: true }) 152 | gte?: string 153 | @Field(() => String, { nullable: true }) 154 | contains?: string 155 | @Field(() => String, { nullable: true }) 156 | startsWith?: string 157 | @Field(() => String, { nullable: true }) 158 | endsWith?: string 159 | @Field(() => StringWithAggregatesFilter, { nullable: true }) 160 | not?: InstanceType 161 | @Field(() => IntFilter, { nullable: true }) 162 | _count?: InstanceType 163 | @Field(() => StringFilter, { nullable: true }) 164 | _min?: InstanceType 165 | @Field(() => StringFilter, { nullable: true }) 166 | _max?: InstanceType 167 | } 168 | 169 | @ObjectType() 170 | export class AggregateUser { 171 | @Field(() => UserCountAggregate, { nullable: true }) 172 | _count?: InstanceType 173 | @Field(() => UserAvgAggregate, { nullable: true }) 174 | _avg?: InstanceType 175 | @Field(() => UserSumAggregate, { nullable: true }) 176 | _sum?: InstanceType 177 | @Field(() => UserMinAggregate, { nullable: true }) 178 | _min?: InstanceType 179 | @Field(() => UserMaxAggregate, { nullable: true }) 180 | _max?: InstanceType 181 | } 182 | 183 | @ArgsType() 184 | export class CreateManyUserArgs { 185 | @Field(() => [UserCreateManyInput], { nullable: false }) 186 | @Type(() => UserCreateManyInput) 187 | @ValidateNested({ each: true }) 188 | data!: Array 189 | @Field(() => Boolean, { nullable: true }) 190 | skipDuplicates?: boolean 191 | } 192 | 193 | @ArgsType() 194 | export class CreateOneUserArgs { 195 | @Field(() => UserCreateInput, { nullable: false }) 196 | @Type(() => UserCreateInput) 197 | @ValidateNested({ each: true }) 198 | data!: InstanceType 199 | } 200 | 201 | @ArgsType() 202 | export class DeleteManyUserArgs { 203 | @Field(() => UserWhereInput, { nullable: true }) 204 | @Type(() => UserWhereInput) 205 | @ValidateNested({ each: true }) 206 | where?: InstanceType 207 | } 208 | 209 | @ArgsType() 210 | export class DeleteOneUserArgs { 211 | @Field(() => UserWhereUniqueInput, { nullable: false }) 212 | @Type(() => UserWhereUniqueInput) 213 | @ValidateNested({ each: true }) 214 | where!: InstanceType 215 | } 216 | 217 | @ArgsType() 218 | export class FindFirstUserArgs { 219 | @Field(() => UserWhereInput, { nullable: true }) 220 | @Type(() => UserWhereInput) 221 | @ValidateNested({ each: true }) 222 | where?: InstanceType 223 | @Field(() => [UserOrderByWithRelationInput], { nullable: true }) 224 | orderBy?: Array 225 | @Field(() => UserWhereUniqueInput, { nullable: true }) 226 | cursor?: InstanceType 227 | @Field(() => Int, { nullable: true }) 228 | take?: number 229 | @Field(() => Int, { nullable: true }) 230 | skip?: number 231 | @Field(() => [UserScalarFieldEnum], { nullable: true }) 232 | distinct?: Array 233 | } 234 | 235 | @ArgsType() 236 | export class FindManyUserArgs { 237 | @Field(() => UserWhereInput, { nullable: true }) 238 | @Type(() => UserWhereInput) 239 | @ValidateNested({ each: true }) 240 | where?: InstanceType 241 | @Field(() => [UserOrderByWithRelationInput], { nullable: true }) 242 | orderBy?: Array 243 | @Field(() => UserWhereUniqueInput, { nullable: true }) 244 | cursor?: InstanceType 245 | @Field(() => Int, { nullable: true }) 246 | take?: number 247 | @Field(() => Int, { nullable: true }) 248 | skip?: number 249 | @Field(() => [UserScalarFieldEnum], { nullable: true }) 250 | distinct?: Array 251 | } 252 | 253 | @ArgsType() 254 | export class FindUniqueUserArgs { 255 | @Field(() => UserWhereUniqueInput, { nullable: false }) 256 | @Type(() => UserWhereUniqueInput) 257 | @ValidateNested({ each: true }) 258 | where!: InstanceType 259 | } 260 | 261 | @ArgsType() 262 | export class UpdateManyUserArgs { 263 | @Field(() => UserUpdateManyMutationInput, { nullable: false }) 264 | @Type(() => UserUpdateManyMutationInput) 265 | @ValidateNested({ each: true }) 266 | data!: InstanceType 267 | @Field(() => UserWhereInput, { nullable: true }) 268 | @Type(() => UserWhereInput) 269 | @ValidateNested({ each: true }) 270 | where?: InstanceType 271 | } 272 | 273 | @ArgsType() 274 | export class UpdateOneUserArgs { 275 | @Field(() => UserUpdateInput, { nullable: false }) 276 | @Type(() => UserUpdateInput) 277 | @ValidateNested({ each: true }) 278 | data!: InstanceType 279 | @Field(() => UserWhereUniqueInput, { nullable: false }) 280 | @Type(() => UserWhereUniqueInput) 281 | @ValidateNested({ each: true }) 282 | where!: InstanceType 283 | } 284 | 285 | @ArgsType() 286 | export class UpsertOneUserArgs { 287 | @Field(() => UserWhereUniqueInput, { nullable: false }) 288 | @Type(() => UserWhereUniqueInput) 289 | @ValidateNested({ each: true }) 290 | where!: InstanceType 291 | @Field(() => UserCreateInput, { nullable: false }) 292 | @Type(() => UserCreateInput) 293 | create!: InstanceType 294 | @Field(() => UserUpdateInput, { nullable: false }) 295 | @Type(() => UserUpdateInput) 296 | update!: InstanceType 297 | } 298 | 299 | @ArgsType() 300 | export class UserAggregateArgs { 301 | @Field(() => UserWhereInput, { nullable: true }) 302 | @Type(() => UserWhereInput) 303 | @ValidateNested({ each: true }) 304 | where?: InstanceType 305 | @Field(() => [UserOrderByWithRelationInput], { nullable: true }) 306 | orderBy?: Array 307 | @Field(() => UserWhereUniqueInput, { nullable: true }) 308 | cursor?: InstanceType 309 | @Field(() => Int, { nullable: true }) 310 | take?: number 311 | @Field(() => Int, { nullable: true }) 312 | skip?: number 313 | @Field(() => UserCountAggregateInput, { nullable: true }) 314 | _count?: InstanceType 315 | @Field(() => UserAvgAggregateInput, { nullable: true }) 316 | _avg?: InstanceType 317 | @Field(() => UserSumAggregateInput, { nullable: true }) 318 | _sum?: InstanceType 319 | @Field(() => UserMinAggregateInput, { nullable: true }) 320 | _min?: InstanceType 321 | @Field(() => UserMaxAggregateInput, { nullable: true }) 322 | _max?: InstanceType 323 | } 324 | 325 | @InputType() 326 | export class UserAvgAggregateInput { 327 | @Field(() => Boolean, { nullable: true }) 328 | id?: true 329 | } 330 | 331 | @ObjectType() 332 | export class UserAvgAggregate { 333 | @Field(() => Float, { nullable: true }) 334 | id?: number 335 | } 336 | 337 | @InputType() 338 | export class UserAvgOrderByAggregateInput { 339 | @Field(() => SortOrder, { nullable: true }) 340 | id?: keyof typeof SortOrder 341 | } 342 | 343 | @InputType() 344 | export class UserCountAggregateInput { 345 | @Field(() => Boolean, { nullable: true }) 346 | id?: true 347 | @Field(() => Boolean, { nullable: true }) 348 | email?: true 349 | @Field(() => Boolean, { nullable: true }) 350 | name?: true 351 | @Field(() => Boolean, { nullable: true }) 352 | password?: true 353 | @Field(() => Boolean, { nullable: true }) 354 | _all?: true 355 | } 356 | 357 | @ObjectType() 358 | export class UserCountAggregate { 359 | @Field(() => Int, { nullable: false }) 360 | id!: number 361 | @Field(() => Int, { nullable: false }) 362 | email!: number 363 | @Field(() => Int, { nullable: false }) 364 | name!: number 365 | @HideField() 366 | password!: number 367 | @Field(() => Int, { nullable: false }) 368 | _all!: number 369 | } 370 | 371 | @InputType() 372 | export class UserCountOrderByAggregateInput { 373 | @Field(() => SortOrder, { nullable: true }) 374 | id?: keyof typeof SortOrder 375 | @Field(() => SortOrder, { nullable: true }) 376 | email?: keyof typeof SortOrder 377 | @Field(() => SortOrder, { nullable: true }) 378 | name?: keyof typeof SortOrder 379 | @Field(() => SortOrder, { nullable: true }) 380 | password?: keyof typeof SortOrder 381 | } 382 | 383 | @InputType() 384 | export class UserCreateManyInput { 385 | @Field(() => Int, { nullable: true }) 386 | id?: number 387 | @Field(() => String, { nullable: false }) 388 | @Validator.IsEmail() 389 | email!: string 390 | @Field(() => String, { nullable: true }) 391 | @Validator.IsString() 392 | @Validator.MaxLength(100) 393 | @Validator.MinLength(3) 394 | name?: string 395 | @Field(() => String, { nullable: false }) 396 | @Validator.IsString() 397 | @Validator.MaxLength(100) 398 | @Validator.MinLength(8) 399 | password!: string 400 | } 401 | 402 | @InputType() 403 | export class UserCreateInput { 404 | @Field(() => String, { nullable: false }) 405 | @Validator.IsEmail() 406 | email!: string 407 | @Field(() => String, { nullable: true }) 408 | @Validator.IsString() 409 | @Validator.MaxLength(100) 410 | @Validator.MinLength(3) 411 | name?: string 412 | @Field(() => String, { nullable: false }) 413 | @Validator.IsString() 414 | @Validator.MaxLength(100) 415 | @Validator.MinLength(8) 416 | password!: string 417 | } 418 | 419 | @ArgsType() 420 | export class UserGroupByArgs { 421 | @Field(() => UserWhereInput, { nullable: true }) 422 | @Type(() => UserWhereInput) 423 | @ValidateNested({ each: true }) 424 | where?: InstanceType 425 | @Field(() => [UserOrderByWithAggregationInput], { nullable: true }) 426 | orderBy?: Array 427 | @Field(() => [UserScalarFieldEnum], { nullable: false }) 428 | by!: Array 429 | @Field(() => UserScalarWhereWithAggregatesInput, { nullable: true }) 430 | having?: InstanceType 431 | @Field(() => Int, { nullable: true }) 432 | take?: number 433 | @Field(() => Int, { nullable: true }) 434 | skip?: number 435 | @Field(() => UserCountAggregateInput, { nullable: true }) 436 | _count?: InstanceType 437 | @Field(() => UserAvgAggregateInput, { nullable: true }) 438 | _avg?: InstanceType 439 | @Field(() => UserSumAggregateInput, { nullable: true }) 440 | _sum?: InstanceType 441 | @Field(() => UserMinAggregateInput, { nullable: true }) 442 | _min?: InstanceType 443 | @Field(() => UserMaxAggregateInput, { nullable: true }) 444 | _max?: InstanceType 445 | } 446 | 447 | @ObjectType() 448 | export class UserGroupBy { 449 | @Field(() => Int, { nullable: false }) 450 | id!: number 451 | @Field(() => String, { nullable: false }) 452 | @Validator.IsEmail() 453 | email!: string 454 | @Field(() => String, { nullable: true }) 455 | @Validator.IsString() 456 | @Validator.MaxLength(100) 457 | @Validator.MinLength(3) 458 | name?: string 459 | @HideField() 460 | password!: string 461 | @Field(() => UserCountAggregate, { nullable: true }) 462 | _count?: InstanceType 463 | @Field(() => UserAvgAggregate, { nullable: true }) 464 | _avg?: InstanceType 465 | @Field(() => UserSumAggregate, { nullable: true }) 466 | _sum?: InstanceType 467 | @Field(() => UserMinAggregate, { nullable: true }) 468 | _min?: InstanceType 469 | @Field(() => UserMaxAggregate, { nullable: true }) 470 | _max?: InstanceType 471 | } 472 | 473 | @InputType() 474 | export class UserMaxAggregateInput { 475 | @Field(() => Boolean, { nullable: true }) 476 | id?: true 477 | @Field(() => Boolean, { nullable: true }) 478 | email?: true 479 | @Field(() => Boolean, { nullable: true }) 480 | name?: true 481 | @Field(() => Boolean, { nullable: true }) 482 | password?: true 483 | } 484 | 485 | @ObjectType() 486 | export class UserMaxAggregate { 487 | @Field(() => Int, { nullable: true }) 488 | id?: number 489 | @Field(() => String, { nullable: true }) 490 | @Validator.IsEmail() 491 | email?: string 492 | @Field(() => String, { nullable: true }) 493 | @Validator.IsString() 494 | @Validator.MaxLength(100) 495 | @Validator.MinLength(3) 496 | name?: string 497 | @HideField() 498 | password?: string 499 | } 500 | 501 | @InputType() 502 | export class UserMaxOrderByAggregateInput { 503 | @Field(() => SortOrder, { nullable: true }) 504 | id?: keyof typeof SortOrder 505 | @Field(() => SortOrder, { nullable: true }) 506 | email?: keyof typeof SortOrder 507 | @Field(() => SortOrder, { nullable: true }) 508 | name?: keyof typeof SortOrder 509 | @Field(() => SortOrder, { nullable: true }) 510 | password?: keyof typeof SortOrder 511 | } 512 | 513 | @InputType() 514 | export class UserMinAggregateInput { 515 | @Field(() => Boolean, { nullable: true }) 516 | id?: true 517 | @Field(() => Boolean, { nullable: true }) 518 | email?: true 519 | @Field(() => Boolean, { nullable: true }) 520 | name?: true 521 | @Field(() => Boolean, { nullable: true }) 522 | password?: true 523 | } 524 | 525 | @ObjectType() 526 | export class UserMinAggregate { 527 | @Field(() => Int, { nullable: true }) 528 | id?: number 529 | @Field(() => String, { nullable: true }) 530 | @Validator.IsEmail() 531 | email?: string 532 | @Field(() => String, { nullable: true }) 533 | @Validator.IsString() 534 | @Validator.MaxLength(100) 535 | @Validator.MinLength(3) 536 | name?: string 537 | @HideField() 538 | password?: string 539 | } 540 | 541 | @InputType() 542 | export class UserMinOrderByAggregateInput { 543 | @Field(() => SortOrder, { nullable: true }) 544 | id?: keyof typeof SortOrder 545 | @Field(() => SortOrder, { nullable: true }) 546 | email?: keyof typeof SortOrder 547 | @Field(() => SortOrder, { nullable: true }) 548 | name?: keyof typeof SortOrder 549 | @Field(() => SortOrder, { nullable: true }) 550 | password?: keyof typeof SortOrder 551 | } 552 | 553 | @InputType() 554 | export class UserOrderByWithAggregationInput { 555 | @Field(() => SortOrder, { nullable: true }) 556 | id?: keyof typeof SortOrder 557 | @Field(() => SortOrder, { nullable: true }) 558 | email?: keyof typeof SortOrder 559 | @Field(() => SortOrder, { nullable: true }) 560 | name?: keyof typeof SortOrder 561 | @Field(() => SortOrder, { nullable: true }) 562 | password?: keyof typeof SortOrder 563 | @Field(() => UserCountOrderByAggregateInput, { nullable: true }) 564 | _count?: InstanceType 565 | @Field(() => UserAvgOrderByAggregateInput, { nullable: true }) 566 | _avg?: InstanceType 567 | @Field(() => UserMaxOrderByAggregateInput, { nullable: true }) 568 | _max?: InstanceType 569 | @Field(() => UserMinOrderByAggregateInput, { nullable: true }) 570 | _min?: InstanceType 571 | @Field(() => UserSumOrderByAggregateInput, { nullable: true }) 572 | _sum?: InstanceType 573 | } 574 | 575 | @InputType() 576 | export class UserOrderByWithRelationInput { 577 | @Field(() => SortOrder, { nullable: true }) 578 | id?: keyof typeof SortOrder 579 | @Field(() => SortOrder, { nullable: true }) 580 | email?: keyof typeof SortOrder 581 | @Field(() => SortOrder, { nullable: true }) 582 | name?: keyof typeof SortOrder 583 | @Field(() => SortOrder, { nullable: true }) 584 | password?: keyof typeof SortOrder 585 | } 586 | 587 | @InputType() 588 | export class UserScalarWhereWithAggregatesInput { 589 | @Field(() => [UserScalarWhereWithAggregatesInput], { nullable: true }) 590 | AND?: Array 591 | @Field(() => [UserScalarWhereWithAggregatesInput], { nullable: true }) 592 | OR?: Array 593 | @Field(() => [UserScalarWhereWithAggregatesInput], { nullable: true }) 594 | NOT?: Array 595 | @Field(() => IntWithAggregatesFilter, { nullable: true }) 596 | id?: InstanceType 597 | @Field(() => StringWithAggregatesFilter, { nullable: true }) 598 | email?: InstanceType 599 | @Field(() => StringWithAggregatesFilter, { nullable: true }) 600 | name?: InstanceType 601 | @Field(() => StringWithAggregatesFilter, { nullable: true }) 602 | password?: InstanceType 603 | } 604 | 605 | @InputType() 606 | export class UserSumAggregateInput { 607 | @Field(() => Boolean, { nullable: true }) 608 | id?: true 609 | } 610 | 611 | @ObjectType() 612 | export class UserSumAggregate { 613 | @Field(() => Int, { nullable: true }) 614 | id?: number 615 | } 616 | 617 | @InputType() 618 | export class UserSumOrderByAggregateInput { 619 | @Field(() => SortOrder, { nullable: true }) 620 | id?: keyof typeof SortOrder 621 | } 622 | 623 | @InputType() 624 | export class UserUncheckedCreateInput { 625 | @Field(() => Int, { nullable: true }) 626 | id?: number 627 | @Field(() => String, { nullable: false }) 628 | @Validator.IsEmail() 629 | email!: string 630 | @Field(() => String, { nullable: true }) 631 | @Validator.IsString() 632 | @Validator.MaxLength(100) 633 | @Validator.MinLength(3) 634 | name?: string 635 | @Field(() => String, { nullable: false }) 636 | @Validator.IsString() 637 | @Validator.MaxLength(100) 638 | @Validator.MinLength(8) 639 | password!: string 640 | } 641 | 642 | @InputType() 643 | export class UserUncheckedUpdateManyInput { 644 | @Field(() => Int, { nullable: true }) 645 | id?: number 646 | @Field(() => String, { nullable: true }) 647 | @Validator.IsEmail() 648 | email?: string 649 | @Field(() => String, { nullable: true }) 650 | @Validator.IsString() 651 | @Validator.MaxLength(100) 652 | @Validator.MinLength(3) 653 | name?: string 654 | @Field(() => String, { nullable: true }) 655 | @Validator.IsString() 656 | @Validator.MaxLength(100) 657 | @Validator.MinLength(8) 658 | password?: string 659 | } 660 | 661 | @InputType() 662 | export class UserUncheckedUpdateInput { 663 | @Field(() => Int, { nullable: true }) 664 | id?: number 665 | @Field(() => String, { nullable: true }) 666 | @Validator.IsEmail() 667 | email?: string 668 | @Field(() => String, { nullable: true }) 669 | @Validator.IsString() 670 | @Validator.MaxLength(100) 671 | @Validator.MinLength(3) 672 | name?: string 673 | @Field(() => String, { nullable: true }) 674 | @Validator.IsString() 675 | @Validator.MaxLength(100) 676 | @Validator.MinLength(8) 677 | password?: string 678 | } 679 | 680 | @InputType() 681 | export class UserUpdateManyMutationInput { 682 | @Field(() => String, { nullable: true }) 683 | @Validator.IsEmail() 684 | email?: string 685 | @Field(() => String, { nullable: true }) 686 | @Validator.IsString() 687 | @Validator.MaxLength(100) 688 | @Validator.MinLength(3) 689 | name?: string 690 | @Field(() => String, { nullable: true }) 691 | @Validator.IsString() 692 | @Validator.MaxLength(100) 693 | @Validator.MinLength(8) 694 | password?: string 695 | } 696 | 697 | @InputType() 698 | export class UserUpdateInput { 699 | @Field(() => String, { nullable: true }) 700 | @Validator.IsEmail() 701 | email?: string 702 | @Field(() => String, { nullable: true }) 703 | @Validator.IsString() 704 | @Validator.MaxLength(100) 705 | @Validator.MinLength(3) 706 | name?: string 707 | @Field(() => String, { nullable: true }) 708 | @Validator.IsString() 709 | @Validator.MaxLength(100) 710 | @Validator.MinLength(8) 711 | password?: string 712 | } 713 | 714 | @InputType() 715 | export class UserWhereUniqueInput { 716 | @Field(() => Int, { nullable: true }) 717 | id?: number 718 | @Field(() => String, { nullable: true }) 719 | @Validator.IsEmail() 720 | email?: string 721 | } 722 | 723 | @InputType() 724 | export class UserWhereInput { 725 | @Field(() => [UserWhereInput], { nullable: true }) 726 | AND?: Array 727 | @Field(() => [UserWhereInput], { nullable: true }) 728 | OR?: Array 729 | @Field(() => [UserWhereInput], { nullable: true }) 730 | NOT?: Array 731 | @Field(() => IntFilter, { nullable: true }) 732 | id?: InstanceType 733 | @Field(() => StringFilter, { nullable: true }) 734 | email?: InstanceType 735 | @Field(() => StringFilter, { nullable: true }) 736 | name?: InstanceType 737 | @Field(() => StringFilter, { nullable: true }) 738 | password?: InstanceType 739 | } 740 | 741 | @ObjectType() 742 | export class User { 743 | @Field(() => ID, { nullable: false }) 744 | id!: number 745 | @Field(() => String, { nullable: false }) 746 | email!: string 747 | @Field(() => String, { nullable: true }) 748 | name!: string | null 749 | @HideField() 750 | password!: string 751 | } 752 | --------------------------------------------------------------------------------